/* Denky — 메인 화면들: 홈 / 카탈로그 / 상세 */

/* =========================================================
   카테고리 가로 스크롤 레일 (세분화 카테고리)
   ========================================================= */
const RAIL_CATS = [
  { label: "티셔츠", type: "상의" },
  { label: "니트", type: "상의" },
  { label: "맨투맨", type: "상의" },
  { label: "후드티", type: "상의" },
  { label: "셔츠", type: "상의" },
  { label: "블라우스", type: "상의" },
  { label: "원피스", type: "원피스" },
  { label: "바지", type: "하의" },
  { label: "청바지", type: "하의" },
  { label: "스커트", type: "하의" },
  { label: "자켓", type: "아우터" },
  { label: "코트", type: "아우터" },
  { label: "패딩", type: "아우터" },
  { label: "가디건", type: "아우터" },
  { label: "점퍼", type: "아우터" },
];
const RAIL_TONES = ["#EDE9FE", "#E3E7F0", "#FAE9EE", "#ECECDF", "#E2ECF7", "#E3EEE5", "#F3ECDD", "#EBE0D4"];
// 종류별 세부 카테고리 칩 — 스마트앱(catalog_data.dart subcats)과 동일한 데이터
const SUBCATS = {
  "티셔츠": ["반팔", "긴팔", "7부", "민소매·나시", "크롭", "오버핏", "프린팅", "스트라이프"],
  "니트": ["라운드넥", "브이넥", "터틀넥(폴라)", "하프넥", "케이블", "크롭 니트", "니트 베스트"],
  "맨투맨": ["기모", "무지", "프린팅", "크롭", "오버핏", "하프집업"],
  "후드티": ["풀오버 후드", "후드 집업", "기모 후드", "크롭 후드"],
  "셔츠": ["기본 셔츠", "오버핏 셔츠", "체크 셔츠", "데님 셔츠", "린넨 셔츠", "크롭 셔츠"],
  "블라우스": ["기본", "프릴·러플", "퍼프소매", "새틴", "카라 블라우스"],
  "원피스": ["미니", "미디", "롱(맥시)", "셔츠 원피스", "니트 원피스", "점프수트", "투피스"],
  "바지": ["슬랙스", "코튼팬츠", "와이드", "스트레이트", "조거", "카고", "부츠컷"],
  "청바지": ["스키니", "슬림", "스트레이트", "와이드", "부츠컷", "밴딩 데님"],
  "스커트": ["미니", "미디", "롱", "플리츠", "랩", "데님 스커트"],
  "자켓": ["데님 자켓", "가죽(레더)", "트러커", "무스탕", "사파리"],
  "코트": ["발마칸", "싱글", "더블", "하프", "롱코트", "핸드메이드", "체크 코트"],
  "패딩": ["숏패딩", "롱패딩", "경량패딩", "베스트", "후드 패딩"],
  "가디건": ["크롭", "롱", "브이넥", "니트 가디건", "케이블"],
  "점퍼": ["바람막이", "항공점퍼(MA-1)", "플리스", "블루종", "코치자켓"],
};

// 라벨로 정확히 거를 수 있는 세부칩 → /products/filter 의 filters 에 병합할 라벨 조건.
// 키워드(이름 부분일치)보다 라벨이 '실측으로 더 많이·정확히' 잡는 칩만 등록한다. (스마트앱과 동일)
// (전수 감사 2026-07-22: 프린팅 193→444 · 무지 54→199 · 스트라이프 117→160 · 체크셔츠 15→151.
//  반팔·긴팔·민소매 등은 키워드가 더 커서 매핑하지 않음 — 추측으로 늘리지 말 것)
const LABEL_SUBCHIPS = {
  "체크 셔츠": { "무늬": ["체크"] },
  "체크 코트": { "무늬": ["체크"] },
  "프린팅": { "무늬": ["그림무늬"] },
  "스트라이프": { "무늬": ["줄무늬"] },
  "무지": { "무늬": ["무지"] },
  "린넨 셔츠": { "소재": ["린넨"] },
  "데님 자켓": { "소재": ["데님"] },
};
// 남성 선택 시 카테고리 레일에서 숨길 '여성 전용' 카테고리 (원피스·스커트·블라우스)
const FEMALE_ONLY_CATS = ["원피스", "스커트", "블라우스"];
// 성별에 맞는 카테고리 목록 — 남성이면 여성 전용 제외, 여성/전체면 전부.
function railCatsFor(gender) {
  return gender === "남성" ? RAIL_CATS.filter((c) => !FEMALE_ONLY_CATS.includes(c.label)) : RAIL_CATS;
}
// 성별로 상품을 거릅니다. (전체=그대로, 그 외=해당 성별+공용 · 성별 없으면 여성 취급) — 앱의 filterGender 와 동일.
function filterGenderList(arr, gender) {
  return gender === "전체" ? arr : arr.filter((p) => (p.gender || "여성") === gender || p.gender === "공용");
}
// 종류별 라인 아이콘(투명 배경 PNG, #1F2937). 톤 배경 위에 contain으로 올림.
const CAT_SLUG = {
  "티셔츠": "tshirt", "니트": "knit", "맨투맨": "sweatshirt", "후드티": "hoodie",
  "셔츠": "shirt", "블라우스": "blouse", "원피스": "dress", "바지": "pants",
  "청바지": "jeans", "스커트": "skirt", "자켓": "jacket", "코트": "coat",
  "패딩": "padding", "가디건": "cardigan", "점퍼": "bomber", "트레이닝세트": "training", "정장": "suit",
};

