// ═══════════════════════════════════════════════════════════════
// 정책 > 보안관리 (3-1, 마스터)
// ═══════════════════════════════════════════════════════════════
const SecurityView = () => {
  const toast = useToast();
  const isMobile = useIsMobile();
  const [s, setS] = React.useState({
    login: true,
    capture: true,
    copy: true,
    pcWatermark: true,    pcAlways: false,
    mobileWatermark: true, mobileAlways: true,
    kakaoId: true,
    align: 'center',
    color: '#8e7fbf',
    lineBreak: 20,
    opacity: 15,
    watermarkText: '',
    clipboardText: '',
  });
  const up = (k, v) => setS(p => ({ ...p, [k]: v }));

  // ── 토글 저장 헬퍼: 즉시 상태 반영 + 서버 저장 (실패 시 롤백) ──
  const saveToggle = (key, value, endpoint, bodyKey) => {
    up(key, value);
    TSL_API.saveSetting(endpoint, { [bodyKey]: value }).catch(err => {
      console.error('saveSetting', endpoint, err);
      up(key, !value); // 롤백
    });
  };
  const saveMode = (key, value, endpoint, bodyKey) => {
    up(key, value);
    const modeVal = value ? 'always' : 'when_disabled';
    TSL_API.saveSetting(endpoint, { [bodyKey]: modeVal }).catch(err => {
      console.error('saveSetting mode', endpoint, err);
      up(key, !value);
    });
  };
  const saveWatermarkContent = () => {
    TSL_API.saveSetting('watermark-content', {
      watermark_show_id: s.kakaoId,
      watermark_custom_text: s.watermarkText,
      watermark_align: s.align,
      watermark_color: s.color,
      watermark_max_chars: s.lineBreak,
      watermark_opacity: s.opacity,
    }).then(() => toast('success', '워터마크 설정이 저장되었습니다'))
      .catch(err => { console.error(err); toast('error', '저장 실패: ' + err.message); });
  };
  const saveClipboardText = () => {
    TSL_API.saveSetting('capture-clipboard-text', { capture_clipboard_text: s.clipboardText })
      .then(() => toast('success', '클립보드 문구가 저장되었습니다'))
      .catch(err => { console.error(err); toast('error', '저장 실패: ' + err.message); });
  };

  // ── Fetch live settings on mount (read-only) ──
  React.useEffect(() => {
    const safe = (name, fn) => TSL_API.setting(name).then(fn).catch(() => {});
    safe('watermark-content', d => setS(p => ({
      ...p,
      watermarkText: d.watermark_custom_text || '',
      kakaoId: d.watermark_show_id !== false,
      align: d.watermark_align || 'center',
      color: d.watermark_color || '#000000',
      lineBreak: d.watermark_max_chars || 20,
      opacity: d.watermark_opacity || 15,
    })));
    // 실제 API 응답 필드명 (user-management-server.js 기준):
    //   login-toggle       → { login_required: bool }
    //   capture-toggle     → { capture_protection: bool }
    //   copy-toggle        → { copy_protection: bool }
    //   watermark-toggle   → { watermark_enabled: bool }
    //   mobile-watermark-toggle → { mobile_watermark_enabled: bool }
    //   watermark-mode     → { watermark_mode: 'always'|'when_disabled' }
    //   mobile-watermark-mode → { mobile_watermark_mode: ... }
    //   capture-clipboard-text → { capture_clipboard_text: string }
    //   bonus-gift-toggle  → { bonus_gift_enabled: bool }
    safe('login-toggle',           d => setS(p => ({ ...p, login: !!d.login_required })));
    safe('capture-toggle',         d => setS(p => ({ ...p, capture: !!d.capture_protection })));
    safe('copy-toggle',            d => setS(p => ({ ...p, copy: !!d.copy_protection })));
    safe('watermark-toggle',       d => setS(p => ({ ...p, pcWatermark: !!d.watermark_enabled })));
    safe('mobile-watermark-toggle', d => setS(p => ({ ...p, mobileWatermark: !!d.mobile_watermark_enabled })));
    safe('watermark-mode',         d => setS(p => ({ ...p, pcAlways: d.watermark_mode === 'always' })));
    safe('mobile-watermark-mode',  d => setS(p => ({ ...p, mobileAlways: d.mobile_watermark_mode === 'always' })));
    safe('capture-clipboard-text', d => setS(p => ({ ...p, clipboardText: d.capture_clipboard_text || '' })));
  }, []);

  // Drag-reorder state for ALL policy blocks (mini + wmedit + clipboard)
  const defaultOrder = ['login', 'capture', 'copy', 'pcWM', 'mobileWM', 'wmEdit', 'clipboard'];
  const [order, setOrder] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem('tsl.security.order')) || defaultOrder; } catch { return defaultOrder; }
  });
  React.useEffect(() => { localStorage.setItem('tsl.security.order', JSON.stringify(order)); }, [order]);
  const [dragId, setDragId] = React.useState(null);
  const [overId, setOverId] = React.useState(null);
  // 슬라이더 조작 중에는 블록 드래그 비활성화
  const [sliderBusy, setSliderBusy] = React.useState(false);
  const onDragStart = (id) => setDragId(id);
  const onDragOver = (id) => (e) => { e.preventDefault(); if (id !== overId) setOverId(id); };
  const onDragLeave = () => setOverId(null);
  const onDrop = (targetId) => (e) => {
    e.preventDefault();
    if (!dragId || dragId === targetId) { setDragId(null); setOverId(null); return; }
    setOrder(o => {
      const next = [...o];
      const from = next.indexOf(dragId);
      const to = next.indexOf(targetId);
      next.splice(from, 1);
      next.splice(to, 0, dragId);
      return next;
    });
    setDragId(null); setOverId(null);
  };
  const onDragEnd = () => { setDragId(null); setOverId(null); };

  // 원본 adminLoginStatus 포맷: <b>ON</b> · {설명} / <b>OFF</b> · {설명}
  const stateText = (on, onMsg = '활성화', offMsg = '비활성화') => on
    ? <span style={{ color: '#16a34a', fontSize: 12 }}><b>ON</b> <span style={{ color: 'var(--ink-3)' }}>· {onMsg}</span></span>
    : <span style={{ color: 'var(--ink-3)', fontSize: 12 }}><b>OFF</b> <span style={{ color: 'var(--ink-4)' }}>· {offMsg}</span></span>;

  // Card definitions by id
  const cards = {
    login: <PolicyMiniCard title="로그인" desc="사용자 로그인 허용 여부" icon="lock"
      toggle={<Toggle value={s.login} onChange={() => saveToggle('login', !s.login, 'login-toggle', 'login_required')} />}
      state={stateText(s.login, '로그인 후 이용 가능', '누구나 이용 가능')} />,
    capture: <PolicyMiniCard title="캡처 방지" desc="스크린샷·화면 녹화 차단" icon="shield"
      toggle={<Toggle value={s.capture} onChange={() => saveToggle('capture', !s.capture, 'capture-toggle', 'capture_protection')} />}
      state={stateText(s.capture, '캡처 차단 중', '캡처 허용')} />,
    copy: <PolicyMiniCard title="복사 방지" desc="텍스트 선택 및 복사 차단" icon="copy"
      toggle={<Toggle value={s.copy} onChange={() => saveToggle('copy', !s.copy, 'copy-toggle', 'copy_protection')} />}
      state={stateText(s.copy, '복사 차단 중', '복사 허용')} />,
    pcWM: <PolicyMiniCard title="PC 워터마크" desc="데스크톱 브라우저 워터마크" icon="image"
      toggle={<Toggle value={s.pcWatermark} onChange={() => saveToggle('pcWatermark', !s.pcWatermark, 'watermark-toggle', 'watermark_enabled')} />}
      state={stateText(s.pcWatermark, 'PC 워터마크 표시', 'PC 워터마크 숨김')}
      extra={
        <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 12, color: 'var(--ink-3)' }}>
          항상 표시
          <Toggle size="sm" value={s.pcAlways} onChange={() => saveMode('pcAlways', !s.pcAlways, 'watermark-mode', 'watermark_mode')} />
        </label>
      } />,
    mobileWM: <PolicyMiniCard title="모바일 워터마크" desc="모바일 앱 워터마크" icon="image"
      toggle={<Toggle value={s.mobileWatermark} onChange={() => saveToggle('mobileWatermark', !s.mobileWatermark, 'mobile-watermark-toggle', 'mobile_watermark_enabled')} />}
      state={stateText(s.mobileWatermark, '모바일 워터마크 표시', '모바일 워터마크 숨김')}
      extra={
        <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 12, color: 'var(--ink-3)' }}>
          항상 표시
          <Toggle size="sm" value={s.mobileAlways} onChange={() => saveMode('mobileAlways', !s.mobileAlways, 'mobile-watermark-mode', 'mobile_watermark_mode')} />
        </label>
      } />,
    wmEdit: (
      <Card title="워터마크 편집" desc="표시할 내용·정렬·스타일을 설정합니다"
        actions={<Button variant="primary" size="sm" icon={<Icon name="check" size={13} />} onClick={saveWatermarkContent}>적용</Button>}>
        {/* 왼쪽: 텍스트, 오른쪽: 카카오ID / [정렬 + 글자색 1줄] */}
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 12 : 16, alignItems: 'start' }}>
          <div>
            <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>
              워터마크 텍스트
              <span style={{ fontWeight: 400, color: 'var(--ink-4)', marginLeft: 6 }}>· 최대 500자</span>
            </div>
            <textarea value={s.watermarkText} onChange={e => up('watermarkText', e.target.value.slice(0, 500))}
              maxLength={500}
              style={{
                width: '100%', minHeight: 180, padding: '12px 14px',
                border: '1px solid var(--line-strong)', borderRadius: 10,
                fontFamily: 'var(--mono)', fontSize: 15, resize: 'vertical', lineHeight: 1.6,
                outline: 'none', color: 'var(--ink)', boxSizing: 'border-box',
              }} />
            <div style={{ marginTop: 6, fontSize: 11.5, color: 'var(--ink-4)', display: 'flex', justifyContent: 'space-between' }}>
              <span>치환: {'{회원ID}'}, {'{카카오ID}'}</span>
              <span style={{ fontFamily: 'var(--mono)' }}>{s.watermarkText.length} / 500</span>
            </div>
          </div>
          <div style={{ display: 'grid', gap: 12 }}>
            {/* 원본: 라벨 바로 오른쪽에 토글 붙여 표시 */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <span style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600 }}>카카오 ID 표시</span>
              <Toggle value={s.kakaoId} onChange={() => up('kakaoId', !s.kakaoId)} />
              <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>{s.kakaoId ? '표시' : '숨김'}</span>
            </div>
            {/* 정렬(작게 왼쪽) + 글자색 오른쪽, 한 행 */}
            <div style={{ display: 'flex', alignItems: 'flex-end', gap: 12 }}>
              <Field label="정렬">
                <div style={{ display: 'flex', gap: 0, border: '1px solid var(--line-strong)', borderRadius: 6, overflow: 'hidden' }}>
                  {[
                    { v: 'left',   icon: 'alignLeft' },
                    { v: 'center', icon: 'alignCenter' },
                    { v: 'right',  icon: 'alignRight' },
                  ].map((o, i) => (
                    <button key={o.v} onClick={() => up('align', o.v)} style={{
                      width: 28, height: 28, padding: 0, cursor: 'pointer',
                      border: 'none', borderLeft: i > 0 ? '1px solid var(--line-strong)' : 'none',
                      background: s.align === o.v ? 'var(--accent-soft)' : '#fff',
                      color: s.align === o.v ? 'var(--accent-ink)' : 'var(--ink-2)',
                      display: 'grid', placeItems: 'center', fontFamily: 'inherit',
                    }}><Icon name={o.icon} size={12} /></button>
                  ))}
                </div>
              </Field>
              <div style={{ flex: 1 }}>
                <Field label="글자색">
                  <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 4 }}>
                    {['#000000','#ffffff','#ff0000','#ff6b00','#facc15','#22c55e','#3b82f6','#8b5cf6','#ec4899','#6b7280'].map(c => (
                      <button key={c} title={c} onClick={() => up('color', c)} style={{
                        width: 20, height: 20, padding: 0, cursor: 'pointer',
                        border: s.color.toLowerCase() === c ? '2px solid var(--accent)' : '1px solid #d1d5db',
                        borderRadius: 5, background: c, boxSizing: 'border-box',
                      }} />
                    ))}
                    <label style={{ position: 'relative', width: 20, height: 20, display: 'inline-block' }}>
                      <input type="color" value={s.color} onChange={e => up('color', e.target.value)}
                        style={{ position: 'absolute', inset: 0, opacity: 0, cursor: 'pointer' }} />
                      <span style={{ position: 'absolute', inset: 0, borderRadius: 5, border: '2px dashed #d1d5db', display: 'grid', placeItems: 'center', fontSize: 11, color: '#999', background: '#fff' }}>+</span>
                    </label>
                  </div>
                </Field>
              </div>
            </div>
            {/* 정렬 바로 아래: 줄바꿈 문자수 */}
            <Field label={`줄바꿈 문자수 · ${s.lineBreak}`}>
              <input type="range" min="1" max="50" value={s.lineBreak}
                onChange={e => up('lineBreak', Number(e.target.value))}
                onPointerDown={() => setSliderBusy(true)}
                onPointerUp={() => setSliderBusy(false)}
                onMouseDown={() => setSliderBusy(true)}
                onMouseUp={() => setSliderBusy(false)}
                onBlur={() => setSliderBusy(false)}
                style={{ width: '100%' }} />
            </Field>
            {/* 줄바꿈 아래: 불투명도 */}
            <Field label={`불투명도 · ${s.opacity}%`}>
              <input type="range" min="1" max="100" value={s.opacity}
                onChange={e => up('opacity', Number(e.target.value))}
                onPointerDown={() => setSliderBusy(true)}
                onPointerUp={() => setSliderBusy(false)}
                onMouseDown={() => setSliderBusy(true)}
                onMouseUp={() => setSliderBusy(false)}
                onBlur={() => setSliderBusy(false)}
                style={{ width: '100%' }} />
            </Field>
          </div>
        </div>
        {/* 미리보기 (카드 맨 하단) */}
        <div style={{ position: 'relative', height: 120, borderRadius: 10, border: '1px solid var(--line)', background: '#fafbfc', overflow: 'hidden', marginTop: 14 }}>
          <div style={{
            position: 'absolute', inset: 0, display: 'grid',
            placeItems: s.align === 'center' ? 'center' : (s.align === 'left' ? 'center start' : 'center end'),
            padding: 16, color: s.color, opacity: s.opacity / 100,
            fontFamily: 'var(--mono)', fontSize: 12, whiteSpace: 'pre-line', textAlign: s.align,
          }}>
            {s.watermarkText.replace(/\{회원ID\}/g, 'TSL00001').replace(/\{날짜시간\}/g, '2026-04-23 13:42').replace(/\{IP\}/g, '192.168.1.100').replace(/\{카카오ID\}/g, s.kakaoId ? 'kakao_user' : '')}
          </div>
          <span style={{ position: 'absolute', top: 8, right: 10, fontSize: 10.5, color: 'var(--ink-4)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>미리보기</span>
        </div>
      </Card>
    ),
    clipboard: (
      <Card title="클립보드 덮어쓰기" desc="복사된 내용을 지정 문구로 자동 교체합니다">
        <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>
              대체 문구 <span style={{ fontWeight: 400, color: 'var(--ink-4)', marginLeft: 6 }}>· 최대 100자</span>
            </div>
            <TextInput value={s.clipboardText} onChange={v => up('clipboardText', v)}
              placeholder="복사 시 대체할 텍스트" width="100%" maxLength={100} />
          </div>
          <Button variant="primary" icon={<Icon name="check" size={13} />} onClick={saveClipboardText}>적용</Button>
        </div>
      </Card>
    ),
  };
  const wideIds = new Set(['wmEdit', 'clipboard']);

  return (
    <div>
      <SectionHeader title="보안 관리"
        desc="로그인·콘텐츠 보호·워터마크 정책 · 카드를 드래그해 순서를 바꿀 수 있습니다" />

      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fit, minmax(340px, 1fr))', gap: isMobile ? 10 : 14 }}>
        {order.filter(id => cards[id]).map(id => (
          <div
            key={id}
            draggable={!sliderBusy}
            onDragStart={sliderBusy ? undefined : () => onDragStart(id)}
            onDragOver={sliderBusy ? undefined : onDragOver(id)}
            onDragLeave={sliderBusy ? undefined : onDragLeave}
            onDrop={sliderBusy ? undefined : onDrop(id)}
            onDragEnd={sliderBusy ? undefined : onDragEnd}
            style={{
              cursor: sliderBusy ? 'default' : (dragId === id ? 'grabbing' : 'grab'),
              opacity: dragId === id ? 0.45 : 1,
              transform: overId === id && dragId !== id ? 'scale(1.01)' : 'scale(1)',
              outline: overId === id && dragId !== id ? '2px solid var(--accent)' : 'none',
              outlineOffset: 2,
              borderRadius: 12,
              transition: 'transform 0.1s, outline 0.1s',
              gridColumn: isMobile ? 'auto' : (wideIds.has(id) ? '1 / -1' : 'auto'),
            }}
          >
            {cards[id]}
          </div>
        ))}
      </div>

      {/* [2026-07-07] 고유번호 시드 설정(토큰운영→보안관리 이동) + 추출자리 설정 — 마스터 전용(각 카드 자체 게이트) */}
      <div style={{ marginTop: isMobile ? 10 : 14, display: 'grid', gap: isMobile ? 10 : 14 }}>
        <SeedSettingsCard />
        <PositionsCard />
      </div>
    </div>
  );
};

