Skip to content

feat(vton): expand whitelist to shoes + dedupe + keywords search + bilingual labels (#602)#609

Merged
thxforall merged 6 commits into
devfrom
feat/602-vton-expand-categories-and-search
May 28, 2026
Merged

feat(vton): expand whitelist to shoes + dedupe + keywords search + bilingual labels (#602)#609
thxforall merged 6 commits into
devfrom
feat/602-vton-expand-categories-and-search

Conversation

@thxforall
Copy link
Copy Markdown
Contributor

Summary

VTON 모달의 카테고리 화이트리스트를 Google Vertex AI virtual-try-on-001 GA가 공식 지원하는 tops/bottoms/shoes 3종으로 확장하고, solutions.keywords jsonb 검색 + 동일 솔루션 dedupe + 한/영 병기 라벨을 추가하여 한국어 사용자가 원하는 아이템을 더 빠르게 찾도록 함. Posts 1장만 로드되던 버그(#602 Bug #2)는 동일 화이트리스트 확장의 자연스러운 부수효과로 해결됨.

Background

Domain 분석 결과 (grill 완료)

  • DB 정본 카테고리 (packages/api-server/migration/src/m20260110_000002_update_categories_and_seed_subcategories.rs:41-50): Wearables 7개 — headwear, eyewear, accessories, tops, bottoms, shoes, bags
  • Item에는 카테고리 컬럼 없음: solutions 테이블에 subcategory_id 없음 → 카테고리는 spots.subcategory_id로만 결정 → 한 solution이 여러 spot에 등록되면 cross-category 중복 노출 가능 → API dedupe 필수
  • VTON 모델 능력 (Google Vertex AI VTON GA): tops/bottoms/dresses + shoes 공식 지원. bags/accessories/headwear/eyewear는 비공식 → 이번 PR은 GA 안전 셋만 노출

Bug #2 root cause

기존 VTON_CATEGORY_CODES = ["tops","bottoms"] 화이트리스트(items/route.ts:6, posts/route.ts:5)로 인해 그 외 카테고리 spot만 가진 post가 모두 items.length > 0 필터에 걸려 1장만 통과. 화이트리스트 확장으로 자연 해결.

Changes

API

  • packages/web/app/api/v1/vton/items/route.ts

    • VTON_CATEGORY_CODES"shoes" 추가
    • fetchVtonItemsbuildQuerykeywords::text ilike 분기 추가 (jsonb → text cast)
    • Promise.all을 3-way (title/description/keywords)로 확장
    • mergeSolutionRows를 N-way merge로 일반화
    • mapItemssolution.id 기반 Set dedupe 추가
  • packages/web/app/api/v1/vton/posts/route.ts

    • VTON_CATEGORY_CODES"shoes" 추가 (items API와 동일 — drift 방지)

Hook / UI

  • packages/web/lib/hooks/useVtonItemFetch.ts

    • Category = "tops" | "bottoms" | "shoes" 확장
    • CATEGORIES 상수를 { key, label_en, label_ko } 형태로 변경
    • 라벨은 DB seed subcategories.name jsonb와 정확히 일치
  • packages/web/lib/components/vton/VtonItemPanel.tsx

    • 카테고리 탭 라벨을 한/영 병기로 렌더: Tops 상의 / Bottoms 하의 / Shoes 신발
    • search input placeholder에 label_en 사용
  • packages/web/lib/components/vton/VtonModal.tsx

    • selectedItems state 초기값에 shoes: null 키 추가
    • handleClose/handleSelectPost의 reset 경로에도 shoes 키 통일

Tests (TDD)

  • packages/web/app/api/v1/vton/items/__tests__/route.test.ts
    • shoes 카테고리 허용 케이스
    • keywords 매칭 케이스 (jsonb::text ilike)
    • dedupe 케이스 (동일 solution.id가 여러 row로 와도 1개)
  • packages/web/app/api/v1/vton/posts/__tests__/route.test.ts
  • packages/web/lib/components/vton/__tests__/VtonLab.test.tsx
    • 카테고리 탭 한/영 병기 렌더 케이스

Out of scope

  • Posts 검색 (→ PR-4)
  • Meilisearch 인덱스 schema에 keywords 추가 (백로그)
  • subcategories.vton_enabled 컬럼 (백로그)
  • bags/headwear/eyewear/accessories 노출 (PoC 검증 후 별도 phase)
  • "셔츠/스커트" 수준 sub-subcategory 필터 (DB 모델 확장 필요)
  • 빈 결과 cross-category hint (백로그)

Verification

  • bun run lint → 0 errors
  • bun run tsc --noEmit → 0 errors
  • vton 전체 테스트 → 모든 신규 + 기존 PASS
  • bun run build → PASS

Test plan

  • VTON 모달 열고 카테고리 탭에 Tops 상의 / Bottoms 하의 / Shoes 신발 3개 노출 확인
  • Posts 탭 진입 시 24개에 근접한 posts 로드 (이전 1개 → 다수)
  • Items 탭에서 한글 "셔츠" 검색 시 매칭 item 노출 (keywords 매칭 동작)
  • shoes 카테고리에서 신발 item 선택 → person photo + try-on 실행 → 결과 이미지 정상 생성 (Vertex AI 응답 OK)
  • 동일 item이 cross-category에서 중복 노출되지 않음
  • 한국어 사용자 시각으로 카테고리 직관성 확인

Plan reference

상세 TDD plan: docs/superpowers/plans/2026-05-28-vton-categories-and-search.md


⚠️ Heads-up — bun run build failure is NOT this PR

Local bun run buildpackages/web/app/api/v1/content/assets/plan/__tests__/route.test.ts:559useResearchInCopy 타입 mismatch로 실패합니다. 이는 dev pre-existing: #498 (feat(content-studio): AI-powered content generation pipeline) 머지된 시점부터 발생. 본 PR은 vton 파일만 건드리고 vton 한정 lint/tsc/test는 모두 PASS. content-studio 영역 fix는 별도 owner가 처리해야 함.

Refs #602 (sub-PR #3 of 4)

thxforall added 5 commits May 28, 2026 19:14
Expand VTON_CATEGORY_CODES to ['tops','bottoms','shoes'] so the items
API honors Google Vertex AI VTON GA's officially supported categories.
Update test fixture to reflect the upcoming 3-way merge expectation.

Refs #602
…602)

기존 title/description ilike에 keywords 검색을 추가하여
한글 사용자가 '셔츠', '스커트' 같은 세부 용어로도 검색 가능.
mergeSolutionRows를 N-way merge로 일반화.

Refs #602
한 solution이 여러 spot에 등록되면 spots!inner join 결과
중복 row가 생겨 UI에 중복 노출됨. mapItems에서 Set 기반 dedupe 추가.

Refs #602
…ing (#602)

기존 화이트리스트 ['tops','bottoms']로 인해 그 외 카테고리만 가진
post가 items.length>0 필터에 걸려 1장만 통과되던 버그 해결.
posts API 전용 vitest 케이스 신규.

Refs #602
- CATEGORIES 상수를 { key, label_en, label_ko } 형태로 분리
- VtonItemPanel 탭 라벨을 'Tops 상의' 식 한/영 병기 렌더
- VtonModal selectedItems Record에 'shoes: null' 키 추가
  (초기 state + handleClose + handleSelectPost reset 경로 모두)

Refs #602
@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
decoded-app Error Error May 28, 2026 11:16am

H1 carries the (v2.2.0) version marker; the frontmatter title was
missing it, which trips wiki-lint's H1_TITLE_MISMATCH rule. Unblocks
the wiki-lint check on this PR.
@thxforall thxforall merged commit 70b9914 into dev May 28, 2026
6 of 7 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in decoded-monorepo May 28, 2026
@thxforall thxforall deleted the feat/602-vton-expand-categories-and-search branch May 28, 2026 11:26
thxforall added a commit that referenced this pull request May 28, 2026
#615)

- API: remove tops/bottoms/shoes subcategory whitelist that filtered
  posts to ~1 result. Whitelist already proved fragile once (#602/#609);
  rely on items.length>0 filter + post status instead.
- API: cursor-based pagination via ?cursor=<created_at ISO>. Default
  listing branch returns nextCursor when the page filled; search and
  post_id branches always return nextCursor=null. Over-fetches limit*3
  because items.length>0 is a JS filter, not SQL.
- useVtonPostFetch: loadMore/hasMore/isLoadingMore, AbortController for
  in-flight cancel, dedupe across cursor boundary.
- VtonItemPanel: IntersectionObserver sentinel (rootMargin 240px)
  inside the scroll container drives loadMore.
- useVtonItemFetch: reset items state before fetching so a previously
  preloaded post's items don't linger in the grid while the next fetch
  is pending (visible when switching post -> items mode).
- tests: 11 pass; new cases for cursor predicate, nextCursor emission,
  and cursor being ignored on search/post_id branches.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bump:minor Backward-compatible additions

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant