// 매장 · 사장님 · 승인 — REAL server (api.live.*)
//
// Only values the server returns are shown. Where the old mock showed
// numbers the server has no source for (30일 주문/매출, 사업자등록번호 …)
// the column is gone or the cell says so. See the morning report for the
// list of 임시 결정.

const LIVE_PAGE_SIZE = 10;

// 라벨 표(INSPECTION_STATUS · SALE_STATUS · OPEN_STATUS · ACCOUNT_STATUS)는 labels.jsx 에 있습니다.
const DAY_LABELS = ['월', '화', '수', '목', '금', '토', '일']; // dayOfWeek 1..7

// 사장님 운영 상태(바쁨·일시정지). 보통이거나 서버가 값을 주지 않으면 아무것도 그리지 않습니다.
const OperationBadge = ({ store }) => {
  const text = operationStatusText(store);
  if (!text) return null;
  return <span style={{ marginLeft: 4 }}><Badge tone={OPERATION_STATUS[store.operationStatus].tone}>{text}</Badge></span>;
};

const StatusBadge = ({ map, value }) => {
  if (!value) return <span style={{ color: 'var(--text-3)' }}>—</span>;
  const s = map[value];
  return s ? <Badge tone={s.tone} dot={s.dot}>{s.label}</Badge> : <Badge tone="neutral">{value}</Badge>;
};

// 서버가 값을 모를 때 문자열 'none'·'null' 을 넣어 둔 행이 있습니다(사장님 계정 일부, 2026-09-23 확인).
// 그대로 그리면 "연락처: none" 처럼 값이 있는 것처럼 보입니다 — 빈 값과 같이 다룹니다(labels.jsx 의 isBlankish).
const dash = (v) => (isBlankish(v) ? '—' : v);

const addressOf = (s) => {
  const line1 = s.roadAddr1 || s.addr1;
  const line2 = s.roadAddr1 ? s.roadAddr2 : s.addr2;
  return [line1, line2].filter(Boolean).join(' ');
};
// "서울특별시 송파구 …" → "서울 송파구". Pure formatting of the server address.
const regionOf = (s) => {
  const parts = (s.roadAddr1 || s.addr1 || '').split(/\s+/).filter(Boolean);
  if (!parts.length) return '';
  const city = parts[0].replace(/(특별자치시|특별자치도|특별시|광역시|도)$/, '');
  return [city, parts[1]].filter(Boolean).join(' ');
};
const ymdOrDash = (s) => ymd(s) || '—';

const errorText = (err) => {
  if (!err) return '';
  if (err.status === 404) return '서버에 아직 없는 기능입니다. (API 미구현)';
  if (err.code === 'NETWORK') return err.message;
  return err.message || '불러오지 못했습니다.';
};

// Loading / error / empty wrapper for a live block.
const LiveBlock = ({ state, empty, emptyTitle = '데이터가 없습니다', emptyDesc, children, compact }) => {
  const pad = compact ? '18px 12px' : '48px 20px';
  if (state.loading && state.data == null) {
    return <div style={{ padding: pad, textAlign: 'center', color: 'var(--text-3)', fontSize: 13 }}>불러오는 중…</div>;
  }
  if (state.error) {
    return (
      <div style={{ padding: pad, textAlign: 'center', fontSize: 13 }}>
        <div style={{ color: 'var(--danger)', fontWeight: 600 }}>{errorText(state.error)}</div>
        <button className="btn btn-sm btn-ghost" style={{ marginTop: 10 }} onClick={state.reload}>
          <Icon name="refresh" size={12}/>다시 시도
        </button>
      </div>
    );
  }
  if (empty) {
    return compact
      ? <div style={{ padding: pad, textAlign: 'center', color: 'var(--text-3)', fontSize: 13, border: '1px dashed var(--border)', borderRadius: 8 }}>{emptyTitle}</div>
      : <Empty title={emptyTitle} desc={emptyDesc}/>;
  }
  return children;
};

// Image or a lettered placeholder. The dev seed sends imgUrl "" everywhere.
const LiveThumb = ({ url, label, size = 36, radius = 8 }) => {
  const [broken, setBroken] = React.useState(false);
  const style = { width: size, height: size, borderRadius: radius, flexShrink: 0, objectFit: 'cover' };
  if (url && !broken) return <img src={url} alt="" style={style} onError={() => setBroken(true)}/>;
  return (
    <div className="thumb" style={{ ...style, display: 'grid', placeItems: 'center' }} title={url ? '사진을 불러오지 못함' : '사진 없음'}>
      {(label || '?').slice(0, 1)}
    </div>
  );
};

const SectionTitle = ({ children, right }) => (
  <h4 style={{ margin: '24px 0 10px', fontSize: 13, color: 'var(--text-3)', fontWeight: 600, display: 'flex', alignItems: 'center' }}>
    <span>{children}</span>
    {right && <span style={{ marginLeft: 'auto', fontWeight: 500 }}>{right}</span>}
  </h4>
);

const Notice = ({ tone = 'info', children }) => (
  <div style={{
    display: 'flex', gap: 8, alignItems: 'flex-start', padding: '10px 12px', borderRadius: 8, fontSize: 12.5, lineHeight: 1.5,
    background: tone === 'danger' ? 'var(--danger-soft)' : 'var(--bg)',
    color: tone === 'danger' ? 'var(--danger)' : 'var(--text-2)',
  }}>
    <Icon name="info" size={14}/><div>{children}</div>
  </div>
);

/* ------------------------------------------------------------------ */
/* 사장님 민감 서류(통장 사본·사업자등록증)                              */
/* ------------------------------------------------------------------ */
// 설계와 확인 결과는 `docs/merchant-document.md`. 세 곳이 같은 블록을 씁니다 —
// 매장 상세의 '서류' 탭 · 정산 회차 상세(통장 사본만, 읽기 전용) · 승인 관리의 사업자 드로어.
//
// 이 블록이 지키는 것 셋:
//  1) **주소가 생기지 않게** — 서버가 파일 주소를 주지 않습니다. 바이트를 받아 blob: 으로 그리고
//     닫을 때 revokeObjectURL 로 버립니다. 복사해 둘 수 있는 URL 이 아예 없습니다.
//  2) **볼 때마다 기록** — 바이트를 받는 경로만 기록을 만듭니다(목록·기록 조회는 안 남습니다).
//  3) **자동으로 열지 않게** — 목록은 파일명·크기·상태만. 썸네일을 미리 받지 않습니다.
//     목록을 그리는 것만으로 기록이 쌓이면 그 기록은 "매장을 열어 본 횟수"가 되고,
//     그러면 "누가 이 통장 사본을 봤나"를 찾을 수 없게 됩니다. 느린 대신 기록이 살아 있습니다.

const useMerchantDocuments = (businessId) => useLive(
  () => (businessId == null ? null : api.live.merchantDocuments.list(businessId)),
  [businessId],
);

// 서류 종류별 "현재 서류" 한 건 + 교체된 것들.
// REPLACED 가 아닌 행은 종류당 정확히 하나입니다(업로드 때 서버가 이전 행을 전부 REPLACED 로 바꿉니다).
const splitByDocType = (documents, docType) => {
  const rows = (documents || []).filter((d) => d.docType === docType);   // 서버가 id DESC 로 줍니다
  const current = rows.find((d) => d.status !== 'REPLACED') || null;
  return { current, olders: rows.filter((d) => d !== current) };
};

// 심사한 사람: 이름이 없으면(탈퇴한 어드민) 숫자를 그대로 노출하지 않고 툴팁에만 둡니다.
const ReviewerName = ({ name, id }) => (name
  ? <span>{name}</span>
  : <span title={id != null ? `어드민 번호 ${id}` : ''}>알 수 없는 어드민</span>);

// 열람 사유 고르기. 기본 선택이 없고, "사유 없이 열기"는 한 번 더 눌러야 합니다.
const DocReasonPicker = ({ onPick, onCancel }) => {
  const [custom, setCustom] = React.useState(null);   // null=버튼 선택 단계, 문자열=직접 입력 중
  return (
    <div style={{ padding: 12, borderRadius: 8, background: 'var(--bg)', marginTop: 8 }}>
      <div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 8 }}>어떤 일로 보시나요?</div>
      {custom === null ? (
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          {DOC_VIEW_REASONS.map((r) => (
            <button key={r} className="btn btn-sm btn-ghost" onClick={() => onPick(r)}>{r}</button>
          ))}
          <button className="btn btn-sm btn-ghost" onClick={() => setCustom('')}>직접 입력</button>
        </div>
      ) : (
        <div style={{ display: 'flex', gap: 6 }}>
          <input className="input" autoFocus style={{ flex: 1 }} maxLength={100}
            placeholder="왜 보는지 짧게" value={custom}
            onChange={(e) => setCustom(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter' && custom.trim()) onPick(custom.trim()); }}/>
          <button className="btn btn-sm btn-primary" disabled={!custom.trim()} onClick={() => onPick(custom.trim())}>열기</button>
          <button className="btn btn-sm btn-ghost" onClick={() => setCustom(null)}>취소</button>
        </div>
      )}
      <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 8, lineHeight: 1.6 }}>
        누가·언제·왜 열었는지 기록에 남습니다. 사유는 선택이지만 비우면 기록에 "사유 없음"으로 남습니다.
        {custom !== null && ' 사유에 개인정보는 적지 마세요.'}
        <br/>※ 정산 계좌 <strong>전체 번호</strong> 조회는 사유가 반드시 필요합니다(정산 화면).
      </div>
      <div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
        <button className="btn btn-sm btn-ghost" style={{ color: 'var(--text-3)' }} onClick={() => onPick('')}>
          사유 없이 열기
        </button>
        <button className="btn btn-sm btn-ghost" onClick={onCancel}>그만두기</button>
      </div>
    </div>
  );
};

