/* Denky — 장바구니 / 결제 / 로그인·회원가입 / 마이페이지 */

/* ============ 사진 자동 측정 (MediaPipe Pose) ============
   정면 전신 사진 + 키(스케일) → 어깨너비·상체길이(어깨~허리)·다리길이(허리~발목)를 추정한다.
   - 브라우저에서만 동작(사진이 기기 밖으로 안 나감). 둘레(가슴/허리)는 정면 한 장으론 부정확해 제외.
   - 추정값일 뿐이라 사용자가 수정하게 한다. 실패하면 조용히 null. */
const MP_VER = "0.10.20";
let _mpLandmarker = null; // 한 번만 생성해 재사용
// Babel(in-browser)이 import() 를 변형하지 못하도록 Function 으로 감싼다.
const _dynImport = (u) => new Function("u", "return import(u)")(u);

async function _getPoseLandmarker() {
  if (_mpLandmarker) return _mpLandmarker;
  const vision = await _dynImport(`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MP_VER}/vision_bundle.mjs`);
  const fileset = await vision.FilesetResolver.forVisionTasks(`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MP_VER}/wasm`);
  _mpLandmarker = await vision.PoseLandmarker.createFromOptions(fileset, {
    baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task" },
    runningMode: "IMAGE",
    numPoses: 1,
  });
  return _mpLandmarker;
}

function _loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = "anonymous";  // Supabase 공개버킷 CORS 허용 가정
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = url;
  });
}

// 휴대폰 사진의 EXIF 회전(예: 세로로 찍었는데 가로 픽셀+"돌리라" 플래그)을 적용해 '똑바로 선' 캔버스를 만든다.
// 이렇게 정규화하지 않으면 MediaPipe 가 '누운 사람'을 보거나, 좌표 환산(naturalWidth)과 어긋나 치수가 전부 틀어진다.
// ★다운스케일★ maxDim 초과 사진은 비율 유지로 축소해 그린다 — 폰 원본(4000×3000)을 자연 해상도
// 캔버스(~48MB)로 만들던 메모리 피크 방지. MediaPipe 랜드마크는 0~1 정규화 좌표라 축소해도 판정 동일.
async function _loadUprightCanvas(url, maxDim = 1600) {
  let bmp = null;
  try {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), 6000);   // fetch 무한대기 방지(멈춤 방지)
    const resp = await fetch(url, { mode: "cors", signal: ctrl.signal });
    clearTimeout(t);
    const blob = await resp.blob();
    bmp = await createImageBitmap(blob, { imageOrientation: "from-image" });  // EXIF 방향 반영(똑바로)
  } catch (e) {
    bmp = await _loadImg(url);  // 폴백: 일반 이미지
  }
  const w0 = bmp.width || bmp.naturalWidth, h0 = bmp.height || bmp.naturalHeight;
  const scale = Math.min(1, maxDim / Math.max(w0, h0, 1));
  const w = Math.max(1, Math.round(w0 * scale)), h = Math.max(1, Math.round(h0 * scale));
  const canvas = document.createElement("canvas");
  canvas.width = w; canvas.height = h;
  canvas.getContext("2d").drawImage(bmp, 0, 0, w, h);
  if (bmp.close) bmp.close();   // ImageBitmap 메모리 해제
  return canvas;
}

// 정면 전신 사진의 '피팅 적합성'을 pose 로 검사한다. 반환 {ok, issues:[{level:'bad'|'warn', msg}]}
async function checkPhotoQuality(imgUrl) {
  try {
    const [landmarker, canvas] = await Promise.all([_getPoseLandmarker(), _loadUprightCanvas(imgUrl)]);
    const res = landmarker.detect(canvas);
    const lm = res && res.landmarks && res.landmarks[0];
    if (!lm || lm.length < 29) {
      return { ok: false, issues: [{ level: "bad", msg: "사람이 또렷이 안 보여요. 정면 전신 사진으로 다시 찍어 주세요." }] };
    }
    const v = (i) => (lm[i].visibility != null ? lm[i].visibility : 1);
    const y = (i) => lm[i].y;
    const issues = [];
    // 1) 전신 — 어깨~발목이 다 보여야 한다.
    if ([11, 12, 23, 24, 27, 28].some((i) => v(i) < 0.5)) {
      issues.push({ level: "bad", msg: "전신(어깨~발목)이 다 안 보여요. 발끝까지 나오게 한 걸음 물러나 주세요." });
    }
    // 2) 정면 — 돌아서 있으면 한쪽 어깨/엉덩이가 가려져 좌우 visibility 차이가 커진다.
    if (Math.abs(v(11) - v(12)) > 0.4 || Math.abs(v(23) - v(24)) > 0.4) {
      issues.push({ level: "warn", msg: "옆으로 돌아서 있어요. 카메라를 정면으로 보고 서 주세요." });
    }
    // 3) 좌우 기울기(roll) — 어깨선이 수평이어야 한다.
    const shoulderW = Math.abs(lm[11].x - lm[12].x) || 0.001;
    if (Math.abs(y(11) - y(12)) / shoulderW > 0.2) {
      issues.push({ level: "warn", msg: "몸이 기울었거나 카메라가 돌아갔어요. 폰을 수평으로 똑바로 세워 주세요." });
    }
    // 4) 카메라 높낮이(pitch) — 다리:상체 픽셀 비율(정상 ~1.7)로 위/아래 촬영을 가늠한다.
    const shY = (y(11) + y(12)) / 2, hipY = (y(23) + y(24)) / 2, ankY = (y(27) + y(28)) / 2;
    const torso = hipY - shY, leg = ankY - hipY;
    if (torso > 0.02 && leg > 0.02) {
      const ratio = leg / torso;
      if (ratio < 1.25) issues.push({ level: "warn", msg: "카메라가 너무 높아요(위에서 내려찍음). 허리 높이에서 수평으로 찍어야 다리가 안 눌려요." });
      else if (ratio > 2.5) issues.push({ level: "warn", msg: "카메라가 너무 낮아요(아래에서 올려찍음). 허리 높이에서 수평으로 찍어야 비율이 안 늘어나요." });
    }
    return { ok: issues.length === 0, issues };
  } catch (e) {
    return { ok: true, issues: [] };  // 검사 자체가 실패하면 막지 않는다(보조 기능)
  }
}

/* ============ 둘레 자동 추정(베타) — 정면 폭 + 측면 깊이 → 타원둘레 근사 ============
   정면 사진의 사람 실루엣 가로폭(width)과 측면 사진의 가로폭(=앞뒤 depth)을 가슴/허리/엉덩이 높이에서 재서
   타원 둘레로 가슴·허리·엉덩이 '둘레'를 추정한다. ⚠ 옷·자세·카메라거리 영향이 커 어디까지나 추정(베타) —
   반드시 사용자가 확인·수정하게 한다. 측정 실패/이상값은 조용히 null. 사진은 브라우저 안에서만 분석된다. */
let _mpSegLandmarker = null;
async function _getSegLandmarker() {
  if (_mpSegLandmarker) return _mpSegLandmarker;
  const vision = await _dynImport(`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MP_VER}/vision_bundle.mjs`);
  const fileset = await vision.FilesetResolver.forVisionTasks(`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MP_VER}/wasm`);
  _mpSegLandmarker = await vision.PoseLandmarker.createFromOptions(fileset, {
    baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/1/pose_landmarker_heavy.task" },
    runningMode: "IMAGE",
    numPoses: 1,
    outputSegmentationMasks: true,   // 사람 실루엣 마스크 → 가로폭 측정에 사용
  });
  return _mpSegLandmarker;
}

// 타원 둘레 근사(라마누잔): 반지름 a(폭/2)·b(깊이/2)
function _ellipseCirc(width, depth) {
  const a = width / 2, b = depth / 2;
  if (!(a > 0) || !(b > 0)) return null;
  return Math.PI * (3 * (a + b) - Math.sqrt((3 * a + b) * (a + 3 * b)));
}

/* ============ 다음(카카오) 우편번호 — 주소 검색 → 우편번호·도로명 자동완성 ============ */
function openPostcode(onComplete) {
  function run() {
    new window.daum.Postcode({
      oncomplete: (data) => {
        const road = data.roadAddress || data.address || "";
        onComplete({ zipcode: data.zonecode || "", address: road });
      },
    }).open();
  }
  if (window.daum && window.daum.Postcode) { run(); return; }
  // SDK 를 한 번만 동적 로드 (무빌드 페이지라 script 태그를 직접 붙임)
  const id = "daum-postcode-sdk";
  let s = document.getElementById(id);
  if (!s) {
    s = document.createElement("script");
    s.id = id;
    s.src = "https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js";
    document.head.appendChild(s);
  }
  s.addEventListener("load", run, { once: true });
}

/* ============ 배송지 주소록 ============ */
function AddressForm({ initial, auth, busy, onSave, onCancel }) {
  // 새 배송지면 이름·전화를 회원정보로 자동 채움(기존 주소 수정 시엔 그 주소 값 유지).
  const [recipient, setRecipient] = useState(initial.recipient_name || (auth && auth.name) || "");
  const [phone, setPhone] = useState(initial.phone || (auth && auth.phone) || "");
  const [zip, setZip] = useState(initial.zipcode || "");
  const [addr, setAddr] = useState(initial.address || "");
  const [detail, setDetail] = useState(initial.address_detail || "");
  const [label, setLabel] = useState(initial.label || "");
  const [isDefault, setIsDefault] = useState(!!initial.is_default);
  const [cands, setCands] = useState([]);      // 주소 자동완성 후보
  const skipRef = useRef(false);               // 선택 직후 재검색 방지
  // 주소를 입력하면(2자↑) 디바운스 후 후보를 띄웁니다.
  useEffect(() => {
    if (skipRef.current) { skipRef.current = false; return; }
    const q = (addr || "").trim();
    if (q.length < 2) { setCands([]); return; }
    let alive = true;
    const t = setTimeout(async () => {
      try { const r = await API.searchAddressCandidates(q); if (alive) setCands(r.items || []); }
      catch (e) { if (alive) setCands([]); }
    }, 300);
    return () => { alive = false; clearTimeout(t); };
  }, [addr]);
  function pick(c) { skipRef.current = true; setZip(c.zipcode || ""); setAddr(c.road_address || c.jibun_address || ""); setCands([]); }
  return (
    <div className="card" style={{ padding: 14, marginTop: 10, background: "var(--surface)" }}>
      <div className="grid-2" style={{ gap: 12 }}>
        <div className="field"><label>받는 분</label><input className="input" value={recipient} onChange={(e) => setRecipient(e.target.value)} /></div>
        <div className="field"><label>연락처</label><input className="input" placeholder="010-0000-0000" value={phone} onChange={(e) => setPhone(e.target.value)} /></div>
      </div>
      <div className="field" style={{ marginTop: 12, position: "relative" }}><label>주소</label>
        <div className="row" style={{ gap: 8 }}>
          <input className="input" style={{ maxWidth: 130 }} placeholder="우편번호" value={zip} readOnly />
          <Btn variant="outline" size="sm" onClick={() => openPostcode(({ zipcode, address }) => { skipRef.current = true; setZip(zipcode); setAddr(address); setCands([]); })}>주소 찾기(팝업)</Btn>
        </div>
        <input className="input" style={{ marginTop: 8 }} placeholder="도로명·건물명 입력하면 후보가 떠요 (예: 진건오남로 178)" value={addr} onChange={(e) => setAddr(e.target.value)} />
        {cands.length > 0 && (
          <div style={{ position: "absolute", left: 0, right: 0, zIndex: 30, background: "#fff", border: "1px solid var(--border)", borderRadius: 8, marginTop: 2, maxHeight: 240, overflowY: "auto", boxShadow: "0 8px 24px rgba(0,0,0,.10)" }}>
            {cands.map((c, i) => (
              <div key={i} onClick={() => pick(c)} style={{ padding: "8px 10px", cursor: "pointer", borderBottom: "1px solid var(--surface-2)" }}>
                <div style={{ fontSize: 13 }}>{c.road_address || c.jibun_address}{c.building_name ? <span className="t-sub" style={{ fontSize: 11 }}> · {c.building_name}</span> : null}</div>
                <div className="t-caption t-sub">{c.zipcode ? `[${c.zipcode}] ` : ""}{c.jibun_address}</div>
              </div>
            ))}
          </div>
        )}
        <input className="input" style={{ marginTop: 8 }} placeholder="상세 주소 입력 (동/호수 등)" value={detail} onChange={(e) => setDetail(e.target.value)} />
      </div>
      <div className="grid-2" style={{ gap: 12, marginTop: 12 }}>
        <div className="field"><label>별칭 (선택)</label><input className="input" placeholder="집 / 회사" value={label} onChange={(e) => setLabel(e.target.value)} /></div>
        <label className="row" style={{ gap: 6, alignItems: "center", marginTop: 24, cursor: "pointer" }}>
          <input type="checkbox" checked={isDefault} onChange={(e) => setIsDefault(e.target.checked)} /> 기본 배송지로 설정
        </label>
      </div>
      <div className="row" style={{ gap: 8, marginTop: 12 }}>
        <Btn variant="primary" size="sm" disabled={busy || !recipient.trim() || !phone.trim() || !addr.trim()}
          onClick={() => onSave({ id: initial.id, recipient_name: recipient.trim(), phone: phone.trim(), zipcode: zip, address: addr, address_detail: detail.trim(), label: label.trim(), is_default: isDefault })}>
          {busy ? "저장 중…" : "저장"}</Btn>
        <Btn variant="ghost" size="sm" onClick={onCancel}>취소</Btn>
      </div>
    </div>
  );
}

function AddressBook({ auth, toast, onChange }) {
  const [items, setItems] = useState(null); // null=로딩
  const [editing, setEditing] = useState(null); // null=닫힘, {}=새로, {id,...}=수정
  const [busy, setBusy] = useState(false);
  function load() { API.addresses().then((r) => setItems(r.items || [])).catch(() => setItems([])); }
  useEffect(() => { load(); }, []);
  async function save(form) {
    setBusy(true);
    try {
      const payload = { label: form.label || null, recipient_name: form.recipient_name, phone: form.phone,
        zipcode: form.zipcode || null, address: form.address, address_detail: form.address_detail || null, is_default: !!form.is_default };
      if (form.id) await API.updateAddress(form.id, payload); else await API.addAddress(payload);
      setEditing(null); load(); if (onChange) onChange();
      if (toast) toast("배송지를 저장했어요");
    } catch (e) { if (toast) toast(e.message || "저장에 실패했어요"); }
    setBusy(false);
  }
  async function setDefault(id) { try { await API.setDefaultAddress(id); load(); if (onChange) onChange(); } catch (e) { if (toast) toast("실패했어요"); } }
  async function remove(id) { try { await API.deleteAddress(id); load(); if (onChange) onChange(); } catch (e) { if (toast) toast("삭제에 실패했어요"); } }
  return (
    <div>
      {items === null ? <p className="t-caption t-sub">불러오는 중…</p> : (
        <>
          {items.length === 0 && !editing && <p className="t-caption t-sub" style={{ margin: "0 0 10px" }}>저장된 배송지가 없어요. 추가하면 결제 시 자동으로 채워져요.</p>}
          <div className="stack" style={{ gap: 10 }}>
            {items.map((a) => (
              <div key={a.id} className="card" style={{ padding: 12, border: a.is_default ? "1.5px solid var(--primary)" : "1px solid var(--border)" }}>
                <div style={{ fontWeight: 600, fontSize: 13 }}>
                  {a.recipient_name}{a.label ? <span className="t-sub" style={{ fontWeight: 400 }}> · {a.label}</span> : null}
                  {a.is_default ? <span style={{ color: "var(--primary)", fontSize: 11, fontWeight: 700 }}> · 기본</span> : null}
                </div>
                <div className="t-caption t-sub" style={{ marginTop: 2 }}>{a.zipcode ? `[${a.zipcode}] ` : ""}{a.address} {a.address_detail || ""}</div>
                <div className="t-caption t-sub">{a.phone}</div>
                <div className="row" style={{ gap: 6, marginTop: 8 }}>
                  {!a.is_default && <Btn variant="ghost" size="sm" onClick={() => setDefault(a.id)}>기본으로</Btn>}
                  <Btn variant="ghost" size="sm" onClick={() => setEditing(a)}>수정</Btn>
                  <Btn variant="ghost" size="sm" onClick={() => remove(a.id)}>삭제</Btn>
                </div>
              </div>
            ))}
          </div>
          {editing ? <AddressForm initial={editing} auth={auth} busy={busy} onSave={save} onCancel={() => setEditing(null)} />
            : <Btn variant="outline" size="sm" block style={{ marginTop: 10 }} onClick={() => setEditing({})}>+ 배송지 추가</Btn>}
        </>
      )}
    </div>
  );
}

/* 뒤로가기 버튼 — 브라우저 history 를 되짚어 직전 화면으로 돌아간다.
   (장바구니·마이 헤더용. 스마트앱의 '뒤로가기 화살표'와 같은 역할) */
function BackBtn({ style }) {
  return (
    <button
      type="button"
      aria-label="뒤로가기"
      onClick={() => window.history.back()}
      style={{
        width: 42, height: 42, borderRadius: 999, flex: "none",
        display: "grid", placeItems: "center", cursor: "pointer",
        border: "1px solid var(--border)", background: "#fff", color: "var(--ink)",
        ...style,
      }}
    >
      <Icon name="chevL" size={22} />
    </button>
  );
}

