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
5 changes: 2 additions & 3 deletions .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
workflow_run:
workflows: [ "CI-PROD" ]
types: [ completed ]
branches: [ main, dev ]
workflow_dispatch:

concurrency:
Expand All @@ -29,9 +30,7 @@ jobs:
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
github.event.workflow_run.conclusion == 'success'
)
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/ci-prod.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: CI-PROD

on:
pull_request:
branches: [ main, dev ]
push:
branches: [ main, dev ]

Expand Down Expand Up @@ -37,21 +35,18 @@ jobs:
run: echo "IMAGE_TAG=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_ENV

- name: Configure AWS credentials (OIDC) # OIDC 기반으로 역할 자격증명
if: github.event_name == 'push'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}

- name: Login to Amazon ECR # 위에서 자격증명하고 ecr 로그인
if: github.event_name == 'push'
uses: aws-actions/amazon-ecr-login@v2

- name: Build Docker image # 이미지 빌드해서
run: |
docker build --build-arg SERVER_PORT=${{ vars.SERVER_PORT }} -t ${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} .

- name: Push Docker image to ECR # 이미지 ecr로 업로드
if: github.event_name == 'push'
run: |
docker push ${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
branches: [ main, dev ]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew

- name: Run tests
run: ./gradlew test --no-daemon
14 changes: 0 additions & 14 deletions api-test/order.http
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,6 @@ Authorization: Bearer {{ord_userToken}}
"reservationId": "{{ord_reservation_cancel}}",
"orderName": "미슐랭 코스 취소용 주문",
"receivingMethod": "PICKUP",
"expiredAt": "2026-06-01T18:00:00",
"items": [ { "optionId": "{{ord_option_id_1_1}}", "quantity": 1 } ]
}