// 받은 바이트를 그립니다. 새 탭으로 열지 않고 블록 안에서만 보여 주고, 닫으면 blob 을 버립니다.
const DocViewer = ({ url, contentType, name, onClose }) => {
  const [rotate, setRotate] = React.useState(0);       // 통장 사본이 눕혀 찍혀 오는 일이 흔합니다
  const isImage = String(contentType || '').startsWith('image/');
  const isPdf = contentType === 'application/pdf';
  return (
    <div style={{ marginTop: 8, border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: 'var(--bg)', fontSize: 12.5 }}>
        <span className="t-mono" style={{ color: 'var(--text-3)' }}>{contentType}</span>
        {isImage && <button className="btn btn-sm btn-ghost" onClick={() => setRotate((r) => (r + 90) % 360)}>회전</button>}
        <button className="btn btn-sm btn-ghost" style={{ marginLeft: 'auto' }} onClick={onClose}>닫기</button>
      </div>
      <div style={{ padding: 10, textAlign: 'center', background: 'var(--bg-2, #fafafa)' }}>
        {isImage && (
          <img src={url} alt={name}
            style={{ maxWidth: '100%', maxHeight: 420, transform: `rotate(${rotate}deg)`, transition: 'transform .15s' }}/>
        )}
        {/* #toolbar=0 — 크롬 기본 PDF 뷰어의 내려받기·인쇄 버튼을 감춥니다. 다운로드 버튼을 두지
            않기로 한 것과 같은 이유입니다(10-1). **다만 이건 억제이지 차단이 아닙니다** —
            브라우저에서 이미지를 저장하거나 개발자도구로 blob 을 꺼내는 것은 화면이 막을 수 없습니다.
            막을 수 있는 척하지 않습니다. 우리가 보장하는 것은 "서버를 거쳐야 하고 그때 기록이
            남는다"이지 "가져갈 수 없다"가 아닙니다. */}
        {isPdf && <iframe src={`${url}#toolbar=0`} title={name} style={{ width: '100%', height: 480, border: 0, background: 'white' }}/>}
        {!isImage && !isPdf && (
          <div style={{ fontSize: 12.5, color: 'var(--text-3)', padding: '24px 0' }}>
            이 형식은 미리 볼 수 없습니다. ({contentType || '형식 없음'})
          </div>
        )}
      </div>
    </div>
  );
};

// 서류 한 줄. 열기·승인·반려가 여기 붙습니다.
const DocRow = ({ doc: d, canReview, onReviewed, onOpened, last }) => {
  const [reason, setReason] = React.useState(false);   // true=사유 고르는 중
  const [view, setView] = React.useState(null);        // { url, contentType }
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState('');
  const [reject, setReject] = React.useState(false);   // true=반려 사유 적는 중
  const [rejectReason, setRejectReason] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  // 드로어를 닫거나 줄이 사라질 때 blob 주소를 반드시 버립니다.
  React.useEffect(() => () => { if (view) URL.revokeObjectURL(view.url); }, [view]);

  const open = async (viewReason) => {
    setReason(false); setLoading(true); setError('');
    try {
      const { blob, contentType } = await api.live.merchantDocuments.file(d.documentId, viewReason);
      setView({ url: URL.createObjectURL(blob), contentType });
      onOpened(d.documentId);
    } catch (e) {
      // 3131: 행은 있는데 저장소에 파일이 없습니다. 서버 문구는 "담당자에게 문의해 주세요"인데
      // 어드민에서는 운영자가 그 담당자라 그대로 쓰지 않습니다.
      setError(e && e.code === '3131'
        ? '저장된 파일을 찾을 수 없습니다. 사장님께 다시 올려 달라고 요청해 주세요.'
        : errorText(e));
    } finally { setLoading(false); }
  };

  const close = () => { if (view) URL.revokeObjectURL(view.url); setView(null); };

  const submit = async (approve) => {
    const r = approve ? '' : rejectReason.trim();
    if (!approve && !r) return;
    const head = `${docTypeLabel(d.docType)} · ${d.originalName || `서류 ${d.documentId}`}`;
    const msg = approve
      ? `이 서류를 승인할까요?\n\n${head}\n\n승인한 사람과 시각이 기록에 남습니다.`
        + (view || d.__seen ? '' : '\n\n⚠ 이 서류의 파일을 아직 열어 보지 않았습니다.')
      : `이 서류를 반려할까요?\n\n${head}\n사유: ${r}\n\n이 사유가 사장님에게 그대로 보입니다.`;
    if (!window.confirm(msg)) return;
    setBusy(true); setError('');
    try {
      await api.live.merchantDocuments.review(d.documentId, approve, r);
      setReject(false); setRejectReason('');
      onReviewed();
    } catch (e) {
      // 3132 는 "이미 심사한 서류입니다"인데, 보는 동안 사장님이 새로 올려 교체된 경우에도 옵니다.
      // 그때 운영자는 심사한 적이 없으므로 그 문장이 틀립니다 — 다시 읽어 상태를 보여 줍니다.
      setError(e && e.code === '3132'
        ? '이 서류는 지금 심사할 수 없습니다. 사장님이 새 서류를 올려 교체되었을 수 있습니다 — 목록을 다시 불러옵니다.'
        : errorText(e));
      if (e && e.code === '3132') onReviewed();
    } finally { setBusy(false); }
  };

  const st = DOCUMENT_STATUS[d.status] || { label: d.status, tone: 'neutral' };
  const prev = docPrevLabel(d.prevStatus);

  return (
    <div style={{ padding: '10px 0', borderBottom: last ? 0 : '1px solid var(--border)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
        <Badge tone={st.tone}>{st.label}</Badge>
        {prev && <span style={{ fontSize: 12, color: 'var(--text-3)' }}>{prev}</span>}
        <span style={{ fontSize: 13, fontWeight: 600 }}>{d.originalName || `서류 ${d.documentId}`}</span>
        <span style={{ fontSize: 12, color: 'var(--text-3)' }}>
          {fmtBytes(d.byteSize)} · {fmtDT(d.uploadedAt)}
          {d.uploadedByName ? ` · ${d.uploadedByName}` : ''}
        </span>
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
          {view
            ? <button className="btn btn-sm btn-ghost" onClick={close}>닫기</button>
            : <button className="btn btn-sm btn-ghost" disabled={loading || reason}
                onClick={() => setReason(true)}>{loading ? '여는 중…' : '열기'}</button>}
          {canReview && d.status === 'SUBMITTED' && !reject && <>
            <button className="btn btn-sm btn-primary" disabled={busy} onClick={() => submit(true)}>승인</button>
            <button className="btn btn-sm btn-danger-ghost" disabled={busy} onClick={() => setReject(true)}>반려</button>
          </>}
        </div>
      </div>

      {(d.reviewedAt || d.reviewReason) && (
        <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>
          심사 {fmtDT(d.reviewedAt)} · <ReviewerName name={d.reviewedByName} id={d.reviewedBy}/>
          {d.reviewReason ? <span style={{ color: 'var(--danger)' }}> · {d.reviewReason}</span> : null}
        </div>
      )}

      {reason && <DocReasonPicker onPick={open} onCancel={() => setReason(false)}/>}
      {error && <div style={{ marginTop: 8 }}><Notice tone="danger">{error}</Notice></div>}
      {view && <DocViewer url={view.url} contentType={view.contentType} name={d.originalName} onClose={close}/>}

      {reject && (
        <div style={{ marginTop: 8, padding: 12, borderRadius: 8, background: 'var(--bg)' }}>
          <div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 6 }}>
            반려 사유 <span style={{ color: 'var(--danger)' }}>*필수</span>
          </div>
          <div style={{ marginBottom: 6 }}><Notice>이 사유는 <strong>사장님에게 그대로 보입니다.</strong> 내부 메모로 쓰지 마세요.</Notice></div>
          <textarea className="input" autoFocus style={{ width: '100%', height: 72, padding: 10, resize: 'vertical' }}
            value={rejectReason} maxLength={255} onChange={(e) => setRejectReason(e.target.value)}
            placeholder="예: 계좌번호가 보이지 않습니다. 다시 촬영해 주세요."/>
          <div style={{ textAlign: 'right', fontSize: 12, color: 'var(--text-3)' }}>{rejectReason.length}/255</div>
          <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', marginTop: 4 }}>
            <button className="btn btn-sm btn-ghost" disabled={busy} onClick={() => { setReject(false); setRejectReason(''); }}>취소</button>
            <button className="btn btn-sm btn-danger-ghost" disabled={busy || !rejectReason.trim()}
              onClick={() => submit(false)}>{busy ? '처리 중…' : '반려 확정'}</button>
          </div>
        </div>
      )}
    </div>
  );
};

// 열람 기록. "기록이 남는다"고만 적고 아무도 못 보면 억제력이 없어서 상세에 노출합니다.
// data 가 배열이고 총건수가 없어(정산 계좌 이력과 같은 모양) 페이지가 아니라 size 를 늘립니다.
// 이 목록을 읽는 것 자체는 기록을 만들지 않습니다(2026-09-27 dev 실측).
const DocAccessLogs = ({ businessId, refreshKey }) => {
  const [size, setSize] = React.useState(3);
  const logs = useLive(() => api.live.merchantDocuments.accessLogs(businessId, { size }), [businessId, size, refreshKey]);
  const rows = logs.data || [];
  return (
    <>
      {/* 이름을 "서류 열람 기록"으로 둡니다 — 정산 회차 상세에는 **계좌** 열람 기록이 이미 있어
          둘 다 "열람 기록"이면 어느 쪽 기록인지 구별되지 않습니다. */}
      <SectionTitle right={rows.length >= size ? <button className="btn btn-sm btn-ghost" onClick={() => setSize((n) => n + 20)}>더 보기</button> : null}>
        서류 열람 기록
      </SectionTitle>
      <LiveBlock state={logs} compact empty={rows.length === 0} emptyTitle="아직 열어 본 기록이 없습니다">
        <div style={{ fontSize: 12.5, lineHeight: 1.9 }}>
          {rows.map((l) => (
            <div key={l.logId} style={{ color: 'var(--text-2)' }}>
              {fmtDT(l.accessedAt)} · {l.adminName || `어드민 ${l.aid}`} · {docTypeLabel(l.docType)}
              {' · '}{l.reason ? l.reason : <span style={{ color: 'var(--text-3)' }}>사유 없음</span>}
            </div>
          ))}
        </div>
      </LiveBlock>
    </>
  );
};