const PolicyMiniCard = ({ title, desc, toggle, state, icon, extra }) => (
  <div style={{
    background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
    padding: 16, display: 'flex', flexDirection: 'column', gap: 12,
  }}>
    <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
      <div style={{ display: 'flex', gap: 10, flex: 1 }}>
        <div style={{
          width: 36, height: 36, borderRadius: 9, background: 'var(--surface-2)',
          display: 'grid', placeItems: 'center', color: 'var(--ink-3)', flexShrink: 0,
        }}><Icon name={icon} size={16} /></div>
        <div>
          <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--ink)' }}>{title}</div>
          <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 2, lineHeight: 1.4 }}>{desc}</div>
        </div>
      </div>
      {toggle}
    </div>
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingTop: 10, borderTop: '1px solid var(--line)' }}>
      {state}
    </div>
    {extra && <div style={{ paddingTop: 4, borderTop: '1px solid var(--line)' }}>{extra}</div>}
  </div>
);

// ═══════════════════════════════════════════════════════════════
// 정책 > 토큰운영 (3-2, 마스터)
// ═══════════════════════════════════════════════════════════════
const TokenOpsView = () => {
  const toast = useToast();
  const isMobile = useIsMobile();
  const [autoCharge, setAutoCharge] = React.useState(500);
  const [giftExpiry, setGiftExpiry] = React.useState(72);
  const [penaltyMonths, setPenaltyMonths] = React.useState(3);
  const [bonusGiftEnabled, setBonusGiftEnabled] = React.useState(true);
  // [BONUS_EXPIRE] 보너스 토큰 사용 기간
  const [bonusExpireEnabled, setBonusExpireEnabled] = React.useState(false);
  const [bonusExpireDays, setBonusExpireDays] = React.useState(30);

  // Fetch live settings on mount
  React.useEffect(() => {
    TSL_API.get('/settings/penalty').then(d => {
      if (typeof d.daily_tokens === 'number') setAutoCharge(d.daily_tokens);
      if (typeof d.penalty_months === 'number') setPenaltyMonths(d.penalty_months);
      if (typeof d.gift_expire_hours === 'number') setGiftExpiry(d.gift_expire_hours);
    }).catch(() => {});
    TSL_API.setting('bonus-gift-toggle').then(d => {
      setBonusGiftEnabled(!!d.bonus_gift_enabled);
    }).catch(() => {});
    // [BONUS_EXPIRE] 현재 설정 조회 (마스터 전용 엔드포인트)
    TSL_API.get('/settings/bonus-expire').then(d => {
      setBonusExpireEnabled(!!d.enabled);
      if (typeof d.days === 'number') setBonusExpireDays(d.days);
    }).catch(() => {});
    // [COIN_POLICY] 기능별 토큰 차감 정책 조회
    TSL_API.get('/settings/coin-policy').then(d => {
      setCoinPolEnabled(!!d.enabled);
      if (d.policies && typeof d.policies === 'object') setCoinPolRates(r => ({ ...r, ...d.policies }));
    }).catch(() => {});
  }, []);

  // 저장: 원본 savePenaltySettings / saveDailyTokens / saveGiftExpire
  const savePenaltyAll = async (partial) => {
    const next = { daily_tokens: autoCharge, penalty_months: penaltyMonths, gift_expire_hours: giftExpiry, ...partial };
    try {
      const res = await fetch('/admin/settings/penalty', { method: 'PUT', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(next) });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
      toast('success', '설정이 저장되었습니다');
    } catch (err) {
      toast('error', '저장 실패: ' + (err.message || '네트워크 오류'));
    }
  };
  const saveBonusGift = (v) => {
    setBonusGiftEnabled(v);
    TSL_API.saveSetting('bonus-gift-toggle', { bonus_gift_enabled: v })
      .then(() => toast('success', v ? '보너스 선물 활성화' : '보너스 선물 비활성화'))
      .catch(err => { setBonusGiftEnabled(!v); toast('error', '저장 실패: ' + err.message); });
  };
  // [BONUS_EXPIRE] 설정 저장 (PUT /admin/settings/bonus-expire, 마스터)
  const saveBonusExpire = async (partial) => {
    const next = { enabled: bonusExpireEnabled, days: bonusExpireDays, ...partial };
    if (next.enabled && (!(next.days >= 1))) {
      toast('error', '사용 기간을 켜려면 만료 일수가 1일 이상이어야 합니다 (0일은 지급 즉시 만료)');
      throw new Error('days<1');
    }
    try {
      const res = await fetch('/admin/settings/bonus-expire', { method: 'PUT', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(next) });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
      toast('success', '보너스 사용 기간 설정이 저장되었습니다');
    } catch (err) {
      toast('error', '저장 실패: ' + (err.message || '네트워크 오류'));
    }
  };
  const toggleBonusExpire = (v) => {
    // 켜는데 일수가 0(미설정)이면 30일로 자동 보정해 함께 저장 (0일=즉시만료 위험이라 서버가 거부함)
    let d = bonusExpireDays;
    if (v && !(d >= 1)) { d = 30; setBonusExpireDays(30); toast('info', '만료 일수가 0이라 30일로 설정했습니다. 아래에서 조정하세요.'); }
    setBonusExpireEnabled(v);
    saveBonusExpire({ enabled: v, days: d }).catch(() => setBonusExpireEnabled(!v));
  };
  const applyDailyTokensNow = async () => {
    if (!confirm(`자동충전 수량을 ${autoCharge}개로 저장하고, 전체 회원 토큰을 지금 즉시 리셋합니다.\n\n계속하시겠습니까?`)) return;
    try {
      const res = await fetch('/admin/settings/apply-daily-tokens-now', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ daily_tokens: autoCharge }) });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
      const cnt = data && (data.applied_to != null ? data.applied_to : (data.count != null ? data.count : (data.updated != null ? data.updated : null)));
      toast('success', cnt != null ? `전체 회원 토큰이 즉시 리셋되었습니다 (${cnt}건)` : '전체 회원 토큰이 즉시 리셋되었습니다');
    } catch (err) {
      toast('error', '실패: ' + (err.message || '네트워크 오류'));
    }
  };

  // 드래그 순서 (원본 data-drag-id 패턴)
  // [COIN_POLICY] 기능별 토큰 차감 정책 상태 (off=현행: 일반질문 1·특수기능 무료)
  const COINPOL_FEATURES = [
    ['chat', '일반 질문'], ['yeobi', '여비정산서'], ['meeting', '회의록'], ['report', '업무보고'],
    ['retire', '퇴직금 계산'], ['ins4', '4대보험료 정산'], ['edi', '4대보험 신고서'], ['ovw', '운영위 자료'], ['secom', '초과근무 등록'], ['xmerge', '엑셀 취합'], ['retiredoc', '직고용 퇴직금'],
  ];
  const [coinPolEnabled, setCoinPolEnabled] = React.useState(false);
  const [coinPolRates, setCoinPolRates] = React.useState({ chat: 1, yeobi: 1, meeting: 1, report: 1, retire: 1, ins4: 1, edi: 1, ovw: 1, secom: 1, xmerge: 0, retiredoc: 0 });
  const saveCoinPolicy = async (partial) => {
    const next = { enabled: coinPolEnabled, default: 1, policies: coinPolRates, ...partial };
    try {
      const res = await fetch('/admin/settings/coin-policy', { method: 'PUT', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(next) });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
      toast('success', '기능별 차감 정책이 저장되었습니다');
    } catch (err) {
      toast('error', '저장 실패: ' + (err.message || '네트워크 오류'));
      throw err;
    }
  };
  const toggleCoinPolicy = (v) => {
    setCoinPolEnabled(v);
    saveCoinPolicy({ enabled: v }).catch(() => setCoinPolEnabled(!v));
  };


  const defaultOrder = ['autoCharge', 'giftExpiry', 'penaltyMonths', 'bonusGift', 'bonusExpire', 'coinPolicy'];
  const [order, setOrder] = React.useState(() => {
    let saved;
    try { saved = JSON.parse(localStorage.getItem('tsl.tokenops.order')); } catch { saved = null; }
    if (!Array.isArray(saved)) return defaultOrder;
    // 신규 카드가 저장된 순서에 없으면 뒤에 append (하위호환)
    return defaultOrder.reduce((acc, id) => acc.includes(id) ? acc : [...acc, id], saved);
  });
  React.useEffect(() => { localStorage.setItem('tsl.tokenops.order', JSON.stringify(order)); }, [order]);
  const [dragId, setDragId] = React.useState(null);
  const [overId, setOverId] = React.useState(null);
  const onDragStart = (id) => setDragId(id);
  const onDragOver = (id) => (e) => { e.preventDefault(); if (id !== overId) setOverId(id); };
  const onDragLeave = () => setOverId(null);
  const onDrop = (targetId) => (e) => {
    e.preventDefault();
    if (!dragId || dragId === targetId) { setDragId(null); setOverId(null); return; }
    setOrder(o => {
      const next = [...o];
      const from = next.indexOf(dragId);
      const to = next.indexOf(targetId);
      next.splice(from, 1);
      next.splice(to, 0, dragId);
      return next;
    });
    setDragId(null); setOverId(null);
  };
  const onDragEnd = () => { setDragId(null); setOverId(null); };

  const cards = {
    autoCharge: (
      <PolicyBigCard
        title="자동 충전 수량"
        desc="매주 토요일 밤 12시(일요일 00:00 KST)에 모든 회원의 일반 토큰이 설정한 수량으로 초기화됩니다."
        icon="zap"
      >
        <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap' }}>
          <div style={{ flex: isMobile ? '1 1 100%' : 1, minWidth: 0 }}>
            <NumberApply value={autoCharge} onChange={setAutoCharge} min={0} max={9999} unit="T" step={100} hideApply />
          </div>
          <Button variant="primary" icon={<Icon name="check" size={13} />} onClick={() => savePenaltyAll({ daily_tokens: autoCharge })}>적용</Button>
          <Button style={{ background: '#f59e0b', color: '#fff', border: '1px solid transparent' }} icon={<Icon name="bolt" size={13} />} onClick={applyDailyTokensNow}>즉시변경</Button>
        </div>
      </PolicyBigCard>
    ),
    giftExpiry: (
      <PolicyBigCard
        title="선물 수락 기한"
        desc="선물/조르기 요청의 수락 대기 시간입니다. 기한 초과 시 자동 반려됩니다."
        icon="gift"
      >
        <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap' }}>
          <div style={{ flex: isMobile ? '1 1 100%' : 1, minWidth: 0 }}>
            <NumberApply value={giftExpiry} onChange={setGiftExpiry} min={1} max={720} unit="시간" step={1} hideApply />
          </div>
          <Button variant="primary" icon={<Icon name="check" size={13} />} onClick={() => savePenaltyAll({ gift_expire_hours: giftExpiry })}>적용</Button>
        </div>
      </PolicyBigCard>
    ),
    penaltyMonths: (
      <PolicyBigCard
        title="패널티 기간"
        desc="탈퇴 후 재가입한 회원에게 적용되는 패널티 기간입니다. 기간 동안 선물하기·조르기·선물받기가 차단됩니다."
        icon="flag"
      >
        <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap' }}>
          <div style={{ flex: isMobile ? '1 1 100%' : 1, minWidth: 0 }}>
            <NumberApply value={penaltyMonths} onChange={setPenaltyMonths} min={0} max={120} unit="개월" step={1} hideApply />
          </div>
          <Button variant="primary" icon={<Icon name="check" size={13} />} onClick={() => savePenaltyAll({ penalty_months: penaltyMonths })}>적용</Button>
        </div>
      </PolicyBigCard>
    ),
    bonusGift: (
      <PolicyBigCard
        title="보너스 토큰 선물"
        desc="회원 간 보너스 토큰 선물 기능 활성화"
        icon="gift"
      >
        <div style={{
          padding: '16px 14px', borderRadius: 10, background: 'var(--surface-2)',
          border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
        }}>
          <div>
            <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
              {bonusGiftEnabled ? '활성화' : '비활성화'}
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-3)', marginTop: 3 }}>
              {bonusGiftEnabled ? '회원들이 서로 보너스 토큰을 선물할 수 있습니다' : '보너스 토큰 선물이 차단됩니다'}
            </div>
          </div>
          <Toggle value={bonusGiftEnabled} onChange={() => saveBonusGift(!bonusGiftEnabled)} />
        </div>
      </PolicyBigCard>
    ),
    // [BONUS_EXPIRE] 보너스 토큰 사용 기간 카드
    bonusExpire: (
      <PolicyBigCard
        title="보너스 토큰 사용 기간"
        desc="보너스 토큰 지급 후 설정 일수 뒤 자동 만료"
        icon="clock"
      >
        <div style={{
          padding: '16px 14px', borderRadius: 10, background: 'var(--surface-2)',
          border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
        }}>
          <div>
            <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
              {bonusExpireEnabled ? '활성화' : '비활성화'}
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-3)', marginTop: 3 }}>
              {bonusExpireEnabled ? '지급 후 설정 일수 뒤 보너스 토큰이 자동 만료됩니다' : '보너스 토큰은 만료되지 않습니다'}
            </div>
          </div>
          <Toggle value={bonusExpireEnabled} onChange={() => toggleBonusExpire(!bonusExpireEnabled)} />
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap', marginTop: 4 }}>
          <div style={{ flex: isMobile ? '1 1 100%' : 1, minWidth: 0 }}>
            <NumberApply value={bonusExpireDays} onChange={setBonusExpireDays} min={1} max={365} unit="일" step={1} hideApply />
          </div>
          <Button variant="primary" icon={<Icon name="check" size={13} />} onClick={() => saveBonusExpire({ days: Math.max(1, bonusExpireDays) })}>적용</Button>
        </div>
      </PolicyBigCard>
    ),
    // [COIN_POLICY] 기능별 토큰 차감 정책 카드
    coinPolicy: (
      <PolicyBigCard
        title="기능별 토큰 차감"
        desc="일반 질문·문서생성 기능별로 차감 토큰 수를 다르게 설정"
        icon="coin"
      >
        <div style={{
          padding: '16px 14px', borderRadius: 10, background: 'var(--surface-2)',
          border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
        }}>
          <div>
            <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
              {coinPolEnabled ? '활성화' : '비활성화'}
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-3)', marginTop: 3 }}>
              {coinPolEnabled ? '아래 요율대로 기능 완료 시 차감됩니다' : '현행 유지: 일반 질문만 1개 차감, 문서생성 기능은 무료'}
            </div>
          </div>
          <Toggle value={coinPolEnabled} onChange={() => toggleCoinPolicy(!coinPolEnabled)} />
        </div>
        {coinPolEnabled && (
          <div style={{ marginTop: 4 }}>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 8 }}>
              {COINPOL_FEATURES.map(([k, label]) => (
                <div key={k} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, padding: '7px 10px', borderRadius: 8, background: '#fff', border: '1px solid var(--line)' }}>
                  <div style={{ fontSize: 12.5, color: 'var(--ink-2)', fontWeight: 600 }}>{label}</div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
                    <input type="number" min={0} max={99} value={coinPolRates[k]}
                      onChange={e => { const v = Math.max(0, Math.min(99, parseInt(e.target.value || '0', 10))); setCoinPolRates(r => ({ ...r, [k]: v })); }}
                      style={{ width: 52, padding: '5px 6px', border: '1px solid var(--line-strong)', borderRadius: 7, fontSize: 13, textAlign: 'right', fontFamily: 'var(--mono)', outline: 'none' }} />
                    <span style={{ fontSize: 11.5, color: 'var(--ink-3)' }}>개</span>
                  </div>
                </div>
              ))}
            </div>
            <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
              <Button variant="primary" icon={<Icon name="check" size={13} />} onClick={() => saveCoinPolicy({})}>요율 저장</Button>
            </div>
          </div>
        )}
      </PolicyBigCard>
    ),
  };

  return (
    <div>
      <SectionHeader title="토큰 운영"
        desc="자동 충전·선물·패널티·보너스 선물 정책 · 카드를 드래그해 순서를 바꿀 수 있습니다" />

      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fit, minmax(380px, 1fr))', gap: isMobile ? 10 : 14 }}>
        {order.filter(id => cards[id]).map(id => (
          <div key={id}
            draggable
            onDragStart={() => onDragStart(id)}
            onDragOver={onDragOver(id)}
            onDragLeave={onDragLeave}
            onDrop={onDrop(id)}
            onDragEnd={onDragEnd}
            style={{
              cursor: 'grab',
              opacity: dragId === id ? 0.4 : 1,
              outline: overId === id && dragId !== id ? '2px dashed var(--accent)' : 'none',
              outlineOffset: -2, borderRadius: 12,
              transition: 'opacity .15s, outline-color .15s',
            }}>{cards[id]}</div>
        ))}
      </div>

    </div>
  );
};

