[YETI] PLP 색상 그룹핑 + 컬러칩 + 리스트 장바구니 담기
현재 PLP는 productId(색상) 단위로 카드를 1:1 표시한다. 동일 스타일의 다른 색상이 별도 카드로 분리되어 나열되는 문제가 있다.
이 문서는 아래 세 가지를 정의한다.
- 색상 그룹핑: 동일 스타일의 색상 변형을 카드 1장으로 묶어 표시
- 컬러칩 인터랙션: 카드 내에서 색상 전환
- 리스트 장바구니 담기: 상세 페이지 이동 없이 즉시 담기
전체 흐름
섹션 제목: “전체 흐름”1. 프론트 → GET /v2/products/grouped (카테고리/필터/정렬 파라미터 포함)2. 서버: a. 카테고리/필터 조건으로 visible 상품 조회 b. style_key로 그룹핑 c. 각 그룹에서 맥락별 규칙으로 대표 색상 선택 d. colorVariants 배열 구성 (visible=true 상품만, 대표 제외, 등록순) e. 대표 상품 기준으로 정렬 f. 그룹 수 기준 페이지네이션 g. 응답 반환3. 프론트: 카드 렌더링 — 대표 색상이 초기 선택 상태4. 사용자: 컬러칩 클릭 → 썸네일/링크 교체 (페이지 이동 없음)5. 사용자: 장바구니 담기 → 단일 SKU면 즉시, 복수 SKU면 모달선행 조건 — DB 마이그레이션
섹션 제목: “선행 조건 — DB 마이그레이션”백엔드 개발 시작 전 완료 필요. product 테이블에 컬럼 2개를 추가한다.
| 컬럼 | 타입 | 설명 |
|---|---|---|
| style_key | VARCHAR(100) NULL | 그룹핑 키. 아래 6개 조건을 SHA-256 해시한 값. 상품 등록/수정 시 자동 계산 |
| representative | BOOLEAN NOT NULL DEFAULT FALSE | 카테고리 대표 색상. 어드민에서 수동 지정 |
ALTER TABLE product ADD COLUMN style_key VARCHAR(100) NULL AFTER product_code, ADD COLUMN representative BOOLEAN NOT NULL DEFAULT FALSE AFTER style_key, ADD INDEX idx_style_key (style_key);style_key 계산 방식
섹션 제목: “style_key 계산 방식”아래 6개 값을 연결한 문자열을 SHA-256 해시한다. 기존 otherColor API의 그룹핑 조건과 동일하게 유지한다.
SHA-256( brandId | seasonCode | category2Code | productName | genderCode | initialPrice )
productName에 사이즈가 포함된 상품(예: TUNDRA 35)은 이 계산으로 사이즈별 그룹이 자연스럽게 분리된다. 별도 처리 불필요.
기존 데이터 마이그레이션
섹션 제목: “기존 데이터 마이그레이션”배포 전 전체 상품에 대해 style_key를 일괄 계산 후 UPDATE.
-- 예시 (실제 구현은 배치 또는 마이그레이션 스크립트로 처리)UPDATE productSET style_key = SHA2( CONCAT(brand_id, '|', season_code, '|', category2_code, '|', product_name, '|', gender_code, '|', initial_price), 256)WHERE style_key IS NULL;배포 순서 미결: 마이그레이션 완료 후 배포 vs 코드에서
style_key IS NULL방어 처리 후 배포 — 개발팀 결정 필요.
대표 색상 선택 규칙
섹션 제목: “대표 색상 선택 규칙”서버가 그룹핑 시 맥락(호출 출처)에 따라 아래 규칙으로 대표 색상을 결정한다. 프론트는 별도 계산 없이 응답의 최상위 productId를 그대로 대표로 사용한다.
| 맥락 | 대표 결정 기준 |
|---|---|
| 카테고리 기본 | 1순위: representative=true AND visible=true → 2순위: visible=true 중 등록순 첫 번째 → 3순위: 전체 품절 시 카드 미노출 |
| 카테고리 + Color 필터 | 필터 colorCode에 해당하는 visible=true 상품 → 대표. 없으면 그룹 전체 미포함. (representative 무시) |
| 컬렉션 | 운영자가 수동 추가한 productId의 색상 → 대표. (representative 무시) |
| 컬렉션 + Color 필터 | 필터 색상에 해당하는 visible=true 상품 → 대표. 없으면 카드 미포함 |
| 검색 (색상 키워드 있음) | 검색어에서 추출된 색상과 매칭되는 상품 → 대표 |
| 검색 (색상 키워드 없음) | 카테고리 기본과 동일: representative=true → fallback(등록순) |
| 검색 + Color 필터 | 필터 색상 우선. 없으면 그룹 미포함 |
API 스펙
섹션 제목: “API 스펙”엔드포인트
섹션 제목: “엔드포인트”기존 /v2/products는 변경 없이 그대로 유지한다. 신규 엔드포인트로 분리.
GET /v2/products/grouped요청 파라미터
섹션 제목: “요청 파라미터”카테고리
| 파라미터 | 타입 | 설명 |
|---|---|---|
| mainCategoryId | number | 1차 카테고리 ID |
| subcategoryIds | number[] | 서브카테고리 ID |
| categoryCodes | string[] | Product Type(2nd) 카테고리 코드 |
필터
| 파라미터 | 타입 | 설명 |
|---|---|---|
| colorCodes | string[] | 색상 필터 (BLA, BLU, RED …) |
| sizes | string[] | 사이즈 필터 |
| minPrice | number | 가격 범위 최솟값 |
| maxPrice | number | 가격 범위 최댓값 |
| brandCodes | string[] | 브랜드 필터 |
| attributes | string[] | 속성 필터 ("key:value" 형식) |
정렬 / 페이지네이션
| 파라미터 | 타입 | 설명 |
|---|---|---|
| sort | string | POPULAR / PRICE_ASC / PRICE_DESC / NEWEST. 대표 상품 기준 적용 |
| page | number | 페이지 번호 (0-based) |
| size | number | 페이지당 그룹 수 |
응답 구조
섹션 제목: “응답 구조”그룹 단위로 반환. 배열 항목 하나 = 카드 1장.
{ "content": [ { "productId": 123, "productCode": "YT26FATUNDRA35001", "brandName": "YETI", "productName": "TUNDRA 35", "currentPrice": 380000, "initialPrice": 380000, "discountedRate": 0, "color": "WHITE", "thumbnailUrl": "https://cdn.example.com/products/123/thumb.jpg", "hoverUrl": "https://cdn.example.com/products/123/hover.jpg", "colorVariants": [ { "productId": 124, "color": "BLACK", "thumbnailUrl": "https://cdn.example.com/products/124/thumb.jpg" }, { "productId": 125, "color": "CAMP GREEN", "thumbnailUrl": "https://cdn.example.com/products/125/thumb.jpg" } ] } ], "totalElements": 42}응답 필드 설명
| 필드 | 설명 |
|---|---|
productId ~ hoverUrl |
대표 색상 상품 정보 |
currentPrice / initialPrice / discountedRate |
대표 상품 기준. 같은 style_key 내 상품은 initialPrice가 동일하므로 가격 정렬 시 충돌 없음 |
colorVariants |
대표 제외 나머지 색상 목록. visible=true 상품만 포함. 등록순 ASC |
colorVariants: [] |
단독 색상 상품 (그룹 내 visible 상품이 1개) |
totalElements |
전체 그룹 수 (≠ 전체 상품 수). 프론트 페이지네이션 기준값 |
캐시 전략
섹션 제목: “캐시 전략”Redis + Spring Cache 활용.
| 항목 | 내용 |
|---|---|
| 캐시 대상 | 전체 그룹 리스트 |
| 페이지네이션 | page/size는 캐시 키 제외. 캐시된 리스트에서 in-memory slice |
| 캐시 키 예 | plp:category:{mainCategoryId}:{subcategoryIds}:{colorCodes}:{sizes}:{sort} |
| 무효화 트리거 | 상품 visible/representative 변경, CategorySort 변경, 컬렉션 상품 추가/삭제 |
프론트엔드 — 카드 컴포넌트
섹션 제목: “프론트엔드 — 카드 컴포넌트”카드 구조
섹션 제목: “카드 구조”┌─────────────────────────┐│ ││ 메인 썸네일 │ ← 선택된 색상의 thumbnailUrl│ (hover → hoverUrl) │ ← hover 시 hoverUrl로 전환 (기존 동작)│ │├─────────────────────────┤│ [칩1] [칩2] [칩3] ... │ ← 컬러칩 (대표 먼저, 이후 colorVariants 순서)├─────────────────────────┤│ Brand Name ││ Product Name ││ ₩380,000 ││ [장바구니 담기] │└─────────────────────────┘컬러칩 정책
섹션 제목: “컬러칩 정책”| 항목 | 정책 |
|---|---|
| 칩 목록 순서 | 대표 색상 칩 먼저, 이후 colorVariants 순서 그대로 |
| 초기 선택 상태 | API 응답의 최상위 productId 기준 (서버가 맥락별 대표 결정 — 프론트 별도 계산 없음) |
| 칩 이미지 | 해당 색상의 thumbnailUrl |
| 색상 1개인 경우 | 칩 1개 표시, 항상 선택 상태 |
| 품절 상품 | 칩 미노출 (colorVariants는 visible=true만 포함) |
인터랙션
섹션 제목: “인터랙션”| 사용자 액션 | 동작 |
|---|---|
| 컬러칩 클릭 | 메인 썸네일을 해당 색상 thumbnailUrl로 교체 + 선택 칩 강조 표시 + 썸네일 클릭 링크를 해당 productId 기준으로 업데이트. 페이지 이동 없음 |
| 메인 썸네일 클릭 | 현재 선택된 색상의 상품 상세 페이지로 이동 (/products/{selectedProductId}) |
| 메인 썸네일 hover | hoverUrl로 이미지 전환 (기존 동작 유지) |
| Color 필터 선택 | colorCodes 파라미터 추가 후 API 재요청. 서버가 이미 필터 색상을 대표로 설정하여 응답 — 프론트 추가 처리 불필요 |
색상 전환 후 hoverUrl 처리 미결:
colorVariants에hoverUrl필드 추가 (백엔드 협의) vs 색상 전환 후 hover 비활성화 — 방향 결정 필요.
장바구니 담기
섹션 제목: “장바구니 담기”| 경우 | 처리 |
|---|---|
sizes.length === 1 (YETI 등 단일 SKU) |
선택된 색상의 productSizeId로 즉시 장바구니 추가. 옵션 선택 UI 없음 |
sizes.length > 1 (복수 사이즈 상품) |
기존 사이즈 선택 모달 그대로 사용 |
sizes.length판단은 프론트에서 분기 처리.
어드민 — 대표 색상 지정
섹션 제목: “어드민 — 대표 색상 지정”| 항목 | 내용 |
|---|---|
| 진입 위치 | 상품 관리 또는 카테고리 진열설정 화면 (정확한 위치는 개발/디자인팀 협의) |
| 기능 | 그룹 내 상품 목록 조회 → 특정 색상에 representative=true 지정/해제 |
| 단일 보장 | 그룹 내 representative=true는 1개만 허용. 새로 지정 시 기존 representative 자동 해제 |
| 캐시 | representative 변경 시 해당 카테고리 캐시 즉시 무효화 |
| 주의 | auto-visible 배치 로직은 representative 컬럼을 절대 건드리지 않음 |
기존 리소스 — 변경 없음
섹션 제목: “기존 리소스 — 변경 없음”| 항목 | 내용 |
|---|---|
otherColor API |
변경 없음. PDP 전용으로 그대로 유지 |
GET /v2/products |
변경 없음. 기존 화면에서 그대로 사용 |
| auto-visible 배치 | 변경 없음 |
| 컬러코드 체계 | 기존 colorCode 목록 (BLA, BLU, RED 등) 그대로 활용 |
미결 사항
섹션 제목: “미결 사항”| 항목 | 담당 | 내용 |
|---|---|---|
| style_key NULL 방어 배포 순서 | 개발팀 | 마이그레이션 완료 후 배포 vs 코드에서 NULL 방어 처리 후 먼저 배포 |
| 이미지 없는 representative 처리 | 개발팀 | representative=true 상품에 썸네일이 없을 경우: fallback 이미지 사용 vs 다음 visible=true 색상 상품으로 대표 fallback |
| 색상 전환 후 hoverUrl | 개발/백엔드 | colorVariants에 hoverUrl 추가 vs 색상 전환 후 hover 비활성화 |
| 어드민 대표 색상 지정 UI 위치 | 개발/디자인 | 상품 관리 vs 카테고리 진열설정 화면 중 위치 확정 필요 |
합격 기준
섹션 제목: “합격 기준”DB / 백엔드
섹션 제목: “DB / 백엔드”-
product테이블에style_key,representative컬럼이 추가된다 - 기존 상품 전체에
style_key값이 마이그레이션된다 - 상품 등록/수정 시
style_key가 자동 계산된다 -
GET /v2/products/grouped호출 시style_key기준으로 그룹핑된 응답이 반환된다 -
totalElements가 상품 수가 아닌 그룹 수를 반환한다 - Color 필터 선택 시 해당 색상 상품이 대표로 설정되어 응답된다
- 전체 품절 그룹은 응답에 포함되지 않는다
- 기존
GET /v2/products는 영향 없다
프론트엔드
섹션 제목: “프론트엔드”- 카드에 컬러칩이 표시된다 (대표 칩 먼저,
colorVariants순서대로) - 초기 렌더링 시 API 응답의 대표 색상이 선택 상태로 표시된다
- 컬러칩 클릭 시 썸네일과 상품 링크가 해당 색상 기준으로 전환된다
- 썸네일 클릭 시 선택된 색상 상품 상세 페이지로 이동된다
-
sizes.length === 1인 경우 옵션 선택 없이 즉시 장바구니에 담긴다 -
sizes.length > 1인 경우 기존 사이즈 선택 모달이 동작한다
어드민
섹션 제목: “어드민”- 그룹 내 상품 목록에서 특정 색상에
representative=true를 지정할 수 있다 -
representative변경 시 그룹 내 기존 대표가 자동 해제된다 -
representative변경 시 캐시가 무효화된다
변경 이력
섹션 제목: “변경 이력”| 버전 | 날짜 | 변경 내용 |
|---|---|---|
| v1.0 | 2026-07-09 | 최초 작성 — DB 마이그레이션, 그룹핑 API, 컬러칩 카드 컴포넌트, 장바구니 담기, 어드민 대표 색상 지정 |