// 서류 블록 본체. `state` 는 부모가 들고 있는 useMerchantDocuments 결과입니다
// (매장 상세는 탭 배지와 승인 확인창에 건수가 필요해 드로어를 열 때 함께 읽습니다 —
//  목록 조회는 열람 기록을 만들지 않으므로 값이 들지 않습니다).
const MerchantDocuments = ({ businessId, state, docTypes = ['BANKBOOK', 'BUSINESS_LICENSE'], canReview = false, showBusiness = true, showAccount = true }) => {
  const [openOlders, setOpenOlders] = React.useState({});
  const [seen, setSeen] = React.useState(() => new Set());   // 이번에 파일을 열어 본 서류
  const [logTick, setLogTick] = React.useState(0);
  const onOpened = (id) => { setSeen((s) => new Set(s).add(id)); setLogTick((n) => n + 1); };

  if (businessId == null) {
    return <Notice>사업자가 연결되지 않은 매장입니다. 서류는 매장이 아니라 사업자에 붙습니다.</Notice>;
  }

  const d = state.data;
  const biz = (d && d.business) || null;
  const acct = (d && d.settlementAccount) || null;

  return (
    <LiveBlock state={state} empty={!d} emptyTitle="서류 정보를 불러오지 못했습니다">
      {d && (
        <div>
          {showBusiness && (
            <>
              <SectionTitle>사업자</SectionTitle>
              <dl className="spec">
                <dt>상호</dt><dd>{(biz && biz.businessName) || EMPTY_VALUE}</dd>
                <dt>사업자등록번호</dt><dd className="t-mono">{(biz && biz.businessNo) || EMPTY_VALUE}</dd>
                <dt>사업장 주소</dt><dd>{(biz && biz.businessAddress) || EMPTY_VALUE}</dd>
                <dt>매장</dt><dd>{(biz && (biz.storeNames || []).join(', ')) || EMPTY_VALUE}</dd>
              </dl>
            </>
          )}

          {/* 통장 사본 심사의 본질은 "사본의 예금주·계좌가 등록된 정산 계좌와 같은가" 입니다.
              전체 번호는 정산 화면의 열람 경로에서만 봅니다(여기서는 마스킹만). */}
          {showAccount && docTypes.includes('BANKBOOK') && (
            <>
              <SectionTitle>등록된 정산 계좌</SectionTitle>
              {acct ? (
                <div style={{ fontSize: 13 }}>
                  {bankLabel(acct.bankCode)} <span className="t-mono">{acct.accountMasked}</span>
                  {' · 예금주 '}<strong>{acct.accountHolder || EMPTY_VALUE}</strong>
                  {acct.updatedAt ? <span style={{ color: 'var(--text-3)' }}> · {fmtDT(acct.updatedAt)} 등록</span> : null}
                </div>
              ) : <Notice>등록된 정산 계좌가 없습니다. 통장 사본과 맞춰 볼 값이 없습니다.</Notice>}
            </>
          )}

          {docTypes.map((t) => {
            const { current, olders } = splitByDocType(d.documents, t);
            return (
              <React.Fragment key={t}>
                <SectionTitle right={olders.length > 0
                  ? <button className="btn btn-sm btn-ghost" onClick={() => setOpenOlders((o) => ({ ...o, [t]: !o[t] }))}>
                      이전 {olders.length}건 {openOlders[t] ? '▴' : '▾'}
                    </button>
                  : null}>
                  {docTypeLabel(t)}
                </SectionTitle>
                {current
                  ? <DocRow doc={{ ...current, __seen: seen.has(current.documentId) }} canReview={canReview}
                      onReviewed={() => { state.reload(); }} onOpened={onOpened} last/>
                  : <Notice>제출된 서류가 없습니다. 사장님이 사장님 화면에서 올립니다.</Notice>}
                {openOlders[t] && olders.map((o, i) => (
                  <DocRow key={o.documentId} doc={o} canReview={false}
                    onReviewed={() => state.reload()} onOpened={onOpened} last={i === olders.length - 1}/>
                ))}
              </React.Fragment>
            );
          })}

          <DocAccessLogs businessId={businessId} refreshKey={logTick}/>
        </div>
      )}
    </LiveBlock>
  );
};

// 사장님(merchant) 뷰 모델: /admin/merchant/search 의 (사장님, 매장) 행을 merchantId 하나로 묶습니다.
// 서버 필드(merchantId, loginId, name, email, phoneNumber, status, lastLoginDate, regDate)는
// 그대로 두고 묶음 결과만 storeIds 에 담습니다.
const toMerchants = (rows) => {
  const byMerchantId = new Map();
  (rows || []).forEach((r) => {
    const { storeId, storeName, ...account } = r;
    const cur = byMerchantId.get(r.merchantId) || { ...account, storeIds: [] };
    if (storeId != null && storeId !== '') {
      const sid = Number(storeId);
      if (!cur.storeIds.includes(sid)) cur.storeIds.push(sid);
    }
    byMerchantId.set(r.merchantId, cur);
  });
  return [...byMerchantId.values()];
};

// merchants: 사장님 목록, merchantsByStore: storeId → [사장님]
const useMerchantIndex = () => {
  const merchants = useLive(async () => toMerchants(await api.live.merchants.search()), []);
  const merchantsByStore = React.useMemo(() => {
    const m = new Map();
    (merchants.data || []).forEach((c) => c.storeIds.forEach((sid) => {
      if (!m.has(sid)) m.set(sid, []);
      m.get(sid).push(c);
    }));
    return m;
  }, [merchants.data]);
  return { merchants, merchantsByStore };
};

/* ------------------------------------------------------------------ */
/* 매장 관리                                                           */
/* ------------------------------------------------------------------ */

