// Activity views (errors, chats, token usage, exchanges)

// ═══════════════════════════════════════════════════════════════
// Error Report Drawer (slides in from right)
// ═══════════════════════════════════════════════════════════════
const ErrorReportDrawer = ({ open, row, onClose, onResolve, isResolved }) => {
  const [lightbox, setLightbox] = React.useState(false);
  const [apiDetail, setApiDetail] = React.useState(null);
  React.useEffect(() => {
    if (!open || !row) { setApiDetail(null); return; }
    fetch('/admin/bug-reports/' + encodeURIComponent(row.id), { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(setApiDetail)
      .catch(() => setApiDetail(null));
  }, [open, row && row.id]);
  const [showFull, setShowFull] = React.useState(false);
  if (!open || !row) return null;
  // 원본 구조: qa_pairs = [{ question, segments: [{ text, is_error }, ...] }, ...]
  // preview에선 질문 1회 + segments를 그룹핑해 렌더 (질문 반복 금지)
  const pairs = apiDetail && Array.isArray(apiDetail.qa_pairs)
    ? apiDetail.qa_pairs.map(p => ({
        question: p.question || '',
        segments: Array.isArray(p.segments) && p.segments.length > 0
          ? p.segments.map(s => ({ text: s.text || '', is_error: !!s.is_error }))
          : [{ text: p.answer || '', is_error: !!p.is_error }],
      }))
    : [];
  const detail = apiDetail ? {
    comment: apiDetail.description || '',
    pairs,
    hasImage: !!apiDetail.has_image,
  } : { comment: row.content, pairs: [], hasImage: false };
  // 원본 showBugDetail 재현: 오류 체크된 부분 (is_error=true) 모아서 상단 강조
  const errorItems = [];
  pairs.forEach(p => p.segments.forEach(s => { if (s.is_error) errorItems.push({ question: p.question, text: s.text }); }));
  const hasErrors = errorItems.length > 0;
  const totalSegCount = pairs.reduce((acc, p) => acc + p.segments.length, 0);
  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 95, display: 'flex',
      background: 'rgba(15,23,42,0.3)', animation: 'tslFadeIn 0.14s ease',
    }} onClick={onClose}>
      <div style={{ flex: 1 }} />
      <div onClick={e => e.stopPropagation()} style={{
        width: 620, maxWidth: '100%', height: '100%', background: '#fff',
        boxShadow: '-16px 0 40px rgba(15,23,42,0.14)',
        display: 'flex', flexDirection: 'column',
        animation: 'tslSlideIn 0.22s cubic-bezier(0.2, 0.9, 0.3, 1)',
      }}>
        <div style={{
          padding: '16px 20px', borderBottom: '1px solid var(--line)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            {row._seq !== undefined && (
              <div style={{
                display: 'inline-flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
                minWidth: 44, padding: '5px 10px', borderRadius: 8,
                background: 'var(--accent-soft)', color: 'var(--accent-ink)',
                flexShrink: 0, lineHeight: 1.15,
              }}>
                <span style={{ fontSize: 10, fontWeight: 600, letterSpacing: '0.04em' }}>순번</span>
                <span style={{ fontSize: 15, fontWeight: 700, fontFamily: 'var(--mono)', marginTop: 2 }}>{row._seq}</span>
              </div>
            )}
            <div>
              {/* 지역을 왼쪽·굵게, 이름/전화를 부가 정보로 */}
              <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--ink)' }}>
                {row.region}
                <span style={{ fontWeight: 400, color: 'var(--ink-3)', fontSize: 13, marginLeft: 8, fontFamily: 'var(--mono)' }}>
                  {row.phone}
                </span>
              </div>
              <div style={{ fontSize: 11.5, color: 'var(--ink-4)', marginTop: 2, fontFamily: 'var(--mono)' }}>
                {row.time}
              </div>
            </div>
          </div>
          <button onClick={onClose} style={{
            border: 'none', background: 'transparent', cursor: 'pointer',
            color: 'var(--ink-3)', padding: 6, borderRadius: 6,
          }}><Icon name="close" size={18} /></button>
        </div>

        <div style={{ padding: '16px 20px', overflowY: 'auto', flex: 1, background: 'var(--surface-2)' }}>
          {/* 보고자 코멘트 */}
          <div style={{
            background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
            padding: 14, marginBottom: 16,
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
              <Icon name="mail" size={13} stroke={2} />
              <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-2)' }}>보고자 메모</span>
              <span style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--ink-4)', marginLeft: 'auto' }}>
                {row.time}
              </span>
            </div>
            <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.6 }}>
              {detail.comment}
            </div>
            {detail.hasImage && (
              <button onClick={() => setLightbox(true)} style={{
                marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 6,
                padding: '6px 10px', borderRadius: 8, border: '1px solid var(--line-strong)',
                background: '#fff', cursor: 'pointer', fontFamily: 'inherit', fontSize: 12,
                color: 'var(--ink-2)',
              }}>
                <Icon name="image" size={13} /> 첨부 이미지 (1)
              </button>
            )}
          </div>

          {/* 원본: 오류 체크된 부분 (is_error=true 세그먼트) 상단 강조 섹션 */}
          {hasErrors && (
            <div style={{ marginBottom: 16 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 600, color: '#ef4444', marginBottom: 8 }}>
                <Icon name="warning" size={14} /> 오류 체크된 부분 ({errorItems.length}건)
              </div>
              {errorItems.map((item, idx) => (
                <div key={'err-' + idx} style={{ border: '2px solid #ef4444', borderRadius: 10, padding: 14, marginBottom: 8, background: '#fef2f2' }}>
                  {item.question && (
                    <div style={{ fontSize: 12, color: '#64748b', marginBottom: 8, padding: '8px 10px', background: '#fff', borderRadius: 6, border: '1px solid #e2e8f0' }}>
                      <Icon name="user" size={12} style={{ marginRight: 4, color: '#94a3b8' }} />
                      <strong> 질문:</strong> {item.question}
                    </div>
                  )}
                  <div style={{ fontSize: 13, color: 'var(--ink)', lineHeight: 1.7, padding: '8px 10px', background: '#fff', borderRadius: 6, border: '1px solid #fecaca' }}>
                    <Icon name="chat" size={12} style={{ marginRight: 4, color: '#ef4444' }} />
                    <strong> 답변:</strong>
                    <div style={{ marginTop: 4, whiteSpace: 'pre-wrap' }} dangerouslySetInnerHTML={{ __html: formatSegText(item.text) }} />
                  </div>
                </div>
              ))}
            </div>
          )}

          {/* 전체 대화 내용 — 원본: 질문 1개 + 답변 segment 여러개 쌓기, 오류 있으면 기본 접힘 */}
          <div>
            <button onClick={() => setShowFull(v => !v)}
              style={{
                width: '100%', textAlign: 'left', padding: '8px 0',
                background: 'transparent', border: 'none', cursor: 'pointer',
                fontSize: 13, fontWeight: 600, color: 'var(--ink-2)',
                display: 'flex', alignItems: 'center', gap: 6,
              }}>
              <Icon name="chat" size={13} />
              전체 대화 내용 ({detail.pairs.length}개 질의, {totalSegCount}개 단락)
              <Icon name={(showFull || !hasErrors) ? 'chevronU' : 'chevron'} size={11} />
            </button>

            {(showFull || !hasErrors) && (
              <div style={{ marginTop: 8 }}>
                {detail.pairs.length === 0 && (
                  <div style={{ padding: 24, textAlign: 'center', fontSize: 12, color: 'var(--ink-4)' }}>
                    대화 기록이 없습니다
                  </div>
                )}
                {detail.pairs.map((p, pi) => (
                  <div key={pi} style={{ marginBottom: 10 }}>
                    {p.question && (
                      <div style={{
                        fontSize: 12, color: '#64748b',
                        padding: '8px 10px', background: '#f1f5f9',
                        borderRadius: '8px 8px 0 0',
                        border: '1px solid #e2e8f0', borderBottom: 'none',
                      }}>
                        <Icon name="user" size={12} style={{ marginRight: 4, color: '#94a3b8' }} />
                        <strong>Q{pi + 1}:</strong> {p.question}
                      </div>
                    )}
                    {p.segments.map((s, si) => {
                      const isLast = si === p.segments.length - 1;
                      const isFirst = si === 0;
                      // 모서리 처리 원본 규칙
                      let radius = '0';
                      if (isLast && (isFirst ? !p.question : true)) radius = isLast && isFirst && !p.question ? '8px' : '0 0 8px 8px';
                      if (isFirst && !p.question) radius = p.segments.length === 1 ? '8px' : '8px 8px 0 0';
                      return (
                        <div key={si} style={{
                          padding: '8px 10px', fontSize: 13, color: 'var(--ink)', lineHeight: 1.6, whiteSpace: 'pre-wrap',
                          /* 사용자 요청: 단락 내부 스크롤 제거, 전체 스크롤로 처리 */
                          border: s.is_error ? '2px solid #ef4444' : '1px solid #e2e8f0',
                          background: s.is_error ? '#fef2f2' : '#fff',
                          borderRadius: radius,
                        }}>
                          {s.is_error && (
                            <div style={{ marginBottom: 4 }}>
                              <span style={{ display: 'inline-block', background: '#ef4444', color: '#fff', borderRadius: 4, padding: '1px 8px', fontSize: 10, fontWeight: 600 }}>오류</span>
                            </div>
                          )}
                          <div dangerouslySetInnerHTML={{ __html: formatSegText(s.text) }} />
                        </div>
                      );
                    })}
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>

        <div style={{
          padding: '12px 20px', borderTop: '1px solid var(--line)',
          display: 'flex', gap: 8, justifyContent: 'flex-end', background: '#fff',
        }}>
          <Button onClick={onClose}>닫기</Button>
          <Button variant="primary" disabled={isResolved}
            icon={<Icon name="check" size={13} />}
            onClick={() => { onResolve(row.id); onClose(); }}>
            {isResolved ? '처리 완료됨' : '처리 완료'}
          </Button>
        </div>
      </div>

      {lightbox && (
        <div onClick={() => setLightbox(false)} style={{
          position: 'fixed', inset: 0, zIndex: 120, background: 'rgba(0,0,0,0.82)',
          display: 'grid', placeItems: 'center', animation: 'tslFadeIn 0.14s ease',
        }}>
          <div style={{
            width: 520, height: 360, background: 'oklch(0.32 0.04 255)',
            borderRadius: 14, display: 'grid', placeItems: 'center',
            color: '#fff', fontSize: 14, gap: 10, flexDirection: 'column',
            position: 'relative',
          }}>
            <Icon name="image" size={56} stroke={1.5} />
            <div style={{ fontSize: 12, color: '#cbd5e1' }}>screenshot_오류.png · 1280×720</div>
            <button onClick={() => setLightbox(false)} style={{
              position: 'absolute', top: 10, right: 10, width: 32, height: 32,
              borderRadius: '50%', background: 'rgba(0,0,0,0.4)', color: '#fff',
              border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer',
              display: 'grid', placeItems: 'center',
            }}><Icon name="close" size={16} /></button>
          </div>
        </div>
      )}
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════
// 활동기록 > 오류보고 (2-1)
// ═══════════════════════════════════════════════════════════════
const ErrorsView = () => {
  const toast = useToast();
  const [sort, onSort] = useSort('time');
  const [start, setStart] = React.useState(daysAgoStr(30));
  const [end, setEnd] = React.useState(todayStr());
  const [preset, setPreset] = React.useState('1m');
  // 원본 setBrPeriod: 프리셋 버튼 = 기간(개월) → start/end 갱신
  const applyPreset = (p) => {
    setPreset(p);
    const map = { '1m': 30, '3m': 90, '1y': 365 };
    setStart(daysAgoStr(map[p] || 30));
    setEnd(todayStr());
  };
  const [query, setQuery] = React.useState('');
  const [hideResolved, setHideResolved] = React.useState(false);
  const [selected, setSelected] = React.useState(new Set());
  const [page, setPage] = React.useState(1);
  const [pageSize, setPageSize] = React.useState(10);
  const [resolvedSet, setResolvedSet] = React.useState(new Set());
  const [deletedIds, setDeletedIds] = React.useState(new Set());
  const { data: liveReports } = useApiData(TSL_API.bugReports, []);
  const baseReports = (liveReports || []).filter(r => !deletedIds.has(r.id));
  const [actions, setActions] = React.useState({});
  React.useEffect(() => {
    if (liveReports) {
      setActions(Object.fromEntries(liveReports.map(r => [r.id, r.action])));
      // 원본: status='resolved' 건들을 체크박스 체크 상태로 초기화
      setResolvedSet(new Set(liveReports.filter(r => r.status === 'resolved').map(r => r.id)));
    }
  }, [liveReports]);
  const [editingAction, setEditingAction] = React.useState(null); // id being edited
  const [actionDraft, setActionDraft] = React.useState('');
  const [drawerRow, setDrawerRow] = React.useState(null);
  const [confirmDel, setConfirmDel] = React.useState(null);
  const isMobile = useIsMobile();

  const toggleResolved = (id) => {
    const isDoneNow = resolvedSet.has(id);
    setResolvedSet(s => {
      const n = new Set(s);
      if (n.has(id)) n.delete(id); else n.add(id);
      return n;
    });
    // 원본 toggleBugDone: status 'done' ↔ 'new'
    fetch('/admin/bug-reports/' + encodeURIComponent(id) + '/status', {
      method: 'PUT', credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ status: isDoneNow ? 'new' : 'done' }),
    }).catch(() => {});
  };

  const filtered = React.useMemo(() => {
    // resolvedSet을 단일 진실원으로 사용 — DB의 r.status='resolved'는 덮어써서 체크해제가 UI에 반영되게
    const computeStatus = (r) => resolvedSet.has(r.id)
      ? 'resolved'
      : (r.status === 'resolved' ? 'open' : r.status);
    let rows = baseReports.filter(r => {
      const status = computeStatus(r);
      if (hideResolved && status === 'resolved') return false;
      if (start && r.dateIso && r.dateIso < start) return false;
      if (end && r.dateIso && r.dateIso > end) return false;
      if (query) {
        const q = query.trim();
        if (!r.content.includes(q) && !r.reporter.includes(q) && !r.region.includes(q)) return false;
      }
      return true;
    }).map(r => ({ ...r, status: computeStatus(r), action: actions[r.id] || r.action }));
    return sortRows(rows, sort, {
      time: r => r.time, reporter: r => r.reporter, region: r => r.region, status: r => r.status,
    });
  }, [baseReports, sort, query, hideResolved, resolvedSet, actions, start, end]);

  const paged = filtered.slice((page - 1) * pageSize, page * pageSize);
  const toggleSel = (id) => setSelected(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const toggleAll = () => setSelected(s => s.size === paged.length ? new Set() : new Set(paged.map(r => r.id)));

  const saveAction = async (id) => {
    const note = actionDraft.trim();
    setActions(a => ({ ...a, [id]: note || '—' }));
    setEditingAction(null);
    try {
      await fetch('/admin/bug-reports/' + encodeURIComponent(id) + '/action-note', {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action_note: note }),
      });
      toast('success', '조치사항이 저장되었습니다');
    } catch (e) {
      toast('error', '저장 실패: ' + e.message);
    }
  };

  const bulkResolve = () => {
    setResolvedSet(s => { const n = new Set(s); selected.forEach(id => n.add(id)); return n; });
    toast('success', `${selected.size}건을 처리완료 처리했습니다`);
    setSelected(new Set());
  };
  const bulkDelete = async () => {
    const ids = Array.from(selected);
    if (!ids.length) return;
    let ok = 0, fail = 0;
    const succeeded = [];
    for (const id of ids) {
      try {
        const res = await fetch('/admin/bug-reports/' + encodeURIComponent(id), {
          method: 'DELETE', credentials: 'same-origin',
        });
        const j = await res.json().catch(() => ({}));
        if (res.ok && j.success) { ok++; succeeded.push(id); } else { fail++; }
      } catch { fail++; }
    }
    if (succeeded.length) {
      setDeletedIds(d => { const n = new Set(d); succeeded.forEach(id => n.add(id)); return n; });
    }
    setSelected(new Set());
    if (fail === 0) toast('success', `${ok}건 삭제 완료`);
    else toast('error', `${ok}건 성공, ${fail}건 실패`);
  };

  // 원본 admin.html line 2748~2810 그대로: 2x2 옵션(선택/전체 × 요약/전체) 모달 → POST /admin/bug-reports/download-excel
  const [brDlOpen, setBrDlOpen] = React.useState(false);
  const [brDlScope, setBrDlScope] = React.useState('all');
  const [brDlDetail, setBrDlDetail] = React.useState('summary');
  const openBugExcel = () => {
    if (!baseReports.length) { toast('info', '다운로드할 데이터가 없습니다.'); return; }
    setBrDlScope(selected.size > 0 ? 'selected' : 'all');
    setBrDlDetail('summary');
    setBrDlOpen(true);
  };
  const confirmBugExcel = async () => {
    const ids = brDlScope === 'selected'
      ? Array.from(selected).filter(id => baseReports.some(r => r.id === id))
      : baseReports.map(r => r.id);
    if (!ids.length) { toast('info', '다운로드할 항목이 없습니다.'); return; }
    setBrDlOpen(false);
    toast('info', '엑셀 생성 중...');
    try {
      const res = await fetch('/admin/bug-reports/download-excel', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ids, full: brDlDetail === 'full' }),
      });
      if (!res.ok) throw new Error('실패');
      const blob = await res.blob();
      const link = document.createElement('a');
      link.href = URL.createObjectURL(blob);
      const suffix = (brDlDetail === 'full' ? '_full' : '') + (brDlScope === 'selected' ? '_selected' : '');
      link.download = 'bug_reports' + suffix + '_' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '.xlsx';
      link.click();
    } catch (e) {
      toast('error', '엑셀 다운로드 실패');
    }
  };

  return (
    <div>
      <SectionHeader
        title="오류 보고"
        desc="사용자 신고된 오류를 확인·처리합니다"
        actions={<>
          <Button icon={<Icon name="refresh" size={13} />}>새로고침</Button>
          <Button variant="excel" icon={<Icon name="download" size={13} />} onClick={openBugExcel}>엑셀 다운로드</Button>
        </>}
      />

      <Toolbar style={{ marginBottom: 14 }}>
        <DateRange start={start} end={end} onStart={setStart} onEnd={setEnd}
          preset={preset} onPreset={applyPreset} />
        <div style={{ width: 1, height: 24, background: 'var(--line)' }} />
        <div style={{ flex: 2, minWidth: 320 }}>
          <TextInput value={query} onChange={setQuery}
            icon={<Icon name="search" size={14} />} placeholder="보고 내용·이름·지역" />
        </div>
        <Button variant="primary">조회</Button>
        <Button onClick={() => { setQuery(''); setStart(daysAgoStr(30)); setEnd(todayStr()); setPreset('1m'); }}>초기화</Button>
        <div style={{ width: 1, height: 24, background: 'var(--line)' }} />
        <button onClick={() => setHideResolved(!hideResolved)} style={{
          padding: '6px 12px', fontSize: 12, fontWeight: 500,
          background: hideResolved ? 'var(--accent-soft)' : '#f1f5f9',
          color: hideResolved ? 'var(--accent-ink)' : '#475569',
          border: `1px solid ${hideResolved ? 'var(--accent)' : '#e2e8f0'}`,
          borderRadius: 6, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6,
          fontFamily: 'inherit', whiteSpace: 'nowrap',
        }}>
          <Icon name={hideResolved ? 'eyeOff' : 'eye'} size={12} />
          처리완료 목록 {hideResolved ? '보기' : '숨기기'}
        </button>
      </Toolbar>

      {selected.size > 0 && (
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '10px 16px', marginBottom: 10, borderRadius: 10,
          background: 'var(--accent-soft)', border: '1px solid var(--accent)',
          color: 'var(--accent-ink)', fontSize: 13, fontWeight: 500,
          animation: 'tslSlideIn 0.2s ease',
        }}>
          <span><b>{selected.size}</b>건 선택됨</span>
          <div style={{ display: 'flex', gap: 6 }}>
            <Button size="sm" onClick={bulkResolve} icon={<Icon name="check" size={12} />}>일괄 처리완료</Button>
            <Button size="sm" variant="danger" onClick={() => setConfirmDel('bulk')}
              icon={<Icon name="trash" size={12} />}>일괄 삭제</Button>
            <Button size="sm" variant="ghost" onClick={() => setSelected(new Set())}>선택 해제</Button>
          </div>
        </div>
      )}

      <TableShell
        minWidth={1240}
        headers={<>
          <th style={{ ...tableStyles.headerCell, width: 40, textAlign: 'center' }}>
            <input type="checkbox" checked={paged.length > 0 && selected.size === paged.length}
              onChange={toggleAll} />
          </th>
          <th style={{ ...tableStyles.headerCell, width: 60, textAlign: 'center' }}>순번</th>
          <SortHeader label="보고 시기" col="time" sort={sort} onSort={onSort} width={isMobile ? 150 : 120} align="center" />
          {!isMobile && <SortHeader label="지역"     col="region" sort={sort} onSort={onSort} width={110} align="center" />}
          <SortHeader label="고유번호"     col="reporter" sort={sort} onSort={onSort} width={isMobile ? 150 : 110} align="center" />
          <th style={{ ...tableStyles.headerCell, textAlign: 'center' }}>보고 내용</th>
          <th style={{ ...tableStyles.headerCell, width: 240, textAlign: 'center' }}>조치사항</th>
          <th style={{ ...tableStyles.headerCell, width: 70, textAlign: 'center' }}>처리</th>
          <th style={{ ...tableStyles.headerCell, textAlign: 'center', width: 60 }}>삭제</th>
        </>}
        footer={<Pagination total={filtered.length} page={page} pageSize={pageSize}
          onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(1); }} />}
      >
        {paged.map((r, i) => {
          const resolved = r.status === 'resolved';
          return (
            <Row key={r.id} last={i === paged.length - 1}
              onClick={() => setDrawerRow({ ...r, _seq: filtered.length - ((page - 1) * pageSize + i) })}
              style={resolved ? { opacity: 0.42, background: '#f1f5f9' } : {}}>
              <td style={{ ...tableStyles.cell, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
                <input type="checkbox" checked={selected.has(r.id)} onChange={() => toggleSel(r.id)} />
              </td>
              <td style={{ ...tableStyles.cell, textAlign: 'center', color: 'var(--ink-4)', fontSize: 12 }}>{(filtered.length - ((page - 1) * pageSize + i))}</td>
              <td style={{ ...tableStyles.cell, ...tableStyles.mono, color: 'var(--ink-3)', textAlign: 'center', fontSize: 11.5, whiteSpace: 'nowrap' }}>
                {r.dtDate} {r.dtTime}
              </td>
              {!isMobile && <td style={{ ...tableStyles.cell, textAlign: 'center' }}>{r.region}</td>}
              <td style={{ ...tableStyles.cell, textAlign: 'center', whiteSpace: 'nowrap', lineHeight: 1.25 }}>
                <div style={{ fontWeight: 600, color: 'var(--ink)', fontFamily: 'var(--mono)' }}>
                  {r.phone}
                  {ERROR_DETAILS[r.id]?.hasImage && (
                    <Icon name="image" size={11} style={{ marginLeft: 5, color: 'var(--ink-4)' }} />
                  )}
                </div>
              </td>
              <td style={{ ...tableStyles.cell, maxWidth: 320, color: 'var(--ink-2)', textAlign: 'left' }}>
                <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                  <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 300, display: 'inline-block' }}>{r.content}</span>
                  {/* 원본: 사용자가 오류로 체크한 건수 뱃지 (is_error=true인 segment 카운트) */}
                  {r.error_count > 0 && (
                    <span title={`오류 체크된 부분: ${r.error_count}건`}
                      style={{ display: 'inline-flex', alignItems: 'center', gap: 3, padding: '1px 8px', borderRadius: 999,
                        background: '#fef2f2', color: '#dc2626', border: '1px solid #fecaca',
                        fontSize: 10.5, fontWeight: 700, flexShrink: 0 }}>
                      오류 {r.error_count}
                    </span>
                  )}
                </div>
              </td>
              <td style={tableStyles.cell} onClick={e => e.stopPropagation()}>
                {editingAction === r.id ? (
                  <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
                    <input autoFocus value={actionDraft}
                      onChange={e => setActionDraft(e.target.value)}
                      onKeyDown={e => {
                        if (e.key === 'Enter') saveAction(r.id);
                        if (e.key === 'Escape') setEditingAction(null);
                      }}
                      style={{
                        flex: 1, minWidth: 0, height: 30, padding: '0 10px',
                        border: '1px solid var(--accent)', borderRadius: 7,
                        outline: 'none', fontFamily: 'inherit', fontSize: 12.5,
                        boxShadow: '0 0 0 3px var(--accent-soft)',
                      }} />
                    <button onClick={() => saveAction(r.id)} style={{
                      width: 26, height: 26, borderRadius: 6, border: 'none',
                      background: 'var(--accent)', color: '#fff', cursor: 'pointer',
                      display: 'grid', placeItems: 'center',
                    }}><Icon name="check" size={12} /></button>
                  </div>
                ) : (
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                    <span style={{
                      flex: 1, color: r.action === '—' ? 'var(--ink-4)' : 'var(--ink-2)',
                      fontSize: 12.5,
                      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                    }}>{r.action}</span>
                    <button onClick={() => { setEditingAction(r.id); setActionDraft(r.action === '—' ? '' : r.action); }} style={{
                      width: 24, height: 24, borderRadius: 6, border: 'none',
                      background: 'transparent', color: 'var(--ink-4)', cursor: 'pointer',
                      display: 'grid', placeItems: 'center',
                    }}
                      onMouseEnter={e => { e.currentTarget.style.background = '#f3f5f8'; e.currentTarget.style.color = 'var(--accent-ink)'; }}
                      onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--ink-4)'; }}
                    ><Icon name="edit" size={11} /></button>
                  </div>
                )}
              </td>
              <td style={{ ...tableStyles.cell, textAlign: 'center', opacity: 1, textDecoration: 'none' }} onClick={e => e.stopPropagation()}>
                <button onClick={() => toggleResolved(r.id)}
                  aria-pressed={resolved}
                  style={{
                    width: 24, height: 24, borderRadius: 6, cursor: 'pointer',
                    border: `1.5px solid ${resolved ? 'oklch(0.55 0.14 155)' : 'var(--line-strong)'}`,
                    background: resolved ? 'oklch(0.55 0.14 155)' : '#fff',
                    color: '#fff',
                    display: 'grid', placeItems: 'center', transition: 'all 0.15s',
                    margin: '0 auto',
                  }}>
                  {resolved && <Icon name="check" size={13} stroke={3} />}
                </button>
              </td>
              <td style={{ ...tableStyles.cell, textAlign: 'right' }} onClick={e => e.stopPropagation()}>
                <button onClick={() => setConfirmDel(r.id)} style={{
                  width: 28, height: 28, borderRadius: 7, border: '1px solid var(--line-strong)',
                  background: '#fff', color: 'oklch(0.52 0.17 25)', cursor: 'pointer',
                  display: 'grid', placeItems: 'center', fontFamily: 'inherit',
                }}><Icon name="trash" size={12} /></button>
              </td>
            </Row>
          );
        })}
      </TableShell>

      <ErrorReportDrawer open={!!drawerRow} row={drawerRow}
        isResolved={drawerRow && resolvedSet.has(drawerRow.id)}
        onClose={() => setDrawerRow(null)}
        onResolve={(id) => { toggleResolved(id); toast('success', '처리 완료되었습니다'); }} />

      <ConfirmDialog open={!!confirmDel} tone="danger"
        title={confirmDel === 'bulk' ? '선택한 항목을 삭제합니다' : '이 오류 보고를 삭제합니다'}
        message={confirmDel === 'bulk'
          ? `${selected.size}건의 오류 보고를 삭제합니다. 복구할 수 없습니다.`
          : '삭제된 오류 보고는 복구할 수 없습니다. 계속하시겠습니까?'}
        confirmLabel="삭제"
        onClose={() => setConfirmDel(null)}
        onConfirm={async () => {
          if (confirmDel === 'bulk') {
            await bulkDelete();
          } else {
            const id = confirmDel;
            try {
              const res = await fetch('/admin/bug-reports/' + encodeURIComponent(id), {
                method: 'DELETE', credentials: 'same-origin',
              });
              const j = await res.json().catch(() => ({}));
              if (!res.ok || !j.success) throw new Error(j.error || ('HTTP ' + res.status));
              setDeletedIds(d => { const n = new Set(d); n.add(id); return n; });
              setSelected(s => { const n = new Set(s); n.delete(id); return n; });
              toast('success', '오류 보고가 삭제되었습니다');
            } catch (e) {
              toast('error', '삭제 실패: ' + e.message);
            }
          }
          setConfirmDel(null);
        }} />

      {/* 엑셀 다운로드 옵션 모달 — 원본 admin.html line 2748~2807 재현 */}
      {brDlOpen && (
        <div onClick={() => setBrDlOpen(false)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, backdropFilter: 'blur(4px)' }}>
          <div onClick={e => e.stopPropagation()} style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12, padding: '28px 32px', maxWidth: 420, width: '92%', boxShadow: '0 8px 32px rgba(0,0,0,0.15)' }}>
            <div style={{ fontSize: 16, fontWeight: 700, color: '#1e293b', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 6 }}>
              <Icon name="download" size={16} style={{ color: '#059669' }} /> 엑셀 다운로드
            </div>
            <div style={{ fontSize: 12, fontWeight: 600, color: '#475569', marginBottom: 6 }}>다운로드 범위</div>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 8, marginBottom: 14 }}>
              {[
                { v: 'selected', l: selected.size === 0 ? '선택항목 다운 (없음)' : `선택항목 다운 (${selected.size}건)`, disabled: selected.size === 0 },
                { v: 'all', l: `전체 다운 (${baseReports.length}건)`, disabled: false },
              ].map(o => (
                <label key={o.v} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', border: `1px solid ${brDlScope === o.v ? 'var(--accent)' : '#e2e8f0'}`, borderRadius: 8, fontSize: 13, color: '#334155', cursor: o.disabled ? 'not-allowed' : 'pointer', background: brDlScope === o.v ? 'var(--accent-soft)' : '#fff', opacity: o.disabled ? 0.5 : 1 }}>
                  <input type="radio" name="brDlScope" value={o.v} checked={brDlScope === o.v} disabled={o.disabled} onChange={() => setBrDlScope(o.v)} />
                  <span>{o.l}</span>
                </label>
              ))}
            </div>
            <div style={{ fontSize: 12, fontWeight: 600, color: '#475569', marginBottom: 6 }}>내용 범위</div>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 8, marginBottom: 20 }}>
              {[
                { v: 'summary', l: '오류 보고 내용만' },
                { v: 'full', l: '상세내용 전체' },
              ].map(o => (
                <label key={o.v} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', border: `1px solid ${brDlDetail === o.v ? 'var(--accent)' : '#e2e8f0'}`, borderRadius: 8, fontSize: 13, color: '#334155', cursor: 'pointer', background: brDlDetail === o.v ? 'var(--accent-soft)' : '#fff' }}>
                  <input type="radio" name="brDlDetail" value={o.v} checked={brDlDetail === o.v} onChange={() => setBrDlDetail(o.v)} />
                  <span>{o.l}</span>
                </label>
              ))}
            </div>
            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
              <Button onClick={() => setBrDlOpen(false)}>취소</Button>
              <Button variant="primary" onClick={confirmBugExcel}>확인</Button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ChatRow — 행 클릭 시 모달(ChatDetailDrawer) 오픈. 인라인 확장 제거.
