Conversation
Walkthrough보안 화이트리스트 포맷 수정과 함께 AI 이미지/LLM 통합, S3 업로드 및 템플릿 생성 파이프라인을 위한 도메인·애플리케이션·어댑터 모듈들이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant API as TemplateController
participant Service as GenerateCardTemplateService
participant Metadata as MetadataExtractor
participant Layout as LayoutPlanner
participant LLM as LlmSenderAdapter
participant ImageModel as ImageModelAdapter
participant Storage as ImageStorageAdapter
participant S3 as AWS S3
User->>API: POST /api/template (request)
API->>Service: generate(command)
Service->>Metadata: extract(command)
Metadata->>LLM: LLM request (metadata prompt)
LLM-->>Metadata: TemplateDesignMetadataResult
Service->>Layout: plan(metadata, llmModel)
Layout->>LLM: LLM request (layout prompt)
LLM-->>Layout: TemplateLayoutPlanResult
rect rgba(200,220,255,0.6)
Note over Service,ImageModel: 배경 이미지 생성(필요시)
Service->>ImageModel: generate(background prompt, model)
ImageModel-->>Service: image bytes
Service->>Storage: upload(bytes, "background/…")
Storage->>S3: PutObject
S3-->>Storage: S3 URL
end
rect rgba(220,255,200,0.6)
Note over Service,ImageModel: 레이어별 이미지 생성(필요시 반복)
loop for each image layer
Service->>ImageModel: generate(layer prompt, model)
ImageModel-->>Service: image bytes
Service->>Storage: upload(bytes, "layer/…")
Storage->>S3: PutObject
S3-->>Storage: S3 URL
end
end
Service->>Service: EditorJsonConverter.toJson(plan)
Service-->>API: Template (with EditorPayload)
API-->>User: HTTP 200 (response)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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.
Actionable comments posted: 16
🧹 Nitpick comments (39)
platform/ai/domain/build.gradle.kts (1)
1-11:java-library및explicitApi설정이 누락되었습니다다른 도메인 모듈과 동일하게 라이브러리 구성을 사용해야
api/implementation구분과 공개 API 관리가 가능합니다. 또한explicitApi()를 넣어두면 도메인 API가 의도치 않게 노출되는 것을 막을 수 있습니다. 아래 패치 적용을 제안드립니다.(docs.gradle.org)plugins { - kotlin("jvm") + kotlin("jvm") + `java-library` } kotlin { - jvmToolchain(21) + jvmToolchain(21) + explicitApi() } dependencies { // 순수 도메인 }platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptId.kt (1)
3-7: 불필요한 빈 클래스 본문 제거 필요
data class본문이 비어 있어 detektEmptyClassBlock경고가 발생합니다. 중괄호를 제거하고 코틀린 스타일에 맞게 정리해 주세요.(detekt.dev)-data class PromptId( - val namespace: String, - val name : String, -) { -} +data class PromptId( + val namespace: String, + val name: String, +)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/ImageModelPort.kt (1)
5-5: 모델 파라미터에 도메인 타입 사용을 권장합니다.
model파라미터가 현재String타입으로 선언되어 있지만, PR의 다른 파일들을 보면ImageModelenum이 도메인에 정의되어 있습니다. 타입 안전성과 명확성을 위해 도메인 enum을 사용하는 것이 좋습니다.다음과 같이 수정을 고려해보세요:
- fun generate(prompt: String, model: String) : ByteArray + fun generate(prompt: String, model: ImageModel) : ByteArray그리고 파일 상단에 import를 추가합니다:
import app.cardcapture.ai.domain.ImageModelplatform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt (1)
3-10: 구조는 적절하나 타입 안전성 개선을 고려해보세요.아웃바운드 커맨드 DTO가 올바르게 정의되었습니다. detekt의 EmptyClassBlock 경고는 data class에 대한 오탐입니다.
다만, 다음 필드들의 타입 안전성 개선을 고려해볼 수 있습니다:
model: String 대신ImageModelenum 사용color: String 대신 hex 형식 검증 또는 별도 타입 사용현재는 검증이 서비스 레이어에서 이루어지는 것으로 보이지만, 향후 타입 레벨에서의 제약이 유용할 수 있습니다.
platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/ImageStoragePort.kt (1)
5-5: 파라미터 역할에 대한 문서화를 추가하면 좋겠습니다.메서드 시그니처는 적절하지만,
key와fileName파라미터의 역할이 명확하지 않습니다. 특히 S3 업로드 시 이 두 값이 어떻게 사용되는지(예: key는 디렉토리 경로, fileName은 실제 파일명) KDoc으로 명시하면 구현 시 혼동을 줄일 수 있습니다.예시:
/** * 이미지를 스토리지에 업로드합니다. * @param bytes 업로드할 이미지 바이트 배열 * @param key 스토리지 경로 (예: "templates/images") * @param fileName 파일명 (예: "image.png") * @return 업로드된 파일의 URL */ fun upload(bytes: ByteArray, key: String, fileName: String): Stringplatform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateRequest.kt (1)
5-11: 웹 계층 입력 검증 추가를 권장합니다.이 DTO는 외부 HTTP 요청의 진입점이지만 입력 검증이 없습니다. 악의적이거나 잘못된 입력으로부터 시스템을 보호하기 위해 검증 어노테이션을 추가하는 것이 좋습니다.
고려할 검증사항:
purpose,color,model,prompt:@NotBlank추가texts:@NotEmpty및@Size제약 추가 (예: 최소 1개, 최대 N개)color: hex 색상 코드 형식 검증 (정규식 패턴)model: 허용된 모델명 검증예시:
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotEmpty import jakarta.validation.constraints.Size data class GenerateCardTemplateRequest( @field:NotBlank val purpose: String, @field:NotEmpty @field:Size(min = 1, max = 10) val texts: List<String>, @field:NotBlank val color: String, @field:NotBlank val model: String, @field:NotBlank val prompt: String )platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateLayoutPlan.kt (2)
8-13: 배경 모드를 타입 안전하게 개선할 수 있습니다.
mode필드가 String으로 선언되어 있지만, 주석에 명시된 "COLOR" 또는 "IMAGE" 값만 허용됩니다. PR의 다른 파일에서BackgroundModeDtoenum이 정의되어 있으니 이를 활용하면 타입 안전성이 향상됩니다.+import app.cardcapture.template.application.model.BackgroundModeDto + data class LayoutBackground( - val mode: String, // "COLOR" or "IMAGE" + val mode: BackgroundModeDto, val colorHex: String?, val prompt: String?, val opacity: Int )
31-47: 타입 안전성을 위해 sealed class 패턴 사용을 권장합니다.
LayoutLayer가type문자열과 nullable 필드들로 텍스트/이미지 레이어를 구분하는 방식은 런타임 에러에 취약합니다:
type = "text"인데concept가 있거나content가 null인 경우type = "image"인데font가 있거나prompt가 null인 경우Kotlin sealed class를 사용하면 컴파일 타임에 타입 안전성을 보장할 수 있습니다.
sealed class LayoutLayer { abstract val id: Int abstract val position: LayoutPosition data class TextLayer( override val id: Int, val role: String, // "headline" | "body" | ... val content: String, val font: String, val size: String, override val position: LayoutPosition ) : LayoutLayer() data class ImageLayer( override val id: Int, val concept: String, val prompt: String, override val position: LayoutPosition ) : LayoutLayer() }이렇게 하면 when 표현식에서도 exhaustive checking이 가능하고, 잘못된 필드 조합이 컴파일 타임에 방지됩니다.
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmResponse.kt (1)
3-8: 도메인 모델이 적절하게 구현되었습니다.LLM 응답을 담는 도메인 모델이 올바르게 정의되었습니다.
resourceUsage를 nullable로 처리한 것도 적절합니다. detekt의 EmptyClassBlock 경고는 data class에 대한 오탐입니다.
선택사항: 필드명 개선 고려
jsonValueRaw필드명이 다소 모호할 수 있습니다.rawJsonContent또는content처럼 더 명확한 이름을 고려해볼 수 있습니다.platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptSpec.kt (1)
3-7: 데이터 클래스로서 적절합니다.프롬프트 스펙을 담는 단순한 데이터 홀더로 적절합니다. detekt의 빈 클래스 경고는 데이터 클래스의 경우 무시해도 됩니다.
version과system값에 대한 검증이 필요하다면 초기화 블록(init)에 추가하는 것을 고려해보세요. 하지만 도메인 레이어에서 유연성을 위해 검증을 생략하는 것도 타당한 설계 선택입니다.platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/AiTemplateDesignPort.kt (1)
6-9: 파라미터 이름을 타입과 일치시키는 것을 고려해보세요.아웃바운드 포트가 명확하게 정의되어 있습니다. 다만 파라미터 이름이
prompt인데 타입이AiTemplateDesignPortCommand이므로,command로 변경하면 더 일관성 있고 명확할 수 있습니다.적용 가능한 변경:
- fun design(prompt: AiTemplateDesignPortCommand): AiTemplateDesignResult + fun design(command: AiTemplateDesignPortCommand): AiTemplateDesignResultplatform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/GenerateImagePortCommand.kt (1)
3-7: 불필요한 빈 본문을 제거하세요.Kotlin 데이터 클래스는 본문이 필요하지 않은 경우 중괄호를 생략할 수 있습니다.
다음 diff를 적용하여 불필요한 빈 본문을 제거하세요:
data class GenerateImagePortCommand( val prompt: String, val model: String -) { -} +)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PositionDto.kt (1)
3-11: 값 범위 검증 추가를 고려하세요.일부 속성들은 특정 범위로 제한되어야 하지만 현재 검증이 없습니다:
opacity: 0-100 범위로 제한되어야 함width,height: 양수여야 함rotate: 일반적으로 0-360도 범위잘못된 값이 렌더링 문제를 일으킬 수 있으므로
init블록에서 검증을 추가하거나 검증된 타입을 사용하는 것을 권장합니다.검증을 추가하려면 다음과 같이 수정할 수 있습니다:
data class PositionDto( val x: Int, val y: Int, val width: Int, val height: Int, val rotate: Int = 0, val zIndex: Int = 0, val opacity: Int = 100 -) +) { + init { + require(width > 0) { "width must be positive" } + require(height > 0) { "height must be positive" } + require(opacity in 0..100) { "opacity must be between 0 and 100" } + } +}platform/ai/adapter/build.gradle.kts (1)
25-25: 고정 버전 사용을 권장합니다.
springmockk에latest.release를 사용하고 있지만, 다른 adapter 모듈들(예: auth-adapter, voucher-adapter)은 고정 버전(예:4.0.2)을 사용합니다. 빌드 안정성과 일관성을 위해 고정 버전 사용을 권장합니다.Based on learnings
다음 diff를 적용하여 고정 버전을 사용하세요:
- testImplementation("com.ninja-squad:springmockk:latest.release") + testImplementation("com.ninja-squad:springmockk:4.0.2")platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiImageGenerateUseCase.kt (1)
5-8: 반환 타입에 타입 안전성 추가를 고려하세요.메서드가
String을 반환하는데, 이는 URL, 파일 경로, S3 키 등 다양한 의미를 가질 수 있어 타입 안전성이 부족합니다. 명확한 도메인 의미를 전달하기 위해 value class나 도메인 타입(예:ImageUrl,GeneratedImageResult)을 사용하는 것을 권장합니다.예를 들어:
@JvmInline value class ImageUrl(val value: String) interface AiImageGenerateUseCase { fun generate(command: AiImageGenerateCommand): ImageUrl }platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt (1)
21-22: 프롬프트 식별자 외부화를 고려하세요.프롬프트 namespace와 name이 하드코딩되어 있습니다. PR 설명에서 "프롬프트 저장소 관리"가 다음 PR에서 구현 예정이라고 언급되었으므로, 향후 확장성을 위해 이러한 값을 설정 파일이나 상수 클래스로 외부화하는 것을 고려해보세요.
platform/template/adapter/build.gradle.kts (1)
25-25: 고정 버전 사용을 권장합니다.
springmockk에latest.release를 사용하고 있지만, 프로젝트의 다른 adapter 모듈들은 고정 버전을 사용합니다. 빌드 안정성과 일관성을 위해 고정 버전 사용을 권장합니다.Based on learnings
다음 diff를 적용하세요:
- testImplementation("com.ninja-squad:springmockk:latest.release") + testImplementation("com.ninja-squad:springmockk:4.0.2")platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/dto/GeminiPreviewImageRequest.kt (1)
3-14: LGTM!Gemini API 요청을 위한 계층적 구조가 잘 정의되어 있습니다.
선택사항: Line 7의
ImagerPartsBlock을ImagePartsBlock으로 변경하는 것이 더 정확한 표현일 수 있습니다 (오타 수정).- data class ImagerPartsBlock( + data class ImagePartsBlock( val parts: List<TextBlock> )platform/template/domain/src/main/kotlin/domain/Template.kt (1)
10-22: data class 사용을 고려해보세요.현재 일반 class로 정의되어 있어
equals(),hashCode(),toString(),copy()메서드가 자동 생성되지 않습니다. 도메인 모델에 커스텀 비즈니스 로직이 필요하지 않다면 data class 사용을 권장합니다.-class Template( +data class Template( val id : Long, val userId: Long, val title: String, val description: String, val prompt: String, val fileUrl: String?, val purpose: String, val texts: List<String>, val color: String, val editorPayload: EditorPayload -) { -} +)Note: 만약 Template에 향후 비즈니스 로직이 추가될 예정이라면 현재 구조를 유지하는 것도 좋은 선택입니다.
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/OpenAiConfig.kt (2)
9-14: baseUrl 외부화를 고려해보세요.현재 baseUrl이 하드코딩되어 있습니다. 향후 테스트나 환경별 설정을 위해
application.yml로 외부화하는 것을 권장합니다.예시:
- private val baseUrl = "https://api.openai.com/v1" + @Value("\${openai.base-url:https://api.openai.com/v1}") + private val baseUrl: String
16-25: WebClient 에러 핸들링 TODO를 추적하세요.TODO 주석이 있습니다. WebClient의 에러 핸들링을 위해
ExchangeFilterFunction을 추가하는 것을 고려해보세요 (예: 타임아웃, 재시도, 로깅).다음 이슈로 등록하여 추적하시겠습니까? 필요하시면 에러 핸들링 구현 예시 코드를 생성해드릴 수 있습니다.
platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiTemplateDesignService.kt (1)
14-19: 불필요한 중간 변수를 제거할 수 있습니다.Line 17의
result변수는 바로 반환되므로 불필요합니다.override fun design(command: AiTemplateDesignCommand): TemplateLayoutPlan { val metadata = metadataExtractor.extract(command) - - val result = layoutPlanner.plan(metadata, command.llmModel) - return result + return layoutPlanner.plan(metadata, command.llmModel) }platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/GoogleAiConfig.kt (2)
10-15: baseUrl을 외부화하는 것을 고려하세요.OpenAiConfig와 마찬가지로 baseUrl을
application.yml로 외부화하면 테스트 및 환경별 설정이 용이합니다.- private val baseUrl = "https://generativelanguage.googleapis.com" + @Value("\${google.base-url:https://generativelanguage.googleapis.com}") + private val baseUrl: String private val keyHeader = "x-goog-api-key"
17-30: 에러 핸들링 추가를 고려하세요.OpenAiConfig에 있는 TODO와 일관성을 위해 여기에도
ExchangeFilterFunction을 통한 에러 핸들링, 로깅, 타임아웃 설정을 고려해보세요.platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/BackgroundPlanDto.kt (1)
3-10: 빈 본문을 제거할 수 있습니다.Data class는 본문이 필요 없으므로 빈 중괄호를 제거하는 것이 Kotlin 관례입니다.
data class BackgroundPlanDto( val mode: BackgroundModeDto, val colorHex: String? = null, val url: String? = null, val prompt: String? = null, val opacity: Int = 100 -) { -} +)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/prompt/SimplePromptLoaderAdapter.kt (3)
8-11: 프롬프트 저장소 전환을 추적하세요.주석에서 언급한 대로 향후 file/db 로더로 전환할 계획이 있습니다. PR 목표에도 "프롬프트 저장소 관리"가 다음 PR로 예정되어 있으므로, 이를 이슈로 등록하여 추적하는 것을 권장합니다.
프롬프트 저장소 관리를 위한 이슈 생성을 도와드릴까요? 파일 기반 또는 DB 기반 로더의 인터페이스 설계 예시를 제공할 수 있습니다.
18-25: 버전 관리 전략을 고려하세요.현재 모든 프롬프트의 버전이 "v0.0"으로 하드코딩되어 있습니다. 향후 프롬프트 변경 이력 추적을 위해 버전 관리 전략을 수립하는 것을 권장합니다.
29-314: 프롬프트가 잘 구조화되어 있습니다.두 프롬프트(metadata, layout) 모두 명확한 JSON 스키마와 상세한 지시사항을 포함하고 있어 LLM이 일관된 결과를 생성하는 데 도움이 됩니다.
다만, 향후 프롬프트가 늘어날 경우 유지보수를 위해 외부 파일로 분리하는 것을 고려하세요.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt (2)
9-42: ObjectMapper 사용을 고려하세요.수동으로 JSON 문자열을 구성하는 것은 에러가 발생하기 쉽고 유지보수가 어렵습니다. Jackson
ObjectMapper를 사용하면 더 안전하고 가독성이 높은 코드를 작성할 수 있습니다.수동 JSON 생성의 잠재적 문제:
- 이스케이프 처리 누락 가능성
- 유효하지 않은 JSON 생성 위험
- 테스트 및 디버깅 어려움
ObjectMapper를 사용한 리팩토링을 고려해보세요.
89-91: Position JSON 가독성을 개선하세요.한 줄로 된 JSON 문자열은 읽기 어렵습니다. 여러 줄로 분리하거나 ObjectMapper를 사용하는 것을 고려하세요.
private fun toPositionJson(pos: PositionDto): String = """ { "x": ${pos.x}, "y": ${pos.y}, "width": ${pos.width}, "height": ${pos.height}, "rotate": ${pos.rotate}, "zIndex": ${pos.zIndex}, "opacity": ${pos.opacity} } """.trimIndent()platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/ImageModelAdapter.kt (1)
17-49: 모델 인자를 무시하면 확장 시 오작동 위험이 큽니다.
generate시그니처로 전달된model값을 전혀 사용하지 않고 항상gemini-2.5-flash-image-preview엔드포인트만 호출하고 있습니다. 다른 이미지 모델을 지원하도록ImageModel이 확장되는 순간 이 어댑터는 잘못된 모델을 계속 호출하게 되어 기능이 깨집니다. 최소한 URI나 요청 페이로드에model인자를 반영하도록 리팩터링해 주세요.platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt (3)
40-41: 하드코딩된 ID 값에 대한 이슈를 생성하세요.
id와userId가 하드코딩되어 있습니다. PR 설명에 DB 연동이 다음 PR에 예정되어 있다고 하지만, 현재 상태로는 여러 요청 시 ID 충돌이 발생할 수 있습니다. 이를 추적할 이슈가 생성되어 있는지 확인해주세요.이 TODO 항목을 추적할 이슈를 생성하시겠습니까?
58-58: 검증 오류에 대해 더 적절한 예외 타입을 사용하세요.
error()를 사용하면IllegalStateException이 발생하는데, 이는 프롬프트가 누락된 경우 적절하지 않을 수 있습니다. 유효성 검사 오류를 나타내는 커스텀 예외나IllegalArgumentException을 고려해보세요.-val prompt = backgroundPlan.prompt ?: error("background image prompt가 필요합니다. ") +val prompt = backgroundPlan.prompt + ?: throw IllegalArgumentException("background image prompt가 필요합니다.")
71-91: 이미지 생성 실패 시 처리 전략을 고려하세요.여러 레이어에 대해 순차적으로 이미지를 생성하는데, 중간에 하나라도 실패하면 전체가 실패합니다. 부분 실패 시나리오를 고려해보세요:
- 일부 레이어만 성공한 경우 처리
- 재시도 로직
- 생성 실패 시 placeholder 이미지 사용
platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/outbound/AiTemplateDesignAdapter.kt (3)
34-36: 문자열 기반 타입 매칭을 enum으로 개선하세요.
background.mode.uppercase()로 문자열 비교하는 방식은 타입 안전성이 떨어지고 오타에 취약합니다. 가능하다면TemplateLayoutPlan의 mode를 enum으로 정의하거나, 최소한 상수로 관리하는 것이 좋습니다.
45-63: 레이어 타입 매칭을 더 안전하게 처리하세요.
layer.type.lowercase()로 문자열 비교하는 방식은 타입 안전성이 부족합니다.추가로,
requireNotNull은 NPE를 방지하지만, 이러한 null 값이 발생하는 것 자체가 upstream 데이터 품질 문제일 수 있습니다. AI 응답 검증 로직을 upstream(AiTemplateDesignService 또는 LayoutPlanner)에서 수행하는 것을 고려해보세요.
62-62: 알 수 없는 레이어 타입에 대한 처리 방식을 개선하세요.
error()를 사용하면IllegalStateException이 발생하는데, 이는 새로운 레이어 타입이 추가될 경우 런타임 실패로 이어집니다. 더 명확한 예외 타입을 사용하거나, 알 수 없는 타입을 건너뛰는 전략을 고려해보세요.-else -> error("Unknown layer type: ${layer.type}") +else -> throw IllegalArgumentException("Unknown layer type: ${layer.type}")platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadata.kt (2)
22-23: 주석을 영어로 통일하거나 KDoc 형식을 사용하세요.한글 주석이 영어 코드와 섞여 있습니다. 팀의 코드 스타일에 따라 주석 언어를 통일하거나, KDoc 형식(
/** ... */)을 사용하여 더 명확한 문서화를 하는 것을 권장합니다.동일한 사항이 lines 27-28, 33-35에도 적용됩니다.
14-19: BackgroundBlock의 유효성 검증 로직을 고려하세요.
mode가 "IMAGE"일 때는prompt가 필요하고, "COLOR"일 때는colorHex가 필요할 것으로 예상됩니다. 이러한 상호 의존성을 data class init 블록이나 factory method에서 검증하는 것을 고려해보세요.예시:
init { when (mode.uppercase()) { "IMAGE" -> require(prompt != null) { "IMAGE mode requires prompt" } "COLOR" -> require(colorHex != null) { "COLOR mode requires colorHex" } } }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (65)
libs/security/src/main/kotlin/app/cardcapture/lib/security/SecurityConfig.kt(1 hunks)platform/ai/adapter/build.gradle.kts(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/GoogleAiConfig.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/ImageModelAdapter.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/dto/GeminiPreviewImageRequest.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/dto/GeminiPreviewImageResponse.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/LlmSenderAdapter.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/OpenAiConfig.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptCompletionMessageRequest.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptCompletionRequest.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptMessageRole.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptResponseFormat.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/prompt/SimplePromptLoaderAdapter.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/ImageStorageAdapter.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/S3Config.kt(1 hunks)platform/ai/application/build.gradle.kts(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadata.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateLayoutPlan.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiImageGenerateUseCase.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiTemplateDesignUseCase.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/dto/AiImageGenerateCommand.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/dto/AiTemplateDesignCommand.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/ImageModelPort.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/ImageStoragePort.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/LlmSenderPort.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiImageGenerateService.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiTemplateDesignService.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt(1 hunks)platform/ai/domain/build.gradle.kts(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/ImageModel.kt(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmModel.kt(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmRequest.kt(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmResponse.kt(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptId.kt(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptLoader.kt(1 hunks)platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptSpec.kt(1 hunks)platform/services/api/build.gradle.kts(1 hunks)platform/services/api/src/main/resources/application.yml(1 hunks)platform/template/adapter/build.gradle.kts(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/TemplateController.kt(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateRequest.kt(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateResponse.kt(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/outbound/AiTemplateDesignAdapter.kt(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/outbound/GeneratedImageAdapter.kt(1 hunks)platform/template/application/build.gradle.kts(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/AiTemplateDesignResult.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/BackgroundModeDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/BackgroundPlanDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedImageLayerDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedLayerDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedTextLayerDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PositionDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/GenerateCardTemplateUseCase.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/dto/GenerateCardTemplateCommand.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/AiTemplateDesignPort.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/GenerateImagePort.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/GenerateImagePortCommand.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt(1 hunks)platform/template/domain/build.gradle.kts(1 hunks)platform/template/domain/src/main/kotlin/domain/EditorPayload.kt(1 hunks)platform/template/domain/src/main/kotlin/domain/Template.kt(1 hunks)settings.gradle.kts(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/domain/src/main/kotlin/app/cardcapture/auth/domain/OAuthProvider.kt:1-5
Timestamp: 2025-09-13T12:03:17.501Z
Learning: InHyeok-J prefers team discussion for architectural decisions involving module structure and naming conventions, especially when changes affect multiple domains like the OAuthProvider enum duplication issue.
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/adapter/src/main/kotlin/app/cardcapture/auth/adapter/outbound/jwt/JwtConfig.kt:0-0
Timestamp: 2025-09-13T12:43:40.003Z
Learning: InHyeok-J prefers to move JWT issuance functionality to the security module and use interface-based approach in auth-adapter for better separation of concerns and maintainability.
📚 Learning: 2025-10-01T02:29:12.107Z
Learnt from: inpink
Repo: SW-rocket-dan/card-capture-server PR: 32
File: build.gradle.kts:32-34
Timestamp: 2025-10-01T02:29:12.107Z
Learning: In the card-capture-server repository, domain modules (e.g., payment/voucher/domain, platform/auth/domain) intentionally avoid Spring dependencies in production code to maintain framework independence per hexagonal architecture principles. However, they may apply io.spring.dependency-management plugin solely for test dependency version management via BOM, without adding any Spring production dependencies. This keeps the domain pure while enabling consistent test dependency versions.
Applied to files:
platform/ai/adapter/build.gradle.ktsplatform/ai/application/build.gradle.ktsplatform/template/application/build.gradle.ktsplatform/template/adapter/build.gradle.kts
🧬 Code graph analysis (11)
platform/ai/domain/build.gradle.kts (4)
platform/auth/domain/build.gradle.kts (3)
{(11-13)jvmToolchain(6-9)kotlin(1-4)platform/member/domain/build.gradle.kts (2)
{(11-13)jvmToolchain(6-9)build.gradle.kts (1)
the(27-35)libs/contracts-auth/build.gradle.kts (1)
jvmToolchain(6-9)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiImageGenerateUseCase.kt (3)
platform/auth/application/src/main/kotlin/app/cardcapture/auth/application/port/inbound/DevelopmentLoginUseCase.kt (1)
login(3-6)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt (1)
getPublishedVouchers(3-5)platform/auth/adapter/src/main/kotlin/app/cardcapture/auth/adapter/inbound/web/DevelopmentLoginController.kt (1)
developmentLoginUseCase(12-24)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/GenerateCardTemplateUseCase.kt (3)
platform/auth/application/src/main/kotlin/app/cardcapture/auth/application/port/outbound/IssueTokenPort.kt (2)
issue(6-10)issue(8-8)platform/auth/application/src/main/kotlin/app/cardcapture/auth/application/port/inbound/DevelopmentLoginUseCase.kt (1)
login(3-6)payment/voucher/application/src/main/kotlin/app/payment/voucher/application/port/inbound/VoucherUseCase.kt (1)
getPublishedVouchers(3-5)
platform/ai/adapter/build.gradle.kts (3)
platform/member/adapter/build.gradle.kts (2)
kotlin(1-6)implementation(8-19)platform/auth/adapter/build.gradle.kts (2)
kotlin(1-6)implementation(8-27)payment/voucher/adapter/build.gradle.kts (2)
implementation(8-25)kotlin(1-6)
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmResponse.kt (3)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/inbound/GetVoucherResponse.kt (1)
id(3-8)platform/auth/adapter/src/main/kotlin/app/cardcapture/auth/adapter/inbound/web/dto/LoginResponse.kt (1)
accessToken(4-8)platform/member/application/src/main/kotlin/app/cardcapture/member/application/port/inbound/dto/AuthMemberView.kt (1)
id(3-8)
platform/template/domain/src/main/kotlin/domain/Template.kt (1)
payment/voucher/adapter/src/main/kotlin/app/payment/voucher/adapter/outbound/VoucherContentJpaEntity.kt (1)
name(14-45)
settings.gradle.kts (1)
platform/auth/domain/build.gradle.kts (2)
{(11-13)jvmToolchain(6-9)
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/GoogleAiConfig.kt (1)
platform/services/api/src/main/kotlin/app/cardcapture/api/platform/PlatformApiApplication.kt (1)
scanBasePackages(11-23)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiTemplateDesignUseCase.kt (1)
platform/auth/application/src/main/kotlin/app/cardcapture/auth/application/port/inbound/DevelopmentLoginUseCase.kt (1)
login(3-6)
platform/template/application/build.gradle.kts (1)
platform/template/domain/src/main/kotlin/domain/Template.kt (1)
id(10-22)
platform/template/adapter/build.gradle.kts (1)
platform/template/domain/src/main/kotlin/domain/Template.kt (1)
id(10-22)
🪛 detekt (1.23.8)
platform/template/domain/src/main/kotlin/domain/EditorPayload.kt
[warning] 5-7: The class or object EditorPayload is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmRequest.kt
[warning] 7-8: The class or object LlmRequest is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/GenerateImagePortCommand.kt
[warning] 6-7: The class or object GenerateImagePortCommand is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmResponse.kt
[warning] 6-8: The class or object LlmResponse is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/dto/GenerateCardTemplateCommand.kt
[warning] 9-10: The class or object GenerateCardTemplateCommand is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/template/domain/src/main/kotlin/domain/Template.kt
[warning] 21-22: The class or object Template is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptCompletionRequest.kt
[warning] 8-9: The class or object GptCompletionRequest is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptSpec.kt
[warning] 6-7: The class or object PromptSpec is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptId.kt
[warning] 6-7: The class or object PromptId is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedImageLayerDto.kt
[warning] 9-10: The class or object PlannedImageLayerDto is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt
[warning] 9-10: The class or object AiTemplateDesignPortCommand is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedTextLayerDto.kt
[warning] 9-10: The class or object PlannedTextLayerDto is empty.
(detekt.empty-blocks.EmptyClassBlock)
⏰ 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). (2)
- GitHub Check: test-platform
- GitHub Check: ktlint
🔇 Additional comments (33)
libs/security/src/main/kotlin/app/cardcapture/lib/security/SecurityConfig.kt (1)
22-26: 화이트리스트 항목과 실제 엔드포인트 간 불일치 확인코드베이스를 검색한 결과, 현재
/api/v1/auth/**경로에 매칭되는 실제 엔드포인트가 없습니다. 기존에는/api/v1/dev/auth/login만 존재하며, 이는 이미/api/v1/dev/auth/**항목으로 화이트리스트되어 있습니다.다음을 확인해주세요:
/api/v1/auth/**경로의 엔드포인트가 현재 계획 중이거나 향후 추가될 예정인지 여부- 해당 경로에 추가될 모든 엔드포인트가 정말로 인증 없이 공개적으로 접근 가능해야 하는지 (예: 로그인, 회원가입 등)
- 더 세밀한 화이트리스트 패턴 사용이 가능한지 (예:
/api/v1/auth/login,/api/v1/auth/signup등)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptMessageRole.kt (1)
5-13: 직렬화 동작이 요구사항과 일치합니다.
@JsonValue로 소문자 문자열을 보장해 OpenAI Chat API 포맷과 잘 맞습니다. 👍platform/services/api/src/main/resources/application.yml (1)
20-34: 배포 환경 환경변수 설정 확인 필요새 프로퍼티(
OPENAI_KEY,GOOGLE_KEY,S3_*)가 누락되면 부팅 시 자격 증명 로딩이 실패합니다. 운영/CI 환경에서 동일 키를 제공하도록 다시 한 번 점검 부탁드립니다.(docs.awspring.io)settings.gradle.kts (1)
13-32: 모듈 include 구성이 일관됩니다새 AI/Template 서브프로젝트 매핑이 설정과 경로에 잘 연결돼 있습니다.
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptResponseFormat.kt (1)
1-6: LGTM! OpenAI API DTO가 적절하게 구현되었습니다.OpenAI API 요청을 위한 response format DTO가 올바르게 구현되었습니다. Kotlin data class는 자동으로
equals,hashCode,toString,copy메서드를 생성하므로 빈 body는 정상입니다.참고: detekt의 EmptyClassBlock 경고는 data class에 대한 오탐(false positive)입니다.
platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateRequest.kt (1)
13-21: 매핑 로직이 올바르게 구현되었습니다.
toCommand()메서드가 웹 계층 DTO를 애플리케이션 계층 커맨드로 적절하게 변환하고 있습니다. 레이어 간 명확한 분리가 잘 이루어져 있습니다.platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateLayoutPlan.kt (1)
15-23: 위치/투명도 값의 유효성 검증을 고려해보세요.
LayoutPosition의 모든 필드가Int타입이지만, 일부 값들은 제약이 필요할 수 있습니다:
x,y: 음수 가능 여부width,height: 양수만 허용해야 할 가능성rotate: 0-360 범위 제한opacity: 0-100 범위 제한zIndex: 음수 허용 범위현재는 LLM 응답을 직접 받는다고 명시되어 있지만, 잘못된 값으로 인한 렌더링 오류를 방지하기 위해 서비스 레이어에서 검증을 추가하거나, init 블록에서 require() 검증을 고려해보세요.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/BackgroundModeDto.kt (1)
3-5: LGTM! 배경 모드 enum이 적절하게 정의되었습니다.배경 유형을 나타내는 enum이 올바르게 구현되었습니다.
COLOR와IMAGE두 가지 모드로 명확하게 구분됩니다.참고: 이 enum은
platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateLayoutPlan.kt파일의LayoutBackground.mode필드에서도 String 대신 활용될 수 있습니다.platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/dto/AiImageGenerateCommand.kt (1)
5-8: LGTM!커맨드 DTO 구조가 깔끔하고 불변성이 잘 유지되고 있습니다.
ImageModel타입을 사용하여 타입 안전성을 확보한 점이 좋습니다.platform/template/domain/build.gradle.kts (1)
1-11: LGTM!순수 도메인 모듈로 프레임워크 의존성 없이 구성된 점이 헥사고날 아키텍처 원칙에 부합합니다. Java 21 툴체인 설정도 적절합니다.
Based on learnings
platform/ai/application/build.gradle.kts (1)
1-21: LGTM!애플리케이션 레이어에 Spring 의존성을 포함한 구성이 헥사고날 아키텍처 원칙에 부합합니다.
ai-domain모듈에 대한 의존 방향이 올바르며, 테스트에서 Mockito를 제외한 것도 프로젝트의 다른 모듈들과 일관성이 있습니다.platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedLayerDto.kt (1)
4-7: LGTM!sealed interface를 사용한 다형성 설계가 깔끔합니다. 텍스트 레이어와 이미지 레이어의 공통 속성(
id,position)을 인터페이스에 정의하고, 각 구현체에서 특화된 속성을 추가하는 구조가 적절합니다.platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptCompletionMessageRequest.kt (1)
3-7: LGTM!GPT API 통신을 위한 메시지 DTO가 적절하게 설계되었습니다.
GptMessageRole열거형을 사용하여 타입 안전성을 확보한 점이 좋습니다. detekt의 빈 클래스 경고는 데이터 클래스의 경우 무시해도 됩니다.platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/AiTemplateDesignResult.kt (1)
3-8: LGTM!템플릿 디자인 결과를 담는 DTO 구조가 명확하고 적절합니다. Kotlin 데이터 클래스는
equals(),hashCode(),toString(),copy()메서드를 자동 생성하므로 빈 바디는 정상입니다.platform/template/application/build.gradle.kts (1)
1-19: LGTM!Application 모듈의 빌드 설정이 적절합니다. 헥사고날 아키텍처에서 application 레이어는 Spring과 같은 프레임워크 의존성을 가질 수 있으며, domain 모듈에 대한 의존성 구조도 올바릅니다.
platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiTemplateDesignUseCase.kt (1)
6-9: LGTM!헥사고날 아키텍처의 인바운드 포트 인터페이스가 명확하게 정의되어 있습니다. 단일 책임 원칙을 잘 따르고 있습니다.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/GenerateImagePort.kt (1)
5-7: LGTM!이미지 생성을 위한 아웃바운드 포트가 명확하게 정의되어 있습니다. 반환 타입이
String이므로 이미지 URL이나 경로를 반환하는 것으로 보입니다.platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/prompt/PromptLoader.kt (1)
3-6: LGTM!프롬프트 로딩을 위한 도메인 인터페이스가 명확합니다. PR 목표에서 언급된 대로 프롬프트 저장소 관리는 향후 PR에서 개선될 예정인 것으로 이해됩니다.
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/LlmRequest.kt (1)
3-8: LGTM!LLM 요청을 위한 데이터 클래스가 명확하게 정의되어 있습니다. Detekt의 빈 클래스 블록 경고는 Kotlin 데이터 클래스의 특성상 false positive이므로 무시해도 됩니다.
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/llm/openai/dto/GptCompletionRequest.kt (1)
3-9: LGTM!OpenAI GPT Completion API 요청을 위한 DTO가 적절하게 정의되어 있습니다. Detekt의 빈 클래스 블록 경고는 Kotlin 데이터 클래스의 특성상 false positive입니다.
platform/services/api/build.gradle.kts (1)
13-14: LGTM!새로운 ai-adapter 및 template-adapter 모듈의 의존성 추가가 적절합니다. PR 목표와 일치하며 기존 패턴을 따르고 있습니다.
platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/LlmSenderPort.kt (1)
6-9: LGTM!LLM 통신을 위한 outbound port 인터페이스가 명확하고 간결합니다. 헥사고날 아키텍처 원칙을 잘 따르고 있으며, 도메인 타입을 적절히 사용하고 있습니다.
platform/ai/adapter/build.gradle.kts (1)
14-15: spring-boot-starter-webflux 사용은 정당하지만, spring-boot-starter-web 필요성을 검증하세요.코드 분석 결과 WebFlux는 적극적으로 사용 중입니다:
WebClient로 외부 API 호출 (OpenAI, Google AI)Mono및flatMap()으로 리액티브 체인 구성bodyToMono()로 응답 처리따라서
spring-boot-starter-webflux는 필수입니다. 하지만 이 어댑터 모듈이 REST 엔드포인트를 노출하는지 확인하세요. 만약 내부 어댑터 역할만 한다면spring-boot-starter-web은 불필요할 수 있으므로 제거를 검토하세요. 두 스타터를 모두 포함하면 의존성이 증가하고 프로그래밍 모델이 혼재됩니다.platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt (1)
32-36: JSON 직렬화 오류 처리 검증 완료 - 애플리케이션 계층에서 오류 처리 전략이 부재합니다.검증 결과,
objectMapper.writeValueAsString(metadata)(35줄)에서 발생 가능한 예외에 대해 애플리케이션 계층의 어떤 레벨에서도 명시적인 오류 처리가 없습니다:
LayoutPlanner.plan(): try-catch 없음, throws 선언 없음AiTemplateDesignService.design(): try-catch 없음, throws 선언 없음AiTemplateDesignAdapter.design(): try-catch 없음, throws 선언 없음GenerateCardTemplateService.generate(): try-catch 없음, throws 선언 없음다음을 확인하세요:
- REST 컨트롤러 계층에서 전역 예외 처리(Global Exception Handler)가 JsonProcessingException을 처리하는지 확인
- 또는 적절한 계층에서 명시적인 try-catch 블록 또는 throws 선언 추가
platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedTextLayerDto.kt (1)
3-10: LGTM!텍스트 레이어를 위한 DTO 구조가 명확하고 적절합니다. detekt의 empty class body 경고는 생성자에 프로퍼티가 정의된 data class의 특성상 false positive입니다.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/dto/GenerateCardTemplateCommand.kt (1)
3-10: LGTM!카드 템플릿 생성을 위한 커맨드 구조가 명확합니다. 모든 필수 필드가 적절하게 정의되어 있습니다.
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/imageai/google/dto/GeminiPreviewImageResponse.kt (1)
4-25: LGTM!Gemini API 응답을 안전하게 처리하기 위한 구조가 잘 설계되어 있습니다. nullable 필드 사용으로 부분 응답에 대한 방어적 처리가 가능합니다.
platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/outbound/GeneratedImageAdapter.kt (1)
9-23: LGTM!템플릿 도메인과 AI 도메인을 연결하는 어댑터가 깔끔하게 구현되었습니다. 관심사의 분리가 잘 되어 있고,
AiImageGenerateCommand.ofOrThrow를 통한 검증 로직도 적절합니다.platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/PlannedImageLayerDto.kt (1)
3-10: 검토 의견은 부정확합니다.코드베이스 분석 결과, 현재 설계는 의도된 것입니다.
AiTemplateDesignAdapter.kt에서PlannedImageLayerDto(url = null, ...)로 인스턴스를 명시적으로 생성하고 있으며, 이는 제안된 제약조건require(url != null || generate)을 위반합니다.실제 사용 패턴:
url = null, generate = false조합은 의도된 상태 (레이어 객체가 존재하지만 아직 채워지지 않음)GenerateCardTemplateService에서는generate = false일 때 prompt를 사용하지 않음EditorJsonConverter에서는 null url을orEmpty()로 안전하게 처리제안된 제약을 추가하면 기존 유효한 사용 패턴이 깨집니다.
Likely an incorrect or invalid review comment.
platform/ai/domain/src/main/kotlin/app/cardcapture/ai/domain/ImageModel.kt (1)
1-11: 구현이 깔끔합니다.Enum 패턴과 팩토리 메서드 구현이 명확하고 올바릅니다. 현재 NANOBANANA 하나만 있지만, 향후 다른 이미지 모델 추가 시 확장하기 좋은 구조입니다.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/GenerateCardTemplateUseCase.kt (1)
6-9: 인터페이스 정의가 명확합니다.Use case 인터페이스가 단일 책임 원칙을 따르고 있으며, 프로젝트의 다른 use case들(예:
DevelopmentLoginUseCase,VoucherUseCase)과 일관된 패턴을 유지하고 있습니다.platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateResponse.kt (2)
11-12: TODO 항목에 대한 추적을 확인하세요.
likes와purchaseCount필드가 TODO로 표시되어 있으며, 현재는 0으로 하드코딩되어 있습니다. PR 설명에 따르면 DB 연동이 다음 PR에 예정되어 있는 것으로 보이지만, 이러한 미구현 항목들이 추적되고 있는지 확인이 필요합니다.
36-36: templateTags 구현 계획을 확인하세요.빈 리스트로 초기화되어 있는
templateTags의 구현 계획을 확인해주세요. 이 필드가 API 응답에 포함되므로, 향후 데이터 채우기 전까지는 API 문서에 이 제약사항을 명시하는 것이 좋습니다.
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/S3Config.kt
Show resolved
Hide resolved
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/S3Config.kt
Show resolved
Hide resolved
platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiImageGenerateService.kt
Show resolved
Hide resolved
platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt
Show resolved
Hide resolved
...lication/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt
Outdated
Show resolved
Hide resolved
.../src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt
Outdated
Show resolved
Hide resolved
.../src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt
Show resolved
Hide resolved
platform/template/domain/src/main/kotlin/domain/EditorPayload.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt (2)
20-21: 프롬프트 ID 상수를 외부 설정으로 관리하는 것을 고려해보세요.현재 하드코딩된
namespace와name값을 설정 파일이나 companion object로 관리하면 테스트 시 모킹이 용이하고, 향후 프롬프트 저장소 관리 기능 구현 시 유연성이 높아집니다.
31-37: 입력 유효성 검증을 추가하는 것을 권장합니다.현재
aiTemplateDesignCommand의 필드들을 검증 없이 사용하고 있습니다. 예를 들어:
texts가 비어있는 경우purpose,color,prompt가 공백인 경우이런 경우 LLM 호출이 의미 없는 결과를 반환하거나 API 비용만 소모할 수 있습니다.
메서드 시작 부분에 간단한 검증을 추가할 수 있습니다:
fun extract(aiTemplateDesignCommand: AiTemplateDesignCommand): TemplateDesignMetadata { require(aiTemplateDesignCommand.texts.isNotEmpty()) { "Texts must not be empty" } require(aiTemplateDesignCommand.purpose.isNotBlank()) { "Purpose must not be blank" } require(aiTemplateDesignCommand.color.isNotBlank()) { "Color must not be blank" } // 기존 로직... }platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/EditorPayload.kt (1)
3-7: 불필요한 빈 줄을 제거하세요.데이터 클래스의 body가 비어있을 때는 중괄호 내부의 빈 줄(6번 줄)이 불필요합니다.
다음 diff를 적용하여 코드를 정리할 수 있습니다:
data class EditorPayload( val editor: String -){ - -} +)platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/Template.kt (1)
10-22:data class사용을 고려하세요.현재
Template은 행위 없이 데이터만 보관하는 클래스입니다. Kotlin의data class를 사용하면 다음과 같은 이점이 있습니다:
equals(),hashCode(),toString(),copy()메서드 자동 생성- 코드 간결성 향상
- 도메인 모델의 의도 명확화
또한, body가 비어있으므로 중괄호를 제거할 수 있습니다.
다음과 같이 리팩토링할 수 있습니다:
-class Template( +data class Template( val id : Long, val userId: Long, val title: String, val description: String, val prompt: String, val fileUrl: String?, val purpose: String, val texts: List<String>, val color: String, val editorPayload: EditorPayload -) { -} +)참고: 만약
id기반의 동등성 비교가 필요하다면 (엔티티 패턴),equals()와hashCode()를 명시적으로 오버라이드하는 것이 더 적절할 수 있습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
.github/workflows/ci.yml(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt(1 hunks)platform/auth/application/src/test/kotlin/app/cardcapture/auth/application/service/DevelopmentLoginServiceTest.kt(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateResponse.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/GenerateCardTemplateUseCase.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt(1 hunks)platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/EditorPayload.kt(1 hunks)platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/Template.kt(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/inbound/web/dto/GenerateCardTemplateResponse.kt
- platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/domain/src/main/kotlin/app/cardcapture/auth/domain/OAuthProvider.kt:1-5
Timestamp: 2025-09-13T12:03:17.501Z
Learning: InHyeok-J prefers team discussion for architectural decisions involving module structure and naming conventions, especially when changes affect multiple domains like the OAuthProvider enum duplication issue.
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/adapter/src/main/kotlin/app/cardcapture/auth/adapter/outbound/jwt/JwtConfig.kt:0-0
Timestamp: 2025-09-13T12:43:40.003Z
Learning: InHyeok-J prefers to move JWT issuance functionality to the security module and use interface-based approach in auth-adapter for better separation of concerns and maintainability.
🧬 Code graph analysis (1)
platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/Template.kt (4)
payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/Voucher.kt (1)
id(5-12)platform/auth/domain/src/main/kotlin/app/cardcapture/auth/domain/AuthMember.kt (1)
id(3-8)platform/member/domain/src/main/kotlin/app/cardcapture/member/domain/Member.kt (1)
id(3-8)payment/voucher/domain/src/main/kotlin/app/payment/voucher/domain/VoucherContent.kt (1)
id(5-14)
🪛 detekt (1.23.8)
platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/Template.kt
[warning] 5-7: The class or object EditorPayload is empty.
(detekt.empty-blocks.EmptyClassBlock)
🔇 Additional comments (3)
platform/auth/application/src/test/kotlin/app/cardcapture/auth/application/service/DevelopmentLoginServiceTest.kt (1)
36-36: 타입 안전성 개선이 잘 적용되었습니다.문자열 대신 enum 값을 직접 사용하도록 변경하여 타입 안전성이 향상되었습니다. 이는 컴파일 타임에 타입 체크가 가능하고 문자열 불일치로 인한 런타임 오류를 방지할 수 있습니다.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/inbound/GenerateCardTemplateUseCase.kt (1)
6-9: 잘 설계된 유스케이스 인터페이스입니다!클린 아키텍처 패턴을 잘 따르고 있으며, 단일 책임 원칙을 준수하고 있습니다. 메서드 시그니처도 명확합니다.
참고: 7번 줄의 빈 줄은 제거해도 됩니다 (선택사항).
platform/template/domain/src/main/kotlin/app/cardcapture/template/domain/Template.kt (1)
3-21: 코드 설계는 의도된 아키텍처입니다. 수정이 필요하지 않습니다.검증 결과, 프로퍼티 중복은 의도된 설계입니다.
GenerateCardTemplateService.kt라인 39-50에서 명확하게 확인됩니다:
title = command.purpose(purpose가 title로 매핑)description = command.texts.firstOrNull() ?: ""(texts의 첫 요소가 description으로 매핑)이 구조는 관심사의 분리를 구현합니다:
- API 계층:
title,description사용 (GenerateCardTemplateResponse)- AI 로직 계층:
purpose,texts사용 (AiTemplateDesignAdapter, MetadataExtractor)Template 클래스 상단의 주석이 이미 이 설계를 설명하고 있으며, "추후 table 분리 가능성"을 언급하여 향후 진화를 고려한 의도된 구조임을 나타냅니다.
platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt (2)
61-66: 이미지 생성 실패에 대한 에러 핸들링을 추가하세요.
generateImagePort.generate()호출이 실패할 경우(네트워크 오류, API 제한, 타임아웃 등)에 대한 예외 처리가 없습니다. 외부 API 호출은 항상 실패 가능성을 고려해야 합니다.다음과 같이 에러 핸들링을 추가하세요:
private fun backgroundImageCheck(backgroundPlan: BackgroundPlanDto, model: String): BackgroundPlanDto { if (backgroundPlan.mode == BackgroundModeDto.COLOR) { return backgroundPlan } val prompt = backgroundPlan.prompt ?: error("background image prompt가 필요합니다.") return try { val backGroundImageUrl = generateImagePort.generate( GenerateImagePortCommand( prompt = prompt, model = model, ), ) backgroundPlan.copy(url = backGroundImageUrl) } catch (e: Exception) { // 로깅 추가 logger.error("Failed to generate background image: ${e.message}", e) // 재시도 로직 또는 fallback 처리 throw ImageGenerationException("배경 이미지 생성 실패: ${e.message}", e) } }
79-84: 레이어 이미지 생성 실패에 대한 에러 핸들링을 추가하세요.배경 이미지와 마찬가지로 레이어 이미지 생성에도 에러 핸들링이 필요합니다.
is PlannedImageLayerDto -> { if (!layer.generate) return@map layer val prompt = layer.prompt try { val img = generateImagePort.generate( GenerateImagePortCommand( prompt = prompt, model = model, ), ) layer.copy( url = img, generate = false, ) } catch (e: Exception) { logger.error("Failed to generate layer image for layer ${layer.id}: ${e.message}", e) throw ImageGenerationException("레이어 이미지 생성 실패: ${e.message}", e) } }platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt (1)
42-46: 예외가 삼켜지고 있습니다 - 디버깅 정보 손실이전 리뷰에서도 지적되었던 문제입니다. JsonProcessingException을 catch하고 있지만 원본 예외가 cause로 전달되지 않아 디버깅 시 중요한 정보(LLM 응답 내용, 파싱 실패 위치 등)가 손실됩니다.
다음과 같이 수정하여 원본 예외를 보존하세요:
try { return objectMapper.readValue<TemplateLayoutPlanResult>(response.jsonValueRaw) } catch (e: JsonProcessingException) { - throw IllegalStateException("fail to llm send result") + throw IllegalStateException("Failed to parse LLM layout plan response", e) }platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt (1)
47-51: 예외가 삼켜지고 있습니다 - 디버깅 정보 손실이전 리뷰에서도 지적된 문제로, LayoutPlanner와 동일한 패턴의 오류 처리 문제가 있습니다. JsonProcessingException을 catch하면서 원본 예외를 cause로 전달하지 않아 LLM 응답 파싱 실패 시 디버깅에 필요한 정보가 손실됩니다.
다음과 같이 수정하여 원본 예외를 보존하세요:
try { return objectMapper.readValue<TemplateDesignMetadataResult>(response.jsonValueRaw) } catch (e: JsonProcessingException) { - throw IllegalStateException("fail to llm send result") + throw IllegalStateException("Failed to parse LLM metadata response", e) }
🧹 Nitpick comments (13)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadataCommand.kt (1)
8-9: 빈 중괄호를 제거하세요.Kotlin 데이터 클래스에서 커스텀 로직이 없는 경우 빈 중괄호는 불필요합니다.
다음 diff를 적용하여 빈 중괄호를 제거하세요:
data class TemplateDesignMetadataCommand( val texts: List<String>, val purpose: String, val color: String, val userPrompt: String -) { -} +)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt (1)
8-9:model과llmModel의 명확한 구분이 필요합니다.두 속성의 이름이 유사하여 혼동될 수 있습니다. 각각의 목적이 무엇인지 명확히 구분되도록 더 구체적인 네이밍을 고려해보세요. 예를 들어:
model→imageGenerationModel또는imageModelllmModel→textGenerationModel또는 유지또는 KDoc 주석을 추가하여 각 속성의 역할을 문서화하는 것도 좋은 방법입니다.
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/S3Config.kt (2)
13-22: 이전 리뷰의 null 안전성 문제가 해결되었습니다.이전 리뷰에서 지적된 nullable 타입 문제가 non-null 타입으로 수정되어 설정 값이 누락될 경우 애플리케이션 시작 시점에 명확한 오류가 발생하도록 개선되었습니다.
선택적 개선 사항: 빈 문자열 검증 추가 고려
현재는 프로퍼티가 빈 문자열인 경우에 대한 검증이 없어, AWS 자격 증명이나 리전이 빈 문자열일 때 런타임에 오류가 발생할 수 있습니다. 다음과 같이
@PostConstruct를 통한 검증을 추가하는 것을 고려해보세요:@PostConstruct fun validate() { require(accessKey.isNotBlank()) { "AWS access key must not be blank" } require(secretKey.isNotBlank()) { "AWS secret key must not be blank" } require(region.isNotBlank()) { "AWS region must not be blank" } }
25-32: 이전 리뷰의 반환 타입 문제가 해결되었습니다.이전 리뷰에서 지적된 nullable 반환 타입이 non-null
S3Client로 수정되어 빈을 주입받는 곳에서 불필요한 null 체크가 제거되었습니다.권장 사항: 프로덕션 환경에서는 IAM 역할 기반 인증 사용 고려
현재
StaticCredentialsProvider를 사용하여 하드코딩된 자격 증명을 사용하고 있습니다. 개발 환경에서는 적절할 수 있으나, 프로덕션 환경에서는 IAM 역할이나 환경별 credential provider를 사용하는 것이 보안상 더 안전합니다.platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt (2)
7-8: 빈 생성자 괄호를 제거하세요.파라미터가 없는 생성자의 빈 괄호는 제거할 수 있습니다.
-class EditorJsonConverter( -) { +class EditorJsonConverter {
10-41: Jackson ObjectMapper 사용을 권장합니다.수동 JSON 문자열 생성은 유지보수가 어렵고 오류가 발생하기 쉽습니다. 특히 중첩된 구조와 특수 문자 처리가 필요한 경우 Jackson의
ObjectMapper를 사용하는 것이 더 안전하고 타입 안전합니다.다음과 같이 리팩토링을 고려하세요:
- 먼저 JSON 구조를 나타내는 data class들을 정의:
data class EditorJson( val designs: List<Design> ) data class Design( val id: Int, val background: Background, val layers: List<Layer> ) data class Background( val url: String, val opacity: Int, val color: String ) sealed interface Layer { val type: String val id: Int val position: Position } data class TextLayer( override val type: String = "text", override val id: Int, override val position: Position, val content: TextContent ) : Layer data class ImageLayer( override val type: String = "image", override val id: Int, override val position: Position, val content: ImageContent ) : Layer // ... 나머지 data class들
- 그 다음 ObjectMapper로 직렬화:
private val objectMapper = ObjectMapper().registerKotlinModule() fun toJson(plan: AiTemplateDesignResult): String { val editorJson = EditorJson( designs = listOf( Design( id = 0, background = Background( url = if (plan.background.mode == BackgroundModeDto.IMAGE) plan.background.url.orEmpty() else "", opacity = plan.background.opacity, color = plan.background.colorHex ?: "" ), layers = plan.layers.map { /* 변환 로직 */ } ) ) ) return objectMapper.writeValueAsString(editorJson) }이렇게 하면:
- 타입 안전성 확보
- JSON 이스케이프 자동 처리
- 테스트 및 유지보수 용이
- 실수로 인한 JSON 구문 오류 방지
platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt (3)
30-30: 하드코딩된 LLM 모델을 추적하세요.현재 "GPT"로 하드코딩되어 있으며 TODO 코멘트가 있습니다. 추후 여러 LLM을 지원할 계획이라면 이슈로 추적하는 것을 권장합니다.
이 작업을 위한 이슈를 생성하시겠습니까?
41-42: TODO 항목들을 이슈로 추적하세요.ID 자동 증가와 보안 관련 TODO가 있습니다. 다음 PR에서 구현 예정이라면 이슈로 추적하는 것이 좋습니다.
이 TODO들을 추적하기 위한 이슈를 생성하시겠습니까?
47-47: 빈 또는 공백 description 처리를 개선하세요.
texts.firstOrNull()이 null이거나 빈 문자열/공백일 수 있습니다. description 필드가 의미 있는 값을 가지도록 추가 검증을 고려하세요.- description = command.texts.firstOrNull() ?: "" , + description = command.texts.firstOrNull()?.takeIf { it.isNotBlank() } + ?: "No description provided",platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/prompt/SimplePromptLoaderAdapter.kt (1)
29-327: 프롬프트 하드코딩 - 임시 구현으로 이해PR 목표에 명시된 대로 다음 PR에서 프롬프트 저장소 관리가 구현될 예정이므로, 현재 하드코딩된 긴 프롬프트 문자열은 의도된 임시 구현으로 이해합니다.
현재 구현으로도 충분히 동작하지만, 다음 PR 전에 프롬프트를 외부 리소스 파일(예:
resources/prompts/)로 분리하면 다음과 같은 이점이 있습니다:
- IDE에서 프롬프트 내용 수정 시 편의성 향상
- 버전 관리 시 diff 가독성 향상
- 다국어 프롬프트 추가 시 확장 용이
platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateLayoutPlanResult.kt (2)
8-13: 타입 안전성 개선 제안
mode필드를 String으로 선언하면 "COLOR"와 "IMAGE" 외의 잘못된 값이 들어올 수 있습니다. Sealed class나 enum을 사용하면 컴파일 타임에 타입 안전성을 보장할 수 있습니다.다음과 같이 sealed class로 개선할 수 있습니다:
sealed class BackgroundMode { data class Color(val colorHex: String, val opacity: Int) : BackgroundMode() data class Image(val prompt: String, val colorHex: String?, val opacity: Int) : BackgroundMode() } data class LayoutBackground( val mode: BackgroundMode )또는 더 간단하게 enum을 사용할 수도 있습니다:
enum class BackgroundMode { COLOR, IMAGE } data class LayoutBackground( val mode: BackgroundMode, val colorHex: String?, val prompt: String?, val opacity: Int )
31-47: LayoutLayer 타입 구분 개선 제안현재
type필드를 String으로 사용하고 text/image 전용 필드를 nullable로 처리하는 방식은 런타임에 타입 오류가 발생할 수 있습니다. Sealed class를 사용하면 컴파일 타임에 타입 안전성을 보장하고 when 표현식에서 exhaustive checking이 가능합니다.다음과 같이 sealed class로 개선할 수 있습니다:
sealed class LayoutLayer { abstract val id: Int abstract val position: LayoutPosition data class Text( override val id: Int, override val position: LayoutPosition, val role: String, val content: String, val font: String, val size: String ) : LayoutLayer() data class Image( override val id: Int, override val position: LayoutPosition, val concept: String, val prompt: String ) : LayoutLayer() }이렇게 하면 레이어를 처리할 때 다음과 같이 타입 안전하게 처리할 수 있습니다:
when (layer) { is LayoutLayer.Text -> // text 필드들에 안전하게 접근 is LayoutLayer.Image -> // image 필드들에 안전하게 접근 }platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadataResult.kt (1)
14-19: 문자열 상수를 enum으로 개선 제안
mode필드를 String으로 선언하면 "COLOR"와 "IMAGE" 외의 잘못된 값이 들어올 수 있습니다. TemplateLayoutPlanResult.kt의 리뷰 코멘트와 유사하게, enum이나 sealed class를 사용하면 타입 안전성을 높일 수 있습니다.예시:
enum class BackgroundMode { COLOR, IMAGE } data class BackgroundBlock( val mode: BackgroundMode, val colorHex: String?, val prompt: String?, val opacity: Int )동일한 패턴을
role,emphasis,purpose,importance등 다른 문자열 상수 필드에도 적용할 수 있습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/prompt/SimplePromptLoaderAdapter.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/ImageStorageAdapter.kt(1 hunks)platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/S3Config.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadataCommand.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadataResult.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateLayoutPlanResult.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiTemplateDesignUseCase.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/ImageStoragePort.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiImageGenerateService.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiTemplateDesignService.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt(1 hunks)platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt(1 hunks)platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/outbound/AiTemplateDesignAdapter.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/model/BackgroundPlanDto.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt(1 hunks)platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/GenerateCardTemplateService.kt(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiImageGenerateService.kt
- platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/inbound/AiTemplateDesignUseCase.kt
- platform/ai/application/src/main/kotlin/app/cardcapture/ai/port/outbound/ImageStoragePort.kt
- platform/template/adapter/src/main/kotlin/app/cardcapture/template/adapter/outbound/AiTemplateDesignAdapter.kt
- platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/AiTemplateDesignService.kt
- platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/s3/ImageStorageAdapter.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/domain/src/main/kotlin/app/cardcapture/auth/domain/OAuthProvider.kt:1-5
Timestamp: 2025-09-13T12:03:17.501Z
Learning: InHyeok-J prefers team discussion for architectural decisions involving module structure and naming conventions, especially when changes affect multiple domains like the OAuthProvider enum duplication issue.
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/adapter/src/main/kotlin/app/cardcapture/auth/adapter/outbound/jwt/JwtConfig.kt:0-0
Timestamp: 2025-09-13T12:43:40.003Z
Learning: InHyeok-J prefers to move JWT issuance functionality to the security module and use interface-based approach in auth-adapter for better separation of concerns and maintainability.
📚 Learning: 2025-09-13T12:43:40.003Z
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/adapter/src/main/kotlin/app/cardcapture/auth/adapter/outbound/jwt/JwtConfig.kt:0-0
Timestamp: 2025-09-13T12:43:40.003Z
Learning: InHyeok-J prefers to move JWT issuance functionality to the security module and use interface-based approach in auth-adapter for better separation of concerns and maintainability.
Applied to files:
platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt
📚 Learning: 2025-09-13T12:03:17.501Z
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 30
File: platform/auth/domain/src/main/kotlin/app/cardcapture/auth/domain/OAuthProvider.kt:1-5
Timestamp: 2025-09-13T12:03:17.501Z
Learning: InHyeok-J prefers team discussion for architectural decisions involving module structure and naming conventions, especially when changes affect multiple domains like the OAuthProvider enum duplication issue.
Applied to files:
platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.ktplatform/ai/application/src/main/kotlin/app/cardcapture/ai/service/MetadataExtractor.kt
📚 Learning: 2025-11-08T12:07:42.627Z
Learnt from: InHyeok-J
Repo: SW-rocket-dan/card-capture-server PR: 42
File: platform/ai/adapter/build.gradle.kts:1-6
Timestamp: 2025-11-08T12:07:42.627Z
Learning: The ai-adapter module includes JPA plugin and spring-boot-starter-data-jpa dependency in preparation for future system prompt and database integration, even though entities are not yet defined in the adapter module.
Applied to files:
platform/ai/adapter/src/main/kotlin/app/cardcapture/ai/outbound/prompt/SimplePromptLoaderAdapter.kt
🪛 detekt (1.23.8)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/service/EditorJsonConverter.kt
[warning] 7-8: An empty default constructor can be removed.
(detekt.empty-blocks.EmptyDefaultConstructor)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadataCommand.kt
[warning] 8-9: The class or object TemplateDesignMetadataCommand is empty.
(detekt.empty-blocks.EmptyClassBlock)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/service/LayoutPlanner.kt
[warning] 44-44: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt
[warning] 10-11: The class or object AiTemplateDesignPortCommand is empty.
(detekt.empty-blocks.EmptyClassBlock)
⏰ 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: test-platform
🔇 Additional comments (2)
platform/ai/application/src/main/kotlin/app/cardcapture/ai/model/TemplateDesignMetadataCommand.kt (1)
3-7: 데이터 클래스 구조가 적절합니다.커맨드 객체로서 불변 속성(val)을 사용하고 Kotlin 데이터 클래스를 활용한 구조가 적절합니다.
platform/template/application/src/main/kotlin/app/cardcapture/template/application/port/outbound/dto/AiTemplateDesignPortCommand.kt (1)
10-11: 정적 분석 경고는 무시해도 됩니다.detekt의
EmptyClassBlock경고가 표시되지만, Kotlin data class의 경우 빈 본문이 정상적이고 관용적인 패턴입니다. 이는 false positive이므로 무시하셔도 됩니다.
.../application/src/main/kotlin/app/cardcapture/template/application/model/BackgroundPlanDto.kt
Outdated
Show resolved
Hide resolved
|
|
||
| data class TextBlock( | ||
| val text: String | ||
| ) |
There was a problem hiding this comment.
가독성과 재사용성을 위해 GeminiPreviewImageRequest, ImagerPartsBlock, TextBlock를 각각 별도 파일로 분리하면 어떨까용?
|
|
||
|
|
||
| @Component | ||
| class LlmSenderAdapter( |
There was a problem hiding this comment.
위에서 GeminiPreviewImageRequest를 사용했는데, 해당 클래스도 GPT 전용으로 사용한다면 네이밍에 OpenAI나 GPT가 들어가면 어떨까요?
|
|
||
| override fun send(request: LlmRequest): LlmResponse { | ||
|
|
||
|
|
| override fun send(request: LlmRequest): LlmResponse { | ||
|
|
||
|
|
||
| val request = GptCompletionRequest( |
There was a problem hiding this comment.
send 메서드에 파라미터도 request고, 메서드 내부에도 request가 있어서 헷갈릴 수 있어 보여용
|
|
||
| - name: Run platform tests | ||
| run: ./gradlew :platform-api:test :auth-domain:test :auth-application:test :auth-adapter:test :member-domain:test :member-application:test :member-adapter:test No newline at end of file | ||
| run: ./gradlew :platform-api:test :auth-domain:test :auth-application:test :auth-adapter:test :member-domain:test :member-application:test :member-adapter:test :template-adapter:test :template-application:test :template-domain:test :ai-adapter:test :ai-application:test :ai-domain:test No newline at end of file |
There was a problem hiding this comment.
이거 패키지 추가할 때마다 여기에 다 적어줘야하면 불편함이 클 거 같아요 백로그 담아두고 추후 개선해보겠습니다 🫡
|
|
||
| @Component | ||
| class ImageModelAdapter( | ||
| private val googleImageWebClient: WebClient, |
There was a problem hiding this comment.
요기도 클래스명에 모델명을 명시하면 좋을 거 같아요! 추후 모델 변경에 대비해 ImageModelPort { 로 추상화된 점은 좋다고 생각합니당
| private val googleImageWebClient: WebClient, | ||
| ) : ImageModelPort { | ||
|
|
||
| override fun generate(prompt: String, model: String): ByteArray { |
There was a problem hiding this comment.
이 메서드에서 model이라는 파라미터는 어디에 사용되나용?? 사용되지 않는다면 삭제하거나, 사용된다면 Enum으로 관리해보면 어떨까용?
| fun openAiWebClient(): WebClient { | ||
| /* | ||
| * @TODO: exception Handling | ||
| */ |
There was a problem hiding this comment.
요기 TODO내용들은 다음 PR에서 다룰 예정인지 궁금합니당!
| @JsonValue | ||
| override fun toString(): String { | ||
| return name.lowercase() | ||
| } |
There was a problem hiding this comment.
toString 구현 후 소문자로 바꿔주시는 이유 궁금해요!
| class SimplePromptLoaderAdapter : PromptLoader { | ||
|
|
||
|
|
||
|
|
|
|
||
| ✅ Use null for unknown scalars and [] | ||
| """.trimIndent() | ||
|
|
There was a problem hiding this comment.
저번 회의 때 구두로 이야기한 부분 댓글로 기록해둬요!
자주 변경될 수 있는 프롬프트의 경우 DB 데이터로 관리하면 좋겠습니당
그리고 운영 이슈 디버깅을 위해 특정 시점에 어떤 프롬프트를 사용했는지 기록하는 구조도 필요해 보여요 😇
PR 변경된 내용
추가 내용
참조
Closes #41
Summary by CodeRabbit
릴리스 노트
새로운 기능
Chores
버그 수정