const Stores = () => {
  const stores = useLive(() => api.live.stores.list(), []);
  const { merchants, merchantsByStore } = useMerchantIndex();
  useInspectionChanged(stores.reload);
  const [view, setView] = React.useState('list');
  const [q, setQ] = React.useState('');
  const [statusF, setStatusF] = React.useState('');
  const [regionF, setRegionF] = React.useState('');
  const [page, setPage] = React.useState(1);
  const [selected, setSelected] = React.useState(null); // storeId
  React.useEffect(() => { setPage(1); }, [q, statusF, regionF]);

  const all = stores.data || [];
  const regions = React.useMemo(() => [...new Set(all.map(regionOf).filter(Boolean))].sort(), [all]);
  const ql = q.trim().toLowerCase();
  const filtered = all.filter((s) => {
    if (statusF && s.status !== statusF) return false;
    if (regionF && regionOf(s) !== regionF) return false;
    if (!ql) return true;
    const merchantText = (merchantsByStore.get(s.storeId) || []).map((c) => c.name).join(' ');
    return [s.name, addressOf(s), s.phoneNumber, String(s.storeId), merchantText]
      .some((v) => (v || '').toLowerCase().includes(ql));
  });
  const totalPages = Math.max(1, Math.ceil(filtered.length / LIVE_PAGE_SIZE));
  const safePage = Math.min(page, totalPages);
  const pageItems = filtered.slice((safePage - 1) * LIVE_PAGE_SIZE, safePage * LIVE_PAGE_SIZE);
  const count = (st) => all.filter((s) => s.status === st).length;
  const merchantNamesOf = (s) => (merchantsByStore.get(s.storeId) || []).map((c) => c.name).join(', ');

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>매장 관리</h1>
          <div className="sub">입점 매장과 검수 상태를 확인합니다 · 총 {stores.data ? `${all.length}개` : '—'}</div>
        </div>
        <div className="actions">
          <button className="btn btn-ghost btn-sm" onClick={() => { stores.reload(); merchants.reload(); }} disabled={stores.loading}>
            <Icon name="refresh" size={14}/>새로고침
          </button>
          <Seg items={[{ value: 'list', label: '목록' }, { value: 'grid', label: '카드' }]} value={view} onChange={setView}/>
        </div>
      </div>

      <div className="stat-grid" style={{ gridTemplateColumns: 'repeat(4,1fr)' }}>
        <MiniStat icon="store"       label="승인" value={stores.data ? `${count('APPROVAL')}개` : '—'} tone="success"/>
        <MiniStat icon="hourglass"   label="검수대기"   value={stores.data ? `${count('AWAIT')}개` : '—'} tone="warn"
          onAction={count('AWAIT') ? () => setStatusF('AWAIT') : undefined} actionLabel="보기"/>
        <MiniStat icon="ban"         label="반려"       value={stores.data ? `${count('REJECT')}개` : '—'} tone="danger"
          onAction={count('REJECT') ? () => setStatusF('REJECT') : undefined} actionLabel="보기"/>
        <MiniStat icon="list"        label="전체"       value={stores.data ? `${all.length}개` : '—'} tone="info"/>
      </div>

      <div className="table-wrap" style={{ marginTop: 14 }}>
        <div className="table-toolbar">
          <input className="input" placeholder="매장명, 주소, 전화, 매장 ID, 사장님" style={{ flex: 1, maxWidth: 340 }}
            value={q} onChange={(e) => setQ(e.target.value)}/>
          <select className="select" value={regionF} onChange={(e) => setRegionF(e.target.value)}>
            <option value="">전체 지역</option>
            {regions.map((r) => <option key={r} value={r}>{r}</option>)}
          </select>
          <select className="select" value={statusF} onChange={(e) => setStatusF(e.target.value)}>
            <option value="">전체 상태</option>
            {Object.entries(INSPECTION_STATUS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
          </select>
        </div>
        {merchants.error && <div style={{ padding: '0 16px 10px' }}><Notice tone="danger">사장님 정보를 불러오지 못해 사장님 이름이 비어 있습니다. ({errorText(merchants.error)})</Notice></div>}

        <LiveBlock state={stores} empty={filtered.length === 0}
          emptyTitle={all.length ? '조건에 맞는 매장이 없습니다' : '등록된 매장이 없습니다'}>
          {view === 'list' ? (
            <table className="table">
              <thead><tr>
                <th>매장 ID</th><th>매장명</th><th>지역</th><th>전화</th><th>이용 방식</th><th>검수 · 운영 상태</th><th>등록일</th><th>수정일</th>
              </tr></thead>
              <tbody>
                {pageItems.map((s) => (
                  <tr key={s.storeId} onClick={() => setSelected(s.storeId)} style={{ cursor: 'pointer' }}>
                    <td className="t-mono">{s.storeId}</td>
                    <td>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                        <LiveThumb label={s.name}/>
                        <div>
                          <div style={{ fontWeight: 600 }}>{s.name}</div>
                          <div style={{ fontSize: 12, color: 'var(--text-3)' }}>
                            {merchantNamesOf(s) ? `사장님 ${merchantNamesOf(s)}` : (merchants.loading ? '…' : '연결된 사장님 없음')}
                          </div>
                        </div>
                      </div>
                    </td>
                    <td style={{ color: 'var(--text-2)' }}>{dash(regionOf(s))}</td>
                    <td className="t-mono">{maskPhone(s.phoneNumber)}</td>
                    <td style={{ fontSize: 12.5 }}>
                      {[s.isTakeoutAvailable === 'Y' && '포장', s.isInStoreAvailable === 'Y' && '매장식사'].filter(Boolean).join(' · ') || '—'}
                    </td>
                    <td>
                      <StatusBadge map={INSPECTION_STATUS} value={s.status}/>
                      <OperationBadge store={s}/>
                    </td>
                    <td style={{ color: 'var(--text-3)' }}>{ymdOrDash(s.regDate)}</td>
                    <td style={{ color: 'var(--text-3)' }}>{ymdOrDash(s.updateDate)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          ) : (
            <div style={{ padding: 16, display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(280px,1fr))', gap: 14 }}>
              {pageItems.map((s) => (
                <div key={s.storeId} className="card" style={{ padding: 0, overflow: 'hidden', cursor: 'pointer' }} onClick={() => setSelected(s.storeId)}>
                  <div style={{ height: 120, background: 'linear-gradient(135deg, #FFF1E6, #FFE3D0)', display: 'grid', placeItems: 'center', color: 'var(--brand)', fontSize: 32, fontWeight: 800 }}>{s.name.slice(0, 1)}</div>
                  <div style={{ padding: 14 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                      <div style={{ fontWeight: 700, flex: 1 }}>{s.name}</div>
                      <StatusBadge map={INSPECTION_STATUS} value={s.status}/>
                    </div>
                    <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>{dash(regionOf(s))} · {maskPhone(s.phoneNumber)}</div>
                    <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 8, paddingTop: 8, borderTop: '1px dashed var(--border)' }}>
                      {merchantNamesOf(s) ? `사장님 ${merchantNamesOf(s)}` : '연결된 사장님 없음'} · 등록 {ymdOrDash(s.regDate)}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          )}
        </LiveBlock>
        <div className="table-foot">
          <span style={{ fontSize: 13, color: 'var(--text-3)' }}>총 {filtered.length}건</span>
          <Pager total={totalPages} current={safePage} onPick={setPage}/>
        </div>
      </div>

      <StoreDrawer storeId={selected} onClose={() => setSelected(null)} merchants={selected ? (merchantsByStore.get(selected) || []) : []}/>
    </div>
  );
};

/* ------------------------------------------------------------------ */
/* 매장 상세 Drawer (정보 · 메뉴 · 검수 처리)                           */
/* ------------------------------------------------------------------ */

const StoreDrawer = ({ storeId, onClose, merchants = [], initialTab = 'info', initialIntent = null }) => {
  const open = storeId != null;
  const detail = useLive(() => (open ? api.live.stores.get(storeId) : null), [storeId]);
  // 매장 상세(DTO)는 사업자가 없어도 **키가 있고 값이 null** 입니다 — 'businessId' in s 로 갈라선 안 됩니다.
  const businessId = (detail.data && detail.data.businessId) != null ? detail.data.businessId : null;
  // 서류 목록을 드로어를 열 때 함께 읽습니다. 탭을 열 때만 읽으면 탭 배지와 승인 확인창의
  // "미심사 서류 n건"을 그릴 수 없습니다. **목록 조회는 열람 기록을 만들지 않아** 값이 들지 않습니다.
  const docs = useMerchantDocuments(businessId);
  const awaitDocs = ((docs.data && docs.data.documents) || []).filter((x) => x.status === 'SUBMITTED').length;
  useDocumentChanged(() => { if (businessId != null) docs.reload(); });
  const [tab, setTab] = React.useState(initialTab);
  // review: null | 'approve' | 'reject'
  const [review, setReview] = React.useState(initialIntent);
  const [reason, setReason] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [actionError, setActionError] = React.useState('');
  React.useEffect(() => {
    setTab(initialTab); setReview(initialIntent); setReason(''); setActionError(''); setBusy(false);
  }, [storeId, initialTab, initialIntent]);

  if (!open) return null;
  const s = detail.data;
  const canReview = s && s.status === 'AWAIT';

  const submit = async () => {
    if (review === 'reject' && !reason.trim()) return;
    // 서류 탭을 열지 않고 매장을 승인해 버리는 것이 가장 흔할 실수라, 미심사 건수를 되읽어 줍니다.
    // 막지는 않습니다 — 매장 승인과 서류 심사는 서버에서 별개이고 서류가 늦게 올 수도 있습니다.
    if (review === 'approve' && awaitDocs > 0 && !window.confirm(
      `이 매장을 승인할까요?\n승인하면 사용자 앱에 매장이 노출됩니다.\n\n`
      + `⚠ 아직 심사하지 않은 서류가 ${awaitDocs}건 있습니다.`)) return;
    setBusy(true); setActionError('');
    try {
      await api.live.stores.setStatus(storeId, review === 'approve'
        ? { status: 'APPROVAL' }
        : { status: 'REJECT', rejectReason: reason.trim() });
      setReview(null); setReason('');
      detail.reload();
    } catch (err) {
      setActionError(errorText(err));
    } finally {
      setBusy(false);
    }
  };

  const footer = (() => {
    if (!canReview) return <button className="btn btn-ghost" onClick={onClose}>닫기</button>;
    if (review === 'approve') return <>
      <span style={{ marginRight: 'auto', fontSize: 12.5, color: 'var(--text-2)' }}>승인하면 사용자 앱에 매장이 노출됩니다.</span>
      <button className="btn btn-ghost" onClick={() => setReview(null)} disabled={busy}>취소</button>
      <button className="btn btn-primary" onClick={submit} disabled={busy}>{busy ? '처리 중…' : '승인 확정'}</button>
    </>;
    if (review === 'reject') return <>
      <button className="btn btn-ghost" onClick={() => setReview(null)} disabled={busy}>취소</button>
      <button className="btn btn-danger-ghost" onClick={submit} disabled={busy || !reason.trim()}
        title={reason.trim() ? '' : '반려 사유를 입력하세요'}>{busy ? '처리 중…' : '반려 확정'}</button>
    </>;
    return <>
      <button className="btn btn-danger-ghost" onClick={() => { setTab('info'); setReview('reject'); }}>반려</button>
      <button className="btn btn-primary" onClick={() => setReview('approve')}>승인</button>
    </>;
  })();

  return (
    <Drawer open onClose={onClose} title={s ? s.name : `매장 ${storeId}`} footer={footer}>
      <Tabs items={[
        { value: 'info', label: '매장 정보' },
        { value: 'menu', label: '메뉴' },
        { value: 'docs', label: '서류', count: awaitDocs > 0 ? awaitDocs : undefined },
      ]} value={tab} onChange={setTab}/>
      <div style={{ height: 14 }}/>
      {review === 'reject' && (
        <div style={{ marginBottom: 16 }}>
          <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 6 }}>반려 사유 <span style={{ color: 'var(--danger)' }}>*필수</span></div>
          <textarea className="input" autoFocus style={{ width: '100%', height: 90, padding: 10, resize: 'vertical' }}
            placeholder="사장님께 전달될 반려 사유를 입력하세요" value={reason} onChange={(e) => setReason(e.target.value)}/>
        </div>
      )}
      {actionError && <div style={{ marginBottom: 14 }}><Notice tone="danger">처리하지 못했습니다: {actionError}</Notice></div>}
      <LiveBlock state={detail} empty={!s} emptyTitle="매장 정보를 찾을 수 없습니다">
        {s && tab === 'info' && <StoreInfo store={s} merchants={merchants} onChanged={detail.reload}/>}
        {s && tab === 'menu' && <StoreMenus storeId={storeId}/>}
        {s && tab === 'docs' && <MerchantDocuments businessId={businessId} state={docs} canReview/>}
      </LiveBlock>
    </Drawer>
  );
};

// 영업이 자정을 넘는 날인지 (종료 ≤ 시작)
const overnight = (w) => !!(w.openTime && w.closeTime && String(w.closeTime).slice(0, 5) <= String(w.openTime).slice(0, 5));

const StoreInfo = ({ store: s, merchants, onChanged }) => {
  const rep = (s.images || []).find((i) => i.isRepresentative === 'Y') || (s.images || [])[0];
  const worktimes = [...(s.worktimes || [])].sort((a, b) => a.dayOfWeek - b.dayOfWeek);
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
        <LiveThumb url={rep && rep.imgUrl} label={s.name} size={56} radius={12}/>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
            <StatusBadge map={INSPECTION_STATUS} value={s.status}/>
            {s.openStatus && <StatusBadge map={OPEN_STATUS} value={s.openStatus}/>}
            <OperationBadge store={s}/>
            {(s.categories || []).map((c) => <Badge key={c.categoryId} tone="neutral">{c.name}</Badge>)}
          </div>
          <div style={{ fontSize: 13, color: 'var(--text-3)', marginTop: 6 }}>
            ★ {typeof s.rating === 'number' ? s.rating.toFixed(1) : '—'} · 리뷰 {s.reviewCount ?? '—'}
          </div>
        </div>
      </div>

      {s.status === 'REJECT' && (
        <div style={{ marginBottom: 14 }}><Notice tone="danger">반려 사유: {s.rejectReason || '(사유 없음)'}</Notice></div>
      )}

      <SectionTitle>매장 정보</SectionTitle>
      <dl className="spec">
        <dt>매장 ID</dt><dd className="t-mono">{s.storeId}</dd>
        <dt>사장님</dt><dd>{merchants.length ? merchants.map((c) => `${c.name} (로그인 ID ${c.loginId})`).join(', ') : '연결된 사장님 없음'}</dd>
        <dt>주소</dt><dd>{dash(addressOf(s))}{s.zipCode ? <span style={{ color: 'var(--text-3)' }}> · {s.zipCode}</span> : null}</dd>
        <dt>전화</dt><dd className="t-mono">{dash(s.phoneNumber)}</dd>
        <dt>이용 방식</dt><dd>{[s.isTakeoutAvailable === 'Y' && '포장', s.isInStoreAvailable === 'Y' && '매장식사'].filter(Boolean).join(' · ') || '—'}</dd>
        {/* store.cook_time 은 '가게 고정 조리시간'이 아니라, 사장님이 주문을 수락할 때
            시간을 안 고르면 쓰는 기본값(fallback)이자 주문 전 손님 추정("보통 약 N분")용이다.
            주문별 실제 조리 시간은 orders.cook_minutes(V15, 2026-09-22). 운영자가 '이 가게는
            무조건 N분'으로 오해하지 않게 '기본 조리 시간'으로 표기한다(2026-11-03). */}
        <dt>기본 조리 시간</dt><dd>{s.cookTime ? `${s.cookTime}분` : '—'}</dd>
        <dt>등록일</dt><dd>{dash(s.regDate)}</dd>
        <dt>수정일</dt><dd>{dash(s.updateDate)}</dd>
      </dl>

      <SectionTitle>영업시간</SectionTitle>
      {worktimes.length === 0 ? <LiveBlock state={{}} empty compact emptyTitle="등록된 영업시간이 없습니다"/> : (
        <dl className="spec">
          {worktimes.map((w) => (
            <React.Fragment key={w.storeWorktimeId || w.dayOfWeek}>
              <dt>{DAY_LABELS[w.dayOfWeek - 1] || w.dayOfWeek}</dt>
              <dd>
                {w.isDayOff === 'Y' ? '정기휴무' : fmtTimeRange(w.openTime, w.closeTime)}
                {w.isDayOff !== 'Y' && w.isBreak === 'Y' && w.breakTimeStart ? <span style={{ color: 'var(--text-3)' }}> · 휴게시간 {fmtTimeRange(w.breakTimeStart, w.breakTimeEnd, overnight(w) ? w.openTime : null)}</span> : null}
              </dd>
            </React.Fragment>
          ))}
        </dl>
      )}
      {(s.holidays || []).length > 0 && (
        <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 8 }}>
          임시휴무: {(s.holidays || []).map((h) => (typeof h === 'string' ? h : (h.holidayDate || JSON.stringify(h)))).join(', ')}
        </div>
      )}

      <SectionTitle right={`${(s.images || []).length}장`}>매장 사진</SectionTitle>
      <ImageReview
        images={(s.images || []).map((i) => ({ id: i.imageId, url: i.imgUrl, status: i.status, tag: i.isRepresentative === 'Y' ? '대표' : '' }))}
        label={s.name} size={88}
        decide={(id, status) => api.live.stores.setImageStatus(s.storeId, id, { status })}
        onChanged={onChanged}/>

      <SectionTitle>소개 · 안내</SectionTitle>
      <dl className="spec">
        <dt>매장 소개</dt><dd style={{ whiteSpace: 'pre-wrap' }}>{dash(s.introduce)}</dd>
        <dt>공지</dt><dd style={{ whiteSpace: 'pre-wrap' }}>{dash(s.notice)}</dd>
        <dt>찾아오는 길</dt><dd style={{ whiteSpace: 'pre-wrap' }}>{dash(s.locationTip)}</dd>
        <dt>주차</dt><dd>{dash(s.parkingInformation)}</dd>
        <dt>원산지</dt><dd style={{ whiteSpace: 'pre-wrap' }}>{dash(s.countryOfOrigin)}</dd>
        <dt>알레르기</dt><dd>{dash(s.allergen)}</dd>
        <dt>영양성분</dt><dd>{dash(s.nutritionFacts)}</dd>
      </dl>

      <SectionTitle>운영 성과</SectionTitle>
      <Notice>매장별 주문·매출 수치는 아직 표시하지 않습니다. 서버 집계(일별 매출 통계)가 모든 매장에서 0으로 내려와 실제 값인지 확인되지 않았습니다.</Notice>
    </div>
  );
};

/* ------------------------------------------------------------------ */
/* 매장별 메뉴 (그룹 → 메뉴 → 옵션, 메뉴 사진 검수)                      */
/* ------------------------------------------------------------------ */

const StoreMenus = ({ storeId }) => {
  const groups = useLive(async () => {
    const list = (await api.live.menus.groups(storeId)) || [];
    const sorted = [...list].sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
    // One detail call per group; a failing group shows its own error.
    const details = await Promise.all(sorted.map((g) =>
      api.live.menus.groupDetail(g.id).then((d) => ({ group: g, detail: d, error: null }), (error) => ({ group: g, detail: null, error }))));
    return details;
  }, [storeId]);
  const [openMenu, setOpenMenu] = React.useState(null);
  const data = groups.data || [];
  const menuCount = data.reduce((n, g) => n + ((g.detail && g.detail.menus) || []).length, 0);
  const awaiting = data.reduce((n, g) => n + ((g.detail && g.detail.menus) || [])
    .reduce((k, m) => k + (m.images || []).filter((i) => i.status === 'AWAIT').length, 0), 0);

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
        <span style={{ fontSize: 13, color: 'var(--text-2)' }}>
          {groups.data ? `메뉴그룹 ${data.length}개 · 메뉴 ${menuCount}개` : ''}
          {awaiting > 0 && <> · <b style={{ color: 'var(--warn, #B7791F)' }}>사진 검수대기 {awaiting}장</b></>}
        </span>
        <button className="btn btn-ghost btn-sm" style={{ marginLeft: 'auto' }} onClick={groups.reload} disabled={groups.loading}>
          <Icon name="refresh" size={12}/>새로고침
        </button>
      </div>
      <LiveBlock state={groups} empty={data.length === 0} compact emptyTitle="등록된 메뉴그룹이 없습니다">
        {data.map(({ group, detail, error }) => {
          const menus = (detail && detail.menus) || [];
          return (
            <div key={group.id} style={{ marginBottom: 16 }}>
              <div style={{ fontWeight: 700, fontSize: 13.5, margin: '0 0 6px' }}>
                {group.name} <span style={{ color: 'var(--text-3)', fontWeight: 500 }}>· {error ? '불러오기 실패' : `${menus.length}개`}</span>
              </div>
              {error ? <Notice tone="danger">{errorText(error)}</Notice>
                : menus.length === 0 ? <LiveBlock state={{}} empty compact emptyTitle="이 그룹에 메뉴가 없습니다"/>
                : (
                  <div className="card" style={{ padding: 0 }}>
                    {menus.map((m, i) => (
                      <MenuRow key={m.id} menu={m} last={i === menus.length - 1}
                        open={openMenu === m.id} onToggle={() => setOpenMenu(openMenu === m.id ? null : m.id)}
                        onChanged={groups.reload}/>
                    ))}
                  </div>
                )}
            </div>
          );
        })}
      </LiveBlock>
      <div style={{ marginTop: 8 }}>
        <Notice>메뉴그룹에 속하지 않은 메뉴는 서버 응답에 포함되지 않아 여기에 나오지 않습니다.</Notice>
      </div>
    </div>
  );
};

const MenuRow = ({ menu: m, last, open, onToggle, onChanged }) => {
  const images = m.images || [];
  const firstUrl = (images.find((i) => i.imgUrl) || {}).imgUrl;
  const awaiting = images.filter((i) => i.status === 'AWAIT').length;
  return (
    <div style={{ borderBottom: last ? 0 : '1px solid var(--border)' }}>
      <div onClick={onToggle} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', cursor: 'pointer' }}>
        <LiveThumb url={firstUrl} label={m.name}/>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 600, fontSize: 13.5 }}>{m.name} <span className="t-mono" style={{ color: 'var(--text-3)', fontWeight: 400, fontSize: 11.5 }}>#{m.id}</span></div>
          <div style={{ fontSize: 12, color: 'var(--text-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{dash(m.description)}</div>
        </div>
        <div style={{ textAlign: 'right', fontSize: 13 }}>
          <div style={{ fontWeight: 600 }}>{typeof m.price === 'number' ? fmtKRW(m.price) : '—'}</div>
          <div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end', marginTop: 3 }}>
            <StatusBadge map={SALE_STATUS} value={m.saleStatus}/>
            {awaiting > 0 && <Badge tone="warn">사진 검수대기 {awaiting}</Badge>}
          </div>
        </div>
        <Icon name={open ? 'chevDown' : 'chevRight'} size={14}/>
      </div>
      {open && <MenuDetailPanel menu={m} onChanged={onChanged}/>}
    </div>
  );
};

const MenuDetailPanel = ({ menu, onChanged }) => {
  const detail = useLive(() => api.live.menus.detail(menu.id), [menu.id]);
  const d = detail.data;
  return (
    <div style={{ padding: '4px 12px 14px 60px', background: 'var(--bg)' }}>
      <dl className="spec" style={{ paddingTop: 8 }}>
        <dt>등록 / 수정</dt><dd>{dash(menu.regDate)} / {dash(menu.updateDate)}</dd>
      </dl>
      <SectionTitle right={`${(menu.images || []).length}장`}>메뉴 사진</SectionTitle>
      <ImageReview
        images={(menu.images || []).map((i) => ({ id: i.id, url: i.imgUrl, status: i.status }))}
        label={menu.name} size={112}
        decide={(id, status) => api.live.menus.setImageStatus(id, { status })}
        onChanged={onChanged}/>
      <SectionTitle>옵션</SectionTitle>
      <LiveBlock state={detail} compact empty={!d || !(d.optionGroups || []).length} emptyTitle="연결된 옵션이 없습니다">
        {d && (d.optionGroups || []).map((g) => (
          <div key={g.id} style={{ marginBottom: 10 }}>
            <div style={{ fontSize: 13, fontWeight: 600 }}>
              {g.name}{' '}
              <span style={{ fontWeight: 400, color: 'var(--text-3)', fontSize: 12 }}>
                {g.isRequired === 'Y' ? '필수' : '선택'} · {g.minSelectCount ?? 0}~{g.maxSelectCount ?? '—'}개
              </span>
            </div>
            <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 2 }}>
              {(g.options || []).map((o) => `${o.name}${o.price ? ` +${fmtNum(o.price)}원` : ''}`).join(' · ') || '옵션 항목 없음'}
            </div>
          </div>
        ))}
      </LiveBlock>
    </div>
  );
};

// Per-image approve / reject with a confirm step. Only AWAIT images get buttons.
// `decide(id, status)` performs the call; `onChanged` refreshes the parent.
const ImageReview = ({ images, label, size = 112, decide, onChanged }) => {
  const [pending, setPending] = React.useState(null); // { id, status }
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  if (!images.length) return <LiveBlock state={{}} empty compact emptyTitle="등록된 사진이 없습니다"/>;
  const confirm = async () => {
    setBusy(true); setErr('');
    try {
      await decide(pending.id, pending.status);
      setPending(null);
      onChanged && onChanged();
    } catch (e) {
      setErr(errorText(e));
    } finally {
      setBusy(false);
    }
  };
  const start = (id, status) => { setErr(''); setPending({ id, status }); };
  return (
    <div>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
        {images.map((im) => (
          <div key={im.id} style={{ width: size }}>
            <LiveThumb url={im.url} label={label} size={size} radius={10}/>
            <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, flexWrap: 'wrap' }}>
              <StatusBadge map={INSPECTION_STATUS} value={im.status}/>
              {im.tag && <Badge tone="brand">{im.tag}</Badge>}
              <span className="t-mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>#{im.id}</span>
            </div>
            {!im.url && <div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 2 }}>사진 주소 없음</div>}
            {im.status === 'AWAIT' && (
              pending && pending.id === im.id ? (
                <div style={{ marginTop: 6 }}>
                  <div style={{ fontSize: 12, marginBottom: 4 }}>{pending.status === 'APPROVAL' ? '이 사진을 승인할까요?' : '이 사진을 반려할까요?'}</div>
                  <div style={{ display: 'flex', gap: 4 }}>
                    <button className={`btn btn-sm ${pending.status === 'APPROVAL' ? 'btn-primary' : 'btn-danger-ghost'}`} disabled={busy} onClick={confirm}>{busy ? '…' : '확정'}</button>
                    <button className="btn btn-sm btn-ghost" disabled={busy} onClick={() => { setPending(null); setErr(''); }}>취소</button>
                  </div>
                </div>
              ) : (
                <div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
                  <button className="btn btn-sm btn-primary" onClick={() => start(im.id, 'APPROVAL')}>승인</button>
                  <button className="btn btn-sm btn-danger-ghost" onClick={() => start(im.id, 'REJECT')}>반려</button>
                </div>
              )
            )}
          </div>
        ))}
      </div>
      {err && <div style={{ marginTop: 8 }}><Notice tone="danger">처리하지 못했습니다: {err}</Notice></div>}
    </div>
  );
};

