상품·주문 공유 상위 컨텍스트 FE 설계 (앱 / React Native)

플랫폼 판정: 이 기능의 두 화면(통합 상품 검색·내 주문 통합 조회)은 최종 소비자(구매자/신청자) 대상이다. 레포 실측 결과 web/은 운영자·관리자 포털(BFF, app/portal/*·app/admin/*, 랜딩이 “운영자 포털(데모)“)이라 소비자 검색·주문내역 서피스가 없다. 소비자 앱은 mobile/(Expo/React Native)이며 이미 product·event·facility·recruitment·limited-drop·order 화면이 존재한다. 따라서 이 기능은 앱 전용이고 design-fe-web은 작성하지 않는다(근거는 “확인 필요” 및 상위 리포트의 PRD Non-Goals 상충 항목 참조).

Background

근거 PRD: 도메인 경계 재설계/20260708-상품주문-공유상위컨텍스트-prd.md (verdict PASS, v2). 근거 BE TDD: 도메인 경계 재설계/20260708-상품주문-공유상위컨텍스트-tdd.md — API 계약(GET /api/catalog·GET /api/orders)을 “인터페이스 시그니처”에서 확정했다. 이 문서는 그 계약을 입력으로 소비한다.

BE는 catalog(판매 대상 5종 통합 검색)·order(주문/신청 4종 통합 조회) 두 읽기 파사드 API를 신설한다. 사용자 관점의 목표는 “요가 매트도 사고 싶고 요가 클래스도 찾고 싶은” 사용자가 5개 화면 대신 한 검색 화면을 쓰고, 흩어진 결제/신청 이력을 한 주문내역 화면에서 보는 것이다. 이 문서는 그 두 화면을 앱에 설계·티켓화한다.

Overview

  • 무엇을: 앱에 화면 2종을 추가한다 — ① 통합 상품 검색(/catalog) ② 내 주문 통합 조회(/orders). 둘 다 BE 읽기 파사드 API를 TanStack Query로 소비하는 읽기 전용 조회 화면이다.
  • : 판매 대상·주문이 5/4개 도메인에 분산돼 사용자가 통합 진입점 없이 개별 화면을 순회해야 한다(PRD Problem Definition).
  • 어떻게: 레포에 이미 정착한 스택(Expo Router + TanStack Query + axios be-client 직접 호출 + useTheme() 테마 토큰 + components/ui 프리미티브 키트)을 그대로 따른다. 신규 라이브러리 도입 없음. 기존 화면((tabs)/search=시설 검색, app/order=goods 전용)은 불변으로 두고 신규 라우트로 추가한다(PRD: additive).

Terminology

용어정의
catalog 화면판매 대상 5종(PRODUCT/LIMITED_DROP/TICKET/PROGRAM/RECRUITMENT) 통합 검색 화면 (app/catalog/index.tsx)
orders 화면주문/신청 4종(BOOKING/TICKETING/GOODS/RECRUITMENT) 통합 조회 화면 (app/orders/index.tsx)
CatalogItem / OrderHistoryItemBE가 정규화해 내려주는 통합 응답 항목 (FE는 그대로 렌더)
itemType / orderType항목의 원본 도메인 판별자. 배지·필터·상세 이동 분기의 기준
sellerTypePRODUCT 항목의 판매자 유형(B2C 개인/중고, B2B 브랜드). PRODUCT에만 값, 그 외 null
failedDomains부분 실패 시 조회에 실패한 도메인 목록. FE는 안내 배너로 노출
detailPath원본 도메인 상세 경로(예: /products/{id}). 항목 탭 시 이 경로로 이동
4상태loading / empty / error / success — 모든 데이터 화면이 처리해야 하는 상태(+ 부분 실패는 success의 하위 변형)

Define Problem

AS-IS (레포 실측, HEAD 808d2101)

  • 통합 검색 부재: 소비자가 판매 대상을 도메인별 화면에서만 찾는다 — (tabs)/store.tsx(상품), app/event/*(티켓), (tabs)/search.tsx(시설 검색, 위치+날씨), app/recruitments/*(모집), app/limited-drop/*(한정판). 5종을 한 번에 검색하는 지점이 없다. (tabs)/search.tsx는 이름이 “검색”이지만 실제로는 시설+날씨 전용이라 이 통합 검색과 다르다(덮어쓰지 않는다).
  • 통합 주문내역 부재: app/order/index.tsxuseMyGoodsOrdersQuery(GET /goods-orders/me)만 조회하는 goods 전용이다. 예약(Booking)·티켓(TicketOrder)·모임 신청(Application)은 각 화면에 흩어져 있어 “내 전체 구매/신청 이력”을 한 화면에서 볼 수 없다.
  • 정착된 FE 패턴(이 설계가 따를 것):
    • 데이터 흐름: api/*.ts(엔드포인트 함수, getBeClient() axios) → lib/use*.ts(TanStack Query 훅) → app/* 화면. 컴포넌트는 api/를 직접 호출하지 않는다(useProductsgetProducts 예시, lib/useProducts.ts#useProducts).
    • 테마: useTheme(){ scheme, tokens }, 색은 전부 시맨틱 토큰(theme/tokens.ts가 SSOT, Toss 팔레트 이미 반영 — accent #3182F6). 신규 화면은 하드코딩 색 금지.
    • UI 키트: components/uiLoadingView(spinner/skeleton)·ErrorView(message+onRetry)·EmptyState(message+description)·SegmentedControl·Badge(숫자 전용)·Card·ListItem·ThemedText(variant primary/secondary/muted/accent/danger) 존재. 4상태 처리를 이 키트로 한다(app/recruitments/index.tsx가 표준 예시).
    • 라우트 상수: lib/navigation.ts ROUTES — 화면은 경로 문자열을 하드코딩하지 않는다.
    • 서버 상태만 Query, 전역 클라이언트 상태는 auth·theme(Zustand)만. 지역 상태는 useState.

TO-BE

  • app/catalog/index.tsx(통합 검색) + app/orders/index.tsx(통합 주문내역) 신규. 둘 다 BE 파사드 API를 Query 훅으로 소비.
  • 재사용 프리미티브(LoadingView/ErrorView/EmptyState/SegmentedControl)를 그대로 쓰고, 도메인 표현 컴포넌트(CatalogItemCard·OrderHistoryItemCard·SellerTypeBadge·PartialFailureBanner)만 신규.
  • 진입점: 마이 탭에 “내 주문 내역”(통합), 홈에 “통합 검색” 진입 추가(기존 goods 전용 /order·시설 search 탭은 유지).

Architecture Benchmarking (토스 벤치마킹)

디자인 입력이 없으므로 토스(Toss) 패턴을 기준으로 화면을 제안한다. 화면별 참고 패턴은 아래 와이어프레임에 1줄씩 명시한다.

사례패턴이 설계에 반영미반영
토스 통합검색단일 검색 필드 상단 고정 + 그 아래 세그먼트 카테고리 탭(전체/…)으로 결과 유형 필터. 결과는 절제된 카드 리스트, accent는 최소catalog 화면: 상단 검색 입력 1개 + itemType SegmentedControl + sellerType 보조 세그먼트. 결과는 CatalogItemCard 리스트검색어 자동완성·추천어(랭킹 고도화는 PRD Non-Goal)
토스 결제/주문 내역시간 역순 리스트, 각 행에 상태 배지 + 절제된 메타(시각·금액), 한 화면 한 과업. 필터는 상단 세그먼트로 조용히orders 화면: createdAt desc 리스트, OrderHistoryItemCard에 orderType 배지 + status + 결제 연계. orderType/status 세그먼트 필터명세·영수증 상세(원본 도메인 상세로 이관)
토스 디자인 시스템(TDS) — 단일 CTA·절제된 색·충분한 여백 (TDS)화면당 accent 1곳, 위계 단순화, 여백으로 그룹핑accent는 활성 세그먼트·재시도 버튼에만. 배지는 채도 낮은 tint
토스 오류/부분 상태 처리실패를 화면 전체 에러로 막지 않고 상단 안내 배너 + 나머지 정상 노출PartialFailureBanner(failedDomains 있을 때만) — 조회된 결과는 그대로 보여주고 실패 도메인만 안내

Detail Design

화면 목록

화면라우트인증소비 API참고 토스 패턴
통합 상품 검색/catalog (신규 스택 라우트)불필요(permitAll)GET /api/catalog토스 통합검색(검색 필드 + 세그먼트 필터)
내 주문 통합 조회/orders (신규 스택 라우트)필요(authenticated)GET /api/orders토스 결제/주문 내역(시간 역순 + 상태 배지)

라우트 명명: 기존 app/order/(단수, goods 전용)와 충돌을 피하려 통합 화면은 app/orders/(복수, BE /api/orders와 일치). 통합 검색은 기존 (tabs)/search(시설)와 구분해 app/catalog/.

텍스트 와이어프레임 — ① 통합 상품 검색 (/catalog)

토스 통합검색 패턴: 상단 단일 검색 필드 → 세그먼트 카테고리 탭 → 절제된 결과 카드 리스트. accent는 활성 세그먼트에만.

┌─────────────────────────────────────┐
│  ← 통합 검색                          │  ← 헤더(뒤로가기 + 타이틀)
├─────────────────────────────────────┤
│  🔍 [ 요가                     ✕ ]   │  ← 검색 입력(디바운스 300ms, clear 버튼)
├─────────────────────────────────────┤
│  [전체][상품][한정판][티켓][클래스][모집]│  ← itemType SegmentedControl(가로 스크롤)
│  판매자: [전체] [중고] [브랜드]         │  ← sellerType 보조 세그먼트(itemType=상품일 때만 노출)
├─────────────────────────────────────┤
│  ⚠ 티켓 정보는 잠시 후 다시 시도해 주세요 │  ← PartialFailureBanner(failedDomains 있을 때만)
├─────────────────────────────────────┤
│  ┌───────────────────────────────┐  │
│  │ [상품·중고]  요가매트 프리미엄     │  │  ← CatalogItemCard: itemType 배지 + sellerType 배지
│  │            32,000원   2일 전      │  │     제목 / 가격(KRW) / createdAt
│  ├───────────────────────────────┤  │
│  │ [클래스]     아침 요가 클래스      │  │
│  │            15,000원   1일 전      │  │
│  ├───────────────────────────────┤  │
│  │ [티켓]       요가 페스티벌         │  │  ← price=null → "가격 상세 확인"
│  │            가격 상세 확인  방금     │  │
│  └───────────────────────────────┘  │
│         (탭 → 원본 detailPath로 이동)   │
└─────────────────────────────────────┘

텍스트 와이어프레임 — ② 내 주문 통합 조회 (/orders)

토스 결제/주문 내역 패턴: 시간 역순 단일 리스트, 각 행 상태 배지 + 절제된 메타. 필터는 상단 세그먼트로 조용히.

┌─────────────────────────────────────┐
│  ← 내 주문 내역                       │
├─────────────────────────────────────┤
│  유형: [전체][예약][티켓][상품][모집]   │  ← orderType SegmentedControl(가로 스크롤)
│  상태: [전체][결제완료][대기][취소]      │  ← status 보조 세그먼트(옵션)
├─────────────────────────────────────┤
│  ⚠ 일부 주문을 불러오지 못했어요        │  ← PartialFailureBanner(failedDomains 있을 때만)
├─────────────────────────────────────┤
│  ┌───────────────────────────────┐  │
│  │ [예약]              결제완료 ●   │  │  ← OrderHistoryItemCard: orderType 배지 + status
│  │ 강남 풋살장 예약                  │  │     (주 표시명 = item.title, BE 계약에 title 확정)
│  │ 결제 #4821        3일 전         │  │  ← paymentId 있으면 "결제 #id", 없으면 "미결제"
│  ├───────────────────────────────┤  │
│  │ [상품]              배송중 ●     │  │
│  │ 요가매트 프리미엄 외 1건           │  │     (title 누락 시에만 "유형명 #sourceId" fallback)
│  │ 결제 #4790        5일 전         │  │
│  └───────────────────────────────┘  │
│         (탭 → 원본 detailPath로 이동)   │
└─────────────────────────────────────┘

화면별 상태 표 (loading / empty / error / success + 부분 실패)

① 통합 상품 검색

상태트리거UI
loadingisLoading(첫 조회·검색어 변경)LoadingView variant="skeleton"(카드 3개 스켈레톤). 검색 입력·세그먼트는 계속 조작 가능
emptysuccess && items.length===0EmptyState message="검색 결과가 없어요" description="다른 검색어나 필터를 시도해 보세요"
errorisError(전체 요청 실패·네트워크)ErrorView message="검색 결과를 불러오지 못했어요" onRetry=refetch
successitems.length>0CatalogItemCard 리스트(createdAt desc, BE 정렬 그대로)
부분 실패success && failedDomains.length>0리스트 상단 PartialFailureBanner(실패 도메인 라벨) + 조회된 항목 정상 노출. 전체 에러로 승격하지 않음

② 내 주문 통합 조회

상태트리거UI
loadingisLoadingLoadingView variant="skeleton"
emptysuccess && items.length===0EmptyState message="주문 내역이 없어요" description="예약·티켓·상품·모임 신청 내역이 여기에 모여요"
error(미인증 401)isError && status 401ErrorView message="로그인이 필요해요" onRetry=()→router.replace('/(auth)/login')(재시도 라벨 대신 “로그인하기”). 401은 be-client 인터셉터가 refresh 시도 후에도 실패하면 로그인 이동 — 화면은 방어적으로 로그인 유도 표시
error(그 외)isErrorErrorView message="주문 내역을 불러오지 못했어요" onRetry=refetch
successitems.length>0OrderHistoryItemCard 리스트(createdAt desc)
부분 실패success && failedDomains.length>0상단 PartialFailureBanner + 조회된 항목 정상 노출

테마 토큰 정의 표 (시맨틱 토큰 → 라이트/다크, theme/tokens.ts SSOT)

신규 토큰 추가 없음. 두 화면·신규 컴포넌트는 기존 토큰만 사용한다(최소주의). 사용 토큰과 용도:

시맨틱 토큰라이트다크이 기능에서의 용도
background#FFFFFF#17171C화면 배경
surface#F9FAFB#202027세그먼트 트랙·스켈레톤·B2C 배지 배경
surfaceElevated#FFFFFF#26262E카드 배경·검색 입력·활성 세그먼트·배너 배경
textPrimary#191F28#F2F4F6항목 제목
textSecondary#4E5968#B0B8C1메타(가격·시각·B2C 배지 텍스트·배너 문구)
textTertiary#8B95A1#6B7684placeholder·비활성 세그먼트
border#E5E8EB#2E2E36카드 구분선·입력 테두리·배너 테두리
accent#3182F6#4E93FB활성 세그먼트·B2B(브랜드) 배지·재시도 버튼(화면당 1곳 원칙 준수)
accentText#FFFFFF#FFFFFFaccent 위 텍스트(B2B 배지 글자)
warning#FF9500#FF9F0APartialFailureBanner 좌측 강조 바·아이콘(부분 실패 신호)
danger#F04452#F76A78ErrorView 메시지(키트 내부)
success#12B886#2AC29B주문 status “결제완료” 상태 점(dot)

itemType 배지 색: 유형별 색을 새로 만들지 않는다 — 배지는 surface(배경)+textSecondary(글자) 중립 tint로 통일하고 라벨 텍스트로 유형을 구분한다(토스식 절제). 유일한 색 강조는 sellerType의 B2B(accent) 배지 하나뿐이라 “화면당 accent 1곳”과 정합.

컴포넌트 트리 (컨테이너/프레젠테이션 분리)

app/catalog/index.tsx        [컨테이너] CatalogScreen
  ├─ useCatalogSearch()       (훅: 서버 상태)  ← lib/useCatalogSearch.ts
  ├─ useState(keyword, itemType, sellerType)  (지역 상태)
  ├─ CatalogSearchControls   [프레젠테이션] 검색 입력 + itemType/sellerType SegmentedControl
  ├─ PartialFailureBanner    [프레젠테이션] failedDomains 라벨
  ├─ LoadingView / EmptyState / ErrorView   (기존 키트)
  └─ FlatList<CatalogItem>
        └─ CatalogItemCard   [프레젠테이션]
              └─ SellerTypeBadge  [프레젠테이션] (PRODUCT 항목만)

app/orders/index.tsx         [컨테이너] OrderHistoryScreen
  ├─ useOrderHistory()        (훅: 서버 상태)  ← lib/useOrderHistory.ts
  ├─ useState(orderType, status)              (지역 상태)
  ├─ SegmentedControl × 2     (기존 키트, orderType/status 필터 — inline)
  ├─ PartialFailureBanner    [프레젠테이션]
  ├─ LoadingView / EmptyState / ErrorView   (기존 키트)
  └─ FlatList<OrderHistoryItem>
        └─ OrderHistoryItemCard  [프레젠테이션]

재사용 컴포넌트 식별

컴포넌트신규/기존프레젠테이션?재사용 범위
LoadingView·ErrorView·EmptyState·SegmentedControl·ThemedText·Card기존(components/ui)그대로 사용
PartialFailureBanner신규(components/common)catalog·orders 양쪽 공유(제네릭: labels: string[])
CatalogSearchControls신규(components/catalog)catalog 전용
CatalogItemCard신규(components/catalog)catalog 전용
SellerTypeBadge신규(components/catalog)CatalogItemCard 내부(PRODUCT 항목)
OrderHistoryItemCard신규(components/order)orders 전용
  • 컨테이너(화면)는 훅 호출·지역 상태·상태 분기만. 데이터 가공(가격 포맷·상대 시각·detailPath 이동)은 프레젠테이션 컴포넌트 또는 lib/*-format.ts 유틸로(컴포넌트 내 비즈니스 로직 금지, no-logic-in-component).
  • 상대 시각(“2일 전”) 포맷은 기존 lib 포맷 유틸 관례를 따라 순수 함수로 분리(기존 lib/*-format.ts 패턴).

상태관리 설계 (서버/클라이언트 구분, 채택 근거)

상태종류저장소근거
catalog 검색 결과서버TanStack Query (useCatalogSearch)서버 데이터는 Query 캐시가 SSOT. 스토어 복사 금지(no-global-by-default). queryKey에 keyword·itemType·sellerType·page 포함 → 파라미터별 캐시
order 이력서버TanStack Query (useOrderHistory)동일. queryKey에 orderType·status·page 포함
keyword 입력값지역useState + 300ms 디바운스화면 밖으로 공유 안 됨 → 전역 승격 근거 없음
itemType·sellerType·status 필터지역useState동일
테마전역기존 Zustand themeStore(변경 없음)이미 전역 — 재사용만
인증(accessToken)전역기존 Zustand authStore(변경 없음)이미 전역 — /orders가 소비만
  • 낙관적 업데이트·재시도 커스텀 없음: 두 화면은 읽기 전용. 재요청은 Query의 refetch/staleTime(기존 30분) 기본값을 쓴다. 낙관적 업데이트는 쓰기가 없어 불필요(미채택).
  • 전역 스토어 신설 없음: 검색·필터 상태는 지역. “일단 전역”을 반려하고 지역 상태로 유지.

API 연동 표 (화면 → 엔드포인트 → 훅 → 에러 처리)

화면엔드포인트(BE 계약)api 함수Query 훅파라미터에러 처리
catalogGET /api/catalog?keyword=&itemType=&sellerType=&page=&size=CatalogSearchResponseapi/catalog.ts#getCataloglib/useCatalogSearch.ts#useCatalogSearchkeyword?·itemType?·sellerType?·page(0)·size(20)전체 실패 → ErrorView+refetch. failedDomains(부분 실패) → PartialFailureBanner, 결과는 정상 노출
ordersGET /api/orders?orderType=&status=&page=&size= (authenticated) → OrderHistoryResponseapi/orderHistory.ts#getOrderHistorylib/useOrderHistory.ts#useOrderHistoryorderType?·status?·page(0)·size(20)401 → 로그인 유도(be-client 인터셉터가 refresh 선처리). 그 외 → ErrorView+refetch. failedDomains → 배너

응답 타입(FE, BE 계약과 필드·타입 일치 — api/catalog-types.ts·api/order-history-types.ts)

// api/catalog-types.ts
export type CatalogItemType = 'PRODUCT' | 'LIMITED_DROP' | 'TICKET' | 'PROGRAM' | 'RECRUITMENT';
export type SellerType = 'B2C' | 'B2B';
export interface CatalogItem {
  itemType: CatalogItemType;
  sourceId: number;
  title: string;
  price: number | null;          // KRW. TICKET 등 대표가 없으면 null → "가격 상세 확인"
  sellerType: SellerType | null; // PRODUCT만 값
  status: string;                // 원본 status enum name
  detailPath: string;            // 예: "/products/123"
  createdAt: string;             // ISO-8601
}
export interface CatalogSearchResponse {
  items: CatalogItem[];
  page: number;
  size: number;
  failedDomains: CatalogItemType[];
}
 
// api/order-history-types.ts
export type OrderType = 'BOOKING' | 'TICKETING' | 'GOODS' | 'RECRUITMENT';
export interface OrderHistoryItem {
  orderType: OrderType;
  sourceId: number;
  title: string;                 // 항목 표시명 (senior-pm 승격 → BE 계약에 추가 확정)
  status: string;
  paymentId: number | null;
  detailPath: string;
  createdAt: string;
}
export interface OrderHistoryResponse {
  items: OrderHistoryItem[];
  page: number;
  size: number;
  failedDomains: OrderType[];
}

BE priceBigDecimal(KRW). FE는 JSON 수치(number)로 받되, 정밀도 이슈가 없는 원화 정수 판매가라 number 매핑이 안전하다. TICKET은 null → “가격 상세 확인” 렌더(BE TDD 가격 정규화 확정 반영). detailPath는 BE가 원본 상세 경로 문자열을 준다. 앱 라우트와 정확히 일치하지 않을 수 있어(웹 경로 형태일 가능성), 항목 탭 시 detailPath{도메인}/{id}를 파싱해 ROUTES로 매핑하는 얇은 어댑터(lib/catalog-navigation.ts)를 둔다 — 파싱 실패 시 이동 무시(안전). itemType↔ROUTES 매핑은 이 유틸이 소유(컴포넌트에 분기 로직 금지).

라우팅·내비게이션 흐름

flowchart LR
    Home["(tabs)/index 홈"]
    Me["(tabs)/me 마이"]
    Catalog["/catalog 통합검색"]
    Orders["/orders 통합주문내역"]
    Login["/(auth)/login"]
    Product["/product/:id"]
    Event["/event/:id"]
    Facility["/facility/:id"]
    Recruitment["/recruitments/:id"]
    LimitedDrop["/limited-drop/:id"]
    Home -->|통합 검색 진입| Catalog
    Me -->|내 주문 내역 진입| Orders
    Orders -->|미인증 401| Login
    Catalog -->|항목 탭 detailPath 매핑| Product
    Catalog --> Event
    Catalog --> Facility
    Catalog --> Recruitment
    Catalog --> LimitedDrop
    Orders -->|항목 탭 detailPath 매핑| Product
  • lib/navigation.ts ROUTEScatalog: '/catalog', orders: '/orders' 추가(와이어업 티켓).
  • 진입점: 홈에 “통합 검색” 진입, 마이 탭에 “내 주문 내역”(통합) 진입. 기존 시설 search 탭·goods /order는 유지(중복 아님 — 범위/도메인 다름).

Testing Plan (implementer TDD 입력 — 사용자 관점 동작 검증, @testing-library/react-native + Jest)

테스트명에 티켓ID 금지(동작 설명 이름만). 각 대상 해피·실패·엣지 최소 3개. 서버 상태는 axios-mock-adapter 또는 훅 모킹으로 주입.

대상유형테스트 케이스
useCatalogSearch훅(renderHook)keyword·itemType·sellerType이 요청 파라미터로 전달된다 / 파라미터가 바뀌면 새 queryKey로 재조회한다 / 응답 failedDomains를 그대로 반환한다
useOrderHistoryorderType·status가 요청 파라미터로 전달된다 / 인증 헤더 없이 401이면 에러 상태가 된다 / 빈 결과를 정상 반환한다
CatalogItemCard컴포넌트제목·가격(KRW 포맷)을 렌더한다 / price=null이면 “가격 상세 확인”을 렌더한다 / PRODUCT면 sellerType 배지를, 그 외 유형이면 배지를 렌더하지 않는다 / 탭하면 detailPath 매핑 경로로 이동한다
SellerTypeBadge컴포넌트B2B면 “브랜드” 라벨을 accent 배지로 렌더한다 / B2C면 “중고” 라벨을 중립 배지로 렌더한다 / null이면 아무것도 렌더하지 않는다
OrderHistoryItemCard컴포넌트orderType 배지와 status를 렌더한다 / paymentId가 있으면 “결제 id”를, 없으면 “미결제”를 렌더한다 / 탭하면 detailPath 경로로 이동한다
PartialFailureBanner컴포넌트labels가 있으면 안내 문구와 실패 유형을 렌더한다 / labels가 빈 배열이면 아무것도 렌더하지 않는다 / 문구가 alert role로 노출된다
CatalogSearchControls컴포넌트검색 입력 타이핑이 디바운스 후 onChange를 호출한다 / itemType 세그먼트 선택이 콜백을 호출한다 / itemType이 상품일 때만 sellerType 세그먼트가 보인다
CatalogScreen컴포넌트(통합)로딩 시 스켈레톤을 보여준다 / 결과 0건이면 empty 문구를 보여준다 / 요청 실패면 에러+재시도를 보여준다 / 성공 시 카드 리스트를 보여준다 / failedDomains가 있으면 배너와 결과를 함께 보여준다
OrderHistoryScreen컴포넌트(통합)성공 시 시간 역순 리스트를 보여준다 / 401이면 로그인 유도를 보여준다 / empty·부분 실패 상태를 처리한다 / orderType 필터 변경 시 재조회한다
라이트/다크컴포넌트각 신규 컴포넌트가 두 모드에서 하드코딩 색 없이 토큰으로 렌더된다(useTheme 모킹으로 scheme 전환 렌더 스냅 아닌 동작 확인)

Release Scenario (기능 플래그·점진 공개)

  • 엔드포인트 미노출 = 사실상 OFF: 두 화면은 신규 추가라 기존 트래픽 영향 0. BE 파사드 API(/api/catalog·/api/orders)가 배포되기 전에는 진입점을 숨긴다.
  • 기능 플래그로 점진 공개: 레포에 이미 lib/feature-flags.ts(isFeatureEnabled, 탭 게이팅 선례 있음)가 있다. 진입점(홈 “통합 검색”·마이 “내 주문 내역”)을 catalog.enabled·orders.unified.enabled 플래그로 게이팅한다 — BE 준비 전엔 진입점 숨김, 준비 후 ON. 화면 라우트 자체는 배포하되 진입점만 토글(무중단, 재기동 불요).
  • 롤백: 플래그 OFF로 진입점 즉시 숨김. 라우트 파일은 남아도 진입 불가라 안전.
  • 의존: BE M1(catalog API)·M2(order API) 배포 완료가 각 화면 ON의 선행 조건. FE 구현은 BE 계약(이 문서 소비)만으로 병렬 진행 가능(모킹으로 TDD).

Open Questions

  • detailPath 형식이 앱 라우트와 어긋날 가능성 — BE가 웹 경로(/products/{id}) 문자열을 주므로, 앱은 itemType/orderType + sourceId로 ROUTES 매핑하는 어댑터를 두는 것이 더 견고할 수 있다. BE 실응답 확인 후 lib/catalog-navigation.ts 매핑 방식을 확정한다(1차: itemType 기준 매핑).
  • orders 화면의 항목 “표시명” — 확정: BE 계약에 title 추가됨(senior-pm 검증으로 역제안 승격). OrderHistoryItemCarditem.title을 주 표시명으로 렌더하고, “유형명 + sourceId”는 title 누락 시의 최후 fallback으로만 남긴다.
  • 깊은 페이지네이션·무한 스크롤 — 1차는 첫 페이지(size=20)만. 무한 스크롤은 BE의 글로벌 페이지네이션 안정화(BE Open Question) 후 후속.
  • 라이트/다크 외 시스템 폰트 스케일(접근성 큰 글씨) 대응은 기존 화면과 동일 수준으로 두고 별도 범위 밖.

Document History

날짜변경 내용
2026-07-08최초 작성 — PRD v2 + BE TDD 계약 기반. 플랫폼 판정(앱 전용, web은 운영자 포털이라 제외). 레포 정착 스택(Expo Router+TanStack Query+테마 토큰+UI 키트) 준수. 화면 2종·컴포넌트 4종 신규, 신규 토큰 0. 11개 티켓(wave 3/5/2/1)으로 분해