Skip to content

♻️ refactor/product/KIKI-48-Product : 요구사항 수정에 따른 상품 조회 수정#63

Merged
doup2001 merged 11 commits intodevelopfrom
feat/product/KIKI-48-Product
Aug 21, 2025
Merged

♻️ refactor/product/KIKI-48-Product : 요구사항 수정에 따른 상품 조회 수정#63
doup2001 merged 11 commits intodevelopfrom
feat/product/KIKI-48-Product

Conversation

@doup2001
Copy link
Member

@doup2001 doup2001 commented Aug 21, 2025

📌 작업한 내용

  • MySQL 도커 컴포즈 설정 수정

  • Swagger 상품 기본 데이터 수정

  • 상품 관련 기능 수정

    • 옵션 내부 가격 존재 시, 최저가 기준으로 "원~" 표기
      • 예시) 79900원, 83000원, 93000원 시, "77900원~" 으로 표기
    • 옵션 내부 가격 없을 시, 기존 가격으로 표기
      • 예시) "77900원" 으로 표기
  • DTO 내 옵션 출력 로직 수정

  • DTO 내 유사 상품 추천 분리

🔍 참고 사항

  • Swagger 및 MySQL docker-compose 파일 변경으로 로컬 환경 설정 시 영향이 있을 수 있음

🖼️ 스크린샷

스크린샷 2025-08-21 16 31 10

🔗 관련 이슈

#62

✅ 체크리스트

  • 로컬에서 빌드 및 테스트 완료
  • 코드 리뷰 반영 완료
  • 문서화 필요 여부 확인

Summary by CodeRabbit

  • 신기능

    • 상품 상세에 옵션 목록 노출 추가.
    • 가격을 숫자 대신 텍스트(옵션 범위 반영 가능)로 표시.
  • 리팩터

    • 상품 상세 응답 구조 조정: 일부 가격·배송·추천 관련 필드 제거 및 단순화.
  • 문서

    • API 예시 값(productId) 업데이트로 명세 가독성 개선.
  • 작업

    • CI 워크플로우 트리거 브랜치 확장.
    • Docker/MySQL 개발 환경 설정 개선(커스텀 my.cnf, 타임존, 메모리 한도 설정 변경, 불필요 서비스 정리).

@doup2001 doup2001 self-assigned this Aug 21, 2025
@doup2001 doup2001 added the 🔨 Refactor 코드 리팩토링 label Aug 21, 2025
@doup2001 doup2001 linked an issue Aug 21, 2025 that may be closed by this pull request
@github-actions
Copy link

github-actions bot commented Aug 21, 2025

Test Results

34 tests   34 ✅  6s ⏱️
13 suites   0 💤
13 files     0 ❌

Results for commit 99c171c.

♻️ This comment has been updated with latest results.

- 가격 계산 함수 util 처리
@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

  • GitHub Actions: push 트리거에 feat/product/KIKI-61-ElasticSearch 브랜치 추가.
  • Docker: MySQL에 my.cnf 마운트 및 TZ=Asia/Seoul 추가. dev 구성에서 logstash, prometheus 제거. es01/kibana 메모리 제한 제거(DEV) 및 기본 compose에서는 ES/Kibana 메모리 제한을 변수화.
  • MySQL: 문자셋 UTF8MB4, 타임존 Asia/Seoul, 연결수/캐시 설정 추가.
  • ProductDetailResponse: 가격 필드(double)→문자열로 변경, 할인 관련 필드 제거, 옵션 리스트 추가 및 가격 문자열 계산 로직 도입.
  • Swagger 예시 ID 값 갱신.
  • Elasticsearch 문서: 인덱스명 products→kikihi.products, thumbnailUrl→thumbnail로 변경.
  • Mongo 문서: thumbnail 필드 매핑 명시, options 타입을 List<Map<String,Object>>로 수정, 변환 헬퍼 제거.
  • 테스트: 가격 검증 관련 단언 제거. 픽스처의 옵션 구조를 Map 기반으로 변경.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant API as ProductController
  participant S as ProductService
  participant R as ProductRepository (Mongo)
  participant D as ProductDocument
  participant DTO as ProductDetailResponse

  C->>API: GET /products/{id}
  API->>S: getProduct(id)
  S->>R: findById(id)
  R-->>S: ProductDocument
  S->>D: toDomain()
  D-->>S: Product (thumbnail, options 등)
  rect rgba(200,230,255,0.3)
    note over S,DTO: 변경: 옵션 매핑, 가격 문자열 계산
    S->>DTO: from(Product)
    DTO-->>S: ProductDetailResponse(originalPrice: String, options: List<...>)
  end
  S-->>API: ProductDetailResponse
  API-->>C: 200 OK + JSON