/* ------------------------------------------------------------------ */
/* 사장님 관리 (사장님 계정 목록)                                       */
/* ------------------------------------------------------------------ */
// 서버에는 사업자(상호·사업자등록번호) 조회 API 가 없어 사장님 계정을 나열합니다.
// (merchant_info.business_id 가 유일 키라 사장님 계정 1개 = 사업자 1개)

const Merchants = () => {
  const { merchants } = useMerchantIndex();
  const stores = useLive(() => api.live.stores.list(), []);
  const storeById = React.useMemo(() => new Map((stores.data || []).map((s) => [s.storeId, s])), [stores.data]);
  const [tab, setTab] = React.useState('all');
  const [q, setQ] = React.useState('');
  const [sort, setSort] = React.useState('reg-desc');
  const [page, setPage] = React.useState(1);
  const [selected, setSelected] = React.useState(null);
  React.useEffect(() => { setPage(1); }, [tab, q, sort]);

  const rows = merchants.data || [];
  const ql = q.trim().toLowerCase();
  const filtered = rows
    .filter((c) => (tab === 'all' || c.status === tab))
    .filter((c) => !ql || [c.name, c.loginId, c.email, c.phoneNumber, String(c.merchantId)]
      .concat(c.storeIds.map((sid) => (storeById.get(sid) || {}).name))
      .some((v) => (v || '').toLowerCase().includes(ql)))
    .sort((a, b) => {
      if (sort === 'stores-desc') return b.storeIds.length - a.storeIds.length;
      if (sort === 'login-desc') return String(b.lastLoginDate || '').localeCompare(String(a.lastLoginDate || ''));
      return String(b.regDate || '').localeCompare(String(a.regDate || ''));
    });
  const totalPages = Math.max(1, Math.ceil(filtered.length / LIVE_PAGE_SIZE));
  const safePage = Math.min(page, totalPages);
  const pageItems = filtered.slice((safePage - 1) * LIVE_PAGE_SIZE, safePage * LIVE_PAGE_SIZE);
  const statusesPresent = Object.keys(ACCOUNT_STATUS).filter((k) => rows.some((c) => c.status === k));
  const storeNames = (c) => c.storeIds.map((sid) => (storeById.get(sid) || {}).name || `#${sid}`);

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>사장님 관리</h1>
          <div className="sub">사장님 계정과 연결된 매장을 확인합니다 · 총 {merchants.data ? `${rows.length}명` : '—'}</div>
        </div>
        <div className="actions">
          <button className="btn btn-ghost btn-sm" onClick={() => { merchants.reload(); stores.reload(); }} disabled={merchants.loading}>
            <Icon name="refresh" size={14}/>새로고침
          </button>
        </div>
      </div>

      <div className="table-wrap">
        <Tabs items={[
          { value: 'all', label: '전체', count: rows.length },
          ...statusesPresent.map((k) => ({ value: k, label: ACCOUNT_STATUS[k].label, count: rows.filter((c) => c.status === k).length })),
        ]} value={tab} onChange={setTab}/>
        <div className="table-toolbar">
          <input className="input" placeholder="이름, 로그인 ID, 사장님 번호, 연락처, 매장명" style={{ flex: 1, maxWidth: 340 }} value={q} onChange={(e) => setQ(e.target.value)}/>
          <select className="select" value={sort} onChange={(e) => setSort(e.target.value)}>
            <option value="reg-desc">가입일 ↓</option>
            <option value="login-desc">최근 접속 ↓</option>
            <option value="stores-desc">매장수 ↓</option>
          </select>
        </div>
        <LiveBlock state={merchants} empty={filtered.length === 0} emptyTitle={rows.length ? '조건에 맞는 사장님이 없습니다' : '등록된 사장님이 없습니다'}>
          <table className="table">
            <thead><tr>
              <th>사장님 번호</th><th>이름 / 로그인 ID</th><th>매장</th><th>연락처</th><th>가입일</th><th>최근 접속</th><th>상태</th><th></th>
            </tr></thead>
            <tbody>
              {pageItems.map((c) => (
                <tr key={c.merchantId} onClick={() => setSelected(c)} style={{ cursor: 'pointer' }}>
                  <td className="t-mono">{c.merchantId}</td>
                  <td><div style={{ fontWeight: 600 }}>{c.name}</div><div className="t-mono" style={{ fontSize: 12, color: 'var(--text-3)' }}>{c.loginId}</div></td>
                  <td>
                    {c.storeIds.length === 0 ? <span style={{ color: 'var(--text-3)' }}>없음</span> : (
                      <div>
                        <Badge tone="brand">{c.storeIds.length}개</Badge>{' '}
                        <span style={{ fontSize: 12.5, color: 'var(--text-2)' }}>{storeNames(c).join(', ')}</span>
                      </div>
                    )}
                  </td>
                  <td className="t-mono">{maskPhone(c.phoneNumber)}</td>
                  <td style={{ color: 'var(--text-3)' }}>{ymdOrDash(c.regDate)}</td>
                  <td style={{ color: 'var(--text-3)' }}>{dash(c.lastLoginDate)}</td>
                  <td><StatusBadge map={ACCOUNT_STATUS} value={c.status}/></td>
                  <td><div className="row-actions"><button className="btn btn-sm btn-ghost" onClick={(e) => { e.stopPropagation(); setSelected(c); }}>상세</button></div></td>
                </tr>
              ))}
            </tbody>
          </table>
        </LiveBlock>
        <div className="table-foot">
          <span style={{ fontSize: 13, color: 'var(--text-3)' }}>총 {filtered.length}건</span>
          <Pager total={totalPages} current={safePage} onPick={setPage}/>
        </div>
      </div>

      <Drawer open={!!selected} onClose={() => setSelected(null)} title={selected ? `사장님 ${selected.name}` : ''}
        footer={<button className="btn btn-ghost" onClick={() => setSelected(null)}>닫기</button>}>
        {selected && <MerchantDetail merchant={selected}/>}
      </Drawer>
    </div>
  );
};