// ═══════════════════════════════════════════════════════════════
// 정책 > 토큰운영 > 고유번호 시드 설정 (마스터 전용)
// 메인(충남) = 단일시드(UNIQUECODE_ENC, region 슬러그 없음) · 전 회원 대상.
// ★현재시드 게이트★: reseed 는 { old_seed, new_seed } 를 보내고,
//   서버가 저장된 시드와 old_seed 를 대조 → 불일치면 403('현재 시드가 일치하지 않습니다').
//   현재 시드 값은 화면에 노출하지 않음(설정됨 ✓ 만 표시).
// (applyDailyTokensNow 와 동일한 confirm + toast 패턴)
// ═══════════════════════════════════════════════════════════════
const SeedSettingsCard = () => {
  const toast = useToast();
  const isMobile = useIsMobile();
  const [myRole, setMyRole] = React.useState(null);
  const [seedInfo, setSeedInfo] = React.useState(null); // 값X — 설정 여부만
  const [oldSeed, setOldSeed] = React.useState('');
  const [newSeed, setNewSeed] = React.useState('');
  const [working, setWorking] = React.useState(false);

  React.useEffect(() => {
    fetch('/admin/me', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(d => setMyRole(d && d.role ? d.role : null))
      .catch(() => {});
    fetch('/admin/settings/uniquecode-seed', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) setSeedInfo(d); })
      .catch(() => {});
  }, []);
  const isMaster = myRole === 'master';
  const seedSet = seedInfo && seedInfo.configured !== false;

  const applyReseed = async () => {
    const oldS = (oldSeed || '').trim();
    const newS = (newSeed || '').trim();
    if (!/^\d+$/.test(oldS)) { toast('error', '현재 시드는 숫자만 입력하세요'); return; }
    if (!/^\d+$/.test(newS)) { toast('error', '새 시드는 숫자만 입력하세요'); return; }
    if (oldS === newS) { toast('error', '새 시드가 현재 시드와 동일합니다'); return; }
    if (!confirm(`고유번호 시드를 ${newS} 로 변경하고, 전체 회원의 고유번호를 지금 즉시 재계산합니다.\n\n이 작업은 모든 회원의 고유번호를 바꿉니다. 계속하시겠습니까?`)) return;
    setWorking(true);
    try {
      const res = await fetch('/admin/settings/reseed-uniquecode', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ old_seed: oldS, new_seed: newS }) });
      if (res.status === 403) { toast('error', '현재 시드가 일치하지 않습니다'); return; }
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
      const cnt = data && (data.recalculated != null && data.recalculated !== true && data.recalculated !== false ? data.recalculated : (data.updated != null ? data.updated : (data.count != null ? data.count : null)));
      toast('success', cnt != null ? `시드 변경 + 전체 재계산 완료 (${cnt}건)` : '시드 변경 + 전체 재계산 완료');
      setOldSeed('');
      setNewSeed('');
      setSeedInfo(si => ({ ...(si || {}), configured: true }));
    } catch (err) {
      toast('error', '실패: ' + (err.message || '네트워크 오류'));
    } finally {
      setWorking(false);
    }
  };

  if (!isMaster) return null;

  const inputStyle = {
    width: '100%', padding: '10px 12px', border: '1px solid var(--line-strong)',
    borderRadius: 10, fontSize: 13, outline: 'none', color: 'var(--ink)',
    fontFamily: 'var(--mono)', boxSizing: 'border-box',
  };

  return (
    <PolicyBigCard
      title="고유번호 시드 설정"
      desc="회원 고유번호 생성에 사용되는 시드값입니다. 변경 시 전체 회원의 고유번호가 즉시 재계산됩니다."
      icon="key"
      hint="보안을 위해 현재 시드 값은 화면에 표시되지 않습니다. 변경하려면 현재 시드를 정확히 입력해야 하며, 일치할 때만 전체 회원의 고유번호가 새 값으로 재계산됩니다."
    >
      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 8 }}>
        <div>
          <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>현재 시드</div>
          <input value={oldSeed} onChange={e => setOldSeed(e.target.value.replace(/\D/g, ''))}
            placeholder="현재 시드 (숫자)" inputMode="numeric" autoComplete="off" style={inputStyle} />
        </div>
        <div>
          <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>새 시드</div>
          <input value={newSeed} onChange={e => setNewSeed(e.target.value.replace(/\D/g, ''))}
            placeholder="새 시드 (숫자)" inputMode="numeric" autoComplete="off" style={inputStyle} />
        </div>
      </div>
      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <Button disabled={working || !oldSeed || !newSeed}
          style={{ background: '#dc2626', color: '#fff', border: '1px solid transparent', opacity: (working || !oldSeed || !newSeed) ? 0.5 : 1 }}
          icon={<Icon name="bolt" size={13} />} onClick={applyReseed}>
          {working ? '재계산 중...' : '시드 변경 + 전체 재계산'}
        </Button>
      </div>
    </PolicyBigCard>
  );
};

