/* 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>
  );
}

/* ---------- 오늘의 무드 데이터 — 홈 배너와 '오늘의 무드' 화면이 같이 쓴다 ----------
   로그인=내 아바타 코디, 비로그인=견본. 오늘 것이 아직이면 서버가 어제 것을 먼저 주고
   30초 간격 재조회(최대 10회)로 준비되는 즉시 교체한다(그동안 '지난 무드'로 표시 — 2026-08-14 지시). */
const kstTodayStr = () => new Date(Date.now() + 9 * 3600 * 1000).toISOString().slice(0, 10);
function useDailyCoordi(auth) {
  const [daily, setDaily] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    let timer = null;
    let tries = 0;
    // ★비로그인 방문자에겐 견본을 보여준다★ — 가입해야만 볼 수 있으면 가입할 이유가 안 보인다.
    if (!(auth && auth.loggedIn)) {
      API.dailyCoordiSample().then((d) => { if (alive) setDaily(d); }).catch(() => {});
      return () => { alive = false; };
    }
    const load = () => {
      API.dailyCoordi().then((d) => {
        if (!alive) return;
        setDaily(d);
        if (tries < 10 && (!d || (d.lookDate && d.lookDate !== kstTodayStr()))) {
          tries += 1;
          timer = setTimeout(load, 30000);
        }
      }).catch(() => {});
    };
    load();
    return () => { alive = false; if (timer) clearTimeout(timer); };
  }, [auth && auth.loggedIn, auth && auth.name]);
  // 견본(비로그인)은 날짜가 오늘이 아니어도 '지난 무드'로 표기하지 않는다 — 예시일 뿐이다.
  const sample = !!(daily && daily.sample);
  const isToday = !!daily && (sample || !daily.lookDate || daily.lookDate === kstTodayStr());
  return { daily, sample, isToday };
}

/* ---------- 홈 배너 슬라이더 — 퀸잇 방식(2026-08-14 사장님 지시): 홈 최상단 + 우하단 '1/5' 숫자 칩 ----------
   장 구성: ①피팅룸 히어로(회전 링이 배너 안에서 산다) ②오늘의 무드(준비되면 끼어듦)
            ③20% 쿠폰 ④무료배송 ⑤정품/중복제거.
   ★자동 슬라이드 10초★ — 퀸잇 실물처럼 자동이되 간격은 넉넉히(2026-08-14 지시 "시간은 길게").
   우하단 '1 | 5 >' 숫자 칩 = 퀸잇 방식(클릭하면 다음 장). */