const MerchantDetail = ({ merchant: c }) => {
  const stores = useLive(() => api.live.merchants.stores(c.merchantId), [c.merchantId]);
  const list = stores.data || [];
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18 }}>
        <Avatar name={c.name} size={40}/>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 17, fontWeight: 700 }}>{c.name}</div>
          <div className="t-mono" style={{ fontSize: 13, color: 'var(--text-3)' }}>{c.loginId}</div>
        </div>
        <StatusBadge map={ACCOUNT_STATUS} value={c.status}/>
      </div>
      <SectionTitle>계정 정보</SectionTitle>
      <dl className="spec">
        <dt>사장님 번호</dt><dd className="t-mono">{c.merchantId}</dd>
        <dt>로그인 ID</dt><dd className="t-mono">{c.loginId}</dd>
        <dt>이메일</dt><dd className="t-mono">{dash(c.email)}</dd>
        <dt>연락처</dt><dd className="t-mono">{dash(c.phoneNumber)}</dd>
        <dt>가입일</dt><dd>{dash(c.regDate)}</dd>
        <dt>최근 접속</dt><dd>{dash(c.lastLoginDate)}</dd>
      </dl>
      <SectionTitle>사업자 정보</SectionTitle>
      <Notice>상호·사업자등록번호·사업장 주소는 서버가 제공하지 않아 표시하지 않습니다.</Notice>
      <SectionTitle right={stores.data ? `${list.length}개` : ''}>연결된 매장</SectionTitle>
      <LiveBlock state={stores} compact empty={list.length === 0} emptyTitle="연결된 매장이 없습니다">
        <div className="card" style={{ padding: 0 }}>
          {list.map((s, i) => (
            <div key={s.storeId} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', borderBottom: i < list.length - 1 ? '1px solid var(--border)' : 0 }}>
              <LiveThumb label={s.storeName}/>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 600, fontSize: 13.5 }}>{s.storeName}</div>
                <div className="t-mono" style={{ fontSize: 12, color: 'var(--text-3)' }}>매장 ID {s.storeId}</div>
              </div>
              <StatusBadge map={INSPECTION_STATUS} value={s.status}/>
            </div>
          ))}
        </div>
      </LiveBlock>
    </div>
  );
};