const ChatRow = ({ r, isLast, onOpen, setFlag }) => (
  <Row last={isLast} onClick={() => onOpen(r)}>
    <td style={{ ...tableStyles.cell, textAlign: 'center' }}>
      <span style={{
        display: 'inline-grid', placeItems: 'center', width: 20, height: 20,
        color: 'var(--ink-4)',
      }}><Icon name="chevronR" size={13} /></span>
    </td>
    <td style={{ ...tableStyles.cell, ...tableStyles.mono, color: 'var(--ink-3)', textAlign: 'center', fontSize: 11.5, whiteSpace: 'nowrap' }}>
      {r.dtDate} {r.dtTime}
    </td>
    <td style={tableStyles.cell}>{r.region}</td>
    <td style={{ ...tableStyles.cell, lineHeight: 1.25 }}>
      <div style={{ fontWeight: 600, color: 'var(--ink)', fontFamily: 'var(--mono)' }}>{r.phone}</div>
    </td>
    <td style={{
      ...tableStyles.cell, color: 'var(--ink-2)', maxWidth: 440,
      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
      textAlign: 'left',
    }}>{r.content}</td>
    <td style={tableStyles.cell} onClick={e => e.stopPropagation()}>
      <InlineFlagSelect value={r.flag} onChange={v => setFlag(r._idx, v)} />
    </td>
  </Row>
);

