diff --git a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java
index f43a925..1115a75 100644
--- a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java
+++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMeta.java
@@ -1,5 +1,6 @@
package com.firstticket.queueservice.programmeta.domain;
+import com.firstticket.queueservice.programmeta.domain.exception.ProgramNotActiveException;
import com.firstticket.queueservice.programmeta.domain.vo.ProgramId;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -79,6 +80,21 @@ public boolean isActiveAt(LocalDateTime now) {
return !openAt.isAfter(now) && !closeAt.isBefore(now);
}
+ /**
+ * 현재 시점에 입장 가능한지 보장. 그렇지 않으면 도메인 예외를 던진다.
+ *
+ *
대기열 진입 시점처럼 "활성 본질을 강제" 해야 하는 곳에서 사용한다.
+ * 호출 측은 분기 없이 메소드만 호출하면 된다 (Tell, Don't Ask).
+ *
+ * @throws ProgramNotActiveException CANCELLED 이거나 openAt 전 / closeAt 후
+ */
+ public void ensureActiveAt(LocalDateTime now) {
+ Objects.requireNonNull(now, "now는 null일 수 없습니다.");
+ if (!isActiveAt(now)) {
+ throw new ProgramNotActiveException();
+ }
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java
index 7e22090..89d683e 100644
--- a/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java
+++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/ProgramMetaRepository.java
@@ -5,6 +5,7 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
/**
* ProgramMeta 영속화 인터페이스.
@@ -19,6 +20,10 @@ public interface ProgramMetaRepository {
Optional findById(ProgramId programId);
+ default Optional findById(UUID programId) {
+ return findById(ProgramId.of(programId));
+ }
+
List findAll();
void deleteById(ProgramId programId);
diff --git a/src/main/java/com/firstticket/queueservice/programmeta/domain/exception/ProgramNotActiveException.java b/src/main/java/com/firstticket/queueservice/programmeta/domain/exception/ProgramNotActiveException.java
new file mode 100644
index 0000000..b9c545f
--- /dev/null
+++ b/src/main/java/com/firstticket/queueservice/programmeta/domain/exception/ProgramNotActiveException.java
@@ -0,0 +1,21 @@
+package com.firstticket.queueservice.programmeta.domain.exception;
+
+import com.firstticket.common.exception.BusinessException;
+import com.firstticket.queueservice.queuetoken.domain.exception.QueueErrorCode;
+
+/**
+ * 현재 시점이 프로그램의 입장 가능 시간이 아닐 때.
+ *
+ * 본질:
+ *
+ * - openAt 전
+ * - closeAt 후
+ * - CANCELLED 상태
+ *
+ */
+public class ProgramNotActiveException extends BusinessException {
+
+ public ProgramNotActiveException() {
+ super(QueueErrorCode.PROGRAM_NOT_ACTIVE);
+ }
+}
diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java b/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java
index a759550..d77d274 100644
--- a/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java
+++ b/src/main/java/com/firstticket/queueservice/queuetoken/application/QueueTokenService.java
@@ -1,5 +1,7 @@
package com.firstticket.queueservice.queuetoken.application;
+import com.firstticket.queueservice.programmeta.domain.ProgramMeta;
+import com.firstticket.queueservice.programmeta.domain.ProgramMetaRepository;
import com.firstticket.queueservice.queuetoken.application.dto.CancelQueueTokenCommand;
import com.firstticket.queueservice.queuetoken.application.dto.GetQueueTokenQuery;
import com.firstticket.queueservice.queuetoken.application.dto.IssueQueueTokenCommand;
@@ -8,12 +10,15 @@
import com.firstticket.queueservice.queuetoken.domain.QueueTokenRepository;
import com.firstticket.queueservice.queuetoken.domain.exception.DuplicateTokenException;
import com.firstticket.queueservice.queuetoken.domain.exception.InvalidTokenStateException;
+import com.firstticket.queueservice.queuetoken.domain.exception.ProgramNotFoundException;
import com.firstticket.queueservice.queuetoken.domain.exception.TokenNotFoundException;
import com.firstticket.queueservice.queuetoken.domain.vo.ProgramId;
import com.firstticket.queueservice.queuetoken.domain.vo.UserId;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
+import java.time.LocalDateTime;
+
/**
* 대기열 진입 / 조회 / 취소를 처리하는 서비스.
*/
@@ -22,6 +27,7 @@
public class QueueTokenService {
private final QueueTokenRepository queueTokenRepository;
+ private final ProgramMetaRepository programMetaRepository;
/**
* 대기열에 진입한다.
@@ -36,7 +42,10 @@ public QueueTokenResult issueToken(IssueQueueTokenCommand command) {
UserId userId = command.userId();
ProgramId programId = command.programId();
- // 같은 user+program 토큰이 있으면 폐기 후 새로 발급
+ // 1. 활성 프로그램 검증
+ validateProgramActive(programId);
+
+ // 2. 같은 user+program 토큰이 있으면 폐기 후 새로 발급
queueTokenRepository.findByUserIdAndProgramId(userId, programId)
.ifPresent(queueTokenRepository::delete);
@@ -87,4 +96,16 @@ public void cancelToken(CancelQueueTokenCommand command) {
queueTokenRepository.delete(token);
}
+
+ /**
+ * ProgramMeta 로 활성 프로그램 검증.
+ *
+ * 존재 여부 + 시간 + status 를 확인한다.
+ */
+ private void validateProgramActive(ProgramId programId) {
+ ProgramMeta programMeta = programMetaRepository.findById(programId.id())
+ .orElseThrow(ProgramNotFoundException::new);
+
+ programMeta.ensureActiveAt(LocalDateTime.now());
+ }
}
diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java
index 74cc35b..3b94d49 100644
--- a/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java
+++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/QueueTokenRepository.java
@@ -55,11 +55,6 @@ public interface QueueTokenRepository {
*/
void admit(QueueToken token);
- /**
- * 현재 큐가 존재하는 모든 프로그램 ID 를 조회한다.
- */
- List findActiveProgramIds();
-
/**
* 특정 프로그램의 모든 대기 / 입장 토큰을 삭제한다.
* Program 이 취소되었을 때 호출.
diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java
new file mode 100644
index 0000000..fae998d
--- /dev/null
+++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/ProgramNotFoundException.java
@@ -0,0 +1,19 @@
+package com.firstticket.queueservice.queuetoken.domain.exception;
+
+import com.firstticket.common.exception.BusinessException;
+
+/**
+ * 입장하려는 프로그램이 ProgramMeta 캐시에 존재하지 않을 때.
+ *
+ * 가능한 본질:
+ *
+ * - program.created 이벤트 도착 전
+ * - 존재하지 않는 programId 로 입장 시도
+ *
+ */
+public class ProgramNotFoundException extends BusinessException {
+
+ public ProgramNotFoundException() {
+ super(QueueErrorCode.PROGRAM_NOT_FOUND);
+ }
+}
diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java
index bf70cb5..cd0c33d 100644
--- a/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java
+++ b/src/main/java/com/firstticket/queueservice/queuetoken/domain/exception/QueueErrorCode.java
@@ -8,6 +8,8 @@
@Getter
@RequiredArgsConstructor
public enum QueueErrorCode implements ErrorCode {
+ PROGRAM_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 프로그램입니다"),
+ PROGRAM_NOT_ACTIVE(HttpStatus.UNPROCESSABLE_ENTITY, "현재 입장 가능 시간이 아닙니다"),
INVALID_TOKEN_STATE(HttpStatus.BAD_REQUEST, "대기 토큰 상태 전이 규칙을 위반했습니다"),
DUPLICATE_TOKEN(HttpStatus.CONFLICT, "이미 대기 중인 토큰이 있습니다"),
TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "토큰을 찾을 수 없습니다");
diff --git a/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java b/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
index 971e593..7ff0b3b 100644
--- a/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
+++ b/src/main/java/com/firstticket/queueservice/queuetoken/infrastructure/redis/RedisQueueTokenRepository.java
@@ -339,55 +339,57 @@ public List