Loading
sequenceDiagram
  autonumber
  participant SYS as App
  participant ESD as ProductESDocument
  participant ES as Elasticsearch(kikihi.products)

  SYS->>ESD: from(Product)
  rect rgba(255,240,200,0.4)
    note right of ESD: 변경: index=kikihi.products<br/>필드 thumbnail 사용
  end
  ESD-->>SYS: ESDocument(thumbnail)
  SYS->>ES: index/update(document)
  ES-->>SYS: ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/product/KIKI-48-Product

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/dev-ci-cd.yml (1)

165-196: 배포 단계에서 docker compose 실행 경로 불일치(치명적)

.env/home/ubuntu/app/docker/로, docker-compose.yml/home/ubuntu/app/로 전송한 뒤, SSH 단계에서 cd /home/ubuntu/app/docker/로 이동해 docker compose를 실행합니다. 이 경우 현재 디렉터리에 compose 파일이 없어 실패합니다(혹은 다른 파일을 참조).

해결 옵션 1: compose 파일을 .env와 같은 디렉터리로 보냅니다.

-      - name: 3. docker-compose.yml 전달
+      - name: 3. docker-compose.yml 전달
         uses: appleboy/scp-action@master
         with:
           host: ${{ secrets.EC2_PUBLIC_IP }}
           username: ${{ secrets.SSH_USER }}
           key: ${{ secrets.EC2_PRIVATE_KEY }}
-          source: "./docker/docker-compose.yml"
-          target: "/home/ubuntu/app/"
+          source: "./docker/docker-compose.yml"
+          target: "/home/ubuntu/app/docker/"

해결 옵션 2: 실행 경로를 compose 파일 위치로 바꾸거나, -f 옵션을 명시합니다.

-            cd /home/ubuntu/app/docker/
-            docker compose pull spring
-            docker compose up -d spring
+            cd /home/ubuntu/app/
+            docker compose --env-file ./docker/.env -f ./docker-compose.yml pull spring
+            docker compose --env-file ./docker/.env -f ./docker-compose.yml up -d spring
🧹 Nitpick comments (17)
.github/workflows/dev-ci-cd.yml (2)

12-12: 브랜치 목록 YAML 스타일 개선 및 린트 경고 해소

YAMLlint 경고(too few spaces after comma)를 해소하고, 유지보수성을 높이기 위해 플로우 시퀀스 대신 블록 시퀀스로 표기하는 것을 권장합니다.

아래처럼 변경하면 가독성과 편집 편의성이 개선됩니다.

-on:
-  push:
-    branches: [ "develop","feat/product/KIKI-61-ElasticSearch"]
+on:
+  push:
+    branches:
+      - develop
+      - feat/product/KIKI-61-ElasticSearch

153-156: CD 잡(배포) 실행 조건 가드 추가 권장

Line 12에서 feature 브랜치에 대한 push 트리거를 추가했기 때문에, 현재 설정으로는 feature 브랜치에서도 CD가 실행됩니다. develop만 배포 대상으로 의도하셨다면 CD 잡에 조건을 추가하세요.

 CD:
   needs: CI
   runs-on: ubuntu-22.04
   environment: Oracle
+  if: github.ref == 'refs/heads/develop'
+  concurrency:
+    group: deploy-oracle-${{ github.ref }}
+    cancel-in-progress: true
  • if: develop 브랜치에서만 배포
  • concurrency: 동일 브랜치 중복 배포 러닝 방지
src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (3)

26-26: indexName 변경에 따른 운영 영향 점검(에일리어스 권장)

indexName = "kikihi.products"로 변경되었습니다. 기존 products 인덱스/템플릿/파이프라인/monstache 설정과의 호환성, 재색인 계획을 점검하세요. 코드 변경 없이 롤오버/재색인을 유연하게 하려면 고정 인덱스명 대신 에일리어스(예: products)를 사용하고 실제 물리 인덱스는 버전 접미사로 운용하는 패턴을 권장합니다.

필요 시:

  • 신규 물리 인덱스: kikihi.products-000001
  • write alias: products -> kikihi.products-000001
  • 재색인 후 alias 스위치

45-47: thumbnail 필드 매핑 타입: Text → Keyword 권장