// ChatDetailDrawer — 오른쪽에서 슬라이드되는 대화 상세 모달 (ErrorReportDrawer와 동일 패턴)
const ChatDetailDrawer = ({ open, row, onClose }) => {
  const [messages, setMessages] = React.useState(null);
  React.useEffect(() => {
    if (!open || !row) { setMessages(null); return; }
    if (!row.session_id) { setMessages([]); return; }
    fetch('/admin/history/messages/' + encodeURIComponent(row.session_id), { credentials: 'same-origin' })
      .then(res => res.ok ? res.json() : { messages: [] })
      .then(d => setMessages(Array.isArray(d) ? d : (d.messages || [])))
      .catch(() => setMessages([]));
  }, [open, row && row.session_id]);
  if (!open || !row) return null;
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 95, display: 'flex',
      background: 'rgba(15,23,42,0.3)', animation: 'tslFadeIn 0.14s ease',
    }}>
      <div style={{ flex: 1 }} />
      <div onClick={e => e.stopPropagation()} style={{
        width: 620, maxWidth: '100%', height: '100%', background: '#fff',
        boxShadow: '-16px 0 40px rgba(15,23,42,0.14)',
        display: 'flex', flexDirection: 'column',
        animation: 'tslSlideIn 0.22s cubic-bezier(0.2, 0.9, 0.3, 1)',
      }}>
        <div style={{
          padding: '16px 20px', borderBottom: '1px solid var(--line)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        }}>
          <div>
            <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--ink)' }}>
              {row.region}
              <span style={{ fontWeight: 400, color: 'var(--ink-3)', fontSize: 13, marginLeft: 8, fontFamily: 'var(--mono)' }}>
                {row.phone}
              </span>
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-4)', marginTop: 2, fontFamily: 'var(--mono)' }}>
              {row.time}
            </div>
          </div>
          <button onClick={onClose} style={{
            border: 'none', background: 'transparent', cursor: 'pointer',
            color: 'var(--ink-3)', padding: 6, borderRadius: 6,
          }}><Icon name="close" size={18} /></button>
        </div>

        <div style={{ padding: '18px 20px', overflowY: 'auto', flex: 1, background: 'var(--surface-2)' }}>
          {messages === null ? (
            <div style={{ fontSize: 12, color: 'var(--ink-4)' }}>불러오는 중…</div>
          ) : messages.length === 0 ? (
            <div style={{ fontSize: 12, color: 'var(--ink-4)' }}>메시지가 없습니다.</div>
          ) : (() => {
            // messages(role/content) → 오류보고 qa_pairs 구조로 변환
            // user 메시지 = 질문, 이후 연속된 ai 메시지 = segments
            const pairs = [];
            let cur = null;
            messages.forEach(m => {
              const isUser = (m.role || m.who) === 'user';
              const text = m.content || m.text || '';
              if (isUser) {
                cur = { question: text, segments: [] };
                pairs.push(cur);
              } else if (cur) {
                cur.segments.push({ text, is_error: !!m.is_error });
              } else {
                pairs.push({ question: '', segments: [{ text, is_error: !!m.is_error }] });
                cur = null;
              }
            });
            // 감사지적사항: is_error 세그먼트 수집 (현재는 messages에 is_error 없으므로 비어있음)
            const errorItems = [];
            pairs.forEach(p => p.segments.forEach(s => { if (s.is_error) errorItems.push({ question: p.question, text: s.text }); }));
            const hasErrors = errorItems.length > 0;
            return (
              <>
                {hasErrors && (
                  <div style={{ marginBottom: 16 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 600, color: '#ef4444', marginBottom: 8 }}>
                      <Icon name="warning" size={14} /> 감사 지적 사항 ({errorItems.length}건)
                    </div>
                    {errorItems.map((item, idx) => (
                      <div key={'err-' + idx} style={{ border: '2px solid #ef4444', borderRadius: 10, padding: 14, marginBottom: 8, background: '#fef2f2' }}>
                        {item.question && (
                          <div style={{ fontSize: 12, color: '#64748b', marginBottom: 8, padding: '8px 10px', background: '#fff', borderRadius: 6, border: '1px solid #e2e8f0' }}>
                            <Icon name="user" size={12} style={{ marginRight: 4, color: '#94a3b8' }} />
                            <strong> 질문:</strong> {item.question}
                          </div>
                        )}
                        <div style={{ fontSize: 13, color: 'var(--ink)', lineHeight: 1.7, padding: '8px 10px', background: '#fff', borderRadius: 6, border: '1px solid #fecaca' }}>
                          <Icon name="chat" size={12} style={{ marginRight: 4, color: '#ef4444' }} />
                          <strong> 답변:</strong>
                          <div style={{ marginTop: 4, whiteSpace: 'pre-wrap' }} dangerouslySetInnerHTML={{ __html: formatSegText(item.text) }} />
                        </div>
                      </div>
                    ))}
                  </div>
                )}
                <div>
                  {pairs.map((p, pi) => (
                    <div key={pi} style={{ marginBottom: 10 }}>
                      {p.question && (
                        <div style={{
                          fontSize: 12, color: '#64748b',
                          padding: '8px 10px', background: '#f1f5f9',
                          borderRadius: '8px 8px 0 0',
                          border: '1px solid #e2e8f0', borderBottom: 'none',
                        }}>
                          <Icon name="user" size={12} style={{ marginRight: 4, color: '#94a3b8' }} />
                          <strong>Q{pi + 1}:</strong> {p.question}
                        </div>
                      )}
                      {p.segments.map((s, si) => {
                        const isLast = si === p.segments.length - 1;
                        const isFirst = si === 0;
                        let radius = '0';
                        if (isLast && (isFirst ? !p.question : true)) radius = isLast && isFirst && !p.question ? '8px' : '0 0 8px 8px';
                        if (isFirst && !p.question) radius = p.segments.length === 1 ? '8px' : '8px 8px 0 0';
                        return (
                          <div key={si} style={{
                            padding: '8px 10px', fontSize: 13, color: 'var(--ink)', lineHeight: 1.6, whiteSpace: 'pre-wrap',
                            border: s.is_error ? '2px solid #ef4444' : '1px solid #e2e8f0',
                            background: s.is_error ? '#fef2f2' : '#fff',
                            borderRadius: radius,
                          }}>
                            {s.is_error && (
                              <div style={{ marginBottom: 4 }}>
                                <span style={{ display: 'inline-block', background: '#ef4444', color: '#fff', borderRadius: 4, padding: '1px 8px', fontSize: 10, fontWeight: 600 }}>오류</span>
                              </div>
                            )}
                            <div dangerouslySetInnerHTML={{ __html: formatSegText(s.text) }} />
                          </div>
                        );
                      })}
                    </div>
                  ))}
                </div>
              </>
            );
          })()}
        </div>

        <div style={{
          padding: '12px 20px', borderTop: '1px solid var(--line)',
          display: 'flex', gap: 8, justifyContent: 'flex-end', background: '#fff',
        }}>
          <Button onClick={onClose}>닫기</Button>
        </div>
      </div>
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════
// 활동기록 > 최근대화 (2-2) — 행 펼치기 + 인라인 플래그
// ═══════════════════════════════════════════════════════════════
const ChatsView = () => {
  const toast = useToast();
  const [sort, onSort] = useSort('time');
  const [region, setRegion] = React.useState('전체');
  const [code, setCode] = React.useState('');
  const [preset, setPreset] = React.useState('1m');
  const [clStart, setClStart] = React.useState(daysAgoStr(30));
  const [clEnd, setClEnd] = React.useState(todayStr());
  // 원본 setClPeriod: 0=오늘, 1=1달, 3=3달, 12=1년
  const applyClPreset = (p) => {
    setPreset(p);
    const map = { 'today': 0, '1m': 30, '3m': 90, '1y': 365 };
    setClStart(daysAgoStr(map[p] ?? 30));
    setClEnd(todayStr());
  };
  const [content, setContent] = React.useState('');
  const [flagFilter, setFlagFilter] = React.useState('all');
  const [selectedChat, setSelectedChat] = React.useState(null);
  const { data: liveChats } = useApiData(TSL_API.chats, []);
  const baseChats = liveChats || [];
  const [flags, setFlags] = React.useState({});
  React.useEffect(() => {
    if (liveChats) setFlags(Object.fromEntries(liveChats.map((r, i) => [i, r.flag])));
  }, [liveChats]);
  const [page, setPage] = React.useState(1);
  const [pageSize, setPageSize] = React.useState(10);

  const filtered = React.useMemo(() => {
    let rows = baseChats.map((r, idx) => ({ ...r, _idx: idx, flag: flags[idx] })).filter(r => {
      if (region !== '전체' && !r.region.startsWith(region)) return false;
      if (code && !(r.name_phone_key || '').includes(code)) return false;
      if (content && !r.content.includes(content)) return false;
      if (clStart && r.dateIso && r.dateIso < clStart) return false;
      if (clEnd && r.dateIso && r.dateIso > clEnd) return false;
      if (flagFilter === 'all') return true;
      if (flagFilter === 'any') return !!r.flag;
      return r.flag === flagFilter;
    });
    return sortRows(rows, sort, { time: r => r.time, region: r => r.region, name: r => r.name });
  }, [baseChats, region, code, content, flagFilter, sort, flags, clStart, clEnd]);

  const paged = filtered.slice((page - 1) * pageSize, page * pageSize);

  const setFlag = (idx, val) => {
    setFlags(f => ({ ...f, [idx]: val || null }));
    toast('info', val ? `"${CHAT_FLAG_META[val].label}"으로 분류되었습니다` : '분류가 해제되었습니다');
  };

  return (
    <div>
      <SectionHeader
        title="최근 대화"
        desc="사용자별 최근 대화 내역을 확인합니다"
        actions={<>
          <Button icon={<Icon name="refresh" size={13} />}>새로고침</Button>
          <Button variant="excel" icon={<Icon name="download" size={13} />} onClick={() => window.location.href="/admin/history/download?start="+encodeURIComponent(clStart)+"&end="+encodeURIComponent(clEnd)}>엑셀 다운로드</Button>
        </>}
      />

      <Toolbar style={{ marginBottom: 10 }}>
        <Select value={region} onChange={setRegion} options={REGIONS} width={130}
          icon={<Icon name="location" size={14} />} />
        <TextInput value={code} onChange={setCode}
          icon={<Icon name="user" size={13} />} placeholder="고유번호" width={180} />
        <DateRange start={clStart} end={clEnd} onStart={setClStart} onEnd={setClEnd} preset={preset} onPreset={applyClPreset} />
      </Toolbar>
      <Toolbar style={{ marginBottom: 10 }}>
        <div style={{ flex: 1 }}>
          <TextInput value={content} onChange={setContent} width="100%"
            icon={<Icon name="search" size={14} />} placeholder="대화 내용 검색" />
        </div>
        <Button variant="primary" icon={<Icon name="search" size={13} />}>조회</Button>
      </Toolbar>

      <div style={{ display: 'flex', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          <Chip active={flagFilter === 'all'} onClick={() => setFlagFilter('all')}>전체</Chip>
          <Chip active={flagFilter === 'any'} onClick={() => setFlagFilter('any')} tone="danger">부적절 전체</Chip>
          <Chip active={flagFilter === 'nonsense'} onClick={() => setFlagFilter('nonsense')} tone="neutral">무의미</Chip>
          <Chip active={flagFilter === 'obscene'} onClick={() => setFlagFilter('obscene')} tone="danger">음란성</Chip>
          <Chip active={flagFilter === 'aggressive'} onClick={() => setFlagFilter('aggressive')} tone="danger">공격성</Chip>
          <Chip active={flagFilter === 'etc'} onClick={() => setFlagFilter('etc')} tone="warn">기타</Chip>
        </div>
      </div>

      <TableShell
        minWidth={1100}
        headers={<>
          <th style={{ ...tableStyles.headerCell, width: 36 }}></th>
          <SortHeader label="날짜"    col="time"   sort={sort} onSort={onSort} width={140} />
          <SortHeader label="지역"    col="region" sort={sort} onSort={onSort} width={130} />
          <SortHeader label="고유번호"    col="name"   sort={sort} onSort={onSort} width={110} />
          <th style={tableStyles.headerCell}>대화 내용</th>
          <th style={{ ...tableStyles.headerCell, width: 130 }}>부적절 질문</th>
        </>}
        footer={<Pagination total={filtered.length} page={page} pageSize={pageSize}
          onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(1); }} />}
      >
        {paged.map((r, i) => (
          <ChatRow key={r.session_id || i} r={r} isLast={i === paged.length - 1} onOpen={setSelectedChat} setFlag={setFlag} />
        ))}
      </TableShell>

      <ChatDetailDrawer open={!!selectedChat} row={selectedChat} onClose={() => setSelectedChat(null)} />
    </div>
  );
};