function BannerSlider({ go, auth, daily, dailyIsToday, dailySample }) {
  const [idx, setIdx] = React.useState(0);
  const [railW, setRailW] = React.useState(460);   // 히어로(920×540 무대) 축소 배율 계산용 실제 폭
  const ref = React.useRef(null);
  const hoverRef = React.useRef(false);            // 마우스 올린 동안은 자동 넘김 일시정지
  const countRef = React.useRef(1);                // 장수 — 무드 장이 늦게 끼어들어 늘 수 있다
  const scrollEndRef = React.useRef(null);         // 손 스크롤 멈춤 감지(숫자 칩 동기화)
  React.useEffect(() => {
    const t = setInterval(() => {
      if (!hoverRef.current) setIdx((i) => (i + 1) % countRef.current);
    }, 10000);
    return () => clearInterval(t);
  }, []);
  React.useEffect(() => {
    const el = ref.current;
    if (el) el.scrollTo({ left: idx * el.clientWidth, behavior: "smooth" });
  }, [idx]);
  React.useEffect(() => {
    const update = () => { if (ref.current) setRailW(ref.current.clientWidth || 460); };
    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);
  const k = railW / 920;                            // 히어로 축소 배율
  const pageStyle = { width: "100%", flex: "none", aspectRatio: "750 / 675", scrollSnapAlign: "start", position: "relative", overflow: "hidden" };

  // 히어로의 링을 카피·CTA 사이 남는 높이에 맞출 축소 배율 (배너 높이 ≈ 폭×0.9, 카피·CTA ≈ 130px)
  const heroK = Math.min(k, Math.max(railW * 0.9 - 130, 160) / 540);
  const pages = [];
  // ①피팅룸 히어로 — 퀸잇식 큰 카피. 배너 전체 클릭: 비로그인 → 체험 창 / 회원 → 피팅룸.
  pages.push(
    <div key="hero" onClick={() => { window.denkyGa && denkyGa("try_banner_tap", { app: "pc" }); (auth && auth.loggedIn) ? go("fitroom") : go("tryexp"); }}
      style={{ ...pageStyle, background: "var(--surface)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "space-between", padding: "16px 0 14px", cursor: "pointer" }}>
      {/* 카피 — 퀸잇 배너처럼 짧고 큰 글씨 */}
      <div style={{ textAlign: "center", lineHeight: 1.25 }}>
        <div style={{ fontSize: 15, fontWeight: 800, color: "var(--ink)" }}>회원가입 없이 바로 입어보는</div>
        <div style={{ fontSize: 26, fontWeight: 900, color: "var(--ink)" }}>AI 가상피팅 체험!</div>
      </div>
      {/* 회전 링 — 남는 높이에 맞춰 축소 */}
      <div style={{ width: 920 * heroK, height: 540 * heroK, overflow: "hidden" }}>
        <div style={{ width: 920, height: 540, transform: `scale(${heroK})`, transformOrigin: "top left" }}>
          <FittingCarousel go={go} interactive={false} hint={false} cardsTappable={false} cardW={78} />
        </div>
      </div>
      {/* CTA — 파란 알약 버튼(퀸잇 배너의 '바로가기' 버튼 위상) */}
      <span style={{ padding: "11px 28px", borderRadius: 999, background: "var(--primary)", color: "#fff", fontSize: 14.5, fontWeight: 800, boxShadow: "0 6px 18px rgba(124,58,237,.35)" }}>
        지금 체험하러 가기
      </span>
    </div>
  );
  // ②오늘의 무드 — 착장 렌더(좌) + 무드·이유(우). 클릭 → 전용 화면
  if (daily) pages.push(
    <div key="mood" onClick={() => go("dailymood")} style={{ ...pageStyle, background: "#F4F5F7", display: "flex", cursor: "pointer" }}>
      <img src={daily.imageUrl} alt="오늘의 무드" style={{ width: "55%", height: "100%", objectFit: "cover", display: "block" }} />
      <div style={{ flex: 1, padding: "16px 14px", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "flex-start", gap: 8, minWidth: 0 }}>
        <span style={{ padding: "3px 10px", borderRadius: 999, background: dailyIsToday ? "var(--primary)" : "rgba(0,0,0,.55)", color: "#fff", fontSize: 11.5, fontWeight: 800 }}>
          {dailySample ? "예시" : (dailyIsToday ? "오늘의 무드" : "지난 무드")}
        </span>
        {daily.concept && <strong style={{ fontSize: 17, lineHeight: 1.25 }}>{daily.concept}</strong>}
        {daily.reason && (
          <p style={{ margin: 0, fontSize: 12.5, color: "var(--sub)", lineHeight: 1.4, display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", overflow: "hidden" }}>
            {daily.reason}
          </p>
        )}
        <span style={{ fontSize: 12.5, fontWeight: 800, color: "var(--primary)" }}>자세히 보기 →</span>
      </div>
    </div>
  );
  // ③20% 쿠폰 — 비로그인은 가입 창으로, 회원은 무반응(2026-08-14 지시) / ④무료배송 ⑤정품 — 카탈로그로
  [
    { src: "/assets/banners/b_coupon.webp", act: auth && auth.loggedIn ? null : () => go("signup") },
    { src: "/assets/banners/b_shipping.webp", act: () => go("catalog") },
    { src: "/assets/banners/b_dedup.webp", act: () => go("catalog") },
  ].forEach((b, i) => pages.push(
    <img key={b.src} src={b.src} alt={`배너 ${i + 1}`} onClick={b.act || undefined} loading="lazy"
      style={{ ...pageStyle, objectFit: "cover", cursor: b.act ? "pointer" : "default", display: "block" }} />
  ));
  const count = pages.length;
  countRef.current = count;
  const cur = Math.min(idx, count - 1);
  // 좌우 화살표 공통 스타일 — PC에선 스와이프가 어려우니 마우스로 넘길 수단(자동 넘김 삭제의 대체)
  const arrowStyle = { position: "absolute", top: "50%", transform: "translateY(-50%)", width: 34, height: 34, borderRadius: "50%", border: "1px solid var(--border)", background: "rgba(255,255,255,.92)", boxShadow: "0 2px 10px rgba(0,0,0,.14)", display: "grid", placeItems: "center", cursor: "pointer", padding: 0, color: "var(--ink)" };

  return (
    <section className="wrap" style={{ paddingTop: 16 }}>
      <div style={{ maxWidth: 460, margin: "0 auto", position: "relative" }}
        onMouseEnter={() => { hoverRef.current = true; }}
        onMouseLeave={() => { hoverRef.current = false; }}>
        <div ref={ref} className="banner-rail"
          onScroll={(e) => {
            // 손으로 넘겼을 때 숫자 칩 동기화 — 스크롤이 멈춘 뒤의 위치로만 갱신
            const el = e.currentTarget;
            if (scrollEndRef.current) clearTimeout(scrollEndRef.current);
            scrollEndRef.current = setTimeout(() => {
              const i = Math.round(el.scrollLeft / el.clientWidth);
              if (i >= 0 && i < count) setIdx(i);
            }, 150);
          }}
          style={{ display: "flex", overflowX: "auto", scrollSnapType: "x mandatory",
                   borderRadius: 16, scrollbarWidth: "none" }}>
          {pages}
        </div>
        {/* 좌우 화살표 — 끝 장에서는 해당 방향 숨김 */}
        {cur > 0 && (
          <button aria-label="이전 배너" onClick={() => setIdx(Math.max(0, cur - 1))} style={{ ...arrowStyle, left: 8 }}>
            <Icon name="chevL" size={17} />
          </button>
        )}
        {cur < count - 1 && (
          <button aria-label="다음 배너" onClick={() => setIdx(Math.min(count - 1, cur + 1))} style={{ ...arrowStyle, right: 8 }}>
            <Icon name="chevR" size={17} />
          </button>
        )}
        {/* 퀸잇 방식(실물 조사) — '1 | 5 >' 숫자 칩, 클릭하면 다음 장 */}
        <button onClick={() => setIdx((cur + 1) % count)} aria-label="다음 배너"
          style={{ position: "absolute", right: 10, bottom: 10, padding: "3px 8px 3px 10px", borderRadius: 999,
                   background: "rgba(0,0,0,.45)", color: "#fff", fontSize: 12, fontWeight: 700,
                   border: 0, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 3 }}>
          {cur + 1} | {count} <Icon name="chevR" size={12} />
        </button>
      </div>
    </section>
  );
}

/* =========================================================
   회전 링 캐러셀 (히어로) — 모델 중심으로 17개 카테고리 카드가 도는 UI
   퀸잇 광고 스타일: 앞쪽 ~6장만 보이고, 뒤로 가면 사라졌다 '카테고리 순서대로' 재등장.
   드래그/휠로 직접 회전. 카드 클릭 → 그 종류 카탈로그.
   ⏸ Phase2(미구현): 카드가 정면(θ≈0) 통과 시 '그 옷을 입은 모델 이미지'로 교체
      — 지금은 front 플래그(정면 감지)만 만들어 두고 강조만 함. 착용 이미지 준비되면 여기 끼우면 됨.
   ========================================================= */
// interactive=false 면 드래그/휠 회전을 끈다(자동회전만) — 배너 안에서 가로 스크롤과 겹치지 않게.
// hint=false 면 하단 '드래그·휠' 안내 배지를 숨긴다(배너 쪽에서 따로 안내).
// cardsTappable=false 면 카드 클릭(→카테고리)을 끈다 — 배너 전체 클릭(→피팅룸)에 양보(2026-08-14 지시).
// cardW — 카드 폭(px). 배너처럼 작은 무대에서는 줄여서 카드가 모델을 압도하지 않게.
function FittingCarousel({ go, interactive = true, hint = true, cardsTappable = true, cardW = 108 }) {
  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: interactive ? (draggingRef.current ? "grabbing" : "grab") : "default" }}
      onMouseDown={interactive ? onDown : undefined} onMouseMove={interactive ? onMove : undefined} onMouseUp={interactive ? onUp : undefined}
      onMouseEnter={interactive ? () => { pausedRef.current = true; setHover(true); } : undefined}
      onMouseLeave={interactive ? () => { pausedRef.current = false; draggingRef.current = false; setHover(false); } : undefined}
      onTouchStart={interactive ? onDown : undefined} onTouchMove={interactive ? onMove : undefined} onTouchEnd={interactive ? onUp : undefined}
      onWheel={interactive ? onWheel : undefined}
    >
      {/* 착용샷 미리 로드(스왑 즉시 반영) — 화면엔 안 보임 */}
      {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={cardsTappable ? c.label : undefined}
          onClick={cardsTappable ? () => { if (!movedRef.current) go("catalog", { labelFilter: { "종류": [c.label] } }); } : undefined}
          style={{
            position: "absolute", left: x, top: y, transform: `translate(-50%,-50%) scale(${scale})`,
            zIndex: z, opacity, transition: draggingRef.current ? "none" : "opacity .18s linear",
            width: cardW, borderRadius: 14, overflow: "hidden", padding: 0, cursor: cardsTappable ? "pointer" : "inherit",
            // ★카드 반투명(글래스) — 모델을 덜 가림
            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: cardsTappable && opacity >= 0.55 ? "auto" : "none",
          }}>
          {/* 카드는 '옷 이미지만'(실제 판매 옷 사진). 실패 시 아이콘 폴백. 중앙 모델만 정면 카드 옷으로 갈아입음. */}
          <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) 배지로 두어 겹침이 원천적으로 없고,
        사용자가 한 번 돌려보면(드래그/휠) 자연스럽게 사라져 시선을 정리한다. */}
    {hint && (
    <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) {} };

  // 오늘의 풀코디 — 배너 무드 장에 쓴다(섹션은 전용 화면으로 이사 — 2026-08-14).
  const { daily, sample: dailySample, isToday: dailyIsToday } = useDailyCoordi(auth);

  // 지금 인기 — 서버가 도매매 순위순(카탈로그 인기 화면과 같은 기준)으로 준다.
  // 성별 필터만 하고 순서는 그대로 둔다(재정렬하면 순위가 뒤섞인다). 32개까지.
  const genderFiltered = filterGenderList(DENKY.products, gender);
  const popular = genderFiltered.slice(0, 32);

  return (
    <div className="page">
      {/* 배너 슬라이더 — 홈 최상단(2026-08-14 사장님 지시: 퀸잇 방식으로 개편).
          첫 장 = 피팅룸 히어로(회전 링), 오늘의 무드가 오면 무드 장이 끼어든다 */}
      <BannerSlider go={go} auth={auth} daily={daily} dailyIsToday={dailyIsToday} dailySample={dailySample} />

      {/* 카테고리 — 배너 바로 아래로 이동(2026-08-14 사장님 지시) */}
      <CategoryRail go={go} gender={gender} setGender={setGender} />

      {/* (히어로 회전 링 캐러셀은 배너 첫 장으로, 오늘의 풀코디 섹션은 배너 무드 장 + 전용 화면으로 이사 — 2026-08-14) */}

      {/* 지금 인기 */}
      <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>
  );
}

/* =========================================================
   오늘의 무드 전용 화면 — 홈 배너의 무드 장을 클릭하면 진입 (2026-08-14)
   홈에 있던 섹션(무드 카드 + 착장 아이템 + 이 무드의 후보 상품)을 통째로 이사해 왔다.
   ========================================================= */
function DailyMoodScreen({ go, openProduct, tryProduct, t, auth }) {
  const { daily, sample: dailySample, isToday: dailyIsToday } = useDailyCoordi(auth);
  return (
    <div className="page">
      <section className="wrap section" style={{ paddingTop: 18 }}>
        {/* 상단 — 뒤로 + 제목 */}
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
          <Btn variant="ghost" icon="chevL" onClick={() => go("home")}>홈으로</Btn>
          <h2 className="t-h1" style={{ margin: 0 }}>오늘의 무드</h2>
        </div>
        {!daily ? (
          <p className="t-small t-sub" style={{ textAlign: "center", padding: "60px 0" }}>오늘의 무드를 불러오는 중…</p>
        ) : (
          <>
            {/* 상단 — 아바타 풀코디 이미지(무드 뱃지 + 이유 오버레이) + 그 아래 코디 팁 (창 중앙 정렬) */}
            <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)" }}>
                    {dailySample ? "예시" : (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>
              )}
              {/* ★비로그인 견본★ — 이미지 밖(카드 아래). 이미지 위에 얹으면 이유 캡션과 겹치고 밑단·신발을 가린다 */}
              {dailySample && (
                <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 14 }}>
                  <span style={{ flex: 1, fontSize: 13.5, fontWeight: 700, color: "var(--ink)" }}>
                    이건 예시예요 — 내 아바타로 매일 새 코디를 받아보세요
                  </span>
                  <Btn variant="primary" onClick={() => go("signup")}>시작하기</Btn>
                </div>
              )}
            </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>
    </div>
  );
}

/* =========================================================
   비회원 가상피팅 체험(TryScreen) — 홈 배너 분기 창 (스마트앱과 동일 흐름, 2026-08-14)
   성별 → 실루엣 체형 → 사진(선택) → 스타일 → [빈 상의·하의 카드 + 상품 리스트] → 피팅 → 결과
   ★사진 무저장★ 셀카는 서버가 저장하지 않는다(결과도 base64 왕복). '모델로 보기'는 캐시 재사용.
   ========================================================= */
const TRY_SHAPES = { // (어깨, 허리, 엉덩이) 반폭 — 실루엣 SVG용, 앱 CustomPaint 와 동일 수치
  X: [0.36, 0.16, 0.36], A: [0.26, 0.20, 0.42], V: [0.43, 0.22, 0.26], H: [0.31, 0.27, 0.31], O: [0.33, 0.40, 0.34],
  TR: [0.40, 0.30, 0.33], IV: [0.46, 0.26, 0.28], RC: [0.36, 0.32, 0.32], RO: [0.37, 0.44, 0.37],
};

function TrySilhouette({ code, color }) {
  const [sh, wa, hi] = TRY_SHAPES[code] || TRY_SHAPES.H;
  const W = 100, H = 150, cx = W / 2;
  const x = (half) => half * W;
  const d = [
    `M ${cx - W * 0.07} ${H * 0.19}`,
    `Q ${cx - x(sh)} ${H * 0.22} ${cx - x(sh)} ${H * 0.30}`,
    `Q ${cx - x((sh + wa) / 2)} ${H * 0.40} ${cx - x(wa)} ${H * 0.50}`,
    `Q ${cx - x(hi + 0.02)} ${H * 0.60} ${cx - x(hi)} ${H * 0.68}`,
    `Q ${cx - x(hi * 0.62)} ${H * 0.80} ${cx - W * 0.10} ${H * 0.97}`,
    `L ${cx + W * 0.10} ${H * 0.97}`,
    `Q ${cx + x(hi * 0.62)} ${H * 0.80} ${cx + x(hi)} ${H * 0.68}`,
    `Q ${cx + x(hi + 0.02)} ${H * 0.60} ${cx + x(wa)} ${H * 0.50}`,
    `Q ${cx + x((sh + wa) / 2)} ${H * 0.40} ${cx + x(sh)} ${H * 0.30}`,
    `Q ${cx + x(sh)} ${H * 0.22} ${cx + W * 0.07} ${H * 0.19}`, "Z",
  ].join(" ");
  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: "100%" }} aria-hidden="true">
      <circle cx={cx} cy={H * 0.09} r={H * 0.08} fill={color} />
      <path d={d} fill={color} />
    </svg>
  );
}