thumbnail은 URL/식별자 성격으로 전문 검색보다는 정확히 일치하는 값 비교/집계가 일반적입니다. FieldType.Keyword가 더 적합합니다. 현재 Text는 불필요한 분석기로 인해 저장 공간/인덱싱 비용이 늘 수 있습니다.

-    @Field(type = FieldType.Text)
-    private String thumbnail;
+    @Field(type = FieldType.Keyword)
+    private String thumbnail;

54-56: options 매핑: Object → Nested 검토

List<Map<String, Object>> options를 검색 쿼리에서 내부 필드 단위로 조건 결합(동일 객체 내 필드의 AND 일치 등)할 계획이면 FieldType.Nested가 필요합니다. Object는 배열의 객체 간 필드 상호 간섭(cross-object matches)이 발생할 수 있습니다.

-    @Field(type = FieldType.Object)
-    private List<Map<String, Object>> options;   //
+    @Field(type = FieldType.Nested)
+    private List<Map<String, Object>> options;   //

주의: 매핑 변경은 재색인이 필요합니다. 실제 질의 패턴을 확인 후 결정하세요.

src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java (2)

35-36: @field("thumbnail") 명시 여부(선택사항)

필드명과 저장 필드명이 동일하여 이 어노테이션은 기능적으로는 불필요합니다. 다만 ES와의 명시적 정합성을 위해 남겨둘 수도 있습니다. 팀 컨벤션에 따라 일관되게 적용 여부를 결정하세요.


44-45: 동적 Map 기반 options: 유효성/스키마 관리 제안

List<Map<String, Object>>로 변경되어 유연성은 커졌지만, 타입 안정성/검증/문서화가 약해집니다. API 레벨에서 최소 키 세트(예: name, values, extraPrice 등)에 대한 검증과 스키마 문서화를 권장합니다. 중장기적으로는 얇은 POJO/record(예: OptionItem)로 감싸는 어댑터 계층을 두면 도메인 모델의 안정성이 좋아집니다.