Expand Down Expand Up @@ -412,7 +411,6 @@ Authorization: Bearer {{ord_userToken}}
"restaurantId": "{{ord_restaurantId_2}}",
"reservationId": "{{ord_reservation_receive}}",
"receivingMethod": "PICKUP",
"expiredAt": "2026-06-01T18:00:00",
"items": [
{ "optionId": "{{ord_option_id_2_1}}", "quantity": 2 },
{ "optionId": "{{ord_option_id_2_2}}", "quantity": 1 }
Expand Down Expand Up @@ -494,7 +492,6 @@ Authorization: Bearer {{dummy_user_token}}
"reservationId": "{{dummy_reservation_id}}",
"orderName": "아웃박스 보상 로직 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1}}",
Expand All @@ -521,7 +518,6 @@ Authorization: Bearer {{dummy_user_token}}
"reservationId": "{{dummy_reservation_id}}",
"orderName": "아웃박스 취소 로직 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1}}",
Expand Down Expand Up @@ -557,7 +553,6 @@ Authorization: Bearer {{dummy_user_token}}
"reservationId": "{{dummy_reservation_id}}",
"orderName": "아웃박스 스케줄러 보상 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1}}",
Expand All @@ -582,7 +577,6 @@ Authorization: Bearer {{dummy_user_token}}
"reservationId": "{{dummy_reservation_id}}",
"orderName": "아웃박스 스케줄러 취소 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1}}",
Expand Down Expand Up @@ -670,7 +664,6 @@ Authorization: Bearer {{dummy_user_token}}
"reservationId": "{{dummy_reservation_id}}",
"orderName": "정상 트랜잭션 분리 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-06-01T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1}}",
Expand Down Expand Up @@ -708,7 +701,6 @@ Authorization: Bearer {{ord_userToken}}
"reservationId": "{{fresh_test_reservation_id}}",
"orderName": "실제 데이터 기반 보상 로직 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1}}",
Expand Down Expand Up @@ -742,7 +734,6 @@ Content-Type: application/json
"reservationId": "{{$random.uuid}}",
"orderName": "취소할 테스트 주문",
"receivingMethod": "PICKUP",
"expiredAt": "2026-06-01T18:00:00",
"items": [
{ "optionId": "44444444-4444-4444-4444-444444444444", "quantity": 1 }
]
Expand Down Expand Up @@ -779,7 +770,6 @@ Content-Type: application/json
"orderName": "Saga 비동기 E2E 테스트 주문",
"receivingMethod": "PICKUP",
"reservedDate": "{{ord_targetDate_receive}}",
"expiredAt": "2026-06-01T18:00:00",
"items": [
{
"optionId": "44444444-4444-4444-4444-444444444444",
Expand Down Expand Up @@ -850,7 +840,6 @@ Content-Type: application/json
"reservationId": "{{$random.uuid}}",
"orderName": "Saga 비동기 강제 취소(거절) 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1_kafka}}",
Expand Down Expand Up @@ -896,7 +885,6 @@ Content-Type: application/json
"reservationId": "{{$random.uuid}}",
"orderName": "결정적 UUID 멱등성 테스트",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1_kafka}}",
Expand Down Expand Up @@ -948,7 +936,6 @@ Content-Type: application/json
"reservationId": "{{$random.uuid}}",
"orderName": "단일 토픽 순서 보장 테스트 주문",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1_kafka}}",
Expand Down Expand Up @@ -1001,7 +988,6 @@ Content-Type: application/json
"reservationId": "{{$random.uuid}}",
"orderName": "단일 토픽 순서 보장 테스트 주문",
"receivingMethod": "PICKUP",
"expiredAt": "2026-12-31T18:00:00",
"items": [
{
"optionId": "{{ord_option_id_2_1_kafka}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ public OrderResult createOrder(CreateOrderCommand command) {
finalOrderName,
verifiedDate, // 포트를 통해 받아온 날짜
method,
command.expiredAt(),
orderItems
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.michelet.order.application.dto;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
Expand All @@ -11,14 +10,12 @@ public record CreateOrderCommand(
UUID restaurantId,
String orderName,
String receivingMethod,
LocalDateTime expiredAt,
List<OrderItemCommand> items
) {
public CreateOrderCommand {
Objects.requireNonNull(userId, "userId는 필수입니다.");
Objects.requireNonNull(reservationId, "reservationId는 필수입니다.");
Objects.requireNonNull(restaurantId, "restaurantId는 필수입니다.");
Objects.requireNonNull(expiredAt, "expiredAt은 필수입니다.");
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("주문 항목은 최소 1개 이상이어야 합니다.");
}
Expand Down
21 changes: 17 additions & 4 deletions src/main/java/com/michelet/order/domain/model/Order.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private Order(UUID userId, UUID reservationId, UUID restaurantId, String orderNa
}

public static Order create(UUID userId, UUID reservationId, UUID restaurantId, String orderName,
LocalDate reservedDate, ReceivingMethod receivingMethod, LocalDateTime expiredAt,
LocalDate reservedDate, ReceivingMethod receivingMethod,
List<OrderItem> items) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// 필수 값에 대한 Fail-fast 검증
Objects.requireNonNull(userId, "userId는 필수입니다.");
Expand All @@ -109,19 +109,24 @@ public static Order create(UUID userId, UUID reservationId, UUID restaurantId, S
throw new IllegalArgumentException("orderName은 필수이며 비어있을 수 없습니다.");
}
Objects.requireNonNull(reservedDate, "reservedDate는 필수입니다.");
Objects.requireNonNull(expiredAt, "expiredAt은 필수입니다.");

if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("주문 항목은 최소 1개 이상이어야 합니다.");
}

ReceivingMethod normalizedReceivingMethod =
receivingMethod != null ? receivingMethod : ReceivingMethod.PICKUP;

LocalDateTime calculatedExpiredAt = calculateExpiredAt(normalizedReceivingMethod);

Order order = Order.builder()
.userId(userId)
.reservationId(reservationId)
.restaurantId(restaurantId)
.orderName(orderName)
.reservedDate(reservedDate)
.receivingMethod(receivingMethod)
.expiredAt(expiredAt)
.receivingMethod(normalizedReceivingMethod) // 정규화된 값 주입
.expiredAt(calculatedExpiredAt) // 정확히 계산된 시간 주입
.build();

// addOrderItem 내부에서 점진적 덧셈을 하므로 N^2 문제 해결 및 별도 calculateTotalAmount 호출 불필요해짐!
Expand Down Expand Up @@ -233,4 +238,12 @@ public void receive(LocalDate currentDate) {
}
this.status = OrderStatus.RECEIVED;
}

private static LocalDateTime calculateExpiredAt(ReceivingMethod receivingMethod) {
LocalDateTime now = LocalDateTime.now();
if (receivingMethod == ReceivingMethod.PICKUP) {
return now.toLocalDate().atTime(23, 0); // 당일 23시 00분
}
return now.plusMinutes(15); // 그 외(SHIPPING 등)는 현재 시간 + 15분
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public class FeignConfig {

private final InternalTokenIssuer internalTokenIssuer;

//FIXME 아직 수정 안한 서비스들 호환을 위해 일단 넣음 - 나중엔 이 부분 삭제
@Bean
public RequestInterceptor internalAuthRequestInterceptor() {
return template -> {
Expand Down Expand Up @@ -54,37 +53,3 @@ public ErrorDecoder errorDecoder() {
};
}
}

//TODO 다른 서비스들 다 feign 연결 끝나면 아래 활성화
//package com.michelet.order.infrastructure.config;
//
//import com.michelet.common.auth.feign.interceptor.InternalFeignInterceptor;
//import com.michelet.common.auth.feign.internal.InternalTokenIssuer;
//import com.michelet.common.auth.feign.config.InternalFeignProperties;
//import feign.RequestInterceptor;
//import feign.codec.ErrorDecoder;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//
//@Configuration
//public class FeignConfig {
//
// @Bean
// public RequestInterceptor internalFeignInterceptor(
// InternalTokenIssuer internalTokenIssuer,
// InternalFeignProperties properties
// ) {
// return new InternalFeignInterceptor(internalTokenIssuer, properties);
// }
//
// @Bean
// public ErrorDecoder errorDecoder() {
// // 에러 로깅 및 예외 변환 로직만 남기기
// return (methodKey, response) -> {
// if (response.status() >= 400 && response.status() < 500) {
// return new IllegalArgumentException("내부 통신 인증 실패");
// }
// return new RuntimeException("외부 서비스 서버 오류");
// };
// }
//}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public ResponseEntity<ApiResponse<Void>> cancelOrder(@PathVariable UUID orderId)
return ResponseEntity.ok(ApiResponse.ok(null));
}

// TODO: 나중에 실제 해당 레스토랑의 오너인지 검증하는 로직 추가 필요 (@CheckOwner 등 AOP 적용 예정)
// NOTE: 나중에 실제 해당 레스토랑의 오너인지 검증하는 로직 추가 필요 (@CheckOwner 등 AOP 적용 예정)
@RequireRole(UserRole.OWNER)
@PatchMapping("/{orderId}/receive")
public ResponseEntity<ApiResponse<Void>> receiveOrder(@PathVariable UUID orderId) {
Expand All @@ -63,15 +63,15 @@ public ResponseEntity<ApiResponse<Void>> receiveOrder(@PathVariable UUID orderId
return ResponseEntity.ok(ApiResponse.ok(null));
}

// TODO: 나중에 테스트 완료 후 `@CheckOwner` 또는 소유자 검증 AOP 적용 필요 - 현재 인가 검증 미적용
// NOTE: 나중에 테스트 완료 후 `@CheckOwner` 또는 소유자 검증 AOP 적용 필요 - 현재 인가 검증 미적용
@RequireRole({UserRole.USER, UserRole.OWNER, UserRole.MASTER}) // 최소한의 인증(로그인 여부)만 요구하도록 추가
@GetMapping("/{orderId}")
public ResponseEntity<ApiResponse<OrderResult>> getOrder(@PathVariable UUID orderId) {
OrderResult result = orderCommandService.getOrder(orderId);
return ResponseEntity.ok(ApiResponse.ok(result));
}

// TODO: 나중에 결제 시스템 연동 완료 시 웹훅 등으로 변경 예정 + 해당 레스토랑 오너 검증 로직 추가 필요 (@CheckOwner AOP 도입)
// NOTE: 나중에 결제 시스템 연동 완료 시 웹훅 등으로 변경 예정 + 해당 레스토랑 오너 검증 로직 추가 필요 (@CheckOwner AOP 도입)
@RequireRole(UserRole.OWNER)
@PatchMapping("/{orderId}/complete")
public ResponseEntity<ApiResponse<Void>> completeOrder(@PathVariable UUID orderId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

Expand All @@ -15,7 +14,6 @@ public record CreateOrderRequest(
@NotNull UUID restaurantId,
String orderName,
@NotBlank String receivingMethod,
@NotNull LocalDateTime expiredAt,
@NotEmpty List<@Valid OrderItemRequest> items
) {
public record OrderItemRequest(
Expand All @@ -31,7 +29,6 @@ public CreateOrderCommand toCommand(UUID userId) {
restaurantId,
orderName,
receivingMethod,
expiredAt,
items.stream().map(it -> new CreateOrderCommand.OrderItemCommand(
it.optionId(), it.quantity()
)).toList()
Expand Down
Loading
Loading