/* ============ 장바구니 ============ */
function CartScreen({ cart, removeItem, go, toast, auth }) {
  const items = cart;
  const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
  const ship = 0; // 전 상품 무료배송 — 판매가에 배송비가 포함돼 결제 시 따로 받지 않는다 (5만원 규칙 폐지)
  const total = subtotal + ship;
  const MAX_FIT = 5; // 한 번에 최대 5벌 (백엔드 MAX_LAYERS)

  // 여러 벌 골라 한꺼번에 피팅 — 선택된 줄(상품+옵션) 키 집합 / 합성중 / 결과
  const [selected, setSelected] = useState(() => new Set());
  const [fitting, setFitting] = useState(false);
  const [result, setResult] = useState(null); // { url, worn: [items] }
  const lineKey = (it) => it.id + "|" + (it._optKey || "");

  function toggle(it) {
    const k = lineKey(it);
    // 피팅 미지원 상품(온전한 옷 컷 없음 판정)은 다중피팅 선택에서도 잠근다.
    if (!selected.has(k) && it.fitting_supported === false) {
      toast && toast("이 상품은 상세 이미지에 온전한 옷 컷이 없어 가상피팅을 지원하지 않아요");
      return;
    }
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(k)) { next.delete(k); return next; }
      if (next.size >= MAX_FIT) { toast && toast("한 번에 최대 " + MAX_FIT + "벌까지 피팅할 수 있어요"); return prev; }
      next.add(k); return next;
    });
  }

  async function fitSelected() {
    if (!auth || !auth.loggedIn) { toast && toast("로그인이 필요해요"); go("login"); return; }
    const lines = items.filter((it) => selected.has(lineKey(it)));
    if (!lines.length) return;
    if (lines.length > MAX_FIT) { toast && toast("한 번에 최대 " + MAX_FIT + "벌까지 피팅할 수 있어요"); return; }
    // 피팅에 쓸 아바타/사진이 없으면 먼저 준비하도록 마이페이지로 안내한다.
    if (!auth.hasBase) { toast && toast("피팅에 쓸 아바타나 사진을 먼저 만들어 주세요"); go("mypage"); return; }
    setFitting(true);
    try {
      const rec = await API.tryon({
        productIds: lines.map((it) => it.id),
        userImage: auth.baseImage,
        // 옷마다 고른 색(옵션값) — 백엔드가 색만 추출해 각 옷을 그 색으로 렌더한다.
        colors: lines.map((it) => (it.selected_options ? Object.values(it.selected_options).join(" ") : null)),
      });
      setFitting(false);
      setSelected(new Set());
      setResult({ url: rec.result_url, worn: lines });
    } catch (e) {
      setFitting(false);
      toast && toast(e && e.status === 401 ? "로그인이 필요해요. 다시 로그인해 주세요." : ((e && e.message) || "피팅 중 문제가 생겼어요. 잠시 후 다시 시도해 주세요."));
    }
  }

  // 결과 모달을 Esc 로도 닫는다. (X 버튼·바깥 클릭과 함께 — 닫는 방법이 하나뿐이면 갇힌 느낌이 든다)
  // ★리스너를 걸지만 반환 함수에서 반드시 해제한다★ — 안 하면 모달을 열 때마다 리스너가 쌓인다(누수).
  // ★이 훅은 아래 `if (!items.length) return` 보다 위에 있어야 한다★ — 조기 반환 뒤에 두면
  //   렌더마다 훅 호출 개수가 달라져 React 규칙이 깨진다.
  useEffect(() => {
    if (!result) return;
    const onKey = (e) => { if (e.key === "Escape") setResult(null); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [result]);

  if (!items.length) {
    return (
      <div className="page wrap" style={{ paddingTop: 40 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 28 }}>
          <BackBtn />
          <h1 className="t-h1" style={{ margin: 0 }}>장바구니</h1>
        </div>
        <div className="empty">
          <div className="ill"><Icon name="cart" 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="primary" onClick={() => go("catalog")}>쇼핑하러 가기</Btn>
        </div>
      </div>
    );
  }

  return (
    <div className="page wrap" style={{ paddingTop: 40, paddingBottom: 64 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 8 }}>
        <BackBtn />
        <h1 className="t-h1" style={{ margin: 0 }}>장바구니 <span className="t-sub" style={{ fontWeight: 400, fontSize: 22 }}>{items.length}</span></h1>
      </div>
      <p className="t-small t-sub" style={{ margin: "0 0 22px" }}>왼쪽 동그라미로 옷을 골라 한꺼번에 피팅해 볼 수 있어요 (최대 {MAX_FIT}벌)</p>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 360px", gap: 36, alignItems: "start" }}>
        <div className="stack" style={{ gap: 14 }}>
          {/* 카드 전체 클릭 → 상품 상세. 왼쪽 동그라미=피팅 선택, 휴지통=삭제 (상세로 안 넘어감). */}
          {items.map((it) => {
            const on = selected.has(lineKey(it));
            return (
            <div key={it.id + (it._optKey || "")} className="card" style={{ padding: 16, display: "flex", gap: 14, cursor: "pointer", alignItems: "center" }}
              onClick={() => go("detail", it)}>
              {/* 피팅 선택 동그라미 — 연회색 테두리는 눈에 안 띄어 '기능 없음'으로 오해됨(사용자 보고).
                  보라 테두리+체크 아이콘 상시 표시 + 아래 '피팅' 라벨로 존재를 분명히 한다. */}
              <div style={{ flex: "none", display: "flex", flexDirection: "column", alignItems: "center", gap: 3 }}>
                <button onClick={(e) => { e.stopPropagation(); toggle(it); }} aria-label="피팅 선택"
                  title="골라서 한꺼번에 입어보기"
                  style={{ width: 30, height: 30, borderRadius: "50%", cursor: "pointer",
                    border: on ? "0" : "2px solid var(--primary)", background: on ? "var(--primary)" : "var(--accent-soft)",
                    color: on ? "#fff" : "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center",
                    boxShadow: on ? "0 2px 8px rgba(109,40,217,.35)" : "none" }}>
                  <Icon name="check" size={16} />
                </button>
                <span style={{ fontSize: 10, fontWeight: 700, color: "var(--primary)" }}>피팅</span>
              </div>
              <div style={{ width: 96, height: 120, borderRadius: 12, overflow: "hidden", flex: "none" }}>
                {it.image
                  ? <img src={it.image} alt={it.name} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
                  : <PhImg tone={it.tone} ink={it.toneInk} type={it.type} />}
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <h3 style={{ fontSize: 16, fontWeight: 500, margin: "0 0 8px" }}>{it.name}</h3>
                <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
                  {it.selected_options
                    ? Object.entries(it.selected_options).map(([k, v]) => <Chip key={k} soft>{k} {v}</Chip>)
                    : [it.type, it.color].filter(Boolean).map((x) => <Chip key={x} outline>{x}</Chip>)}
                </div>
                <div className="row" style={{ justifyContent: "space-between", marginTop: 14 }}>
                  <span className="t-small t-sub" style={{ fontWeight: 600 }}>수량 {it.qty}개</span>
                  <div className="row" style={{ gap: 16 }}>
                    <Price value={it.price * it.qty} />
                    <button onClick={(e) => { e.stopPropagation(); removeItem(it.id, it._optKey); }} style={{ background: "none", border: 0, color: "var(--sub)", cursor: "pointer" }} aria-label="삭제"><Icon name="trash" size={18} /></button>
                  </div>
                </div>
              </div>
            </div>
            );
          })}
        </div>

        {/* 요약 */}
        <aside className="card" style={{ padding: 24, position: "sticky", top: 96 }}>
          {/* 선택한 옷 한꺼번에 피팅 — 버튼을 항상 노출해 기능이 있다는 걸 알게 한다.
              (예전엔 1벌 이상 골라야만 나타나서 '다중피팅이 없다'고 오해하기 쉬웠음) */}
          <div style={{ marginBottom: 18, paddingBottom: 18, borderBottom: "1px solid var(--border)" }}>
            <Btn variant="primary" size="lg" block icon="sparkle" onClick={fitSelected}
              disabled={fitting || selected.size === 0}>
              {selected.size > 0 ? `선택한 ${selected.size}벌 피팅하기` : "동그라미로 옷을 골라 피팅하기"}
            </Btn>
            {selected.size > 0 && (
              <button onClick={() => setSelected(new Set())} style={{ background: "none", border: 0, color: "var(--sub)", cursor: "pointer", fontSize: 12, marginTop: 8, width: "100%" }}>선택 해제</button>
            )}
          </div>
          <h3 className="t-h3" style={{ margin: "0 0 18px" }}>주문 요약</h3>
          <div className="stack" style={{ gap: 12 }}>
            <Row k="상품 금액" v={`${DENKY.won(subtotal)}원`} />
            <Row k="배송비" v="무료" />
            <p className="t-caption t-sub" style={{ margin: 0 }}>전 상품 무료배송</p>
            <hr className="divider" />
            <div className="row" style={{ justifyContent: "space-between" }}>
              <strong style={{ fontSize: 15 }}>결제 예정 금액</strong>
              <Price value={total} lg />
            </div>
          </div>
          <Btn variant="primary" size="lg" block iconR="arrowR" style={{ marginTop: 22 }} onClick={() => go("checkout")}>주문하기</Btn>
          <p className="t-caption t-sub row" style={{ gap: 6, justifyContent: "center", marginTop: 12 }}>
            <Icon name="shield" size={14} />안전한 결제 · 안심 배송
          </p>
        </aside>
      </div>

      {/* 여러 벌 합성 중 오버레이 */}
      {fitting && (
        <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" }}>
          <div style={{ background: "#fff", borderRadius: 16, padding: "26px 30px", textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center" }}>
            <div className="spinner spinner-dark" style={{ width: 40, height: 40 }}></div>
            <p style={{ margin: "14px 0 2px", fontWeight: 700 }}>여러 옷을 입혀보는 중…</p>
            <p className="t-caption t-sub" style={{ margin: 0 }}>최대 1분 정도 걸려요</p>
          </div>
        </div>
      )}

      {/* 합성 결과 — 큰 이미지 + 입은 옷별 사러가기 + 피팅기록 보기 */}
      {result && (
        <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.9)", zIndex: 1000, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 20 }}
          onClick={() => setResult(null)}>
          <img src={result.url} alt="피팅 결과" style={{ maxHeight: "68vh", maxWidth: "92vw", borderRadius: 12, objectFit: "contain" }} onClick={(e) => e.stopPropagation()} />
          <div style={{ marginTop: 16, background: "#fff", borderRadius: 16, padding: 16, display: "flex", flexDirection: "column", gap: 8, width: "min(380px,92vw)" }} onClick={(e) => e.stopPropagation()}>
            {result.worn.map((w) => (
              <Btn key={lineKey(w)} variant="outline" block icon="bag" onClick={() => { setResult(null); go("detail", w); }}>
                {result.worn.length === 1 ? "이 옷 사러가기" : shortName(w.name) + " 사러가기"}
              </Btn>
            ))}
            {/* 친구에게 물어보기는 '피팅 기록'에서만 한다.
                여기(장바구니 결과 모달)는 옷이 여러 벌이면 버튼이 7개까지 늘고, 맥락도 '결제'라
                공유 버튼이 구매 동선을 끊는다. 아래 [피팅 기록에서 보기]가 그 출구다. */}
            <Btn variant="dark" block icon="sparkle" onClick={() => { setResult(null); go("mypage", { tab: "fitting" }); }}>피팅 기록에서 보기</Btn>
          </div>
          {/* 닫기 — 다른 전체화면 모달(피팅기록 확대·사진 확대)과 같은 모양.
              바깥 클릭만으로 닫히면 '이미지와 버튼 패널에는 stopPropagation 이 걸려 있어'
              누를 수 있는 여백이 얇은 띠뿐이라, 제대로 닫힌 건지 헷갈린다. */}
          <button onClick={(e) => { e.stopPropagation(); setResult(null); }} aria-label="닫기"
            style={{ position: "fixed", top: 20, right: 20, width: 44, height: 44, borderRadius: 999, border: "none", background: "rgba(255,255,255,.15)", color: "#fff", fontSize: 22, cursor: "pointer" }}>✕</button>
        </div>
      )}
    </div>
  );
}
// 결제(주문) 화면 수량 스테퍼 버튼 스타일 — 장바구니는 +/- 를 없앴지만 결제 화면엔 남아있다.
const qtyBtn = { width: 36, height: 36, border: 0, background: "#fff", fontSize: 18, color: "var(--ink)" };
function Row({ k, v }) {
  return <div className="row" style={{ justifyContent: "space-between" }}><span className="t-small t-sub">{k}</span><span className="t-small" style={{ fontWeight: 600 }}>{v}</span></div>;
}

/* ============ 결제 ============ */
function CheckoutScreen({ cart, setQty, go, toast, clearCart, intent, auth, checkoutEnabled = true }) {
  // 로그인 사용자의 기본 배송지를 결제 폼에 자동으로 채웁니다.
  // 주소 = 기본주소 + 상세주소를 한 줄로 합칩니다.
  const fullAddress = [auth && auth.address, auth && auth.addressDetail].filter(Boolean).join(" ");
  // 결제수단은 카드 단일(토스 결제창 CARD) — 선택 상태가 필요 없어졌다.
  // intent.payOrderId 가 있으면 '직접판매(marketplace) pending 주문 결제' 모드,
  // 없으면 기존 '장바구니 주문' 모드입니다.
  const payOrderId = intent && intent.payOrderId;
  const [pendingQty, setPendingQty] = useState(1);    // 바로구매(단일상품) 수량
  const unitPrice = payOrderId ? intent.amount : 0;    // checkoutIntent 의 qty1 금액 = 단가
  const items = payOrderId
    ? [{ id: -1, name: intent.productName || "상품", price: unitPrice, qty: pendingQty, type: "상의", selected_options: intent.selectedOptions || null }]
    : cart;
  const subtotal = items.reduce((s, it) => s + it.price * it.qty, 0);
  // 전 상품 무료배송 — 판매가에 배송비가 포함돼 결제 시 따로 받지 않는다 (5만원 규칙 폐지)
  const ship = 0;
  const total = payOrderId ? unitPrice * pendingQty : subtotal + ship;
  // 수량 변경 — 장바구니는 cart 상태를, 바로구매는 로컬 pendingQty 를 바꿉니다.
  function changeQty(it, delta) {
    if (payOrderId) setPendingQty((q) => Math.min(20, Math.max(1, q + delta)));
    else setQty(it.id, it.qty + delta, it._optKey);
  }
  const [done, setDone] = useState(null);   // 완료된 주문 정보(주문번호 표시용)
  // 배송지 입력값 — 로그인 사용자 프로필(이름/연락처/기본 배송지)로 초기값을 채웁니다.
  const [recipient, setRecipient] = useState((auth && auth.name) || "");
  const [phone, setPhone] = useState((auth && auth.phone) || "");
  const [zip, setZip] = useState((auth && auth.zipcode) || "");  // 우편번호 — 위탁 자동 발주에 필요
  const [address, setAddress] = useState(fullAddress);
  const [request, setRequest] = useState("");
  const [busy, setBusy] = useState(false);

  // 내 기본 배송지를 (다시) 불러와 폼을 채웁니다.
  function fillFromProfile() {
    setRecipient((auth && auth.name) || "");
    setPhone((auth && auth.phone) || "");
    setZip((auth && auth.zipcode) || "");
    setAddress(fullAddress);
  }
  const hasSavedAddress = !!(auth && auth.loggedIn && (auth.name || auth.phone || fullAddress));

  // 결제 — 직접판매면 pending 주문 확정(pay), 장바구니면 새 주문 생성(createOrder).
  async function placeOrder() {
    if (!items.length) { toast("주문할 상품이 없어요"); return; }
    if (!recipient.trim() || !phone.trim() || !address.trim()) { toast("배송지(받는 분/연락처/주소)를 입력해 주세요"); return; }
    setBusy(true);
    try {
      let order;
      if (payOrderId) {
        // 수량을 바꿨으면(>1) 그 수량으로 pending 주문을 다시 만들어 결제합니다.
        let oid = payOrderId;
        if (pendingQty > 1 && intent.productId) {
          const fresh = await API.checkoutIntent(intent.productId, intent.selectedOptions || undefined, pendingQty);
          oid = fresh.order_id;
        }
        order = await API.payOrder(oid, {
          recipient_name: recipient.trim(),
          phone: phone.trim(),
          zipcode: zip.trim() || null,
          address: address.trim(),
          request_note: request.trim() || null,
        });
      } else {
        order = await API.createOrder({
          items: cart.map((it) => ({ product_id: it.id, qty: it.qty, selected_options: it.selected_options || undefined })),
          recipient_name: recipient.trim(),
          phone: phone.trim(),
          zipcode: zip.trim() || null,
          address: address.trim(),
          request_note: request.trim() || null,
        });
        clearCart();
      }
      setDone(order);
    } catch (e) {
      if (e.status === 401) { toast("로그인이 필요해요"); go("login"); }
      else toast(e.message || "주문에 실패했어요");
    }
    setBusy(false);
  }

  if (done) {
    return (
      <div className="page wrap" style={{ paddingTop: 80, maxWidth: 520, paddingBottom: 80 }}>
        <div className="card" style={{ padding: 48, textAlign: "center" }}>
          <div style={{ width: 76, height: 76, borderRadius: "50%", background: "#E7F6ED", color: "var(--success)", display: "grid", placeItems: "center", margin: "0 auto 22px" }}>
            <Icon name="check" size={40} stroke={2.4} />
          </div>
          <h2 className="t-h2" style={{ margin: "0 0 10px" }}>주문이 완료됐어요</h2>
          <p className="t-body t-sub" style={{ margin: "0 0 28px" }}>주문번호 <strong className="t-mono" style={{ color: "var(--ink)" }}>DK-{String(done.id).padStart(6, "0")}</strong><br />배송 현황은 마이페이지에서 확인할 수 있어요.</p>
          <div className="stack" style={{ gap: 12 }}>
            <Btn variant="primary" block onClick={() => go("mypage")}>주문 내역 보기</Btn>
            <Btn variant="ghost" block onClick={() => go("home")}>홈으로</Btn>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="page wrap" style={{ paddingTop: 40, paddingBottom: 64 }}>
      <h1 className="t-h1" style={{ marginBottom: 28 }}>결제</h1>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 360px", gap: 36, alignItems: "start" }}>
        <div className="stack" style={{ gap: 24 }}>
          {/* 배송지 */}
          <section className="card" style={{ padding: 24 }}>
            <div className="row" style={{ justifyContent: "space-between", margin: "0 0 18px" }}>
              <h3 className="t-h3" style={{ margin: 0 }}>배송지</h3>
              {hasSavedAddress && (
                <button onClick={fillFromProfile} className="t-small"
                  style={{ background: "none", border: 0, color: "var(--primary)", fontWeight: 600, cursor: "pointer", display: "flex", alignItems: "center", gap: 5 }}>
                  <Icon name="user" size={15} />내 기본 배송지 불러오기
                </button>
              )}
            </div>
            <div className="grid-2" style={{ gap: 16 }}>
              <div className="field"><label>받는 분</label><input className="input" placeholder="홍길동" value={recipient} onChange={(e) => setRecipient(e.target.value)} /></div>
              <div className="field"><label>연락처</label><input className="input" placeholder="010-1234-5678" value={phone} onChange={(e) => setPhone(e.target.value)} /></div>
              {/* 우편번호 — 위탁 상품 자동 발주(도매꾹 배송대행)에 필요 */}
              <div className="field"><label>우편번호</label><input className="input" placeholder="12345" maxLength={5} value={zip} onChange={(e) => setZip(e.target.value.replace(/\D/g, ""))} /></div>
              <div className="field" style={{ gridColumn: "1 / -1" }}><label>주소</label><input className="input" placeholder="서울특별시 마포구 …" value={address} onChange={(e) => setAddress(e.target.value)} /></div>
              <div className="field" style={{ gridColumn: "1 / -1" }}><label>배송 요청사항</label><input className="input" placeholder="문 앞에 놓아주세요" value={request} onChange={(e) => setRequest(e.target.value)} /></div>
            </div>
          </section>
          {/* 결제수단 — 현재 지원 수단은 카드뿐(토스 결제창 method=CARD).
              지원 안 하는 수단(카카오페이·무통장입금)을 보여주면 소비자 오인 + PG 심사 지적 대상이라 뺐다.
              무통장입금을 붙이려면 가상계좌 웹훅(입금 확인) 구현이 선행돼야 한다. */}
          <section className="card" style={{ padding: 24 }}>
            <h3 className="t-h3" style={{ margin: "0 0 18px" }}>결제수단</h3>
            <div className="grid-3" style={{ gap: 12 }}>
              <button className="card"
                style={{ padding: "16px 12px", textAlign: "center", fontWeight: 600, fontSize: 14, cursor: "default", borderColor: "var(--primary)", background: "var(--accent-soft)", color: "var(--primary-dark)" }}>
                신용/체크카드
              </button>
            </div>
            <p className="t-cap" style={{ margin: "10px 0 0", color: "var(--sub)" }}>토스페이먼츠 결제창에서 안전하게 결제됩니다.</p>
          </section>
          {/* 주문 상품 */}
          <section className="card" style={{ padding: 24 }}>
            <h3 className="t-h3" style={{ margin: "0 0 16px" }}>주문 상품 {items.length}</h3>
            <div className="stack" style={{ gap: 12 }}>
              {items.map((it) => (
                <div key={it.id + (it._optKey || "")} className="row" style={{ gap: 14 }}>
                  <div style={{ width: 56, height: 70, borderRadius: 8, overflow: "hidden", flex: "none" }}>
                    {it.image
                      ? <img src={it.image} alt={it.name} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
                      : <PhImg tone={it.tone} ink={it.toneInk} type={it.type} />}
                  </div>
                  <div style={{ flex: 1 }}>
                    <p style={{ fontSize: 14, fontWeight: 500, margin: "0 0 4px" }}>{it.name}</p>
                    {it.selected_options && <p className="t-small t-sub" style={{ margin: "0 0 6px" }}>{Object.entries(it.selected_options).map(([k, v]) => `${k} ${v}`).join(" · ")}</p>}
                    <div className="row" style={{ border: "1px solid var(--border-strong)", borderRadius: 8, overflow: "hidden", width: "fit-content" }}>
                      <button onClick={() => changeQty(it, -1)} style={qtyBtn} aria-label="수량 줄이기">−</button>
                      <span style={{ width: 38, textAlign: "center", fontWeight: 600 }}>{it.qty}</span>
                      <button onClick={() => changeQty(it, +1)} style={qtyBtn} aria-label="수량 늘리기">+</button>
                    </div>
                  </div>
                  <Price value={it.price * it.qty} />
                </div>
              ))}
            </div>
          </section>
        </div>

        <aside className="card" style={{ padding: 24, position: "sticky", top: 96 }}>
          <h3 className="t-h3" style={{ margin: "0 0 18px" }}>결제 금액</h3>
          <div className="stack" style={{ gap: 12 }}>
            <Row k="상품 금액" v={`${DENKY.won(subtotal)}원`} />
            <Row k="배송비" v={ship ? `${DENKY.won(ship)}원` : "무료"} />
            <hr className="divider" />
            <div className="row" style={{ justifyContent: "space-between" }}>
              <strong style={{ fontSize: 15 }}>최종 결제 금액</strong><Price value={total} lg />
            </div>
          </div>
          {/* 소프트 오픈 — 결제가 잠긴 동안은 버튼 대신 안내 (서버도 /payments/checkout 에서 막는다) */}
          {!checkoutEnabled && (
            <p className="t-small" style={{ margin: "22px 0 0", padding: "12px 14px", borderRadius: 10, background: "var(--surface)", color: "var(--sub)", lineHeight: 1.6, textAlign: "center" }}>
              지금은 <strong style={{ color: "var(--ink)" }}>시범 운영 중</strong>이라 결제가 잠겨 있어요.<br />구매는 곧 열릴 예정이에요!
            </p>
          )}
          <Btn variant="primary" size="lg" block style={{ marginTop: checkoutEnabled ? 22 : 10 }} disabled={busy || !checkoutEnabled} onClick={placeOrder}>{!checkoutEnabled ? "결제 준비 중" : (busy ? "결제 중…" : `${DENKY.won(total)}원 결제하기`)}</Btn>
        </aside>
      </div>
    </div>
  );
}

/* ============ 로그인 / 회원가입 ============ */

/* ---------- 비밀번호 작성규칙 (백엔드 validate_password_strength 와 동일한 조합 규칙) ----------
   대문자·소문자·숫자·특수문자 중 3종 이상 조합 8자 이상, 또는 2종 조합 10자 이상. 최대 72자.
   (흔한 비밀번호·이메일 아이디 포함 등 나머지 검사는 서버가 하고, 그 메시지를 그대로 보여준다) */
const PW_RULE_TEXT = "영문 대/소문자·숫자·특수문자 중 3종 조합 8자 이상 또는 2종 조합 10자 이상";
function passwordRuleError(pw) {
  // 문자 종류 세기 — 대문자 / 소문자 / 숫자 / 특수문자(영숫자 외 전부)
  const kinds = [/[A-Z]/, /[a-z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((re) => re.test(pw || "")).length;
  const len = (pw || "").length;
  if (len > 72) return "비밀번호는 최대 72자까지 쓸 수 있어요.";
  if (kinds >= 3 && len >= 8) return null;
  if (kinds >= 2 && len >= 10) return null;
  return "비밀번호가 규칙에 안 맞아요 — " + PW_RULE_TEXT + "으로 만들어 주세요.";
}

/* ---------- 이메일 인증 코드 입력 (가입 직후 · 마이페이지 배너 공용) ----------
   6자리 코드 입력 + [인증하기] + [재발송]. 인증에 성공하면 백엔드가 토큰을 발급해
   자동 로그인되고(api.js 가 저장), onVerified 콜백을 부른다. */
function EmailVerifyBox({ email, toast, onVerified, initialDevCode }) {
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [resending, setResending] = useState(false);
  const [error, setError] = useState("");
  // 로컬 개발(메일 발송 꺼짐) 환경에서만 응답에 실려 오는 코드 힌트
  const [devCode, setDevCode] = useState(initialDevCode || null);

  // 인증하기 — 코드가 맞으면 인증 완료 + 토큰 저장(자동 로그인)
  async function verify() {
    setError("");
    const c = code.trim();
    if (!/^\d{6}$/.test(c)) { setError("메일로 받은 6자리 숫자 코드를 입력해 주세요."); return; }
    setBusy(true);
    try {
      await API.verifyEmail(email, c);
      if (onVerified) await onVerified();
    } catch (e) {
      setError(e.message || "인증에 실패했어요. 잠시 후 다시 시도해 주세요.");
    }
    setBusy(false);
  }

  // 재발송 — 새 코드를 메일로 다시 보낸다 (개발 환경이면 코드 힌트도 갱신)
  async function resend() {
    setResending(true);
    try {
      const r = await API.resendVerification(email);
      if (r && r.dev_verification_code) setDevCode(r.dev_verification_code);
      if (toast) toast("인증 코드를 다시 보냈어요. 메일함(스팸함 포함)을 확인해 주세요");
    } catch (e) {
      if (toast) toast(e.message || "재발송에 실패했어요. 잠시 후 다시 시도해 주세요");
    }
    setResending(false);
  }

  return (
    <div className="stack" style={{ gap: 10 }}>
      <div className="field"><label>인증코드 (6자리)</label>
        <input className="input" type="text" inputMode="numeric" maxLength={6} placeholder="123456"
          value={code} onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
          onKeyDown={(e) => { if (e.key === "Enter") verify(); }}
          style={{ letterSpacing: 4, fontWeight: 700 }} /></div>
      {devCode && (
        <p className="t-caption t-sub" style={{ margin: 0 }}>
          개발용 코드: <strong className="t-mono" style={{ color: "var(--ink)" }}>{devCode}</strong> (메일 발송이 꺼진 로컬에서만 보여요)
        </p>
      )}
      {error && <p className="t-small" style={{ color: "var(--error)", margin: 0 }}>{error}</p>}
      <div className="row" style={{ gap: 8 }}>
        <div style={{ flex: 1 }}>
          <Btn variant="primary" block disabled={busy} onClick={verify}>{busy ? "확인 중…" : "인증하기"}</Btn>
        </div>
        <Btn variant="outline" disabled={resending} onClick={resend}>{resending ? "발송 중…" : "재발송"}</Btn>
      </div>
    </div>
  );
}

function AuthScreen({ mode, go, auth, toast, returnTo }) {
  const signup = mode === "signup";
  // 로그인/가입 성공 후 돌아갈 화면. (예: 가상피팅 게이트에서 로그인 → 다시 tryon 으로)
  const afterAuth = returnTo || "home";
  // ---- 회원(User) 테이블 항목들 ----
  const [email, setEmail] = useState("");       // 이메일 (필수)
  const [password, setPassword] = useState("");  // 비밀번호 (필수 — 3종 8자+ 또는 2종 10자+)
  const [confirm, setConfirm] = useState("");    // 비밀번호 확인
  // 가입 직후 '이메일 인증' 단계 — {email, devCode}. null 이면 평소 폼 표시.
  const [verifyStep, setVerifyStep] = useState(null);
  const [name, setName] = useState("");          // 이름
  const [phone, setPhone] = useState("");        // 전화번호
  const [gender, setGender] = useState("");      // 성별: male/female/other
  const [height, setHeight] = useState("");      // 키 (cm) — 가입 필수
  const [weight, setWeight] = useState("");      // 몸무게 (kg) — 가입 필수
  const [usualTop, setUsualTop] = useState("");    // 평소 상의 — 가입 필수
  const [usualBottom, setUsualBottom] = useState(""); // 평소 하의 — 가입 필수
  const [photo, setPhoto] = useState(null);      // 앞모습 사진 dataURL (가상피팅용)
  const [sidePhoto, setSidePhoto] = useState(null); // 측면(옆모습) 사진 dataURL (둘레 추정용)
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [showPw, setShowPw] = useState(false);       // 비밀번호 표시 토글
  const [showConfirm, setShowConfirm] = useState(false);
  const fileRef = useRef(null);
  const sideFileRef = useRef(null);
  // ---- 약관 동의 (개인정보보호법) ----
  const [birthdate, setBirthdate] = useState(""); // 생년월일 YYYY-MM-DD (가입 필수 — 만 14세 확인)
  const [agree, setAgree] = useState({ age14: false, terms: false, privacy: false, overseas: false, marketing: false });
  const [legalDoc, setLegalDoc] = useState(null);    // 모달로 열 문서 키
  const allRequired = agree.age14 && agree.terms && agree.privacy && agree.overseas; // 필수 (만 14세 + 3종)
  const allChecked = allRequired && agree.marketing;
  const toggleAll = (v) => setAgree({ age14: v, terms: v, privacy: v, overseas: v, marketing: v });
  const setOne = (k, v) => setAgree((a) => ({ ...a, [k]: v }));
  // ---- 소셜 신규 계정 만 14세 온보딩 ----
  const [ageGate, setAgeGate] = useState(false);       // 온보딩 모달 표시 여부
  const [agBirth, setAgBirth] = useState("");          // 온보딩 생년월일
  const [agConsent, setAgConsent] = useState(false);   // 온보딩 만 14세 동의
  const [agBusy, setAgBusy] = useState(false);
  const [agError, setAgError] = useState("");

  // 앞모습 사진 파일 선택 → base64(dataURL)로 보관 (가입 시 함께 등록)
  function onPickPhoto(e) {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    readFileAsDataUrl(f).then(setPhoto).catch(() => {});
  }

  // 측면(옆모습) 사진 파일 선택 → base64 보관 (가입 시 함께 등록)
  function onPickSidePhoto(e) {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    readFileAsDataUrl(f).then(setSidePhoto).catch(() => {});
  }

  // 실제 백엔드로 로그인/회원가입을 처리합니다.
  async function submit() {
    setError("");
    if (!email || !password) { setError("이메일과 비밀번호를 입력해 주세요."); return; }
    // 이메일 형식 검증 — 형식이 안 맞으면 친절한 안내(서버 422 시스템 메시지 대신)
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
      setError("이메일 형식이 올바르지 않아요. (예: you@example.com)");
      return;
    }
    if (signup) {
      // 비밀번호 조합 규칙(3종 8자+ / 2종 10자+)을 클라이언트에서 먼저 검사 — 서버 422에만 기대지 않는다
      const pwErr = passwordRuleError(password);
      if (pwErr) { setError(pwErr); return; }
      if (password !== confirm) { setError("비밀번호가 일치하지 않아요."); return; }
      // 가입 필수: 성별·키·몸무게·평소 상/하의 사이즈 (아바타·사이즈 추천의 기초)
      if (!gender) { setError("성별을 선택해 주세요."); return; }
      const h = parseInt(height, 10), w = parseInt(weight, 10);
      if (!(h >= 50 && h <= 250)) { setError("키를 정확히 입력해 주세요 (50~250cm)."); return; }
      if (!(w >= 20 && w <= 300)) { setError("몸무게를 정확히 입력해 주세요 (20~300kg)."); return; }
      if (!usualTop.trim() || !usualBottom.trim()) { setError("평소 입는 상의·하의 사이즈를 입력해 주세요."); return; }
      // 만 14세 이상 관문 (개인정보보호법) — 생년월일 필수 + 만 14세 이상 동의 필수.
      if (!birthdate) { setError("생년월일을 입력해 주세요. (만 14세 이상만 가입할 수 있어요)"); return; }
      if (!allRequired) { setError("필수 약관(만 14세 이상·이용약관·개인정보 수집·이용·국외이전)에 동의해 주세요."); return; }
    }
    setBusy(true);
    try {
      if (signup) {
        // 회원 테이블 항목을 채워 가입합니다. (선택 항목은 입력했을 때만 보냄)
        const payload = { email, password };
        if (name.trim()) payload.name = name.trim();
        if (phone.trim()) payload.phone = phone.trim();
        if (gender) payload.gender = gender;
        payload.height = parseInt(height, 10);
        payload.weight = parseInt(weight, 10);
        payload.usual_top_size = usualTop.trim();
        payload.usual_bottom_size = usualBottom.trim();
        if (photo) payload.front_photo = photo; // 앞모습 사진까지 함께 가입
        if (sidePhoto) payload.side_photo = sidePhoto; // 측면 사진도 함께
        payload.birthdate = birthdate;               // [필수] 생년월일 (만 14세 확인)
        payload.age_consent = agree.age14;           // [필수] 만 14세 이상 + 약관 동의
        payload.marketing_consent = agree.marketing; // [선택] 마케팅 수신 동의
        const res = await auth.signup(payload);
        if (res && res.verification_required) {
          // 자동 로그인하지 않고 '이메일 인증' 단계로 — 인증해야 가상피팅을 쓸 수 있다.
          setVerifyStep({ email: email.trim().toLowerCase(), devCode: res.dev_verification_code || null });
          toast("가입 완료! 메일로 보낸 인증코드를 입력해 주세요");
        } else {
          // 인증이 꺼진 환경(테스트 등) — 예전처럼 바로 로그인하고 진행
          await auth.login(email, password);
          toast("환영해요! 가입이 완료됐어요");
          go(afterAuth);
        }
      } else {
        await auth.login(email, password);
        toast("로그인됐어요");
        go(afterAuth);  // 원래 보던 화면(예: tryon)으로 복귀, 없으면 홈
      }
    } catch (e) {
      setError(e.message || "처리 중 문제가 생겼어요.");
    }
    setBusy(false);
  }

  // '나중에 하기' — 인증 없이 로그인만 하고 진행한다. (로그인은 미인증도 허용, 가상피팅만 잠김)
  async function skipVerification() {
    setError("");
    setBusy(true);
    try {
      await auth.login(verifyStep.email, password);
      toast("로그인됐어요 — 가상피팅을 쓰려면 마이페이지에서 이메일 인증을 완료해 주세요");
      go(afterAuth);
    } catch (e) {
      setError(e.message || "로그인에 실패했어요. 잠시 후 다시 시도해 주세요.");
    }
    setBusy(false);
  }

  // 소셜 로그인 — 팝업으로 동의 → 인가코드 → 백엔드 → 우리 토큰.
  async function startSocial(provider) {
    setError("");
    const redirectUri = location.origin + "/oauth-callback.html";
    let url;
    try { ({ url } = await API.socialLoginUrl(provider, redirectUri)); }
    catch (e) { setError((provider === "kakao" ? "카카오" : "네이버") + " 로그인이 아직 설정되지 않았어요."); return; }
    const popup = window.open(url, "denky-social", "width=480,height=720");
    if (!popup) { setError("팝업이 차단됐어요. 팝업을 허용한 뒤 다시 시도해 주세요."); return; }
    function onMsg(e) {
      if (e.origin !== location.origin || !e.data || !e.data.denkyOauth) return;
      window.removeEventListener("message", onMsg);
      const { code, error: oerr } = e.data.denkyOauth;
      if (oerr || !code) { setError("소셜 로그인이 취소됐어요."); return; }
      (async () => {
        setBusy(true);
        try {
          const res = await API.socialLogin(provider, code, redirectUri);
          await auth.refresh();
          if (res && res.age_gate_required) {
            setAgeGate(true);   // 소셜 신규 계정 → 만 14세 온보딩 강제 (완료해야 진입)
          } else {
            toast("로그인됐어요");
            go(afterAuth);  // 원래 보던 화면으로 복귀
          }
        } catch (err) { setError(err.message || "소셜 로그인 처리에 실패했어요."); }
        setBusy(false);
      })();
    }
    window.addEventListener("message", onMsg);
  }

  // 소셜 온보딩 — 생년월일+동의 저장 후 진입. (만 14세 미만·미동의는 서버가 거부)
  async function submitAgeGate() {
    setAgError("");
    if (!agBirth) { setAgError("생년월일을 입력해 주세요."); return; }
    if (!agConsent) { setAgError("만 14세 이상 동의가 필요해요."); return; }
    setAgBusy(true);
    try {
      await API.completeAgeGate(agBirth, agConsent);
      await auth.refresh();
      setAgeGate(false);
      toast("환영해요! 가입이 완료됐어요");
      go(afterAuth);
    } catch (e) {
      setAgError(e.message || "처리 중 문제가 생겼어요.");  // 예: 만 14세 미만은 이용할 수 없어요
    }
    setAgBusy(false);
  }
  // 온보딩 취소 → 로그아웃(관문 미완료면 앱 진입 불가)
  function cancelAgeGate() {
    API.logout();
    auth.refresh();
    setAgeGate(false);
    toast("만 14세 이상 확인이 필요해요");
  }

  // 소셜 로그인 버튼 묶음 (재사용)
  const socialButtons = (
    <div className="stack" style={{ gap: 8 }}>
      <button type="button" disabled={busy} onClick={() => startSocial("kakao")}
        style={{ height: 46, borderRadius: 10, border: "none", background: "#FEE500", color: "#191600", fontWeight: 700, fontSize: 14, cursor: "pointer", fontFamily: "inherit" }}>
        카카오로 시작하기
      </button>
      <button type="button" disabled={busy} onClick={() => startSocial("naver")}
        style={{ height: 46, borderRadius: 10, border: "none", background: "#03C75A", color: "#fff", fontWeight: 700, fontSize: 14, cursor: "pointer", fontFamily: "inherit" }}>
        네이버로 시작하기
      </button>
      <div className="row" style={{ alignItems: "center", gap: 10, margin: "4px 0" }}>
        <div style={{ flex: 1, height: 1, background: "var(--border)" }} />
        <span className="t-caption t-sub">또는 이메일로</span>
        <div style={{ flex: 1, height: 1, background: "var(--border)" }} />
      </div>
    </div>
  );

  return (
    <>
    <div className="page" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", minHeight: "calc(100vh - var(--header-h))" }}>
      {/* 좌측 비주얼 */}
      <div style={{ background: "var(--ink)", color: "#fff", padding: "64px 56px", display: "flex", flexDirection: "column", justifyContent: "center", position: "relative", overflow: "hidden" }}>
        <p className="eyebrow" style={{ color: "#C4B5FD" }}>VIRTUAL FITTING</p>
        <h2 className="t-h1" style={{ fontSize: 40, lineHeight: 1.2, margin: "0 0 16px", color: "#fff" }}>입어보고<br />결정하는 쇼핑</h2>
        <p className="t-body" style={{ color: "rgba(255,255,255,.7)", maxWidth: 340 }}>ibeobwa 회원이 되면 내 사진으로 가상피팅을 즐길 수 있어요.</p>
        {/* 가상피팅 전→후 실물 예시 — 실제 피팅기록에서 뽑은 아바타(전)와 착장 렌더(후) 사본.
             후(After)가 결과물이므로 카드를 더 크게 둬 시선이 결과로 가게 한다. */}
        <div style={{ display: "flex", gap: 18, marginTop: 40, alignItems: "center" }}>
          <div className="card" style={{ aspectRatio: "3/4", width: 190, borderRadius: 16, overflow: "hidden", position: "relative", flexShrink: 0 }}>
            <img src="assets/login-before.jpg" alt="가상피팅 전 — 내 아바타" draggable="false"
              style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
            <span style={{ position: "absolute", left: 8, bottom: 8, padding: "3px 10px", borderRadius: 999, background: "rgba(0,0,0,.55)", color: "#fff", fontSize: 11, fontWeight: 700 }}>Before</span>
          </div>
          <div className="card" style={{ aspectRatio: "3/4", width: 250, borderRadius: 16, borderColor: "var(--primary)", overflow: "hidden", position: "relative", flexShrink: 0, boxShadow: "0 12px 32px rgba(0,0,0,.35)" }}>
            <img src="assets/login-after.jpg" alt="가상피팅 후 — 옷을 입은 모습" draggable="false"
              style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
            <span style={{ position: "absolute", left: 8, bottom: 8, padding: "3px 10px", borderRadius: 999, background: "var(--primary)", color: "#fff", fontSize: 12, fontWeight: 700 }}>After</span>
          </div>
        </div>
      </div>

      {/* 우측 폼 */}
      <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "center", padding: "48px 40px" }}>
        <div style={{ width: "100%", maxWidth: 380 }}>
          {verifyStep ? (
            /* ── 가입 직후: 이메일 인증 단계 (코드 입력·재발송·나중에 하기) */
            <>
              <h1 className="t-h2" style={{ margin: "0 0 6px" }}>이메일 인증</h1>
              <p className="t-small t-sub" style={{ margin: "0 0 20px", lineHeight: 1.6 }}>
                <strong style={{ color: "var(--ink)" }}>{verifyStep.email}</strong> 메일로 6자리 인증코드를 보냈어요.<br />
                인증을 완료해야 가상피팅을 쓸 수 있어요.
              </p>
              <EmailVerifyBox email={verifyStep.email} toast={toast} initialDevCode={verifyStep.devCode}
                onVerified={async () => {
                  // 인증 성공 = 자동 로그인(토큰 저장됨) → 상태 갱신 후 원래 화면으로
                  await auth.refresh();
                  toast("이메일 인증 완료! 환영해요");
                  go(afterAuth);
                }} />
              {error && <p className="t-small" style={{ color: "var(--error)", margin: "14px 0 0" }}>{error}</p>}
              <p className="t-small t-sub" style={{ textAlign: "center", marginTop: 20 }}>
                <button type="button" disabled={busy} onClick={skipVerification}
                  style={{ background: "none", border: 0, color: "var(--sub)", textDecoration: "underline", cursor: "pointer", fontFamily: "inherit", fontSize: 13 }}>
                  나중에 하기 (인증 없이 둘러보기)
                </button>
              </p>
            </>
          ) : (
            <>
          <h1 className="t-h2" style={{ margin: "0 0 6px" }}>{signup ? "회원가입" : "로그인"}</h1>
          <p className="t-small t-sub" style={{ margin: "0 0 20px" }}>{signup ? "몇 가지만 입력하면 바로 시작할 수 있어요." : "다시 오셨네요! 반가워요."}</p>

          {socialButtons}

          <div className="stack" style={{ gap: 16 }}>
            <div className="field"><label>이메일</label>
              <input className="input" type="email" placeholder="you@example.com" value={email}
                onChange={(e) => setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !signup) submit(); }} /></div>
            <div className="field"><label>비밀번호</label>
              <div style={{ position: "relative" }}>
                <input className="input" type={showPw ? "text" : "password"} placeholder="••••••••" value={password}
                  onChange={(e) => setPassword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !signup) submit(); }}
                  style={{ paddingRight: 42 }} />
                <button type="button" onClick={() => setShowPw((v) => !v)} aria-label={showPw ? "비밀번호 숨기기" : "비밀번호 표시"}
                  style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", background: "none", border: 0, cursor: "pointer", color: "var(--sub)", display: "flex", padding: 0 }}>
                  <Icon name={showPw ? "eyeoff" : "eye"} size={18} />
                </button>
              </div>
              {/* 가입 폼에만 작성규칙 안내 (서버와 동일 규칙) */}
              {signup && <p className="t-caption t-sub" style={{ margin: "6px 0 0" }}>{PW_RULE_TEXT}</p>}
            </div>
            {signup && (
              <>
                <div className="field"><label>비밀번호 확인</label>
                  <div style={{ position: "relative" }}>
                    <input className="input" type={showConfirm ? "text" : "password"} placeholder="••••••••" value={confirm}
                      onChange={(e) => setConfirm(e.target.value)} style={{ paddingRight: 42 }} />
                    <button type="button" onClick={() => setShowConfirm((v) => !v)} aria-label={showConfirm ? "비밀번호 숨기기" : "비밀번호 표시"}
                      style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", background: "none", border: 0, cursor: "pointer", color: "var(--sub)", display: "flex", padding: 0 }}>
                      <Icon name={showConfirm ? "eyeoff" : "eye"} size={18} />
                    </button>
                  </div></div>
                <div className="field"><label>이름 <span className="t-sub" style={{ fontWeight: 400 }}>(선택)</span></label>
                  <input className="input" placeholder="홍길동" value={name} onChange={(e) => setName(e.target.value)} /></div>
                {/* 체형 정보 — 아바타·사이즈 추천의 기초라 가입 필수 (줄자 없이 아는 값들) */}
                {/* 성별 — 내 정보 폼과 동일한 칩 3개 (활성 = 파란 그라데이션) */}
                <div className="field"><label>성별</label>
                  <div className="row" style={{ gap: 8 }}>
                    {[["male", "남성"], ["female", "여성"]].map(([v, l]) => (
                      <button key={v} type="button" onClick={() => setGender(v)}
                        style={{
                          flex: 1, height: 40, borderRadius: 10, cursor: "pointer", fontWeight: 700, fontSize: 13,
                          background: gender === v ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "var(--surface-2)",
                          color: gender === v ? "#fff" : "var(--sub)",
                          border: gender === v ? "none" : "1px solid var(--border)",
                        }}>{l}</button>
                    ))}
                  </div>
                </div>
                <div className="grid-2" style={{ gap: 10 }}>
                  <div className="field"><label>키(cm)</label>
                    <input className="input" type="number" min="50" max="250" placeholder="예: 172" value={height} onChange={(e) => setHeight(e.target.value)} /></div>
                  <div className="field"><label>몸무게(kg)</label>
                    <input className="input" type="number" min="20" max="300" placeholder="예: 65" value={weight} onChange={(e) => setWeight(e.target.value)} /></div>
                </div>
                <div className="grid-2" style={{ gap: 10 }}>
                  <div className="field"><label>평소 상의</label>
                    <input className="input" type="text" maxLength={10} placeholder="예: M, 95" value={usualTop} onChange={(e) => setUsualTop(e.target.value)} /></div>
                  <div className="field"><label>평소 하의</label>
                    <input className="input" type="text" maxLength={10} placeholder="예: 30, M" value={usualBottom} onChange={(e) => setUsualBottom(e.target.value)} /></div>
                </div>
                <div className="field"><label>생년월일 (만 14세 이상)</label>
                  <input className="input" type="date" max={new Date().toISOString().slice(0, 10)} value={birthdate} onChange={(e) => setBirthdate(e.target.value)} /></div>
                <div className="card" style={{ background: "var(--accent-soft)", padding: 14, display: "flex", gap: 10, alignItems: "flex-start" }}>
                  <Icon name="sparkle" size={18} stroke={1.8} />
                  <p className="t-caption" style={{ margin: 0, lineHeight: 1.5 }}>
                    <strong style={{ color: "var(--ink)" }}>키·몸무게·평소 사이즈로 딱 맞는 사이즈를 추천</strong>해요. 둘레는 안 재도 돼요 — 사진·둘레는 가입 후 내 정보에서 채우면 됩니다.
                  </p>
                </div>

                {/* 약관 동의 (개인정보보호법) */}
                <div className="card" style={{ border: "1px solid var(--border)", padding: 16 }}>
                  <label className="row" style={{ gap: 10, cursor: "pointer", fontWeight: 700, fontSize: 14 }}>
                    <input type="checkbox" checked={allChecked} onChange={(e) => toggleAll(e.target.checked)} />
                    전체 동의 <span className="t-sub" style={{ fontWeight: 400 }}>(선택 항목 포함)</span>
                  </label>
                  <div style={{ height: 1, background: "var(--border)", margin: "12px 0" }} />
                  <div className="stack" style={{ gap: 11 }}>
                    {[
                      ["age14", "[필수] 만 14세 이상입니다", null],
                      ["terms", "[필수] 이용약관 동의", "terms"],
                      ["privacy", "[필수] 개인정보 수집·이용 동의", "privacy"],
                      ["overseas", "[필수] 개인정보 국외 이전(Google·미국) 동의", "overseas"],
                      ["marketing", "[선택] 마케팅 정보 수신 동의", null],
                    ].map(([key, label, docKey]) => (
                      <div key={key} className="row" style={{ justifyContent: "space-between", gap: 8 }}>
                        <label className="row" style={{ gap: 10, cursor: "pointer", fontSize: 13 }}>
                          <input type="checkbox" checked={agree[key]} onChange={(e) => setOne(key, e.target.checked)} style={{ flex: "none" }} />
                          {label}
                        </label>
                        {docKey && (
                          <button type="button" onClick={() => setLegalDoc(docKey)}
                            style={{ background: "none", border: 0, color: "var(--sub)", textDecoration: "underline", cursor: "pointer", fontSize: 12, flex: "none" }}>
                            보기
                          </button>
                        )}
                      </div>
                    ))}
                  </div>
                </div>
              </>
            )}
          </div>

          {error && <p className="t-small" style={{ color: "var(--error)", margin: "14px 0 0" }}>{error}</p>}

          <Btn variant="primary" size="lg" block style={{ marginTop: 24 }} disabled={busy} onClick={submit}>
            {busy ? "처리 중…" : (signup ? "가입하고 시작하기" : "로그인")}
          </Btn>

          {/* 비밀번호 재설정 — 로그인 화면에서만 (메일로 코드 받아 새 비밀번호로 교체) */}
          {!signup && (
            <p style={{ textAlign: "center", margin: "14px 0 0" }}>
              <button type="button" onClick={() => go("reset", returnTo ? { returnTo } : undefined)}
                style={{ background: "none", border: 0, color: "var(--sub)", textDecoration: "underline", cursor: "pointer", fontFamily: "inherit", fontSize: 13 }}>
                비밀번호를 잊으셨나요?
              </button>
            </p>
          )}

          <p className="t-small t-sub" style={{ textAlign: "center", marginTop: 20 }}>
            {signup ? "이미 회원이신가요? " : "아직 회원이 아니신가요? "}
            <button onClick={() => go(signup ? "login" : "signup", returnTo ? { returnTo } : undefined)} style={{ background: "none", border: 0, color: "var(--primary)", fontWeight: 600 }}>
              {signup ? "로그인" : "회원가입"}
            </button>
          </p>
            </>
          )}
        </div>
      </div>
    </div>
    <LegalModal docKey={legalDoc} onClose={() => setLegalDoc(null)} />
    {ageGate && (
      <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "grid", placeItems: "center", zIndex: 1000, padding: 20 }}>
        <div className="card" style={{ background: "var(--surface)", borderRadius: 14, padding: 22, width: "min(400px, 100%)" }}>
          <h2 className="t-h3" style={{ margin: "0 0 6px" }}>만 14세 이상 확인</h2>
          <p className="t-small t-sub" style={{ margin: "0 0 16px" }}>소셜 계정 가입을 마치려면 생년월일과 동의가 필요해요.</p>
          <div className="field">
            <label>생년월일</label>
            <input className="input" type="date" max={new Date().toISOString().slice(0, 10)} value={agBirth} onChange={(e) => setAgBirth(e.target.value)} />
          </div>
          <label className="row" style={{ gap: 10, cursor: "pointer", fontSize: 13, margin: "12px 0", alignItems: "flex-start" }}>
            <input type="checkbox" checked={agConsent} onChange={(e) => setAgConsent(e.target.checked)} style={{ flex: "none", marginTop: 2 }} />
            [필수] 만 14세 이상이며, 이용약관·개인정보 수집·이용에 동의합니다.
          </label>
          {agError && <p className="t-small" style={{ color: "var(--error)", margin: "0 0 12px" }}>{agError}</p>}
          <div className="row" style={{ gap: 10 }}>
            <button type="button" disabled={agBusy} onClick={cancelAgeGate}
              style={{ flex: 1, height: 46, borderRadius: 10, border: "1px solid var(--border)", background: "var(--surface)", color: "var(--sub)", cursor: "pointer", fontFamily: "inherit", fontSize: 14 }}>취소</button>
            <div style={{ flex: 1 }}>
              <Btn variant="primary" size="lg" block disabled={agBusy} onClick={submitAgeGate}>{agBusy ? "처리 중…" : "완료"}</Btn>
            </div>
          </div>
        </div>
      </div>
    )}
    </>
  );
}

/* ============ 비밀번호 재설정 (로그인 화면 '비밀번호를 잊으셨나요?') ============
   ① 이메일 입력 → 재설정 코드 발송 (가입 여부는 노출하지 않음)
   ② 코드 6자리 + 새 비밀번호 입력 → 교체 성공 시 새 토큰으로 자동 로그인 (기존 토큰 무효) */
function PasswordResetScreen({ go, auth, toast, returnTo }) {
  const afterAuth = returnTo || "home";      // 성공 후 돌아갈 화면 (로그인 게이트에서 왔으면 그곳으로)
  const [step, setStep] = useState(1);       // 1=이메일 입력 / 2=코드+새 비밀번호
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
  const [pw, setPw] = useState("");
  const [pw2, setPw2] = useState("");
  const [showPw, setShowPw] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [devCode, setDevCode] = useState(null); // 로컬 개발(메일 발송 꺼짐)에서만 응답에 실려 온다

  // ① 재설정 코드 요청 — 성공하면 항상 같은 안내 (계정 존재 여부 비노출)
  async function requestCode() {
    setError("");
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
      setError("이메일 형식이 올바르지 않아요. (예: you@example.com)");
      return;
    }
    setBusy(true);
    try {
      const r = await API.requestPasswordReset(email.trim().toLowerCase());
      if (r && r.dev_reset_code) setDevCode(r.dev_reset_code);
      setStep(2);
      toast("가입된 이메일이라면 재설정 코드를 보냈어요. 메일함(스팸함 포함)을 확인해 주세요");
    } catch (e) {
      setError(e.message || "요청에 실패했어요. 잠시 후 다시 시도해 주세요.");
    }
    setBusy(false);
  }

  // ② 코드 + 새 비밀번호 확인 — 성공하면 자동 로그인 후 원래 화면으로
  async function submitReset() {
    setError("");
    if (!/^\d{6}$/.test(code.trim())) { setError("메일로 받은 6자리 숫자 코드를 입력해 주세요."); return; }
    const pwErr = passwordRuleError(pw);
    if (pwErr) { setError(pwErr); return; }
    if (pw !== pw2) { setError("비밀번호가 일치하지 않아요."); return; }
    setBusy(true);
    try {
      await API.confirmPasswordReset(email.trim().toLowerCase(), code.trim(), pw);
      await auth.refresh();
      toast("비밀번호를 변경하고 로그인했어요");
      go(afterAuth);
    } catch (e) {
      setError(e.message || "변경에 실패했어요. 잠시 후 다시 시도해 주세요.");
    }
    setBusy(false);
  }

  return (
    <div className="page wrap" style={{ paddingTop: 64, paddingBottom: 80, maxWidth: 440 }}>
      <div className="card" style={{ padding: 32 }}>
        <h1 className="t-h2" style={{ margin: "0 0 6px" }}>비밀번호 재설정</h1>
        <p className="t-small t-sub" style={{ margin: "0 0 20px" }}>
          {step === 1 ? "가입한 이메일로 6자리 재설정 코드를 보내드려요." : "메일로 받은 코드와 새 비밀번호를 입력해 주세요."}
        </p>
        <div className="stack" style={{ gap: 14 }}>
          <div className="field"><label>이메일</label>
            <input className="input" type="email" placeholder="you@example.com" value={email} disabled={step === 2}
              onChange={(e) => setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && step === 1) requestCode(); }} /></div>
          {step === 2 && (
            <>
              <div className="field"><label>재설정 코드 (6자리)</label>
                <input className="input" type="text" inputMode="numeric" maxLength={6} placeholder="123456"
                  value={code} onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
                  style={{ letterSpacing: 4, fontWeight: 700 }} /></div>
              {devCode && (
                <p className="t-caption t-sub" style={{ margin: "-6px 0 0" }}>
                  개발용 코드: <strong className="t-mono" style={{ color: "var(--ink)" }}>{devCode}</strong> (메일 발송이 꺼진 로컬에서만 보여요)
                </p>
              )}
              <div className="field"><label>새 비밀번호</label>
                <div style={{ position: "relative" }}>
                  <input className="input" type={showPw ? "text" : "password"} placeholder="••••••••" value={pw}
                    onChange={(e) => setPw(e.target.value)} style={{ paddingRight: 42 }} />
                  <button type="button" onClick={() => setShowPw((v) => !v)} aria-label={showPw ? "비밀번호 숨기기" : "비밀번호 표시"}
                    style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", background: "none", border: 0, cursor: "pointer", color: "var(--sub)", display: "flex", padding: 0 }}>
                    <Icon name={showPw ? "eyeoff" : "eye"} size={18} />
                  </button>
                </div>
                <p className="t-caption t-sub" style={{ margin: "6px 0 0" }}>{PW_RULE_TEXT}</p>
              </div>
              <div className="field"><label>새 비밀번호 확인</label>
                <input className="input" type={showPw ? "text" : "password"} placeholder="••••••••" value={pw2}
                  onChange={(e) => setPw2(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submitReset(); }} /></div>
            </>
          )}
          {error && <p className="t-small" style={{ color: "var(--error)", margin: 0 }}>{error}</p>}
          {step === 1
            ? <Btn variant="primary" size="lg" block disabled={busy} onClick={requestCode}>{busy ? "요청 중…" : "재설정 코드 받기"}</Btn>
            : <Btn variant="primary" size="lg" block disabled={busy} onClick={submitReset}>{busy ? "변경 중…" : "비밀번호 변경"}</Btn>}
          {step === 2 && (
            <p style={{ textAlign: "center", margin: 0 }}>
              <button type="button" disabled={busy} onClick={requestCode}
                style={{ background: "none", border: 0, color: "var(--sub)", textDecoration: "underline", cursor: "pointer", fontSize: 13, fontFamily: "inherit" }}>
                코드 재발송
              </button>
            </p>
          )}
        </div>
        <p className="t-small t-sub" style={{ textAlign: "center", margin: "20px 0 0" }}>
          <button type="button" onClick={() => go("login", returnTo ? { returnTo } : undefined)}
            style={{ background: "none", border: 0, color: "var(--primary)", fontWeight: 600, cursor: "pointer", fontFamily: "inherit" }}>
            로그인으로 돌아가기
          </button>
        </p>
      </div>
    </div>
  );
}