// compact: 카탈로그 상단용 — 성별 탭 없이 카드 레일만, active(선택된 종류)는 테두리로 강조.
// onPick 이 있으면 화면 이동 대신 그 콜백을 부른다(카탈로그 안에서 종류만 전환).
function CategoryRail({ go, gender, setGender, compact = false, active = null, onPick = null }) {
  const scroller = React.useRef(null);
  const [edge, setEdge] = React.useState({ l: false, r: false });
  const cats = railCatsFor(gender); // 남성이면 여성 전용 카테고리 제외
  const pick = (c) => (onPick ? onPick(c) : go("catalog", { labelFilter: { "종류": [c.label] } }));

  const by = (dx) => {
    const el = scroller.current; if (!el) return;
    const start = el.scrollLeft;
    const end = Math.max(0, Math.min(start + dx, el.scrollWidth - el.clientWidth));
    const t0 = performance.now(), dur = 380;
    const ease = (x) => 1 - Math.pow(1 - x, 3);
    let done = false;
    const step = (t) => {
      const p = Math.min((t - t0) / dur, 1);
      el.scrollLeft = start + (end - start) * ease(p);
      if (p < 1) requestAnimationFrame(step); else done = true;
    };
    requestAnimationFrame(step);
    // fallback: rAF가 throttle되는 환경에서도 최종 위치 보장
    setTimeout(() => { if (!done) el.scrollLeft = end; }, dur + 80);
  };

  // 커서가 레일 양끝(약 96px) 근처로 가면 해당 화살표를 노출
  const onMove = (e) => {
    const el = scroller.current; if (!el) return;
    const r = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - r.left;
    const ZONE = 96;
    const canL = el.scrollLeft > 4;
    const canR = el.scrollLeft < el.scrollWidth - el.clientWidth - 4;
    setEdge({ l: canL && x < ZONE, r: canR && x > r.width - ZONE });
  };
  const onLeave = () => setEdge({ l: false, r: false });

  return (
    <section className={compact ? "" : "wrap section"} style={compact ? { marginBottom: 16 } : { paddingTop: 28, paddingBottom: 20 }}>
      {/* 성별 탭 — 기본 여성. 남성 선택 시 아래 카테고리 카드에서 여성 전용(원피스/스커트/블라우스) 숨김 */}
      {/* 창 크기와 무관하게 항상 가운데 정렬 (좁아지면 줄바꿈). compact(카탈로그)에서는 숨김 — 홈에서만. */}
      {!compact && (
      <div className="row" style={{ gap: 8, marginBottom: 16, justifyContent: "center", flexWrap: "wrap" }}>
        {["여성", "남성", "전체"].map((g) => {
          const on = gender === g;
          return (
            <button key={g} onClick={() => setGender(g)}
              style={{
                height: 36, padding: "0 18px", borderRadius: 999, cursor: "pointer",
                fontSize: 13.5, fontWeight: 700,
                border: on ? "none" : "1px solid var(--border)",
                background: on ? "var(--primary)" : "var(--surface)",
                color: on ? "#fff" : "var(--sub)",
              }}>
              {g}
            </button>
          );
        })}
      </div>
      )}
      <div className="cat-rail-wrap" onMouseMove={onMove} onMouseLeave={onLeave}>
        <button className="rail-arrow rail-arrow-l" onClick={() => by(-300)} aria-label="이전" tabIndex={edge.l ? 0 : -1}
          style={{ opacity: edge.l ? 1 : 0, pointerEvents: edge.l ? "auto" : "none" }}>
          <Icon name="chevR" size={18} style={{ transform: "rotate(180deg)" }} />
        </button>
        <div ref={scroller} className="cat-rail">
          {cats.map((c, i) => {
            const on = active === c.label; // 카탈로그에서 현재 보고 있는 종류는 테두리+굵은 라벨로 강조
            return (
            <button key={c.label} className="cat-tile" onClick={() => pick(c)}>
              <div className="cat-thumb" style={{ background: RAIL_TONES[i % RAIL_TONES.length], boxShadow: on ? "0 0 0 2.5px var(--primary)" : "none" }}>
                {CAT_SLUG[c.label]
                  ? <img src={`assets/cat/${CAT_SLUG[c.label]}.png`} alt={c.label} style={{ width: "100%", height: "100%", objectFit: "contain", padding: "16%", display: "block" }} />
                  : <GarmentIcon label={c.label} tone={RAIL_TONES[i % RAIL_TONES.length]} ink="#8A8FA0" />}
              </div>
              <span className="cat-label" style={on ? { fontWeight: 800, color: "var(--primary)" } : undefined}>{c.label}</span>
            </button>
            );
          })}
        </div>
        <button className="rail-arrow rail-arrow-r" onClick={() => by(300)} aria-label="다음" tabIndex={edge.r ? 0 : -1}
          style={{ opacity: edge.r ? 1 : 0, pointerEvents: edge.r ? "auto" : "none" }}>
          <Icon name="chevR" size={18} />
        </button>
      </div>
    </section>
  );
}

/* =========================================================
   회전 링 캐러셀 (히어로) — 모델 중심으로 17개 카테고리 카드가 도는 UI
   퀸잇 광고 스타일: 앞쪽 ~6장만 보이고, 뒤로 가면 사라졌다 '카테고리 순서대로' 재등장.
   드래그/휠로 직접 회전. 카드 클릭 → 그 종류 카탈로그.
   ⏸ Phase2(미구현): 카드가 정면(θ≈0) 통과 시 '그 옷을 입은 모델 이미지'로 교체
      — 지금은 front 플래그(정면 감지)만 만들어 두고 강조만 함. 착용 이미지 준비되면 여기 끼우면 됨.
   ========================================================= */