// ═══════════════════════════════════════════════════════════════
// 정책 > 보안관리 > 고유번호 추출자리 설정 (마스터 전용)
// 현재 추출자리(GET /admin/settings/uniquecode-positions)를 표시하고,
// 변경은 ★전화가 서버에 오지 않는★ exe(회원관리) 재설정모드 플로우로만:
//   ① 아래에서 새 자리 선택 → ② exe 재설정모드에 옛자리/새자리 동일하게 입력해
//   매핑파일([옛고유번호,새고유번호,이름2]) 생성 → ③ 여기 업로드 → 검증(dry-run) → 적용.
// 적용 시 서버가 DB 교체 + 자리 재봉인 + 자동 재시작(POST /admin/members/rekey).
// ═══════════════════════════════════════════════════════════════
const PositionsCard = () => {
  const toast = useToast();
  const isMobile = useIsMobile();
  const [myRole, setMyRole] = React.useState(null);
  const [cur, setCur] = React.useState(null);       // 현재 자리(서버)
  const [sel, setSel] = React.useState([]);          // 새 자리 선택
  const [file, setFile] = React.useState(null);
  const [report, setReport] = React.useState(null);
  const [working, setWorking] = React.useState(false);
  const fileRef = React.useRef(null);

  React.useEffect(() => {
    fetch('/admin/me', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => setMyRole(d && d.role ? d.role : null)).catch(() => {});
    fetch('/admin/settings/uniquecode-positions', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d && Array.isArray(d.positions)) { setCur(d.positions); setSel(d.positions); } })
      .catch(() => {});
  }, []);

  if (myRole !== 'master') return null;

  const togglePos = (n) => setSel(s => s.includes(n) ? s.filter(x => x !== n) : [...s, n].sort((a, b) => a - b));
  const changed = cur && JSON.stringify(sel) !== JSON.stringify(cur);

  const run = async (apply) => {
    if (!file) { toast('error', 'exe 재설정모드에서 만든 매핑파일(.xlsx)을 선택하세요'); return; }
    if (apply && !sel.length) { toast('error', '새 추출자리를 1개 이상 선택하세요'); return; }
    if (apply && !confirm(`추출자리를 [${sel.join(',')}] 로 변경하고, 매핑파일대로 전체 회원 고유번호를 교체합니다.\n\n적용 직후 서버가 자동 재시작되며 이후 로그인부터 새 자리가 적용됩니다.\n(사전 백업 자동 생성 · 관리자 코드는 보존)\n\n계속하시겠습니까?`)) return;
    setWorking(true); setReport(null);
    try {
      const fd = new FormData();
      fd.append('file', file);
      if (apply) { fd.append('apply', '1'); fd.append('confirm', 'REKEY'); if (changed) fd.append('newPos', sel.join(',')); }
      const res = await fetch('/admin/members/rekey', { method: 'POST', credentials: 'same-origin', body: fd });
      const data = await res.json().catch(() => ({}));
      setReport(data);
      if (!res.ok) { toast('error', data.error || ('HTTP ' + res.status)); return; }
      if (data.dryRun) {
        toast(data.ok ? 'success' : 'error', data.ok ? '검증 통과 — 적용 가능합니다' : '검증 실패 — 아래 상세를 확인하세요');
      } else {
        toast('success', `적용 완료 (${data.updated}건 교체)` + (data.configNote && data.configNote.positions && data.configNote.positions.applied ? ' — 새 자리 재봉인 + 서버 자동 재시작 중' : ''));
        if (data.configNote && data.configNote.positions && data.configNote.positions.applied) setCur(sel);
        setFile(null); if (fileRef.current) fileRef.current.value = '';
      }
    } catch (e) {
      toast('error', '실패: ' + (e.message || '네트워크 오류'));
    } finally { setWorking(false); }
  };

  const chip = (n) => {
    const on = sel.includes(n);
    return (
      <button key={n} onClick={() => togglePos(n)} style={{
        width: 38, height: 38, borderRadius: 9, cursor: 'pointer', fontWeight: 700, fontSize: 14,
        border: on ? '2px solid var(--accent)' : '1px solid var(--line-strong)',
        background: on ? 'var(--accent-soft)' : '#fff', color: on ? 'var(--accent)' : 'var(--ink-3)',
      }}>{n}</button>
    );
  };

  const chk = report && report.checks;
  return (
    <PolicyBigCard
      title="고유번호 추출자리 설정"
      desc="전화번호(010 제외 뒤 8자리)에서 어느 위치의 숫자를 추출해 고유번호를 만들지 정합니다. 로그인 인증에 즉시 사용됩니다."
      icon="shield"
      hint="자리 변경은 전체 회원 고유번호 재계산이 필요합니다. 전화번호는 서버로 오지 않습니다 — 회원관리 프로그램(exe) 재설정모드에서 [옛자리→새자리] 매핑파일(고유번호만 포함)을 만들어 업로드하세요. 검증(dry-run) 통과 후에만 적용됩니다."
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 12.5, color: 'var(--ink-2)', fontWeight: 600 }}>현재 자리</div>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 14, fontWeight: 700, color: 'var(--ink)' }}>
          {cur ? cur.join(', ') : '조회 중...'}
        </div>
      </div>
      <div>
        <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>새 자리 선택 (1~8번째, 복수)</div>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>{[1, 2, 3, 4, 5, 6, 7, 8].map(chip)}</div>
        {changed && <div style={{ fontSize: 11.5, color: '#dc2626', marginTop: 6 }}>변경 예정: [{cur.join(',')}] → [{sel.join(',')}] — 적용 시 전 회원 고유번호가 바뀝니다</div>}
      </div>
      <div>
        <div style={{ fontSize: 12, color: 'var(--ink-2)', fontWeight: 600, marginBottom: 6 }}>exe 재설정 매핑파일 (.xlsx · 옛고유번호/새고유번호 — 전화 미포함)</div>
        <input ref={fileRef} type="file" accept=".xlsx" onChange={e => setFile(e.target.files && e.target.files[0] || null)}
          style={{ fontSize: 12.5, width: '100%' }} />
      </div>
      {report && (
        <div style={{ padding: '10px 12px', borderRadius: 10, background: 'var(--surface-2)', border: '1px solid var(--line)', fontSize: 12, lineHeight: 1.6 }}>
          {chk && <div style={{ fontFamily: 'var(--mono)' }}>
            매핑 {chk.mappingRows}행 · 변경 {chk.changeCount} · DB일치 {chk.matched} · 미존재 {chk.missCount} · 누락 {chk.uncoveredCount} · 충돌 {chk.newCollidesExistingCount}
          </div>}
          {report.fatal && report.fatal.length > 0 && (
            <div style={{ color: '#dc2626', marginTop: 4 }}>{report.fatal.map((f, i) => <div key={i}>✗ {f}</div>)}</div>
          )}
          {report.dryRun && report.ok && <div style={{ color: '#16a34a', marginTop: 4 }}>✓ 검증 통과 — 적용 가능</div>}
          {report.applied && <div style={{ color: '#16a34a', marginTop: 4 }}>✓ 적용 완료 ({report.updated}건){report.configNote && report.configNote.positions && report.configNote.positions.warning ? ' · ★' + report.configNote.positions.warning + '★' : ''}</div>}
        </div>
      )}
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
        <Button disabled={working || !file} icon={<Icon name="search" size={13} />} onClick={() => run(false)}>
          {working ? '처리 중...' : '검증 (dry-run)'}
        </Button>
        <Button disabled={working || !file || !(report && report.dryRun && report.ok)}
          style={{ background: '#dc2626', color: '#fff', border: '1px solid transparent', opacity: (working || !file || !(report && report.dryRun && report.ok)) ? 0.5 : 1 }}
          icon={<Icon name="bolt" size={13} />} onClick={() => run(true)}>
          적용 (전체 교체{changed ? ' + 자리 변경' : ''})
        </Button>
      </div>
    </PolicyBigCard>
  );
};