/* ============ 마이페이지 ============ */

// (사진에서 직접 측정 기능 삭제 — prepMeasure/MeasureEditor 제거. 키·몸무게 기반 둘레 추정으로 대체)

// 반품 사유 — 백엔드 허용값(enum)과 글자까지 동일해야 한다. 사이즈 추천 학습 신호로 쓰인다.
const RETURN_REASONS = ["너무작아서", "너무커서", "색상다름", "스타일불만", "품질불량", "단순변심", "기타"];

/* '친구에게 물어보기' 시트 — 질문을 적고 카톡으로 피팅 결과를 보냅니다.
 *
 * 안내 동의를 최초 1회만 받는 이유: 링크를 받은 사람은 누구나 볼 수 있고 카톡 대화방에
 * 사진이 함께 보이므로, 사용자가 그 사실을 모른 채 보내면 안 됩니다. (약관 제4조의2)
 */
const _ASK_CONSENT_KEY = "denky_share_consent";

function AskFriendsSheet({ historyId, toast, onClose }) {
  const [question, setQuestion] = useState("");
  const [includePhoto, setIncludePhoto] = useState(true);
  const [agreed, setAgreed] = useState(() => localStorage.getItem(_ASK_CONSENT_KEY) === "1");
  const [share, setShare] = useState(null);   // 미리 만들어 둔 공유
  const [err, setErr] = useState("");

  // 실제로 보냈는지 — 안 보내고 닫으면 방금 만든 링크를 되돌리는 데 씁니다.
  const sentRef = useRef(false);
  const shareRef = useRef(null);
  useEffect(() => { shareRef.current = share; }, [share]);

  // ★시트를 열 때 링크를 미리 만들어 둡니다★
  //   버튼 클릭에서 서버 왕복을 기다린 뒤 카톡 창을 열면, 브라우저가 팝업 허용
  //   (클릭 직후 짧은 시간)을 거둬 창이 '조용히' 막힙니다 — 예외도 안 나서 원인을 알 수 없습니다.
  //   미리 만들어 두면 클릭이 곧바로 팝업을 열 수 있습니다.
  useEffect(() => {
    let alive = true;
    API.createFitShare(historyId, { includePhoto: true })
      .then((s) => { if (alive) setShare(s); })
      .catch((e) => { if (alive) setErr((e && e.message) || "공유 링크를 만들지 못했어요"); });
    return () => { alive = false; };
  }, [historyId]);

  /* ★안 보내고 닫으면 방금 만든 링크를 회수합니다★
     미리 만들기 때문에, 시트를 열었다 그냥 닫아도 공유가 살아남습니다. 그러면 피팅 기록이
     '공유 중'으로 보이고 "친구 의견 · 아직 없어요" 패널까지 떠서, 보내지도 않았는데 보낸 것처럼
     읽힙니다. 공개 버킷에 카드 사본도 남습니다.
     ★created 플래그만 믿습니다★ — 이미 있던 공유를 돌려받은 경우(false)에는 손대지 않습니다.
     예전에 진짜로 보낸 공유를 회수해 버리면 안 되기 때문입니다.

     ★회수를 '기다린 뒤에' 닫습니다★ — 처음엔 unmount 정리에만 맡겼는데, 부모가 닫자마자
     공유 목록을 다시 불러서 '아직 살아있는 공유'를 보고 패널을 띄웠습니다(실측 로그:
     POST share → GET shares → DELETE share 순서). 닫기 흐름에서 먼저 끝내야 합니다. */
  const doneRef = useRef(false);   // 회수를 이미 처리했는지 (중복 호출 방지)
  function reclaim() {
    const s = shareRef.current;
    if (!s || !s.created || sentRef.current || doneRef.current) return null;
    doneRef.current = true;
    return API.revokeFitShare(s.id).catch(() => {});
  }
  async function closeSheet() {
    await reclaim();      // 회수가 끝난 뒤에 닫아야 부모의 목록 갱신이 올바른 상태를 봅니다
    onClose();
  }
  // 부모가 갑자기 언마운트하는 경우(확대창 ✕ 등)의 백업 — 기다리지 못하므로 던지고 끝냅니다.
  useEffect(() => () => { reclaim(); }, []);

  // ★여기서 await 를 쓰지 않습니다★ (팝업 차단 방지 — kakaoShare.js 주석 참고)
  function send() {
    if (!agreed || !share) return;
    sentRef.current = true;   // 되돌리지 않습니다 (카톡 창이 떴으면 보낸 것으로 봅니다)
    localStorage.setItem(_ASK_CONSENT_KEY, "1");
    // 방금 적은 질문·사진 여부를 카드에 바로 반영합니다. (카드 내용은 서버가 아니라 이 값에서 옵니다)
    window.KakaoShare.send(share, { question, includePhoto }).then((how) => {
      if (how === "kakao") toast("카카오톡 공유창을 열었어요");
      else if (how === "copied") toast("링크를 복사했어요. 친구에게 붙여넣어 보내주세요");
      else toast("아래 링크를 복사해 보내주세요");
    });
    // 공유 페이지(친구가 보는 화면)에도 질문을 반영합니다. 창은 이미 떴으니 기다리지 않습니다.
    if (question.trim()) {
      API.createFitShare(historyId, { question, includePhoto }).catch(() => {});
    }
  }

  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", zIndex: 1100, display: "grid", placeItems: "center", padding: 16 }}
      onClick={(e) => { if (e.target === e.currentTarget) closeSheet(); }}>
      <div className="card" style={{ width: "100%", maxWidth: 420, padding: 20, borderRadius: 18 }}>
        <h3 className="t-h3" style={{ margin: "0 0 6px" }}>친구에게 물어보기</h3>
        <p className="t-small t-sub" style={{ margin: "0 0 16px" }}>카톡으로 보내면 친구가 어울리는지 골라줘요.</p>

        <label className="t-small" style={{ fontWeight: 600, display: "block", marginBottom: 6 }}>질문 (선택)</label>
        <input value={question} maxLength={100} placeholder="면접에 이거 어때?"
          onChange={(e) => setQuestion(e.target.value)}
          style={{ width: "100%", padding: "11px 12px", borderRadius: 10, border: "1.5px solid var(--line)", fontSize: 14 }} />

        <label className="row" style={{ gap: 8, marginTop: 14, cursor: "pointer", alignItems: "flex-start" }}>
          <input type="checkbox" checked={includePhoto} onChange={(e) => setIncludePhoto(e.target.checked)} style={{ marginTop: 3 }} />
          <span className="t-small">
            카톡 카드에 사진 보이기
            <span className="t-sub" style={{ display: "block", fontSize: 12, marginTop: 2 }}>
              끄면 대화방엔 사진이 안 보이고, 링크를 열어야 보여요.
            </span>
          </span>
        </label>

        <div style={{ background: "#FFF7ED", border: "1px solid #FED7AA", borderRadius: 10, padding: "10px 12px", margin: "14px 0 0" }}>
          <p style={{ fontSize: 12, color: "#9A3412", margin: 0, lineHeight: 1.55 }}>
            링크를 받은 사람은 <b>누구나</b> 볼 수 있어요. 아는 사람에게만 보내주세요.
            <br />7일 뒤 자동으로 만료되고, 마이페이지에서 언제든 <b>공유를 중단</b>할 수 있어요.
          </p>
        </div>

        {!agreed && (
          <label className="row" style={{ gap: 8, marginTop: 12, cursor: "pointer" }}>
            <input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
            <span className="t-small" style={{ fontWeight: 600 }}>위 내용을 확인했어요</span>
          </label>
        )}

        {/* 링크는 항상 보여줍니다 — 카톡 창이 팝업 차단으로 안 뜨는 경우에도
            사용자가 스스로 복사해 보낼 길이 남아야 합니다. */}
        <div style={{ marginTop: 14 }}>
          <label className="t-small" style={{ fontWeight: 600, display: "block", marginBottom: 6 }}>공유 링크</label>
          <input readOnly value={share ? share.share_url : (err || "링크를 만들고 있어요…")}
            onFocus={(e) => share && e.target.select()}
            style={{ width: "100%", padding: "10px 12px", borderRadius: 10, border: "1.5px solid var(--line)", fontSize: 12.5, background: "var(--bg-sub)", color: err ? "var(--error)" : "inherit" }} />
        </div>

        <div className="stack" style={{ gap: 8, marginTop: 18 }}>
          <Btn variant="primary" size="lg" block disabled={!agreed || !share} onClick={send}>
            {share ? "카톡으로 보내기" : (err ? "다시 시도해 주세요" : "링크 준비 중…")}
          </Btn>
          <Btn variant="outline" block disabled={!share} onClick={async () => {
            sentRef.current = true;   // 복사도 '보낸 것'으로 봅니다 — 링크를 회수하면 안 됩니다
            const ok = await window.KakaoShare.copyLink(share.share_url);
            toast(ok ? "링크를 복사했어요" : "복사에 실패했어요. 주소를 직접 선택해 복사해 주세요");
          }}>링크 복사</Btn>
          <p className="t-caption t-sub" style={{ margin: "2px 0 0", textAlign: "center" }}>
            카톡 창이 안 뜨면 팝업 차단을 확인하거나 위 링크를 복사해 보내주세요.
          </p>
          <Btn variant="ghost" block onClick={closeSheet}>닫기</Btn>
        </div>
      </div>
    </div>
  );
}

/* 공유 목록을 history_id → 살아있는 공유 로 접습니다.
   만료·중단된 공유는 배지·집계에 쓰지 않으므로 걸러냅니다. */
function _byHistory(rows) {
  const map = {};
  (rows || []).forEach((s) => { if (s.is_active) map[s.history_id] = s; });
  return map;
}