function FittingCarousel({ go }) {
  const [, force] = React.useState(0);            // rAF 리렌더 트리거
  const [hover, setHover] = React.useState(false);   // 마우스 올림 — 좌우 회전 화살표 힌트 표시용
  const [hinted, setHinted] = React.useState(false);  // 한 번이라도 돌려봤나 — 이후 안내 배지를 지움(시선 정리)
  const rotRef = React.useRef(0);                  // 링 회전각(도)
  const draggingRef = React.useRef(false);
  const pausedRef = React.useRef(false);           // 마우스 올리면 자동회전 일시정지
  const lastXRef = React.useRef(0);
  const movedRef = React.useRef(false);            // 드래그로 움직였는지(클릭 오발동 방지)
  const failedWearRef = React.useRef(new Set());   // 없는(404) 착용샷 기억 → 베이스로 폴백(깜빡임 방지)
  const N = RAIL_CATS.length;                       // 15
  const STEP = 360 / N;

  // 자동 회전 (드래그/호버 아닐 때 천천히)
  React.useEffect(() => {
    let prev = performance.now();
    let raf = 0;
    const tick = (now) => {
      const dt = now - prev; prev = now;
      if (!pausedRef.current && !draggingRef.current) {
        rotRef.current = (rotRef.current + dt * 0.011) % 360;   // 초당 ~11도
        force((v) => v + 1);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  const clientX = (e) => (e.touches && e.touches[0] ? e.touches[0].clientX : e.clientX);
  const onDown = (e) => { draggingRef.current = true; movedRef.current = false; lastXRef.current = clientX(e); };
  const onMove = (e) => {
    if (!draggingRef.current) return;
    const x = clientX(e); const dx = x - lastXRef.current; lastXRef.current = x;
    if (Math.abs(dx) > 2) { movedRef.current = true; if (!hinted) setHinted(true); }  // 직접 돌려봤으면 안내 배지 제거
    rotRef.current = (rotRef.current + dx * 0.35) % 360; force((v) => v + 1);
  };
  const onUp = () => { draggingRef.current = false; };
  const onWheel = (e) => {
    if (!hinted) setHinted(true);   // 휠로 돌려봤으면 안내 배지 제거
    rotRef.current = (rotRef.current + (e.deltaY || e.deltaX) * 0.12) % 360; force((v) => v + 1);
  };

  const W = 920, H = 540;                 // 무대 크기
  const cx = W / 2;
  const modelCy = H * 0.5;                // 모델 세로 중심 (스테이지 정중앙)
  const ringCy = H * 0.5;                 // 링(궤도) 중심도 모델 중심에 맞춰 '모델을 감싸고' 돌게
  const RX = 340, RY = 158;               // 타원 반경(세로를 크게 → '옆으로'가 아니라 '감싸고 도는' 3D 궤도)
  const rot = rotRef.current;
  // 착용샷/베이스 프레임(619×984) 안에서 인물이 가로 ~78%(x≈484)에 몰려 있음.
  // 이미지 중심을 cx에 둬도 인물은 오른쪽으로 치우쳐 보이므로, 인물이 cx에 오도록 왼쪽으로 보정.
  const MODEL_ASPECT = 619 / 984;         // 착용샷 프레임 비율
  const modelH = H * 0.86;
  const modelW = modelH * MODEL_ASPECT;   // objectFit:contain 렌더 시 실제 이미지 폭
  const PERSON_CX = 0.782;                 // 프레임 내 인물 가로중심 비율 (x484/619)
  const modelDx = (PERSON_CX - 0.5) * modelW;  // 인물을 cx로 옮길 좌측 이동량(px)

  // 각 카드의 화면 배치 (co: +1=정면(앞·아래·큼) … -1=뒤(위·작음·모델 뒤))
  const cards = RAIL_CATS.map((c, i) => {
    const ang = (i * STEP + rot) * Math.PI / 180;
    const s = Math.sin(ang), co = Math.cos(ang);
    const depth = (co + 1) / 2;                                  // 0(뒤)~1(정면)
    const x = cx + RX * s;
    const y = ringCy + RY * co;                                  // 정면=아래(모델 앞), 뒤=위(모델 뒤)
    const scale = 0.5 + depth * 0.62;                            // 뒤 0.5 ~ 앞 1.12
    // ★뒤 카드도 '반투명하게' 계속 보이게(사라지지 않음) → 모델을 감싸고 도는 궤도가 보인다.
    const opacity = 0.26 + depth * 0.68;                         // 뒤 0.26 ~ 앞 0.94 (앞쪽 ~6장이 또렷)
    const z = Math.round(depth * 200);                           // 모델(z=100) 기준: 앞>100(앞), 뒤<100(뒤)
    return { c, i, x, y, scale, opacity, z, depth, front: depth > 0.93 };
  });

  // ★착용샷 전환 — 중앙 모델은 '이미지 하나만'. 기본은 누끼 베이스, '원피스' 카드가 정면에
  //   '오기 전'(depth>0.55)부터 원피스 입은 모델로 교체하고 지나가면 다시 베이스로.
  //   (우선 원피스만. 다른 카테고리는 착용샷 준비되면 WEAR_SRC 에 추가하면 자동 확장)
  const BASE_SRC = "assets/model-base.png";
  // 17개 전부 매핑(파일 없으면 onError 로 베이스 폴백 + failedWearRef 기억)
  const WEAR_SRC = {};
  RAIL_CATS.forEach((rc) => { if (CAT_SLUG[rc.label]) WEAR_SRC[rc.label] = `assets/wear-model/${CAT_SLUG[rc.label]}.png`; });
  // 카드가 '모델 앞(정면)'에 왔을 때만 교체 — 옆이 아니라 가장 앞 카드(depth 최대)를 기준으로.
  let centerSrc = BASE_SRC, bestDepth = 0.9;
  for (const cd of cards) {
    const src = WEAR_SRC[cd.c.label];
    if (src && cd.depth > bestDepth && !failedWearRef.current.has(src)) { bestDepth = cd.depth; centerSrc = src; }
  }

  return (
    <>
    <div
      style={{ position: "relative", width: "100%", maxWidth: W, margin: "0 auto", height: H, userSelect: "none", touchAction: "pan-y", cursor: draggingRef.current ? "grabbing" : "grab" }}
      onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp}
      onMouseEnter={() => { pausedRef.current = true; setHover(true); }}
      onMouseLeave={() => { pausedRef.current = false; draggingRef.current = false; setHover(false); }}
      onTouchStart={onDown} onTouchMove={onMove} onTouchEnd={onUp}
      onWheel={onWheel}
    >
      {/* 착용샷 미리 로드(스왑 즉시 반영) — 화면엔 안 보임 */}
      {Object.values(WEAR_SRC).map((s) => <img key={s} src={s} alt="" style={{ display: "none" }} />)}
      {/* 중앙 모델 — 이미지 하나만. 기본 누끼 / 원피스 정면 접근 시 착용샷으로 교체 */}
      <img src={centerSrc} alt="모델" draggable="false"
        onError={(e) => { failedWearRef.current.add(centerSrc); if (!e.currentTarget.src.endsWith("model-base.png")) e.currentTarget.src = BASE_SRC; }}
        style={{ position: "absolute", left: cx, top: modelCy, transform: `translate(calc(-50% - ${modelDx}px), -50%)`, height: modelH, objectFit: "contain", zIndex: 100, pointerEvents: "none" }} />
      {/* 회전 카드들 (앞=크고 또렷·모델 앞 / 뒤=작고 반투명·모델 뒤 → 빙글빙글 도는 궤도) */}
      {cards.map(({ c, i, x, y, scale, opacity, z, front }) => (
        <button key={c.label} title={c.label}
          onClick={() => { if (!movedRef.current) go("catalog", { labelFilter: { "종류": [c.label] } }); }}
          style={{
            position: "absolute", left: x, top: y, transform: `translate(-50%,-50%) scale(${scale})`,
            zIndex: z, opacity, transition: draggingRef.current ? "none" : "opacity .18s linear",
            width: 108, borderRadius: 14, overflow: "hidden", padding: 0, cursor: "pointer",
            // ★카드 반투명(글래스) — 모델을 덜 가림
            background: "rgba(255,255,255,0.55)", backdropFilter: "blur(2px)", WebkitBackdropFilter: "blur(2px)",
            border: front ? "2px solid var(--primary)" : "1px solid rgba(255,255,255,0.7)",
            boxShadow: front ? "0 14px 34px rgba(124,58,237,.22)" : "0 6px 16px rgba(0,0,0,.10)",
            pointerEvents: opacity < 0.55 ? "none" : "auto",
          }}>
          {/* 카드는 '옷 이미지만'(실제 판매 옷 사진). 실패 시 아이콘 폴백. 중앙 모델만 정면 카드 옷으로 갈아입음. */}
          <div style={{ aspectRatio: "3 / 4", overflow: "hidden", background: "rgba(255,255,255,0.65)", display: "grid", placeItems: "center" }}>
            {CAT_SLUG[c.label]
              ? <img src={`assets/garment/${CAT_SLUG[c.label]}.png`} alt={c.label} draggable="false"
                  onError={(e) => { if (!e.currentTarget.src.includes("/cat/")) e.currentTarget.src = `assets/cat/${CAT_SLUG[c.label]}.png`; }}
                  style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
              : <GarmentIcon label={c.label} tone={RAIL_TONES[i % RAIL_TONES.length]} ink="#8A8FA0" />}
          </div>
          <div style={{ fontSize: 12, fontWeight: 700, padding: "6px 0", textAlign: "center", color: front ? "var(--primary)" : "var(--ink)", background: "rgba(255,255,255,0.6)" }}>{c.label}</div>
        </button>
      ))}
      {/* (호버 좌우 화살표 힌트 삭제 — 사용자 요청 07-27. 조작 안내는 아래 '드래그·휠' 배지가 담당) */}
    </div>
    {/* 안내 배지 — 예전엔 무대 밖 bottom:-20 의 작은 회색 글씨라 아래 카테고리 탭과 겹치고 잘 안 보였다.
        이제 '레이아웃 흐름 안'의 고대비 필(pill) 배지로 두어 겹침이 원천적으로 없고,
        사용자가 한 번 돌려보면(드래그/휠) 자연스럽게 사라져 시선을 정리한다. */}
    <div style={{ display: "flex", justifyContent: "center", marginTop: 10 }}>
      <div style={{
        display: "inline-flex", alignItems: "center", gap: 8, padding: "8px 16px", borderRadius: 999,
        background: "#fff", border: "1px solid rgba(0,0,0,0.08)", boxShadow: "0 4px 14px rgba(0,0,0,.10)",
        fontSize: 13, fontWeight: 700, color: "var(--ink)", pointerEvents: "none",
        opacity: hinted ? 0 : 1, visibility: hinted ? "hidden" : "visible",
        transition: "opacity .5s ease, visibility .5s",
      }}>
        {/* 좌우 드래그 아이콘 */}
        <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M7 12H2m0 0l3-3m-3 3l3 3M17 12h5m0 0l-3-3m3 3l-3 3" />
        </svg>
        드래그<span style={{ opacity: .45 }}>·</span>
        {/* 마우스 휠 아이콘 */}
        <svg width="13" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" strokeWidth="2.2" strokeLinecap="round" aria-hidden="true">
          <rect x="6.5" y="2.5" width="11" height="19" rx="5.5" />
          <line x1="12" y1="6.5" x2="12" y2="10" />
        </svg>
        휠로 돌려보세요<span style={{ opacity: .45, margin: "0 2px" }}>|</span>카드를 누르면 그 종류로 이동
      </div>
    </div>
    </>
  );
}

/* =========================================================
   홈
   ========================================================= */
function HomeScreen({ go, openProduct, tryProduct, t, auth }) {
  // 성별 탭 — 기본 여성. localStorage 로 유지(앱과 동일하게). 남성이면 여성 카테고리/상품을 숨김.
  const [gender, setGenderState] = React.useState(() => localStorage.getItem("denky-gender") || "여성");
  const setGender = (g) => { setGenderState(g); try { localStorage.setItem("denky-gender", g); sessionStorage.setItem("gender_explicit", "1"); } catch (e) {} };

  // 오늘의 풀코디 — 아바타가 오늘 추천 코디를 입은 렌더 이미지(스마트앱 홈과 동일).
  // 로그인 + 아바타·키가 있어야 뜬다. 자격 없음/미생성이면 null → 섹션 자체를 숨긴다.
  const [daily, setDaily] = React.useState(null);
  React.useEffect(() => {
    if (!(auth && auth.loggedIn)) { setDaily(null); return; }
    let alive = true;
    let timer = null;
    let tries = 0;
    // 오늘 날짜(KST) — 서버의 look_date 와 같은 기준
    const todayKst = () => new Date(Date.now() + 9 * 3600 * 1000).toISOString().slice(0, 10);
    const load = () => {
      API.dailyCoordi().then((d) => {
        if (!alive) return;
        setDaily(d);
        // 하루 첫 접속엔 서버가 '어제 코디'를 먼저 주고 오늘 것은 백그라운드로 생성한다.
        // 아직 오늘 날짜가 아니면 30초 간격으로 재조회(최대 10회)해 준비되는 즉시 교체한다.
        if (tries < 10 && (!d || (d.lookDate && d.lookDate !== todayKst()))) {
          tries += 1;
          timer = setTimeout(load, 30000);
        }
      }).catch(() => {});
    };
    load();
    return () => { alive = false; if (timer) clearTimeout(timer); };
  }, [auth && auth.loggedIn, auth && auth.name]);

  // 지금 보고 있는 코디가 '오늘 것'인지 (아니면 지난 코디를 임시로 보여주는 중)
  const dailyIsToday = !!daily && (!daily.lookDate
    || daily.lookDate === new Date(Date.now() + 9 * 3600 * 1000).toISOString().slice(0, 10));

  // 지금 인기 — 성별로 먼저 거른 뒤, 자체/입점(marketplace) 상품을 먼저 보여주고, 그 안에서 인기 표시 우선,
  // 그다음 도매매 위탁(consignment) 상품으로 32개까지 채웁니다. (자체/입점 우선 노출)
  const byPopular = (arr) => [...arr.filter((p) => p.popular), ...arr.filter((p) => !p.popular)];
  const genderFiltered = filterGenderList(DENKY.products, gender);
  const sellerFirst = genderFiltered.filter((p) => p.saleType === "marketplace");
  const consignmentRest = genderFiltered.filter((p) => p.saleType !== "marketplace");
  const popular = [...byPopular(sellerFirst), ...byPopular(consignmentRest)].slice(0, 32);

  return (
    <div className="page">
      {/* HERO — 회전 링 캐러셀 (기존 VIRTUAL FITTING 문구 + 모델A/B 비교 슬라이더 대체) */}
      <section style={{ background: "var(--surface)", overflow: "hidden" }}>
        <div className="wrap" style={{ padding: "20px 24px" }}>
          <FittingCarousel go={go} />
        </div>
      </section>

      {/* 오늘의 풀코디 — 아바타 착장 이미지(왼쪽) + 무드·이유·팁·착장 아이템(오른쪽). 자격 있을 때만. */}
      {daily && (
        <section className="wrap section" style={{ paddingTop: 24 }}>
          {/* 상단 — 아바타 풀코디 이미지(무드 뱃지 + 이유 오버레이) + 그 아래 코디 팁 (창 중앙 정렬) */}
          <div style={{ maxWidth: 380, margin: "0 auto" }}>
            <div style={{ position: "relative", borderRadius: 18, overflow: "hidden", background: "#F4F5F7" }}>
              <img src={daily.imageUrl} alt="오늘의 풀코디" style={{ display: "block", width: "100%", aspectRatio: "3 / 4", objectFit: "contain" }} />
              {daily.concept && (
                <span style={{ position: "absolute", top: 12, left: 12, padding: "5px 11px", borderRadius: 999, background: dailyIsToday ? "var(--primary)" : "rgba(0,0,0,.55)", color: "#fff", fontSize: 12, fontWeight: 800, boxShadow: "0 2px 8px rgba(0,0,0,.18)" }}>
                  {dailyIsToday ? "오늘의 무드" : "지난 무드"} · {daily.concept}
                </span>
              )}
              {/* 오늘 것이 아직이면 솔직하게 알린다 — 준비되면 폴링이 자동 교체한다 */}
              {!dailyIsToday && (
                <span style={{ position: "absolute", top: 12, right: 12, padding: "5px 11px", borderRadius: 999, background: "rgba(255,255,255,.92)", color: "var(--ink)", fontSize: 12, fontWeight: 700, boxShadow: "0 2px 8px rgba(0,0,0,.12)" }}>
                  오늘 무드 준비 중…
                </span>
              )}
              {daily.reason && (
                <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, padding: "28px 14px 12px", background: "linear-gradient(to bottom, transparent, rgba(0,0,0,.72))", color: "#fff", fontSize: 13, fontWeight: 600, lineHeight: 1.4, display: "flex", gap: 6 }}>
                  <Icon name="sparkle" size={14} />
                  <span>{daily.reason}</span>
                </div>
              )}
            </div>
            {/* 코디 팁 — 아바타 이미지 바로 아래 (중앙) */}
            {daily.tips.length > 0 && (
              <p className="t-small t-sub" style={{ margin: "10px 2px 0", textAlign: "center" }}>💡 {daily.tips.join(" · ")}</p>
            )}
          </div>
          {/* 하단 — 착장 아이템: 창 중앙에 모아 배치(고정폭 카드 + 가운데 정렬) */}
          <h3 className="t-h3" style={{ margin: "22px 0 12px", textAlign: "center" }}>착장 아이템</h3>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, 200px)", justifyContent: "center", gap: 20 }}>
            {daily.items.map((p) => (
              <ProductCard key={p.id} p={p} onOpen={openProduct} onTry={tryProduct} showInfo={t.cardInfo} />
            ))}
          </div>
          {/* 이 무드의 후보 — 오늘 코디를 짤 때 후보로 검토된 상품(상의15+하의15). 구 캐시엔 없어 빈 배열이면 숨김 */}
          {daily.candidates && daily.candidates.length > 0 && (
            <>
              <h3 className="t-h3" style={{ margin: "30px 0 12px", textAlign: "center" }}>
                이 무드의 후보 상품{daily.concept ? ` · ${daily.concept}` : ""}
              </h3>
              <div className={t.gridCols === "3열" ? "grid-3" : "grid-catalog"}>
                {daily.candidates.map((p) => (
                  <ProductCard key={p.id} p={p} onOpen={openProduct} onTry={tryProduct} showInfo={t.cardInfo} />
                ))}
              </div>
            </>
          )}
        </section>
      )}

      {/* 카테고리 — 가로 스크롤 (세분화) + 성별 탭 */}
      <CategoryRail go={go} gender={gender} setGender={setGender} />

      {/* 지금 인기 */}
      <section id="home-popular" className="wrap section" style={{ paddingTop: 24 }}>
        <div className="section-head">
          <div>
            <h2 className="t-h1">지금 인기</h2>
          </div>
          <Btn variant="ghost" iconR="arrowR" onClick={() => go("catalog")}>전체 보기</Btn>
        </div>
        {/* 검색/카탈로그와 동일하게 항상 5열(창 크기 반응). '3열' 설정 시에만 3열 */}
        <div className={t.gridCols === "3열" ? "grid-3" : "grid-catalog"}>
          {popular.slice(0, 32).map((p) => (
            <ProductCard key={p.id} p={p} onOpen={openProduct} onTry={tryProduct} showInfo={t.cardInfo} />
          ))}
        </div>
      </section>

      {/* 가상피팅 이렇게 써요 */}
      <section style={{ background: "var(--surface)" }}>
        <div className="wrap section">
          <div className="section-head" style={{ marginBottom: 40 }}>
            <div>
              <p className="eyebrow">HOW IT WORKS</p>
              <h2 className="t-h1">가상피팅, 이렇게 써요</h2>
            </div>
          </div>
          <div className="grid-3">
            {[
              { n: "01", ic: "camera", t: "앞모습 사진 등록", d: "정면을 보고 찍은 사진 한 장이면 충분해요. 마이페이지에서 언제든 바꿀 수 있어요." },
              { n: "02", ic: "sparkle", t: "옷을 골라 합성", d: "마음에 드는 옷의 '입어보기'를 누르면 내 사진 위에 자연스럽게 합성돼요." },
              { n: "03", ic: "layers", t: "겹쳐 입고 비교", d: "위에 다른 옷을 더 입혀 코디를 완성하고, 마음에 들면 바로 구매로 이어가요." },
            ].map((s) => (
              <div key={s.n} className="card" style={{ padding: 28 }}>
                <div className="row" style={{ justifyContent: "space-between", marginBottom: 18 }}>
                  <span className="t-mono" style={{ color: "var(--primary)", fontWeight: 700, fontSize: 14 }}>{s.n}</span>
                  <span style={{ width: 44, height: 44, borderRadius: 12, background: "var(--accent-soft)", color: "var(--primary)", display: "grid", placeItems: "center" }}>
                    <Icon name={s.ic} size={22} />
                  </span>
                </div>
                <h3 className="t-h3" style={{ margin: "0 0 8px" }}>{s.t}</h3>
                <p className="t-small t-sub" style={{ margin: 0 }}>{s.d}</p>
              </div>
            ))}
          </div>
        </div>
      </section>
    </div>
  );
}