const PolicyBigCard = ({ title, desc, icon, hint, children }) => {
  const isMobile = useIsMobile();
  return (
  <div style={{
    background: '#fff', border: '1px solid var(--line)', borderRadius: 12,
    padding: isMobile ? 12 : 18,
    display: 'flex', flexDirection: 'column', gap: isMobile ? 10 : 14,
    minWidth: 0,
  }}>
    <div style={{ display: 'flex', gap: 12 }}>
      <div style={{
        width: 40, height: 40, borderRadius: 10, flexShrink: 0,
        background: 'var(--accent-soft)', display: 'grid', placeItems: 'center',
        color: 'var(--accent-ink)',
      }}><Icon name={icon} size={18} /></div>
      <div>
        <div style={{ fontSize: 14.5, fontWeight: 700, color: 'var(--ink)', letterSpacing: '-0.01em' }}>{title}</div>
        <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 3, lineHeight: 1.45 }}>{desc}</div>
      </div>
    </div>
    {children}
    {hint && (
      <div style={{
        fontSize: 11.5, color: 'var(--ink-3)', padding: '8px 10px',
        background: 'var(--surface-2)', borderRadius: 8, display: 'flex', gap: 6, lineHeight: 1.45,
      }}>
        <span style={{ color: 'var(--ink-4)', flexShrink: 0 }}><Icon name="info" size={13} /></span>
        {hint}
      </div>
    )}
  </div>
  );
};