const InlineFlagSelect = ({ value, onChange }) => {
  const opts = [
    { value: '', label: '—' },
    { value: 'nonsense', label: '무의미' },
    { value: 'obscene', label: '음란성' },
    { value: 'aggressive', label: '공격성' },
    { value: 'etc', label: '기타' },
  ];
  const current = opts.find(o => o.value === (value || ''));
  const tone = value ? CHAT_FLAG_META[value].tone : null;
  const colors = tone ? TONE_PALETTE[tone] : { bg: '#fff', fg: 'var(--ink-4)' };
  return (
    <label style={{
      display: 'inline-flex', alignItems: 'center', gap: 4,
      height: 26, borderRadius: 999,
      background: value ? colors.bg : 'transparent',
      border: value ? 'none' : '1px dashed var(--line-strong)',
      padding: '0 4px 0 10px', cursor: 'pointer', position: 'relative',
    }}>
      <span style={{
        fontSize: 11.5, fontWeight: 600, color: colors.fg,
        pointerEvents: 'none',
      }}>{current.label}</span>
      <Icon name="chevron" size={11} style={{ color: colors.fg, opacity: 0.8 }} />
      <select value={value || ''} onChange={e => onChange(e.target.value)} style={{
        position: 'absolute', inset: 0, opacity: 0, cursor: 'pointer',
      }}>
        {opts.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
      </select>
    </label>
  );
};

// ═══════════════════════════════════════════════════════════════
// 활동기록 > 토큰사용내역 (2-3, 마스터)
// ═══════════════════════════════════════════════════════════════
const TokenUsageView = () => {
  const [sort, onSort] = useSort('total');
  const [page, setPage] = React.useState(1);
  const [pageSize, setPageSize] = React.useState(10);
  const [tuStart, setTuStart] = React.useState(daysAgoStr(30));
  const [tuEnd, setTuEnd] = React.useState(todayStr());
  const [preset, setPreset] = React.useState('1m');
  const [refreshKey, setRefreshKey] = React.useState(0);

  // 조회 버튼(refreshKey)을 눌렀을 때만 재조회. 날짜 자체 변경으로는 재조회하지 않음 (초기 로드는 refreshKey=0에서 한 번)
  const [queryStart, setQueryStart] = React.useState(tuStart);
  const [queryEnd, setQueryEnd] = React.useState(tuEnd);
  const { data: liveUsage, fetching } = useApiData(() => TSL_API.tokenUsage(queryStart, queryEnd), [refreshKey]);
  const runQuery = () => { setQueryStart(tuStart); setQueryEnd(tuEnd); setRefreshKey(k => k + 1); };
  // [2026-07-09] 기간 프리셋 클릭 시 ★즉시 조회★(기존엔 조회 버튼을 또 눌러야 반영돼 "버튼이 안 먹는다"는 오해가 있었음)
  const applyPreset = (p) => {
    setPreset(p);
    const map = { '1m': 30, '3m': 90, '1y': 365 };
    const ns = daysAgoStr(map[p] || 30), ne = todayStr();
    setTuStart(ns); setTuEnd(ne);
    setQueryStart(ns); setQueryEnd(ne); setRefreshKey(k => k + 1);
  };
  const baseUsage = liveUsage || [];
  const withTotal = baseUsage.map(r => ({ ...r, total: r.free_used + r.bonus_used }));
  // 사용자 정렬 적용 후 순위를 유동적으로 재부여 (rank 컬럼 정렬 = total 기준)
  const sorted = sortRows(withTotal, sort, {
    rank: r => r.total, region: r => r.region, member: r => r.member,
    free: r => r.free_used, bonus: r => r.bonus_used, total: r => r.total,
    bad: r => r.bad, err: r => r.err,
  });
  const rows = sorted.map((r, i) => ({ ...r, rank: i + 1 }));
  const paged = rows.slice((page - 1) * pageSize, page * pageSize);

  return (
    <div>
      <SectionHeader
        title="토큰 사용 내역"
        desc="기간별 회원 토큰 소비 현황 (순위 기준)"
        actions={<>
          {fetching && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12.5, color: 'var(--ink-3)', marginRight: 4 }}>
            <span style={{ display: 'inline-block', width: 13, height: 13, border: '2px solid var(--line-strong)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'tuSpin 0.7s linear infinite' }} />
            불러오는 중…
          </span>}
          <Button icon={<Icon name="refresh" size={13} />} disabled={fetching} onClick={() => setRefreshKey(k => k + 1)}>새로고침</Button>
          <Button variant="excel" icon={<Icon name="download" size={13} />}
            onClick={() => { window.location.href = '/admin/token-usage/download-excel?start=' + encodeURIComponent(tuStart) + '&end=' + encodeURIComponent(tuEnd); }}>
            엑셀 다운로드
          </Button>
        </>}
      />
      <style>{'@keyframes tuSpin{to{transform:rotate(360deg)}}'}</style>

      <Toolbar style={{ marginBottom: 16 }}>
        <DateRange start={tuStart} end={tuEnd} onStart={setTuStart} onEnd={setTuEnd} preset={preset} onPreset={applyPreset} />
        <Button variant="primary" disabled={fetching} onClick={runQuery}>{fetching ? '조회 중…' : '조회'}</Button>
      </Toolbar>

      <TableShell
        minWidth={1200}
        headers={<>
          <SortHeader label="순위"       col="rank"   sort={sort} onSort={onSort} align="right" width={60} />
          <SortHeader label="지역"       col="region" sort={sort} onSort={onSort} width={130} />
          <SortHeader label="고유번호"       col="member" sort={sort} onSort={onSort} width={120} />
          <SortHeader label="일반 토큰 사용량"  col="free"   sort={sort} onSort={onSort} align="right" width={130} />
          <SortHeader label="보너스 토큰 사용량" col="bonus" sort={sort} onSort={onSort} align="right" width={140} />
          <SortHeader label="총 사용량"   col="total"  sort={sort} onSort={onSort} align="right" width={100} />
          <SortHeader label="부적절 질문" col="bad"    sort={sort} onSort={onSort} align="right" width={100} />
          <SortHeader label="오류보고"   col="err"    sort={sort} onSort={onSort} align="right" width={90} />
          <th style={tableStyles.headerCell}>비고</th>
        </>}
        footer={<Pagination total={rows.length} page={page} pageSize={pageSize}
          onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(1); }} />}
      >
        {paged.map((r, i) => (
          <Row key={r.member_id} last={i === paged.length - 1}>
            <td style={{ ...tableStyles.cell, textAlign: 'center', fontWeight: 700, color: r.rank <= 3 ? 'var(--accent-ink)' : 'var(--ink-3)' }}>
              {r.rank <= 3 ? <span style={{
                display: 'inline-grid', placeItems: 'center', width: 24, height: 24, borderRadius: '50%',
                background: 'var(--accent-soft)', fontSize: 12,
              }}>{r.rank}</span> : r.rank}
            </td>
            <td style={tableStyles.cell}>{r.region}</td>
            <td style={{ ...tableStyles.cell, lineHeight: 1.25 }}>
              <div style={{ fontWeight: 600, color: 'var(--ink)', fontFamily: 'var(--mono)' }}>{r.phone}</div>
            </td>
            <td style={{ ...tableStyles.cell, textAlign: 'right', ...tableStyles.num }}>{r.free_used.toLocaleString()}</td>
            <td style={{ ...tableStyles.cell, textAlign: 'right', ...tableStyles.num, color: 'oklch(0.48 0.12 300)', fontWeight: 600 }}>{r.bonus_used.toLocaleString()}</td>
            {/* 원본 .tu-total-cell — JetBrains Mono 14px 700 #059669, gradient bg */}
            <td style={{
              ...tableStyles.cell, textAlign: 'center',
              fontFamily: 'var(--mono)', fontWeight: 700, fontSize: 14, color: '#059669',
              letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums',
              background: 'linear-gradient(135deg, rgba(5,150,105,0.06) 0%, rgba(52,211,153,0.04) 100%)',
            }}>{r.total.toLocaleString()}</td>
            <td style={{ ...tableStyles.cell, textAlign: 'center', ...tableStyles.num,
              color: r.bad > 0 ? '#dc2626' : '#d4d4d8', fontWeight: r.bad > 0 ? 700 : 400 }}>
              {r.bad > 0 ? `${r.bad}회` : 0}
            </td>
            <td style={{ ...tableStyles.cell, textAlign: 'right', ...tableStyles.num,
              color: r.err > 0 ? 'oklch(0.52 0.14 75)' : 'var(--ink-4)', fontWeight: r.err > 0 ? 700 : 400 }}>{r.err}</td>
            <td style={{ ...tableStyles.cell, color: r.note !== '—' ? 'var(--ink-2)' : 'var(--ink-4)', fontSize: 12 }}>{r.note}</td>
          </Row>
        ))}
      </TableShell>
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════
// 활동기록 > 토큰교환내역 (2-4, 마스터)
// ═══════════════════════════════════════════════════════════════
const ExchangesView = () => {
  const [sort, onSort] = useSort('time');
  const [page, setPage] = React.useState(1);
  const [pageSize, setPageSize] = React.useState(10);
  const [txStart, setTxStart] = React.useState(daysAgoStr(7));
  const [txEnd, setTxEnd] = React.useState(todayStr());
  const [txPreset, setTxPreset] = React.useState('1m');
  const [refreshKey, setRefreshKey] = React.useState(0);
  const applyTxPreset = (p) => {
    setTxPreset(p);
    const map = { '1m': 30, '3m': 90, '1y': 365 };
    setTxStart(daysAgoStr(map[p] || 30));
    setTxEnd(todayStr());
  };
  const { data: liveTx } = useApiData(TSL_API.transactions, [refreshKey]);
  const baseTx = liveTx || [];
  const filteredTx = baseTx.filter(r => {
    if (!r.dateIso) return true;
    if (txStart && r.dateIso < txStart) return false;
    if (txEnd && r.dateIso > txEnd) return false;
    return true;
  });
  const rows = sortRows(filteredTx.map(r => ({ ...r, total: r.free + r.bonus })), sort, {
    time: r => r.time, region: r => r.region, from: r => r.from, to: r => r.to,
    free: r => r.free, bonus: r => r.bonus, total: r => r.total, status: r => r.status,
  });
  const paged = rows.slice((page - 1) * pageSize, page * pageSize);

  return (
    <div>
      <SectionHeader
        title="토큰 교환 내역"
        desc="회원 간 토큰 선물·교환 이력"
        actions={<>
          <Button icon={<Icon name="refresh" size={13} />} onClick={() => setRefreshKey(k => k + 1)}>새로고침</Button>
          <Button variant="excel" icon={<Icon name="download" size={13} />}
            onClick={() => {
              // 원본엔 별도 엑셀 엔드포인트 없음 — 현재 조회된 rows를 CSV로 내보냄
              const header = ['시간','지역','보낸사람','받는사람','일반','보너스','합계','상태','사유'];
              const csvRows = rows.map(r => [r.time, r.region, r.from, r.to, r.free, r.bonus, r.total,
                (EXCHANGE_STATUS[r.status]||{label:r.status}).label, r.reason]
                .map(v => '"' + String(v).replace(/"/g,'""') + '"').join(','));
              const csv = '﻿' + header.join(',') + '\n' + csvRows.join('\n');
              const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
              const link = document.createElement('a');
              link.href = URL.createObjectURL(blob);
              link.download = 'transactions_' + new Date().toISOString().slice(0,10).replace(/-/g,'') + '.csv';
              link.click();
            }}>엑셀 다운로드</Button>
        </>}
      />

      <Toolbar style={{ marginBottom: 16 }}>
        <DateRange start={txStart} end={txEnd} onStart={setTxStart} onEnd={setTxEnd} preset={txPreset} onPreset={applyTxPreset} />
        <Button variant="primary" onClick={() => setRefreshKey(k => k + 1)}>조회</Button>
      </Toolbar>

      <TableShell
        minWidth={1240}
        headers={<>
          <SortHeader label="시간"     col="time"   sort={sort} onSort={onSort} width={150} />
          <SortHeader label="지역"     col="region" sort={sort} onSort={onSort} width={70} />
          <SortHeader label="보낸사람" col="from"   sort={sort} onSort={onSort} width={140} />
          <SortHeader label="받는사람" col="to"     sort={sort} onSort={onSort} width={140} />
          <SortHeader label="일반"     col="free"   sort={sort} onSort={onSort} align="right" width={80} />
          <SortHeader label="보너스"   col="bonus"  sort={sort} onSort={onSort} align="right" width={90} />
          <SortHeader label="합계"     col="total"  sort={sort} onSort={onSort} align="right" width={90} />
          <SortHeader label="상태"     col="status" sort={sort} onSort={onSort} width={90} />
          <th style={tableStyles.headerCell}>사유</th>
        </>}
        footer={<Pagination total={rows.length} page={page} pageSize={pageSize}
          onPage={setPage} onPageSize={(n) => { setPageSize(n); setPage(1); }} />}
      >
        {paged.map((r, i) => (
          <Row key={i} last={i === paged.length - 1}>
            <td style={{ ...tableStyles.cell, ...tableStyles.mono, color: 'var(--ink-3)' }}>{r.time}</td>
            <td style={tableStyles.cell}>{r.region}</td>
            <td style={{ ...tableStyles.cell, lineHeight: 1.25 }}>
              <div style={{ fontWeight: 600, color: 'var(--ink)', fontFamily: 'var(--mono)' }}>{r.fromPhone}</div>
            </td>
            <td style={{ ...tableStyles.cell, lineHeight: 1.25 }}>
              <div style={{ fontWeight: 600, color: 'var(--ink)', fontFamily: 'var(--mono)' }}>{r.toPhone}</div>
            </td>
            <td style={{ ...tableStyles.cell, textAlign: 'right', ...tableStyles.num }}>
              {r.free > 0 ? r.free.toLocaleString() : <span style={{ color: 'var(--ink-4)' }}>—</span>}
            </td>
            <td style={{ ...tableStyles.cell, textAlign: 'right', ...tableStyles.num,
              color: r.bonus > 0 ? 'oklch(0.48 0.12 300)' : 'var(--ink-4)', fontWeight: r.bonus > 0 ? 600 : 400 }}>
              {r.bonus > 0 ? r.bonus.toLocaleString() : '—'}
            </td>
            <td style={{ ...tableStyles.cell, textAlign: 'right', ...tableStyles.num, fontWeight: 700, color: 'var(--ink)' }}>
              {r.total.toLocaleString()}
            </td>
            <td style={tableStyles.cell}>
              <Pill label={(EXCHANGE_STATUS[r.status]||{label:r.status,tone:'neutral'}).label} tone={(EXCHANGE_STATUS[r.status]||{tone:'neutral'}).tone} size="sm" />
            </td>
            <td style={{ ...tableStyles.cell, color: r.reason !== '—' ? 'var(--ink-3)' : 'var(--ink-4)', fontSize: 12 }}>{r.reason}</td>
          </Row>
        ))}
      </TableShell>
    </div>
  );
};

Object.assign(window, { ErrorsView, ChatsView, TokenUsageView, ExchangesView });
