-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 단계별 리뷰 생성 및 AI 연동 #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cddab0a
a13f600
8d88830
b4030e2
c9e3c0a
2a453a1
5f84b8c
1a0a92b
ad130f8
ea14c66
9fcb733
02855d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,26 +3,39 @@ | |
| import java.util.List; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| import com.ongil.backend.domain.review.dto.request.ReviewFinalSubmitRequest; | ||
| import com.ongil.backend.domain.review.dto.request.ReviewListRequest; | ||
| import com.ongil.backend.domain.review.dto.request.ReviewStep1Request; | ||
| import com.ongil.backend.domain.review.dto.request.ReviewStep2MaterialRequest; | ||
| import com.ongil.backend.domain.review.dto.request.ReviewStep2SizeRequest; | ||
| import com.ongil.backend.domain.review.dto.response.*; | ||
| import com.ongil.backend.domain.review.enums.ReviewType; | ||
| import com.ongil.backend.domain.review.service.ReviewService; | ||
| import com.ongil.backend.domain.review.service.ReviewCommandService; | ||
| import com.ongil.backend.domain.review.service.ReviewQueryService; | ||
| import com.ongil.backend.global.common.dto.DataResponse; | ||
| import com.ongil.backend.global.common.exception.ErrorCode; | ||
| import com.ongil.backend.global.common.exception.ValidationException; | ||
| import com.ongil.backend.global.config.s3.S3ImageService; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Tag(name = "Review", description = "리뷰 API (토큰 필요)") | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class ReviewController { | ||
|
|
||
| private final ReviewService reviewService; | ||
| private final ReviewCommandService reviewCommandService; | ||
| private final ReviewQueryService reviewQueryService; | ||
| private final S3ImageService s3ImageService; | ||
|
|
||
| @Operation(summary = "상품별 리뷰 목록 조회", description = "상품의 리뷰 목록을 조회합니다. 유사 체형 필터, 사이즈/색상 필터, 정렬을 지원합니다.") | ||
| @GetMapping("/api/products/{productId}/reviews") | ||
|
|
@@ -31,7 +44,7 @@ public DataResponse<Page<ReviewListResponse>> getProductReviews( | |
| @AuthenticationPrincipal Long userId, | ||
| @ModelAttribute ReviewListRequest request | ||
| ) { | ||
| Page<ReviewListResponse> reviews = reviewService.getProductReviews(productId, userId, request); | ||
| Page<ReviewListResponse> reviews = reviewQueryService.getProductReviews(productId, userId, request); | ||
| return DataResponse.from(reviews); | ||
| } | ||
|
|
||
|
|
@@ -41,7 +54,7 @@ public DataResponse<ReviewSummaryResponse> getReviewSummary( | |
| @PathVariable Long productId, | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| ReviewSummaryResponse summary = reviewService.getReviewSummary(productId, userId); | ||
| ReviewSummaryResponse summary = reviewQueryService.getReviewSummary(productId, userId); | ||
| return DataResponse.from(summary); | ||
| } | ||
|
|
||
|
|
@@ -51,7 +64,7 @@ public DataResponse<ReviewDetailResponse> getReviewDetail( | |
| @PathVariable Long reviewId, | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| ReviewDetailResponse detail = reviewService.getReviewDetail(reviewId, userId); | ||
| ReviewDetailResponse detail = reviewQueryService.getReviewDetail(reviewId, userId); | ||
| return DataResponse.from(detail); | ||
| } | ||
|
|
||
|
|
@@ -63,7 +76,7 @@ public DataResponse<Page<MyReviewResponse>> getMyReviews( | |
| @Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") int page, | ||
| @Parameter(description = "페이지 크기") @RequestParam(defaultValue = "10") int pageSize | ||
| ) { | ||
| Page<MyReviewResponse> reviews = reviewService.getMyReviews(userId, reviewType, page, pageSize); | ||
| Page<MyReviewResponse> reviews = reviewQueryService.getMyReviews(userId, reviewType, page, pageSize); | ||
| return DataResponse.from(reviews); | ||
| } | ||
|
|
||
|
|
@@ -72,7 +85,7 @@ public DataResponse<Page<MyReviewResponse>> getMyReviews( | |
| public DataResponse<List<PendingReviewResponse>> getPendingReviews( | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| List<PendingReviewResponse> pendingReviews = reviewService.getPendingReviews(userId); | ||
| List<PendingReviewResponse> pendingReviews = reviewQueryService.getPendingReviews(userId); | ||
| return DataResponse.from(pendingReviews); | ||
| } | ||
|
|
||
|
|
@@ -81,7 +94,7 @@ public DataResponse<List<PendingReviewResponse>> getPendingReviews( | |
| public DataResponse<PendingReviewCountResponse> getPendingReviewCount( | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| int count = reviewService.getPendingReviewCount(userId); | ||
| int count = reviewQueryService.getPendingReviewCount(userId); | ||
| PendingReviewCountResponse response = PendingReviewCountResponse.builder() | ||
| .pendingReviewCount(count) | ||
| .build(); | ||
|
|
@@ -94,7 +107,97 @@ public DataResponse<ReviewHelpfulResponse> toggleHelpful( | |
| @PathVariable Long reviewId, | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| ReviewHelpfulResponse response = reviewService.toggleHelpful(reviewId, userId); | ||
| ReviewHelpfulResponse response = reviewQueryService.toggleHelpful(reviewId, userId); | ||
| return DataResponse.from(response); | ||
| } | ||
|
|
||
| @Operation(summary = "리뷰 작성 시작(ID 발급)", description = "리뷰 작성 진입 시 DRAFT 상태의 리뷰 ID를 미리 발급받습니다.") | ||
| @PostMapping("/api/reviews/init") | ||
| public DataResponse<ReviewIdResponse> initializeReview( | ||
| @AuthenticationPrincipal Long userId, | ||
| @RequestParam Long orderItemId | ||
| ) { | ||
| Long reviewId = reviewCommandService.initializeReview(userId, orderItemId); | ||
| return DataResponse.from(new ReviewIdResponse(reviewId)); | ||
| } | ||
|
|
||
| @Operation(summary = "리뷰 작성 1단계", description = "별점, 착용감 등 1차 답변을 기입합니다. 이전 단계 수정 시에도 사용됩니다.") | ||
| @PatchMapping("/api/reviews/{reviewId}/step1") | ||
| public DataResponse<ReviewStep1Response> updateReviewStep1( | ||
| @AuthenticationPrincipal Long userId, | ||
| @PathVariable Long reviewId, | ||
| @Valid @RequestBody ReviewStep1Request request | ||
| ) { | ||
| ReviewStep1Response response = reviewCommandService.updateReviewStep1(userId, reviewId, request); | ||
| return DataResponse.from(response); | ||
| } | ||
|
|
||
| @Operation(summary = "리뷰 작성 2단계 - 사이즈", description = "사이즈 2차 질문(불편 부위)에 답변합니다.") | ||
| @PatchMapping("/api/reviews/{reviewId}/step2/size") | ||
| public DataResponse<Void> updateReviewStep2Size( | ||
| @AuthenticationPrincipal Long userId, | ||
| @PathVariable Long reviewId, | ||
| @Valid @RequestBody ReviewStep2SizeRequest request | ||
| ) { | ||
| reviewCommandService.updateReviewStep2Size(userId, reviewId, request); | ||
| return DataResponse.ok(); | ||
| } | ||
|
|
||
| @Operation(summary = "리뷰 작성 2단계 - 소재", description = "소재 2차 질문(소재 특징)에 답변합니다.") | ||
| @PatchMapping("/api/reviews/{reviewId}/step2/material") | ||
| public DataResponse<Void> updateReviewStep2Material( | ||
| @AuthenticationPrincipal Long userId, | ||
| @PathVariable Long reviewId, | ||
| @Valid @RequestBody ReviewStep2MaterialRequest request | ||
| ) { | ||
| reviewCommandService.updateReviewStep2Material(userId, reviewId, request); | ||
| return DataResponse.ok(); | ||
| } | ||
|
marshmallowing marked this conversation as resolved.
|
||
|
|
||
| @Operation(summary = "리뷰 작성 3단계 - 사이즈 AI 생성", description = "사이즈 관련 AI 리뷰를 생성합니다.") | ||
| @GetMapping("/api/reviews/{reviewId}/ai/size") | ||
| public DataResponse<AiReviewResponse> generateSizeAiReview( | ||
| @PathVariable Long reviewId, | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| AiReviewResponse response = reviewCommandService.generateSizeAiReview(userId, reviewId); | ||
| return DataResponse.from(response); | ||
| } | ||
|
|
||
| @Operation(summary = "리뷰 작성 3단계 - 소재 AI 생성", description = "소재 관련 AI 리뷰를 생성합니다.") | ||
| @GetMapping("/api/reviews/{reviewId}/ai/material") | ||
| public DataResponse<AiReviewResponse> generateMaterialAiReview( | ||
| @PathVariable Long reviewId, | ||
| @AuthenticationPrincipal Long userId | ||
| ) { | ||
| AiReviewResponse response = reviewCommandService.generateMaterialAiReview(userId, reviewId); | ||
| return DataResponse.from(response); | ||
| } | ||
|
|
||
| @Operation(summary = "리뷰 사진 업로드", description = "리뷰용 사진을 최대 5장까지 S3에 업로드합니다.") | ||
| @PostMapping(value = "/api/reviews/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | ||
| public DataResponse<List<String>> uploadReviewImages( | ||
| @RequestPart("images") List<MultipartFile> images | ||
| ) { | ||
| if (images.size() > 5) { | ||
| throw new ValidationException(ErrorCode.INVALID_PARAMETER); | ||
| } | ||
|
|
||
| List<String> imageUrls = images.stream() | ||
| .map(s3ImageService::uploadReviewImage) | ||
| .toList(); | ||
| return DataResponse.from(imageUrls); | ||
| } | ||
|
Comment on lines
+177
to
+190
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "ReviewController.java" -type fRepository: IT-Cotato/12th-OnGil-BE Length of output: 147 🏁 Script executed: # ReviewController 파일의 전체 구조 확인
git ls-files "*ReviewController.java"Repository: IT-Cotato/12th-OnGil-BE Length of output: 145 🏁 Script executed: # Spring Security 설정 파일 탐색
fd "Security|Config" --type f -e java | head -20Repository: IT-Cotato/12th-OnGil-BE Length of output: 600 🏁 Script executed: # /api/reviews 경로에 대한 보안 설정 확인
rg "reviews" -t java | head -30Repository: IT-Cotato/12th-OnGil-BE Length of output: 4010 🏁 Script executed: cat -n src/main/java/com/ongil/backend/global/config/SecurityConfig.javaRepository: IT-Cotato/12th-OnGil-BE Length of output: 4867 🏁 Script executed: # Read ReviewController to check all write endpoints and auth patterns
cat -n src/main/java/com/ongil/backend/domain/review/controller/ReviewController.javaRepository: IT-Cotato/12th-OnGil-BE Length of output: 9864 이미지 업로드 엔드포인트에서 Spring Security의 catch-all 규칙( 💡 수정 제안 public DataResponse<List<String>> uploadReviewImages(
+ `@AuthenticationPrincipal` Long userId,
`@RequestPart`("images") List<MultipartFile> images
) {🤖 Prompt for AI Agents |
||
|
|
||
| @Operation(summary = "리뷰 최종 제출", description = "최종 리뷰 문장들과 사진을 저장하고 상태를 COMPLETED로 변경합니다.") | ||
| @PostMapping("/api/reviews/{reviewId}/submit") | ||
| public DataResponse<Void> submitReview( | ||
| @AuthenticationPrincipal Long userId, | ||
| @PathVariable Long reviewId, | ||
| @Valid @RequestBody ReviewFinalSubmitRequest request | ||
| ) { | ||
| reviewCommandService.submitReview(userId, reviewId, request); | ||
| return DataResponse.ok(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,7 +33,8 @@ public ReviewListResponse toListResponse(Review review, boolean isHelpful) { | |||||||||||||||||||||||||||||||||||
| .rating(review.getRating()) | ||||||||||||||||||||||||||||||||||||
| .helpfulCount(review.getHelpfulCount()) | ||||||||||||||||||||||||||||||||||||
| .isHelpful(isHelpful) | ||||||||||||||||||||||||||||||||||||
| .aiGeneratedReview(review.getAiGeneratedReview()) | ||||||||||||||||||||||||||||||||||||
| .sizeReview(parseToList(review.getSizeReview(), "\n")) | ||||||||||||||||||||||||||||||||||||
| .materialReview(parseToList(review.getMaterialReview(), "\n")) | ||||||||||||||||||||||||||||||||||||
| .textReview(review.getTextReview()) | ||||||||||||||||||||||||||||||||||||
| .reviewImageUrls(parseImageUrls(review.getReviewImageUrls())) | ||||||||||||||||||||||||||||||||||||
| .reviewer(buildReviewerInfo(user, review.getProduct())) | ||||||||||||||||||||||||||||||||||||
|
|
@@ -66,9 +67,9 @@ private ReviewListResponse.PurchaseOption buildPurchaseOption(OrderItem orderIte | |||||||||||||||||||||||||||||||||||
| // 선택지 답변 요약 구성 | ||||||||||||||||||||||||||||||||||||
| private ReviewListResponse.AnswerSummary buildAnswerSummary(Review review) { | ||||||||||||||||||||||||||||||||||||
| return ReviewListResponse.AnswerSummary.builder() | ||||||||||||||||||||||||||||||||||||
| .sizeAnswer(review.getSizeAnswer()) | ||||||||||||||||||||||||||||||||||||
| .colorAnswer(review.getColorAnswer()) | ||||||||||||||||||||||||||||||||||||
| .materialAnswer(review.getMaterialAnswer()) | ||||||||||||||||||||||||||||||||||||
| .sizeAnswer(review.getSizeAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .colorAnswer(review.getColorAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .materialAnswer(review.getMaterialAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
68
to
74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 70-72에서 COMPLETED 상태의 리뷰만 조회된다면 문제없겠지만, 데이터 마이그레이션이나 스케줄러의 DRAFT 삭제 타이밍 등 엣지 케이스에서 null이 들어올 수 있습니다. 🛡️ null-safe 처리 예시 private ReviewListResponse.AnswerSummary buildAnswerSummary(Review review) {
return ReviewListResponse.AnswerSummary.builder()
- .sizeAnswer(review.getSizeAnswer().getDisplayName())
- .colorAnswer(review.getColorAnswer().getDisplayName())
- .materialAnswer(review.getMaterialAnswer().getDisplayName())
+ .sizeAnswer(review.getSizeAnswer() != null ? review.getSizeAnswer().getDisplayName() : null)
+ .colorAnswer(review.getColorAnswer() != null ? review.getColorAnswer().getDisplayName() : null)
+ .materialAnswer(review.getMaterialAnswer() != null ? review.getMaterialAnswer().getDisplayName() : null)
.build();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -86,7 +87,8 @@ public ReviewDetailResponse toDetailResponse(Review review, boolean isHelpful) { | |||||||||||||||||||||||||||||||||||
| .rating(review.getRating()) | ||||||||||||||||||||||||||||||||||||
| .helpfulCount(review.getHelpfulCount()) | ||||||||||||||||||||||||||||||||||||
| .isHelpful(isHelpful) | ||||||||||||||||||||||||||||||||||||
| .aiGeneratedReview(review.getAiGeneratedReview()) | ||||||||||||||||||||||||||||||||||||
| .sizeReview(parseToList(review.getSizeReview(), "\n")) | ||||||||||||||||||||||||||||||||||||
| .materialReview(parseToList(review.getMaterialReview(), "\n")) | ||||||||||||||||||||||||||||||||||||
| .textReview(review.getTextReview()) | ||||||||||||||||||||||||||||||||||||
| .reviewImageUrls(parseImageUrls(review.getReviewImageUrls())) | ||||||||||||||||||||||||||||||||||||
| .reviewer(buildDetailReviewerInfo(user)) | ||||||||||||||||||||||||||||||||||||
|
|
@@ -138,9 +140,9 @@ private ReviewDetailResponse.ProductInfo buildProductInfo(Product product) { | |||||||||||||||||||||||||||||||||||
| // 1차 리뷰 답변 구성 | ||||||||||||||||||||||||||||||||||||
| private ReviewDetailResponse.InitialFirstAnswers buildInitialFirstAnswers(Review review) { | ||||||||||||||||||||||||||||||||||||
| return ReviewDetailResponse.InitialFirstAnswers.builder() | ||||||||||||||||||||||||||||||||||||
| .sizeAnswer(review.getSizeAnswer()) | ||||||||||||||||||||||||||||||||||||
| .colorAnswer(review.getColorAnswer()) | ||||||||||||||||||||||||||||||||||||
| .materialAnswer(review.getMaterialAnswer()) | ||||||||||||||||||||||||||||||||||||
| .sizeAnswer(review.getSizeAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .colorAnswer(review.getColorAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .materialAnswer(review.getMaterialAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -172,7 +174,8 @@ public MyReviewResponse toMyReviewResponse(Review review) { | |||||||||||||||||||||||||||||||||||
| .reviewType(review.getReviewType().name()) | ||||||||||||||||||||||||||||||||||||
| .rating(review.getRating()) | ||||||||||||||||||||||||||||||||||||
| .helpfulCount(review.getHelpfulCount()) | ||||||||||||||||||||||||||||||||||||
| .aiGeneratedReview(review.getAiGeneratedReview()) | ||||||||||||||||||||||||||||||||||||
| .sizeReview(parseToList(review.getSizeReview(), "\n")) | ||||||||||||||||||||||||||||||||||||
| .materialReview(parseToList(review.getMaterialReview(), "\n")) | ||||||||||||||||||||||||||||||||||||
| .textReview(review.getTextReview()) | ||||||||||||||||||||||||||||||||||||
| .reviewImageUrls(parseImageUrls(review.getReviewImageUrls())) | ||||||||||||||||||||||||||||||||||||
| .product(buildMyReviewProductInfo(product)) | ||||||||||||||||||||||||||||||||||||
|
|
@@ -202,9 +205,9 @@ private MyReviewResponse.ProductInfo buildMyReviewProductInfo(Product product) { | |||||||||||||||||||||||||||||||||||
| // 내가 작성한 리뷰 - 선택지 답변 요약 구성(2차 답변도 포함) | ||||||||||||||||||||||||||||||||||||
| private MyReviewResponse.AnswerSummary buildMyReviewAnswerSummary(Review review) { | ||||||||||||||||||||||||||||||||||||
| return MyReviewResponse.AnswerSummary.builder() | ||||||||||||||||||||||||||||||||||||
| .sizeAnswer(review.getSizeAnswer()) | ||||||||||||||||||||||||||||||||||||
| .colorAnswer(review.getColorAnswer()) | ||||||||||||||||||||||||||||||||||||
| .materialAnswer(review.getMaterialAnswer()) | ||||||||||||||||||||||||||||||||||||
| .sizeAnswer(review.getSizeAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .colorAnswer(review.getColorAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .materialAnswer(review.getMaterialAnswer().getDisplayName()) | ||||||||||||||||||||||||||||||||||||
| .fitIssueParts(review.getFitIssueParts()) | ||||||||||||||||||||||||||||||||||||
| .materialFeatures(review.getMaterialFeatures()) | ||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||
|
|
@@ -278,4 +281,15 @@ private String formatPurchaseOption(OrderItem orderItem) { | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private List<String> parseToList(String content, String delimiter) { | ||||||||||||||||||||||||||||||||||||
| if (content == null || content.isBlank()) { | ||||||||||||||||||||||||||||||||||||
| return Collections.emptyList(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return Arrays.stream(content.split(delimiter)) | ||||||||||||||||||||||||||||||||||||
| .map(String::trim) | ||||||||||||||||||||||||||||||||||||
| .filter(s -> !s.isEmpty()) | ||||||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: IT-Cotato/12th-OnGil-BE
Length of output: 819
🏁 Script executed:
Repository: IT-Cotato/12th-OnGil-BE
Length of output: 131
🏁 Script executed:
Repository: IT-Cotato/12th-OnGil-BE
Length of output: 49
🏁 Script executed:
# Read Order entity to check orderNumber field and constraints cat -n src/main/java/com/ongil/backend/domain/order/entity/Order.javaRepository: IT-Cotato/12th-OnGil-BE
Length of output: 4037
🏁 Script executed:
Repository: IT-Cotato/12th-OnGil-BE
Length of output: 273
🏁 Script executed:
Repository: IT-Cotato/12th-OnGil-BE
Length of output: 225
🏁 Script executed:
Repository: IT-Cotato/12th-OnGil-BE
Length of output: 2142
🏁 Script executed:
# Check application.yml for any relevant config cat src/main/resources/application.ymlRepository: IT-Cotato/12th-OnGil-BE
Length of output: 1243
주문번호 생성의 고유성 위반 위험: 데이터베이스 제약 충돌 발생 가능
UUID(128비트)를 hex 10자(40비트)로 절삭하면 약 1조 개 조합만 남습니다. Order 엔티티에
@Column(unique = true)제약이 설정되어 있으므로, 약 3천만 건의 주문이 누적되면 충돌 확률이 50%에 도달하고, 이는DataIntegrityViolationException으로 이어져 주문 생성이 실패합니다. 현재 코드에는 충돌 시 재시도 로직이 없으므로 프로덕션 환경에서 안정성 문제가 됩니다.개선 방안:
🤖 Prompt for AI Agents