const NumberApply = ({ value, onChange, min, max, unit, step, hideApply, onApply }) => {
  const isMobile = useIsMobile();
  return (
  <div style={{ display: 'flex', gap: 8, alignItems: 'stretch', minWidth: 0 }}>
    <div style={{
      flex: 1, display: 'flex', alignItems: 'center', gap: 0, minWidth: 0,
      border: '1px solid var(--line-strong)', borderRadius: 10, background: '#fff',
      overflow: 'hidden', height: isMobile ? 40 : 44,
    }}>
      <button onClick={() => onChange(Math.max(min, value - step))} style={stepBtn}>−</button>
      <div style={{ flex: 1, display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 6 }}>
        <input
          type="number" value={value} min={min} max={max}
          onChange={e => {
            const v = Number(e.target.value);
            if (Number.isNaN(v)) return;
            onChange(Math.min(max, Math.max(min, v)));
          }}
          style={{
            width: isMobile ? 60 : 80, minWidth: 0, textAlign: 'right', border: 'none', outline: 'none',
            fontFamily: 'var(--mono)', fontSize: isMobile ? 16 : 18, fontWeight: 700,
            color: 'var(--ink)', background: 'transparent', fontVariantNumeric: 'tabular-nums',
          }} />
        <span style={{ fontSize: 12.5, color: 'var(--ink-3)', fontWeight: 500 }}>{unit}</span>
      </div>
      <button onClick={() => onChange(Math.min(max, value + step))} style={stepBtn}>+</button>
    </div>
    {!hideApply && <Button variant="primary" size="lg" icon={<Icon name="check" size={13} />} onClick={onApply}>적용</Button>}
  </div>
  );
};