function MyPageScreen({ go, openProduct, tryProduct, auth, toast, onContinueFit, initialTab }) {
  const [tab, setTab] = useState(initialTab || "fitting");  // 진입 시 탭 지정 가능(예: 사이즈 패널의 '둘레 넣기' → profile)
  const [fits, setFits] = useState(null); // 피팅 기록 (null = 로딩중)
  // 사진 등록 전 신체정보(성별·키·몸무게) 강제 입력 모달 — 아바타 체형 정확도의 필수 재료
  const [bodyGate, setBodyGate] = useState(false);
  const [bgGender, setBgGender] = useState("");
  const [bgHeight, setBgHeight] = useState("");
  const [bgWeight, setBgWeight] = useState("");
  const [bgBusy, setBgBusy] = useState(false);
  const [bgError, setBgError] = useState("");
  const [zoomPhoto, setZoomPhoto] = useState(null); // 내 정보 사진 클릭 시 확대 보기
  const [fitZoom, setFitZoom] = useState(null); // 피팅 기록 확대 (겹쳐입기 포함 — 스마트앱과 동일)
  // '친구에게 물어보기' 공유 — history_id 로 찾을 수 있게 맵으로 보관합니다.
  // (푸시·알림 인프라가 없어서, 새 응답은 화면을 열 때 폴링으로 확인합니다)
  const [sharesByHistory, setSharesByHistory] = useState({});
  const [shareDetail, setShareDetail] = useState(null); // 확대창에서 본 공유 상세(집계+한줄평)
  const [zoomAskFor, setZoomAskFor] = useState(null);   // 피팅기록 확대창에서 물어보기 대상 id
  const [addrCands, setAddrCands] = useState([]); // 기본 배송지 주소 자동완성 후보
  const addrTimer = useRef(null); // 주소 자동완성 디바운스
  const [orders, setOrders] = useState(null); // 주문 내역 (null = 로딩중)
  const [orderMonths, setOrderMonths] = useState(null); // 주문 기간 필터 (null=전체, 3/6=최근 N개월)
  const [orderDetail, setOrderDetail] = useState(null); // 주문 상세 모달 (null=닫힘)
  const [returnTarget, setReturnTarget] = useState(null); // 반품 사유 선택 모달의 대상 주문 (null=닫힘)
  const [returnReason, setReturnReason] = useState("");   // 고른 반품 사유
  const [showVerifyBox, setShowVerifyBox] = useState(false); // 이메일 미인증 배너의 코드 입력 펼침
  // 내 정보 수정 폼 (실제 프로필 값으로 채움)
  const [pName, setPName] = useState("");
  const [pPhone, setPPhone] = useState("");
  const [pGender, setPGender] = useState("");
  const [pBirth, setPBirth] = useState("");      // 생년월일 (YYYY-MM-DD)
  const [pHeight, setPHeight] = useState("");
  const [pWeight, setPWeight] = useState("");
  const [pChest, setPChest] = useState("");      // 가슴둘레 (cm)
  const [pWaist, setPWaist] = useState("");      // 허리둘레 (cm)
  const [pHip, setPHip] = useState("");          // 엉덩이둘레 (cm)
  const [pShoulder, setPShoulder] = useState(""); // 어깨너비 (cm, 직선)
  const [pTorso, setPTorso] = useState("");       // 상체길이 어깨~허리 (cm)
  const [pLeg, setPLeg] = useState("");           // 다리길이 허리~발목 (cm)
  const [pArm, setPArm] = useState("");           // 팔길이 어깨~손목 (cm)
  const [pUsualTop, setPUsualTop] = useState("");    // 평소 상의 사이즈
  const [pUsualBottom, setPUsualBottom] = useState(""); // 평소 하의 사이즈
  const [checkingPhoto, setCheckingPhoto] = useState(false); // 업로드 사진 검사중
  const [pendingPhoto, setPendingPhoto] = useState(null);    // 검사 후 등록 대기 dataURL
  const [photoIssues, setPhotoIssues] = useState([]);        // 검사 결과 경고 목록
  const [showGuide, setShowGuide] = useState(false);         // 촬영 가이드 토글
  // 기본 배송지 (결제 화면 자동 채움용)
  const [pZip, setPZip] = useState("");
  const [pAddr, setPAddr] = useState("");
  const [pAddrDetail, setPAddrDetail] = useState("");
  const [savingProfile, setSavingProfile] = useState(false);
  const [pwCur, setPwCur] = useState("");        // 비밀번호 변경 — 현재
  const [pwNew, setPwNew] = useState("");        // 비밀번호 변경 — 새
  const [pwBusy, setPwBusy] = useState(false);
  const [coupons, setCoupons] = useState(null); // 피팅 쿠폰함 (null = 로딩중)
  const [referral, setReferral] = useState(null); // 친구초대 현황 (null = 로딩중/미지원)
  const [wishes, setWishes] = useState(null); // 찜한 상품 (null = 로딩중)
  const fileRef = useRef(null);
  const sideFileRef = useRef(null);  // 측면 사진용
  // 아바타 폴링 언마운트 가드 — 페이지를 떠나면 3분짜리 폴링 루프가 헛돌지 않게 멈춘다.
  const pollAliveRef = useRef(true);
  useEffect(() => () => { pollAliveRef.current = false; }, []);
  const tabs = [["fitting", "피팅 기록"], ["wish", "찜"], ["orders", "주문"], ["profile", "내 정보"]];
  const tabIcons = { fitting: "sparkle", wish: "heart", orders: "bag", profile: "user" };

  // PC 배치 — 넓은 화면은 좌측 사이드바(프로필+메뉴) + 우측 콘텐츠 2단, 좁으면 기존 1단(모바일형)
  const [isNarrow, setIsNarrow] = useState(() => typeof window !== "undefined" && window.innerWidth <= 920);
  useEffect(() => {
    const onR = () => setIsNarrow(window.innerWidth <= 920);
    window.addEventListener("resize", onR);
    return () => window.removeEventListener("resize", onR);
  }, []);

  // 피팅 기록·찜·주문 내역·쿠폰함을 실제 백엔드에서 불러옵니다. (로그인 상태에서만)
  useEffect(() => {
    if (!auth || !auth.loggedIn) return;
    let alive = true;
    API.tryonHistory().then((rows) => { if (alive) setFits(rows || []); }).catch(() => { if (alive) setFits([]); });
    API.fitShares().then((rows) => { if (alive) setSharesByHistory(_byHistory(rows)); }).catch(() => {});
    API.orders().then((rows) => { if (alive) setOrders(rows || []); }).catch(() => { if (alive) setOrders([]); });
    API.coupons().then((rows) => { if (alive) setCoupons(rows || []); }).catch(() => { if (alive) setCoupons([]); });
    // 초대 현황은 실패해도 조용히 넘어갑니다 — 서버가 구버전이면 없는 엔드포인트입니다.
    API.referralStatus().then((s) => { if (alive) setReferral(s || null); }).catch(() => {});
    API.wishlist().then((d) => { if (alive) setWishes((d && d.items) || []); }).catch(() => { if (alive) setWishes([]); });
    return () => { alive = false; };
  }, [auth && auth.loggedIn]);

  // 주소 입력 시(2자↑) 디바운스 후 카카오 후보를 띄운다 — 스마트앱의 주소 자동완성과 동일
  function onAddrChange(v) {
    setPAddr(v);
    if (addrTimer.current) clearTimeout(addrTimer.current);
    const q = v.trim();
    if (q.length < 2) { setAddrCands([]); return; }
    addrTimer.current = setTimeout(async () => {
      try { setAddrCands(await API.searchAddressCandidates(q)); }
      catch (e) { setAddrCands([]); }
    }, 300);
  }
  function pickAddr(c) {
    setPZip((c.zipcode || "").toString());
    const road = (c.road_address || "").toString();
    setPAddr(road || (c.jibun_address || "").toString());
    setAddrCands([]);
  }

  // 앞모습 사진 등록 시작 — 성별·키·몸무게가 비어 있으면 먼저 입력받은 뒤 사진 선택으로 진행한다.
  function startPhotoRegister() {
    if (!auth.gender || !auth.height || !auth.weight) {
      setBgGender(auth.gender || "");
      setBgHeight(auth.height != null ? String(auth.height) : "");
      setBgWeight(auth.weight != null ? String(auth.weight) : "");
      setBgError("");
      setBodyGate(true);
      return;
    }
    if (fileRef.current) fileRef.current.click();
  }

  // 신체정보 저장 → 성공하면 바로 사진 선택 창을 연다. (만 나이 검증 등 서버 오류는 그대로 안내)
  async function submitBodyGate() {
    setBgError("");
    const h = parseInt(bgHeight, 10), w = parseInt(bgWeight, 10);
    if (!bgGender) { setBgError("성별을 선택해 주세요."); return; }
    if (!(h >= 50 && h <= 250)) { setBgError("키를 정확히 입력해 주세요 (50~250cm)."); return; }
    if (!(w >= 20 && w <= 300)) { setBgError("몸무게를 정확히 입력해 주세요 (20~300kg)."); return; }
    setBgBusy(true);
    try {
      await auth.updateProfile({ gender: bgGender, height: h, weight: w });
      setBodyGate(false);
      if (fileRef.current) fileRef.current.click();
    } catch (e) {
      setBgError(e.message || "저장에 실패했어요.");
    }
    setBgBusy(false);
  }

  // 피팅 기록을 확대하면 그 기록의 공유 상세(한줄평)를 불러오고 '확인했음'으로 표시한다.
  // → 새 응답 배지가 꺼진다. (푸시가 없어 배지+폴링으로 알린다)
  useEffect(() => {
    if (!fitZoom) { setShareDetail(null); return; }
    const sh = sharesByHistory[fitZoom.id];
    if (!sh) return;
    let alive = true;
    API.fitShare(sh.id).then((d) => { if (alive) setShareDetail(d); }).catch(() => {});
    API.readFitShare(sh.id)
      .then(() => {
        if (!alive) return;
        setSharesByHistory((m) => (m[fitZoom.id] ? { ...m, [fitZoom.id]: { ...m[fitZoom.id], new_count: 0 } } : m));
      })
      .catch(() => {});
    return () => { alive = false; };
  }, [fitZoom && fitZoom.id]);

  // 새로고침 — 피팅 기록·찜·주문·쿠폰함을 다시 불러온다 (스마트앱의 새로고침 버튼과 동일)
  function reloadAll() {
    API.tryonHistory().then((rows) => setFits(rows || [])).catch(() => {});
    API.fitShares().then((rows) => setSharesByHistory(_byHistory(rows))).catch(() => {});
    API.orders(orderMonths).then((rows) => setOrders(rows || [])).catch(() => {});
    API.coupons().then((rows) => setCoupons(rows || [])).catch(() => {});
    API.referralStatus().then((s) => setReferral(s || null)).catch(() => {});
    API.wishlist().then((d) => setWishes((d && d.items) || [])).catch(() => {});
  }

  // 내 정보 폼을 현재 프로필 값으로 동기화합니다.
  useEffect(() => {
    if (!auth) return;
    setPName(auth.name || "");
    setPPhone(auth.phone || "");
    setPGender(auth.gender || "");
    setPBirth(auth.birthdate || "");
    setPHeight(auth.height != null ? String(auth.height) : "");
    setPWeight(auth.weight != null ? String(auth.weight) : "");
    setPChest(auth.chest != null ? String(auth.chest) : "");
    setPWaist(auth.waist != null ? String(auth.waist) : "");
    setPHip(auth.hip != null ? String(auth.hip) : "");
    setPShoulder(auth.shoulder != null ? String(auth.shoulder) : "");
    setPTorso(auth.torsoLength != null ? String(auth.torsoLength) : "");
    setPLeg(auth.legLength != null ? String(auth.legLength) : "");
    setPArm(auth.armLength != null ? String(auth.armLength) : "");
    setPUsualTop(auth.usualTopSize != null ? String(auth.usualTopSize) : "");
    setPUsualBottom(auth.usualBottomSize != null ? String(auth.usualBottomSize) : "");
    setPZip(auth.zipcode || "");
    setPAddr(auth.address || "");
    setPAddrDetail(auth.addressDetail || "");
  }, [auth && auth.name, auth && auth.phone, auth && auth.gender, auth && auth.height, auth && auth.weight, auth && auth.chest, auth && auth.waist, auth && auth.hip, auth && auth.shoulder, auth && auth.torsoLength, auth && auth.legLength, auth && auth.armLength, auth && auth.address, auth && auth.loggedIn]);

  // 내 정보 저장 → PATCH /auth/me
  async function saveProfile() {
    setSavingProfile(true);
    try {
      // 신체값을 백엔드 허용 범위로 보정해서 보낸다. (범위 밖이면 422로 '전체' 저장이 실패해
      // 전화·생년월일까지 안 저장되는 문제 방지. 측정값은 추정치라 범위로 캡해도 무방.)
      const clamp = (s, lo, hi) => { const v = parseInt(s, 10); return (s !== "" && !isNaN(v)) ? Math.max(lo, Math.min(hi, v)) : null; };
      await auth.updateProfile({
        name: pName.trim() || null,
        phone: pPhone.trim() || null,
        gender: pGender || null,
        birthdate: pBirth || null,
        height: clamp(pHeight, 50, 250),
        weight: clamp(pWeight, 20, 300),
        chest: clamp(pChest, 40, 200),
        waist: clamp(pWaist, 40, 200),
        hip: clamp(pHip, 40, 200),
        shoulder: clamp(pShoulder, 20, 80),
        torso_length: clamp(pTorso, 20, 80),
        leg_length: clamp(pLeg, 50, 140),
        arm_length: clamp(pArm, 40, 80),
        usual_top_size: pUsualTop.trim() || null,
        usual_bottom_size: pUsualBottom.trim() || null,
        // 기본 배송지 — 스마트앱과 동일하게 폼에서 함께 저장 (결제 화면 자동 채움)
        zipcode: pZip.trim() || null,
        address: pAddr.trim() || null,
        address_detail: pAddrDetail.trim() || null,
      });
      // (요청) 저장 성공 안내 토스트는 표시하지 않는다. 실패 메시지는 유지.
    } catch (e) {
      if (toast) toast(e.message || "저장에 실패했어요");
    }
    setSavingProfile(false);
  }

  // 비밀번호 변경 → 성공 시 새 토큰 저장. (입력칸 비우고 안내)
  async function changePassword() {
    // 새 비밀번호 조합 규칙(3종 8자+ / 2종 10자+)을 먼저 검사 — 서버 422에만 기대지 않는다
    const pwErr = passwordRuleError(pwNew);
    if (pwErr) { if (toast) toast(pwErr); return; }
    setPwBusy(true);
    try {
      await API.changePassword(pwCur, pwNew);
      setPwCur(""); setPwNew("");
      if (toast) toast("비밀번호를 변경했어요");
    } catch (e) {
      if (toast) toast(e.message || "비밀번호 변경에 실패했어요");
    }
    setPwBusy(false);
  }

  // 구매확정 — 배송완료 주문을 확정하면 피팅 쿠폰이 발급되고 무료 누적이 리셋됩니다.
  async function doConfirmOrder(order) {
    if (!window.confirm("이 주문을 구매확정할까요?\n확정하면 피팅 쿠폰이 발급되며, 되돌릴 수 없어요.")) return;
    try {
      await auth.confirmOrder(order.id);
      if (toast) toast("구매확정 완료! 피팅 쿠폰이 쿠폰함에 발급됐어요");
      API.orders(orderMonths).then((rows) => setOrders(rows || []));
      API.coupons().then((rows) => setCoupons(rows || []));
    } catch (e) {
      if (toast) toast(e.message || "구매확정에 실패했어요");
    }
  }

  // 주문 취소(청약철회) — 배송 전(결제완료) 주문만. 재고·무료배송 쿠폰이 되돌아옵니다.
  async function doCancelOrder(order) {
    if (!window.confirm("이 주문을 취소할까요?\n결제 금액은 결제수단으로 환불돼요.")) return;
    try {
      await API.cancelOrder(order.id);
      if (toast) toast("주문이 취소됐어요");
      API.orders(orderMonths).then((rows) => setOrders(rows || []));
      auth.refresh(); // 무료배송 쿠폰 반환 반영
    } catch (e) {
      if (toast) toast(e.message || "주문 취소에 실패했어요");
    }
  }

  // 반품 신청 — 사유 선택 모달을 연다. (사유는 사이즈 추천 학습의 핵심 신호라 꼭 받는다)
  function doReturnOrder(order) {
    setReturnReason("");
    setReturnTarget(order);
  }

  // 사유를 고른 뒤 실제 접수 — 배송완료 주문만. 판매자 확인 후 수거·환불이 진행됩니다.
  async function submitReturn() {
    if (!returnTarget || !returnReason) return;
    try {
      await API.returnOrder(returnTarget.id, returnReason);
      setReturnTarget(null);
      if (toast) toast("반품 신청이 접수됐어요. 판매자 확인 후 수거·환불이 진행돼요");
      API.orders(orderMonths).then((rows) => setOrders(rows || []));
    } catch (e) {
      if (toast) toast(e.message || "반품 신청에 실패했어요");
    }
  }

  // 리뷰 쓰기 — 상품 상세(리뷰 섹션)로 이동. (구매확정 주문에서 진입)
  async function doWriteReview(item) {
    try {
      const p = await API.product(item.product_id);
      openProduct(API.normalize(p));
      if (toast) toast("상세페이지의 리뷰 섹션에서 리뷰를 남겨주세요");
    } catch (e) {
      if (toast) toast("상품을 찾을 수 없어요 (판매 종료)");
    }
  }

  // 다시 구매 — 상품 상세로 이동. (옵션은 상세에서 다시 선택)
  async function doRebuy(item) {
    try {
      const p = await API.product(item.product_id);
      setOrderDetail(null);
      openProduct(API.normalize(p));
    } catch (e) {
      if (toast) toast("상품을 찾을 수 없어요 (판매 종료)");
    }
  }

  // 쿠폰 → 피팅 횟수 전환
  async function doConvertCoupon(coupon) {
    try {
      const r = await auth.convertCoupon(coupon.id);
      if (toast) toast(`쿠폰 ${coupon.fits}회를 전환했어요 (바로 쓸 수 있는 피팅 ${r.balance}회)`);
      API.coupons().then((rows) => setCoupons(rows || []));
    } catch (e) {
      if (toast) toast(e.message || "쿠폰 전환에 실패했어요");
    }
  }

  // 사진 등록 후 아바타 자동 생성(백그라운드)이 끝날 때까지 몇 초 간격으로 내 정보를 새로고침한다.
  async function pollAvatarStatus() {
    for (let i = 0; i < 40; i++) {  // 최대 약 3분 (5초 × 40)
      await new Promise((r) => setTimeout(r, 5000));
      if (!pollAliveRef.current) return;  // 페이지를 떠났으면 중단(불필요한 API 왕복·언마운트 setState 방지)
      let me = null;
      try { me = await API.me(); } catch (e) {}
      if (auth.refresh) await auth.refresh();  // auth 상태 갱신(미리보기·버튼 반영)
      const st = me && me.avatar_status;
      if (st === "ready") { if (toast) toast("피팅 아바타가 완성됐어요! 이제 아바타로 피팅해요"); return; }
      if (st === "failed") { if (toast) toast("아바타 생성에 실패했어요. 사진을 다시 등록해 주세요"); return; }
    }
  }

  // 새로 생성한 아바타(후보)를 바꾸기/취소
  async function decideAvatar(accept) {
    try {
      await auth.confirmAvatar(accept);
      if (toast) toast(accept ? "새 아바타로 바꿨어요" : "기존 아바타를 유지할게요");
    } catch (e) {
      if (toast) toast(e.message || "처리에 실패했어요");
    }
  }

  // 회원 탈퇴 — 되돌릴 수 없으므로 경고로 이중 확인 후 진행. 완료되면 로그아웃 상태로 홈으로.
  async function doDeleteAccount() {
    if (!window.confirm("탈퇴하면 피팅 기록·주문 내역·쿠폰이 모두 삭제되며 되돌릴 수 없어요.\n정말 탈퇴할까요?")) return;
    try {
      await auth.deleteAccount();
      if (toast) toast("탈퇴가 완료됐어요. 그동안 이용해 주셔서 감사합니다");
    } catch (e) {
      if (toast) toast(e.message || "탈퇴 처리에 실패했어요. 잠시 후 다시 시도해 주세요");
    }
  }

  // 사진을 바꾸면 아바타가 재생성됨 — 무료 한도 초과면 쿠폰 차감을 미리 안내하고 동의를 받는다.
  // 계속하면 true, 취소하면 false.
  function confirmAvatarCharge() {
    if ((auth.avatarGenCount || 0) < (auth.avatarFreeLimit || 3)) return true;
    return window.confirm(
      `무료 아바타 생성 ${auth.avatarFreeLimit || 3}회를 모두 사용했어요.\n`
      + `사진을 바꿔 아바타를 다시 만들면 피팅 쿠폰 잔고에서 ${auth.avatarExtraFitCost || 1}회가 차감됩니다. 계속할까요?`);
  }

  // 앞모습 사진 업로드 — 올리기 전에 '피팅 적합성'을 검사해, 문제 있으면 확인을 받습니다.
  async function onPickPhoto(e) {
    const f = e.target.files && e.target.files[0];
    e.target.value = "";  // 같은 파일을 다시 고를 수 있도록 비움
    if (!f) return;
    if (!confirmAvatarCharge()) return;  // 한도 초과 시 요금 안내 → 취소하면 중단
    const dataUrl = await readFileAsDataUrl(f);
    setCheckingPhoto(true);
    const q = await checkPhotoQuality(dataUrl);
    setCheckingPhoto(false);
    if (q.ok) {
      // 적합하면 바로 등록 → 아바타 자동 생성 대기
      try { await auth.uploadPhoto(dataUrl); if (toast) toast("사진 등록 완료! 피팅 아바타를 만들고 있어요 (1~2분)"); pollAvatarStatus(); }
      catch (err) { if (toast) toast(err.message || "사진 등록에 실패했어요"); } // 402(쿠폰 부족) 등 서버 안내 노출
    } else {
      // 문제가 있으면 미리보기 + 경고를 보여주고 사용자가 선택
      setPendingPhoto(dataUrl);
      setPhotoIssues(q.issues);
    }
  }

  // 경고에도 불구하고 '이대로 등록' 했을 때 (요금 확인은 onPickPhoto 에서 이미 받음)
  async function confirmPendingPhoto() {
    if (!pendingPhoto) return;
    try { await auth.uploadPhoto(pendingPhoto); if (toast) toast("사진 등록 완료! 피팅 아바타를 만들고 있어요 (1~2분)"); pollAvatarStatus(); }
    catch (err) { if (toast) toast(err.message || "사진 등록에 실패했어요"); }
    setPendingPhoto(null); setPhotoIssues([]);
  }

  // 측면(옆모습) 사진 등록 — 정면과 별개로 저장합니다. (정면 포즈 검사는 안 함)
  async function onPickSidePhoto(e) {
    const f = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!f) return;
    try {
      const dataUrl = await readFileAsDataUrl(f);
      await auth.uploadSidePhoto(dataUrl);
      if (toast) toast("측면 사진을 등록했어요");
    } catch (err) { if (toast) toast("사진 등록에 실패했어요"); }
  }

  // 로그인 안 했으면 안내 화면을 보여줍니다.
  if (!auth || !auth.loggedIn) {
    return (
      <div className="page wrap" style={{ paddingTop: 80, paddingBottom: 80, maxWidth: 540 }}>
        <div className="card" style={{ padding: 40, textAlign: "center" }}>
          <div style={{ width: 80, height: 80, borderRadius: "50%", background: "var(--accent-soft)", color: "var(--primary)", display: "grid", placeItems: "center", margin: "0 auto 22px" }}>
            <Icon name="user" size={34} />
          </div>
          <h2 className="t-h2" style={{ margin: "0 0 10px" }}>로그인이 필요해요</h2>
          <p className="t-body t-sub" style={{ margin: "0 0 28px" }}>로그인하면 내 정보와 피팅 기록을 볼 수 있어요.</p>
          <Btn variant="primary" size="lg" block onClick={() => go("login")}>로그인하기</Btn>
        </div>
      </div>
    );
  }

  // 실제 로그인 사용자 정보. (이름이 없으면 이메일 앞부분을 표시)
  const displayName = auth.name || (auth.email ? auth.email.split("@")[0] : "회원");

  return (
    // PC 마이페이지 — 넓은 화면: 좌측 사이드바(아바타 카드+메뉴) 고정 + 우측 콘텐츠. 좁으면 1단.
    <div className="page wrap" style={{ paddingTop: 32, paddingBottom: 64, ...(isNarrow ? { maxWidth: 560 } : {}) }}>
      {/* 헤더 — 넓은 화면은 본문과 같은 그리드(320px+콘텐츠)로 맞춰, 탭이 아바타 카드 라인을 지나
          오른쪽 콘텐츠 시작 위치에서 나오게 한다 ('마이'와 붙지 않음) */}
      <div style={isNarrow
        ? { display: "flex", alignItems: "center", gap: 12, margin: "0 0 16px" }
        : { display: "grid", gridTemplateColumns: "320px minmax(0, 1fr)", alignItems: "center", gap: 24, margin: "0 0 16px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          <BackBtn />
          <h1 className="t-h1" style={{ margin: 0 }}>마이</h1>
        </div>
        {/* 넓은 화면 — 메뉴(피팅 기록/찜/주문/내 정보)는 콘텐츠 칼럼 위 가로 탭 */}
        {!isNarrow && (
          <div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
            {tabs.map(([k, l]) => (
              <button key={k} onClick={() => setTab(k)}
                style={{
                  display: "flex", alignItems: "center", gap: 7, height: 38, padding: "0 16px",
                  border: tab === k ? "none" : "1px solid var(--border)", borderRadius: 999, cursor: "pointer",
                  fontWeight: 700, fontSize: 13.5,
                  background: tab === k ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "var(--surface)",
                  color: tab === k ? "#fff" : "var(--sub)",
                  boxShadow: tab === k ? "0 4px 12px rgba(30,120,239,.35)" : "none",
                }}>
                <Icon name={tabIcons[k]} size={15} />{l}
              </button>
            ))}
          </div>
        )}
      </div>
      <input ref={fileRef} type="file" accept="image/*" hidden onChange={onPickPhoto} />
      <input ref={sideFileRef} type="file" accept="image/*" hidden onChange={onPickSidePhoto} />
      <div style={{ display: "grid", gridTemplateColumns: isNarrow ? "1fr" : "320px minmax(0, 1fr)", gap: 24, alignItems: "start" }}>
      {/* ── 왼쪽 사이드바: 아바타/프로필 카드 + 세로 메뉴 (스크롤해도 따라오는 sticky) */}
      <aside style={isNarrow ? undefined : { position: "sticky", top: 84 }}>
      {/* 프로필 + 가상피팅 아바타 통합 카드 — 스마트앱 마이의 통합 카드와 동일 구성.
          피팅은 '아바타 전용': 사진 등록 → 아바타 자동 생성 → 그 아바타로만 피팅 (사진/아바타 선택 없음) */}
      {(
        <div className="card" style={{ padding: 22, marginBottom: 24 }}>
          <h3 className="t-h3" style={{ margin: "0 0 6px", fontSize: 14 }}>가상피팅 아바타</h3>
          <p className="t-small t-sub" style={{ margin: "0 0 14px" }}>
            앞모습 사진을 등록하면 <strong style={{ color: "var(--ink)" }}>내 얼굴을 닮은 피팅 아바타</strong>를 자동으로 만들어, 그 아바타로 피팅해요. (1~2분 소요)
          </p>
          <div className="row" style={{ gap: 16, alignItems: "flex-start", flexWrap: "wrap" }}>
            {/* 왼쪽 미리보기 = 피팅 모델(아바타). 아직 없으면 방금 올린 사진(생성 중). 생성 중이면 스피너 오버레이.
                아바타는 2:3(전신) 비율 → 카드도 2:3(140×210) + contain 으로 전신이 잘리지 않게 */}
            {(() => {
              const previewUrl = auth.avatarUrl || auth.photoUrl;
              const processing = auth.avatarStatus === "processing";
              return (
                <div onClick={() => previewUrl && setZoomPhoto(previewUrl)}
                  style={{ position: "relative", width: 140, height: 210, borderRadius: 12, overflow: "hidden", flex: "none", border: "1px solid var(--border)", background: "var(--surface-2)", cursor: previewUrl ? "zoom-in" : "default" }}>
                  {previewUrl
                    ? <img src={previewUrl} alt="가상피팅 모델" style={{ width: "100%", height: "100%", objectFit: "contain" }} />
                    : <PhImg tone="#EDE9FE" ink="#A78BFA" type="상의" label="모델" />}
                  {processing && (
                    <div style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,.45)", display: "grid", placeItems: "center" }}>
                      <div className="spinner" style={{ width: 24, height: 24, borderColor: "rgba(255,255,255,.4)", borderTopColor: "#fff" }} />
                    </div>
                  )}
                </div>
              );
            })()}
            <div className="stack" style={{ gap: 8, flex: 1, minWidth: 160 }}>
              {/* 프로필 요약 — 이름·남은 피팅 (아바타 미리보기 옆으로 이동) */}
              {(() => {
                // 무료 피팅은 '잔고(지갑)'입니다 — 매주 충전되고 안 쓰면 이월됩니다.
                // 그래서 "이번 주 몇/몇"이 아니라 '지금 가진 횟수'를 보여줍니다.
                const freeLeft = auth.freeFitBalance || 0;
                const refillOver = (auth.lifetimeFreeGranted || 0) >= (auth.lifetimeFreeLimit || 60);
                return (
                  <>
                    <p className="t-h3" style={{ margin: 0 }}>{displayName} 님</p>
                    <p className="t-small" style={{ margin: 0, fontWeight: 700 }}>
                      <Icon name="sparkle" size={13} style={{ color: "var(--primary)", verticalAlign: "-2px" }} /> 남은 피팅 {freeLeft}회
                      {(auth.fitCouponBalance || 0) > 0 && <span style={{ color: "var(--primary)" }}> + 쿠폰 {auth.fitCouponBalance}회</span>}
                    </p>
                    <p className="t-caption t-sub" style={{ margin: 0 }}>
                      {refillOver
                        ? "무료 충전이 끝났어요. 남은 횟수는 계속 쓸 수 있고, 구매확정하면 충전이 다시 시작돼요."
                        : `매주 월요일 무료 ${auth.weeklyTryonLimit || 10}회 충전 · 안 쓰면 최대 ${auth.freeFitBalanceCap || 40}회까지 쌓여요`}
                    </p>
                  </>
                );
              })()}
              {/* 생성 상태 — 진행/실패/없음만 안내 (준비 완료 문구는 표시하지 않음) */}
              {auth.avatarStatus === "processing"
                ? <p className="t-small" style={{ margin: 0, color: "var(--primary)", fontWeight: 700 }}>아바타 만드는 중… (1~2분)</p>
                : auth.avatarStatus === "failed"
                  ? <p className="t-small" style={{ margin: 0, color: "#DC2626" }}>아바타 생성에 실패했어요. 사진을 다시 등록해 주세요.</p>
                  : !auth.avatarUrl
                    ? <p className="t-small t-sub" style={{ margin: 0 }}>아직 아바타가 없어요. 사진을 등록하면 자동으로 만들어져요.</p>
                    : null}
              {/* 내 사진 등록 — 등록하면 자동으로 아바타 생성. 성별·키·몸무게가 없으면 먼저 입력받는다 */}
              <Btn variant="soft" block icon="camera" disabled={checkingPhoto || auth.avatarStatus === "processing"}
                onClick={startPhotoRegister}>
                {auth.avatarStatus === "processing" ? "생성 중…" : (checkingPhoto ? "사진 확인 중…" : (auth.hasPhoto ? "앞모습 사진 변경" : "앞모습 사진 등록"))}
              </Btn>
              {/* 촬영 가이드 — 사진 버튼 바로 아래로 이동 */}
              <p style={{ textAlign: "center", margin: 0 }}>
                <a href="#" onClick={(e) => { e.preventDefault(); setShowGuide((v) => !v); }}
                  style={{ color: "var(--primary)", fontSize: 13 }}>촬영 가이드 보기</a>
              </p>
            </div>
          </div>
          {/* 새로 생성한 아바타(후보) — 기존과 나란히 보여주고 바꾸기/취소를 고르게 한다 */}
          {auth.avatarCandidateUrl && (
            <div style={{ marginTop: 16, padding: 16, border: "1.5px solid var(--primary)", borderRadius: 12 }}>
              <p className="t-small" style={{ margin: "0 0 12px", fontWeight: 700 }}>새로 생성한 아바타로 바꿀까요?</p>
              <div className="row" style={{ gap: 16, alignItems: "flex-start", flexWrap: "wrap" }}>
                <div style={{ textAlign: "center" }}>
                  <div style={{ width: 96, height: 144, borderRadius: 12, overflow: "hidden", border: "1px solid var(--border)", background: "var(--surface-2)" }}>
                    <img src={auth.avatarUrl} alt="기존 아바타" style={{ width: "100%", height: "100%", objectFit: "contain" }} />
                  </div>
                  <p className="t-caption t-sub" style={{ margin: "6px 0 0" }}>기존</p>
                </div>
                <div style={{ textAlign: "center" }}>
                  <div style={{ width: 96, height: 144, borderRadius: 12, overflow: "hidden", border: "1.5px solid var(--primary)", background: "var(--surface-2)" }}>
                    <img src={auth.avatarCandidateUrl} alt="새 아바타" style={{ width: "100%", height: "100%", objectFit: "contain" }} />
                  </div>
                  <p className="t-caption" style={{ margin: "6px 0 0", color: "var(--primary)", fontWeight: 700 }}>새 아바타</p>
                </div>
              </div>
              <div className="row" style={{ gap: 8, marginTop: 14 }}>
                <Btn variant="primary" size="sm" onClick={() => decideAvatar(true)}>바꾸기</Btn>
                <Btn variant="outline" size="sm" onClick={() => decideAvatar(false)}>취소 (기존 유지)</Btn>
              </div>
            </div>
          )}
          {/* 촬영 가이드/사진 경고 — 가이드 링크는 사진 버튼 아래로 이동 */}
          <div className="stack" style={{ gap: 8, marginTop: 12 }}>
            {/* 촬영 가이드 — 스마트앱과 동일한 팝업(다이얼로그) 형태로 통일.
                내용: Google Doppl 등 가상피팅 서비스 공식 권장사항 + 전신촬영 전문가 팁 기반 */}
            {showGuide && (
              <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "grid", placeItems: "center", zIndex: 1000, padding: 20 }}
                onClick={() => setShowGuide(false)}>
                <div className="card" style={{ background: "var(--surface)", borderRadius: 14, padding: 22, width: "min(380px, 100%)", maxHeight: "80vh", overflowY: "auto" }}
                  onClick={(e) => e.stopPropagation()}>
                  <h2 className="t-h3" style={{ margin: "0 0 14px" }}>잘 나오는 피팅 사진</h2>
                  <strong style={{ fontSize: 12.5, display: "block", margin: "0 0 6px", color: "var(--primary)" }}>이렇게 찍어요</strong>
                  <ul style={{ margin: "0 0 10px", paddingLeft: 16, fontSize: 13, lineHeight: 1.7, color: "var(--sub)" }}>
                    <li><b style={{ color: "var(--ink)" }}>밝고 고른 조명</b>에서 정면 전신, <b style={{ color: "var(--ink)" }}>발끝까지</b> 나오게</li>
                    <li><b style={{ color: "var(--ink)" }}>카메라 = 허리~가슴 높이, 수평</b> (위/아래에서 찍으면 비율 왜곡)</li>
                    <li>2m 이상 떨어져 세로로, <b style={{ color: "var(--ink)" }}>기본(1x) 렌즈</b> (초광각은 몸이 휘어 보여요)</li>
                    <li>팔은 몸 옆에, 다리는 살짝 벌려 — 팔다리가 가려지면 인식 실패</li>
                  </ul>
                  <strong style={{ fontSize: 12.5, display: "block", margin: "0 0 6px", color: "var(--primary)" }}>옷·배경</strong>
                  <ul style={{ margin: "0 0 10px", paddingLeft: 16, fontSize: 13, lineHeight: 1.7, color: "var(--sub)" }}>
                    <li><b style={{ color: "var(--ink)" }}>몸에 붙는 단색 옷</b> (헐렁하거나 무늬가 복잡하면 인식이 어려워요)</li>
                    <li>깔끔한 단색 배경, 주변 물건은 치우기</li>
                  </ul>
                  <strong style={{ fontSize: 12.5, display: "block", margin: "0 0 6px", color: "var(--primary)" }}>피해 주세요</strong>
                  <ul style={{ margin: 0, paddingLeft: 16, fontSize: 13, lineHeight: 1.7, color: "var(--sub)" }}>
                    <li><b style={{ color: "var(--ink)" }}>거울 셀카·기울인 각도</b> (체형이 왜곡돼요)</li>
                    <li>다른 사람·반려동물이 같이 나온 사진</li>
                    <li>가방·모자·긴 머리카락 등 몸을 가리는 것</li>
                  </ul>
                  <div style={{ textAlign: "right", marginTop: 12 }}>
                    <Btn variant="primary" size="sm" onClick={() => setShowGuide(false)}>확인</Btn>
                  </div>
                </div>
              </div>
            )}

            {/* 업로드 사진 경고 — 검사에서 걸리면 미리보기 + 안내 후 사용자가 선택 */}
            {pendingPhoto && (
              <div className="card" style={{ padding: 16, display: "flex", gap: 14, alignItems: "flex-start", border: "1px solid #FCD34D", background: "#FFFBEB" }}>
                <img src={pendingPhoto} alt="" style={{ width: 70, height: 94, objectFit: "cover", borderRadius: 8, flex: "none", border: "1px solid var(--border)" }} />
                <div style={{ flex: 1 }}>
                  <strong style={{ fontSize: 13, display: "block", marginBottom: 6 }}>이 사진, 피팅 품질이 떨어질 수 있어요</strong>
                  <ul style={{ margin: "0 0 10px", paddingLeft: 16, fontSize: 12, lineHeight: 1.6 }}>
                    {photoIssues.map((it, i) => (
                      <li key={i} style={{ color: it.level === "bad" ? "#DC2626" : "#B45309" }}>{it.msg}</li>
                    ))}
                  </ul>
                  <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
                    <Btn variant="primary" size="sm" icon="camera" onClick={() => fileRef.current && fileRef.current.click()}>다시 선택</Btn>
                    <Btn variant="outline" size="sm" onClick={confirmPendingPhoto}>이대로 등록</Btn>
                    <Btn variant="ghost" size="sm" onClick={() => { setPendingPhoto(null); setPhotoIssues([]); }}>취소</Btn>
                  </div>
                </div>
              </div>
            )}
          </div>

          <p className="t-caption t-sub" style={{ margin: "12px 0 0" }}>
            무료 생성 {Math.max(0, (auth.avatarFreeLimit || 3) - (auth.avatarGenCount || 0))}회 남음
            {(auth.avatarExtraFitCost || 0) > 0 ? ` (소진 후 사진 변경 시 쿠폰 ${auth.avatarExtraFitCost}회 차감)` : ""}
          </p>
        </div>
      )}

      {/* (넓은 화면 메뉴는 상단 '마이' 타이틀 옆 가로 탭으로 이동) */}
      </aside>

      {/* ── 오른쪽 콘텐츠 영역 */}
      <main style={{ minWidth: 0 }}>
      {/* 이메일 미인증 배너 — 인증해야 가상피팅 사용 가능(403). 코드 입력은 가입 직후와 같은 공용 컴포넌트 */}
      {auth.isEmailVerified === false && (
        <div className="card" style={{ padding: 16, marginBottom: 16, border: "1px solid #FCD34D", background: "#FFFBEB" }}>
          <div className="row" style={{ gap: 10, alignItems: "center", flexWrap: "wrap" }}>
            <Icon name="shield" size={18} style={{ color: "#B45309" }} />
            <p className="t-small" style={{ margin: 0, flex: 1, fontWeight: 700, minWidth: 200 }}>이메일 인증을 완료하면 가상피팅을 쓸 수 있어요</p>
            <Btn variant="outline" size="sm" onClick={() => setShowVerifyBox((v) => !v)}>{showVerifyBox ? "닫기" : "인증코드 입력"}</Btn>
          </div>
          {showVerifyBox && (
            <div style={{ marginTop: 12 }}>
              <p className="t-caption t-sub" style={{ margin: "0 0 8px" }}>
                <strong style={{ color: "var(--ink)" }}>{auth.email}</strong> 메일로 받은 6자리 코드를 입력해 주세요. 코드가 없거나 만료됐으면 [재발송]을 눌러 주세요.
              </p>
              <EmailVerifyBox email={auth.email} toast={toast}
                onVerified={async () => {
                  await auth.refresh();          // is_email_verified 갱신 → 배너가 사라진다
                  setShowVerifyBox(false);
                  if (toast) toast("이메일 인증 완료! 이제 가상피팅을 쓸 수 있어요");
                }} />
            </div>
          )}
        </div>
      )}
      {/* 탭 — 좁은 화면 전용 알약형 탭바 (활성 = 파란 그라데이션) */}
      {isNarrow && (
      <div className="row" style={{ gap: 4, padding: 4, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 14, marginBottom: 20 }}>
        {tabs.map(([k, l]) => (
          <button key={k} onClick={() => setTab(k)}
            style={{
              flex: 1, height: 34, border: 0, borderRadius: 10, cursor: "pointer",
              fontWeight: 700, fontSize: 12,
              background: tab === k ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "transparent",
              color: tab === k ? "#fff" : "var(--sub)",
              boxShadow: tab === k ? "0 4px 12px rgba(30,120,239,.35)" : "none",
            }}>
            {l}
          </button>
        ))}
      </div>
      )}

      {/* 찜 탭 — 스마트앱과 계정을 공유하는 위시리스트 (해제·상품 이동 가능) */}
      {tab === "wish" && (
        wishes === null ? (
          <div style={{ display: "grid", placeItems: "center", padding: "60px 0" }}>
            <div className="spinner spinner-dark" style={{ width: 34, height: 34 }}></div>
          </div>
        ) : wishes.length === 0 ? (
          <div className="empty">
            <div className="ill"><Icon name="heart" 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="primary" onClick={() => go("catalog")}>옷 보러 가기</Btn>
          </div>
        ) : (
          <div style={{ display: "grid", gridTemplateColumns: isNarrow ? "1fr 1fr" : "repeat(auto-fill, minmax(200px, 1fr))", gap: 14 }}>
            {/* PC는 폭에 맞춰 3~4열 자동, 좁은 화면은 2열 — 하트로 바로 해제 */}
            {wishes.map((p) => {
              const np = API.normalize(p);
              return <ProductCard key={np.id} p={np} onOpen={openProduct} onTry={tryProduct} />;
            })}
          </div>
        )
      )}

      {tab === "fitting" && (
        fits === null ? (
          <div style={{ display: "grid", placeItems: "center", padding: "60px 0" }}>
            <div className="spinner spinner-dark" style={{ width: 34, height: 34 }}></div>
          </div>
        ) : fits.length === 0 ? (
          <div className="empty">
            <div className="ill"><Icon name="sparkle" 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="primary" onClick={() => go("catalog")}>옷 보러 가기</Btn>
          </div>
        ) : (
          <>
            {/* 스마트앱과 동일 — 새로고침 버튼 + 3열 이미지 그리드, 탭하면 확대(겹쳐입기 포함) */}
            <div className="row" style={{ justifyContent: "flex-end", marginBottom: 8 }}>
              <Btn variant="ghost" size="sm" onClick={reloadAll}>새로고침</Btn>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: isNarrow ? "1fr 1fr 1fr" : "repeat(auto-fill, minmax(170px, 1fr))", gap: 10 }}>
              {fits.map((rec) => {
                // 입은 옷 이름 — 살아있는 상품 → 피팅 당시 스냅샷 순. (상품이 삭제돼도 표시)
                const names = (rec.steps || []).map((s) => (s.product && s.product.name) || s.product_name).filter(Boolean);
                // 2벌 동시 피팅이면 두 상품명을 모두, 3벌 이상이면 "첫 옷 외 N벌"
                const worn = names.length === 0 ? "" : names.length === 1 ? names[0]
                  : names.length === 2 ? `${names[0]} · ${names[1]}` : `${names[0]} 외 ${names.length - 1}벌`;
                return (
                <div key={rec.id} onClick={() => setFitZoom(rec)}
                  style={{ position: "relative", aspectRatio: "3/4", borderRadius: 12, overflow: "hidden", cursor: "zoom-in", background: "var(--surface-2)" }}>
                  <img src={rec.result_url} alt="피팅 결과" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
                  {/* 하단 옷 이름 라벨 */}
                  {worn && (
                    <span style={{ position: "absolute", left: 0, right: 0, bottom: 0, padding: "16px 34px 6px 8px",
                      background: "linear-gradient(180deg, transparent, rgba(0,0,0,.55))", color: "#fff",
                      fontSize: 10.5, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{worn}</span>
                  )}
                  {/* 우하단 겹쳐입기 배지 (스마트앱과 동일) */}
                  <span style={{ position: "absolute", right: 6, bottom: 6, width: 24, height: 24, borderRadius: 999, background: "linear-gradient(135deg,#4AA6FF,#1E78EF)", display: "grid", placeItems: "center", color: "#fff" }}>
                    <Icon name="layers" size={13} />
                  </span>
                  {/* 친구 응답 배지 — 공유 중이고 아직 확인하지 않은 응답이 있을 때만 */}
                  {(() => {
                    const sh = sharesByHistory[rec.id];
                    if (!sh) return null;
                    return (
                      <span title={sh.new_count > 0 ? `새 응답 ${sh.new_count}개` : "친구에게 물어보는 중"}
                        style={{ position: "absolute", left: 6, top: 6, minWidth: 22, height: 22, padding: "0 6px", borderRadius: 999,
                          background: sh.new_count > 0 ? "#EF4444" : "rgba(0,0,0,.5)", color: "#fff",
                          display: "grid", placeItems: "center", fontSize: 11, fontWeight: 800 }}>
                        {sh.new_count > 0 ? sh.new_count : "💬"}
                      </span>
                    );
                  })()}
                  {/* 삭제 버튼 — 확인 후 기록을 지우고 목록에서 제거 */}
                  <button type="button" title="기록 삭제" aria-label="피팅 기록 삭제"
                    style={{ position: "absolute", top: 6, right: 6, width: 24, height: 24, borderRadius: "50%", background: "rgba(0,0,0,.45)", color: "#fff", border: "none", cursor: "pointer", display: "grid", placeItems: "center", fontSize: 12, lineHeight: 1 }}
                    onClick={async (e) => {
                      e.stopPropagation();
                      if (!window.confirm("이 피팅 기록을 삭제할까요?\n삭제한 기록은 되돌릴 수 없어요.")) return;
                      try {
                        await API.deleteTryon(rec.id);
                        setFits((arr) => (arr || []).filter((r) => r.id !== rec.id));
                        toast && toast("피팅 기록을 삭제했어요");
                      } catch (err) {
                        toast && toast(err.message || "삭제에 실패했어요");
                      }
                    }}>✕</button>
                </div>
                );
              })}
            </div>
          </>
        )
      )}

      {/* 주문 상세 — 배송지·요청사항·운송장·금액 내역(상품/배송비 분리)·항목별 다시 구매 */}
      {orderDetail && (() => {
        const o = orderDetail;
        const subtotal = (o.items || []).reduce((sum, it) => sum + Math.round(Number(it.price)) * it.qty, 0);
        const shipping = Math.round(Number(o.total_amount)) - subtotal;
        const kv = (k, v, bold) => (
          <div className="row" style={{ gap: 8, alignItems: "flex-start", marginBottom: 6 }}>
            <span className="t-caption t-sub" style={{ width: 76, flex: "none" }}>{k}</span>
            <span className="t-small" style={{ flex: 1, fontWeight: bold ? 800 : 400, color: "var(--ink)" }}>{v}</span>
          </div>
        );
        return (
          <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "grid", placeItems: "center", zIndex: 1000, padding: 20 }}
            onClick={() => setOrderDetail(null)}>
            <div className="card" style={{ background: "var(--surface)", borderRadius: 14, padding: 22, width: "min(420px, 100%)", maxHeight: "82vh", overflowY: "auto" }}
              onClick={(e) => e.stopPropagation()}>
              <h2 className="t-h3" style={{ margin: "0 0 14px" }}>주문 상세 · {o.id}</h2>
              {kv("상태", o.status)}
              {kv("주문일", new Date(o.created_at).toLocaleDateString("ko-KR"))}
              <hr style={{ border: 0, borderTop: "1px solid var(--border)", margin: "10px 0" }} />
              {kv("받는 분", o.recipient_name || "-")}
              {kv("연락처", o.phone || "-")}
              {kv("배송지", o.address || "-")}
              {o.request_note && kv("요청사항", o.request_note)}
              {o.tracking_number && kv("운송장", ((o.courier || "") + " " + o.tracking_number).trim())}
              <hr style={{ border: 0, borderTop: "1px solid var(--border)", margin: "10px 0" }} />
              {(o.items || []).map((it) => (
                <div key={it.id} className="row" style={{ gap: 8, alignItems: "center", marginBottom: 6 }}>
                  <span className="t-small" style={{ flex: 1, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {it.product_name}{it.option_text ? " (" + it.option_text + ")" : ""} × {it.qty}
                  </span>
                  <span className="t-small" style={{ fontWeight: 600, flex: "none" }}>{DENKY.won(Math.round(Number(it.price)) * it.qty)}원</span>
                  {it.product_id && (
                    <button type="button" onClick={() => doRebuy(it)}
                      style={{ background: "none", border: 0, color: "var(--primary)", cursor: "pointer", fontSize: 12, fontWeight: 700, flex: "none", fontFamily: "inherit" }}>다시 구매</button>
                  )}
                </div>
              ))}
              <hr style={{ border: 0, borderTop: "1px solid var(--border)", margin: "10px 0" }} />
              {kv("상품금액", DENKY.won(subtotal) + "원")}
              {kv("배송비", shipping > 0 ? DENKY.won(shipping) + "원" : "무료")}
              {kv("총 결제금액", DENKY.won(Math.round(Number(o.total_amount))) + "원", true)}
              <div style={{ textAlign: "right", marginTop: 10 }}>
                <Btn variant="outline" size="sm" onClick={() => setOrderDetail(null)}>닫기</Btn>
              </div>
            </div>
          </div>
        );
      })()}

      {/* 반품 사유 선택 — 사유를 골라야 접수된다 (사이즈 추천 학습 신호로 백엔드에 저장) */}
      {returnTarget && (
        <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "grid", placeItems: "center", zIndex: 1000, padding: 20 }}
          onClick={() => setReturnTarget(null)}>
          <div className="card" style={{ background: "var(--surface)", borderRadius: 14, padding: 22, width: "min(380px, 100%)" }}
            onClick={(e) => e.stopPropagation()}>
            <h2 className="t-h3" style={{ margin: "0 0 6px" }}>반품 사유를 골라주세요</h2>
            <p className="t-small t-sub" style={{ margin: "0 0 14px" }}>주문번호 {returnTarget.id} · 신청하면 판매자 확인 후 수거·환불이 진행돼요.</p>
            <div className="row" style={{ gap: 8, flexWrap: "wrap", marginBottom: 18 }}>
              {RETURN_REASONS.map((r) => (
                <button key={r} type="button" onClick={() => setReturnReason(r)}
                  style={{
                    padding: "8px 14px", borderRadius: 999, cursor: "pointer", fontWeight: 700, fontSize: 13, fontFamily: "inherit",
                    background: returnReason === r ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "var(--surface-2)",
                    color: returnReason === r ? "#fff" : "var(--sub)",
                    border: returnReason === r ? "none" : "1px solid var(--border)",
                  }}>{r}</button>
              ))}
            </div>
            <div className="row" style={{ gap: 10 }}>
              <button type="button" onClick={() => setReturnTarget(null)}
                style={{ flex: 1, height: 46, borderRadius: 10, border: "1px solid var(--border)", background: "var(--surface)", color: "var(--sub)", cursor: "pointer", fontFamily: "inherit", fontSize: 14 }}>취소</button>
              <div style={{ flex: 1 }}>
                <Btn variant="primary" size="lg" block disabled={!returnReason} onClick={submitReturn}>반품 신청</Btn>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* 신체정보 강제 입력 — 사진 등록 전에 성별·키·몸무게가 없으면 여기서 먼저 받는다 */}
      {bodyGate && (
        <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "grid", placeItems: "center", zIndex: 1000, padding: 20 }}>
          <div className="card" style={{ background: "var(--surface)", borderRadius: 14, padding: 22, width: "min(380px, 100%)" }}>
            <h2 className="t-h3" style={{ margin: "0 0 6px" }}>신체 정보 입력</h2>
            <p className="t-small t-sub" style={{ margin: "0 0 14px" }}>내 체형을 닮은 아바타를 만들려면 성별·키·몸무게가 필요해요.</p>
            <div className="row" style={{ gap: 8, marginBottom: 10 }}>
              {[["male", "남성"], ["female", "여성"]].map(([v, l]) => (
                <button key={v} type="button" onClick={() => setBgGender(v)}
                  style={{ flex: 1, height: 40, borderRadius: 10, cursor: "pointer", fontWeight: 700, fontSize: 13, fontFamily: "inherit",
                    background: bgGender === v ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "var(--surface-2)",
                    color: bgGender === v ? "#fff" : "var(--sub)", border: bgGender === v ? "none" : "1px solid var(--border)" }}>
                  {l}
                </button>
              ))}
            </div>
            <div className="grid-2" style={{ gap: 10 }}>
              <div className="field"><label>키(cm)</label>
                <input className="input" type="number" placeholder="예: 172" value={bgHeight} onChange={(e) => setBgHeight(e.target.value)} /></div>
              <div className="field"><label>몸무게(kg)</label>
                <input className="input" type="number" placeholder="예: 65" value={bgWeight} onChange={(e) => setBgWeight(e.target.value)} /></div>
            </div>
            {bgError && <p className="t-small" style={{ color: "var(--error)", margin: "0 0 12px" }}>{bgError}</p>}
            <div className="row" style={{ gap: 10 }}>
              <button type="button" disabled={bgBusy} onClick={() => setBodyGate(false)}
                style={{ flex: 1, height: 46, borderRadius: 10, border: "1px solid var(--border)", background: "var(--surface)", color: "var(--sub)", cursor: "pointer", fontFamily: "inherit", fontSize: 14 }}>취소</button>
              <div style={{ flex: 1 }}>
                <Btn variant="primary" size="lg" block disabled={bgBusy} onClick={submitBodyGate}>{bgBusy ? "저장 중…" : "저장하고 계속"}</Btn>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* 피팅 기록 확대 — 스마트앱과 동일하게 확대창 안에서 '이 위에 겹쳐입기' 가능
          ★display:grid + placeItems:center 를 쓰지 않는다★ — 내용이 화면보다 길어지면
          가운데 정렬된 항목의 위쪽이 스크롤 영역 밖으로 밀려 닿을 수 없게 되고, 아래 패널·버튼이
          사진과 겹쳐 보인다. flex + margin:auto 는 짧을 때 가운데, 길 때 위에서부터 스크롤된다. */}
      {fitZoom && (
        <div onClick={() => { setFitZoom(null); setShareDetail(null); }}
          style={{ position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,.9)", display: "flex", justifyContent: "center", padding: "24px 24px 40px", overflowY: "auto", WebkitOverflowScrolling: "touch" }}>
          <div onClick={(e) => e.stopPropagation()} style={{ width: "min(400px, 88vw)", margin: "auto 0", flexShrink: 0 }}>
            {/* 사진 높이를 화면의 절반쯤으로 묶습니다 — 안 묶으면 세로가 짧은 창에서
                아래 내용(친구 의견·버튼)이 화면 밖으로 나가 잘립니다. */}
            <img src={fitZoom.result_url} alt="피팅 결과 확대"
              style={{ width: "100%", aspectRatio: "3/4", maxHeight: "58vh", objectFit: "contain", background: "#000", borderRadius: 12, display: "block" }} />

            {/* 아직 공유 안 한 기록이면 여기서 바로 물어볼 수 있다.
                (지난 피팅도 나중에 물어볼 수 있어야 한다 — 피팅 직후에만 되면 기회를 놓친다) */}
            {!sharesByHistory[fitZoom.id] && (
              <Btn block style={{ marginTop: 14, background: "#FEE500", color: "#191600", border: "none", fontWeight: 700 }}
                onClick={() => setZoomAskFor(fitZoom.id)}>
                💬 친구에게 물어보기
              </Btn>
            )}

            {/* 친구 응답 — 공유 중인 기록이면 찬반 집계와 한줄평을 보여주고, 여기서 중단할 수 있다. */}
            {(() => {
              const sh = sharesByHistory[fitZoom.id];
              if (!sh) return null;
              const d = (shareDetail && shareDetail.id === sh.id) ? shareDetail : sh;
              const t = d.tally || { fits: 0, not_fits: 0, total: 0 };
              return (
                /* 배경을 반투명 흰색(.08)으로 두면 뒤가 밝은 사진일 때 흰 글자가 안 보입니다.
                   무엇에 겹쳐도 읽히도록 어두운 불투명 배경 + 테두리로 바꿨습니다. */
                <div style={{ marginTop: 14, background: "rgba(20,20,20,.92)", border: "1px solid rgba(255,255,255,.14)", borderRadius: 12, padding: "14px 14px 10px" }}>
                  <p style={{ color: "#fff", fontSize: 13.5, fontWeight: 800, margin: "0 0 8px" }}>
                    친구 의견 {t.total > 0 ? `· 어울려요 ${t.fits} · 글쎄요 ${t.not_fits}` : "· 아직 없어요"}
                  </p>
                  {(d.comments || []).map((c, i) => (
                    <p key={i} style={{ color: "rgba(255,255,255,.85)", fontSize: 12.5, lineHeight: 1.55, margin: "0 0 6px" }}>
                      <b>{c.verdict}</b>{c.comment ? " · " + c.comment : ""}
                    </p>
                  ))}
                  <p style={{ color: "rgba(255,255,255,.5)", fontSize: 11.5, margin: "8px 0 0" }}>
                    {sh.view_count > 0 ? `${sh.view_count}번 열렸어요 · ` : ""}
                    {(() => {
                      const left = Math.ceil((new Date(sh.expires_at) - Date.now()) / 86400000);
                      return left > 0 ? `${left}일 뒤 만료` : "곧 만료";
                    })()}
                  </p>
                  <button type="button"
                    onClick={async () => {
                      if (!window.confirm("공유를 중단할까요?\n링크를 받은 친구도 더는 볼 수 없어요. (이미 보낸 카톡의 미리보기 사진도 사라져요)")) return;
                      try {
                        await API.revokeFitShare(sh.id);
                        setSharesByHistory((m) => { const n = { ...m }; delete n[fitZoom.id]; return n; });
                        setShareDetail(null);
                        toast && toast("공유를 중단했어요");
                      } catch (err) { toast && toast(err.message || "중단에 실패했어요"); }
                    }}
                    style={{ background: "none", border: "none", color: "#FCA5A5", fontSize: 12.5, fontWeight: 700, cursor: "pointer", padding: "8px 0 2px", fontFamily: "inherit" }}>
                    공유 중단하기
                  </button>
                </div>
              );
            })()}

            <Btn variant="primary" size="lg" block icon="layers" style={{ marginTop: 14 }}
              onClick={() => { const rec = fitZoom; setFitZoom(null); setShareDetail(null); onContinueFit && onContinueFit(rec); }}>이 위에 겹쳐입기</Btn>
            {/* 피팅했던 상품 사러가기 — 살아있는 상품만 (삭제된 상품은 이동 불가) */}
            {(() => {
              // 백엔드 상품(JSON) → 화면 형식으로 정규화 (찜 탭과 동일 패턴)
              const ps = (fitZoom.steps || []).map((s) => s.product).filter(Boolean).map((p) => API.normalize(p));
              return ps.map((p) => (
                <Btn key={p.id} variant="soft" size="lg" block icon="bag" style={{ marginTop: 8 }}
                  onClick={() => { setFitZoom(null); setShareDetail(null); openProduct(p); }}>
                  {ps.length === 1 ? "이 옷 사러가기" : `${shortName(p.name)} 사러가기`}
                </Btn>
              ));
            })()}
          </div>
          {/* 카톡으로 친구에게 물어보기 — 공유를 만드는 유일한 진입점 */}
          {zoomAskFor && (
            <div onClick={(e) => e.stopPropagation()}>
              <AskFriendsSheet historyId={zoomAskFor} toast={toast}
                onClose={() => {
                  setZoomAskFor(null);
                  // 새로 만든 공유를 배지·집계에 반영
                  API.fitShares().then((rows) => setSharesByHistory(_byHistory(rows))).catch(() => {});
                }} />
            </div>
          )}
          <button onClick={(e) => { e.stopPropagation(); setFitZoom(null); setShareDetail(null); }} aria-label="닫기"
            style={{ position: "fixed", top: 20, right: 20, width: 44, height: 44, borderRadius: 999, border: "none", background: "rgba(255,255,255,.15)", color: "#fff", fontSize: 22, cursor: "pointer" }}>✕</button>
        </div>
      )}

      {tab === "orders" && (
        <div className="row" style={{ gap: 6, marginBottom: 12 }}>
          {[[null, "전체"], [3, "3개월"], [6, "6개월"]].map(([m, l]) => (
            <button key={l} type="button"
              onClick={() => { setOrderMonths(m); setOrders(null); API.orders(m).then((rows) => setOrders(rows || [])).catch(() => setOrders([])); }}
              style={{
                padding: "6px 12px", borderRadius: 999, cursor: "pointer", fontWeight: 700, fontSize: 12, fontFamily: "inherit",
                background: orderMonths === m ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "var(--surface-2)",
                color: orderMonths === m ? "#fff" : "var(--sub)",
                border: orderMonths === m ? "none" : "1px solid var(--border)",
              }}>{l}</button>
          ))}
        </div>
      )}
      {tab === "orders" && (
        orders === null ? (
          <div style={{ display: "grid", placeItems: "center", padding: "60px 0" }}>
            <div className="spinner spinner-dark" style={{ width: 34, height: 34 }}></div>
          </div>
        ) : orders.length === 0 ? (
          <div className="empty">
            <div className="ill"><Icon name="bag" 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={() => go("catalog")}>쇼핑하러 가기</Btn>
          </div>
        ) : (
          <div className="stack" style={{ gap: 12 }}>
            {/* 스마트앱과 동일 — 주문번호+상태칩 / 날짜 / 상품 줄들 / 결제금액 / 구매확정 */}
            {orders.map((o) => (
              <div key={o.id} className="card" style={{ padding: 15 }}>
                <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
                  <strong style={{ fontSize: 13 }}>주문번호 {o.id}
                    <button type="button" onClick={() => setOrderDetail(o)}
                      style={{ background: "none", border: 0, color: "var(--primary)", cursor: "pointer", fontSize: 12, marginLeft: 8, textDecoration: "underline", fontFamily: "inherit" }}>상세보기</button>
                  </strong>
                  <Chip soft>{o.status}</Chip>
                </div>
                <p className="t-caption t-sub" style={{ margin: "0 0 10px" }}>{new Date(o.created_at).toLocaleDateString("ko-KR")}</p>
                {o.items.map((it) => (
                  <div key={it.id} className="row" style={{ gap: 10, alignItems: "flex-start", marginBottom: 8 }}>
                    <div style={{ width: 44, height: 54, borderRadius: 8, overflow: "hidden", flex: "none", background: "var(--surface-2)" }}>
                      {it.image_url && <img src={API.imageUrl(it.image_url)} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />}
                    </div>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <p style={{ margin: 0, fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{it.product_name}</p>
                      {/* 구매 당시 선택 옵션 ("블랙 / 100") — 있을 때만 */}
                      {it.option_text && <p className="t-caption t-sub" style={{ margin: "2px 0 0" }}>{it.option_text}</p>}
                    </div>
                    <div style={{ textAlign: "right", flex: "none" }}>
                      <p className="t-small" style={{ margin: 0, fontWeight: 600 }}>{DENKY.won(Math.round(Number(it.price)))}원</p>
                      <p className="t-caption t-sub" style={{ margin: "2px 0 0" }}>{it.qty}개</p>
                    </div>
                  </div>
                ))}
                <hr style={{ border: 0, borderTop: "1px solid var(--border)", margin: "10px 0" }} />
                <div className="row" style={{ justifyContent: "space-between", alignItems: "center" }}>
                  <span className="t-small t-sub">결제금액</span>
                  <strong style={{ fontSize: 14 }}>{DENKY.won(Math.round(Number(o.total_amount)))}원</strong>
                </div>
                {/* 배송 조회 — 판매자가 송장을 입력하면 표시. 클릭 시 네이버 택배조회로 이동 */}
                {o.tracking_number && (
                  <a href={"https://search.naver.com/search.naver?query=" + encodeURIComponent((o.courier || "택배") + " " + o.tracking_number)}
                    target="_blank" rel="noreferrer"
                    className="row" style={{ gap: 6, alignItems: "center", marginTop: 8, padding: "8px 10px", background: "var(--accent-soft)", borderRadius: 10, textDecoration: "none" }}>
                    <span style={{ fontSize: 13 }}>🚚</span>
                    <span className="t-small" style={{ flex: 1, fontWeight: 600, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{(o.courier || "택배")} {o.tracking_number}</span>
                    <span className="t-caption" style={{ color: "var(--primary)", fontWeight: 700, flex: "none" }}>배송조회</span>
                  </a>
                )}
                {/* 상태별 액션 — 결제완료: 취소(청약철회) / 배송완료: 구매확정·반품 신청 / 구매확정: 리뷰 쓰기 */}
                {o.status === "결제완료" && (
                  <Btn variant="outline" size="sm" block style={{ marginTop: 10 }} onClick={() => doCancelOrder(o)}>주문 취소</Btn>
                )}
                {o.status === "배송완료" && (
                  <>
                    <Btn variant="primary" size="sm" block style={{ marginTop: 10 }} onClick={() => doConfirmOrder(o)}>구매확정</Btn>
                    <Btn variant="outline" size="sm" block style={{ marginTop: 8 }} onClick={() => doReturnOrder(o)}>반품 신청</Btn>
                  </>
                )}
                {o.status === "구매확정" && o.items.filter((it) => it.product_id).map((it) => (
                  <Btn key={"rv" + it.id} variant="soft" size="sm" block style={{ marginTop: 8 }} onClick={() => doWriteReview(it)}>
                    {o.items.length === 1 ? "리뷰 쓰기" : `리뷰 쓰기 · ${it.product_name}`}
                  </Btn>
                ))}
              </div>
            ))}
          </div>
        )
      )}

      {tab === "profile" && (
        // PC에선 2단: 왼쪽 = 내 정보 입력(폼) / 오른쪽 = 혜택·배송지·활동·계정. 좁으면 위아래 1단.
        <div style={{ display: "grid", gridTemplateColumns: isNarrow ? "1fr" : "minmax(0, 1fr) 380px", gap: 24, alignItems: "start" }}>
          <div className="stack" style={{ gap: 16 }}>
            {/* ── 내 정보: 이메일 · 완성도 · 기본/신체정보 · 사진 (스마트앱과 동일 순서) */}
            <p className="t-small" style={{ margin: "8px 0 -6px", fontWeight: 800 }}>👤 내 정보</p>
            {/* 이메일 (변경 불가) — 스마트앱과 같은 박스형 */}
            <div className="card" style={{ padding: "13px 16px", display: "flex", gap: 8, alignItems: "center" }}>
              <span className="t-small t-sub" style={{ width: 70, flex: "none" }}>이메일</span>
              <span style={{ fontSize: 13.5 }}>{auth.email || "-"}</span>
            </div>
            {(() => {
              const items = [
                ["정면 사진", auth.hasPhoto],
                ["키", auth.height != null],
                ["몸무게", auth.weight != null],
                ["평소 상의", !!auth.usualTopSize],
                ["평소 하의", !!auth.usualBottomSize],
                ["가슴둘레", auth.chest != null],
                ["허리둘레", auth.waist != null],
                ["엉덩이둘레", auth.hip != null],
                ["어깨너비", auth.shoulder != null],
                // 신체 길이(상체/다리/팔)는 기장·수선 참고용(선택)이라 완성도에서 제외.
                // 측면(옆모습) 사진은 등록 기능이 제거돼 완성도에서 뺀다 (채울 수 없어 100%가 안 되던 문제 수정)
              ];
              const filled = items.filter(([, v]) => v).length;
              const pct = Math.round((filled / items.length) * 100);
              const complete = filled === items.length;
              // ★클릭하면 첫 번째 빈 '치수' 칸으로 스크롤+포커스한다.★ (입력창을 찾아 헤매지 않게)
              // 키·몸무게·둘레가 다 차 있고 사진만 남았으면 사진 등록 위치를 안내.
              const goToFirstMissing = () => {
                const miss = [
                  [pHeight, "prof-height"], [pWeight, "prof-weight"],
                  [pUsualTop, "prof-usualtop"], [pUsualBottom, "prof-usualbottom"],
                  [pChest, "prof-chest"], [pWaist, "prof-waist"], [pHip, "prof-hip"], [pShoulder, "prof-shoulder"],
                ].find(([v]) => !String(v ?? "").trim());
                if (miss) {
                  const el = document.getElementById(miss[1]);
                  if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); setTimeout(() => el.focus({ preventScroll: true }), 300); }
                } else if (toast) {
                  toast("정면 사진만 남았어요! 위 '가상피팅 모델' 카드에서 등록하면 100%예요");
                }
              };
              return (
                <div className="card" onClick={complete ? undefined : goToFirstMissing}
                  style={{ padding: 14, background: "var(--accent-soft)", cursor: complete ? "default" : "pointer" }}>
                  <div className="row" style={{ justifyContent: "space-between", marginBottom: 6 }}>
                    <strong style={{ fontSize: 13 }}>내 체형 프로필 {pct}%</strong>
                    <span className="t-caption" style={{ color: complete ? "var(--sub)" : "var(--primary)", fontWeight: 600 }}>
                      {filled}/{items.length}{complete ? "" : " ›"}
                    </span>
                  </div>
                  <div style={{ height: 8, borderRadius: 999, background: "#fff", overflow: "hidden" }}>
                    <div style={{ width: pct + "%", height: "100%", background: "var(--primary)", transition: "width .3s" }} />
                  </div>
                  <p className="t-caption" style={{ margin: "8px 0 0", lineHeight: 1.5, color: complete ? "var(--sub)" : "var(--primary-dark)", fontWeight: complete ? 400 : 600 }}>
                    {complete ? "완성! 사이즈 추천이 가장 정확해요 🎉" : "클릭하면 남은 항목을 바로 입력할 수 있어요"}
                  </p>
                </div>
              );
            })()}
            <div className="field"><label>이름</label>
              <input className="input" placeholder="이름" value={pName} onChange={(e) => setPName(e.target.value)} /></div>
            {/* 성별 — 스마트앱과 동일한 칩 3개 (활성 = 파란 그라데이션) */}
            <div className="field"><label>성별</label>
              <div className="row" style={{ gap: 8 }}>
                {[["male", "남성"], ["female", "여성"]].map(([v, l]) => (
                  <button key={v} type="button" onClick={() => setPGender(v)}
                    style={{
                      flex: 1, height: 40, borderRadius: 10, cursor: "pointer", fontWeight: 700, fontSize: 13,
                      background: pGender === v ? "linear-gradient(135deg,#4AA6FF,#1E78EF)" : "var(--surface-2)",
                      color: pGender === v ? "#fff" : "var(--sub)",
                      border: pGender === v ? "none" : "1px solid var(--border)",
                    }}>{l}</button>
                ))}
              </div>
            </div>
            <div className="field"><label>생년월일</label>
              <input className="input" type="date" max={new Date().toISOString().slice(0, 10)} value={pBirth} onChange={(e) => setPBirth(e.target.value)} /></div>
            <div className="grid-2" style={{ gap: 10 }}>
              <div className="field"><label>키(cm)</label>
                <input id="prof-height" className="input" type="number" min="50" max="250" placeholder="예: 172" value={pHeight} onChange={(e) => setPHeight(e.target.value)} /></div>
              <div className="field"><label>몸무게(kg)</label>
                <input id="prof-weight" className="input" type="number" min="20" max="300" placeholder="예: 65" value={pWeight} onChange={(e) => setPWeight(e.target.value)} /></div>
            </div>

            {/* ★평소 사이즈 — 줄자 없이 사이즈를 추천하는 가장 강한 신호라 맨 위로 강조 (파란 그라데이션) */}
            <div style={{ background: "linear-gradient(135deg,#4AA6FF,#1E78EF)", borderRadius: 12, padding: 14, color: "#fff" }}>
              <p style={{ margin: "0 0 10px", fontSize: 13.5, fontWeight: 800 }}>평소 입는 사이즈</p>
              <div className="grid-2" style={{ gap: 10 }}>
                <div className="field"><label style={{ color: "#fff" }}>평소 상의</label>
                  <input id="prof-usualtop" className="input" type="text" maxLength={10} placeholder={auth.estUsualTop ? `예상 ${auth.estUsualTop}` : "예: M, 95"} value={pUsualTop} onChange={(e) => setPUsualTop(e.target.value)} /></div>
                <div className="field"><label style={{ color: "#fff" }}>평소 하의</label>
                  <input id="prof-usualbottom" className="input" type="text" maxLength={10} placeholder={auth.estUsualBottom ? `예상 ${auth.estUsualBottom}` : "예: 30, M"} value={pUsualBottom} onChange={(e) => setPUsualBottom(e.target.value)} /></div>
              </div>
            </div>

            {/* 신체 둘레 — 실측을 넣으면 더 정밀. 안 넣으면 키·몸무게로 계산한 '예상값'을 힌트로 보여줘 부담 제거 */}
            <div style={{ background: "var(--accent-soft)", borderRadius: 12, padding: 12 }}>
              <p className="row" style={{ gap: 6, alignItems: "center", margin: "0 0 4px", fontSize: 13, fontWeight: 700, color: "var(--primary)" }}>
                <Icon name="ruler" size={15} />신체 둘레
              </p>
              <div className="grid-2" style={{ gap: 10 }}>
                <div className="field"><label>가슴둘레(cm)</label>
                  <input id="prof-chest" className="input" type="number" min="40" max="200" placeholder={auth.estChest ? `예상 ${auth.estChest}` : (_isF(auth) ? "예: 85" : _isM(auth) ? "예: 95" : "예: 85~95")} value={pChest} onChange={(e) => setPChest(e.target.value)} /></div>
                <div className="field"><label>허리둘레(cm)</label>
                  <input id="prof-waist" className="input" type="number" min="40" max="200" placeholder={auth.estWaist ? `예상 ${auth.estWaist}` : (_isF(auth) ? "예: 66" : _isM(auth) ? "예: 80" : "예: 66~80")} value={pWaist} onChange={(e) => setPWaist(e.target.value)} /></div>
              </div>
              <div className="grid-2" style={{ gap: 10, marginTop: 10 }}>
                <div className="field"><label>엉덩이둘레(cm)</label>
                  <input id="prof-hip" className="input" type="number" min="40" max="200" placeholder={auth.estHip ? `예상 ${auth.estHip}` : (_isF(auth) ? "예: 92" : _isM(auth) ? "예: 95" : "예: 92~95")} value={pHip} onChange={(e) => setPHip(e.target.value)} /></div>
                <div className="field"><label>어깨너비(cm)</label>
                  <input id="prof-shoulder" className="input" type="number" min="20" max="80" placeholder={auth.estShoulder ? `예상 ${auth.estShoulder} · 직선` : (_isF(auth) ? "직선 예: 38" : _isM(auth) ? "직선 예: 44" : "직선 예: 38~44")} value={pShoulder} onChange={(e) => setPShoulder(e.target.value)} /></div>
              </div>
            </div>

            {/* 신체 길이 — 기장·수선용 (선택). 사진 직접측정은 제거됨. */}
            <div style={{ background: "var(--accent-soft)", borderRadius: 12, padding: 12 }}>
              <p className="row" style={{ gap: 6, alignItems: "center", margin: "0 0 4px", fontSize: 13, fontWeight: 700, color: "var(--primary)" }}>
                <Icon name="ruler" size={15} />신체 길이
              </p>
              <div className="grid-2" style={{ gap: 10 }}>
                {/* 길이도 둘레처럼 키 기반 '예상값'을 힌트로 — 실측 부담 제거 */}
                <div className="field"><label>상체길이(cm)</label>
                  <input className="input" type="number" min="20" max="80" placeholder={auth.estTorsoLength ? `예상 ${auth.estTorsoLength} · 어깨~허리` : "어깨~허리"} value={pTorso} onChange={(e) => setPTorso(e.target.value)} /></div>
                <div className="field"><label>다리길이(cm)</label>
                  <input className="input" type="number" min="50" max="140" placeholder={auth.estLegLength ? `예상 ${auth.estLegLength} · 허리~발목` : "허리~발목"} value={pLeg} onChange={(e) => setPLeg(e.target.value)} /></div>
              </div>
              <div className="field" style={{ marginTop: 10 }}><label>팔길이(cm)</label>
                <input className="input" type="number" min="40" max="80" placeholder={auth.estArmLength ? `예상 ${auth.estArmLength} · 어깨~손목 (선택)` : "어깨~손목 (선택)"} value={pArm} onChange={(e) => setPArm(e.target.value)} /></div>
            </div>

            {/* 기본 배송지 — 스마트앱과 동일 (우편번호+주소 검색, 주소 자동완성, 상세주소) */}
            <div>
              <p style={{ margin: "0 0 6px", fontSize: 13, fontWeight: 600 }}>기본 배송지</p>
              <div className="row" style={{ gap: 8, alignItems: "flex-end" }}>
                <div className="field" style={{ flex: 1, marginBottom: 0 }}><label>우편번호</label>
                  <input className="input" placeholder="주소 검색" value={pZip} onChange={(e) => setPZip(e.target.value)} /></div>
                <Btn variant="outline" size="sm" style={{ marginBottom: 4 }}
                  onClick={() => toast && toast("아래 주소 칸에 2자 이상 입력하면 자동완성 후보가 떠요")}>주소 검색</Btn>
              </div>
              <div className="field" style={{ marginTop: 10, marginBottom: 0, position: "relative" }}><label>주소</label>
                <input className="input" placeholder="도로명/지번 주소" value={pAddr} onChange={(e) => onAddrChange(e.target.value)} />
                {/* 자동완성 후보 — 고르면 우편번호까지 채워진다 (스마트앱과 동일) */}
                {addrCands.length > 0 && (
                  <div className="card" style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4, padding: 6, maxHeight: 220, overflowY: "auto", boxShadow: "0 10px 30px rgba(0,0,0,.12)" }}>
                    {addrCands.map((c, i) => (
                      <button key={i} type="button" onClick={() => pickAddr(c)}
                        style={{ display: "block", width: "100%", textAlign: "left", background: "none", border: 0, padding: "8px 10px", borderRadius: 8, cursor: "pointer", fontSize: 13 }}>
                        {(c.road_address || c.jibun_address || "")}{c.zipcode ? ` (${c.zipcode})` : ""}
                      </button>
                    ))}
                  </div>
                )}
              </div>
              <div className="field" style={{ marginTop: 10, marginBottom: 0 }}><label>상세주소</label>
                <input className="input" placeholder="동/호수 등" value={pAddrDetail} onChange={(e) => setPAddrDetail(e.target.value)} /></div>
            </div>

            {/* 저장 — 스마트앱과 동일 (내 정보 저장, 큰 버튼) */}
            <Btn variant="primary" size="lg" block disabled={savingProfile} onClick={saveProfile}>{savingProfile ? "저장 중…" : "내 정보 저장"}</Btn>
          </div>

          {/* ── 오른쪽 칼럼(PC) — 혜택·배송지·활동·계정. 좁은 화면에선 폼 아래로 이어짐 */}
          <div className="stack" style={{ gap: 16 }}>
            {/* ── 혜택: 피팅 쿠폰함 — 구매확정 보상. [횟수로 전환]하면 잔고로 옮겨져 무료 소진 후 사용됩니다 */}
            <p className="t-small" style={{ margin: "0 0 -6px", fontWeight: 800 }}>🎟 혜택</p>
            <div className="card" style={{ padding: 18 }}>
              <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
                <strong style={{ fontSize: 14 }}>피팅 쿠폰함</strong>
                <span className="t-caption t-sub">바로 쓸 수 있는 피팅 <strong style={{ color: "var(--primary)" }}>{(auth.freeFitBalance || 0) + (auth.fitCouponBalance || 0)}회</strong></span>
              </div>
              {(coupons || []).filter((c) => !c.converted_at).length === 0 ? (
                <p className="t-caption t-sub" style={{ margin: 0 }}>보유한 쿠폰이 없어요. 상품을 구매하고 구매확정하면 피팅 쿠폰을 드려요.</p>
              ) : (
                <div className="stack" style={{ gap: 8 }}>
                  {(coupons || []).filter((c) => !c.converted_at).map((c) => {
                    const expired = new Date(c.expires_at) < new Date();
                    return (
                      <div key={c.id} className="row" style={{ justifyContent: "space-between", alignItems: "center", border: "1px dashed var(--border)", borderRadius: 10, padding: "10px 12px" }}>
                        <div>
                          <strong style={{ fontSize: 13.5 }}>피팅 {c.fits}회 쿠폰</strong>
                          {/* 발급 사유 배지 — 쿠폰함에 구매확정분과 초대분이 섞여 있어 구분이 필요합니다 */}
                          {c.source === "초대가입" && (
                            <span className="t-caption" style={{ marginLeft: 6, padding: "1px 6px", borderRadius: 999, background: "rgba(0,0,0,.06)", fontWeight: 700 }}>
                              친구 초대
                            </span>
                          )}
                          <p className="t-caption t-sub" style={{ margin: "2px 0 0" }}>
                            {expired ? "유효기간 만료" : `${new Date(c.expires_at).toLocaleDateString("ko-KR")}까지`}
                          </p>
                        </div>
                        <Btn variant="primary" size="sm" disabled={expired} onClick={() => doConvertCoupon(c)}>횟수로 전환</Btn>
                      </div>
                    );
                  })}
                </div>
              )}
              <p className="t-caption t-sub" style={{ margin: "10px 0 0" }}>
                * 무료 피팅은 매주 {auth.weeklyTryonLimit || 10}회씩 충전되고, 안 쓰면 최대 {auth.freeFitBalanceCap || 40}회까지 쌓여요.
                구매 없이 받을 수 있는 무료 충전은 평생 {auth.lifetimeFreeLimit || 60}회까지예요
                (현재 {auth.lifetimeFreeGranted || 0}회 받음 · 구매확정하면 충전이 다시 시작돼요).
              </p>
            </div>

            {/* ── 친구 초대 현황 — 초대 경로는 둘입니다 (계획서 C-10):
                 · 피팅 기록의 [물어보기] 공유 링크 — 피팅 결과가 곧 초대장 (주 경로)
                 · 아래의 개인 초대 링크(invite_token) — 피팅 없이도, 카톡 밖으로도 초대 (보조 경로)
                 어느 쪽이든 보상은 '친구 회원가입' 하나뿐 — 링크를 뿌리는 행위에는 보상이 없습니다. */}
            {referral && referral.enabled && (
              <div className="card" style={{ padding: 18 }}>
                <div className="row" style={{ justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
                  <strong style={{ fontSize: 14 }}>친구 초대</strong>
                  <span className="t-caption t-sub">
                    초대한 친구 <strong style={{ color: "var(--primary)" }}>{referral.invited_count || 0}명</strong>
                  </span>
                </div>
                <p className="t-caption t-sub" style={{ margin: 0 }}>
                  내 초대 링크나 피팅 <strong>물어보기</strong> 링크로 친구가 가입하면
                  피팅 <strong>{referral.fits_per_invitee}회</strong>를 드려요.
                  {(referral.earned_fits || 0) > 0 && <> 지금까지 <strong>{referral.earned_fits}회</strong> 받았어요.</>}
                </p>
                {referral.invite_token && window.KakaoShare && (
                  <div className="row" style={{ gap: 8, marginTop: 10 }}>
                    <Btn variant="outline" size="sm" block onClick={async () => {
                      const url = window.location.origin + "/?ref_share=" + encodeURIComponent(referral.invite_token);
                      toast((await window.KakaoShare.copyLink(url))
                        ? "초대 링크를 복사했어요. 친구에게 붙여넣어 보내주세요!"
                        : "복사하지 못했어요. 잠시 후 다시 시도해 주세요.");
                    }}>초대 링크 복사</Btn>
                    <Btn size="sm" block onClick={async () => {
                      const url = window.location.origin + "/?ref_share=" + encodeURIComponent(referral.invite_token);
                      const r = await window.KakaoShare.invite(url);
                      if (r === "copied") toast("카톡을 열 수 없어 링크를 복사했어요. 붙여넣어 보내주세요!");
                      else if (r === "failed") toast("초대 링크를 보내지 못했어요. 잠시 후 다시 시도해 주세요.");
                    }}>카톡으로 초대</Btn>
                  </div>
                )}
                {referral.monthly_left_invitees === 0 ? (
                  <p className="t-caption" style={{ margin: "8px 0 0", color: "var(--muted)" }}>
                    이번 달 보상 한도에 도달했어요. 다음 달에 다시 받을 수 있어요.
                  </p>
                ) : referral.lifetime_left_fits === 0 ? (
                  <p className="t-caption" style={{ margin: "8px 0 0", color: "var(--muted)" }}>
                    초대 보상을 모두 받았어요. 초대는 계속할 수 있어요.
                  </p>
                ) : null}
              </div>
            )}

            {/* ── 배송지 관리: 여러 배송지 등록·기본지정·삭제 (기존 주소록 컴포넌트 재사용) */}
            <p className="t-small" style={{ margin: "12px 0 -6px", fontWeight: 800 }}>📦 배송지 관리</p>
            <div className="card" style={{ padding: 16 }}>
              <AddressBook auth={auth} toast={toast} onChange={() => auth.refresh()} />
            </div>

            {/* ── 내 활동: 상품 문의 내역 · 내가 쓴 리뷰 */}
            <p className="t-small" style={{ margin: "12px 0 -6px", fontWeight: 800 }}>💬 내 활동</p>
            <MyInquiriesCard />
            <MyReviewsCard toast={toast} />

            {/* (사진 등록은 '가상피팅 모델 선택' 카드로 이동 — 피팅 베이스는 그 카드에서 관리) */}
            {/* ── 계정: 마케팅 수신 · 비밀번호 변경 · 로그아웃 · 회원 탈퇴 (스마트앱과 동일 구성) */}
            <p className="t-small" style={{ margin: "12px 0 -6px", fontWeight: 800 }}>🔒 계정</p>
            {/* 소셜 연결 상태 — 카카오/네이버로 가입한 계정이면 표시 */}
            {auth.oauthProvider && (
              <div className="card row" style={{ padding: "12px 16px", justifyContent: "space-between", alignItems: "center" }}>
                <span className="t-small" style={{ fontWeight: 600 }}>소셜 로그인</span>
                <span style={{
                  padding: "3px 10px", borderRadius: 999, fontSize: 11.5, fontWeight: 700,
                  background: auth.oauthProvider === "kakao" ? "#FEE500" : "#03C75A",
                  color: auth.oauthProvider === "kakao" ? "#191600" : "#fff",
                }}>{auth.oauthProvider === "kakao" ? "카카오 연결됨" : "네이버 연결됨"}</span>
              </div>
            )}
            {/* 마케팅 수신 동의 — 언제든 켜고 끌 수 있다 (개인정보보호법상 쉬운 철회 수단) */}
            <div className="card row" style={{ padding: "12px 16px", justifyContent: "space-between", alignItems: "center" }}>
              <span className="t-small" style={{ fontWeight: 600 }}>마케팅 정보 수신 동의</span>
              <input type="checkbox" checked={auth.marketingConsent} style={{ cursor: "pointer" }}
                onChange={async (e) => {
                  const v = e.target.checked;
                  try {
                    await auth.updateProfile({ marketing_consent: v });
                    if (toast) toast(v ? "마케팅 수신에 동의했어요" : "마케팅 수신 동의를 철회했어요");
                  } catch (err) { if (toast) toast(err.message || "변경에 실패했어요"); }
                }} />
            </div>
            {/* 비밀번호 변경 — 스마트앱과 동일 (제목+캡션, 힌트만 있는 입력 2개, 작은 버튼) */}
            <div className="card" style={{ padding: 14, background: "var(--surface-2)" }}>
              <strong style={{ fontSize: 13, display: "block" }}>비밀번호 변경</strong>
              <p className="t-caption t-sub" style={{ margin: "2px 0 10px" }}>변경하면 다른 기기에서는 다시 로그인해야 해요.</p>
              <div className="stack" style={{ gap: 8 }}>
                <input className="input" type="password" placeholder="현재 비밀번호" value={pwCur} onChange={(e) => setPwCur(e.target.value)} style={{ background: "#fff" }} />
                <input className="input" type="password" placeholder="새 비밀번호" value={pwNew} onChange={(e) => setPwNew(e.target.value)} style={{ background: "#fff" }} />
                {/* 작성규칙 안내 — 서버(validate_password_strength)와 동일 */}
                <p className="t-caption t-sub" style={{ margin: 0 }}>{PW_RULE_TEXT}</p>
                <Btn variant="outline" size="sm" disabled={pwBusy || !pwCur || pwNew.length < 8} style={{ alignSelf: "flex-start" }} onClick={changePassword}>{pwBusy ? "변경 중…" : "비밀번호 변경"}</Btn>
              </div>
            </div>
            <Btn variant="ghost" block onClick={() => auth.logout()}>로그아웃</Btn>
            <p style={{ textAlign: "center", margin: 0 }}>
              <button type="button" onClick={doDeleteAccount}
                style={{ background: "none", border: 0, color: "#DC2626", fontSize: 13, cursor: "pointer", padding: "6px 8px" }}>
                회원 탈퇴
              </button>
            </p>
          </div>
        </div>
      )}

      {/* 사진 확대 보기 — 내 정보 사진 / 피팅 기록 클릭 시 전체 화면 (모든 탭 공통) */}
      {zoomPhoto && (
        <div onClick={() => setZoomPhoto(null)} style={{ position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,.85)", display: "grid", placeItems: "center", padding: 24, cursor: "zoom-out" }}>
          <img src={zoomPhoto} alt="확대 보기" style={{ maxWidth: "92vw", maxHeight: "88vh", objectFit: "contain", borderRadius: 12, boxShadow: "0 20px 60px rgba(0,0,0,.5)" }} />
          <button onClick={(e) => { e.stopPropagation(); setZoomPhoto(null); }} aria-label="닫기"
            style={{ position: "fixed", top: 20, right: 20, width: 44, height: 44, borderRadius: 999, border: "none", background: "rgba(255,255,255,.15)", color: "#fff", fontSize: 22, cursor: "pointer" }}>✕</button>
        </div>
      )}
      </main>
      </div>
    </div>
  );
}

/* ============ 내 활동 (마이페이지 전용) ============ */
// 내 문의 내역 — 상품 문의와 판매자 답변을 한곳에서 확인.
function MyInquiriesCard() {
  const [items, setItems] = useState(null);
  useEffect(() => { API.myInquiries().then(setItems).catch(() => setItems([])); }, []);
  return (
    <div className="card" style={{ padding: 16 }}>
      <strong style={{ fontSize: 13, display: "block", marginBottom: 8 }}>상품 문의 내역</strong>
      {items === null ? (
        <p className="t-caption t-sub" style={{ margin: 0 }}>불러오는 중…</p>
      ) : items.length === 0 ? (
        <p className="t-caption t-sub" style={{ margin: 0 }}>남긴 문의가 없어요. 상품 상세에서 궁금한 점을 물어보세요.</p>
      ) : (
        <div className="stack" style={{ gap: 10 }}>
          {items.map((q) => (
            <div key={q.id}>
              <div className="row" style={{ gap: 6, alignItems: "center" }}>
                <span style={{
                  padding: "1px 7px", borderRadius: 999, fontSize: 11, fontWeight: 700,
                  background: q.answer ? "var(--accent-soft)" : "var(--surface-2)",
                  color: q.answer ? "var(--primary)" : "var(--sub)",
                }}>{q.answer ? "답변완료" : "답변대기"}</span>
                <span className="t-caption t-sub" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{q.product_name || "삭제된 상품"}</span>
              </div>
              <p className="t-small" style={{ margin: "4px 0 0", color: "var(--ink)" }}>Q. {q.content}</p>
              {q.answer && <p className="t-small t-sub" style={{ margin: "2px 0 0" }}>A. {q.answer}</p>}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// 내가 쓴 리뷰 — 목록 확인과 삭제. (수정은 삭제 후 다시 작성)
function MyReviewsCard({ toast }) {
  const [items, setItems] = useState(null);
  function load() { API.myReviews().then(setItems).catch(() => setItems([])); }
  useEffect(load, []);
  async function remove(id) {
    if (!window.confirm("이 리뷰를 삭제할까요?")) return;
    try { await API.deleteMyReview(id); load(); if (toast) toast("리뷰를 삭제했어요"); }
    catch (e) { if (toast) toast(e.message || "삭제에 실패했어요"); }
  }
  return (
    <div className="card" style={{ padding: 16 }}>
      <strong style={{ fontSize: 13, display: "block", marginBottom: 8 }}>내가 쓴 리뷰</strong>
      {items === null ? (
        <p className="t-caption t-sub" style={{ margin: 0 }}>불러오는 중…</p>
      ) : items.length === 0 ? (
        <p className="t-caption t-sub" style={{ margin: 0 }}>작성한 리뷰가 없어요. 구매확정한 주문에서 리뷰를 남겨보세요.</p>
      ) : (
        <div className="stack" style={{ gap: 10 }}>
          {items.map((r) => (
            <div key={r.id} className="row" style={{ gap: 8, alignItems: "flex-start" }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="row" style={{ gap: 6, alignItems: "center" }}>
                  <span style={{ color: "#F59E0B", fontSize: 12 }}>{"★".repeat(r.rating || 0)}</span>
                  <span className="t-caption t-sub" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.product_name || "삭제된 상품"}</span>
                </div>
                {r.content && <p className="t-small" style={{ margin: "2px 0 0", color: "var(--ink)" }}>{r.content}</p>}
              </div>
              <button type="button" onClick={() => remove(r.id)}
                style={{ background: "none", border: 0, color: "#DC2626", cursor: "pointer", fontSize: 12, flex: "none", fontFamily: "inherit" }}>삭제</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { CartScreen, CheckoutScreen, AuthScreen, MyPageScreen });

/* 성별 판별 — 둘레 기본 예시를 성별에 맞게 (예상값이 없을 때만 쓰는 폴백 문구용).
   '예: 44(남성 기준)'가 여성 계정에서 "너무 넓다" 오해를 부른 사례로 추가(07-27). */
function _isF(auth) { return !!(auth && auth.gender && /^(f|w|여)/i.test(auth.gender)); }
function _isM(auth) { return !!(auth && auth.gender && /^(m|남)/i.test(auth.gender)); }
