Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ public ResponseEntity<DataResponse<TokenRefreshResDto>> refresh(
return ResponseEntity.ok(DataResponse.from(res));
}



@PostMapping("/logout")
@Operation(summary = "로그아웃 API", description = "Redis에 저장된 리프레시 토큰을 삭제하여 로그아웃 처리")
public ResponseEntity<DataResponse<String>> logout(
Expand All @@ -93,4 +91,5 @@ public ResponseEntity<DataResponse<String>> withdraw(
authService.withdraw(userId);
return ResponseEntity.ok(DataResponse.from("회원 탈퇴가 완료되었습니다."));
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.ongil.backend.domain.order.converter;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;

Expand Down Expand Up @@ -31,9 +29,11 @@ public class OrderConverter {

// Request -> Order 엔티티
public Order toOrder(OrderCreateRequest request, User user, int finalAmount) {
String datePart = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String randomPart = UUID.randomUUID().toString().substring(0, 8);
String orderNumber = "ORD-" + datePart + "-" + randomPart;
String orderNumber = UUID.randomUUID()
.toString()
.replace("-", "")
.substring(0, 10)
.toUpperCase();
Comment on lines +32 to +36
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# 1. OrderConverter.java 파일 확인 및 라인 32-36 검증
cat -n src/main/java/com/ongil/backend/domain/order/converter/OrderConverter.java | sed -n '25,45p'

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 819


🏁 Script executed:

# 2. Order 엔티티 조회 - orderNumber 컬럼 정의 및 unique 제약 확인
find . -type f -name "Order.java" | grep -E "entity|domain" | head -5

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 131


🏁 Script executed:

# 3. 데이터베이스 마이그레이션/스키마 파일 검색
fd -t f "\.sql$|\.xml$" | grep -iE "schema|migration|flyway|liquibase" | head -20

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.java

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 4037


🏁 Script executed:

# Search for database schema/JPA annotations related to orderNumber constraint
rg -n "orderNumber|order_number" src/main/java/com/ongil/backend/domain/order/entity/Order.java -A 3 -B 1

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 273


🏁 Script executed:

# Look for application properties or schema generation config
find . -type f \( -name "application*.yml" -o -name "application*.properties" -o -name "*.sql" \) | head -10

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 225


🏁 Script executed:

# Check if there's any retry logic or exception handling in OrderController/OrderService
rg -n "orderNumber|OrderConverter" src/main/java/com/ongil/backend/domain/order/service/ -A 5 -B 2 | head -50

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 2142


🏁 Script executed:

# Check application.yml for any relevant config
cat src/main/resources/application.yml

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 1243


주문번호 생성의 고유성 위반 위험: 데이터베이스 제약 충돌 발생 가능

UUID(128비트)를 hex 10자(40비트)로 절삭하면 약 1조 개 조합만 남습니다. Order 엔티티에 @Column(unique = true) 제약이 설정되어 있으므로, 약 3천만 건의 주문이 누적되면 충돌 확률이 50%에 도달하고, 이는 DataIntegrityViolationException으로 이어져 주문 생성이 실패합니다. 현재 코드에는 충돌 시 재시도 로직이 없으므로 프로덕션 환경에서 안정성 문제가 됩니다.

개선 방안:

  • UUID 전체(32자) 또는 더 긴 substring 사용
  • DB sequence 기반 주문번호 생성
  • 충돌 발생 시 재시도 로직 추가 (제한된 횟수)
🤖 Prompt for AI Agents
In `@src/main/java/com/ongil/backend/domain/order/converter/OrderConverter.java`
around lines 32 - 36, The current OrderConverter uses
UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase() to
build orderNumber which is too short and risks unique-constraint collisions;
update OrderConverter to stop truncating to 10 chars (use the full 32-char hex
string or a longer substring) OR implement a bounded retry safe-generation: in
the orderNumber generation path (the code around orderNumber, UUID.randomUUID,
replace, substring) loop to regenerate and check uniqueness via the Order
repository (e.g., existsByOrderNumber) with a limited number of attempts and
throw a clear exception if exceeded; ensure the final chosen approach removes
the unsafe substring(0, 10) usage or adds the retry+repository check to prevent
DataIntegrityViolationException.


return Order.builder()
.orderNumber(orderNumber)
Expand All @@ -44,7 +44,7 @@ public Order toOrder(OrderCreateRequest request, User user, int finalAmount) {
.detailAddress(request.detailAddress())
.postalCode(request.postalCode())
.deliveryMessage(request.deliveryMessage())
.orderStatus(OrderStatus.ORDER_RECEIVED)
.orderStatus(OrderStatus.CONFIRMED)
.user(user)
.build();
}
Expand Down Expand Up @@ -110,6 +110,7 @@ public OrderItemDto toOrderItemDto(OrderItem orderItem) {
String brandName = product.getBrand() != null ? product.getBrand().getName() : "일반 브랜드";

return new OrderItemDto(
orderItem.getId(),
product.getId(),
brandName,
product.getName(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ongil.backend.domain.order.dto.response;

public record OrderItemDto(
Long orderItemId,
Long productId,
String brandName,
String productName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@
@Getter
@RequiredArgsConstructor
public enum OrderStatus {
ORDER_RECEIVED("주문 접수"),
SHIPPING("배송 중"),
DELIVERED("배송 완료"),
CONFIRMED("구매 확정"),
CANCELED("취소");

private final String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public OrderCancelResponse cancelOrder(Long userId, Long orderId, OrderCancelReq
public OrderDetailResponse updateDeliveryAddress(Long userId, Long orderId, DeliveryAddressUpdateRequest request) {
Order order = getOrderAndValidateOwner(userId, orderId);

if (order.getOrderStatus() != OrderStatus.ORDER_RECEIVED) {
if (order.getOrderStatus() != OrderStatus.CONFIRMED) {
throw new AppException(ErrorCode.ORDER_UPDATE_NOT_ALLOWED);
}

Expand Down Expand Up @@ -251,7 +251,7 @@ private void validateCancelable(Order order) {
if (order.getOrderStatus() == OrderStatus.CANCELED) {
throw new AppException(ErrorCode.ORDER_ALREADY_CANCELED);
}
if (order.getOrderStatus() != OrderStatus.ORDER_RECEIVED) {
if (order.getOrderStatus() != OrderStatus.CONFIRMED) {
throw new AppException(ErrorCode.ORDER_CANCEL_NOT_ALLOWED);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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();
Expand All @@ -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();
}
Comment thread
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ReviewController.java" -type f

Repository: 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 -20

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 600


🏁 Script executed:

# /api/reviews 경로에 대한 보안 설정 확인
rg "reviews" -t java | head -30

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 4010


🏁 Script executed:

cat -n src/main/java/com/ongil/backend/global/config/SecurityConfig.java

Repository: 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.java

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 9864


이미지 업로드 엔드포인트에서 userId 파라미터가 누락되었습니다.

Spring Security의 catch-all 규칙(.anyRequest().authenticated())으로 경로가 보호되고 있으나, 컨트롤러 메서드에 @AuthenticationPrincipal Long userId 파라미터가 없습니다. 이로 인해 인증된 사용자를 구분할 수 없으며, 다른 모든 write 엔드포인트(/api/reviews/init, /api/reviews/{reviewId}/step1 등)와 일관성이 깨집니다. 업로드된 이미지를 사용자와 연결하려면 반드시 필요합니다.

💡 수정 제안
 public DataResponse<List<String>> uploadReviewImages(
+    `@AuthenticationPrincipal` Long userId,
     `@RequestPart`("images") List<MultipartFile> images
 ) {
🤖 Prompt for AI Agents
In
`@src/main/java/com/ongil/backend/domain/review/controller/ReviewController.java`
around lines 176 - 189, The uploadReviewImages method lacks the authenticated
user id parameter; update the controller method uploadReviewImages to accept
`@AuthenticationPrincipal` Long userId and pass that userId into the S3 upload
flow (use the appropriate s3ImageService.uploadReviewImage overload or update it
to accept userId and the MultipartFile) so uploaded images are associated with
the authenticated user, and keep the same validation (max 5) and return behavior
as before to maintain consistency with other endpoints like init/step1.


@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
Expand Up @@ -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()))
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

getDisplayName() 호출 시 Enum 필드가 null이면 NPE 발생

Line 70-72에서 review.getSizeAnswer(), getColorAnswer(), getMaterialAnswer()가 null인 경우 NPE가 발생합니다. 동일한 패턴이 buildInitialFirstAnswers (Line 143-145)와 buildMyReviewAnswerSummary (Line 208-210)에도 반복됩니다.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
}
private ReviewListResponse.AnswerSummary buildAnswerSummary(Review review) {
return ReviewListResponse.AnswerSummary.builder()
.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();
}
🤖 Prompt for AI Agents
In `@src/main/java/com/ongil/backend/domain/review/converter/ReviewConverter.java`
around lines 68 - 74, The three methods buildAnswerSummary,
buildInitialFirstAnswers, and buildMyReviewAnswerSummary call getDisplayName()
on enum fields (sizeAnswer, colorAnswer, materialAnswer) which can be null and
cause NPEs; update each method to null-safe access by checking each
review.getSizeAnswer()/getColorAnswer()/getMaterialAnswer() before calling
getDisplayName() and pass a safe default (e.g., null or a constant like
"UNKNOWN" or empty string) into the ReviewListResponse.AnswerSummary builder
when the enum is null (use ternary/Optional or helper method to map enum ->
display string safely), ensuring every builder field (sizeAnswer, colorAnswer,
materialAnswer) is populated without invoking getDisplayName() on a null
reference.


Expand All @@ -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))
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}

}
Loading