const TRY_NOTICE = "· 사진은 피팅 이미지 생성에만 쓰고, 서버에 저장하지 않아요.\n· 생성 과정에서 사진이 해외 서버로 전송·처리돼요.\n· 결과는 AI 생성 이미지라 실제 착용 모습과 다를 수 있어요.\n· 만 14세 이상, 본인 사진만 사용해 주세요.";
const TRY_STYLE_EMOJI = { casual: "🧢", office: "💼", guest: "💐", daily: "☀️" };

function TryScreen({ go, toast }) {
  const [step, setStep] = React.useState(0);        // 0 성별·체형·사진 → 1 스타일 → 2 조합 → 3 결과
  const [config, setConfig] = React.useState(null);
  const [gender, setGender] = React.useState("female");
  const [body, setBody] = React.useState(null);
  const [heightCm, setHeightCm] = React.useState("");
  const [weightKg, setWeightKg] = React.useState("");
  const [selfie, setSelfie] = React.useState(null); // dataURL — 메모리에만(무저장)
  const [style, setStyle] = React.useState(null);
  const [styleName, setStyleName] = React.useState("");
  const [slotTab, setSlotTab] = React.useState("상의");
  const [picked, setPicked] = React.useState({ "상의": null, "하의": null });
  const [items, setItems] = React.useState({ "상의": [], "하의": [] });
  const [totals, setTotals] = React.useState({ "상의": 0, "하의": 0 });
  const [loadingSlot, setLoadingSlot] = React.useState({});
  const [remaining, setRemaining] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [sharedOnce, setSharedOnce] = React.useState(false);
  const [result, setResult] = React.useState(null); // {image, imageUrl, products}
  const fileRef = React.useRef(null);
  const tokenRef = React.useRef(localStorage.getItem("denky_try_token") || null);

  React.useEffect(() => {
    window.denkyGa && denkyGa("try_open", { app: "pc" });
    API.tryConfig().then(setConfig).catch(() => {});
    const t = tokenRef.current;
    if (t) API.trySessionInfo(t).then((s) => setRemaining(s.remaining)).catch(() => { tokenRef.current = null; });
  }, []);

  async function ensureToken() {
    if (tokenRef.current) return tokenRef.current;
    const s = await API.trySession();
    tokenRef.current = s.token;
    setRemaining(s.remaining);
    try { localStorage.setItem("denky_try_token", s.token); } catch (e) {}
    return s.token;
  }

  function pickStyle(key, name) {
    window.denkyGa && denkyGa("try_style", { style: key, gender });
    setStyle(key); setStyleName(name); setStep(2); setSlotTab("상의");
    setPicked({ "상의": null, "하의": null });
    setItems({ "상의": [], "하의": [] }); setTotals({ "상의": 0, "하의": 0 });
    loadMore(key, "상의", 0); loadMore(key, "하의", 0);
  }

  async function loadMore(styleKey, slot, offset) {
    setLoadingSlot((s) => ({ ...s, [slot]: true }));
    try {
      const r = await API.tryProducts(styleKey, slot, gender, offset, 24);
      setItems((prev) => ({ ...prev, [slot]: offset === 0 ? r.items : [...prev[slot], ...r.items] }));
      setTotals((prev) => ({ ...prev, [slot]: r.total }));
    } catch (e) { /* 목록 실패는 조용히 — 더 보기 재시도 가능 */ }
    setLoadingSlot((s) => ({ ...s, [slot]: false }));
  }

  function pickProduct(p) {
    setPicked((prev) => ({ ...prev, [slotTab]: p }));
    if (slotTab === "상의" && !picked["하의"]) setSlotTab("하의");
    else if (slotTab === "하의" && !picked["상의"]) setSlotTab("상의");
  }

  async function onPickPhoto(e) {
    const f = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!f) return;
    try {
      const dataUrl = await window.readFileAsDataUrl(f);
      setSelfie(dataUrl);
      window.denkyGa && denkyGa("try_photo", { source: "file", app: "pc" });
    } catch (err) { toast(err.message || "사진을 읽지 못했어요"); }
  }

  async function runFit(mode) {
    const top = picked["상의"], bottom = picked["하의"];
    if (!top || !bottom) { toast("상의와 하의를 하나씩 골라 주세요"); return; }
    let s = selfie;
    if (mode === "selfie" && !s) { fileRef.current && fileRef.current.click(); return; }
    setBusy(true);
    try {
      const token = await ensureToken();
      const r = await API.tryFit({
        token, body, product_ids: [top.id, bottom.id], mode,
        selfie: mode === "selfie" ? s : undefined,
        height: parseInt(heightCm, 10) || undefined,
        weight: parseInt(weightKg, 10) || undefined,
      });
      window.denkyGa && denkyGa("try_fit_done", { mode, style, body, app: "pc" });
      setRemaining(r.remaining);
      setResult({ image: r.image, imageUrl: r.image_url, products: r.products });
      setStep(3);
    } catch (e) {
      if (e && e.status === 429) {
        toast(e.message || "무료 체험 횟수를 모두 썼어요");
        window.denkyGa && denkyGa("try_signup_cta", { from: "limit", app: "pc" });
        go("signup");
      } else {
        toast((e && e.message) || "피팅 생성에 실패했어요. 잠시 후 다시 시도해 주세요.");
      }
    }
    setBusy(false);
  }

  async function shareForBonus() {
    const token = tokenRef.current;
    if (!token) return;
    const img = (result && result.imageUrl) || (picked["상의"] && picked["상의"].image_url) || "";
    let sent = false;
    try {
      // 초대 카드(간단 링크 카드) 재사용 — 체험 딥링크를 보낸다. img 는 카드에 안 실리지만 무해.
      sent = window.KakaoShare ? KakaoShare.invite("https://m.ibeobwa.com/?try=1") === "kakao" : false;
      void img;
    } catch (e) { sent = false; }
    if (!sent) {
      // 카톡을 못 열면 링크 복사로 폴백 — 보너스는 공유 동작이 일어났을 때만
      try { await navigator.clipboard.writeText("https://m.ibeobwa.com/?try=1"); toast("체험 링크를 복사했어요 — 친구에게 붙여넣기!"); sent = true; }
      catch (e) { toast("공유를 열지 못했어요"); return; }
    }
    try {
      const s = await API.tryShare(token);
      setSharedOnce(true);
      setRemaining(s.remaining);
      window.denkyGa && denkyGa("try_share", { app: "pc" });
      toast("공유 보너스 +1회!");
    } catch (e) {/* 보너스 실패는 조용히 */}
  }

  const bodies = config ? config.bodies.filter((b) => b.gender === gender) : [];
  const styles = config ? config.styles.filter((st) => (st.genders || ["female"]).includes(gender)) : [];

  return (
    <div className="page">
      <section className="wrap section" style={{ paddingTop: 18, maxWidth: 560 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
          <Btn variant="ghost" icon="chevL" onClick={() => (step === 0 ? go("home") : setStep(step === 3 ? 2 : step - 1))}>뒤로</Btn>
          <h2 className="t-h1" style={{ margin: 0, flex: 1 }}>AI 가상피팅 체험</h2>
          {remaining != null && (
            <span style={{ padding: "5px 10px", borderRadius: 999, background: "var(--accent-soft)", color: "var(--primary)", fontSize: 12, fontWeight: 800 }}>피팅 {remaining}회</span>
          )}
        </div>

        {step === 0 && (
          <>
            <div style={{ display: "flex", gap: 8, marginBottom: 14 }}>
              {[["female", "여성"], ["male", "남성"]].map(([g, label]) => (
                <button key={g} onClick={() => { if (gender !== g) { setGender(g); setBody(null); } }}
                  style={{ padding: "8px 18px", borderRadius: 999, fontSize: 13.5, fontWeight: 800, cursor: "pointer",
                           border: `1px solid ${gender === g ? "var(--primary)" : "var(--border)"}`,
                           background: gender === g ? "var(--primary)" : "#fff", color: gender === g ? "#fff" : "var(--ink)" }}>
                  {label}
                </button>
              ))}
            </div>
            <h3 className="t-h3" style={{ margin: "0 0 4px" }}>나와 비슷한 실루엣을 골라 주세요</h3>
            <p className="t-small t-sub" style={{ margin: "0 0 12px" }}>체형에 맞춰 옷맵시를 미리 보여드려요</p>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
              {bodies.map((b) => {
                const on = body === b.code;
                return (
                  <button key={b.code} onClick={() => { setBody(b.code); window.denkyGa && denkyGa("try_body", { code: b.code }); }}
                    style={{ padding: "10px 6px 8px", borderRadius: 14, cursor: "pointer", textAlign: "center",
                             border: `${on ? 2 : 1}px solid ${on ? "var(--primary)" : "var(--border)"}`,
                             background: on ? "var(--accent-soft)" : "#fff" }}>
                    <div style={{ height: 110, padding: "0 14px" }}>
                      <TrySilhouette code={b.code} color={on ? "#7C3AED" : "#B9C0CC"} />
                    </div>
                    <div style={{ fontSize: 12.5, fontWeight: 800, marginTop: 4, color: on ? "var(--primary)" : "var(--ink)" }}>
                      {b.code.length === 1 ? `${b.code}자 · ${b.name}` : b.name}
                    </div>
                    <div className="t-sub" style={{ fontSize: 10.5, lineHeight: 1.3, marginTop: 2 }}>{b.desc}</div>
                  </button>
                );
              })}
            </div>
            <h3 className="t-h3" style={{ margin: "18px 0 4px" }}>키·몸무게 (선택)</h3>
            <p className="t-small t-sub" style={{ margin: "0 0 8px" }}>넣으면 내 사진 피팅의 핏이 더 정확해져요 — 저장하지 않아요</p>
            <div style={{ display: "flex", gap: 10 }}>
              <input className="input" type="number" placeholder="키(cm) 예: 162" value={heightCm} onChange={(e) => setHeightCm(e.target.value)} style={{ flex: 1 }} />
              <input className="input" type="number" placeholder="몸무게(kg) 예: 55" value={weightKg} onChange={(e) => setWeightKg(e.target.value)} style={{ flex: 1 }} />
            </div>
            <h3 className="t-h3" style={{ margin: "18px 0 4px" }}>내 얼굴 사진 (선택)</h3>
            <p className="t-small t-sub" style={{ margin: "0 0 8px" }}>올려두면 고른 옷을 내 얼굴로 바로 입어볼 수 있어요. 정면·밝은 곳 사진 한 장이면 충분해요.</p>
            <div style={{ padding: 11, borderRadius: 12, background: "#F4F5F7", fontSize: 12, lineHeight: 1.55, color: "var(--sub)", whiteSpace: "pre-line" }}>{TRY_NOTICE}</div>
            <div style={{ marginTop: 10 }}>
              {!selfie ? (
                <Btn variant="outline" block onClick={() => fileRef.current && fileRef.current.click()}>동의하고 사진 올리기</Btn>
              ) : (
                <div style={{ display: "flex", alignItems: "center", gap: 10, padding: 10, borderRadius: 14, border: "1.5px solid var(--primary)", background: "#fff" }}>
                  <img src={selfie} alt="내 사진" style={{ width: 48, height: 60, objectFit: "cover", borderRadius: 10 }} />
                  <span style={{ flex: 1, fontSize: 12.5, fontWeight: 700 }}>사진 준비 완료 — 조합을 고르면 바로 입어봐요</span>
                  <button className="t-small" style={{ border: 0, background: "none", cursor: "pointer", color: "var(--primary)" }} onClick={() => fileRef.current && fileRef.current.click()}>변경</button>
                  <button className="t-small" style={{ border: 0, background: "none", cursor: "pointer", color: "var(--sub)" }} onClick={() => setSelfie(null)}>삭제</button>
                </div>
              )}
              <input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }} onChange={onPickPhoto} />
            </div>
            <div style={{ marginTop: 20 }}>
              <Btn variant="primary" block onClick={() => (body ? setStep(1) : toast("실루엣을 먼저 골라 주세요"))}>다음 — 스타일 고르기</Btn>
            </div>
          </>
        )}

        {step === 1 && (
          <>
            <h3 className="t-h3" style={{ margin: "0 0 12px" }}>어떤 스타일이 궁금하세요?</h3>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 12 }}>
              {styles.map((st) => (
                <button key={st.key} onClick={() => pickStyle(st.key, st.name)}
                  style={{ padding: "22px 10px", borderRadius: 14, border: "1px solid var(--border)", background: "#fff", cursor: "pointer" }}>
                  <div style={{ fontSize: 26 }}>{TRY_STYLE_EMOJI[st.key] || "👚"}</div>
                  <div style={{ fontSize: 15, fontWeight: 800, marginTop: 6 }}>{st.name}</div>
                </button>
              ))}
            </div>
          </>
        )}

        {step === 2 && (
          <>
            <div style={{ display: "flex", gap: 10 }}>
              {["상의", "하의"].map((slot) => {
                const p = picked[slot];
                const on = slotTab === slot;
                return (
                  <button key={slot} onClick={() => setSlotTab(slot)}
                    style={{ flex: 1, height: 118, borderRadius: 14, cursor: "pointer", overflow: "hidden", padding: 0,
                             border: `${on ? 2 : 1}px solid ${on ? "var(--primary)" : "var(--border)"}`,
                             background: p ? "#fff" : "#F4F5F7", display: "flex", alignItems: "center" }}>
                    {p ? (
                      <>
                        <img src={p.image_url} alt="" style={{ width: 80, height: "100%", objectFit: "cover" }} />
                        <span style={{ flex: 1, padding: "0 8px", textAlign: "left" }}>
                          <span style={{ display: "block", fontSize: 11, lineHeight: 1.3, maxHeight: 30, overflow: "hidden" }}>{p.name}</span>
                          <b style={{ fontSize: 12 }}>{Number(p.price).toLocaleString()}원</b>
                        </span>
                      </>
                    ) : (
                      <span style={{ flex: 1, textAlign: "center", color: "var(--sub)", fontSize: 12.5, fontWeight: 700 }}>+ {slot} 고르기</span>
                    )}
                  </button>
                );
              })}
            </div>
            <div style={{ margin: "10px 0" }}>
              <Btn variant="primary" block onClick={() => setBusyOrFitSheet()}>{picked["상의"] && picked["하의"] ? "이 조합으로 피팅하기" : "상의·하의를 골라 주세요"}</Btn>
            </div>
            <div style={{ display: "flex", borderBottom: "1px solid var(--border)", marginBottom: 12 }}>
              {["상의", "하의"].map((slot) => (
                <button key={slot} onClick={() => setSlotTab(slot)}
                  style={{ flex: 1, padding: "9px 0", background: "none", cursor: "pointer", fontWeight: 800, fontSize: 13.5,
                           border: 0, borderBottom: `${slotTab === slot ? 2.5 : 0}px solid var(--primary)`,
                           color: slotTab === slot ? "var(--primary)" : "var(--sub)" }}>
                  {styleName} {slot}
                </button>
              ))}
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
              {items[slotTab].map((p) => {
                const on = picked[slotTab] && picked[slotTab].id === p.id;
                return (
                  <button key={p.id} onClick={() => pickProduct(p)}
                    style={{ borderRadius: 12, overflow: "hidden", cursor: "pointer", padding: 0, background: "#fff", textAlign: "left",
                             border: `${on ? 2 : 1}px solid ${on ? "var(--primary)" : "var(--border)"}` }}>
                    <img src={p.image_url} alt={p.name} loading="lazy" style={{ width: "100%", aspectRatio: "3/4", objectFit: "cover", display: "block" }} />
                    <div style={{ padding: "5px 6px 7px" }}>
                      <div style={{ fontSize: 10.5, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.name}</div>
                      <b style={{ fontSize: 11.5 }}>{Number(p.price).toLocaleString()}원</b>
                    </div>
                  </button>
                );
              })}
              {items[slotTab].length < (totals[slotTab] || 0) && (
                <button onClick={() => loadMore(style, slotTab, items[slotTab].length)}
                  style={{ borderRadius: 12, border: "1px dashed var(--border)", background: "none", cursor: "pointer", minHeight: 120, color: "var(--primary)", fontWeight: 700, fontSize: 13 }}>
                  {loadingSlot[slotTab] ? "불러오는 중…" : "더 보기"}
                </button>
              )}
            </div>
          </>
        )}

        {step === 3 && result && (
          <>
            <div style={{ position: "relative", borderRadius: 18, overflow: "hidden", maxWidth: 420, margin: "0 auto" }}>
              <img src={result.image || result.imageUrl} alt="피팅 결과" style={{ display: "block", width: "100%", aspectRatio: "3/4", objectFit: "cover" }} />
              <span style={{ position: "absolute", top: 12, left: 12, padding: "4px 10px", borderRadius: 999, background: "rgba(0,0,0,.55)", color: "#fff", fontSize: 11, fontWeight: 800 }}>AI 생성</span>
            </div>
            <p className="t-small t-sub" style={{ margin: "8px 0 14px", textAlign: "center" }}>참고용 이미지예요 — 구매 전 사이즈표 확인! (우클릭으로 저장할 수 있어요)</p>
            <div style={{ display: "flex", gap: 10, justifyContent: "center" }}>
              <Btn variant="ghost" onClick={() => setStep(2)}>다른 조합 입어보기</Btn>
              {!sharedOnce && <Btn variant="primary" onClick={shareForBonus}>카톡 공유하고 +1회</Btn>}
            </div>
            <div style={{ marginTop: 20, padding: 16, borderRadius: 16, background: "linear-gradient(135deg, #7C3AED, #4F46E5)", color: "#fff" }}>
              <b style={{ fontSize: 14.5 }}>이 창을 닫으면 결과는 사라져요</b>
              <p style={{ margin: "4px 0 12px", fontSize: 12, opacity: .85, lineHeight: 1.4 }}>가입하면 무료 피팅 10회 + 매주 10회 충전, 모든 상품을 마음껏 입어볼 수 있어요.</p>
              <Btn variant="outline" block onClick={() => { window.denkyGa && denkyGa("try_signup_cta", { from: "result", app: "pc" }); go("signup"); }}>무료로 가입하고 계속하기</Btn>
            </div>
          </>
        )}

        {busy && (
          <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", display: "grid", placeItems: "center", zIndex: 90 }}>
            <div style={{ color: "#fff", fontWeight: 700, fontSize: 14, textAlign: "center" }}>
              <div style={{ fontSize: 30, marginBottom: 10 }}>⏳</div>
              입혀보는 중… (약 10초)
            </div>
          </div>
        )}
      </section>
    </div>
  );

  // 피팅 방식 선택 — 사진이 있으면 바로 셀카 피팅, 없으면 모델/사진 선택을 확인창으로
  function setBusyOrFitSheet() {
    if (!(picked["상의"] && picked["하의"])) { toast("상의와 하의를 하나씩 골라 주세요"); return; }
    if (selfie) { runFit("selfie"); return; }
    // 사진이 없으면: 모델로 볼지, 사진을 올릴지 고른다
    if (window.confirm("내 사진 없이 체형 모델로 입어볼까요?\n[확인] 모델로 보기 · [취소] 내 사진 올리기")) {
      runFit("model");
    } else {
      fileRef.current && fileRef.current.click();
    }
  }
}

/* =========================================================
   카탈로그 / 검색
   ========================================================= */
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 });