/* =========================================================
   카탈로그 / 검색
   ========================================================= */
function FilterGroups({ selected, toggle, layout }) {
  const groups = DENKY.labelFilters; // 백엔드 labels 와 1:1 매칭되는 택소노미
  const [open, setOpen] = useState(() => Object.keys(groups).reduce((a, k) => (a[k] = true, a), {}));
  return (
    <>
      {Object.entries(groups).map(([group, items]) => (
        <div key={group} style={{ borderBottom: "1px solid var(--border)", padding: "4px 0", breakInside: "avoid", WebkitColumnBreakInside: "avoid" }}>
          <button onClick={() => setOpen((o) => ({ ...o, [group]: !o[group] }))}
            style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, background: "none", border: 0, padding: "14px 2px", fontWeight: 700, fontSize: 14, color: "var(--ink)" }}>
            {/* 그룹 이름 옆에 현재 고른 값 표기 — 예: 성별 (남성) */}
            <span style={{ display: "flex", alignItems: "baseline", gap: 6, minWidth: 0 }}>
              {group}
              {selected[group]?.length > 0 && (
                <span style={{ fontWeight: 600, fontSize: 12.5, color: "var(--primary-dark)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  ({selected[group].join(", ")})
                </span>
              )}
            </span>
            <Icon name="chevD" size={16} style={{ flex: "none", color: "var(--sub)", transform: open[group] ? "none" : "rotate(-90deg)", transition: "transform .18s" }} />
          </button>
          {open[group] && (
            group === "색상" ? (
              <div style={{ display: "grid", gridTemplateColumns: "repeat(5,1fr)", gap: 12, padding: "4px 2px 16px" }}>
                {items.map((c) => {
                  const on = selected[group]?.includes(c);
                  return (
                    <label key={c} className="swatch-check">
                      <input type="checkbox" checked={!!on} onChange={() => toggle(group, c)} />
                      <span className="sw" style={{ background: DENKY.labelColors[c] }}></span>
                      <span className="nm">{c}</span>
                    </label>
                  );
                })}
              </div>
            ) : (
              <div style={{ paddingBottom: 10 }}>
                {items.map((it) => (
                  <Check key={it} label={it} checked={selected[group]?.includes(it)} onChange={() => toggle(group, it)} />
                ))}
              </div>
            )
          )}
        </div>
      ))}
    </>
  );
}