const stepBtn = {
  width: 44, height: '100%', border: 'none', background: '#fafbfc',
  color: 'var(--ink-2)', cursor: 'pointer', fontSize: 18, fontWeight: 600,
  fontFamily: 'inherit',
};

// ═══════════════════════════════════════════════════════════════
// AppDownloadView — 모바일 어플 다운로드 (PWA install)
// 진짜 설치 가능할 때만(=브라우저가 beforeinstallprompt 발화) 버튼 활성화.
// HTTPS·SW·manifest 조건 모두 충족 시에만 발화함.
// ═══════════════════════════════════════════════════════════════
const AppDownloadView = () => {
  const isMobile = useIsMobile();
  const [canInstall, setCanInstall] = React.useState(!!(window.__pwa && window.__pwa.deferred));
  const [installed, setInstalled]   = React.useState(!!(window.__pwa && window.__pwa.installed));
  // 설치 진행 단계: 'idle' | 'awaiting' (사용자 선택 대기) | 'installing' (수락 후 실제 설치 대기)
  const [phase, setPhase] = React.useState('idle');

  React.useEffect(() => {
    const onReady = () => setCanInstall(true);
    const onDone  = () => { setInstalled(true); setCanInstall(false); setPhase('idle'); };
    window.addEventListener('tsl-pwa-ready', onReady);
    window.addEventListener('tsl-pwa-installed', onDone);
    return () => {
      window.removeEventListener('tsl-pwa-ready', onReady);
      window.removeEventListener('tsl-pwa-installed', onDone);
    };
  }, []);

  async function handleInstall() {
    const dp = window.__pwa && window.__pwa.deferred;
    if (!dp) return;
    setPhase('awaiting');
    try {
      dp.prompt();
      const choice = await dp.userChoice;
      window.__pwa.deferred = null;
      setCanInstall(false);
      if (choice && choice.outcome === 'accepted') {
        // 사용자는 수락했지만 실제 설치 완료는 'appinstalled' 이벤트로만 확인.
        // 이벤트가 안 오면 일정 시간 후 idle 복귀(드물지만 일부 브라우저에서 발생).
        setPhase('installing');
        setTimeout(() => {
          setPhase(p => (p === 'installing' ? 'idle' : p));
        }, 30000);
      } else {
        setPhase('idle');
      }
    } catch {
      setPhase('idle');
    }
  }

  const cardW = isMobile ? '100%' : 360;
  const isInsecure = typeof window !== 'undefined' && !window.isSecureContext;
  const isBusy = phase === 'awaiting' || phase === 'installing';
  const buttonLabel =
    phase === 'awaiting'   ? '설치 확인 중…' :
    phase === 'installing' ? '다운로드 중…' :
    '어플 설치하기';

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      gap: 18, padding: isMobile ? '20px 8px' : '40px 20px',
    }}>
      <div style={{
        background: '#fff', border: '1px solid var(--line)', borderRadius: 16,
        padding: isMobile ? '24px 18px' : '32px 28px',
        boxShadow: 'var(--shadow-md)',
        width: cardW, maxWidth: '100%', textAlign: 'center',
      }}>
        <img src="/uploads/app-icon-512.png" alt="TSL"
          style={{
            width: 120, height: 120, borderRadius: 24, display: 'block',
            margin: '0 auto 18px', boxShadow: '0 4px 16px rgba(15,23,42,0.10)',
            background: '#fff',
          }}
        />
        <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--ink)', marginBottom: 6 }}>
          TSL 관리자
        </div>
        <div style={{ fontSize: 13, color: 'var(--ink-3)', lineHeight: 1.55, marginBottom: 22 }}>
          홈 화면에 추가해 어플처럼 사용할 수 있습니다.
        </div>

        {installed ? (
          <div style={{
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
            padding: '12px 16px', borderRadius: 12,
            background: 'var(--ok-soft)', color: 'var(--ok)',
            fontSize: 13.5, fontWeight: 600, width: '100%',
          }}>
            <Icon name="check" size={16} /> 이미 설치되었습니다
          </div>
        ) : (
          <button
            onClick={handleInstall}
            disabled={!canInstall || isBusy}
            style={{
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
              width: '100%', padding: '13px 18px', borderRadius: 12,
              border: 'none', cursor: (canInstall && !isBusy) ? 'pointer' : 'not-allowed',
              background: (canInstall && !isBusy) ? 'var(--accent)' : (isBusy ? 'var(--accent-soft)' : '#e5e7eb'),
              color: (canInstall && !isBusy) ? '#fff' : (isBusy ? 'var(--accent-ink)' : 'var(--ink-4)'),
              fontSize: 14.5, fontWeight: 700, fontFamily: 'inherit',
            }}
          >
            <Icon name="download" size={16} /> {buttonLabel}
          </button>
        )}

        {!installed && !canInstall && isInsecure && (
          <div style={{
            marginTop: 12, padding: '10px 12px', borderRadius: 8,
            background: '#fffbeb', border: '1px solid #fde68a',
            fontSize: 11.5, color: '#78350f', lineHeight: 1.55, textAlign: 'left',
          }}>
            현재 주소가 <b>http://</b>(보안되지 않음)이라 자동 설치가 차단됩니다.
            HTTPS 주소로 접속하시면 버튼이 활성화됩니다.
          </div>
        )}
      </div>
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════
// CategoryView / TagView — 신규 개발 대기 (placeholder)
// 백엔드 endpoint (routes/admin.js) 및 DynamoDB 테이블 미구현.
// 사용자 결정 후 본 구현 진행: 어떤 카테고리/태그를 관리할 것인가?
//   (a) 검색 카테고리 (법령/단체협약/임용규정 등)
//   (b) 회원 태그 (관심분야/직무 등)
//   (c) 정책 분류 (공지/약관 등)
// ═══════════════════════════════════════════════════════════════
const PolicyPlaceholder = ({ title, options }) => {
  const isMobile = useIsMobile();
  return (
    <div style={{ padding: isMobile ? '20px 14px' : '40px 28px', maxWidth: 720, margin: '0 auto' }}>
      <div style={{
        background: '#fff', border: '1px solid var(--line)', borderRadius: 16,
        padding: isMobile ? '24px 20px' : '32px 28px', boxShadow: 'var(--shadow-md)',
      }}>
        <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--ink)', marginBottom: 8 }}>
          {title}
        </div>
        <div style={{ fontSize: 13, color: 'var(--ink-3)', lineHeight: 1.6, marginBottom: 18 }}>
          신규 개발 대기 — 메인 서버에도 동일 기능이 없어 사용자 결정이 필요합니다.
          어떤 항목을 관리할지 확정 후 backend(routes/admin.js) + DynamoDB 테이블 + 본 화면을 일괄 구현합니다.
        </div>
        <div style={{
          padding: '14px 16px', background: 'var(--accent-soft)', borderRadius: 10,
          fontSize: 12.5, color: 'var(--accent-ink)', lineHeight: 1.7,
        }}>
          <div style={{ fontWeight: 700, marginBottom: 6 }}>구현 후보</div>
          <ul style={{ margin: 0, paddingLeft: 18 }}>
            {options.map((o, i) => <li key={i}>{o}</li>)}
          </ul>
        </div>
      </div>
    </div>
  );
};

