diff --git a/.github/workflows/ci-prod.yml b/.github/workflows/ci-prod.yml index 916cebd..14ea66a 100644 --- a/.github/workflows/ci-prod.yml +++ b/.github/workflows/ci-prod.yml @@ -2,40 +2,49 @@ name: CI-PROD on: pull_request: - branches: [ main, dev ] # main 브랜치에 pr 했을때 트리거 + branches: [ main ] push: - branches: [ main, dev ] # main 브랜치에 push 했을때 트리거 - -permissions: - contents: read # 내 코드를 읽을 수 있게(코드이동) 접근 권한을 준다. - id-token: write # OIDC 토큰을 발급할 수 있게 한다. + branches: [ main ] jobs: - build: + test: runs-on: ubuntu-latest + permissions: + contents: read steps: - - name: Checkout # 내 코드 이동 깃허브 컴퓨터로 + - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 17 # jdk 환경 설치 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: "17" - - name: Set up Gradle => gradle 설치 + - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - - name: Grant execute permission for gradlew # 실행 권한 설정 + - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: Run tests # 테스트 실행 + - name: Run tests run: ./gradlew test --no-daemon -Dspring.profiles.active=test - - name: Generate image tag # pr이 아닌 push 일때 이미지 태그 생성 - if: github.event_name == 'push' # 커밋 기반으로 어떤 코드 버전이 배포됐는지 정확히 추적 가능 - run: echo "IMAGE_TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV # 태그란? 이미지 버전 이름 + build-and-push: + if: github.event_name == 'push' + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate image tag + run: echo "IMAGE_TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Configure AWS credentials (OIDC) # OIDC 기반으로 역할 자격증명 if: github.event_name == 'push' @@ -44,16 +53,13 @@ jobs: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: ${{ vars.AWS_REGION }} - - name: Login to Amazon ECR # 위에서 자격증명하고 ecr 로그인 - if: github.event_name == 'push' + - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@v2 - - name: Build Docker image # 이미지 빌드해서 - if: github.event_name == 'push' + - name: Build Docker image run: | docker build -t ${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} . - - name: Push Docker image to ECR # 이미지 ecr로 업로드 - if: github.event_name == 'push' + - name: Push Docker image to ECR run: | docker push ${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} diff --git a/src/main/java/com/michelet/reservation/domain/entity/Reservation.java b/src/main/java/com/michelet/reservation/domain/entity/Reservation.java index b4514c1..12ed581 100644 --- a/src/main/java/com/michelet/reservation/domain/entity/Reservation.java +++ b/src/main/java/com/michelet/reservation/domain/entity/Reservation.java @@ -113,18 +113,9 @@ public void complete(LocalDateTime now) { if (this.status == ReservationStatus.COMPLETED) { return; // 이미 완료된 경우 멱등 처리 } - if (state.status() != ReservationStatus.CONFIRMED) { - throw new BusinessException(ReservationErrorCode.INVALID_STATUS_TRANSITION); - } - LocalDateTime windowStart = noshowDeadline.minusMinutes(60); // noshowDeadline - 60min (= slotStart - 30min) - if (now.isBefore(windowStart)) { - throw new BusinessException(ReservationErrorCode.CHECK_IN_TOO_EARLY); - } - if (now.isAfter(noshowDeadline)) { - throw new BusinessException(ReservationErrorCode.CHECK_IN_TOO_LATE); - } - this.status = state.complete(); - this.state = ReservationStateFactory.from(this.status); + state.assertCompletable(now, noshowDeadline); + this.status = state.complete(); + this.state = ReservationStateFactory.from(this.status); this.checkedInAt = now; } @@ -138,13 +129,11 @@ public boolean requiresSlotReturn() { } public boolean isCancellable() { - return state.status() == ReservationStatus.CONFIRMED - && !LocalDate.now().isAfter(cancelDeadline); + return state.isCancellable(cancelDeadline); } public boolean isModifiable() { - return state.status() == ReservationStatus.CONFIRMED - && !LocalDate.now().isAfter(modifyDeadline); + return state.isModifiable(modifyDeadline); } public boolean requiresRefund() { @@ -157,18 +146,13 @@ public void modify( GuestCount newGuestCount, LocalDateTime newNoshowDeadline ) { - if (state.status() != ReservationStatus.CONFIRMED) { - throw new BusinessException(ReservationErrorCode.INVALID_STATUS_TRANSITION); - } - if (LocalDate.now().isAfter(modifyDeadline)) { - throw new BusinessException(ReservationErrorCode.MODIFY_DEADLINE_EXCEEDED); - } - this.timeSlotId = newTimeSlotId; - this.reservedDate = newReservedDate; - this.guestCount = newGuestCount; - this.cancelDeadline = newReservedDate.minusDays(2); - this.modifyDeadline = newReservedDate.minusDays(2); - this.noshowDeadline = newNoshowDeadline; + state.assertModifiable(modifyDeadline); + this.timeSlotId = newTimeSlotId; + this.reservedDate = newReservedDate; + this.guestCount = newGuestCount; + this.cancelDeadline = newReservedDate.minusDays(2); + this.modifyDeadline = newReservedDate.minusDays(2); + this.noshowDeadline = newNoshowDeadline; } private static void validateCreateInput( diff --git a/src/main/java/com/michelet/reservation/domain/enums/ReservationTransition.java b/src/main/java/com/michelet/reservation/domain/enums/ReservationTransition.java deleted file mode 100644 index 9451783..0000000 --- a/src/main/java/com/michelet/reservation/domain/enums/ReservationTransition.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.michelet.reservation.domain.enums; - -import java.util.Arrays; - -public enum ReservationTransition { - - WAITING_TO_CONFIRMED (ReservationStatus.WAITING, ReservationStatus.CONFIRMED), - WAITING_TO_CANCELLED_UNPAID (ReservationStatus.WAITING, ReservationStatus.CANCELLED_UNPAID), - CONFIRMED_TO_CANCELLED_PAID (ReservationStatus.CONFIRMED, ReservationStatus.CANCELLED_PAID), - CONFIRMED_TO_COMPLETED (ReservationStatus.CONFIRMED, ReservationStatus.COMPLETED), - CONFIRMED_TO_NO_SHOW (ReservationStatus.CONFIRMED, ReservationStatus.NO_SHOW); - - private final ReservationStatus from; - private final ReservationStatus to; - - ReservationTransition(ReservationStatus from, ReservationStatus to) { - this.from = from; - this.to = to; - } - - public ReservationStatus from() { return from; } - public ReservationStatus to() { return to; } - - public static boolean isAllowed(ReservationStatus from, ReservationStatus to) { - return Arrays.stream(values()).anyMatch(t -> t.from == from && t.to == to); - } -} diff --git a/src/main/java/com/michelet/reservation/domain/state/ConfirmedState.java b/src/main/java/com/michelet/reservation/domain/state/ConfirmedState.java index 7f7783b..5274311 100644 --- a/src/main/java/com/michelet/reservation/domain/state/ConfirmedState.java +++ b/src/main/java/com/michelet/reservation/domain/state/ConfirmedState.java @@ -4,6 +4,7 @@ import com.michelet.reservation.domain.enums.ReservationStatus; import com.michelet.reservation.domain.exception.ReservationErrorCode; import java.time.LocalDate; +import java.time.LocalDateTime; public class ConfirmedState implements ReservationState { @@ -25,6 +26,34 @@ public ReservationStatus markNoShow() { return ReservationStatus.NO_SHOW; } + @Override + public boolean isCancellable(LocalDate cancelDeadline) { + return !LocalDate.now().isAfter(cancelDeadline); + } + + @Override + public boolean isModifiable(LocalDate modifyDeadline) { + return !LocalDate.now().isAfter(modifyDeadline); + } + + @Override + public void assertModifiable(LocalDate modifyDeadline) { + if (LocalDate.now().isAfter(modifyDeadline)) { + throw new BusinessException(ReservationErrorCode.MODIFY_DEADLINE_EXCEEDED); + } + } + + @Override + public void assertCompletable(LocalDateTime now, LocalDateTime noshowDeadline) { + LocalDateTime windowStart = noshowDeadline.minusMinutes(60); + if (now.isBefore(windowStart)) { + throw new BusinessException(ReservationErrorCode.CHECK_IN_TOO_EARLY); + } + if (now.isAfter(noshowDeadline)) { + throw new BusinessException(ReservationErrorCode.CHECK_IN_TOO_LATE); + } + } + @Override public boolean requiresSlotReturn() { return true; } diff --git a/src/main/java/com/michelet/reservation/domain/state/ReservationState.java b/src/main/java/com/michelet/reservation/domain/state/ReservationState.java index f1e386c..6b1f97d 100644 --- a/src/main/java/com/michelet/reservation/domain/state/ReservationState.java +++ b/src/main/java/com/michelet/reservation/domain/state/ReservationState.java @@ -4,6 +4,7 @@ import com.michelet.reservation.domain.enums.ReservationStatus; import com.michelet.reservation.domain.exception.ReservationErrorCode; import java.time.LocalDate; +import java.time.LocalDateTime; public interface ReservationState { @@ -28,6 +29,23 @@ default ReservationStatus markNoShow() { throw new BusinessException(ReservationErrorCode.INVALID_STATUS_TRANSITION); } + // 조건 판단 메서드 — CONFIRMED 외 상태는 false / 예외 + default boolean isCancellable(LocalDate cancelDeadline) { + return false; + } + + default boolean isModifiable(LocalDate modifyDeadline) { + return false; + } + + default void assertModifiable(LocalDate modifyDeadline) { + throw new BusinessException(ReservationErrorCode.INVALID_STATUS_TRANSITION); + } + + default void assertCompletable(LocalDateTime now, LocalDateTime noshowDeadline) { + throw new BusinessException(ReservationErrorCode.INVALID_STATUS_TRANSITION); + } + // 정책 메서드 — 상태별 구현 필수 boolean requiresSlotReturn(); boolean requiresRefund(); diff --git a/src/test/java/com/michelet/reservation/domain/enums/ReservationTransitionTest.java b/src/test/java/com/michelet/reservation/domain/enums/ReservationTransitionTest.java deleted file mode 100644 index f855fe7..0000000 --- a/src/test/java/com/michelet/reservation/domain/enums/ReservationTransitionTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.michelet.reservation.domain.enums; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class ReservationTransitionTest { - - @Nested - class AllowedTransitions { - - @Test - void WAITING에서_CONFIRMED로_전이_허용() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.WAITING, ReservationStatus.CONFIRMED)).isTrue(); - } - - @Test - void WAITING에서_CANCELLED_UNPAID로_전이_허용() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.WAITING, ReservationStatus.CANCELLED_UNPAID)).isTrue(); - } - - @Test - void CONFIRMED에서_CANCELLED_PAID로_전이_허용() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CONFIRMED, ReservationStatus.CANCELLED_PAID)).isTrue(); - } - - @Test - void CONFIRMED에서_COMPLETED로_전이_허용() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CONFIRMED, ReservationStatus.COMPLETED)).isTrue(); - } - - @Test - void CONFIRMED에서_NO_SHOW로_전이_허용() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CONFIRMED, ReservationStatus.NO_SHOW)).isTrue(); - } - } - - @Nested - class ForbiddenTransitions { - - @Test - void CONFIRMED에서_WAITING으로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CONFIRMED, ReservationStatus.WAITING)).isFalse(); - } - - @Test - void CONFIRMED에서_CONFIRMED로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CONFIRMED, ReservationStatus.CONFIRMED)).isFalse(); - } - - @Test - void COMPLETED에서_CONFIRMED로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.COMPLETED, ReservationStatus.CONFIRMED)).isFalse(); - } - - @Test - void NO_SHOW에서_CONFIRMED로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.NO_SHOW, ReservationStatus.CONFIRMED)).isFalse(); - } - - @Test - void CANCELLED_PAID에서_CONFIRMED로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CANCELLED_PAID, ReservationStatus.CONFIRMED)).isFalse(); - } - - @Test - void CANCELLED_UNPAID에서_CONFIRMED로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.CANCELLED_UNPAID, ReservationStatus.CONFIRMED)).isFalse(); - } - - @Test - void WAITING에서_CANCELLED_PAID로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.WAITING, ReservationStatus.CANCELLED_PAID)).isFalse(); - } - - @Test - void WAITING에서_COMPLETED로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.WAITING, ReservationStatus.COMPLETED)).isFalse(); - } - - @Test - void WAITING에서_NO_SHOW로_전이_금지() { - assertThat(ReservationTransition.isAllowed(ReservationStatus.WAITING, ReservationStatus.NO_SHOW)).isFalse(); - } - } - - @Nested - class TransitionCount { - - @Test - void 허용_전이는_정확히_5개다() { - long count = 0; - for (ReservationStatus from : ReservationStatus.values()) { - for (ReservationStatus to : ReservationStatus.values()) { - if (ReservationTransition.isAllowed(from, to)) { - count++; - } - } - } - assertThat(count).isEqualTo(5); - } - } -}