-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] AI 피팅 기능 구현 #44
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
Conversation
WalkthroughS3 업로드 및 AI 피팅 기능을 추가했다. AWS S3 presigned URL 발급, Gemini 이미지 생성 연동, Redis 기반 토큰 버킷 크레딧 검사/차감(Lua 스크립트 + Redisson), 관련 도메인/서비스/레포지토리/컨트롤러/DTO/Swagger를 새로 도입했고 serializer 패키지 재배치, 설정·CI 환경변수 확장도 포함한다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Controller as FitController
participant Facade as FitFacade
participant ImgSvc as FittingSourceImageService
participant S3 as S3ImageApi
User->>Controller: POST /fit/source-images/upload-url
Controller->>Facade: createUploadUrl(userId)
Facade->>ImgSvc: createPendingImage(userId)
Facade->>S3: generatePresignedUrl(s3Key)
S3-->>Facade: S3PresignedUrlInfo
Facade->>ImgSvc: persist s3Key on image
Facade-->>Controller: FittingSourceImageUploadInfo
Controller-->>User: 200 OK (upload URL)
User->>Controller: PUT /fit/source-images/{imageId}
Controller->>Facade: completeImageUpload(imageId, userId)
Facade->>ImgSvc: getFittingSourceImage(imageId, userId)
Facade->>S3: getImageUrl(s3Key)
Facade->>ImgSvc: completeImageUpload(image, imageUrl)
Controller-->>User: 204 No Content
sequenceDiagram
autonumber
actor User
participant Controller as FitController
participant Facade as FitFacade
participant Credit as CreditService
participant Gemini as GeminiImageApi
participant Redis as Redisson/Lua
participant Http as External HTTP
User->>Controller: POST /fit/ai-fitting (sourceUrl, clothingUrl)
Controller->>Facade: generateAiFitting(userId, sourceUrl, clothingUrl)
Facade->>Credit: executeWithCreditCheck(userId, action)
Credit->>Redis: acquire lock (user)
alt lock acquired
Credit->>Redis: eval credit_check.lua(now)
alt credit available
Facade->>Gemini: generateAiFitting(source, clothing)
Gemini->>Http: GET source image, GET clothing image
Http-->>Gemini: 200 images
Gemini->>Http: POST Gemini generateContent
Http-->>Gemini: 200 response (candidates)
Gemini-->>Facade: generated image ByteArray
Credit->>Redis: eval credit_deduct.lua(now)
Facade-->>Controller: 200 OK (image/png)
Controller-->>User: image/png
else no credit
Credit-->>Facade: throw rate-limit error
Controller-->>User: error (429/4xx)
end
else lock denied
Credit-->>Facade: throw lock error
Controller-->>User: error (429/4xx)
end
Credit->>Redis: unlock (finally)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/resources/application-prod.yaml (1)
60-81: 필수 환경 변수 명 불일치로 부팅 실패 발생
${AWS_S3_BUCKET}/${AWS_REGION}플레이스홀더는 컨테이너에 주입되지 않아 Spring 부팅 단계에서Could not resolve placeholder예외가 발생합니다. 현재 배포 워크플로우는CLOUD_AWS_S3_BUCKET/CLOUD_AWS_REGION_STATIC만 전달하므로, 설정 키와 환경 변수 명을 일치시켜 주세요.- bucket: ${AWS_S3_BUCKET} + bucket: ${CLOUD_AWS_S3_BUCKET} ... - static: ${AWS_REGION} + static: ${CLOUD_AWS_REGION_STATIC}
🧹 Nitpick comments (22)
src/main/kotlin/com/dh/baro/look/application/dto/AiFittingInfo.kt (1)
3-7: ByteArray 가변성으로 인한 동등성/해시 불안정 가능성 — 방어적 복사 또는 사용 제약 명시 권장generatedImageData는 가변(ByteArray)이라 외부에서 내용 변경 시 equals/hashCode 결과가 달라질 수 있어, 컬렉션 키/Set 원소로 사용 시 심각한 버그가 발생할 수 있습니다. 방어적 복사(생성 시 copyOf) 또는 팩토리 메서드로 복사 생성, 최소한 Javadoc/KDoc에 “외부에서 변형 금지”를 명시하는 것을 권장합니다. toString()에서 바이트 내용을 노출하지 않도록 마스킹도 고려해 주세요.
가능한 toString() 보강 예시(파일 내 임의 위치 추가):
override fun toString(): String = "AiFittingInfo(sourceImageUrl=$sourceImageUrl, clothingImageUrl=$clothingImageUrl, generatedImageData=<${generatedImageData.size} bytes>)"src/main/kotlin/com/dh/baro/look/domain/repository/FittingSourceImageRepository.kt (1)
8-8: 페이징/상한 없는 전체 조회 — Pageable/limit 메서드 추가 권장 및 인덱스 확인OrderByIdDesc로 전체 List 반환은 사용자별 데이터가 많아질 경우 메모리/성능 이슈가 있습니다. Pageable 버전 또는 findFirstBy…OrderByIdDesc 추가를 권장합니다. 또한 (user_id, upload_status) 복합 인덱스를 DDL에 보장해 주세요.
예시:
fun findByUserIdAndUploadStatusOrderByIdDesc( userId: Long, uploadStatus: FittingSourceImageStatus, pageable: Pageable ): Page<FittingSourceImage> fun findFirstByUserIdAndUploadStatusOrderByIdDesc( userId: Long, uploadStatus: FittingSourceImageStatus ): FittingSourceImage?src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiResponse.kt (1)
3-17: 외부 API 스키마 변화 내성 확보 — 알 수 없는 필드 무시 설정 권장응답 필드가 가변적인 특성상 @JsonIgnoreProperties(ignoreUnknown = true)를 부여해 방어적으로 역직렬화하는 것을 권장합니다.
적용 diff(주요 선언부에 어노테이션 추가):
data class GeminiApiResponse( - val candidates: List<GeminiCandidate>?, - val usageMetadata: GeminiUsageMetadata?, -) + val candidates: List<GeminiCandidate>?, + val usageMetadata: GeminiUsageMetadata?, +) @@ -data class GeminiCandidate( +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +data class GeminiCandidate( val content: GeminiContent?, val finishReason: String?, ) @@ -data class GeminiUsageMetadata( +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +data class GeminiUsageMetadata( val promptTokenCount: Int?, val candidatesTokenCount: Int?, val totalTokenCount: Int?, )추가로 파일 상단에 import를 두고 싶다면:
import com.fasterxml.jackson.annotation.JsonIgnorePropertiessrc/main/kotlin/com/dh/baro/look/domain/service/CreditService.kt (1)
29-38: 신용 차감 타이밍과 사용자 경험성공 후 차감(현재 방식)은 실패 시 “작업은 됐는데 차감 실패로 에러 반환” 가능성이 있습니다. 반대로 선차감은 실패 시 보상(환급) 로직이 필요합니다. 요구사항에 따라:
- 워치독 기반으로 전체 구간을 단일 임계영역으로 유지(현재 권장 수정과 조합)하여 체크→실행→차감의 원자성을 최대화
- 또는 RRateLimiter/원자적 Lua(체크+차감)로 ‘선차감-실패 시 롤백’ 패턴 구축
src/main/kotlin/com/dh/baro/look/domain/service/FittingSourceImageService.kt (2)
19-26: 조회 가독성 개선: findByIdOrNull 사용 제안Optional.orElse(null) 패턴 대신 Kotlin 확장인 findByIdOrNull이 더 간결합니다.
- val image = fittingSourceImageRepository.findById(imageId).orElse(null) - ?: throw IllegalArgumentException(ErrorMessage.FITTING_SOURCE_IMAGE_NOT_FOUND.format(imageId)) + import org.springframework.data.repository.findByIdOrNull + val image = fittingSourceImageRepository.findByIdOrNull(imageId) + ?: throw IllegalArgumentException(ErrorMessage.FITTING_SOURCE_IMAGE_NOT_FOUND.format(imageId))
22-24: 예외 타입 일관성require/requireNotNull는 IllegalArgumentException을 던집니다. 상위 계층에서 도메인 예외 구분이 필요하다면 도메인 전용 예외(또는 ErrorMessage 기반 예외)로 맞추는 편이 좋습니다.
src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiImageApi.kt (2)
50-57: 이미지 MIME 타입 하드코딩입력 이미지 타입을 모두 image/jpeg로 고정하고 있습니다. 실제 타입(PNG/WebP 등)에 맞춰 MIME을 세팅하거나 최소한 "image/*"로 포괄 지정하는 편이 안전합니다.
- GeminiImagePart(GeminiInlineData("image/jpeg", clothingImageData)), - GeminiImagePart(GeminiInlineData("image/jpeg", sourceImageData)), + GeminiImagePart(GeminiInlineData("image/*", clothingImageData)), + GeminiImagePart(GeminiInlineData("image/*", sourceImageData)),또는 호출부에서 실제 Content-Type을 받아와 반영하는 API로 확장하는 방안을 고려해주세요.
16-16: RestClient 구성 일원화 및 헤더 기본값 설정 제안API 키 헤더와 타임아웃을 RestClient 빌더로 한 번만 구성하면 중복/누락을 줄일 수 있습니다.
예: RestClient.builder().baseUrl(geminiApiUrl).defaultHeader("x-goog-api-key", geminiApiKey) ... 로 생성한 빈을 주입.
src/main/kotlin/com/dh/baro/look/domain/FittingSourceImage.kt (1)
41-48: 세터에 입력값 검증 추가 제안s3Key는 빈 문자열/공백을 거부하도록 최소 검증을 추가하면 데이터 정합성이 좋아집니다.
- fun setS3Key(s3Key: String) { - this.s3Key = s3Key - } + fun setS3Key(s3Key: String) { + require(s3Key.isNotBlank()) { "s3Key must not be blank" } + this.s3Key = s3Key + }src/main/kotlin/com/dh/baro/look/infra/redis/CreditRepositoryImpl.kt (1)
44-61: READ_ONLY 모드 가능 여부 검토checkCreditAvailability가 쓰기 부작용이 없다면 RScript.Mode.READ_ONLY로 내리는 편이 안전합니다.
스크립트가 TTL 갱신 등 쓰기를 수행한다면 현 상태 유지가 맞습니다. 확인 부탁드립니다.
src/main/kotlin/com/dh/baro/look/infra/s3/S3ImageApi.kt (3)
26-43: S3Presigner 자원 안전하게 닫기예외 시 close가 보장되지 않습니다. use 블록으로 자원 관리를 명확히 하세요.
- val s3Presigner = S3Presigner.builder() - .region(Region.of(region)) - .build() + val presignedRequest = S3Presigner.builder() + .region(Region.of(region)) + .build() + .use { s3Presigner -> + val putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build() + + val presignRequest = PutObjectPresignRequest.builder() + .putObjectRequest(putObjectRequest) + .signatureDuration(duration) + .build() + + s3Presigner.presignPutObject(presignRequest) + } - - val putObjectRequest = PutObjectRequest.builder() - .bucket(bucketName) - .key(s3Key) - .build() - - val presignRequest = PutObjectPresignRequest.builder() - .putObjectRequest(putObjectRequest) - .signatureDuration(duration) - .build() - - val presignedRequest = s3Presigner.presignPutObject(presignRequest) - - s3Presigner.close()
54-58: 키 확장자(.jpg) 고정 vs 허용 MIME 목록 불일치S3 키를 .jpg로 고정하면서 allowedTypes엔 png/webp가 포함되어 혼선을 유발합니다. 실제 업로드 타입과 키 확장자를 정렬하거나, 확장자를 제거/범용화하는 것이 좋습니다.
- 모든 업로드가 JPEG임을 보장합니까? 아니라면 generatePresignedUrl에 contentType(또는 확장자) 인자를 추가하고 PutObjectRequest에 .contentType(...)을 지정해 강제하는 방안을 제안합니다.
- 아니면 allowedTypes를 JPEG로만 제한하세요.
예시(시그니처 변경 필요):
- fun generatePresignedUrl(imageId: Long): S3PresignedUrlInfo { + fun generatePresignedUrl(imageId: Long, contentType: String): S3PresignedUrlInfo { - val s3Key = generateS3Key(imageId) + val s3Key = generateS3Key(imageId, contentType) @@ - val putObjectRequest = PutObjectRequest.builder() + val putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) .key(s3Key) + .contentType(contentType) .build() @@ - private fun generateS3Key(imageId: Long): String { + private fun generateS3Key(imageId: Long, contentType: String): String { val timestamp = System.currentTimeMillis() val uuid = UUID.randomUUID().toString().substring(0, 8) - return "fitting-source-images/$imageId/$timestamp-$uuid.jpg" + val ext = when (contentType.lowercase()) { + "image/jpeg", "image/jpg" -> "jpg" + "image/png" -> "png" + "image/webp" -> "webp" + else -> "bin" + } + return "fitting-source-images/$imageId/$timestamp-$uuid.$ext" }Also applies to: 64-73
44-52: 콘텐츠 타입·사이즈 정책 집행 한계 안내PUT presign은 파일 크기 제한을 강제하지 못합니다. 서버 측 업로드 완료 후 S3 객체 메타데이터(Content-Type/Content-Length) 검증과 초과 시 삭제 로직을 운영에 추가하세요.
src/main/kotlin/com/dh/baro/look/application/FitFacade.kt (2)
26-27: Kotlin 스타일 속성 접근 일관화(get/setS3Key → 속성 접근) 검토도메인이 Kotlin 클래스라면
image.s3Key/image.s3Key = ...형태의 속성 접근이 가독성과 일관성에 더 좋습니다. 이전 PR에서 선호하신 “불변 id는 게터 대신 필드 접근” 컨벤션과도 부합합니다.현재 FittingSourceImage가 Kotlin 속성을 노출한다면 아래처럼 정리해 주세요.
pendingImage.setS3Key(s3Info.s3Key)→pendingImage.s3Key = s3Info.s3Keyimage.getS3Key()→image.s3KeyAlso applies to: 40-40
52-54: 외부 호출의 안정성(타임아웃/차단 시간) 점검크레딧 체크 임계 구간에서 외부(GenAI) 호출을 동기 블록으로 수행하면 지연 시 크레딧 리소스 점유가 길어질 수 있습니다. HTTP 타임아웃, 재시도/백오프, 서킷브레이커 적용 여부를 확인해 주세요.
- geminiImageApi 내부 HTTP 클라이언트에 연결/읽기/전체 타임아웃 설정
- 재시도는 멱등성 보장되는 범위에서만 적용
- 실패 시 크레딧 복구/롤백 시맨틱 확인(예약→성공 시 확정, 실패 시 자동 복구)
src/main/kotlin/com/dh/baro/look/presentation/FitController.kt (4)
44-45: 응답 콘텐츠 타입을 매핑에 선언(produces)해 문서/호환성 개선이미지 바이트를 반환하므로 produces를 명시해 주세요.
- @PostMapping("/ai-fitting") + @PostMapping("/ai-fitting", produces = [MediaType.IMAGE_PNG_VALUE])
44-49: Swagger 인터페이스와 시그니처 정합성 확보(override 추가)FitSwagger에 AI 피팅 API가 누락되어 문서화에서 빠집니다. Swagger 인터페이스에 메서드를 추가하고 컨트롤러에서 override로 구현하세요. 우선 컨트롤러 메서드에 override를 붙여 정합성을 맞춰 주세요.
- @PostMapping("/ai-fitting", produces = [MediaType.IMAGE_PNG_VALUE]) - fun generateAiFitting( + @PostMapping("/ai-fitting", produces = [MediaType.IMAGE_PNG_VALUE]) + override fun generateAiFitting( @CurrentUser userId: Long, @Valid @RequestBody request: AiFittingRequest, ): ResponseEntity<ByteArray> {주의: FitSwagger.kt에 해당 시그니처와 문서가 추가되어야 합니다(아래 코멘트 참고).
45-53: 사용자 입력 URL의 출처 제한(SSRF/임의 외부 URL 사용) 검증
sourceImageUrl/clothingImageUrl가 임의 외부 URL이면 SSRF·라이선스·콘텐츠 안전성 문제가 생길 수 있습니다. 우리 S3 버킷(또는 허용 도메인)로만 제한하는 검증을 DTO/Validator에서 적용하세요.
- AiFittingRequest에 허용 도메인 화이트리스트(예: 정규식으로 our‑bucket.s3..amazonaws.com) 검증
- 필요 시 서버가 직접 바이트를 읽지 않도록(외부 다운로드 방지) 설계 확인
55-57: 대용량 이미지 대비 스트리밍 응답 고려메모리에 통째로 적재하는 ByteArray 대신 ByteArrayResource/InputStreamResource 사용을 검토하세요.
예시:
return ResponseEntity.ok() .contentType(MediaType.IMAGE_PNG) .body(ByteArrayResource(fittingResult.generatedImageData))src/main/kotlin/com/dh/baro/look/presentation/swagger/FitSwagger.kt (2)
266-269: AI 피팅 엔드포인트 문서화 누락 추가컨트롤러에 존재하는
/fit/ai-fittingAPI가 문서에서 빠져 있습니다. 아래처럼 메서드를 추가해 주세요.fun getUserFittingSourceImages( @Parameter(hidden = true) userId: Long ): FittingSourceImageListResponse -} + + /* ───────────────────────────── AI 피팅 생성 ───────────────────────────── */ + @Operation( + summary = "AI 피팅 이미지 생성", + description = """ + 업로드된 소스 이미지와 의류 이미지를 사용해 AI 피팅 이미지를 생성합니다. + 요청은 JSON으로 받고, 응답은 image/png 바이너리입니다. + """, + responses = [ + ApiResponse( + responseCode = "200", description = "생성 성공", + content = [Content( + mediaType = "image/png", + schema = Schema(type = "string", format = "binary") + )] + ), + ApiResponse( + responseCode = "401", + description = "미인증", + content = [Content(mediaType = APPLICATION_JSON_VALUE, schema = Schema(implementation = ErrorResponse::class))] + ), + ApiResponse( + responseCode = "429", + description = "크레딧 부족 또는 요금제 제한", + content = [Content(mediaType = APPLICATION_JSON_VALUE, schema = Schema(implementation = ErrorResponse::class))] + ) + ] + ) + fun generateAiFitting( + @Parameter(hidden = true) userId: Long, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + content = [Content(schema = Schema(implementation = AiFittingRequest::class), mediaType = APPLICATION_JSON_VALUE)] + ) + request: AiFittingRequest + ): ByteArray +}
15-21: 문서 정의 방식 일관성 확인(인터페이스에 매핑 부재)LookSwagger는 인터페이스에 @PostMapping 등 매핑을 포함하는 반면, FitSwagger는 매핑이 컨트롤러에만 있습니다. springdoc 설정에 따라 인터페이스의 @operation만으로는 병합이 안 될 수 있습니다. 두 가지 중 하나로 통일해 주세요.
옵션:
- 인터페이스에 @RequestMapping("/fit") 및 각 메서드에 @PostMapping/@PutMapping/@GetMapping 추가
- 혹은 컨트롤러 메서드에 @operation을 직접 선언해 인터페이스 의존 제거
src/main/resources/lua/credit_deduct.lua (1)
2-2: 사용하지 않는 인자 정리 제안
ARGV[1]에서 변환한current_time이 이후 로직에서 전혀 쓰이지 않아 혼동을 줄이도록 제거하거나 사용할 로직을 추가하는 편이 좋겠습니다.스크립트 호출부에서 인자를 계속 전달하더라도 아래처럼 불필요한 로컬 변수를 제거할 수 있습니다.
-local current_time = tonumber(ARGV[1])
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (51)
.github/workflows/deploy-dev.yaml(1 hunks).github/workflows/deploy-prod.yaml(1 hunks)gradle/core.gradle(1 hunks)src/main/kotlin/com/dh/baro/cart/presentation/dto/CartItemResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/core/Cursor.kt(1 hunks)src/main/kotlin/com/dh/baro/core/ErrorMessage.kt(1 hunks)src/main/kotlin/com/dh/baro/core/GlobalExceptionHandler.kt(1 hunks)src/main/kotlin/com/dh/baro/core/serialization/EventSerializer.kt(1 hunks)src/main/kotlin/com/dh/baro/core/serialization/JacksonEventSerializer.kt(1 hunks)src/main/kotlin/com/dh/baro/core/serialization/LongToStringSerializer.kt(1 hunks)src/main/kotlin/com/dh/baro/identity/presentation/dto/UserProfileResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/application/FitFacade.kt(1 hunks)src/main/kotlin/com/dh/baro/look/application/LookFacade.kt(1 hunks)src/main/kotlin/com/dh/baro/look/application/dto/AiFittingInfo.kt(1 hunks)src/main/kotlin/com/dh/baro/look/application/dto/FittingSourceImageUploadInfo.kt(1 hunks)src/main/kotlin/com/dh/baro/look/application/dto/LookCreateCommand.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/FittingSourceImage.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/FittingSourceImageStatus.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/repository/CreditRepository.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/repository/FittingSourceImageRepository.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/service/CreditService.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/service/FittingSourceImageService.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/service/LookService.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiRequest.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiImageApi.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/redis/CreditRepositoryImpl.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/s3/S3ImageApi.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/s3/S3PresignedUrlInfo.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/FitController.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/AiFittingRequest.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/FittingSourceImageDto.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/FittingSourceImageListResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/FittingSourceImageUploadUrlResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/LookCreateRequest.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/LookCreateResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/dto/LookDto.kt(1 hunks)src/main/kotlin/com/dh/baro/look/presentation/swagger/FitSwagger.kt(1 hunks)src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/order/presentation/dto/OrderSummary.kt(1 hunks)src/main/kotlin/com/dh/baro/product/presentation/dto/CategoryResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/product/presentation/dto/PopularCursor.kt(1 hunks)src/main/kotlin/com/dh/baro/product/presentation/dto/ProductCreateResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/product/presentation/dto/ProductDetail.kt(1 hunks)src/main/kotlin/com/dh/baro/product/presentation/dto/ProductListItem.kt(1 hunks)src/main/resources/application-dev.yaml(2 hunks)src/main/resources/application-prod.yaml(2 hunks)src/main/resources/lua/credit_check.lua(1 hunks)src/main/resources/lua/credit_deduct.lua(1 hunks)src/test/kotlin/com/dh/baro/look/domain/LookServiceTest.kt(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: codrin2
PR: S-BARO/server#17
File: src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt:38-59
Timestamp: 2025-08-12T13:52:29.449Z
Learning: codrin2 prefers direct field access for immutable id properties (val) rather than using getters, reasoning that immutable fields pose less encapsulation risk.
🧬 Code graph analysis (16)
src/main/kotlin/com/dh/baro/core/Cursor.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (1)
serialize(7-15)
src/main/kotlin/com/dh/baro/product/presentation/dto/PopularCursor.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (2)
serialize(7-15)serialize(8-14)
src/main/kotlin/com/dh/baro/look/domain/FittingSourceImageStatus.kt (3)
src/main/kotlin/com/dh/baro/identity/domain/StoreStatus.kt (1)
DRAFT(3-10)src/main/kotlin/com/dh/baro/order/domain/OrderStatus.kt (1)
ORDERED(3-10)src/main/kotlin/com/dh/baro/core/outbox/MessageStatus.kt (1)
INIT(3-8)
src/main/kotlin/com/dh/baro/look/domain/service/LookService.kt (1)
src/main/kotlin/com/dh/baro/look/application/LookCreateCommand.kt (1)
creatorId(3-10)
src/main/kotlin/com/dh/baro/product/presentation/dto/ProductCreateResponse.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (2)
serialize(7-15)serialize(8-14)
src/main/kotlin/com/dh/baro/look/presentation/dto/AiFittingRequest.kt (2)
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderCreateRequest.kt (2)
message(10-28)message(20-26)src/main/kotlin/com/dh/baro/product/presentation/dto/ProductCreateRequest.kt (1)
min(7-46)
src/main/kotlin/com/dh/baro/product/presentation/dto/ProductDetail.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (2)
serialize(7-15)serialize(8-14)
src/main/kotlin/com/dh/baro/product/presentation/dto/ProductListItem.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (2)
serialize(7-15)serialize(8-14)
src/main/kotlin/com/dh/baro/product/presentation/dto/CategoryResponse.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (2)
serialize(7-15)serialize(8-14)
src/main/kotlin/com/dh/baro/core/GlobalExceptionHandler.kt (2)
src/main/kotlin/com/dh/baro/core/advice/GlobalExceptionHandler.kt (8)
logger(25-159)HttpStatus(153-158)HttpStatus(146-151)HttpStatus(116-121)HttpStatus(79-84)HttpStatus(95-100)HttpStatus(131-136)HttpStatus(109-114)src/main/kotlin/com/dh/baro/core/ErrorResponse.kt (2)
Include(9-75)field(36-51)
src/main/kotlin/com/dh/baro/look/presentation/dto/LookCreateResponse.kt (1)
src/main/kotlin/com/dh/baro/core/LongToStringSerializer.kt (2)
serialize(7-15)serialize(8-14)
src/main/resources/lua/credit_deduct.lua (1)
src/main/kotlin/com/dh/baro/product/infra/redis/InventoryRedisRepository.kt (2)
redissonClient(13-136)loadLuaScripts(22-27)
src/main/kotlin/com/dh/baro/look/infra/redis/CreditRepositoryImpl.kt (1)
src/main/kotlin/com/dh/baro/product/infra/redis/InventoryRedisRepository.kt (8)
redissonClient(13-136)loadLuaScripts(22-27)rollbackStocks(45-56)it(115-115)it(72-72)it(89-89)executeScript(65-107)deductStocks(29-43)
src/main/kotlin/com/dh/baro/look/presentation/swagger/FitSwagger.kt (1)
src/main/kotlin/com/dh/baro/look/presentation/swagger/LookSwagger.kt (6)
name(16-215)summary(24-60)summary(168-206)summary(63-96)summary(120-165)summary(99-117)
src/main/kotlin/com/dh/baro/look/presentation/dto/LookCreateRequest.kt (1)
src/main/kotlin/com/dh/baro/look/application/LookCreateCommand.kt (1)
creatorId(3-10)
src/main/kotlin/com/dh/baro/look/infra/s3/S3PresignedUrlInfo.kt (1)
src/main/kotlin/com/dh/baro/look/application/LookCreateCommand.kt (1)
creatorId(3-10)
🪛 detekt (1.23.8)
src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiImageApi.kt
[warning] 38-38: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
[warning] 72-72: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: CI-CD
🔇 Additional comments (37)
src/main/kotlin/com/dh/baro/look/presentation/dto/LookCreateResponse.kt (1)
3-3: 패키지 이동 반영 적절합니다
LongToStringSerializer의 신규 위치를 잘 따라가고 있어서 직렬화 동작에 문제 없을 듯합니다.src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (1)
3-3: 직렬화 유틸 패키지 정리 확인
패키지 구조 개편에 맞춰 import만 조정된 상태라 다른 영향은 없어 보입니다.src/main/kotlin/com/dh/baro/product/presentation/dto/ProductListItem.kt (1)
3-3: Serializer 경로 업데이트 문제없습니다
Look/상품 DTO 전반과 동일하게 패키지 이동을 반영했네요. 기존 동작 그대로 유지됩니다.src/main/kotlin/com/dh/baro/look/application/LookFacade.kt (1)
5-27: LookCreateCommand 경로 변경 검증 완료
새 dto 패키지로 이동한 LookCreateCommand를 사용하는 부분만 조정되었고, createLook 흐름에는 추가 영향이 없습니다.src/main/kotlin/com/dh/baro/look/presentation/dto/LookDto.kt (1)
3-3: Serializer import 일관성 유지
다른 DTO와 맞춰 패키지 경로만 변경된 것으로 확인했습니다.src/main/kotlin/com/dh/baro/product/presentation/dto/PopularCursor.kt (1)
3-3: 인입된 패키지 경로 정리 OK
LongToStringSerializer 패키지 이동 반영만 있어서 기능 영향 없습니다.src/main/kotlin/com/dh/baro/cart/presentation/dto/CartItemResponse.kt (1)
4-4: 패키지 리팩토링 반영 OK
LongToStringSerializer의 신규 패키지 경로를 정확히 따라가서 직렬화 설정이 계속 정상 동작하겠습니다.src/main/kotlin/com/dh/baro/identity/presentation/dto/UserProfileResponse.kt (1)
3-3: 패키지 이동 처리 확인
새 패키지 경로로 직렬화기를 교체해 DTO 직렬화가 유지됩니다.src/main/kotlin/com/dh/baro/order/presentation/dto/OrderSummary.kt (1)
3-3: 일관된 직렬화 패키지 적용
다른 DTO들과 동일하게 새 serialization 네임스페이스를 사용하여 혼선을 줄였습니다.src/main/kotlin/com/dh/baro/look/domain/service/LookService.kt (1)
4-4: LookCreateCommand 위치 변경 반영 OK
DTO 패키지로 이동한 커맨드 객체를 정상 참조하고 있어 서비스 로직에 영향 없습니다.src/main/kotlin/com/dh/baro/look/domain/FittingSourceImageStatus.kt (1)
1-6: 새 상태 enum 정의 확인
PENDING/COMPLETED 두 상태가 명확하게 선언되어 후속 도메인 로직에서 활용하기 좋습니다.src/main/kotlin/com/dh/baro/product/presentation/dto/ProductDetail.kt (1)
3-3: 직렬화기 경로 업데이트 완료
새 패키지 경로로 수정되어 다른 상품 DTO들과 일관성을 유지합니다.src/main/kotlin/com/dh/baro/core/serialization/JacksonEventSerializer.kt (1)
1-1: 직렬화 모듈 패키지 통합 적용
직렬화 관련 컴포넌트를 동일 패키지로 정리해 모듈 구조가 더 명확해졌습니다.src/main/kotlin/com/dh/baro/product/presentation/dto/CategoryResponse.kt (1)
3-3: 패키지 경로 정리 완료
CategoryResponse에서도 새 LongToStringSerializer 위치를 사용해 전역 리팩토링이 일관됩니다.src/main/kotlin/com/dh/baro/look/domain/repository/CreditRepository.kt (1)
3-5: 인터페이스 설계 적절합니다
Redis 구현과 자연스럽게 맞물릴 수 있는 최소 계약만 노출되어 있어 확장하기 좋아 보입니다.src/main/kotlin/com/dh/baro/look/application/dto/FittingSourceImageUploadInfo.kt (1)
5-11: DTO 구성 깔끔합니다
사전 서명 URL 흐름에 필요한 정보가 모두 포함돼 있어 상위 계층 매핑이 수월할 것 같습니다.src/main/kotlin/com/dh/baro/look/presentation/dto/LookCreateRequest.kt (1)
3-30: 패키지 정리 잘 반영됐습니다
LookCreateCommand이동에 맞춰 표현 계층에서도 일관되게 참조하도록 정리된 점 좋습니다.src/main/kotlin/com/dh/baro/look/presentation/dto/FittingSourceImageDto.kt (1)
6-18: 매퍼 책임 분리가 명확합니다
도메인 엔티티를 표현용 DTO로 변환하는 역할이 컴패니언으로 깔끔히 정리돼 있어 재사용에 용이해 보여요.src/main/kotlin/com/dh/baro/look/presentation/dto/FittingSourceImageListResponse.kt (1)
5-12: 리스트 응답 구조 적합합니다
단일 DTO 매핑을 재활용해 일관된 응답 형태를 보장하는 점이 좋습니다.src/main/kotlin/com/dh/baro/look/application/dto/AiFittingInfo.kt (1)
8-26: LGTM — 배열 콘텐츠 기준 동등성/해시 구현 적절contentEquals/contentHashCode 사용으로 ByteArray 참조 동등성 문제를 올바르게 회피했습니다.
src/main/kotlin/com/dh/baro/look/presentation/dto/FittingSourceImageUploadUrlResponse.kt (1)
6-22: LGTM — 계층 간 DTO 변환 단순·명확Instant 직렬화 포맷(ISO-8601)과 allowedTypes가 프론트 기대치와 합치하는지 최종 확인만 부탁드립니다.
src/main/kotlin/com/dh/baro/core/ErrorMessage.kt (1)
54-65: LGTM — 에러 메시지 체계 확장 적절도메인별로 구체적인 메시지가 추가되어 예외 매핑 및 관측성 향상에 도움이 됩니다.
src/main/kotlin/com/dh/baro/look/infra/redis/CreditRepositoryImpl.kt (1)
25-42: 루아 스크립트 리소스 포함 확인 src/main/resources/lua 경로에 credit_check.lua 및 credit_deduct.lua가 존재하여 패키징에 포함됩니다.src/main/kotlin/com/dh/baro/core/Cursor.kt (1)
3-3: Serializer 패키지 이동 반영 확인
LongToStringSerializerimport 경로가 새 패키지 구조에 맞게 정리되어 일관성이 유지됩니다..github/workflows/deploy-dev.yaml (1)
117-121: 배포 환경 변수 확장 확인
S3·Gemini 관련 시크릿을 컨테이너 환경에 주입하도록 추가되어, 애플리케이션 설정과 잘 맞춰졌습니다.src/main/kotlin/com/dh/baro/core/serialization/EventSerializer.kt (1)
1-5: 패키지 정리 문제 없음
직렬화 관련 타입을 동일 패키지로 모아둔 구조가 분명해져, 추가 조정 없이도 의존성이 깔끔합니다..github/workflows/deploy-prod.yaml (1)
111-115: 프로덕션 환경 변수 주입 확인
Prod 배포에도 동일한 AWS/Gemini 시크릿이 전달되어 환경별 구성 차이를 최소화했습니다.src/main/kotlin/com/dh/baro/look/presentation/dto/AiFittingRequest.kt (1)
5-11: 요청 DTO 검증 구성 적절
필수 URL 필드에@NotBlank를 적용해 기본 입력 검증을 확보한 구성이 적절합니다.src/main/kotlin/com/dh/baro/core/GlobalExceptionHandler.kt (1)
1-24: 패키지 이동 후 의존성 정상
전역 예외 핸들러를 루트 패키지로 옮겨ErrorResponse등과 같은 공간에서 관리하게 되어 추가 import 없이도 동작이 명확합니다.src/main/kotlin/com/dh/baro/product/presentation/dto/ProductCreateResponse.kt (1)
3-3: Serializer 경로 업데이트 확인
LongToStringSerializer의 새 위치를 반영해 직렬화가 계속 정상 동작할 수 있도록 정리된 점 좋습니다.src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt (1)
3-3: 패키지 변경 반영 깔끔합니다.
LongToStringSerializer새 위치로의 import 교체가 정확히 적용되어 직렬화 동작에 영향이 없음을 확인했습니다.gradle/core.gradle (1)
3-4: AWS SDK BOM 도입 적절합니다.
BOM과 S3 모듈을 함께 추가해 버전 정합성이 확보된 점 확인했습니다.src/main/kotlin/com/dh/baro/core/serialization/LongToStringSerializer.kt (1)
1-1: 직렬화 유틸의 패키지 이동 확인했습니다.
패키지 선언만 교체되어 기존 시리얼라이저 동작은 그대로 유지됩니다.src/test/kotlin/com/dh/baro/look/domain/LookServiceTest.kt (1)
4-4: 테스트 import 정정 완료되었습니다.
LookCreateCommand의 새 위치를 테스트에서도 일관되게 참조하고 있습니다.src/main/resources/application-dev.yaml (1)
60-81: 새 인프라 설정 추가가 일관됩니다.
S3 버킷/리전 및 Gemini API 설정이 환경 변수 기반으로 안전하게 연결되었습니다.src/main/kotlin/com/dh/baro/look/application/dto/LookCreateCommand.kt (1)
1-10: DTO 패키지 정리 확인했습니다.
도메인 커맨드 객체가dto패키지로 이동했지만 구조는 유지되어 호환성 문제가 없습니다.src/main/kotlin/com/dh/baro/look/infra/s3/S3PresignedUrlInfo.kt (1)
1-12: 프리사인 URL 응답 모델 구성 적절합니다.
필요 필드를 모두 포함한 DTO로 추가된 점 확인했습니다.
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.
Actionable comments posted: 2
🧹 Nitpick comments (4)
src/main/kotlin/com/dh/baro/look/domain/service/CreditService.kt (1)
19-23: 인터럽트 플래그 복구 필요InterruptedException을 잡은 뒤 플래그를 복구하지 않으면 상위 호출자가 인터럽트 사실을 감지하지 못해 후속 처리(정리, 중단 등)에 실패할 수 있습니다. IllegalStateException을 던지기 전에
Thread.currentThread().interrupt()를 호출해 인터럽트 신호를 보존해 주세요.val acquired = try { lock.tryLock(LOCK_WAIT_TIME, TimeUnit.SECONDS) } catch (e: InterruptedException) { + Thread.currentThread().interrupt() throw IllegalStateException(ErrorMessage.AI_FITTING_TOKEN_BUCKET_ERROR.message) }src/main/kotlin/com/dh/baro/look/domain/FittingSourceImage.kt (1)
40-43: 업로드 완료 상태 전환 시 중복 호출 방지 장치가 필요합니다.
markAsUploaded가 이미COMPLETED인 엔티티에서도 다시 호출될 수 있어, 재시도나 중복 요청이 들어오면 마지막 호출이imageUrl을 덮어써 버립니다. 업로드 완료 이후 상태를 비가역적으로 유지하도록 가드 로직을 두는 편이 안전합니다.fun markAsUploaded(imageUrl: String) { - this.imageUrl = imageUrl - this.uploadStatus = FittingSourceImageStatus.COMPLETED + if (uploadStatus == FittingSourceImageStatus.COMPLETED) { + throw IllegalStateException("이미 업로드가 완료된 이미지입니다.") + } + + this.imageUrl = imageUrl + this.uploadStatus = FittingSourceImageStatus.COMPLETED }src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiImageApi.kt (2)
28-40: 예외 원인 보존을 위해 cause 전달 필요현재
IllegalArgumentException으로 감싸며 원래 예외를 버리고 있어 스택 추적이 유실됩니다. 운영 장애 분석이 어려워지니 cause를 함께 넘겨 주세요.- } catch (e: Exception) { - throw IllegalArgumentException(ErrorMessage.IMAGE_DOWNLOAD_FAILED.format(e.message ?: "Unknown error")) + } catch (e: Exception) { + throw IllegalArgumentException( + ErrorMessage.IMAGE_DOWNLOAD_FAILED.format(e.message ?: "Unknown error"), + e, + )
63-74: API 호출 실패 시 원인 추적이 어렵습니다여기도 동일하게 cause를 전달하지 않아 예외 체인이 끊깁니다. 동일한 방식으로 수정 부탁드립니다.
- } catch (e: Exception) { - throw IllegalArgumentException(ErrorMessage.GEMINI_API_REQUEST_FAILED.format(e.message ?: "Unknown error")) + } catch (e: Exception) { + throw IllegalArgumentException( + ErrorMessage.GEMINI_API_REQUEST_FAILED.format(e.message ?: "Unknown error"), + e, + )
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/main/kotlin/com/dh/baro/look/application/FitFacade.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/FittingSourceImage.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/service/CreditService.kt(1 hunks)src/main/kotlin/com/dh/baro/look/domain/service/FittingSourceImageService.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiRequest.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiResponse.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiImageApi.kt(1 hunks)src/main/kotlin/com/dh/baro/look/infra/s3/S3ImageApi.kt(1 hunks)src/main/resources/lua/credit_check.lua(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/main/resources/lua/credit_check.lua
- src/main/kotlin/com/dh/baro/look/application/FitFacade.kt
- src/main/kotlin/com/dh/baro/look/infra/s3/S3ImageApi.kt
- src/main/kotlin/com/dh/baro/look/domain/service/FittingSourceImageService.kt
- src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiResponse.kt
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: codrin2
PR: S-BARO/server#17
File: src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt:38-59
Timestamp: 2025-08-12T13:52:29.449Z
Learning: codrin2 prefers direct field access for immutable id properties (val) rather than using getters, reasoning that immutable fields pose less encapsulation risk.
🧬 Code graph analysis (1)
src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiApiRequest.kt (1)
src/main/kotlin/com/dh/baro/identity/infra/KakaoUserInfoResponse.kt (1)
ignoreUnknown(6-22)
🪛 detekt (1.23.8)
src/main/kotlin/com/dh/baro/look/infra/gemini/GeminiImageApi.kt
[warning] 38-38: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
[warning] 72-72: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: CI-CD
Issue Number
#43
As-Is
To-Be
✅ Check List
📸 Test Screenshot
Additional Description