Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/main/java/com/debatetimer/domain/sharing/ActiveSharing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.debatetimer.domain.sharing;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
import java.time.LocalDateTime;
import lombok.Getter;

/**
* 기둝 쀑인 라이브 곡유 ν•œ 번의 μ§„ν–‰ μƒνƒœ
* - μ‚¬νšŒμž 연결이 끊긴 λ’€ μž¬μ—°κ²° 유예 μ‹œκ°„μ΄ μ§€λ‚˜κ±°λ‚˜, κ³΅μœ κ°€ λ„ˆλ¬΄ 였래 이어지면 λ§Œλ£Œλœλ‹€.
* - λ§ˆμ§€λ§‰ νƒ€μž„λ°•μŠ€κΉŒμ§€ μ§„ν–‰ν•œ κ³΅μœ λŠ” μ‚¬νšŒμžκ°€ 직접 μ’…λ£Œν•˜μ§€ μ•Šμ•„λ„ λκΉŒμ§€ μ§„ν–‰ν•œ κ²ƒμœΌλ‘œ λ³Έλ‹€.
* - 연결이 끊긴 채 만료되면 끊긴 μ‹œκ°μ„ μ’…λ£Œ μ‹œκ°μœΌλ‘œ λ³Έλ‹€.
*/
public class ActiveSharing {

public static final Duration RECONNECT_GRACE = Duration.ofMinutes(1);
public static final Duration STALE_THRESHOLD = Duration.ofHours(12);

@Getter
private final long logId;

@Getter
@NotNull
private final String memberEmail;

@Getter
@NotNull
private final String tableName;

private final int timeBoxCount;

@NotNull
private final LocalDateTime startedAt;

@Nullable
private final Integer lastSequence;

@Nullable
private final LocalDateTime disconnectedAt;

public ActiveSharing(
long logId,
String memberEmail,
String tableName,
int timeBoxCount,
LocalDateTime startedAt
) {
this(logId, memberEmail, tableName, timeBoxCount, startedAt, null, null);
}

private ActiveSharing(
long logId,
String memberEmail,
String tableName,
int timeBoxCount,
LocalDateTime startedAt,
@Nullable Integer lastSequence,
@Nullable LocalDateTime disconnectedAt
) {
this.logId = logId;
this.memberEmail = memberEmail;
this.tableName = tableName;
this.timeBoxCount = timeBoxCount;
this.startedAt = startedAt;
this.lastSequence = lastSequence;
this.disconnectedAt = disconnectedAt;
}

/**
* 끊긴 κ³΅μœ κ°€ λ§ˆμ§€λ§‰ νƒ€μž„λ°•μŠ€κΉŒμ§€ μ§„ν–‰λλŠ”μ§€ νŒλ‹¨ν•  수 μžˆλ„λ‘, 청쀑 화면에 ν‘œμ‹œλ  νƒ€μž„λ°•μŠ€ μˆœμ„œλ₯Ό κΈ°λ‘ν•œλ‹€.
*/
public ActiveSharing record(TimerEvent timerEvent) {
TimerEventData data = timerEvent.getTimerEventData();
if (data == null) {
return this;
}
int displayedSequence = timerEvent.getEventType().toDisplayedSequence(data.getSequence());
return new ActiveSharing(logId, memberEmail, tableName, timeBoxCount, startedAt, displayedSequence,
disconnectedAt);
}

public ActiveSharing disconnect(LocalDateTime at) {
if (disconnectedAt != null) {
return this;
}
return new ActiveSharing(logId, memberEmail, tableName, timeBoxCount, startedAt, lastSequence, at);
}

public ActiveSharing reconnect() {
return new ActiveSharing(logId, memberEmail, tableName, timeBoxCount, startedAt, lastSequence, null);
}

public boolean isReconnectExpired(LocalDateTime now) {
return disconnectedAt != null && !now.isBefore(disconnectedAt.plus(RECONNECT_GRACE));
}

public boolean isExpired(LocalDateTime now) {
return isReconnectExpired(now) || !now.isBefore(startedAt.plus(STALE_THRESHOLD));
}

// νƒ€μž„λ°•μŠ€ μˆœμ„œλŠ” 0λΆ€ν„° μ‹œμž‘ν•œλ‹€
public boolean isCompleted() {
return lastSequence != null && lastSequence >= timeBoxCount - 1;
}

public LocalDateTime endedAt(LocalDateTime now) {
if (disconnectedAt == null) {
return now;
}
return disconnectedAt;
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/debatetimer/domain/sharing/SharingLog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.debatetimer.domain.sharing;

import jakarta.annotation.Nullable;
import java.time.Duration;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
* 라이브 곡유 ν•œ 번(μ‚¬νšŒμžμ˜ 곡유 μ‹œμž‘λΆ€ν„° μ’…λ£ŒκΉŒμ§€)의 기둝
*/
@Getter
@RequiredArgsConstructor
public class SharingLog {

private final Long id;
private final long tableId;
private final long memberId;
private final LocalDateTime startedAt;

@Nullable
private final LocalDateTime endedAt;

private final int audienceCount;
private final SharingLogStatus status;

public SharingLog(long tableId, long memberId, LocalDateTime startedAt) {
this(null, tableId, memberId, startedAt, null, 0, SharingLogStatus.SHARING);
}

public long getDurationSeconds() {
if (endedAt == null) {
return 0;
}
return Math.max(Duration.between(startedAt, endedAt).toSeconds(), 0);
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/debatetimer/domain/sharing/TimerEventType.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,18 @@ public void validateEventData(Object eventData) {
public boolean isSync() {
return this == SYNC;
}

/**
* μ΄λ²€νŠΈκ°€ 반영된 λ’€ 청쀑 화면에 ν‘œμ‹œλ  νƒ€μž„λ°•μŠ€ μˆœμ„œλ₯Ό κ΅¬ν•œλ‹€.
* μ΄μ „Β·λ‹€μŒ μ΄λ²€νŠΈλŠ” μ΄λ™ν•˜κΈ° μ „ μˆœμ„œλ₯Ό λ‹΄κ³  μžˆμ–΄, μ΄λ™ν•œ λ’€ μˆœμ„œλ‘œ λ°”κΎΌλ‹€.
*/
public int toDisplayedSequence(int sequence) {
if (this == NEXT) {
return sequence + 1;
}
if (this == BEFORE) {
return Math.max(sequence - 1, 0);
}
return sequence;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public CustomizeTable getById(long tableId) {
.toDomain();
}

@Transactional(readOnly = true)
public CustomizeTable getWithMemberById(long tableId) {
return tableRepository.getWithMemberById(tableId)
.toDomain();
}

@Transactional(readOnly = true)
public List<CustomizeTimeBox> getMemberCustomizeTimeBoxes(long tableId, Member member) {
CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.debatetimer.domainrepository.sharing;

import com.debatetimer.domain.sharing.SharingLog;
import com.debatetimer.domain.sharing.SharingLogStatus;
import com.debatetimer.entity.sharing.SharingLogEntity;
import com.debatetimer.repository.sharing.SharingLogRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@RequiredArgsConstructor
public class SharingLogDomainRepository {

private final SharingLogRepository sharingLogRepository;

@Transactional
public SharingLog create(SharingLog sharingLog) {
return sharingLogRepository.save(new SharingLogEntity(sharingLog))
.toDomain();
}

@Transactional(readOnly = true)
public SharingLog getById(long id) {
return sharingLogRepository.getById(id)
.toDomain();
}

@Transactional(readOnly = true)
public List<SharingLog> findAllFinishedStartedBetween(LocalDateTime from, LocalDateTime to) {
return sharingLogRepository.findAllByStatusAndStartedAtGreaterThanEqualAndStartedAtLessThan(
SharingLogStatus.FINISHED, from, to)
.stream()
.map(SharingLogEntity::toDomain)
.toList();
}

@Transactional
public void increaseAudienceCount(long id) {
sharingLogRepository.increaseAudienceCount(id);
}

/**
* μ§„ν–‰ 쀑인 기둝을 μ’…λ£Œν•œλ‹€. λ‹€λ₯Έ κ²½λ‘œμ—μ„œ 이미 λλ‚œ 기둝이면 λ°”κΎΈμ§€ μ•Šκ³  빈 값을 λ°˜ν™˜ν•œλ‹€.
*/
@Transactional
public Optional<SharingLog> finish(long id, LocalDateTime endedAt) {
int updated = sharingLogRepository.updateStatusIfSharing(
id, SharingLogStatus.FINISHED, endedAt, SharingLogStatus.SHARING);
if (updated == 0) {
return Optional.empty();
}
return Optional.of(getById(id));
}

@Transactional
public void abandon(long id, LocalDateTime endedAt) {
sharingLogRepository.updateStatusIfSharing(id, SharingLogStatus.ABANDONED, endedAt, SharingLogStatus.SHARING);
}

@Transactional
public void abandonStartedBefore(LocalDateTime threshold) {
sharingLogRepository.abandonStaleSharings(SharingLogStatus.ABANDONED, SharingLogStatus.SHARING, threshold);
}
}
33 changes: 11 additions & 22 deletions src/main/java/com/debatetimer/entity/sharing/SharingLogEntity.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.debatetimer.entity.sharing;

import com.debatetimer.domain.sharing.SharingLog;
import com.debatetimer.domain.sharing.SharingLogStatus;
import com.debatetimer.entity.BaseTimeEntity;
import jakarta.annotation.Nullable;
Expand All @@ -12,7 +13,6 @@
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.time.Duration;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
Expand Down Expand Up @@ -49,28 +49,17 @@ public class SharingLogEntity extends BaseTimeEntity {
@Enumerated(EnumType.STRING)
private SharingLogStatus status;

public SharingLogEntity(long tableId, long memberId, LocalDateTime startedAt) {
this.tableId = tableId;
this.memberId = memberId;
this.startedAt = startedAt;
this.audienceCount = 0;
this.status = SharingLogStatus.SHARING;
public SharingLogEntity(SharingLog sharingLog) {
this.id = sharingLog.getId();
this.tableId = sharingLog.getTableId();
this.memberId = sharingLog.getMemberId();
this.startedAt = sharingLog.getStartedAt();
this.endedAt = sharingLog.getEndedAt();
this.audienceCount = sharingLog.getAudienceCount();
this.status = sharingLog.getStatus();
}

public void finish(LocalDateTime endedAt) {
this.status = SharingLogStatus.FINISHED;
this.endedAt = endedAt;
}

public void abandon(LocalDateTime endedAt) {
this.status = SharingLogStatus.ABANDONED;
this.endedAt = endedAt;
}

public long getDurationSeconds() {
if (endedAt == null) {
return 0;
}
return Math.max(Duration.between(startedAt, endedAt).toSeconds(), 0);
public SharingLog toDomain() {
return new SharingLog(id, tableId, memberId, startedAt, endedAt, audienceCount, status);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import java.time.LocalDateTime;

/**
* μ‚¬νšŒμžκ°€ 곡유 μ’…λ£Œ(FINISHED)λ₯Ό λͺ…μ‹œμ μœΌλ‘œ λ°œν–‰ν•΄ 곡유 기둝이 μ’…λ£Œλ˜μ—ˆμŒμ„ μ•Œλ¦°λ‹€.
* 곡유 기둝이 μ’…λ£Œλ˜μ—ˆμŒμ„ μ•Œλ¦°λ‹€. μ‚¬νšŒμžκ°€ 직접 μ’…λ£Œν–ˆκ±°λ‚˜, λ§ˆμ§€λ§‰ νƒ€μž„λ°•μŠ€μ—μ„œ 끊긴 λ’€ λŒμ•„μ˜€μ§€ μ•Šμ€ κ²½μš°μ΄λ‹€.
*/
public record SharingFinishedEvent(
long sharingLogId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

/**
* 곡유 μ’…λ£Œ 기둝이 μ €μž₯된 뒀에 곡유 톡계λ₯Ό μ•Œλ¦°λ‹€.
* μ’…λ£Œ μ΄λ²€νŠΈλŠ” 기둝을 μ €μž₯ν•œ νŠΈλžœμž­μ…˜μ΄ λλ‚œ λ’€ λ°œν–‰λ˜λ―€λ‘œ, νŠΈλžœμž­μ…˜ λ°–μ—μ„œ λ°œν–‰λΌλ„ μ•Œλ¦°λ‹€.
* μ•Œλ¦Όμ€ 곡유 쀑계와 λ¬΄κ΄€ν•˜λ―€λ‘œ 별도 μŠ€λ ˆλ“œμ—μ„œ 보내고, μ‹€νŒ¨ν•΄λ„ 기둝만 남긴닀.
*/
@Slf4j
Expand All @@ -20,7 +21,7 @@ public class SharingFinishedListener {
private final SharingNotifier sharingNotifier;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void handleSharingFinished(SharingFinishedEvent event) {
try {
sharingNotifier.sendSharingFinished(event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public enum ClientErrorCode implements ResponseErrorCode {
TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ν† λ‘  ν…Œμ΄λΈ”μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "ν…Œμ΄λΈ”μ„ μ†Œμœ ν•œ νšŒμ›μ΄ μ•„λ‹™λ‹ˆλ‹€."),
POLL_NOT_FOUND(HttpStatus.NOT_FOUND, "νˆ¬ν‘œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
SHARING_LOG_NOT_FOUND(HttpStatus.NOT_FOUND, "곡유 기둝을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
UNAUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "μ ‘κ·Ό κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€"),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰 κΈ°ν•œμ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.debatetimer.exception.errorcode.ClientErrorCode;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.repository.Repository;

public interface CustomizeTableRepository extends Repository<CustomizeTableEntity, Long> {
Expand All @@ -28,5 +29,14 @@ default CustomizeTableEntity getById(long tableId) {
.orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND));
}

// νŠΈλžœμž­μ…˜ λ°–μ—μ„œλ„ ν…Œμ΄λΈ” μ†Œμœ  νšŒμ› 정보λ₯Ό μ“Έ 수 μžˆλ„λ‘ ν•¨κ»˜ μ‘°νšŒν•œλ‹€
@EntityGraph(attributePaths = "member")
Optional<CustomizeTableEntity> findWithMemberById(long id);

default CustomizeTableEntity getWithMemberById(long tableId) {
return findWithMemberById(tableId)
.orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND));
}

void delete(CustomizeTableEntity table);
}
Loading
Loading