// [USAGE_STATS] 사용통계 탭 (구 카테고리 탭 재사용) — 기능별 토큰 사용 집계
const CategoryView = () => {
  const toast = useToast();
  const isMobile = useIsMobile();
  const FEATURE_LABEL = { chat: '일반 질문', yeobi: '여비정산서', meeting: '회의록', report: '업무보고', retire: '퇴직금 계산', ins4: '4대보험료 정산', edi: '4대보험 신고서', ovw: '운영위 자료', secom: '초과근무 등록', xmerge: '엑셀 취합', retiredoc: '직고용 퇴직금', '(기록전)': '(기능기록 전 사용분)' };
  // [USERCOL] 이용자 역할별 컬럼 정의 (서버 users_by_role/count_by_role 키와 1:1) — 한 곳에서만 관리
  const USER_ROLE_COLS = [
    { key: 'regular', label: '일반', full: '일반 회원' },
    { key: 'admin',   label: '지역', full: '지역관리자' },
    { key: 'master',  label: '총괄', full: '총괄관리자' },
  ];
  // [USECNT] 역할별 분할 그룹: 사용횟수(count_by_role)·이용자(users_by_role) 동일 패턴 렌더
  const ROLE_SPLIT_GROUPS = [
    { field: 'count_by_role', totalField: 'count', name: '사용횟수' },
    { field: 'users_by_role', totalField: 'users', name: '이용자' },
  ];
  // 기능 | 토큰 | [횟수: 전체·일반·지역·총괄] | [이용자: 전체·일반·지역·총괄]
  const STAT_GRID = 'minmax(110px, 1fr) 50px 50px 42px 42px 42px 50px 42px 42px 42px';
  const [statDays, setStatDays] = React.useState(30);
  const [stats, setStats] = React.useState(null);
  const [statBusy, setStatBusy] = React.useState(false);
  const loadStats = async (d) => {
    setStatBusy(true);
    try {
      const res = await fetch('/admin/coins/usage-stats?days=' + (d || statDays), { credentials: 'same-origin' });
      const data = await res.json().catch(() => null);
      if (!res.ok || !data) throw new Error((data && data.error) || ('HTTP ' + res.status));
      setStats(data);
    } catch (err) {
      toast('error', '통계 조회 실패: ' + (err.message || '네트워크 오류'));
    } finally { setStatBusy(false); }
  };
  React.useEffect(() => { loadStats(30); }, []);
  return (
    <div>
      <SectionHeader title="사용 통계" desc="기간 내 토큰 사용 내역을 기능별로 집계합니다" />
      <div style={{ maxWidth: 760 }}>
        <PolicyBigCard title="기능별 사용 통계" desc="일반 질문·문서생성 기능별 차감 토큰과 사용횟수·이용자 수 — 각각 역할별(일반/지역관리자/총괄관리자) 분할" icon="activity">
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
            {[7, 30, 90].map(d => (
              <button key={d} onClick={() => { setStatDays(d); loadStats(d); }} style={{
                padding: '6px 14px', borderRadius: 8, cursor: 'pointer', fontSize: 12.5, fontWeight: 600,
                border: statDays === d ? '2px solid var(--accent)' : '1px solid var(--line-strong)',
                background: statDays === d ? 'var(--accent-soft)' : '#fff', color: statDays === d ? 'var(--accent)' : 'var(--ink-3)',
              }}>최근 {d}일</button>
            ))}
            <div style={{ flex: 1 }} />
            <Button icon={<Icon name="refresh" size={13} />} onClick={() => loadStats()} disabled={statBusy}>{statBusy ? '조회 중...' : '새로고침'}</Button>
          </div>
          {stats && (
            <div style={{ marginTop: 4 }}>
              <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 8 }}>
                총 <b style={{ color: 'var(--ink)' }}>{stats.total_uses}</b>회 사용 · 토큰 <b style={{ color: 'var(--ink)' }}>{stats.total_tokens}</b>개 차감
              </div>
              {stats.by_feature.length === 0 ? (
                <div style={{ padding: '14px 12px', borderRadius: 10, background: 'var(--surface-2)', border: '1px solid var(--line)', fontSize: 12.5, color: 'var(--ink-3)' }}>기간 내 사용 내역이 없습니다.</div>
              ) : (
                <div style={{ borderRadius: 10, border: '1px solid var(--line)', overflow: 'hidden' }}>
                  <div style={{ overflowX: 'auto' }}>
                    <div style={{ minWidth: 580 }}>
                      {/* 그룹 헤더: 사용횟수 | 이용자 */}
                      <div style={{ display: 'grid', gridTemplateColumns: STAT_GRID, gap: 0, padding: '6px 12px 0', background: 'var(--surface-2)', fontSize: 11, fontWeight: 700, color: 'var(--ink-3)' }}>
                        <div /><div />
                        {ROLE_SPLIT_GROUPS.map(g => (
                          <div key={g.field} style={{ gridColumn: 'span 4', textAlign: 'center', borderLeft: '1px solid var(--line)' }}>{g.name}</div>
                        ))}
                      </div>
                      <div style={{ display: 'grid', gridTemplateColumns: STAT_GRID, gap: 0, padding: '4px 12px 8px', background: 'var(--surface-2)', fontSize: 11.5, fontWeight: 700, color: 'var(--ink-2)' }}>
                        <div>기능</div><div style={{ textAlign: 'right' }}>토큰</div>
                        {ROLE_SPLIT_GROUPS.map(g => (
                          <React.Fragment key={g.field}>
                            <div title={g.name + ' — 전체'} style={{ textAlign: 'right', borderLeft: '1px solid var(--line)', cursor: 'help' }}>전체</div>
                            {USER_ROLE_COLS.map(c => (
                              <div key={c.key} title={g.name + ' — ' + c.full} style={{ textAlign: 'right', cursor: 'help' }}>{c.label}</div>
                            ))}
                          </React.Fragment>
                        ))}
                      </div>
                      {stats.by_feature.map(f => (
                        <div key={f.feature} style={{ display: 'grid', gridTemplateColumns: STAT_GRID, padding: '8px 12px', borderTop: '1px solid var(--line)', fontSize: 12.5, background: '#fff' }}>
                          <div style={{ fontWeight: 600, color: 'var(--ink)' }}>{FEATURE_LABEL[f.feature] || f.feature}</div>
                          <div style={{ textAlign: 'right', fontFamily: 'var(--mono)', fontWeight: 700 }}>{f.tokens}</div>
                          {ROLE_SPLIT_GROUPS.map(g => (
                            <React.Fragment key={g.field}>
                              <div title={g.name + ' — 전체'} style={{ textAlign: 'right', fontFamily: 'var(--mono)', borderLeft: '1px solid var(--line)' }}>{f[g.totalField]}</div>
                              {USER_ROLE_COLS.map(c => (
                                <div key={c.key} title={g.name + ' — ' + c.full} style={{ textAlign: 'right', fontFamily: 'var(--mono)', color: 'var(--ink-3)' }}>
                                  {f[g.field] ? (f[g.field][c.key] || 0) : '-'}
                                </div>
                              ))}
                            </React.Fragment>
                          ))}
                        </div>
                      ))}
                    </div>
                  </div>
                </div>
              )}
            </div>
          )}
        </PolicyBigCard>
      </div>
    </div>
  );
};

const TagView = () => (
  <PolicyPlaceholder
    title="태그 관리"
    options={[
      '회원 태그 (관심분야 / 직무 / 소속 등)',
      '문서 태그 (검색 결과에 부착되는 키워드)',
      '대화 태그 (오류보고/세션 분류)',
    ]}
  />
);

Object.assign(window, { SecurityView, TokenOpsView, AppDownloadView, CategoryView, TagView });
