diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9e92da5..3a9600a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,31 +25,31 @@ jobs: build-and-test: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 - - name: Set up JDK 21 - uses: actions/setup-java@v5 - with: - java-version: '21' - distribution: 'temurin' + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v6 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 - - name: Grant execute permission to gradlew - run: chmod +x gradlew + - name: Grant execute permission to gradlew + run: chmod +x gradlew - - name: Build & Test - run: ./gradlew build --no-daemon + - name: Build & Test + run: ./gradlew build --no-daemon - - name: Upload test report (on failure) - if: failure() - uses: actions/upload-artifact@v5 - with: - name: test-report - path: build/reports/tests/ - retention-days: 7 + - name: Upload test report (on failure) + if: failure() + uses: actions/upload-artifact@v5 + with: + name: test-report + path: build/reports/tests/ + retention-days: 7 push-to-ecr: needs: build-and-test @@ -57,48 +57,50 @@ jobs: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - id: ecr-login - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build, tag, and push image to ECR - env: - REGISTRY: ${{ steps.ecr-login.outputs.registry }} - IMAGE_TAG: ${{ github.sha }} - run: | - docker buildx build --platform linux/amd64 \ - --build-arg GITHUB_USER=${{ secrets.GH_USER }} \ - --secret id=github_token,env=GITHUB_TOKEN \ - -t $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ - -t $REGISTRY/$ECR_REPOSITORY:latest \ - --push \ - . - - - name: Show pushed image - run: | - echo "✅ Pushed: $ECR_REPOSITORY:${{ github.sha }}" - echo "✅ Pushed: $ECR_REPOSITORY:latest" - - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition program-service \ - --query taskDefinition > task-definition.json - - - name: Deploy to ECS - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: task-definition.json - service: program-service - cluster: first-ticket-cluster - wait-for-service-stability: false + - uses: actions/checkout@v6 + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build, tag, and push image to ECR + env: + REGISTRY: ${{ steps.ecr-login.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + GH_USER: ${{ secrets.GH_USER }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + docker buildx build --platform linux/amd64 \ + --build-arg GITHUB_USER="$GH_USER" \ + --secret id=github_token,src=<(echo "$GH_TOKEN") \ + -t $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ + -t $REGISTRY/$ECR_REPOSITORY:latest \ + --push \ + . + + - name: Show pushed image + run: | + echo "✅ Pushed: $ECR_REPOSITORY:${{ github.sha }}" + echo "✅ Pushed: $ECR_REPOSITORY:latest" + + - name: Download task definition + run: | + aws ecs describe-task-definition --task-definition program-service \ + --query taskDefinition > task-definition.json + + - name: Deploy to ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + service: program-service + cluster: first-ticket-cluster + wait-for-service-stability: false diff --git a/Dockerfile b/Dockerfile index 2227423..9af7e1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,19 +11,15 @@ COPY gradle gradle COPY build.gradle settings.gradle ./ RUN chmod +x gradlew -# 2. 의존성 미리 다운로드 ARG GITHUB_USER -RUN --mount=type=secret,id=github_token \ - GITHUB_TOKEN="$(cat /run/secrets/github_token)" \ - GITHUB_USER=$GITHUB_USER \ - ./gradlew dependencies --no-daemon -# 3. 소스 코드 복사 및 실행 가능한 JAR 빌드 COPY src src + RUN --mount=type=secret,id=github_token \ GITHUB_TOKEN="$(cat /run/secrets/github_token)" \ GITHUB_USER=$GITHUB_USER \ - ./gradlew clean bootJar --no-daemon -x test + ./gradlew clean bootJar --no-daemon -x test -x asciidoctor + # 4. Spring Boot 3의 계층화 기능을 활용해 레이어 추출 RUN java -Djarmode=layertools -jar build/libs/*.jar extract diff --git a/src/main/java/com/firstticket/programservice/application/dto/command/CreateScheduleCommand.java b/src/main/java/com/firstticket/programservice/application/dto/command/CreateScheduleCommand.java index 73983f1..f9a5ddd 100644 --- a/src/main/java/com/firstticket/programservice/application/dto/command/CreateScheduleCommand.java +++ b/src/main/java/com/firstticket/programservice/application/dto/command/CreateScheduleCommand.java @@ -13,7 +13,6 @@ public record CreateScheduleCommand( LocalDateTime eventStartAt, LocalDateTime eventEndAt, LocalDateTime saleStartAt, - LocalDateTime saleEndAt, - int totalCapacity + LocalDateTime saleEndAt ) { } diff --git a/src/main/java/com/firstticket/programservice/application/dto/command/UpdateScheduleCommand.java b/src/main/java/com/firstticket/programservice/application/dto/command/UpdateScheduleCommand.java index 9c3d7f1..e5742ac 100644 --- a/src/main/java/com/firstticket/programservice/application/dto/command/UpdateScheduleCommand.java +++ b/src/main/java/com/firstticket/programservice/application/dto/command/UpdateScheduleCommand.java @@ -16,7 +16,6 @@ public record UpdateScheduleCommand( LocalDateTime eventEndAt, LocalDateTime saleStartAt, LocalDateTime saleEndAt, - UUID venueId, - Integer totalCapacity + UUID venueId ) { } diff --git a/src/main/java/com/firstticket/programservice/application/service/ProgramCommandService.java b/src/main/java/com/firstticket/programservice/application/service/ProgramCommandService.java index d35e91c..4730dbf 100644 --- a/src/main/java/com/firstticket/programservice/application/service/ProgramCommandService.java +++ b/src/main/java/com/firstticket/programservice/application/service/ProgramCommandService.java @@ -257,13 +257,6 @@ public ProgramResult createSchedule(UUID requesterId, CreateScheduleCommand comm VenueValidationData venueValidation = venueProvider.validateVenue(command.venueId(), program.getType()); - // 스케줄 totalCapacity가 venue 내 해당 타입 구역 전체 수용량을 초과하는지 검증 - // ex) SEATED 프로그램인데 venue에 SEATED 구역 수용량 합계가 500석이면 - // totalCapacity는 500을 넘을 수 없다 - if (command.totalCapacity() > venueValidation.totalCapacity()) { - throw new ProgramException(ProgramErrorCode.TOTAL_CAPACITY_EXCEEDS_VENUE_LIMIT); - } - // 2. 공연장 시간 겹침 검증 // 비관적 락으로 동시 요청 간 TOCTOU 방지 (V-04) // DB 레벨 exclusion constraint(tsrange)와 이중 방어 @@ -275,7 +268,7 @@ public ProgramResult createSchedule(UUID requesterId, CreateScheduleCommand comm // 3. 스케줄 생성 program.addSchedule(command.venueId(), command.eventStartAt(), command.eventEndAt(), command.saleStartAt(), - command.saleEndAt(), command.totalCapacity(), LocalDateTime.now()); + command.saleEndAt(), venueValidation.totalCapacity(), LocalDateTime.now()); programRepository.save(program); return ProgramResult.from(program); @@ -294,14 +287,14 @@ public ProgramResult updateSchedule(UUID requesterId, UpdateScheduleCommand comm Schedule schedule = findScheduleInProgram(program, command.scheduleId()); + Integer newTotalCapacity = null; + // venueId가 변경되는 경우에만 존재 여부 확인 if (command.venueId() != null) { VenueValidationData venueValidation = venueProvider.validateVenue(command.venueId(), program.getType()); - if (command.totalCapacity() > venueValidation.totalCapacity()) { - throw new ProgramException(ProgramErrorCode.TOTAL_CAPACITY_EXCEEDS_VENUE_LIMIT); - } + newTotalCapacity = venueValidation.totalCapacity(); // 변경된 venueId 기준으로 시간 겹침 검증 // 자기 자신은 제외하고 검증 @@ -321,10 +314,12 @@ public ProgramResult updateSchedule(UUID requesterId, UpdateScheduleCommand comm } } + // schedule.update() 단일 호출 + // totalCapacity: venueId 변경 시 → venue 계산값, 미변경 시 → null (기존 값 유지) schedule.update( command.eventStartAt(), command.eventEndAt(), command.saleStartAt(), command.saleEndAt(), - command.venueId(), command.totalCapacity(), + command.venueId(), newTotalCapacity, LocalDateTime.now() ); @@ -566,9 +561,6 @@ private void validateCreateScheduleCommand(CreateScheduleCommand command) { if (command.saleStartAt() == null || command.saleEndAt() == null) { throw new ProgramException(ProgramErrorCode.INVALID_SALE_PERIOD); } - if (command.totalCapacity() <= 0) { - throw new ProgramException(ProgramErrorCode.INVALID_CAPACITY); - } } /** @@ -580,10 +572,6 @@ private void validateUpdateScheduleCommand(UpdateScheduleCommand command) { if (command.scheduleId() == null) { throw new ProgramException(ProgramErrorCode.SCHEDULE_NOT_FOUND); } - // 도메인의 Schedule.update()와 일관성 유지 — 0도 차단 - if (command.totalCapacity() != null && command.totalCapacity() <= 0) { - throw new ProgramException(ProgramErrorCode.INVALID_CAPACITY); - } } /** diff --git a/src/main/java/com/firstticket/programservice/presentation/dto/request/CreateScheduleRequest.java b/src/main/java/com/firstticket/programservice/presentation/dto/request/CreateScheduleRequest.java index 035f5fb..79a1cb7 100644 --- a/src/main/java/com/firstticket/programservice/presentation/dto/request/CreateScheduleRequest.java +++ b/src/main/java/com/firstticket/programservice/presentation/dto/request/CreateScheduleRequest.java @@ -7,7 +7,6 @@ import jakarta.validation.constraints.FutureOrPresent; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; /** * 스케줄 등록 요청 DTO. @@ -33,18 +32,14 @@ public record CreateScheduleRequest( @NotNull(message = "판매 종료 일시는 필수입니다") @FutureOrPresent(message = "판매 종료 일시는 현재 시각 이후여야 합니다") - LocalDateTime saleEndAt, - - @Positive(message = "수용 인원은 1명 이상이어야 합니다") - int totalCapacity + LocalDateTime saleEndAt ) { public CreateScheduleCommand toCommand(UUID programId) { return new CreateScheduleCommand( programId, venueId, eventStartAt, eventEndAt, - saleStartAt, saleEndAt, - totalCapacity + saleStartAt, saleEndAt ); } } diff --git a/src/main/java/com/firstticket/programservice/presentation/dto/request/UpdateScheduleRequest.java b/src/main/java/com/firstticket/programservice/presentation/dto/request/UpdateScheduleRequest.java index ae408bb..fc905d2 100644 --- a/src/main/java/com/firstticket/programservice/presentation/dto/request/UpdateScheduleRequest.java +++ b/src/main/java/com/firstticket/programservice/presentation/dto/request/UpdateScheduleRequest.java @@ -5,8 +5,6 @@ import com.firstticket.programservice.application.dto.command.UpdateScheduleCommand; -import jakarta.validation.constraints.Positive; - /** * 스케줄 수정 요청 DTO. * null이면 기존 값 유지 (부분 업데이트). @@ -17,16 +15,14 @@ public record UpdateScheduleRequest( LocalDateTime eventEndAt, LocalDateTime saleStartAt, LocalDateTime saleEndAt, - UUID venueId, - @Positive(message = "수용 인원은 1명 이상이어야 합니다") - Integer totalCapacity // ← primitive int → Integer + UUID venueId ) { public UpdateScheduleCommand toCommand(UUID programId, UUID scheduleId) { return new UpdateScheduleCommand( programId, scheduleId, eventStartAt, eventEndAt, saleStartAt, saleEndAt, - venueId, totalCapacity + venueId ); } } diff --git a/src/test/java/com/firstticket/programservice/application/service/ProgramServiceTest.java b/src/test/java/com/firstticket/programservice/application/service/ProgramServiceTest.java index d521e9d..d9c8845 100644 --- a/src/test/java/com/firstticket/programservice/application/service/ProgramServiceTest.java +++ b/src/test/java/com/firstticket/programservice/application/service/ProgramServiceTest.java @@ -462,8 +462,7 @@ private CreateScheduleCommand validCommand() { return new CreateScheduleCommand( PROGRAM_ID, VENUE_ID, FUTURE, FUTURE.plusHours(2), - FUTURE.minusDays(30), FUTURE.minusDays(1), - 500 + FUTURE.minusDays(30), FUTURE.minusDays(1) ); } @@ -484,21 +483,6 @@ void success() { assertThat(result.schedules()).hasSize(1); } - @Test - @DisplayName("totalCapacity가 공연장 수용량 초과 시 TOTAL_CAPACITY_EXCEEDS_VENUE_LIMIT 예외 — 도메인 규칙 위반") - void fail_capacityExceedsVenueLimit() { - Program program = draftProgram(); - given(programRepository.findByIdWithSchedules(PROGRAM_ID)) - .willReturn(Optional.of(program)); - given(venueProvider.validateVenue(VENUE_ID, ProgramType.SEATED)) - .willReturn(venueValidation(499)); // command.totalCapacity=500 > 499 - - assertThatThrownBy(() -> programCommandService.createSchedule(OWNER_ID, validCommand())) - .isInstanceOf(ProgramException.class) - .extracting(e -> ((ProgramException)e).getErrorCode()) - .isEqualTo(ProgramErrorCode.TOTAL_CAPACITY_EXCEEDS_VENUE_LIMIT); - } - @Test @DisplayName("공연장 시간 겹침 시 VENUE_TIME_CONFLICT 예외 — 도메인 규칙 위반 (V-04)") void fail_venueTimeConflict() { diff --git a/src/test/java/com/firstticket/programservice/presentation/ProgramControllerTest.java b/src/test/java/com/firstticket/programservice/presentation/ProgramControllerTest.java index 7e4b9d5..2d5441d 100644 --- a/src/test/java/com/firstticket/programservice/presentation/ProgramControllerTest.java +++ b/src/test/java/com/firstticket/programservice/presentation/ProgramControllerTest.java @@ -707,8 +707,7 @@ void success() throws Exception { "eventStartAt": "2027-06-01T14:00:00", "eventEndAt": "2027-06-01T17:00:00", "saleStartAt": "2027-05-01T10:00:00", - "saleEndAt": "2027-05-31T23:59:59", - "totalCapacity": 500 + "saleEndAt": "2027-05-31T23:59:59" } """.formatted(VENUE_ID))) .andExpect(status().isCreated()) @@ -729,8 +728,7 @@ void success() throws Exception { fieldWithPath("eventStartAt").description("공연 시작 일시 (현재 이후)"), fieldWithPath("eventEndAt").description("공연 종료 일시 (현재 이후)"), fieldWithPath("saleStartAt").description("판매 시작 일시 (현재 이후)"), - fieldWithPath("saleEndAt").description("판매 종료 일시 (현재 이후)"), - fieldWithPath("totalCapacity").description("수용 인원 (1 이상)") + fieldWithPath("saleEndAt").description("판매 종료 일시 (현재 이후)") ) )); } @@ -748,8 +746,7 @@ void fail_missingVenueId() throws Exception { "eventStartAt": "2027-06-01T14:00:00", "eventEndAt": "2027-06-01T17:00:00", "saleStartAt": "2027-05-01T10:00:00", - "saleEndAt": "2027-05-31T23:59:59", - "totalCapacity": 500 + "saleEndAt": "2027-05-31T23:59:59" } """)) .andExpect(status().isBadRequest()) @@ -781,8 +778,7 @@ void fail_venueConflict() throws Exception { "eventStartAt": "2027-06-01T14:00:00", "eventEndAt": "2027-06-01T17:00:00", "saleStartAt": "2027-05-01T10:00:00", - "saleEndAt": "2027-05-31T23:59:59", - "totalCapacity": 500 + "saleEndAt": "2027-05-31T23:59:59" } """.formatted(VENUE_ID))) .andExpect(status().isConflict()) @@ -925,8 +921,7 @@ void success() throws Exception { fieldWithPath("eventEndAt").type(JsonFieldType.STRING).optional().description("공연 종료 일시"), fieldWithPath("saleStartAt").type(JsonFieldType.STRING).optional().description("판매 시작 일시"), fieldWithPath("saleEndAt").type(JsonFieldType.STRING).optional().description("판매 종료 일시"), - fieldWithPath("venueId").type(JsonFieldType.STRING).optional().description("공연장 UUID"), - fieldWithPath("totalCapacity").type(JsonFieldType.NUMBER).optional().description("수용 인원") + fieldWithPath("venueId").type(JsonFieldType.STRING).optional().description("공연장 UUID") ) )); }