// 세부칩 — 예전 큐레이션 칩(반팔/긴팔/민소매·나시/크롭/프린팅 …). on=선택됨.
function SubChip({ label, on, onClick }) {
  return (
    <button onClick={onClick} style={{
      height: 34, padding: "0 14px", borderRadius: 999, cursor: "pointer",
      fontSize: 13, fontWeight: 600, whiteSpace: "nowrap",
      border: on ? "none" : "1px solid var(--border)",
      background: on ? "var(--primary)" : "var(--surface)",
      color: on ? "#fff" : "var(--sub)",
    }}>{label}</button>
  );
}

function CatalogScreen({ go, openProduct, tryProduct, t, initial, navId, cacheRef, scrollRef, query, setQuery, onSearch }) {
  const PAGE = 24; // 한 번에 불러올 상품 수 ('더 보기'로 이어서 로드)
  // 뒤로가기 복원용 캐시 — 이 navId 로 전에 저장해둔 스냅샷이 있으면(=뒤로가기로 돌아옴) 재검색을 건너뛴다.
  // (앞으로 새로 이동한 경우엔 navId 가 매번 새로 발급되니 캐시가 없어 정상적으로 새로 검색한다)
  const cached = cacheRef && navId != null ? cacheRef.current[navId] : null;
  const [selected, setSelected] = useState(() => (cached ? cached.selected : {})); // 라벨 체크박스 선택 {항목:[값]}
  const [filterOpen, setFilterOpen] = useState(false); // 필터 팝업 열림 여부
  // 정렬 — 스마트앱과 동일한 서버 정렬 키('추천'=개인화 재랭킹). 칩 UI 로 고른다.
  const SORT_OPTIONS = [["추천", "추천"], ["인기", "popular"], ["최신", "newest"], ["낮은가격", "price_low"], ["높은가격", "price_high"], ["평점", "rating"]];
  const [sort, setSort] = useState(() => (cached ? cached.sort : "추천"));
  const sortRef = useRef(cached ? cached.sort : "추천");   // fetchPage 클로저에서 최신 정렬을 읽기 위한 ref
  const [railLabel, setRailLabel] = useState(() => (cached ? cached.railLabel : null)); // 선택된 종류 — 스마트앱 레일과 동일
  const [sub, setSub] = useState(() => (cached ? cached.sub : null));  // 선택된 세부칩(null=전체). 서버 키워드로 필터.
  const subRef = useRef(cached ? cached.sub : null);                    // fetchPage 클로저에서 최신 세부칩을 읽기 위한 ref
  const [results, setResults] = useState(() => (cached ? cached.results : DENKY.products));
  const [total, setTotal] = useState(() => (cached ? cached.total : 0));         // 조건에 맞는 전체 개수
  const [loading, setLoading] = useState(false);
  const [loadingMore, setLoadingMore] = useState(false);
  const [noMore, setNoMore] = useState(false);  // 더보기로 새로 붙는 게 없으면 '더 보기' 감춤
  const [searchMore, setSearchMore] = useState(() => (cached ? !!cached.searchMore : false)); // 의미검색 모드에서 다음 페이지가 더 있는지
  const modeRef = useRef(cached ? cached.mode : { kind: "all" }); // 현재 목록 종류('더 보기'가 같은 종류를 이어 로드)
  // 서버가 지금까지 내려준 개수(= 다음 요청의 offset). 중복 제거로 목록이 줄어도 페이지가 밀리지 않게 따로 셉니다.
  const offsetRef = useRef(cached ? (cached.offset != null ? cached.offset : cached.results.length) : 0);

  // 현재 모드 + offset 으로 한 페이지를 불러옵니다. (의미검색도 offset 페이지네이션을 씁니다)
  async function fetchPage(mode, offset) {
    // 성별(홈 토글=denky-gender, 없으면 여성)을 검색/카테고리/전체 모든 목록에 동일 적용한다.
    // (예전엔 검색만 성별을 넘겨, 가디건 등 카테고리 클릭 시 반대 성별이 섞였음)
    const g = (typeof localStorage !== "undefined" && localStorage.getItem("denky-gender")) || "여성";
    if (mode.kind === "search") {
      // 의미검색도 offset 으로 이어받는다 — 같은 (검색어·회원·날짜)면 순서가 재현돼 안전하다.
      const r = await API.searchPaged(mode.q, { k: PAGE, offset, gender: g });   // 성별(회원 디폴트) 반영
      return { items: r.items, total: r.total, hasMore: r.hasMore };
    }
    // 정렬은 서버에 맡긴다(스마트앱과 동일) — '추천'이면 로그인 회원의 체형·취향으로 재랭킹.
    const so = sortRef.current;
    // 세부칩(subRef)은 라벨 매핑이 있으면 라벨 필터로, 없으면 서버 키워드로 전달 — 전량 대상
    // 페이지네이션(예전 '민소매 9개' 버그 해소, '체크 셔츠'는 키워드가 15/68만 잡아 라벨로 전환).
    if (mode.kind === "label") {
      const lm = subRef.current ? LABEL_SUBCHIPS[subRef.current] : null;
      const filters = lm ? { ...mode.filters, ...lm } : mode.filters;
      return API.filterPaged(filters, { limit: PAGE, offset, gender: g, sort: so, keyword: lm ? undefined : (subRef.current || undefined) });
    }
    if (mode.kind === "category") return API.productsPaged({ category: mode.type, limit: PAGE, offset, gender: g, sort: so });
    return API.productsPaged({ limit: PAGE, offset, gender: g, sort: so }); // all
  }

  // 첫 페이지(새 검색/필터) — 결과를 교체합니다.
  async function loadFirst(mode) {
    modeRef.current = mode;
    setLoading(true);
    setNoMore(false);   // 새 목록이니 '더 보기' 다시 활성화
    try {
      const r = await fetchPage(mode, 0);
      setResults(r.items);
      // 검색은 서버 total 이 '이번 페이지 개수'일 수 있어, 지금까지 받은 개수와 큰 쪽을 표시용으로 씁니다.
      setTotal(mode.kind === "search" ? Math.max(Number(r.total) || 0, r.items.length) : r.total);
      setSearchMore(r.hasMore === true);
      offsetRef.current = r.items.length;
    }
    catch (e) { setResults([]); setTotal(0); setSearchMore(false); offsetRef.current = 0; }
    setLoading(false);
  }

  // 더 보기 — 다음 페이지를 이어서 붙입니다.
  // ★중복 방어★ 이미 담긴 상품 id 는 제외하고 붙인다(혹시 서버가 겹치는 페이지를 줘도 '수만 증가' 안 되게).
  async function loadMore() {
    setLoadingMore(true);
    try {
      // ★간헐 서버 오류에 더보기가 멈추지 않게 1회 재시도★ — 추천(개인화 재랭킹) 응답이 무거워
      //   가끔 500(idle-in-tx 등)이 나는데, 예전엔 조용히 삼켜(catch{}) '더보기가 안 넘어가는' 것처럼 보였다.
      let r;
      try { r = await fetchPage(modeRef.current, offsetRef.current); }
      catch (e1) {
        await new Promise((res) => setTimeout(res, 600));
        r = await fetchPage(modeRef.current, offsetRef.current);   // 재시도(실패 시 아래 catch 로)
      }
      const page = r.items || [];
      const seen = new Set(results.map((p) => p.id));
      const fresh = page.filter((p) => !seen.has(p.id));
      if (!fresh.length) setNoMore(true);   // 새 상품이 하나도 없으면 끝으로 간주
      const merged = [...results, ...fresh];
      setResults(merged);
      offsetRef.current += page.length;     // 다음 offset — 중복으로 버린 것도 서버는 이미 준 것이라 함께 셈
      // 검색은 서버 total 이 '이번 페이지 개수'일 수 있어, 누적 개수와 큰 쪽을 표시용으로 씁니다.
      setTotal(modeRef.current.kind === "search" ? Math.max(Number(r.total) || 0, merged.length) : r.total);
      setSearchMore(r.hasMore === true);
    } catch (e) {
      // 재시도까지 실패: 이번 클릭만 무시하고 '더보기' 버튼은 유지(noMore 를 켜지 않음) → 다시 누르면 재시도.
    }
    setLoadingMore(false);
  }

  // 기존 호출부 호환용 얇은 래퍼
  const runServerSearch = (q) => loadFirst({ kind: "search", q });
  const loadCategory = (type) => loadFirst({ kind: "category", type });
  const runLabelSearch = (filters) => loadFirst({ kind: "label", filters });

  // 진입/검색어 변화에 따라 결과를 불러옵니다.
  // (뒤로가기로 돌아와 캐시가 있으면 — 위에서 이미 state 를 복원했으니 재검색하지 않는다)
  useEffect(() => {
    if (cached) return;
    // 홈 카테고리 카드('종류' 단일 라벨)로 들어오면 레일 선택 상태로 표시(스마트앱과 동일)
    const rl = initial?.labelFilter?.["종류"]?.length === 1 ? initial.labelFilter["종류"][0] : null;
    setRailLabel(rl);
    setSub(null); subRef.current = null;
    if (initial?.labelFilter) { setSelected(initial.labelFilter); runLabelSearch(initial.labelFilter); } // 카테고리 칩(라벨)으로 진입
    else if (initial?.q) runServerSearch(initial.q);
    else if (initial?.type) loadCategory(initial.type); // (구) DB category 로드
    else if (query && query.trim()) runServerSearch(query.trim()); // '다른 옷 고르기'로 돌아오면 이전 검색을 다시 보여줍니다.
    else loadFirst({ kind: "all" }); // 기본: 전체 상품을 페이지네이션으로
  }, [initial]);

  // 화면 상태가 바뀔 때마다 캐시에 계속 갱신 — 나중에 뒤로가기로 돌아오면 이 스냅샷을 그대로 복원.
  useEffect(() => {
    if (!cacheRef || navId == null) return;
    cacheRef.current[navId] = {
      ...(cacheRef.current[navId] || {}),
      results, total, selected, sort, railLabel, sub, mode: modeRef.current,
      offset: offsetRef.current, searchMore,   // 뒤로가기로 돌아와도 '더 보기'를 이어서 누를 수 있게
    };
    // ★캐시 상한★ — navId 마다 결과 배열 스냅샷을 통째로 들고 있어, 탐색을 많이 할수록
    // JS 힙에 상품 배열이 계속 쌓였다. 현재 화면을 뺀 오래된 스냅샷부터 밀어낸다(최근 8개 유지).
    const keys = Object.keys(cacheRef.current);
    for (let i = 0; keys.length - i > 8; i++) {
      if (String(keys[i]) !== String(navId)) delete cacheRef.current[keys[i]];
    }
  }, [results, total, selected, sort, railLabel, sub, searchMore]);

  // 스크롤 위치도 계속 캐시에 기록 — 뒤로가기로 돌아왔을 때 그 자리로 복원하기 위함(스마트앱과 동일 UX).
  useEffect(() => {
    const el = scrollRef && scrollRef.current;
    if (!el || !cacheRef || navId == null) return;
    let raf = null;
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        raf = null;
        cacheRef.current[navId] = { ...(cacheRef.current[navId] || {}), scrollY: el.scrollTop };
      });
    };
    el.addEventListener("scroll", onScroll, { passive: true });
    return () => { el.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); };
  }, []);

  // 캐시로 복원된 화면이면 마운트 직후(레이아웃 확정 후) 저장해둔 스크롤 위치로 되돌린다.
  React.useLayoutEffect(() => {
    if (cached && scrollRef && scrollRef.current) {
      scrollRef.current.scrollTo({ top: cached.scrollY || 0 });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // 카탈로그 안에서 카테고리 카드로 종류 전환 (화면 이동 없이 목록만 교체 — 스마트앱 _pickRail)
  function pickRail(c) {
    setRailLabel(c.label);
    setSub(null); subRef.current = null;
    const filters = { "종류": [c.label] };
    setSelected(filters);
    if (setQuery) setQuery("");  // 이전 검색어 잔상 제거
    runLabelSearch(filters);
  }

  // 세부칩 선택 → 같은 종류를 세부칩 키워드로 서버 재조회(페이지 0). 같은 칩 재클릭은 해제(전체).
  function pickSub(s) {
    const next = (sub === s) ? null : s;
    setSub(next); subRef.current = next;
    loadFirst(modeRef.current);   // 현재 종류(mode.filters) 유지, 키워드만 바꿔 다시 로드
  }

  // 정렬 칩 선택 — 서버 정렬로 다시 불러온다. (의미검색 결과는 클라이언트 정렬로만)
  function pickSort(key) {
    if (sort === key) return;
    setSort(key);
    sortRef.current = key;
    if (modeRef.current.kind !== "search") loadFirst(modeRef.current);
  }

  const toggle = (g, v) => setSelected((s) => {
    const cur = s[g] || [];
    const next = cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v];
    const out = { ...s, [g]: next };
    if (!next.length) delete out[g];
    return out;
  });
  const clearAll = () => setSelected({});
  const activeChips = Object.entries(selected).flatMap(([g, vs]) => vs.map((v) => ({ g, v })));

  // 체크한 항목들을 '라벨 필터'로 정확히 거릅니다. (성별/종류/색상/... = labels)
  function searchSelected() {
    const hasAny = Object.values(selected).some((v) => v && v.length);
    if (!hasAny) { onSearch(""); return; } // 아무것도 안 고르면 전체로
    runLabelSearch(selected);
  }

  // 홈에서 고른 성별(localStorage 유지)로 카탈로그 결과도 거릅니다. (남성 선택 시 여성 상품 숨김)
  // 홈 '지금 인기'와 동일 규칙: 해당 성별 + 공용 상품을 남깁니다. (성별 라벨 없으면 여성 취급)
  const gender = (typeof localStorage !== "undefined" && localStorage.getItem("denky-gender")) || "여성";
  let view = filterGenderList([...results], gender);
  // 의미검색 결과만 클라이언트 정렬(서버 정렬 미지원). 나머지 모드는 서버가 이미 정렬해서 내려줌.
  if (modeRef.current.kind === "search") {
    if (sort === "price_low") view.sort((a, b) => a.price - b.price);
    if (sort === "price_high") view.sort((a, b) => b.price - a.price);
    if (sort === "rating") view.sort((a, b) => Number(b.rating) - Number(a.rating));
  }
  // (세부필터는 이제 서버가 처리 — 클라이언트 정제 제거. facet 칩 선택이 modeRef.filters 에 반영됨)

  // 더 보기 가능 여부 + 표시용 전체 개수
  // total 은 서버가 (종류+facet+성별)로 거른 실제 전체 개수 — 세부칩을 눌러도 정확하다(예: 민소매 394).
  // 의미검색은 전체 개수를 모를 수 있어 '다음 페이지 있음(searchMore)' 신호로 판단한다.
  const hasMore = !noMore && (modeRef.current.kind === "search" ? searchMore : results.length < total);
  const totalCount = total || view.length;
  // 의미검색은 '전체 몇 개'를 서버가 알려주지 않을 수 있다 — 그럴 땐 '전체 N개' 대신 '지금 N개 표시 중'으로 적는다.
  const knownTotal = modeRef.current.kind !== "search" || total > view.length;

  // 좁은 화면(모바일)에서는 상품 그리드 열 수와 팝업 컬럼 수를 줄입니다.
  const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && window.innerWidth <= 720);
  useEffect(() => {
    const onR = () => setIsMobile(window.innerWidth <= 720);
    window.addEventListener("resize", onR);
    return () => window.removeEventListener("resize", onR);
  }, []);

  return (
    <div className="page wrap" style={{ paddingTop: 24, paddingBottom: 64 }}>
      <div>
        <div style={{ minWidth: 0 }}>
          {/* 카테고리 카드 레일 — 스마트앱과 동일하게 카탈로그 상단에서 종류 전환 (선택 = 테두리 강조) */}
          <CategoryRail compact active={railLabel} gender={gender} onPick={pickRail} />

          {/* 세부칩 — 맨 앞 '전체' + 예전 큐레이션 칩(반팔/긴팔/민소매·나시/크롭/프린팅 …). 누르면 서버 키워드 필터로 전량. */}
          {railLabel && SUBCATS[railLabel] && (
            <div className="row" style={{ gap: 8, flexWrap: "wrap", marginBottom: 14 }}>
              <SubChip label="전체" on={!sub} onClick={() => pickSub(null)} />
              {SUBCATS[railLabel].map((s) => (
                <SubChip key={s} label={s} on={sub === s} onClick={() => pickSub(s)} />
              ))}
            </div>
          )}

          {/* 결과 헤더 */}
          <div className="row" style={{ justifyContent: "space-between", marginBottom: 18, flexWrap: "wrap", gap: 12 }}>
            <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
              {/* 필터 버튼 — 누르면 팝업(모달)이 뜬다 */}
              <Btn variant="outline" size="sm" icon="filter" onClick={() => setFilterOpen(true)}>
                필터{activeChips.length > 0 && <span className="badge" style={{ marginLeft: 4 }}>{activeChips.length}</span>}
              </Btn>
              {activeChips.length === 0
                ? (knownTotal
                    ? <span className="t-small t-sub">전체 <strong style={{ color: "var(--ink)" }}>{totalCount}</strong>개 상품{hasMore ? ` (${view.length} 표시 중)` : ""}</span>
                    : <span className="t-small t-sub"><strong style={{ color: "var(--ink)" }}>{view.length}</strong>개 상품 표시 중</span>)
                : <>
                    {activeChips.map(({ g, v }) => (
                      <Chip key={g + v} soft onRemove={() => toggle(g, v)}>{v}</Chip>
                    ))}
                    <button onClick={clearAll} className="t-small" style={{ background: "none", border: 0, color: "var(--sub)", textDecoration: "underline" }}>전체 해제</button>
                  </>}
            </div>
            {/* 정렬 칩 — 스마트앱과 동일(추천=개인화 서버 재랭킹) */}
            <div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
              {SORT_OPTIONS.map(([label, key]) => {
                const on = sort === key;
                return (
                  <button key={key} onClick={() => pickSort(key)}
                    style={{
                      height: 34, padding: "0 13px", borderRadius: 999, cursor: "pointer",
                      fontSize: 13, fontWeight: 600,
                      border: on ? "none" : "1px solid var(--border)",
                      background: on ? "var(--ink)" : "var(--surface)",
                      color: on ? "#fff" : "var(--sub)",
                    }}>
                    {label}
                  </button>
                );
              })}
            </div>
          </div>

          {loading ? (
            <div style={{ display: "grid", placeItems: "center", padding: "80px 0" }}>
              <div className="spinner spinner-dark" style={{ width: 36, height: 36 }}></div>
            </div>
          ) : view.length === 0 ? (
            <div className="empty">
              <div className="ill"><Icon name="search" size={48} stroke={1.4} /></div>
              <h3 className="t-h2" style={{ margin: "0 0 8px" }}>맞는 상품이 없어요</h3>
              <p className="t-body t-sub" style={{ margin: "0 0 24px" }}>조건을 줄이거나 다른 키워드로 다시 검색해 보세요.</p>
              <Btn variant="outline" onClick={() => { clearAll(); onSearch(""); }}>필터 초기화</Btn>
            </div>
          ) : (
            <>
              {/* 카탈로그는 항상 5열(창 크기에 맞춰 카드 스케일). '3열' 설정 시에만 3열 유지 */}
              <div className={t.gridCols === "3열" ? "grid-3" : "grid-catalog"}>
                {view.map((p) => (
                  <ProductCard key={p.id} p={p} onOpen={openProduct} onTry={tryProduct} showInfo={t.cardInfo} />
                ))}
              </div>
              {hasMore && (
                <div style={{ display: "grid", placeItems: "center", marginTop: 36 }}>
                  <Btn variant="outline" size="lg" onClick={loadMore} disabled={loadingMore}>
                    {/* 전체 개수를 아는 목록만 (n / 전체)로 표시 — 의미검색은 전체를 모를 수 있어 '더 보기'만 */}
                    {loadingMore ? "불러오는 중…" : (total > view.length ? `더 보기 (${view.length} / ${total})` : "더 보기")}
                  </Btn>
                </div>
              )}
            </>
          )}
        </div>
      </div>

      {/* 필터 팝업(모달) — '필터' 버튼으로 열림. 배경 클릭/X로 닫힘. */}
      {filterOpen && (
        <div onClick={() => setFilterOpen(false)}
          style={{ position: "fixed", inset: 0, background: "rgba(15,15,25,.55)", zIndex: 1000, display: "grid", placeItems: "center", padding: 20 }}>
          <div onClick={(e) => e.stopPropagation()}
            style={{ background: "var(--bg)", borderRadius: 16, maxWidth: 720, width: "100%", maxHeight: "85vh", display: "flex", flexDirection: "column", overflow: "hidden", boxShadow: "var(--sh-2)" }}>
            {/* 헤더 */}
            <div className="row" style={{ justifyContent: "space-between", padding: "18px 22px", borderBottom: "1px solid var(--border)" }}>
              <strong style={{ fontSize: 16, display: "flex", alignItems: "center", gap: 8 }}>
                <Icon name="filter" size={18} />필터{activeChips.length > 0 && <span className="badge" style={{ marginLeft: 4 }}>{activeChips.length}</span>}
              </strong>
              <button onClick={() => setFilterOpen(false)} aria-label="닫기"
                style={{ background: "none", border: 0, cursor: "pointer", color: "var(--sub)", display: "flex", padding: 0 }}>
                <Icon name="close" size={20} />
              </button>
            </div>
            {/* 본문 — 그룹들 (좁은 화면 1열 · 넓은 화면 2열) */}
            <div style={{ padding: "4px 22px 8px", overflowY: "auto", columns: isMobile ? 1 : 2, columnGap: 28 }}>
              <FilterGroups selected={selected} toggle={toggle} layout={t.filterLayout} />
            </div>
            {/* 하단 버튼 — 초기화 / 검색 */}
            <div className="row" style={{ gap: 10, padding: "14px 22px", borderTop: "1px solid var(--border)" }}>
              <Btn variant="ghost" onClick={clearAll} disabled={activeChips.length === 0}>초기화</Btn>
              <Btn variant="primary" block icon="search" onClick={() => { searchSelected(); setFilterOpen(false); }}>선택한 조건으로 검색</Btn>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { HomeScreen, CatalogScreen });
