// 1:1 문의 — REAL server (api.live.counsels, /admin/counsel)
//
// 목록 검색 → 상세 → 답변 등록 / 상태 수동 변경. 서버 필드 이름을 그대로 씁니다.
// 상태 두 갈래:
//   - 답변완료(COMPLETE): 답변 등록 시 서버가 자동으로 만듭니다(WAIT/PROCESS/HOLD → COMPLETE).
//     이 상태는 손으로 찍을 수 없습니다(PUT /status 로 COMPLETE 를 보내면 3162).
//   - 처리중(PROCESS)·보류(HOLD)·대기(WAIT): 운영자가 상세 드로어의 "상태 변경" 버튼으로
//     직접 바꿉니다 → PUT /admin/counsel/{id}/status { csStatus } (yumyumpickup #123, 2026-10-07 배포).
//
// ── 삭제 여부는 상태와 **다른 축**입니다 (yumyumpickup V44 · 계약 합의 2026-10-01)
//
// 손님이 지운 문의를 어드민 목록은 **일부러 그대로 보여 줍니다** — 답변까지 한 문의가 손님의 삭제로
// 운영자 화면에서 사라지면 분쟁 근거가 없어집니다. 거르는 필터는 손님 본인 목록 한 곳에만 있습니다.
//
// 전에는 `cs_status` 한 칸이 진행 상태와 삭제 여부를 **같이** 담아, 한쪽을 쓰면 다른 쪽이 지워졌습니다
// (그게 "어드민 답변이 손님의 삭제를 되살리던" 결함의 뿌리입니다 — yumyumpickup#293·#295).
// V44 가 `is_delete` 를 떼어냈고, 응답도 `isDelete` 를 따로 줍니다.
//   csStatusCode : 진행 상태만 (WAIT · COMPLETE · PROCESS · HOLD)
//   isDelete     : 'Y' | 'N'  — FAQ·쿠폰과 같은 모양입니다
//   replyCount   : 답변 행 수 (목록에도 옵니다)
//
// 🔴 **"답변이 있나"는 `replyCount` 로 봅니다 — 상태로 보면 틀립니다.**
//    dev 문의 1 은 WAIT 인데 답변이 1건 있습니다(답변 등록이 상태를 바꾸기 전에 생긴 행).
//    backend 도 같은 이유로 손님 쪽 수정 제한을 답변 행의 존재로 판단합니다(CounselService 주석).
//
// ⚠️ 라벨에 **순서를 주장하지 않습니다.** "답변 뒤 삭제"와 "삭제 뒤 답변"이 둘 다
//    COMPLETE + isDelete='Y' 로 옵니다 — 스키마에 삭제 시각이 없어 순서가 기록되지 않습니다.
//    줄 수 있는 것은 "삭제됨" × "답변 기록이 있나" 두 사실뿐이고, 그것만 적습니다.

const COUNSEL_PAGE_SIZE = 20;

// 연락처 표시(maskPhone · fmtPhone)는 labels.jsx 에 있습니다.
const csLabel = (map, code, serverValue) => (map[code] || {}).label || serverValue || code || '—';

// 서버가 isDelete 를 주지 않을 때를 대비한 길입니다. 옛 응답은 지워진 문의를
// csStatusCode='DELETE' 로 합성해 줬습니다.
//
// 🔴 **지우지 마세요. 죽은 코드가 아닙니다.** 2026-10-01 에 서버가 isDelete 를 주기 시작해서
//    평소에는 이 갈래가 돌지 않습니다. 도는 때는 둘입니다:
//      - 서버를 그 이전으로 **되돌릴** 때(합성이 돌아옵니다)
//      - 화면이 서버보다 먼저 배포될 때(그때처럼)
//    없으면 그 상황에서 "삭제" 행이 아무 표시 없이 보통 행처럼 보입니다 — 운영자가 손님이 지운
//    문의를 모르고 답하게 됩니다.
const hasDeleteField = (c) => !!c && c.isDelete != null;
const csDeleted = (c) => (hasDeleteField(c) ? c.isDelete === 'Y' : !!c && c.csStatusCode === 'DELETE');
// 진행 상태. 옛 응답에서는 지워진 문의의 진행 상태를 **알 수 없습니다**(DELETE 로 덮여 옵니다) → null.
// null 일 때 화면은 진행 상태를 그리지 않습니다. 모르는 것을 지어내지 않습니다.
const csProgress = (c) => {
  if (!c) return null;
  if (hasDeleteField(c)) return c.csStatusCode;
  return c.csStatusCode === 'DELETE' ? null : c.csStatusCode;
};
// 답변 수. 목록은 replyCount, 상세는 replies[] 를 씁니다. 없으면 null — "0건"이라고 하지 않습니다.
const csReplyCount = (c) => {
  if (!c) return null;
  if (Array.isArray(c.replies)) return c.replies.length;
  return typeof c.replyCount === 'number' ? c.replyCount : null;
};