/* ------------------------------------------------------------------ */
/* 승인 관리                                                           */
/* ------------------------------------------------------------------ */
// 매장: store.status 로 검수대기·반려·승인을 나눕니다(실서버).
// 메뉴 사진: 매장을 가로지르는 검수대기 목록(/admin/menu/image/search)을 씁니다.
// 사업자: 사장님이 올린 민감 서류(통장 사본·사업자등록증)의 심사대기 목록입니다.
//   **이 탭이 없으면 발견 자체가 안 됩니다** — 계좌를 바꿔 새 통장 사본이 올라와도 매장 상태는
//   바뀌지 않으니 어느 화면에도 뜨지 않고, 아무도 그 사업자를 열어 보지 않으면 사본이 영원히
//   심사대기로 남습니다. 그러면 정산이 조용히 막힙니다(docs/merchant-document.md 2-3).
// 정산계좌: 서버에 승인 개념이 없어 연결하지 않았습니다. 가짜 대기
// 건을 실제 건과 섞어 보여 주면 운영자가 오인할 수 있어 목 데이터도 띄우지 않습니다.

const Approvals = () => {
  const [tab, setTab] = React.useState('store');
  React.useEffect(() => {
    const i = window.__routeIntent;
    if (i && i.route === 'approvals') {
      if (i.tab === 'store' || i.tab === 'menu') setTab(i.tab);
      delete window.__routeIntent;
    }
  }, []);
  const stores = useLive(() => api.live.stores.list(), []);
  const images = useLive(() => api.live.menus.awaitingImages(), []);
  const { merchantsByStore } = useMerchantIndex();
  // Decisions made inside the drawer (store or photo) refresh both lists.
  useInspectionChanged(() => { stores.reload(); images.reload(); });
  const [statusF, setStatusF] = React.useState('AWAIT');
  const [q, setQ] = React.useState('');
  const [page, setPage] = React.useState(1);
  const [drawer, setDrawer] = React.useState(null); // { storeId, tab, intent }
  // 서류 심사대기. 목록은 서버가 걸러 주고(status·keyword) 정렬은 id ASC(오래 기다린 것 먼저)입니다.
  const [docStatus, setDocStatus] = React.useState('SUBMITTED');
  const [docQ, setDocQ] = React.useState('');
  const [docPage, setDocPage] = React.useState(1);
  const [bizDrawer, setBizDrawer] = React.useState(null);   // businessId
  const docs = useLive(
    () => api.live.merchantDocuments.pending({ status: docStatus, keyword: docQ.trim(), page: docPage, size: LIVE_PAGE_SIZE }),
    [docStatus, docQ, docPage],
  );
  // 목록 응답의 두 모양(배열 · {totalCount,list})을 둘 다 읽는 이 저장소 관례를 그대로 씁니다.
  const docRows = listRows(docs.data);
  const docTotal = listTotal(docs.data);
  // 뱃지는 "심사대기"만 셉니다(필터와 무관하게 늘 같은 수를 보여 줘야 하므로 따로 읽습니다).
  const docAwait = useLive(() => api.live.merchantDocuments.pending({ status: 'SUBMITTED', page: 1, size: 1 }), []);
  const docAwaitCount = listTotal(docAwait.data);
  useDocumentChanged(() => { docs.reload(); docAwait.reload(); });
  React.useEffect(() => { setDocPage(1); }, [docStatus, docQ]);
  React.useEffect(() => { setPage(1); }, [tab, statusF, q]);

  const all = stores.data || [];
  const imgs = images.data || [];
  const ql = q.trim().toLowerCase();
  const merchantNames = (sid) => (merchantsByStore.get(sid) || []).map((c) => c.name).join(', ');
  const storeRows = all
    .filter((s) => !statusF || s.status === statusF)
    .filter((s) => !ql || [s.name, merchantNames(s.storeId)].some((v) => (v || '').toLowerCase().includes(ql)))
    .sort((a, b) => String(a.regDate || '').localeCompare(String(b.regDate || ''))); // 오래된 요청 먼저
  const imageRows = imgs
    .filter((r) => !ql || [r.storeName, r.menuName].some((v) => (v || '').toLowerCase().includes(ql)));
  const rows = tab === 'store' ? storeRows : tab === 'menu' ? imageRows : [];
  const totalPages = Math.max(1, Math.ceil(rows.length / LIVE_PAGE_SIZE));
  const safePage = Math.min(page, totalPages);
  const pageItems = rows.slice((safePage - 1) * LIVE_PAGE_SIZE, safePage * LIVE_PAGE_SIZE);
  const awaitStores = all.filter((s) => s.status === 'AWAIT').length;

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>승인 관리</h1>
          <div className="sub">매장 입점과 메뉴 사진 검수 요청을 처리합니다</div>
        </div>
        <div className="actions">
          <button className="btn btn-ghost btn-sm" onClick={() => { stores.reload(); images.reload(); }} disabled={stores.loading}>
            <Icon name="refresh" size={14}/>새로고침
          </button>
        </div>
      </div>

      <div className="stat-grid" style={{ gridTemplateColumns: 'repeat(4,1fr)' }}>
        <MiniStat icon="hourglass" label="매장 검수대기" value={stores.data ? `${awaitStores}건` : '—'} tone="warn"
          onAction={() => { setTab('store'); setStatusF('AWAIT'); }}/>
        <MiniStat icon="image" label="메뉴 사진 검수대기" value={images.data ? `${imgs.length}건` : '—'} tone="warn"
          hint={images.error ? '목록 API 없음' : undefined}
          onAction={images.data ? () => setTab('menu') : undefined}/>
        <MiniStat icon="ban" label="매장 반려" value={stores.data ? `${all.filter((s) => s.status === 'REJECT').length}건` : '—'} tone="danger"
          onAction={() => { setTab('store'); setStatusF('REJECT'); }} actionLabel="보기"/>
        <MiniStat icon="clock" label="처리 이력" value="—" tone="info" hint="서버에 이력 없음"/>
      </div>

      <div className="table-wrap" style={{ marginTop: 14 }}>
        <Tabs items={[
          { value: 'store', label: '매장', count: stores.data ? awaitStores : undefined },
          { value: 'menu', label: '메뉴 사진', count: images.data ? imgs.length : undefined },
          { value: 'biz', label: '사업자 서류', count: docAwaitCount == null ? undefined : docAwaitCount },
          { value: 'bank', label: '정산계좌' },
        ]} value={tab} onChange={setTab}/>

        {tab === 'biz' ? (
          <>
            <div className="table-toolbar">
              <select className="select" style={{ minWidth: 130 }} value={docStatus} onChange={(e) => setDocStatus(e.target.value)}>
                <option value="SUBMITTED">심사대기</option>
                <option value="REJECTED">반려</option>
                <option value="APPROVED">승인</option>
                <option value="REPLACED">교체됨</option>
                <option value="">전체</option>
              </select>
              {/* placeholder 에는 **서버가 실제로 보는 것만** 적습니다(상호·사업자등록번호·가게 이름).
                  파일 이름은 검색되지 않습니다 — 안 되는 것을 적으면 빈 결과가 거짓말이 됩니다. */}
              <input className="input" placeholder="상호, 사업자등록번호, 가게명" style={{ flex: 1, maxWidth: 300 }}
                value={docQ} onChange={(e) => setDocQ(e.target.value)}/>
            </div>
            <LiveBlock state={docs} empty={docRows.length === 0}
              emptyTitle={docStatus === 'SUBMITTED' ? '심사대기 중인 서류가 없습니다' : '조건에 맞는 서류가 없습니다'}>
              <table className="table">
                <thead><tr>
                  <th>서류</th><th>사업자</th><th>파일</th><th>접수일시</th><th>상태</th><th></th>
                </tr></thead>
                <tbody>
                  {docRows.map((r) => (
                    <tr key={r.documentId} onClick={() => setBizDrawer(r.businessId)} style={{ cursor: 'pointer' }}>
                      <td>{docTypeLabel(r.docType)}</td>
                      <td>
                        <div style={{ fontWeight: 600 }}>{r.businessName || `사업자 ${r.businessId}`}</div>
                        {/* storeNamesText 는 쉼표로 이은 **문자열**입니다(상세의 storeNames 는 배열).
                            가게 이름에 쉼표가 있으면 되돌릴 수 없으므로 split 하지 않고 그대로 씁니다. */}
                        <div style={{ fontSize: 12, color: 'var(--text-3)' }}>{r.storeNamesText || EMPTY_VALUE}</div>
                      </td>
                      <td style={{ fontSize: 12.5 }}>{r.originalName || EMPTY_VALUE}</td>
                      <td style={{ color: 'var(--text-3)' }}>{fmtDT(r.uploadedAt)}</td>
                      <td>
                        <StatusBadge map={DOCUMENT_STATUS} value={r.status}/>
                        {docPrevLabel(r.prevStatus) && (
                          <div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 3 }}>{docPrevLabel(r.prevStatus)}</div>
                        )}
                        {r.reviewedAt && (
                          <div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 3 }}>
                            {fmtDT(r.reviewedAt)} · <ReviewerName name={r.reviewedByName} id={r.reviewedBy}/>
                          </div>
                        )}
                      </td>
                      <td><div className="row-actions">
                        <button className="btn btn-sm btn-ghost" onClick={(e) => { e.stopPropagation(); setBizDrawer(r.businessId); }}>서류 보기</button>
                      </div></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </LiveBlock>
            <div className="table-foot">
              <span style={{ fontSize: 13, color: 'var(--text-3)' }}>
                총 {docTotal == null ? EMPTY_VALUE : `${docTotal}건`} · 서류를 열면 누가·언제·왜 열었는지 기록에 남습니다
              </span>
              <Pager total={listLastPage(docs.data, LIVE_PAGE_SIZE) || 1} current={docPage} onPick={setDocPage}/>
            </div>
          </>
        ) : tab === 'bank' ? (
          <Empty title="아직 연결되지 않은 승인 유형입니다"
            desc="서버에 정산계좌 승인 요청·처리 API가 없습니다."/>
        ) : (
          <>
            <div className="table-toolbar">
              {tab === 'store' && (
                <select className="select" style={{ minWidth: 130 }} value={statusF} onChange={(e) => setStatusF(e.target.value)}>
                  <option value="AWAIT">검수대기</option>
                  <option value="REJECT">반려</option>
                  <option value="APPROVAL">승인</option>
                  <option value="">전체</option>
                </select>
              )}
              <input className="input" placeholder={tab === 'store' ? '매장명, 사장님' : '매장명, 메뉴명'} style={{ flex: 1, maxWidth: 280 }} value={q} onChange={(e) => setQ(e.target.value)}/>
            </div>

            {tab === 'store' ? (
              <LiveBlock state={stores} empty={storeRows.length === 0}
                emptyTitle={statusF === 'AWAIT' ? '검수대기 중인 매장이 없습니다' : '조건에 맞는 매장이 없습니다'}>
                <table className="table">
                  <thead><tr>
                    <th>매장 ID</th><th>매장명</th><th>사장님</th><th>등록일시</th><th>상태</th><th></th>
                  </tr></thead>
                  <tbody>
                    {pageItems.map((s) => (
                      <tr key={s.storeId}>
                        <td className="t-mono">{s.storeId}</td>
                        <td>
                          <div style={{ fontWeight: 600 }}>{s.name}</div>
                          <div style={{ fontSize: 12, color: 'var(--text-3)' }}>{dash(addressOf(s))}</div>
                        </td>
                        <td>{merchantNames(s.storeId) || <span style={{ color: 'var(--text-3)' }}>없음</span>}</td>
                        <td style={{ color: 'var(--text-3)' }}>{dash(s.regDate)}</td>
                        <td>
                          <StatusBadge map={INSPECTION_STATUS} value={s.status}/>
                          {s.status === 'REJECT' && s.rejectReason && (
                            <div style={{ fontSize: 11.5, color: 'var(--danger)', marginTop: 3, maxWidth: 220 }}>{s.rejectReason}</div>
                          )}
                        </td>
                        <td>
                          <div className="row-actions">
                            <button className="btn btn-sm btn-ghost" onClick={() => setDrawer({ storeId: s.storeId, tab: 'info', intent: null })}>상세</button>
                            {s.status === 'AWAIT' && <>
                              <button className="btn btn-sm btn-primary" onClick={() => setDrawer({ storeId: s.storeId, tab: 'info', intent: 'approve' })}>승인</button>
                              <button className="btn btn-sm btn-danger-ghost" onClick={() => setDrawer({ storeId: s.storeId, tab: 'info', intent: 'reject' })}>반려</button>
                            </>}
                          </div>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </LiveBlock>
            ) : (
              <LiveBlock state={images} empty={imageRows.length === 0} emptyTitle="검수대기 중인 메뉴 사진이 없습니다">
                {imgs.length >= AWAIT_IMAGE_LIMIT && (
                  <div style={{ padding: '0 16px 10px' }}><Notice>오래 기다린 순으로 {AWAIT_IMAGE_LIMIT}건까지만 불러왔습니다. 더 있을 수 있으니 처리한 뒤 새로고침하세요.</Notice></div>
                )}
                <table className="table">
                  <thead><tr>
                    <th>사진</th><th>메뉴</th><th>매장</th><th>등록일시</th><th>상태</th><th></th>
                  </tr></thead>
                  <tbody>
                    {pageItems.map((r) => (
                      <tr key={r.menuImageId}>
                        <td><LiveThumb url={r.imgUrl} label={r.menuName} size={44}/></td>
                        <td><div style={{ fontWeight: 600 }}>{dash(r.menuName)}</div><div className="t-mono" style={{ fontSize: 12, color: 'var(--text-3)' }}>메뉴 #{r.menuId} · 사진 #{r.menuImageId}</div></td>
                        <td>{dash(r.storeName)}</td>
                        <td style={{ color: 'var(--text-3)' }}>{dash(r.regDate)}</td>
                        <td><StatusBadge map={INSPECTION_STATUS} value={r.status}/></td>
                        <td><div className="row-actions">
                          <button className="btn btn-sm btn-ghost" onClick={() => setDrawer({ storeId: r.storeId, tab: 'menu', intent: null })}>매장 메뉴에서 검수</button>
                        </div></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </LiveBlock>
            )}
            <div className="table-foot">
              <span style={{ fontSize: 13, color: 'var(--text-3)' }}>총 {rows.length}건 · 누가 언제 처리했는지는 서버에 기록되지 않습니다</span>
              <Pager total={totalPages} current={safePage} onPick={setPage}/>
            </div>
          </>
        )}
      </div>

      <StoreDrawer
        storeId={drawer ? drawer.storeId : null}
        initialTab={drawer ? drawer.tab : 'info'}
        initialIntent={drawer ? drawer.intent : null}
        merchants={drawer ? (merchantsByStore.get(drawer.storeId) || []) : []}
        onClose={() => setDrawer(null)}/>

      <BusinessDocumentsDrawer businessId={bizDrawer} onClose={() => setBizDrawer(null)}/>
    </div>
  );
};

// 사업자 하나의 서류를 보는 드로어. **새 라우트를 만들지 않습니다** — 서류는 언제나
// "이 사업자의 것"이라, 사업자를 고르지 않고 서류부터 보는 화면은 있으면 안 됩니다.
const BusinessDocumentsDrawer = ({ businessId, onClose }) => {
  const state = useMerchantDocuments(businessId);
  useDocumentChanged(() => { if (businessId != null) state.reload(); });
  if (businessId == null) return null;
  const biz = (state.data && state.data.business) || null;
  return (
    <Drawer open onClose={onClose} title={biz ? biz.businessName : `사업자 ${businessId}`}
      footer={<button className="btn btn-ghost" onClick={onClose}>닫기</button>}>
      <MerchantDocuments businessId={businessId} state={state} canReview/>
    </Drawer>
  );
};

Object.assign(window, { Merchants, Stores, Approvals, StoreDrawer, LiveBlock, LiveThumb, Notice, StatusBadge, useMerchantIndex, errorText, addressOf, regionOf,
  MerchantDocuments, useMerchantDocuments, BusinessDocumentsDrawer, SectionTitle });