원하시면 Bean Validation(예: @Validated + 커스텀 Validator`) 기반의 옵션 유효성 검증 템플릿을 제공해 드릴게요.

Also applies to: 60-60

docker/docker-compose.yml (5)

63-64: MySQL 설정 마운트 위치 조정 권장

현재 /etc/my.cnf로 마운트하면 이미지 기본 설정을 완전히 대체할 수 있습니다. 드롭인 방식(/etc/mysql/conf.d/*.cnf)을 쓰면 기본 설정을 보존하면서 우리 설정만 추가할 수 있어 안전합니다.

-      - ./mysql/my.cnf:/etc/my.cnf
+      - ./mysql/my.cnf:/etc/mysql/conf.d/kikihi.cnf

참고: /etc/my.cnf를 대체해야 한다면, 기본 include 디렉터리(!includedir /etc/mysql/conf.d/ 등)를 my.cnf에 명시적으로 추가하는 것을 권장합니다.


69-69: TZ 설정과 MySQL time zone 일관성 확인

컨테이너 TZ=Asia/Seoul은 OS 시간대만 설정합니다. MySQL 서버의 default_time_zone(my.cnf)도 설정되어 있다면 서로 일관되게 유지하세요. MySQL이 이름 기반 시간대를 쓰려면 timezone 테이블 로딩이 필요합니다(없다면 +09:00 오프셋 사용 고려).


178-178: mem_limit 환경변수 기본값 제공

.envES_MEM_LIMIT/KB_MEM_LIMIT가 누락될 경우 파싱 실패가 발생할 수 있습니다. 기본값을 지정해 안전하게 처리하세요.

-    mem_limit: ${ES_MEM_LIMIT}
+    mem_limit: ${ES_MEM_LIMIT:-1g}
...
-    mem_limit: ${KB_MEM_LIMIT}
+    mem_limit: ${KB_MEM_LIMIT:-1g}

Also applies to: 227-227


138-139: 오타: monstahce → monstache

사용자 생성/패스워드 설정 부분에서 monstahce_system 오타가 있습니다. 보안 유저 명칭과 일치하도록 수정하세요.

-        echo "Setting monstahce_system password";
-        until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/monstahce_system/_password -d "{\"password\":\"${MONSTACHE_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
+        echo "Setting monstache_system password";
+        until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/monstache_system/_password -d "{\"password\":\"${MONSTACHE_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;

5-5: latest 태그 사용 지양(재현성/롤백성)

kikihistore/server:latest는 재현성/롤백성을 해칩니다. CI 빌드 번호나 Git SHA 기반의 불변 태그를 권장합니다. CD에서 latest pull로 인해 의도치 않은 버전이 배포될 수 있습니다.

docker/mysql/my.cnf (3)

9-10: Collation 최신화 제안

MySQL 8.0에서는 utf8mb4_0900_ai_ci가 일반적으로 더 정확하고 현대적인 정렬을 제공합니다. 특별한 호환성 이슈가 없다면 교체를 고려하세요.

-collation-server = utf8mb4_general_ci
+collation-server = utf8mb4_0900_ai_ci

12-14: default-time-zone 설정 주의(타임존 테이블 의존성)

'Asia/Seoul'과 같은 이름 기반 설정은 MySQL 타임존 테이블이 로딩되어 있어야 합니다. 그렇지 않으면 적용 실패/경고가 발생할 수 있습니다. 운영 단순화를 위해 오프셋 기반 설정으로 전환하거나, 초기화 스크립트로 타임존 테이블을 로딩하세요.

간단 대안:

-default-time-zone = 'Asia/Seoul'
+default_time_zone = '+09:00'

또는 compose 초기화 스크립트(docker-entrypoint-initdb.d/)에서 타임존 로딩:

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p"${MYSQL_ROOT_PASSWORD}" mysql

1-6: /etc/my.cnf 대체 시 include 유지 권장

현재 compose에서 /etc/my.cnf로 직접 마운트하는 경우, 이미지 기본 my.cnf의 include 지시문을 잃을 수 있습니다. 안전하게 기본 설정을 포함하도록 include를 명시하세요(섹션 밖 최상단/하단).

예시:

 [client]
 default-character-set = utf8mb4

 [mysql]
 default-character-set = utf8mb4

 [mysqld]
 # Character set & collation
 character-set-server = utf8mb4
-collation-server = utf8mb4_general_ci
+collation-server = utf8mb4_0900_ai_ci
@@
 max_connections = 200
+
+!includedir /etc/mysql/conf.d/
+!includedir /etc/mysql/mysql.conf.d/

Also applies to: 7-21

src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductDetailResponse.java (2)

141-170: ProductOptions.from 메서드의 null 안전성 개선

Line 156에서 Double.valueOf를 사용하지만 null 처리가 없어 NPE가 발생할 수 있습니다. 또한 vendors 처리 로직이 복잡합니다.

 public static List<ProductOptions> from(Product product) {
     List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
             .orElse(Collections.emptyList());
 
     return productOptions.stream()
             /// option_name 없는 건 필터링
             .filter(option -> option.get("option_name") != null)
             .map(option -> {
 
                 /// 옵션 명 가져오기
                 String optionName = (String) option.get("option_name");
                 Double price = null;
 
                 /// 가격이 있다면
                 if (option.get("main_price") != null) {
-                    price = Double.valueOf(option.get("main_price").toString());
+                    try {
+                        price = Double.valueOf(option.get("main_price").toString());
+                    } catch (NumberFormatException e) {
+                        // 로그 남기거나 기본값 설정
+                        price = null;
+                    }
                 }
 
                 String site = null;
                 String siteUrl = null;
                 Object vendorsObj = option.get("vendors");
                 if (vendorsObj instanceof Map<?, ?> vendors) {
                     site = (String) vendors.get("shop");
                     siteUrl = (String) vendors.get("url");
                 }
 
                 return of(optionName, price, site, siteUrl);
             })
             .toList();
 }

44-45: 스키마 예시와 실제 출력 형식 불일치

@Schema 어노테이션의 예시가 "599000 ~"인데 실제 코드에서는 "599000원 ~" 형식으로 출력됩니다.

-        @Schema(description = "최저가 가격부터", example = "599000 ~")
+        @Schema(description = "최저가 가격부터", example = "599000원 ~")
         String originalPrice,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5090797 and c9e5151.

📒 Files selected for processing (11)
  • .github/workflows/dev-ci-cd.yml (1 hunks)
  • docker/dev-docker-compose.yml (1 hunks)
  • docker/docker-compose.yml (3 hunks)
  • docker/mysql/my.cnf (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductDetailResponse.java (4 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/BookmarkControllerSpec.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java (1 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (4 hunks)
  • src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java (3 hunks)
  • src/test/java/site/kikihi/custom/platform/application/service/ProductServiceIntTest.java (0 hunks)
  • src/test/java/site/kikihi/custom/platform/domain/product/ProductFixtures.java (8 hunks)
💤 Files with no reviewable changes (1)
  • src/test/java/site/kikihi/custom/platform/application/service/ProductServiceIntTest.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (1)
src/main/java/site/kikihi/custom/platform/adapter/out/mongo/product/ProductDocument.java (1)
  • Document (17-64)
🪛 YAMLlint (1.37.1)
.github/workflows/dev-ci-cd.yml

[warning] 12-12: too few spaces after comma

(commas)

🔇 Additional comments (5)
src/main/java/site/kikihi/custom/platform/adapter/out/elasticSearch/ProductESDocument.java (1)

73-75: 필드명 변경 전파 확인 완료
다음 검사를 통해 더 이상 thumbnailUrl 또는 구 인덱스명(products)에 대한 참조가 없습니다. 따라서 필드명 변경(thumbnailUrlthumbnail)이 전파된 것으로 판단됩니다.

검사에 사용한 스크립트 예시:

#!/bin/bash
# 잔여 thumbnailUrl 참조 확인
rg -nC2 thumbnailUrl

# @Document(indexName="products") 잔여 확인
rg -nC2 '@Document.*indexName.*"products"'
docker/dev-docker-compose.yml (2)

21-21: 타임존 설정이 올바르게 적용되었습니다

MySQL 컨테이너에 한국 시간대를 설정한 것은 적절합니다. 이는 my.cnf의 타임존 설정과 일치합니다.


15-15: MySQL 설정 파일 존재 및 내용 확인 완료

docker/mysql/my.cnf 파일이 정상적으로 존재하며, 다음과 같은 설정이 포함되어 있습니다:

  • 기본 문자셋: utf8mb4
  • 서버 타임존: Asia/Seoul
  • 호스트명 네임 해석 비활성화(skip-host-cache, skip-name-resolve)
  • 최대 연결 수(max_connections): 200

따라서 마운트 경로 및 설정 파일 관련 문제는 없습니다. 계속 진행해 주세요.

src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/BookmarkControllerSpec.java (1)

91-91: Swagger 예시 Product ID 업데이트 확인

Product ID가 6896ed675198cf586e933d6c로 변경되었습니다. 이 ID가 실제 테스트 환경에서 유효한지 확인이 필요합니다.

src/main/java/site/kikihi/custom/platform/adapter/in/web/swagger/ProductControllerSpec.java (1)

31-31: Swagger 예시 ID 일관성 확인

Product ID 예시가 BookmarkControllerSpec.java의 변경사항과 일치하게 업데이트되었습니다.

Comment on lines 94 to 118
private static String getPrice(Product product) {

// 옵션 Optional 처리
List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
.orElse(Collections.emptyList());

// 옵션이 없다면 기본 가격 제공
if (productOptions.isEmpty()) {
return (int) product.getPrice() + "원";
}

// 옵션 중 최저가 찾기
OptionalDouble minPrice = productOptions.stream()
.map(option -> option.get("main_price"))
.filter(Objects::nonNull)
.mapToDouble(price -> Double.parseDouble(price.toString()))
.min();

// 최저가 있으면 옵션 가격으로, 없으면 기본 가격으로
if (minPrice.isPresent()) {
return (int) minPrice.getAsDouble() + "원 ~";
} else {
return (int) product.getPrice() + "원";
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

가격 계산 로직의 견고성 개선 필요

getPrice 메서드에서 몇 가지 개선이 필요합니다:

  1. Line 102, 114, 116: 정수 변환 시 소수점 이하가 손실됩니다
  2. Line 109: Double.parseDouble이 NumberFormatException을 발생시킬 수 있습니다
  3. 공백 처리가 일관되지 않습니다 (102번 줄은 "원", 114번 줄은 "원 ~")
 private static String getPrice(Product product) {
 
     // 옵션 Optional 처리
     List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
             .orElse(Collections.emptyList());
 
     // 옵션이 없다면 기본 가격 제공
     if (productOptions.isEmpty()) {
-        return (int) product.getPrice() + "원";
+        return String.format("%.0f원", product.getPrice());
     }
 
     // 옵션 중 최저가 찾기
     OptionalDouble minPrice = productOptions.stream()
             .map(option -> option.get("main_price"))
             .filter(Objects::nonNull)
-            .mapToDouble(price -> Double.parseDouble(price.toString()))
+            .mapToDouble(price -> {
+                try {
+                    return Double.parseDouble(price.toString());
+                } catch (NumberFormatException e) {
+                    return Double.MAX_VALUE;
+                }
+            })
+            .filter(price -> price != Double.MAX_VALUE)
             .min();
 
     // 최저가 있으면 옵션 가격으로, 없으면 기본 가격으로
     if (minPrice.isPresent()) {
-        return (int) minPrice.getAsDouble() + "원 ~";
+        return String.format("%.0f원 ~", minPrice.getAsDouble());
     } else {
-        return (int) product.getPrice() + "원";
+        return String.format("%.0f원", product.getPrice());
     }
 }
📝 Committable suggestion

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

Suggested change
private static String getPrice(Product product) {
// 옵션 Optional 처리
List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
.orElse(Collections.emptyList());
// 옵션이 없다면 기본 가격 제공
if (productOptions.isEmpty()) {
return (int) product.getPrice() + "원";
}
// 옵션 중 최저가 찾기
OptionalDouble minPrice = productOptions.stream()
.map(option -> option.get("main_price"))
.filter(Objects::nonNull)
.mapToDouble(price -> Double.parseDouble(price.toString()))
.min();
// 최저가 있으면 옵션 가격으로, 없으면 기본 가격으로
if (minPrice.isPresent()) {
return (int) minPrice.getAsDouble() + "원 ~";
} else {
return (int) product.getPrice() + "원";
}
}
private static String getPrice(Product product) {
// 옵션 Optional 처리
List<Map<String, Object>> productOptions = Optional.ofNullable(product.getOptions())
.orElse(Collections.emptyList());
// 옵션이 없다면 기본 가격 제공
if (productOptions.isEmpty()) {
return String.format("%.0f원", product.getPrice());
}
// 옵션 중 최저가 찾기
OptionalDouble minPrice = productOptions.stream()
.map(option -> option.get("main_price"))
.filter(Objects::nonNull)
.mapToDouble(price -> {
try {
return Double.parseDouble(price.toString());
} catch (NumberFormatException e) {
return Double.MAX_VALUE;
}
})
.filter(price -> price != Double.MAX_VALUE)
.min();
// 최저가 있으면 옵션 가격으로, 없으면 기본 가격으로
if (minPrice.isPresent()) {
return String.format("%.0f원 ~", minPrice.getAsDouble());
} else {
return String.format("%.0f원", product.getPrice());
}
}
🤖 Prompt for AI Agents
In
src/main/java/site/kikihi/custom/platform/adapter/in/web/dto/response/product/ProductDetailResponse.java
around lines 94 to 118, the getPrice method currently truncates decimals by
casting to int, risks NumberFormatException from Double.parseDouble, and uses
inconsistent suffix spacing ("원" vs "원 ~"); change the logic to safely parse
numeric option values (handle Number, BigDecimal, or String) using a try/catch
or a safe parse helper that falls back to product.getPrice() on parse failure,
compute the displayed price without losing cents (use BigDecimal or double and
format to show no decimal if whole number otherwise show decimal as needed), and
normalize the suffix/spacing consistently (e.g., "원" for single price and "원 ~"
with same spacing for range); ensure nulls are handled and the method always
returns a well-formatted string even when parsing fails.

Comment on lines +12 to +15
Map<String, Object> options = new HashMap<>();
options.put("option_name", "화이트");
options.put("option_name", "블랙");
options.put("option_name", "실버");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중복된 Map key로 인한 데이터 손실 문제

모든 fixture 메서드에서 동일한 key "option_name"을 세 번 put하고 있어, 최종적으로는 마지막 값인 "실버"만 남게 됩니다. 의도한 대로 세 가지 옵션을 모두 포함하려면 List 구조로 변경해야 합니다.

-        Map<String, Object> options = new HashMap<>();
-        options.put("option_name", "화이트");
-        options.put("option_name", "블랙");
-        options.put("option_name", "실버");
+        List<Map<String, Object>> optionsList = new ArrayList<>();
+        
+        Map<String, Object> option1 = new HashMap<>();
+        option1.put("option_name", "화이트");
+        optionsList.add(option1);
+        
+        Map<String, Object> option2 = new HashMap<>();
+        option2.put("option_name", "블랙");
+        optionsList.add(option2);
+        
+        Map<String, Object> option3 = new HashMap<>();
+        option3.put("option_name", "실버");
+        optionsList.add(option3);

그리고 builder 호출 부분도 수정:

-                .options(List.of(options))
+                .options(optionsList)

Also applies to: 36-39, 60-63, 84-87

@doup2001 doup2001 merged commit 7db77e1 into develop Aug 21, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

♻️ refactor : 상품 조회 기능 수정

1 participant