const SEARCH_FIELDS = [
  { value: 'email',       label: '이메일' },
  { value: 'phoneNumber', label: '연락처' },
  { value: 'orderId',     label: '주문번호' },
];

// 삭제 여부 탭 — 서버 isDelete 파라미터 그대로입니다. FAQ(page-rest.jsx)와 같은 모양이라
// 운영자가 같은 조작을 배웁니다. **기본은 '전체'** 입니다(FAQ 는 '사용중'이 기본 — 반대입니다):
// 문의는 지운 것도 보여 주는 게 결정이라, 기본으로 거르면 지금 보이는 행이 조용히 사라집니다.
const CS_DELETE_TABS = [
  { value: '',  label: '전체' },
  { value: 'N', label: '삭제 아님' },
  { value: 'Y', label: '삭제됨' },
];

const EMPTY_FILTER = { field: 'email', keyword: '', csStatus: '', csCategory: '', isDelete: '', regDateFrom: '', regDateTo: '' };

const Counsels = () => {
  // draft = 입력 중인 값, applied = 서버에 보낸 값 (검색 버튼/Enter 로 반영)
  const [draft, setDraft] = React.useState(EMPTY_FILTER);
  const [applied, setApplied] = React.useState(EMPTY_FILTER);
  const [page, setPage] = React.useState(1);
  const [selected, setSelected] = React.useState(null); // userCounselId
  const [formError, setFormError] = React.useState('');

  const query = React.useMemo(() => {
    const q = { page, size: COUNSEL_PAGE_SIZE };
    const kw = applied.keyword.trim();
    if (kw) q[applied.field] = applied.field === 'phoneNumber' ? kw.replace(/\D/g, '') : kw;
    if (applied.csStatus) q.csStatus = applied.csStatus;
    if (applied.csCategory) q.csCategory = applied.csCategory;
    // 안 보내면 서버가 전부 줍니다(= ALL). 지금 보이는 행을 하나도 숨기지 않습니다.
    if (applied.isDelete) q.isDelete = applied.isDelete;
    if (applied.regDateFrom) q.regDateFrom = applied.regDateFrom;
    if (applied.regDateTo) q.regDateTo = applied.regDateTo;
    return q;
  }, [applied, page]);

  const list = useLive(() => api.live.counsels.search(query), [JSON.stringify(query)]);
  useCounselChanged(list.reload);
  const rows = listRows(list.data);
  const lastPage = listLastPage(list.data, COUNSEL_PAGE_SIZE);
  const total = listTotal(list.data);
  const hasNext = lastPage == null ? rows.length === COUNSEL_PAGE_SIZE : page < lastPage;

  const set = (k, v) => setDraft((d) => ({ ...d, [k]: v }));
  const apply = (next = draft) => {
    const kw = next.keyword.trim();
    if (next.field === 'orderId' && kw && !/^\d+$/.test(kw)) { setFormError('주문번호는 숫자만 입력하세요.'); return; }
    if (next.regDateFrom && next.regDateTo && next.regDateFrom > next.regDateTo) { setFormError('시작일이 종료일보다 늦습니다.'); return; }
    setFormError('');
    setApplied(next);
    setPage(1);
  };
  // 셀렉트는 바로 반영
  const setAndApply = (k, v) => { const next = { ...draft, [k]: v }; setDraft(next); apply(next); };
  const reset = () => { setDraft(EMPTY_FILTER); apply(EMPTY_FILTER); };

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <h1>1:1 문의</h1>
          {/* 답변 등록이 상태를 어떻게 바꾸는지는 **행마다 다르므로** 드로어가 말합니다.
              여기서는 목록에 지운 문의가 섞여 있다는 사실만 적습니다 — 운영자가 "삭제" 뱃지를
              보고 "왜 여기 있나" 하지 않게. */}
          <div className="sub">사용자 문의를 확인하고 답변합니다 · 사용자가 지운 문의도 운영자에게는 그대로 보입니다</div>
        </div>
        <div className="actions">
          <button className="btn btn-ghost btn-sm" onClick={list.reload} disabled={list.loading}>
            <Icon name="refresh" size={14}/>새로고침
          </button>
        </div>
      </div>

      <div className="table-wrap" data-loading={list.loading ? 'true' : 'false'}>
        {/* 🔴 상태 탭에서 DELETE 를 **뺍니다.** 서버가 CsStatusEnum.DELETE 를 걷으면
            csStatus=DELETE 로 검색하는 순간 2103("존재하지 않는 문의 상태")이 납니다.
            빼도 운영자가 잃는 것은 없습니다 — 지운 문의는 목록에 계속 보이고, 골라내는 일은
            아래 "삭제 여부" 탭이 대신합니다. */}
        <Tabs items={[
          { value: '', label: '전체' },
          ...['WAIT', 'COMPLETE', 'PROCESS', 'HOLD'].map((k) => ({ value: k, label: CS_STATUS[k].label })),
        ]} value={draft.csStatus} onChange={(v) => setAndApply('csStatus', v)}/>
        {/* 서버가 isDelete 를 주기 전에는 **이 줄을 그리지 않습니다.** 눌러도 아무 일이 없는 탭은
            운영자가 "고장 났나" 하고 멈춥니다. 그 동안에도 목록에는 지운 문의가 다 보이므로
            정보를 잃지는 않습니다(필터만 없습니다). 첫 행으로 판단합니다 — 서버가 주기 시작하면
            모든 행에 옵니다. */}
        {rows.length > 0 && hasDeleteField(rows[0]) && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '0 16px', flexWrap: 'wrap' }}>
            <span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>삭제 여부</span>
            <Tabs items={CS_DELETE_TABS} value={draft.isDelete} onChange={(v) => setAndApply('isDelete', v)}/>
          </div>
        )}
        <div className="table-toolbar" style={{ flexWrap: 'wrap' }}>
          <select className="select" value={draft.field} onChange={(e) => set('field', e.target.value)} aria-label="검색 항목">
            {SEARCH_FIELDS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}
          </select>
          <input className="input" style={{ flex: 1, minWidth: 200, maxWidth: 300 }}
            placeholder={draft.field === 'orderId' ? '주문번호 (숫자)' : draft.field === 'phoneNumber' ? '연락처 (숫자만 비교)' : '이메일'}
            value={draft.keyword} onChange={(e) => set('keyword', e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') apply(); }}/>
          <select className="select" value={draft.csCategory} onChange={(e) => setAndApply('csCategory', e.target.value)} aria-label="문의 유형">
            <option value="">전체 유형</option>
            {Object.entries(CS_CATEGORY).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
          </select>
          <input className="input" type="date" value={draft.regDateFrom} onChange={(e) => set('regDateFrom', e.target.value)} aria-label="등록일 시작"/>
          <span style={{ color: 'var(--text-3)' }}>~</span>
          <input className="input" type="date" value={draft.regDateTo} onChange={(e) => set('regDateTo', e.target.value)} aria-label="등록일 끝"/>
          <button className="btn btn-primary btn-sm" onClick={() => apply()}><Icon name="search" size={14}/>검색</button>
          <button className="btn btn-ghost btn-sm" onClick={reset}>초기화</button>
        </div>
        {formError && <div style={{ padding: '0 16px 10px' }}><Notice tone="danger">{formError}</Notice></div>}
        {/* 새 조건으로 불러오는 동안 이전 결과를 흐리게 두고 알림 — 이전 결과를 새 결과로 오인하지 않게 */}
        {list.loading && list.data && (
          <div style={{ padding: '0 16px 10px', fontSize: 12.5, color: 'var(--text-3)' }}>새 조건으로 불러오는 중… 아래는 이전 결과입니다.</div>
        )}

        <LiveBlock state={list} empty={rows.length === 0}
          emptyTitle={page > 1 ? '이 페이지에는 문의가 없습니다' : '조건에 맞는 문의가 없습니다'}>
          <table className="table" style={{ opacity: list.loading ? 0.45 : 1, transition: 'opacity 0.15s' }}>
            <thead><tr>
              <th>문의 번호</th><th>유형</th><th>제목</th><th>사용자</th><th>주문번호</th><th>등록일시</th><th>상태</th>
            </tr></thead>
            <tbody>
              {rows.map((c) => (
                <tr key={c.userCounselId} onClick={() => setSelected(c.userCounselId)} style={{ cursor: 'pointer' }}>
                  <td className="t-mono">{c.userCounselId}</td>
                  <td style={{ whiteSpace: 'nowrap' }}>{csLabel(CS_CATEGORY, c.csCategoryCode, c.csCategoryValue)}</td>
                  <td style={{ maxWidth: 360 }}>
                    <div style={{ fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.title || '(제목 없음)'}</div>
                    <div style={{ fontSize: 12, color: 'var(--text-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.content}</div>
                  </td>
                  <td>
                    <div style={{ fontSize: 13 }}>{maskEmail(c.email)}</div>
                    <div className="t-mono" style={{ fontSize: 12, color: 'var(--text-3)' }}>#{c.uid} · {maskPhone(c.phoneNumber)}</div>
                  </td>
                  <td className="t-mono">{c.orderId ?? '—'}</td>
                  <td style={{ color: 'var(--text-3)', whiteSpace: 'nowrap' }}>{fmtDT(c.regDate)}</td>
                  {/* 진행 상태와 삭제 여부는 **다른 축**이라 뱃지를 따로 둡니다. 한 라벨로 합치면
                      조합마다 새 라벨을 만들게 되고, 그게 이 결함의 뿌리였습니다(한 칸에 두 가지). */}
                  <td style={{ whiteSpace: 'nowrap' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
                      {csProgress(c) ? <StatusBadge map={CS_STATUS} value={csProgress(c)}/> : null}
                      {csDeleted(c) && <Badge tone="neutral">삭제</Badge>}
                    </div>
                    {/* 답변 수는 지워진 행(분쟁 근거가 있나)과 "WAIT 인데 답변이 있는" 행에서
                        쓸모가 있습니다. 서버가 replyCount 를 주기 전에는 그리지 않습니다. */}
                    {csReplyCount(c) != null && (csDeleted(c) || csReplyCount(c) > 0) && (
                      <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}>답변 {csReplyCount(c)}건</div>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </LiveBlock>
        <div className="table-foot">
          <span style={{ fontSize: 13, color: 'var(--text-3)' }}>
            {total == null
              ? `${page}페이지 · ${rows.length}건 (페이지당 ${COUNSEL_PAGE_SIZE}건, 서버가 전체 건수를 주지 않습니다)`
              : `총 ${fmtNum(total)}건 · ${page}/${lastPage}페이지`}
          </span>
          <div style={{ display: 'flex', gap: 6 }}>
            <button className="btn btn-ghost btn-sm" disabled={page <= 1 || list.loading} onClick={() => setPage((p) => p - 1)}>
              <Icon name="chevLeft" size={14}/>이전
            </button>
            <button className="btn btn-ghost btn-sm" disabled={!hasNext || list.loading} onClick={() => setPage((p) => p + 1)}>
              다음<Icon name="chevRight" size={14}/>
            </button>
          </div>
        </div>
      </div>

      <CounselDrawer userCounselId={selected} onClose={() => setSelected(null)}/>
    </div>
  );
};

const CounselDrawer = ({ userCounselId, onClose }) => {
  const open = userCounselId != null;
  const detail = useLive(() => (open ? api.live.counsels.detail(userCounselId) : null), [userCounselId]);
  const [content, setContent] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const [done, setDone] = React.useState('');
  // 상태 수동 변경(처리중·보류·대기)은 답변 등록과 **별개 조작**이라 상태를 따로 둡니다.
  const [statusBusy, setStatusBusy] = React.useState(false);
  const [statusError, setStatusError] = React.useState('');
  const [statusDone, setStatusDone] = React.useState('');
  // 연락처는 기본 마스킹(목록과 같은 규칙). 운영자가 되묻기 전화를 할 때만 '전체 보기'로 펼칩니다.
  // 다른 문의로 바뀌면 다시 가린 상태로 되돌립니다.
  const [showPhone, setShowPhone] = React.useState(false);
  React.useEffect(() => {
    setContent(''); setBusy(false); setError(''); setDone('');
    setStatusBusy(false); setStatusError(''); setStatusDone('');
    setShowPhone(false);
  }, [userCounselId]);
  if (!open) return null;

  const c = detail.data;
  // 없는 문의는 오류가 아니라 모든 필드가 null 인 객체로 옵니다.
  const found = !!(c && c.userCounselId != null);
  const replies = (found && c.replies) || [];
  // 손님이 지운 문의. 답변은 등록되고 기록으로 남지만 **손님은 보지 못합니다**.
  // isDelete 로 봅니다 — 서버가 아직 안 주면 옛 모양(csStatusCode='DELETE')으로 떨어집니다.
  // submit 과 렌더 양쪽에서 씁니다.
  const deleted = found && csDeleted(c);
  // 진행 상태. 옛 응답에서 지워진 문의는 DELETE 로 덮여 와 **알 수 없습니다** → null.
  const progress = found ? csProgress(c) : null;

  const submit = async () => {
    const text = content.trim();
    if (!text) return;
    // 🔴 "첫 답변인가"는 **답변 행 수**로 봅니다 — 상태로 보면 틀립니다.
    //    dev 문의 1 은 WAIT(답변대기)인데 답변이 1개 달려 있습니다(답변 등록이 COMPLETE 로
    //    바꾸는 처리가 들어오기 전에 만들어진 행. 2026-09-30 dev 실측). 상태로 판단하면 그
    //    문의에 "등록하면 답변완료로 바뀝니다" = 첫 답변이라고 말해 버립니다.
    //    backend 도 같은 이유로 손님 쪽 문의 수정 제한을 상태가 아니라 답변 행으로 판단합니다
    //    (CounselService.modifyUserCounsel 주석).
    const firstReply = replies.length === 0;
    // 상태가 바뀌는지는 **따로** 봅니다.
    // 🔴 V44(yumyumpickup#295) 뒤로 서버는 **삭제 여부와 무관하게** COMPLETE 로 바꿉니다
    //    (CounselService.saveUserCounselReply — 가드가 없어졌습니다. is_delete 가 따로 있어서
    //    상태를 써도 손님의 삭제에 닿지 않기 때문입니다). 그래서 어제 여기 있던
    //    "삭제라 상태가 바뀌지 않습니다" 는 **이제 거짓입니다.**
    //    삭제 표시(isDelete)는 답변해도 그대로 남습니다 — 그쪽이 손님에게 중요한 사실입니다.
    // 진행 상태를 모르는 동안(옛 응답 + 지워진 행)에는 바뀐다고도 안 바뀐다고도 하지 않습니다.
    const willComplete = progress != null && progress !== 'COMPLETE';
    const nowLabel = csLabel(CS_STATUS, progress, c.csStatusValue);
    const msg = [
      firstReply ? '답변을 등록할까요?' : `답변을 추가로 등록할까요? (이미 ${replies.length}개 등록돼 있습니다)`,
      // 지운 문의에서는 "사용자에게 보입니다" 가 거짓입니다 — 아래 DELETE 줄과 정면으로 모순됩니다.
      deleted ? '등록한 답변은 이 화면에서 고치거나 지울 수 없습니다.'
              : '등록하면 사용자에게 보입니다. 등록한 답변은 이 화면에서 고치거나 지울 수 없습니다.',
      // 손님이 지운 문의. 이 자리의 문구는 두 번 거짓이 됐다가 고쳐졌습니다 — 서버 동작이 바뀔 때마다
      // 같이 손봐야 하는 자리입니다.
      //   #293 전 : 답변이 상태를 덮어 지운 문의가 손님 목록에 되살아났습니다 → "다시 보이게 됩니다"
      //   #293 후 : 되살아나지 않습니다 → 그 문구가 거짓이 됐습니다
      //   #295 후 : 상태는 COMPLETE 로 바뀌지만 **삭제 표시는 남습니다** → "상태는 삭제로 남습니다" 도 거짓
      // 지금 참인 것만 적습니다: 답변은 기록으로 남고, 손님은 보지 못하고, 삭제 표시는 유지됩니다.
      deleted
        ? '\u26a0 사용자가 삭제한 문의입니다. 답변은 기록으로 남아 이 화면에서 계속 볼 수 있습니다(분쟁 근거).\n다만 사용자는 이 답변을 보지 못하고, "삭제" 표시도 그대로 남습니다.'
        : willComplete ? `문의 상태가 "${nowLabel}"에서 "답변완료"로 바뀝니다.` : '',
    ].filter(Boolean).join('\n');
    if (!window.confirm(msg)) return;
    setBusy(true); setError(''); setDone('');
    try {
      await api.live.counsels.reply(userCounselId, text);
      setContent('');
      setDone('답변을 등록했습니다.');
      detail.reload();
    } catch (err) {
      setError(errorText(err));
    } finally {
      setBusy(false);
    }
  };

  // 운영자가 손으로 바꿀 수 있는 상태만. 🔴 COMPLETE(답변완료)는 뺍니다 — 서버가 이 API 로
  // COMPLETE 를 받으면 3162 로 거절합니다. 답변완료는 답변 등록이 답변을 실제로 넣은 뒤에만
  // 자동으로 됩니다(답변 0건인데 COMPLETE 로 찍으면 목록의 replyCount 와 어긋나므로).
  const changeStatus = async (next) => {
    if (next === progress) return; // 이미 그 상태
    const nextLabel = csLabel(CS_STATUS, next, null);
    const nowLabel = progress ? csLabel(CS_STATUS, progress, c.csStatusValue) : null;
    const msg = nowLabel
      ? `문의 상태를 "${nowLabel}"에서 "${nextLabel}"(으)로 바꿀까요?`
      : `문의 상태를 "${nextLabel}"(으)로 바꿀까요?`;
    if (!window.confirm(msg)) return;
    setStatusBusy(true); setStatusError(''); setStatusDone('');
    try {
      await api.live.counsels.setStatus(userCounselId, next);
      setStatusDone(`상태를 "${nextLabel}"(으)로 바꿨습니다.`);
      // 성공 응답만 믿지 않고 서버 값을 다시 읽어 그립니다. 목록은 COUNSEL_EVENT 로 갱신됩니다.
      detail.reload();
    } catch (err) {
      setStatusError(errorText(err));
    } finally {
      setStatusBusy(false);
    }
  };

  const footer = found ? <>
    <span style={{ marginRight: 'auto', fontSize: 12.5, color: 'var(--text-3)' }}>{content.trim().length}자</span>
    <button className="btn btn-ghost" onClick={onClose} disabled={busy}>닫기</button>
    <button className="btn btn-primary" onClick={submit} disabled={busy || !content.trim()}
      title={content.trim() ? '' : '답변 내용을 입력하세요'}>
      <Icon name="send" size={14}/>{busy ? '등록 중…' : '답변 등록'}
    </button>
  </> : <button className="btn btn-ghost" onClick={onClose}>닫기</button>;

  return (
    <Drawer open onClose={onClose} title={`1:1 문의 ${userCounselId}`} footer={footer}>
      <div data-loading={detail.loading ? 'true' : 'false'}>
      {detail.loading && detail.data && (
        <div style={{ marginBottom: 10, fontSize: 12.5, color: 'var(--text-3)' }}>최신 내용을 불러오는 중…</div>
      )}
      <LiveBlock state={detail} empty={!found} emptyTitle="문의를 찾을 수 없습니다" emptyDesc="삭제됐거나 없는 문의 번호입니다.">
        {found && (
          <div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
              {progress ? <StatusBadge map={CS_STATUS} value={progress}/> : null}
              {deleted && <Badge tone="neutral">삭제</Badge>}
              <Badge tone="neutral">{csLabel(CS_CATEGORY, c.csCategoryCode, c.csCategoryValue)}</Badge>
              <span style={{ marginLeft: 'auto', fontSize: 13, color: 'var(--text-3)' }}>{fmtDT(c.regDate)} 등록</span>
            </div>

            <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 8 }}>{c.title || '(제목 없음)'}</div>
            <div className="card" style={{ padding: 14, whiteSpace: 'pre-wrap', lineHeight: 1.6, fontSize: 13.5 }}>{c.content || '(내용 없음)'}</div>

            {/* 상태 수동 변경 — 처리중·보류·대기. 🔴 답변완료(COMPLETE)는 버튼에 없습니다:
                답변완료는 답변 등록으로만 됩니다(서버가 이 API 로 COMPLETE 를 받으면 3162).
                되돌릴 수 있는 조작이지만 상태를 바꾸므로 누르면 확인창을 띄웁니다. */}
            <SectionTitle>상태 변경</SectionTitle>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              {['WAIT', 'PROCESS', 'HOLD'].map((k) => {
                const isNow = progress === k;
                return (
                  <button key={k}
                    className={`btn btn-sm ${isNow ? 'btn-primary' : 'btn-ghost'}`}
                    disabled={statusBusy || isNow}
                    title={isNow ? '현재 상태입니다' : `"${CS_STATUS[k].label}"(으)로 변경`}
                    onClick={() => changeStatus(k)}>
                    {CS_STATUS[k].label}{isNow ? ' (현재)' : ''}
                  </button>
                );
              })}
              {statusBusy && <span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>변경 중…</span>}
            </div>
            {/* 답변완료는 왜 버튼이 없는지 운영자에게 한 줄로 알립니다 — "왜 없지?" 를 막습니다. */}
            <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 6 }}>
              답변완료는 손으로 바꿀 수 없습니다 — 답변을 등록하면 자동으로 답변완료가 됩니다.
            </div>
            {statusError && <div style={{ marginTop: 8 }}><Notice tone="danger">상태를 바꾸지 못했습니다: {statusError}</Notice></div>}
            {statusDone && <div style={{ marginTop: 8, fontSize: 13, color: 'var(--success)', fontWeight: 600 }}>{statusDone}</div>}

            <SectionTitle>문의한 사용자</SectionTitle>
            <dl className="spec">
              <dt>사용자 번호</dt><dd className="t-mono">{c.uid ?? '—'}</dd>
              <dt>이메일</dt><dd className="t-mono">{c.email || '—'}</dd>
              <dt>연락처</dt>
              <dd className="t-mono">
                {(() => {
                  // 목록과 같은 마스킹(maskPhone)을 기본으로 보여 주고, 실제 전화번호일 때만
                  // '전체 보기' 토글을 답니다. 빈 값·'none'·탈퇴 자리표시(withdrawn-…)는
                  // maskPhone 과 fmtPhone 결과가 같아 가릴 것이 없으므로 토글을 숨깁니다.
                  const masked = maskPhone(c.phoneNumber);
                  const full = fmtPhone(c.phoneNumber);
                  const canReveal = masked !== full;
                  return (
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                      <span>{canReveal ? (showPhone ? full : masked) : full}</span>
                      {canReveal && (
                        <button type="button"
                          className="btn btn-sm btn-ghost"
                          style={{ fontFamily: 'var(--font-sans, inherit)' }}
                          onClick={() => setShowPhone((v) => !v)}
                          aria-pressed={showPhone}
                          title={showPhone ? '연락처를 다시 가립니다' : '연락처 전체를 봅니다'}>
                          {showPhone ? '가리기' : '전체 보기'}
                        </button>
                      )}
                    </span>
                  );
                })()}
              </dd>
              <dt>주문번호</dt><dd className="t-mono">{c.orderId ?? '주문과 연결되지 않은 문의'}</dd>
            </dl>

            <SectionTitle right={`${replies.length}개`}>답변</SectionTitle>
            {replies.length === 0 ? (
              <LiveBlock state={{}} empty compact emptyTitle="아직 답변이 없습니다"/>
            ) : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                {replies.map((r) => (
                  <div key={r.replyId} style={{ paddingLeft: 14, borderLeft: '2px solid var(--border)' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text-3)' }}>
                      <Icon name="cornerDownRight" size={13}/>
                      {/* 답변 운영자 이름(replyAdminName). 서버가 null 로 주면(관리자 행을 못 찾음)
                          "운영자" 로 폴백합니다 — aid 를 이름처럼 쓰지 않습니다. aid 는 식별용으로 옆에 둡니다. */}
                      <span style={{ fontWeight: 600, color: 'var(--text-2)' }}>{r.replyAdminName || '운영자'}</span>
                      <span className="t-mono" style={{ color: 'var(--text-3)' }}>#{r.aid}</span>
                      <span>{fmtDT(r.regDate)}</span>
                    </div>
                    <div style={{ whiteSpace: 'pre-wrap', fontSize: 13.5, lineHeight: 1.6, marginTop: 4 }}>{r.content}</div>
                  </div>
                ))}
              </div>
            )}

            <SectionTitle>답변 작성</SectionTitle>
            {/* 지운 문의라는 사실은 **쓰기 전에** 보여야 합니다. 확인창은 다 쓰고 누른 뒤에 뜨는데,
                그때는 이미 사용자가 읽을 것을 전제로 글을 쓴 상태입니다. */}
            {deleted && (
              <div style={{ marginBottom: 8 }}>
                <Notice>
                  <strong>답변해 주세요 — 기록으로 남습니다.</strong> 사용자가 지운 문의라도 답변은 이 화면에
                  그대로 남아, 나중에 분쟁이 생기면 회사가 무엇을 답했는지 보여 주는 근거가 됩니다.
                  {' '}다만 <strong>사용자는 이 답변을 보지 못하고</strong>(사용자가 지운 문의를 되살리지 않습니다),
                  목록의 "삭제" 표시도 그대로 남습니다. 환불처럼 실제 처리가 필요한 사안이면 답변과 별개로 다른 경로로 연락하세요.
                </Notice>
              </div>
            )}
            <textarea className="input" style={{ width: '100%', height: 120, padding: 10, resize: 'vertical' }}
              placeholder={deleted ? '기록으로 남길 답변을 입력하세요 (사용자에게는 보이지 않습니다)' : '사용자에게 보낼 답변을 입력하세요'}
              value={content} disabled={busy}
              onChange={(e) => setContent(e.target.value)}/>
            <div style={{ marginTop: 8 }}>
              {/* 진행 상태를 모르는 동안(옛 응답 + 지워진 행)에는 상태 이야기를 하지 않습니다.
                  처리중·보류·대기는 위 "상태 변경" 버튼으로 바꿉니다(답변완료는 답변 등록으로만). */}
              <Notice>{progress == null
                ? '답변을 등록하면 서버가 상태를 "답변완료"로 바꿉니다.'
                : deleted
                  ? '답변을 등록하면 서버가 상태를 "답변완료"로 바꿉니다. "삭제" 표시는 그대로 남습니다.'
                  : '답변을 등록하면 서버가 상태를 "답변완료"로 바꿉니다. 처리중·보류·대기는 위 "상태 변경"에서 바꾸세요.'}</Notice>
            </div>
            {error && <div style={{ marginTop: 8 }}><Notice tone="danger">답변을 등록하지 못했습니다: {error}</Notice></div>}
            {done && <div style={{ marginTop: 8, fontSize: 13, color: 'var(--success)', fontWeight: 600 }}>{done}</div>}
          </div>
        )}
      </LiveBlock>
      </div>
    </Drawer>
  );
};

Object.assign(window, { Counsels });
