/* =================================================================
   skills-ui.jsx — 46개 스킬이 화면에 나타나는 자리.
   skills-runtime.js 가 스킬을 구동하고, 이 파일은 그것을 그립니다.
   index.html 의 메인 스크립트보다 먼저 로드되며, 이름은 전부 Sk 로 시작합니다
   (메인 스크립트의 전역 이름과 겹치지 않도록).
   ================================================================= */

/** 스킬 → 이 콘솔에서 적용된 위치. 스킬 라이브러리 화면이 이 표를 보여줍니다. */
const SK_APPLIED = {
  'auth-company-routing':        ['회사 경로 /c/velrano', 'sk-access'],
  'auth-email-login':            ['로그인 화면', null],
  'auth-google-oauth':           ['로그인 · Google', null],
  'auth-password-recovery':      ['로그인 · 비밀번호 찾기', null],
  'auth-role-guard':             ['메뉴 노출 · 내보내기 권한', 'sk-access'],
  'data-audit-log':              ['모든 변경 기록', 'sk-changelog'],
  'data-backup-restore':         ['설정 › 백업 · 복원', 'sk-backup'],
  'data-client-errors':          ['오류 자동 수집', 'sk-data'],
  'data-file-storage':           ['로고 · 영상 파일 보관', 'sk-data'],
  'data-pending-queue':          ['상단 동기화 배지', 'sk-data'],
  'data-realtime-sync':          ['탭 간 실시간 동기화', 'sk-data'],
  'data-records-store':          ['문의 레코드 저장소', 'sk-data'],
  'export-pdf-print':            ['메신저 › PDF 인쇄', null],
  'export-word-doc':             ['메신저 › Word 내보내기', null],
  'header-clock':                ['상단 시계', 'sk-tz'],
  'header-global-search':        ['상단 검색 ( / 키)', null],
  'header-language-switcher':    ['상단 언어 선택 · 11개 언어', null],
  'header-notification-center':  ['상단 알림 벨', null],
  'header-theme-mode':           ['설정 › 테마 팔레트', 'sk-theme'],
  'integration-cloud-translate': ['문의 상세 › 번역', 'sk-int'],
  'integration-cloudflare-kv':   ['설정 › 연동 › Cloudflare KV', 'sk-int'],
  'integration-gemini-ai':       ['문의 상세 › AI 답변 초안', 'sk-int'],
  'integration-github-backup':   ['설정 › 연동 › GitHub 백업', 'sk-int'],
  'integration-google-drive':    ['문의 상세 › Drive 업로드', 'sk-int'],
  'integration-webhook-connector': ['설정 › 연동 › 웹훅', 'sk-int'],
  'platform-date-time-picker':   ['변경 이력 날짜 필터', 'sk-changelog'],
  'platform-dirty-guard':        ['저장 안 된 변경 경고', null],
  'platform-i18n-engine':        ['메뉴 · 알림 번역', 'sk-lang'],
  'notify-sound-alerts':         ['설정 › 알림음', 'sk-sound'],
  'setting-admin-notice':        ['상단 공지 배너 · 알림', 'sk-notice'],
  'setting-brand':               ['로고 · 시스템 이름', 'sk-brand'],
  'setting-change-log':          ['설정 › 변경 이력', 'sk-changelog'],
  'setting-daily-slots':         ['설정 › 일일 보고 항목', 'sk-slots'],
  'setting-demo-mode':           ['샘플 문의 (관리자 전용)', 'sk-demo'],
  'setting-font':                ['설정 › 글꼴', 'sk-font'],
  'setting-language-overrides':  ['설정 › 문구 수정', 'sk-lang'],
  'setting-perf-items':          ['설정 › 평가 항목', 'sk-perf'],
  'setting-permissions':         ['설정 › 권한 매트릭스', 'sk-perms'],
  'setting-sidebar-menu':        ['설정 › 메뉴 순서 · 이름', 'sk-menu'],
  'setting-staff-master':        ['설정 › 직원 마스터', 'sk-staff'],
  'setting-team-roles':          ['설정 › 팀 목록', 'sk-teams'],
  'setting-timezone':            ['설정 › 시간대', 'sk-tz'],
  'setting-video-guide':         ['설정 › 영상 가이드', 'sk-video'],
  'shell-layout-switch':         ['사이드바 ⇄ 상단바', 'sk-layout'],
  'shell-sidebar-nav':           ['사이드바 메뉴', 'sk-menu'],
  'shell-toast-modal':           ['알림 토스트 · 확인 창', null],
};

/* ---------------------------------------------------------------
   런타임 훅 — App 이 한 번 만들고, 바뀔 때마다 다시 그립니다.
   --------------------------------------------------------------- */
function useSkillRuntime(live) {
  const ref = React.useRef(null);
  const liveRef = React.useRef(live);
  liveRef.current = live;
  const [, force] = React.useState(0);

  if (!ref.current && window.SkillRuntime) {
    const M = window.SkillRuntime;
    try {
      ref.current = M.attachOverrides(M.createSkillRuntime({
        accounts: ACCOUNTS,
        getSession: () => liveRef.current.session,
        getCaps: () => liveRef.current.caps || {},
        getInquiries: () => liveRef.current.inquiries || [],
        getSlaMs: () => liveRef.current.slaMs,
        getView: () => liveRef.current.view,
      }));
      window.__SKR = ref.current;
    } catch (e) {
      console.error('[skills] 런타임 생성 실패', e);
    }
  }
  const rt = ref.current;

  React.useEffect(() => (rt ? rt.on(() => force((n) => n + 1)) : undefined), [rt]);

  /* 로그인할 때마다: 동기화 · 시계 · 오류 수집 시작, 회사 경로로 정리 */
  const uid = live.session && live.session.uid;
  React.useEffect(() => {
    if (!rt || !uid) return;
    let alive = true;
    rt.start().then(async () => {
      if (!alive) return;
      const d = await rt.company.decide(uid, location.pathname);
      if (d.outcome === 'redirect' || (d.canonical && d.canonical !== location.pathname)) {
        history.replaceState(null, '', (d.path || d.canonical) + location.search + location.hash);
      }
      rt.emit();
    }).catch((e) => rt.errors.report(e));
    return () => { alive = false; };
  }, [rt, uid]);

  return rt;
}

/* ---------------------------------------------------------------
   공용 조각
   --------------------------------------------------------------- */
function SkBtn({ primary, danger, small, children, className = '', ...rest }) {
  const tone = primary ? 'sb-btn-primary' : 'sb-btn-ghost';
  const st = danger ? { color: 'var(--critical)', borderColor: 'rgba(208,59,59,.45)' } : undefined;
  return (
    <button type="button" className={'sb-btn ' + tone + (small ? ' !px-2.5 !py-1.5 !text-[11.5px]' : ' !px-3 !py-2 !text-[12.5px]') + ' ' + className}
      style={st} {...rest}>{children}</button>
  );
}

function SkCard({ id, icon, title, desc, skills = [], children, actions, wide }) {
  return (
    <section id={id} className={'sb-card p-5 scroll-mt-24 ' + (wide ? 'xl:col-span-2' : '')}>
      <div className="flex items-start gap-3 mb-4">
        <span className="inline-flex items-center justify-center w-9 h-9 rounded-xl shrink-0"
          style={{ background: 'color-mix(in srgb, var(--accent) 14%, transparent)', color: 'var(--accent)' }}>
          <Icon name={icon} size={17} />
        </span>
        <div className="min-w-0 flex-1">
          <h3 className="text-[14.5px] font-bold ink-1">{title}</h3>
          {desc && <p className="text-[12px] ink-3 mt-0.5 leading-relaxed">{desc}</p>}
          <div className="flex flex-wrap gap-1 mt-1.5">
            {skills.map((s) => (
              <span key={s} className="text-[10px] font-bold px-1.5 py-0.5 rounded-md sb-mono"
                style={{ background: 'var(--surface-3)', color: 'var(--ink-3)' }}>{s}</span>
            ))}
          </div>
        </div>
        {actions && <div className="flex gap-1.5 shrink-0">{actions}</div>}
      </div>
      {children}
    </section>
  );
}

function SkField({ label, hint, children }) {
  return (
    <label className="block">
      <span className="block text-[11.5px] font-bold ink-2 mb-1">{label}</span>
      {children}
      {hint && <span className="block text-[11px] ink-3 mt-1">{hint}</span>}
    </label>
  );
}

function SkNote({ tone = 'info', children }) {
  const c = { ok: 'var(--good)', warn: 'var(--warning)', danger: 'var(--critical)', info: 'var(--accent)' }[tone];
  return (
    <div className="text-[12px] font-semibold rounded-lg px-3 py-2 mt-3"
      style={{ color: c, background: 'color-mix(in srgb, ' + c + ' 12%, transparent)', border: '1px solid color-mix(in srgb, ' + c + ' 35%, transparent)' }}>
      {children}
    </div>
  );
}

const skFmt = (iso) => { if (!iso) return '-'; const d = new Date(iso); return isNaN(d) ? String(iso) : d.toLocaleString(); };

function skDownload(name, text, type = 'application/json') {
  const url = URL.createObjectURL(new Blob([text], { type }));
  const a = document.createElement('a'); a.href = url; a.download = name; a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

function skReadFile(file, as = 'text') {
  return new Promise((res, rej) => {
    const fr = new FileReader();
    fr.onload = () => res(fr.result); fr.onerror = rej;
    as === 'url' ? fr.readAsDataURL(file) : fr.readAsText(file);
  });
}

/** 결과 객체 → 한 줄 상태 (스킬이 돌려주는 reason 을 그대로 보여줍니다) */
function skResult(r, okText) {
  if (!r) return null;
  if (r.ok === false) return { tone: 'danger', text: '실패 · ' + (r.reason || r.code || r.error && r.error.message || 'error') };
  return { tone: 'ok', text: okText || '완료' };
}

/* ---------------------------------------------------------------
   상단바: 시계 · 언어 · 레이아웃 · 동기화 · 데모 · 알림 · 검색
   --------------------------------------------------------------- */
function SkClock({ rt }) {
  const [v, setV] = React.useState(rt.clockView);
  React.useEffect(() => rt.onClock(setV), [rt]);
  if (!v) return null;
  return (
    <div className="hidden lg:flex flex-col items-end leading-tight px-1 sb-num shrink-0" title={rt.tz.timeZone()}>
      <span className="text-[13px] font-bold ink-1 whitespace-nowrap">{v.seconds || v.time}</span>
      <span className="text-[10px] font-semibold ink-3 whitespace-nowrap">{v.date} · {v.zone}</span>
    </div>
  );
}

function SkPopover({ open, onClose, children, width = 300, align = 'right' }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const k = (e) => e.key === 'Escape' && onClose();
    setTimeout(() => document.addEventListener('mousedown', h), 0);
    document.addEventListener('keydown', k);
    return () => { document.removeEventListener('mousedown', h); document.removeEventListener('keydown', k); };
  }, [open]);
  if (!open) return null;
  return (
    <div ref={ref} className={'absolute top-full mt-2 z-50 rounded-xl overflow-hidden ' + (align === 'right' ? 'right-0' : 'left-0')}
      style={{ width, background: 'var(--surface-1)', border: '1px solid var(--border)', boxShadow: 'var(--shadow-lg)' }}>
      {children}
    </div>
  );
}

function SkLangPicker({ rt }) {
  const [open, setOpen] = React.useState(false);
  const cur = rt.lang.current;
  return (
    <div className="relative">
      <button className="sb-icon-btn !w-auto !px-2 gap-1.5 flex items-center" onClick={() => setOpen((v) => !v)}
        title={rt.lang.label(cur)} aria-label="language">
        <span className="inline-flex w-[20px] h-[14px] rounded-[2px] overflow-hidden" dangerouslySetInnerHTML={{ __html: rt.lang.flag(cur) }} />
        <span className="text-[11px] font-bold ink-2 uppercase hidden xl:inline">{cur}</span>
      </button>
      <SkPopover open={open} onClose={() => setOpen(false)} width={210}>
        <div className="p-1.5 max-h-[340px] overflow-y-auto sb-scroll">
          {rt.lang.list().map((l) => (
            <button key={l} onClick={async () => { await rt.lang.set(l); setOpen(false); }}
              className="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[12.5px] font-semibold text-left"
              style={l === cur ? { background: 'color-mix(in srgb, var(--accent) 14%, transparent)', color: 'var(--accent)' } : { color: 'var(--ink-2)' }}>
              <span className="inline-flex w-[20px] h-[14px] rounded-[2px] overflow-hidden shrink-0" dangerouslySetInnerHTML={{ __html: rt.lang.flag(l) }} />
              <span className="flex-1 truncate" dir={rt.lang.dir(l)}>{rt.lang.label(l)}</span>
              {l === cur && <Icon name="Check" size={13} />}
            </button>
          ))}
        </div>
      </SkPopover>
    </div>
  );
}

function SkLayoutToggle({ rt }) {
  const top = rt.layout.layout === 'topnav';
  return (
    <button className="sb-icon-btn hidden md:inline-flex" onClick={() => rt.layout.toggleLayout()}
      title={top ? '사이드바 레이아웃으로' : '상단바 레이아웃으로'} aria-label="layout">
      <Icon name={top ? 'SlidersHorizontal' : 'Menu'} size={17} />
    </button>
  );
}

function SkSyncPill({ rt }) {
  const s = rt.sync.state;
  const pending = rt.queue.length;
  const tone = s.status === 'error' || s.status === 'reconnecting' ? 'var(--warning)' : pending ? 'var(--warning)' : 'var(--good)';
  const label = rt.queue.label(rt.t, s.lastAt ? new Date(s.lastAt).toLocaleTimeString() : null);
  return (
    <button className="hidden xl:flex items-center gap-1.5 text-[11px] font-semibold ink-3 px-2 py-1.5 rounded-lg whitespace-nowrap shrink-0"
      style={{ background: 'var(--surface-3)' }} onClick={() => { rt.queue.flush(); rt.sync.syncAll(); }}
      title={label + ' · ' + s.mode + ' / ' + s.status + ' — 클릭하면 지금 동기화'}>
      <span className="sb-dot" style={{ background: tone }} />{pending ? rt.t('topbar.pending', { n: pending }) : rt.t('queue.idle')}
    </button>
  );
}

function SkDemoPill({ rt, setView }) {
  if (!rt.isAdmin()) return null;
  const p = rt.demo.pill();
  if (!p.show) return null;
  return (
    <button onClick={() => { setView('settings'); setTimeout(() => skJump('sk-demo'), 60); }}
      className="hidden md:flex items-center gap-1.5 text-[11px] font-bold px-2 py-1 rounded-lg"
      style={{ background: p.soon ? 'rgba(250,178,25,.18)' : 'color-mix(in srgb, var(--accent) 14%, transparent)', color: p.soon ? 'var(--warning)' : 'var(--accent)' }}>
      <Icon name="Sparkles" size={12} />{p.text}
    </button>
  );
}

function skJump(id) { const el = document.getElementById(id); el && el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }

function SkBell({ rt, setView }) {
  const [open, setOpen] = React.useState(false);
  const items = rt.notify.list();
  const unseen = rt.notify.unseen();
  const lvColor = { danger: 'var(--critical)', warn: 'var(--warning)', brand: 'var(--accent)' };
  return (
    <div className="relative">
      <button className="sb-icon-btn relative" aria-label={rt.t('nt.title')}
        onClick={() => { const o = !open; setOpen(o); if (o) setTimeout(() => { rt.notify.markAllSeen(); rt.emit(); }, 1200); }}>
        <Icon name={unseen ? 'BellRing' : 'Bell'} size={17} />
        {unseen > 0 && (
          <span className="absolute top-1 right-1 min-w-[15px] h-[15px] px-1 rounded-full text-[9px] font-extrabold flex items-center justify-center sb-num"
            style={{ background: items.some((n) => n.lv === 'danger' && !n.seen) ? 'var(--critical)' : 'var(--accent)', color: '#fff' }}>
            {rt.notify.badge(unseen)}
          </span>
        )}
      </button>
      <SkPopover open={open} onClose={() => setOpen(false)} width={340}>
        <div className="flex items-center justify-between px-4 py-3 border-b sb-border">
          <span className="text-[13px] font-bold ink-1">{rt.t('nt.title')}</span>
          <button className="text-[11.5px] font-semibold" style={{ color: 'var(--accent)' }}
            onClick={() => { rt.notify.markAllSeen(); rt.emit(); }}>{rt.t('nt.readAll')}</button>
        </div>
        <div className="max-h-[380px] overflow-y-auto sb-scroll">
          {items.length === 0 && <div className="px-4 py-8 text-center text-[12.5px] ink-3">{rt.t('nt.empty')}</div>}
          {items.map((n) => (
            <div key={n.key} className="flex items-start gap-2.5 px-4 py-3 border-b sb-border cursor-pointer"
              style={{ background: n.seen ? 'transparent' : 'color-mix(in srgb, var(--accent) 6%, transparent)' }}
              onClick={() => { rt.notify.markSeen(n.key); setOpen(false); n.go && setView(n.go); rt.emit(); }}>
              <span className="text-[16px] leading-none mt-0.5">{n.icon}</span>
              <div className="min-w-0 flex-1">
                <div className="text-[12.5px] font-bold" style={{ color: lvColor[n.lv] || 'var(--ink-1)' }}>{n.title}</div>
                <div className="text-[12px] ink-2 mt-0.5 line-clamp-2">{n.desc}</div>
                {n.at && <div className="text-[10.5px] ink-3 mt-1">{skFmt(n.at)}</div>}
              </div>
              {rt.isAdmin() && (
                <button className="ink-3 hover:opacity-70" title="숨기기"
                  onClick={(e) => { e.stopPropagation(); rt.notify.drop(n.key, { allowed: true }); rt.emit(); }}>
                  <Icon name="X" size={13} />
                </button>
              )}
            </div>
          ))}
        </div>
      </SkPopover>
    </div>
  );
}

function SkSearch({ rt, value, onChange, onPick }) {
  const inputRef = React.useRef(null);
  const [focus, setFocus] = React.useState(false);
  React.useEffect(() => {
    const M = rt.modules['header-global-search'];
    return M.bindSearchHotkey({ input: inputRef.current, isSignedIn: () => !!rt.user(),
      onEscape: () => { onChange(''); inputRef.current && inputRef.current.blur(); } });
  }, [rt]);
  const hits = value ? rt.search.flat(value) : [];
  return (
    <div className="relative hidden md:block w-52 2xl:w-60 shrink-0">
      <span className="absolute left-3 top-1/2 -translate-y-1/2 ink-3"><Icon name="Search" size={15} /></span>
      <kbd className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-[10px] font-bold ink-3 px-1.5 rounded border sb-border">/</kbd>
      <input ref={inputRef} className="sb-input !pl-9 !pr-8 !py-2 !text-[13px]" placeholder={rt.t('search.ph')}
        value={value} onChange={(e) => onChange(e.target.value)}
        onFocus={() => setFocus(true)} onBlur={() => setTimeout(() => setFocus(false), 150)} />
      {focus && value && (
        <div className="absolute top-full mt-2 left-0 w-[360px] z-50 rounded-xl overflow-hidden"
          style={{ background: 'var(--surface-1)', border: '1px solid var(--border)', boxShadow: 'var(--shadow-lg)' }}>
          {hits.length === 0 && <div className="px-4 py-4 text-[12.5px] ink-3">{rt.t('search.none')}</div>}
          {hits.map((h) => (
            <button key={h.id} className="w-full text-left px-4 py-2.5 border-b sb-border hover:opacity-80"
              onMouseDown={() => onPick(h)}>
              <div className="text-[12.5px] font-bold ink-1" dangerouslySetInnerHTML={{ __html: rt.search.highlight(h.title || '', value) }} />
              <div className="text-[11.5px] ink-3" dangerouslySetInnerHTML={{ __html: rt.search.highlight((h.sub || '') + ' · ' + (h.channel || ''), value) }} />
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

function SkNoticeBanner({ rt }) {
  const n = rt.notice.active();
  if (!n) return null;
  return (
    <div className="flex items-center gap-2 px-4 sm:px-6 py-2 text-[12.5px] font-semibold"
      style={{ background: 'rgba(208,59,59,.14)', borderBottom: '1px solid rgba(208,59,59,.4)', color: 'var(--critical)' }}>
      <Icon name="Megaphone" size={14} />
      <span className="flex-1 min-w-0 truncate">{n.text}</span>
      {n.expiresAt && <span className="text-[11px] opacity-80">~ {skFmt(n.expiresAt)}</span>}
    </div>
  );
}

function SkTopNav({ items, view, setView }) {
  return (
    <nav className="sb-surface border-b sb-border px-4 sm:px-6 flex gap-1 overflow-x-auto sb-scroll">
      {items.map((n) => {
        const active = view === n.key;
        return (
          <button key={n.key} onClick={() => setView(n.key)}
            className="flex items-center gap-2 px-3 py-2.5 text-[12.5px] font-semibold whitespace-nowrap border-b-2"
            style={{ borderColor: active ? 'var(--accent)' : 'transparent', color: active ? 'var(--accent)' : 'var(--ink-2)' }}>
            <Icon name={n.icon} size={15} />{n.label}
          </button>
        );
      })}
    </nav>
  );
}

/* ---------------------------------------------------------------
   shell-toast-modal — 토스트(중복은 카운터로)와 확인 창
   --------------------------------------------------------------- */
function SkToasts({ rt }) {
  const color = { ok: 'var(--good)', danger: 'var(--critical)', warn: 'var(--warning)', info: 'var(--accent)' };
  const icon = { ok: 'CircleCheck', danger: 'TriangleAlert', warn: 'CircleAlert', info: 'Info' };
  return (
    <div className="fixed bottom-5 right-5 z-[90] flex flex-col gap-2 w-[340px] max-w-[calc(100vw-40px)]">
      {rt.toasts.map((x) => (
        <div key={x.id} className="sb-card flex items-start gap-2.5 p-3 sb-fade-up" style={{ boxShadow: 'var(--shadow-lg)', borderLeft: '3px solid ' + color[x.kind] }}>
          <Icon name={icon[x.kind] || 'Info'} size={16} style={{ color: color[x.kind] }} className="mt-px" />
          <div className="flex-1 min-w-0 text-[12.5px] ink-1 whitespace-pre-line">{x.text}</div>
          {x.count > 1 && <span className="text-[10px] font-extrabold px-1.5 rounded-md sb-num" style={{ background: 'var(--surface-3)' }}>×{x.count}</span>}
          <button className="ink-3" onClick={() => rt.toaster.dismiss(x.id)} aria-label="close"><Icon name="X" size={13} /></button>
        </div>
      ))}
    </div>
  );
}

function SkModal({ rt }) {
  const m = rt.modal;
  React.useEffect(() => rt.modals.bindEscape(), [rt]);
  if (!m || !m.open) return null;
  return (
    <div className="fixed inset-0 z-[95] flex items-center justify-center p-4">
      <div className="absolute inset-0" style={{ background: 'rgba(8,8,7,.55)' }} onClick={() => rt.modals.close()} />
      <div role="dialog" aria-modal="true" className={'relative sb-card p-6 w-full ' + (m.wide ? 'max-w-[720px]' : 'max-w-[440px]')} style={{ boxShadow: 'var(--shadow-lg)' }}>
        {m.title && <h3 className="text-[15px] font-bold ink-1 mb-3">{m.title}</h3>}
        <div className="text-[13px] ink-2">{m.content}</div>
      </div>
    </div>
  );
}

/** 확인 창 — shell-toast-modal 의 modals 위에서. */
function skConfirm(rt, title, body, okLabel = '확인', danger) {
  return new Promise((resolve) => {
    rt.modals.open(
      <div>
        <div className="whitespace-pre-line">{body}</div>
        <div className="flex justify-end gap-2 mt-5">
          <SkBtn onClick={() => rt.modals.close(null, false)}>취소</SkBtn>
          <SkBtn primary={!danger} danger={danger} onClick={() => rt.modals.close(null, true)}>{okLabel}</SkBtn>
        </div>
      </div>,
      { title, onClose: (r) => resolve(r === true) });
  });
}

/* ---------------------------------------------------------------
   platform-dirty-guard — 저장 안 된 카드가 있으면 하단 막대 + 이탈 경고
   --------------------------------------------------------------- */
function SkDirtyBar({ rt }) {
  React.useEffect(() => rt.dirty.bindUnload(window), [rt]);
  const s = rt.dirty.state;
  if (!s.count) return null;
  return (
    <div className="sticky bottom-0 z-30 flex items-center gap-3 px-4 py-3 mt-4 rounded-xl"
      style={{ background: 'rgba(250,178,25,.16)', border: '1px solid rgba(250,178,25,.5)' }}>
      <Icon name="CircleAlert" size={16} style={{ color: 'var(--warning)' }} />
      <span className="text-[12.5px] font-bold ink-1 flex-1">
        {rt.dirty.message({ one: '저장 안 된 변경이 있습니다: {{labels}}', many: '{{n}}개 카드에 저장 안 된 변경이 있습니다: {{labels}}' })
          .replace('{{labels}}', s.labels.join(', ')).replace('{{n}}', s.count)}
      </span>
      <SkBtn primary small onClick={async () => { await rt.dirty.saveAll(); rt.emit(); }}>모두 저장</SkBtn>
    </div>
  );
}

/** 카드의 초안을 dirty-guard 에 등록합니다. */
function useSkDirty(rt, id, label, read, saveFn) {
  React.useEffect(() => rt.dirty.register(id, { read, label, save: saveFn }), [rt, id]);
}

/* =================================================================
   설정 › 스킬 센터 — 카드 하나가 스킬 하나 이상을 실제로 구동합니다
   ================================================================= */
function SkSettingsCenter({ rt, theme, setTheme }) {
  if (!rt) return null;
  const admin = rt.isAdmin();
  const sections = [
    ['sk-brand', '브랜드'], ['sk-theme', '테마'], ['sk-font', '글꼴'], ['sk-layout', '레이아웃'], ['sk-tz', '시간대'],
    ['sk-menu', '메뉴'], ['sk-perms', '권한'], ['sk-notice', '공지'], ['sk-demo', '데모'], ['sk-lang', '문구'],
    ['sk-staff', '직원'], ['sk-teams', '팀'], ['sk-perf', '평가'], ['sk-slots', '일일보고'], ['sk-video', '영상'],
    ['sk-sound', '알림음'], ['sk-changelog', '변경이력'], ['sk-backup', '백업'], ['sk-data', '데이터'],
    ['sk-int', '연동'], ['sk-access', '접근'],
  ];
  return (
    <div className="px-4 sm:px-6 pb-10">
      <div className="sb-card p-4 mb-4 flex flex-wrap items-center gap-2">
        <Icon name="BookOpen" size={16} style={{ color: 'var(--accent)' }} />
        <span className="text-[13px] font-bold ink-1 mr-2">스킬 설정 센터</span>
        <span className="text-[11.5px] ink-3 mr-auto">Skills/ 의 46개 패키지가 아래 카드에서 실제로 동작합니다.</span>
        <div className="flex flex-wrap gap-1 w-full mt-1">
          {sections.map(([id, l]) => (
            <button key={id} onClick={() => skJump(id)} className="text-[11px] font-semibold px-2 py-1 rounded-md"
              style={{ background: 'var(--surface-3)', color: 'var(--ink-2)' }}>{l}</button>
          ))}
        </div>
      </div>
      {!admin && <SkNote tone="warn">대부분의 설정은 관리자만 바꿀 수 있습니다. 스킬이 권한을 직접 확인하므로 저장 시 거절됩니다.</SkNote>}
      <div className="grid xl:grid-cols-2 gap-4 mt-4">
        <SkBrandCard rt={rt} />
        <SkThemeCard rt={rt} theme={theme} setTheme={setTheme} />
        <SkFontCard rt={rt} />
        <SkLayoutCard rt={rt} />
        <SkTimezoneCard rt={rt} />
        <SkMenuCard rt={rt} />
        <SkPermsCard rt={rt} />
        <SkNoticeCard rt={rt} />
        <SkDemoCard rt={rt} />
        <SkLangCard rt={rt} />
        <SkStaffCard rt={rt} />
        <SkTeamsCard rt={rt} />
        <SkPerfCard rt={rt} />
        <SkSlotsCard rt={rt} />
        <SkVideoCard rt={rt} />
        <SkSoundCard rt={rt} />
        <SkChangeLogCard rt={rt} />
        <SkBackupCard rt={rt} />
        <SkDataCard rt={rt} />
        <SkIntegrationsCard rt={rt} />
        <SkAccessCard rt={rt} />
      </div>
      <SkDirtyBar rt={rt} />
    </div>
  );
}

/* ---- setting-brand + data-file-storage ---- */
function SkBrandCard({ rt }) {
  const cfg = rt.cfg;
  const [f, setF] = React.useState({ company: cfg.BRAND_COMPANY || '', system: cfg.BRAND_SYSTEM || '', tagline: cfg.BRAND_TAGLINE || '' });
  const [msg, setMsg] = React.useState(null);
  const fRef = React.useRef(f); fRef.current = f;
  const saveFn = async () => { const r = await rt.brand.apply(fRef.current); rt.modules['setting-brand'].paintBrand(rt.brand.view()); return r; };
  useSkDirty(rt, 'brand', '브랜드', () => ({ ...fRef.current, draft: rt.brand.draft || null }), saveFn);
  const v = rt.brand.view();

  const onLogo = async (file) => {
    if (!file) return;
    const ok = rt.brand.validateLogoFile(file);
    if (ok && ok.ok === false) return setMsg({ tone: 'danger', text: rt.t(ok.reason === 'size' ? 'toast.brandLogoBig' : 'toast.brandLogoType', { mb: rt.modules['setting-brand'].LOGO_MAX_MB }) });
    let url = await skReadFile(file, 'url');
    if (!/svg/.test(file.type)) {
      const img = await new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.src = url; });
      const box = rt.brand.scaleTo(img.width, img.height);
      const c = document.createElement('canvas'); c.width = box.w; c.height = box.h;
      c.getContext('2d').drawImage(img, 0, 0, box.w, box.h);
      url = c.toDataURL('image/png');
    }
    rt.brand.setDraftLogo(url); rt.emit();
    setMsg({ tone: 'info', text: '미리보기 중 — 저장해야 반영됩니다.' });
  };

  return (
    <SkCard id="sk-brand" icon="Building2" title="브랜드" skills={['setting-brand', 'data-file-storage']}
      desc="로고 · 회사 이름 · 시스템 이름 · 태그라인. 사이드바 · 로그인 · 브라우저 탭 제목에 반영됩니다.">
      <div className="flex items-center gap-3 mb-4">
        <img src={v.logo} alt="" className="w-14 h-14 rounded-xl object-contain" style={{ background: 'var(--surface-3)' }} />
        <div className="min-w-0">
          <div className="text-[14px] font-extrabold ink-1 truncate">{v.system}</div>
          <div className="text-[11.5px] ink-3 truncate">{v.company} · {v.tagline}</div>
        </div>
      </div>
      <div className="grid sm:grid-cols-3 gap-2.5">
        <SkField label="회사 이름"><input className="sb-input" value={f.company} placeholder={v.company} onChange={(e) => setF({ ...f, company: e.target.value })} /></SkField>
        <SkField label="시스템 이름"><input className="sb-input" value={f.system} placeholder={v.system} onChange={(e) => setF({ ...f, system: e.target.value })} /></SkField>
        <SkField label="태그라인"><input className="sb-input" value={f.tagline} placeholder={v.tagline} onChange={(e) => setF({ ...f, tagline: e.target.value })} /></SkField>
      </div>
      <div className="flex flex-wrap gap-2 mt-3">
        <label className="sb-btn sb-btn-ghost !px-3 !py-2 !text-[12.5px] cursor-pointer">
          <Icon name="Plus" size={14} />로고 올리기
          <input type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml" hidden onChange={(e) => onLogo(e.target.files[0])} />
        </label>
        {rt.brand.hasCustomLogo() && <SkBtn onClick={() => { rt.brand.removeLogo(); rt.emit(); }}>로고 제거</SkBtn>}
        <div className="flex-1" />
        <SkBtn onClick={async () => { await rt.brand.reset(); setF({ company: '', system: '', tagline: '' }); rt.modules['setting-brand'].paintBrand(rt.brand.view()); }}>기본값</SkBtn>
        <SkBtn primary onClick={async () => { const r = await saveFn(); rt.dirty.commit('brand'); setMsg(skResult(r, '저장됨 — 화면에 반영했습니다')); }}>저장</SkBtn>
      </div>
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- header-theme-mode ---- */
function SkThemeCard({ rt, theme, setTheme }) {
  const sw = { velrano: ['#2a78d6', '#121418'], neutral: ['#f4f5f7', '#1b1f26'], warm: ['#faf6f0', '#231d19'], cool: ['#f1f5fa', '#172029'] };
  return (
    <SkCard id="sk-theme" icon="Sun" title="테마 · 팔레트" skills={['header-theme-mode']}
      desc="낮/밤 모드와 배경 팔레트 3종 (+ 기본). 고객이 고른 배경색이 전 화면에 적용됩니다.">
      <div className="grid grid-cols-4 gap-2">
        {rt.theme.palettes().map((p) => {
          const on = rt.theme.palette === p.id;
          return (
            <button key={p.id} onClick={() => { rt.theme.setPalette(p.id); rt.audit.push({ action: 'update', module: 'settings', label: 'palette:' + p.id }); }}
              className="rounded-xl p-2.5 text-left" style={{ border: '2px solid ' + (on ? 'var(--accent)' : 'var(--border)') }}>
              <div className="flex gap-1 mb-1.5">{(sw[p.id] || []).map((c) => <span key={c} className="w-5 h-5 rounded-md" style={{ background: c, border: '1px solid var(--border)' }} />)}</div>
              <div className="text-[12px] font-bold ink-1">{p.label}</div>
            </button>
          );
        })}
      </div>
      <div className="flex gap-2 mt-3">
        {['light', 'dark'].map((m) => (
          <SkBtn key={m} primary={theme === m} onClick={() => setTheme(m)}>
            <Icon name={m === 'light' ? 'Sun' : 'Moon'} size={14} />{m === 'light' ? '낮 모드' : '밤 모드'}
          </SkBtn>
        ))}
      </div>
    </SkCard>
  );
}

/* ---- setting-font ---- */
function SkFontCard({ rt }) {
  useSkDirty(rt, 'font', '글꼴', () => rt.font.readDraft(), () => rt.font.commit());
  return (
    <SkCard id="sk-font" icon="FileText" title="글꼴" skills={['setting-font']}
      desc="하나의 CSS 변수로 앱 전체 글꼴을 바꿉니다. 누르면 미리보기, 저장해야 유지됩니다."
      actions={<>
        <SkBtn small onClick={() => { rt.font.discardDraft(); rt.emit(); }}>되돌리기</SkBtn>
        <SkBtn small primary onClick={async () => { await rt.font.commit(); rt.dirty.commit('font'); rt.toaster.ok('글꼴을 저장했습니다'); }}>저장</SkBtn>
      </>}>
      <div className="grid grid-cols-2 gap-2">
        {rt.font.options().map((o) => {
          const on = rt.font.current() === o.id;
          return (
            <button key={o.id} onClick={() => { rt.font.preview(o.id); rt.emit(); }}
              className="rounded-xl p-3 text-left" style={{ border: '2px solid ' + (on ? 'var(--accent)' : 'var(--border)'), fontFamily: o.stack }}>
              <div className="text-[18px] font-bold ink-1">{o.sample}</div>
              <div className="text-[11.5px] ink-3 mt-1">{o.label}{rt.font.key() === o.id ? ' · 저장됨' : ''}</div>
            </button>
          );
        })}
      </div>
    </SkCard>
  );
}

/* ---- shell-layout-switch ---- */
function SkLayoutCard({ rt }) {
  const cur = rt.layout.layout;
  return (
    <SkCard id="sk-layout" icon="SlidersHorizontal" title="레이아웃" skills={['shell-layout-switch', 'shell-sidebar-nav']}
      desc="사이드바 ⇄ 상단바. 상단바 오른쪽 버튼과 이 카드가 같은 상태를 씁니다.">
      <div className="grid grid-cols-2 gap-2">
        {[['sidebar', '사이드바', 'Menu'], ['topnav', '상단바', 'SlidersHorizontal']].map(([id, l, ic]) => (
          <button key={id} onClick={() => rt.layout.setLayout(id)} className="rounded-xl p-3 flex items-center gap-2.5"
            style={{ border: '2px solid ' + (cur === id ? 'var(--accent)' : 'var(--border)') }}>
            <Icon name={ic} size={18} style={{ color: cur === id ? 'var(--accent)' : 'var(--ink-3)' }} />
            <span className="text-[13px] font-bold ink-1">{l}</span>
          </button>
        ))}
      </div>
    </SkCard>
  );
}

/* ---- setting-timezone + header-clock ---- */
function SkTimezoneCard({ rt }) {
  const [zone, setZone] = React.useState(rt.tz.timeZone());
  const [off, setOff] = React.useState(rt.tz.offsetMinutes());
  const [msg, setMsg] = React.useState(null);
  const v = rt.tz.validate({ timeZone: zone, offsetMinutes: Number(off) });
  return (
    <SkCard id="sk-tz" icon="Clock" title="시간대" skills={['setting-timezone', 'header-clock']}
      desc="앱 전체 시계의 기준 시간대와 ±720분 보정. 상단 시계가 즉시 따라갑니다.">
      <div className="grid grid-cols-[1fr_120px] gap-2.5">
        <SkField label="시간대">
          <select className="sb-input" value={zone} onChange={(e) => setZone(e.target.value)}>
            {rt.tz.options().map((o) => <option key={o.id} value={o.id}>{o.label} · {o.utc}</option>)}
          </select>
        </SkField>
        <SkField label="보정 (분)"><input className="sb-input" type="number" value={off} onChange={(e) => setOff(e.target.value)} /></SkField>
      </div>
      {!v.ok && <SkNote tone="danger">{v.errors.map((e) => e.field + ': ' + e.code).join(', ')}</SkNote>}
      <div className="flex items-center gap-2 mt-3">
        <span className="text-[12px] ink-3 flex-1">현재: <b className="ink-1">{rt.tz.label()}</b>{rt.clockView ? ' · ' + rt.clockView.date + ' ' + rt.clockView.time : ''}</span>
        <SkBtn primary disabled={!v.ok} onClick={async () => {
          if (!(await skConfirm(rt, '시간대 변경', '다음 항목이 새 기준으로 바뀝니다:\n' + rt.tz.impact().join(', '), '변경'))) return;
          const r = await rt.tz.apply({ timeZone: zone, offsetMinutes: Number(off) });
          setMsg(skResult(r, '적용됨 — 상단 시계가 바뀌었습니다'));
        }}>적용</SkBtn>
      </div>
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-sidebar-menu + shell-sidebar-nav ---- */
function SkMenuCard({ rt }) {
  useSkDirty(rt, 'menu', '메뉴', () => rt.menu.readDraft(), () => rt.menu.commit());
  const rows = rt.menu.rows();
  const go = (fn) => { fn(); rt.emit(); };
  return (
    <SkCard id="sk-menu" icon="Menu" title="메뉴 순서 · 이름 · 숨김" skills={['setting-sidebar-menu', 'shell-sidebar-nav']}
      desc="화살표로 순서를, 입력란으로 이름을 바꿉니다. 사이드바와 상단바에 동시에 반영됩니다."
      actions={<>
        <SkBtn small onClick={() => go(() => rt.menu.resetToDefaults())}>기본값</SkBtn>
        <SkBtn small primary onClick={async () => { const r = await rt.menu.commit(); rt.dirty.commit('menu'); rt.toaster.show(r && r.ok === false ? '저장 실패: ' + r.reason : '메뉴를 저장했습니다', r && r.ok === false ? 'danger' : 'ok'); }}>저장</SkBtn>
      </>}>
      <div className="flex flex-col gap-1.5">
        {rows.map((r, i) => (
          <div key={r.key} className="flex items-center gap-2 p-2 rounded-lg" style={{ background: 'var(--surface-3)', opacity: r.visible ? 1 : 0.5 }}>
            <Icon name={r.icon} size={15} className="ink-3" />
            <input className="sb-input !py-1.5 !text-[12.5px] flex-1" value={r.label} onChange={(e) => go(() => rt.menu.rename(r.key, e.target.value))} />
            <button className="sb-icon-btn !w-7 !h-7" disabled={!r.canMoveUp} onClick={() => go(() => rt.menu.move(i, -1))}><Icon name="ChevronDown" size={14} style={{ transform: 'rotate(180deg)' }} /></button>
            <button className="sb-icon-btn !w-7 !h-7" disabled={!r.canMoveDown} onClick={() => go(() => rt.menu.move(i, 1))}><Icon name="ChevronDown" size={14} /></button>
            <Switch checked={r.visible} locked={r.adminOnly} label="표시" onChange={(on) => go(() => rt.menu.setVisible(r.key, on))} />
          </div>
        ))}
      </div>
    </SkCard>
  );
}

/* ---- setting-permissions + auth-role-guard ---- */
function SkPermsCard({ rt }) {
  useSkDirty(rt, 'perms', '권한', () => rt.perms.readDraft(), () => rt.perms.commit());
  const [role, setRole] = React.useState('employee');
  const table = rt.perms.table().find((x) => x.role === role) || { rows: [] };
  const acts = ['read', 'create', 'update', 'delete'];
  const label = { read: '조회', create: '생성', update: '수정', delete: '삭제' };
  return (
    <SkCard id="sk-perms" icon="ShieldCheck" title="권한 매트릭스" skills={['setting-permissions', 'auth-role-guard']}
      desc="역할 × 메뉴 × 동작. 쓰기 권한은 조회를 포함합니다. 관리자는 항상 전체 권한입니다."
      actions={<SkBtn small primary onClick={async () => { const r = await rt.perms.commit(); rt.dirty.commit('perms'); rt.toaster.show(r && r.ok === false ? '저장 실패: ' + r.reason : '권한을 저장했습니다', r && r.ok === false ? 'danger' : 'ok'); }}>저장</SkBtn>}>
      <div className="flex gap-1.5 mb-3">
        {rt.perms.roles().map((r) => <SkBtn key={r} small primary={role === r} onClick={() => setRole(r)}>{r === 'manager' ? '매니저' : '직원'}</SkBtn>)}
      </div>
      <table className="w-full text-[12px]">
        <thead><tr className="ink-3"><th className="text-left py-1.5">메뉴</th>{acts.map((a) => <th key={a} className="py-1.5">{label[a]}</th>)}<th className="py-1.5">결과</th></tr></thead>
        <tbody>
          {table.rows.map((r) => (
            <tr key={r.module} className="border-t sb-border">
              <td className="py-1.5 font-semibold ink-1">{rt.t('nav.' + r.module)}</td>
              {acts.map((a) => (
                <td key={a} className="text-center">
                  <input type="checkbox" checked={!!r.checks[a]} onChange={(e) => { rt.perms.set(role, r.module, a, e.target.checked); rt.emit(); }} />
                </td>
              ))}
              <td className="text-center text-[11px] font-bold" style={{ color: r.result === 'master' ? 'var(--accent)' : r.result === 'reader' ? 'var(--good)' : 'var(--ink-3)' }}>{r.result}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </SkCard>
  );
}

/* ---- setting-admin-notice + notify-sound-alerts + header-notification-center ---- */
function SkNoticeCard({ rt }) {
  const stored = rt.notice.stored();
  const [text, setText] = React.useState(stored ? stored.text : '');
  const [hours, setHours] = React.useState(24);
  const [msg, setMsg] = React.useState(null);
  const active = rt.notice.active();
  return (
    <SkCard id="sk-notice" icon="Megaphone" title="관리자 공지" skills={['setting-admin-notice', 'header-notification-center', 'notify-sound-alerts']}
      desc="280자 공지 하나. 모든 화면 상단에 빨간 배너로, 알림 벨 맨 위에 표시되고 알림음이 울립니다.">
      <textarea className="sb-input resize-none" rows={3} value={text} maxLength={rt.notice.maxLength} onChange={(e) => setText(e.target.value)} placeholder="공지 내용" />
      <div className="flex flex-wrap items-center gap-2 mt-2">
        <span className="text-[11.5px] ink-3 sb-num">{rt.notice.remaining(text)}자 남음</span>
        <div className="flex-1" />
        <select className="sb-input !w-auto !py-1.5 !text-[12px]" value={hours} onChange={(e) => setHours(Number(e.target.value))}>
          {rt.notice.choices().map((h) => <option key={h} value={h}>{h === 0 ? '만료 없음' : h + '시간 후 만료'}</option>)}
        </select>
        {active && <SkBtn danger onClick={async () => { const r = await rt.notice.clear(); setText(''); setMsg(skResult(r, '공지를 내렸습니다')); }}>내리기</SkBtn>}
        <SkBtn primary onClick={async () => {
          const r = await rt.notice.publish(text, { expireHours: hours });
          if (r && r.ok !== false) { rt.sound.unlock(); rt.sound.alert('admin.notice', { relevant: true }); }
          setMsg(skResult(r, '게시됨 — 상단 배너와 알림 벨에 표시됩니다'));
        }}>게시</SkBtn>
      </div>
      {active && <SkNote tone="danger">게시 중 · {rt.notice.msLeft() ? Math.ceil(rt.notice.msLeft() / 60000) + '분 남음' : '만료 없음'}</SkNote>}
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-demo-mode + data-records-store ---- */
function SkDemoCard({ rt }) {
  const [hours, setHours] = React.useState(rt.demo.expireHours() || 0);
  const [, tick] = React.useState(0);
  React.useEffect(() => rt.demo.schedule({ onTick: () => tick((n) => n + 1) }), [rt, rt.demo.on]);
  const count = rt.inquiries().filter((q) => q.demo).length;
  return (
    <SkCard id="sk-demo" icon="Sparkles" title="데모 모드 (샘플 문의)" skills={['setting-demo-mode', 'data-records-store']}
      desc="샘플 문의 5건을 관리자에게만 보여줍니다. 직원은 항상 실제 데이터만 봅니다. 만료 시각이 지나면 자동으로 꺼집니다.">
      <div className="flex flex-wrap items-center gap-2">
        <Switch checked={rt.demo.on} label="데모 모드" onChange={async (on) => {
          if (on) { await rt.demo.enable({ expireHours: hours }); await rt.seedDemo(); }
          else await rt.demo.disable();
          rt.emit();
        }} />
        <span className="text-[13px] font-bold ink-1">{rt.demo.on ? '켜짐' : '꺼짐'}</span>
        <span className="text-[12px] ink-3">· 샘플 {count}건</span>
        <div className="flex-1" />
        <select className="sb-input !w-auto !py-1.5 !text-[12px]" value={hours} onChange={(e) => setHours(Number(e.target.value))}>
          {rt.demo.choices().map((h) => <option key={h} value={h}>{h === 0 ? '만료 없음' : h + '시간'}</option>)}
        </select>
        <SkBtn small onClick={async () => { await rt.seedDemo(); rt.toaster.ok('샘플 문의를 다시 채웠습니다'); }}>다시 채우기</SkBtn>
      </div>
      {rt.demo.pill().show && <SkNote tone={rt.demo.pill().soon ? 'warn' : 'info'}>{rt.demo.pill().text}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-language-overrides + platform-i18n-engine ---- */
function SkLangCard({ rt }) {
  const [lang, setLang] = React.useState(rt.lang.current);
  const [q, setQ] = React.useState('');
  const [only, setOnly] = React.useState(false);
  useSkDirty(rt, 'overrides', '문구 수정', () => rt.overrides.readDraft(), () => rt.overrides.commit());
  const rows = rt.overrides.rows(lang, { query: q, onlyOverridden: only });
  return (
    <SkCard id="sk-lang" icon="Languages" title="문구 수정 (언어별)" skills={['setting-language-overrides', 'platform-i18n-engine', 'header-language-switcher']}
      desc="번역 사전을 고치지 않고 문구를 덮어씁니다. 지우면 원래 문구로 돌아갑니다."
      actions={<>
        <SkBtn small onClick={() => skDownload('language-overrides.json', rt.overrides.exportJson())}>내보내기</SkBtn>
        <SkBtn small primary onClick={async () => { const r = await rt.overrides.commit(); rt.dirty.commit('overrides'); rt.toaster.show(r && r.ok === false ? '저장 실패: ' + r.reason : '문구를 저장했습니다', r && r.ok === false ? 'danger' : 'ok'); }}>저장</SkBtn>
      </>}>
      <div className="flex flex-wrap gap-2 mb-2">
        <select className="sb-input !w-auto !py-1.5 !text-[12px]" value={lang} onChange={(e) => setLang(e.target.value)}>
          {rt.lang.list().map((l) => <option key={l} value={l}>{rt.lang.label(l)}</option>)}
        </select>
        <input className="sb-input !py-1.5 !text-[12px] flex-1 min-w-[120px]" placeholder="키 · 문구 검색" value={q} onChange={(e) => setQ(e.target.value)} />
        <label className="flex items-center gap-1.5 text-[12px] ink-2"><input type="checkbox" checked={only} onChange={(e) => setOnly(e.target.checked)} />수정한 것만</label>
      </div>
      <div className="max-h-[260px] overflow-y-auto sb-scroll flex flex-col gap-1">
        {rows.map((r) => (
          <div key={r.key} className="grid grid-cols-[140px_1fr] gap-2 items-center">
            <span className="text-[10.5px] sb-mono ink-3 truncate" title={r.key}>{r.key}</span>
            <input className="sb-input !py-1 !text-[12px]" value={r.value || ''} placeholder={r.shipped}
              style={r.overridden ? { borderColor: 'var(--accent)' } : undefined}
              onChange={(e) => { rt.overrides.set(lang, r.key, e.target.value); rt.emit(); }} />
          </div>
        ))}
      </div>
      <div className="text-[11px] ink-3 mt-2">수정 {rt.overrides.count(lang)}건 · 누락 키 {rt.i18n.missingIn(lang).length}개</div>
    </SkCard>
  );
}

/* ---- setting-staff-master ---- */
function SkStaffCard({ rt }) {
  const [d, setD] = React.useState({ code: '', name: '', email: '', team: '' });
  const [msg, setMsg] = React.useState(null);
  const teams = rt.teams.options('', { placeholder: '팀 선택' });
  return (
    <SkCard id="sk-staff" icon="Users" title="직원 마스터" skills={['setting-staff-master', 'setting-team-roles']}
      desc="다른 모든 화면이 코드로 참조하는 직원 목록. 로그인 계정과는 분리되어 코드로만 연결됩니다.">
      <div className="flex flex-col gap-1.5 mb-3">
        {rt.staff.rows().map((r, i) => (
          <div key={r.code} className="flex items-center gap-2.5 p-2 rounded-lg" style={{ background: 'var(--surface-3)', opacity: r.active ? 1 : 0.5 }}>
            <span className="w-8 h-8 rounded-full flex items-center justify-center text-[11px] font-extrabold" style={{ background: 'var(--accent)', color: '#fff' }}>{r.initial}</span>
            <div className="min-w-0 flex-1">
              <div className="text-[12.5px] font-bold ink-1">{r.name} <span className="ink-3 font-normal sb-mono text-[11px]">@{r.code}</span></div>
              <div className="text-[11px] ink-3 truncate">{rt.teams.label(r.team)} · {r.email}{r.account ? ' · 계정 연결됨' : ' · 계정 없음'}</div>
            </div>
            <button className="sb-icon-btn !w-7 !h-7" disabled={!r.canReorder || i === 0} onClick={async () => { await rt.staff.move(i, -1); }}><Icon name="ChevronDown" size={14} style={{ transform: 'rotate(180deg)' }} /></button>
            <Switch checked={r.active} label="재직" onChange={async (on) => { const x = await rt.staff.setActive(r.code, on); x && x.ok === false && setMsg(skResult(x)); }} />
          </div>
        ))}
      </div>
      <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
        <input className="sb-input !py-1.5 !text-[12px]" placeholder="코드" value={d.code} onChange={(e) => setD({ ...d, code: e.target.value })} />
        <input className="sb-input !py-1.5 !text-[12px]" placeholder="이름" value={d.name} onChange={(e) => setD({ ...d, name: e.target.value })} />
        <input className="sb-input !py-1.5 !text-[12px]" placeholder="이메일" value={d.email} onChange={(e) => setD({ ...d, email: e.target.value })} />
        <select className="sb-input !py-1.5 !text-[12px]" value={d.team} onChange={(e) => setD({ ...d, team: e.target.value })}>
          {teams.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
        </select>
      </div>
      <div className="flex justify-end mt-2">
        <SkBtn primary small onClick={async () => {
          const v = rt.staff.validate(d, { isNew: true });
          if (!v.ok) return setMsg({ tone: 'danger', text: v.errors.map((e) => e.field + ': ' + e.code).join(', ') });
          const r = await rt.staff.upsert(d); setMsg(skResult(r, '직원을 추가했습니다'));
          if (!r || r.ok !== false) setD({ code: '', name: '', email: '', team: '' });
        }}>직원 추가</SkBtn>
      </div>
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-team-roles ---- */
function SkTeamsCard({ rt }) {
  const [v, setV] = React.useState('');
  const [msg, setMsg] = React.useState(null);
  return (
    <SkCard id="sk-teams" icon="Briefcase" title="팀 목록" skills={['setting-team-roles']}
      desc="직원 마스터의 팀 드롭다운. 사용 중인 팀은 지우기 전에 막고 몇 명이 쓰는지 알려줍니다."
      actions={<SkBtn small onClick={async () => setMsg(skResult(await rt.teams.resetToDefaults(), '기본값으로 되돌렸습니다'))}>기본값</SkBtn>}>
      <div className="flex flex-wrap gap-1.5 mb-3">
        {rt.teams.rows().map((r) => (
          <span key={r.value} className="inline-flex items-center gap-1 text-[12px] font-semibold px-2 py-1 rounded-lg" style={{ background: 'var(--surface-3)' }}>
            {rt.t(r.label)}{r.inUse ? <span className="ink-3 text-[10.5px]">· {r.inUse}명</span> : null}
            <button className="ink-3" onClick={async () => setMsg(skResult(await rt.teams.remove(r.index), '삭제했습니다'))}><Icon name="X" size={12} /></button>
          </span>
        ))}
      </div>
      <div className="flex gap-2">
        <input className="sb-input !py-1.5 !text-[12.5px]" placeholder="새 팀 이름" value={v} onChange={(e) => setV(e.target.value)} />
        <SkBtn primary small onClick={async () => { const r = await rt.teams.add(v); setMsg(skResult(r, '추가했습니다')); if (!r || r.ok !== false) setV(''); }}>추가</SkBtn>
      </div>
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-perf-items ---- */
function SkPerfCard({ rt }) {
  const warn = rt.perf.warnings();
  return (
    <SkCard id="sk-perf" icon="Gauge" title="평가 항목" skills={['setting-perf-items']}
      desc="응대 품질 점수 체계를 코드가 아닌 설정으로. 항목마다 최대 점수와 자동 규칙이 있습니다.">
      <div className="flex flex-col gap-1.5">
        {rt.perf.rows().map((r) => (
          <div key={r.id} className="flex items-center gap-2.5 p-2 rounded-lg" style={{ background: 'var(--surface-3)', opacity: r.active ? 1 : 0.5 }}>
            <div className="min-w-0 flex-1">
              <div className="text-[12.5px] font-bold ink-1">{r.name} <span className="ink-3 font-semibold sb-num">· {r.max}점</span></div>
              <div className="text-[11px] ink-3 truncate">{r.auto ? '자동: ' + r.auto : '수동 평가'} · {r.desc}</div>
            </div>
            <Switch checked={r.active} label="사용" onChange={async (on) => { await rt.perf.setActive(r.id, on); }} />
          </div>
        ))}
      </div>
      <div className="text-[11.5px] ink-3 mt-2 sb-num">합계 {rt.perf.maxTotal()}점</div>
      {warn.length > 0 && <SkNote tone="warn">{warn.join(' · ')}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-daily-slots ---- */
function SkSlotsCard({ rt }) {
  const [d, setD] = React.useState({ key: '', label: '', due: 18 });
  const [msg, setMsg] = React.useState(null);
  return (
    <SkCard id="sk-slots" icon="Calendar" title="일일 보고 항목" skills={['setting-daily-slots']}
      desc="직원이 매일 채우는 칸. 키는 고정, 이름은 자유롭게 — 이름을 바꿔도 지난 보고가 깨지지 않습니다."
      actions={<SkBtn small onClick={async () => setMsg(skResult(await rt.slots.restoreDefaults(), '기본 항목을 되살렸습니다'))}>기본값</SkBtn>}>
      <div className="flex flex-col gap-1.5">
        {rt.slots.rows().map((r, i) => (
          <div key={r.key} className="flex items-center gap-2.5 p-2 rounded-lg" style={{ background: 'var(--surface-3)' }}>
            <span className="text-[16px]">{r.icon}</span>
            <div className="min-w-0 flex-1">
              <div className="text-[12.5px] font-bold ink-1">{rt.t(r.label)} <span className="sb-mono ink-3 text-[10.5px]">{r.key}</span></div>
              <div className="text-[11px] ink-3">{rt.t(r.hint)} · {r.due == null ? '알림 없음' : r.due + '시까지'}</div>
            </div>
            <button className="sb-icon-btn !w-7 !h-7" disabled={i === 0} onClick={async () => await rt.slots.move(i, -1)}><Icon name="ChevronDown" size={14} style={{ transform: 'rotate(180deg)' }} /></button>
            {!r.shipped && <button className="sb-icon-btn !w-7 !h-7" onClick={async () => setMsg(skResult(await rt.slots.remove([r.key]), '삭제했습니다'))}><Icon name="Trash2" size={13} /></button>}
          </div>
        ))}
      </div>
      <div className="grid grid-cols-[1fr_1fr_70px_auto] gap-2 mt-3">
        <input className="sb-input !py-1.5 !text-[12px]" placeholder="키 (영문)" value={d.key} onChange={(e) => setD({ ...d, key: e.target.value })} />
        <input className="sb-input !py-1.5 !text-[12px]" placeholder="이름" value={d.label} onChange={(e) => setD({ ...d, label: e.target.value })} />
        <input className="sb-input !py-1.5 !text-[12px]" type="number" min="0" max="23" value={d.due} onChange={(e) => setD({ ...d, due: Number(e.target.value) })} />
        <SkBtn primary small onClick={async () => {
          const v = rt.slots.validate(d, { isNew: true });
          if (!v.ok) return setMsg({ tone: 'danger', text: v.errors.map((e) => e.field + ': ' + e.code).join(', ') });
          setMsg(skResult(await rt.slots.upsert(d), '추가했습니다')); setD({ key: '', label: '', due: 18 });
        }}>추가</SkBtn>
      </div>
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- setting-video-guide + data-file-storage ---- */
function SkVideoCard({ rt }) {
  const [url, setUrl] = React.useState(rt.cfg.TUTORIAL_URL || '');
  const [view, setView] = React.useState(null);
  const [msg, setMsg] = React.useState(null);
  React.useEffect(() => { let a = true; rt.video.view().then((v) => a && setView(v)).catch(() => {}); return () => { a = false; }; }, [rt, rt.cfg.TUTORIAL_URL, rt.cfg.TUTORIAL_FILE]);
  const parsed = url ? rt.video.parseUrl(url) : null;
  return (
    <SkCard id="sk-video" icon="CirclePlay" title="영상 사용 가이드" skills={['setting-video-guide', 'data-file-storage']}
      desc="YouTube · Vimeo · Loom 주소를 넣거나 영상 파일을 올립니다 (브라우저 보관이라 작은 파일만).">
      <div className="flex gap-2">
        <input className="sb-input !py-1.5 !text-[12.5px]" placeholder="https://www.youtube.com/watch?v=…" value={url} onChange={(e) => setUrl(e.target.value)} />
        <SkBtn primary small onClick={async () => setMsg(skResult(await rt.video.setUrl(url), '저장했습니다'))}>저장</SkBtn>
        <label className="sb-btn sb-btn-ghost !px-2.5 !py-1.5 !text-[11.5px] cursor-pointer shrink-0">
          파일<input type="file" accept="video/*" hidden onChange={async (e) => { const f = e.target.files[0]; if (!f) return; const v = rt.video.validate(f); if (v && v.ok === false) return setMsg(skResult(v)); setMsg(skResult(await rt.video.upload(f), '업로드했습니다')); }} />
        </label>
      </div>
      {parsed && parsed.ok === false && <SkNote tone="danger">주소 오류: {parsed.reason}</SkNote>}
      {view && view.kind === 'url' && view.embedUrl && (
        <iframe className="w-full aspect-video rounded-xl mt-3" src={view.embedUrl} title="video guide" allow="fullscreen; picture-in-picture" />
      )}
      {view && view.kind === 'file' && view.src && <video className="w-full rounded-xl mt-3" src={view.src} controls />}
      {view && view.kind === 'file' && <div className="flex justify-end mt-2"><SkBtn small danger onClick={async () => setMsg(skResult(await rt.video.removeFile(), '파일을 지웠습니다'))}>파일 삭제</SkBtn></div>}
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- notify-sound-alerts ---- */
function SkSoundCard({ rt }) {
  const cfg = rt.cfg;
  const trig = cfg.SOUND_TRIGGERS || {};
  const map = cfg.SOUND_MAP || {};
  const voices = rt.sound.usableVoices ? rt.sound.usableVoices() : [];
  const used = ['sync.task', 'admin.notice'];
  const label = { 'sync.task': '신규 문의 도착', 'admin.notice': '관리자 공지' };
  return (
    <SkCard id="sk-sound" icon="Volume2" title="알림음 · 음성 알림" skills={['notify-sound-alerts']}
      desc="트리거마다 차임 프리셋을 고르고, 음성 알림은 관련 있을 때만 말합니다. 브라우저 정책상 먼저 [소리 켜기]를 눌러야 합니다.">
      <div className="flex flex-wrap items-center gap-3 mb-3">
        <SkBtn small primary={!rt.sound.unlocked} onClick={() => { rt.sound.unlock(); rt.emit(); }}>{rt.sound.unlocked ? '소리 켜짐 ✓' : '소리 켜기'}</SkBtn>
        <label className="flex items-center gap-2 text-[12.5px] ink-2"><Switch checked={cfg.SOUND_ON !== false} label="효과음" onChange={(on) => rt.save({ SOUND_ON: on })} />효과음</label>
        <label className="flex items-center gap-2 text-[12.5px] ink-2"><Switch checked={!!cfg.TTS_ON} label="음성" onChange={(on) => rt.save({ TTS_ON: on })} />음성 알림</label>
      </div>
      {used.map((k) => (
        <div key={k} className="flex items-center gap-2 py-1.5 border-t sb-border">
          <Switch checked={rt.sound.isTriggerOn(k)} label={k} onChange={(on) => rt.save({ SOUND_TRIGGERS: { ...trig, [k]: on } })} />
          <span className="text-[12.5px] font-semibold ink-1 flex-1">{label[k]} <span className="sb-mono ink-3 text-[10.5px]">{k}</span></span>
          <select className="sb-input !w-auto !py-1 !text-[12px]" value={rt.sound.presetFor(k)} onChange={(e) => rt.save({ SOUND_MAP: { ...map, [k]: e.target.value } })}>
            {rt.sound.presets().map((p) => <option key={p} value={p}>{p}</option>)}
          </select>
          <SkBtn small onClick={() => { rt.sound.unlock(); rt.sound.test(k); }}>▶</SkBtn>
        </div>
      ))}
      {voices.length > 0 && (
        <div className="flex items-center gap-2 pt-2 border-t sb-border">
          <span className="text-[12px] ink-3">음성</span>
          <select className="sb-input !py-1 !text-[12px] flex-1" value={cfg.TTS_VOICE || ''} onChange={(e) => rt.save({ TTS_VOICE: e.target.value })}>
            <option value="">자동</option>
            {voices.map((v) => <option key={v.name} value={v.name}>{v.name} ({v.lang})</option>)}
          </select>
          <SkBtn small onClick={() => { rt.sound.unlock(); rt.sound.testVoice(); }}>말하기</SkBtn>
        </div>
      )}
    </SkCard>
  );
}

/* ---- platform-date-time-picker (변경 이력의 날짜 필터에 사용) ---- */
function SkDatePicker({ rt, value, onChange, placeholder }) {
  const [open, setOpen] = React.useState(false);
  const pRef = React.useRef(null);
  const [, tick] = React.useState(0);
  if (!pRef.current) pRef.current = rt.picker({ value: value || '', onChange: (v) => { onChange(v); setOpen(false); } });
  const p = pRef.current;
  return (
    <div className="relative">
      <button className="sb-input !py-1.5 !text-[12px] text-left w-[120px]" onClick={() => { if (!open) p.open(); setOpen(!open); tick((n) => n + 1); }}>
        {value || <span className="ink-3">{placeholder}</span>}
      </button>
      <SkPopover open={open} onClose={() => setOpen(false)} width={250} align="left">
        <div className="p-3">
          <div className="flex items-center justify-between mb-2">
            <button className="sb-icon-btn !w-7 !h-7" onClick={() => { p.shiftMonth(-1); tick((n) => n + 1); }}><Icon name="ChevronLeft" size={14} /></button>
            <span className="text-[12.5px] font-bold ink-1">{p.monthLabel()}</span>
            <button className="sb-icon-btn !w-7 !h-7" onClick={() => { p.shiftMonth(1); tick((n) => n + 1); }}><Icon name="ChevronRight" size={14} /></button>
          </div>
          <div className="grid grid-cols-7 gap-0.5 text-center">
            {p.weekdays().map((w) => <span key={w} className="text-[10px] font-bold ink-3 py-1">{w}</span>)}
            {p.grid().map((c) => (
              <button key={c.date} disabled={c.disabled} onClick={() => p.pickDate(c.date)}
                className="text-[11.5px] py-1 rounded-md"
                style={{ opacity: c.outside ? 0.35 : 1, background: c.selected ? 'var(--accent)' : c.today ? 'var(--surface-3)' : 'transparent', color: c.selected ? '#fff' : 'var(--ink-1)' }}>
                {c.day}
              </button>
            ))}
          </div>
          <div className="flex justify-between mt-2">
            <button className="text-[11.5px] font-semibold" style={{ color: 'var(--accent)' }} onClick={() => { p.goToday(); tick((n) => n + 1); }}>오늘</button>
            <button className="text-[11.5px] font-semibold ink-3" onClick={() => { p.clear(); onChange(''); setOpen(false); }}>지우기</button>
          </div>
        </div>
      </SkPopover>
    </div>
  );
}

/* ---- setting-change-log + data-audit-log ---- */
function SkChangeLogCard({ rt }) {
  const [f, setF] = React.useState({ query: '', action: '', module: '', actor: '', from: '', to: '' });
  const [page, setPage] = React.useState(0);
  const filters = { ...f, from: f.from || undefined, to: f.to ? f.to + 'T23:59:59' : undefined };
  const p = rt.changeLog.page(filters, { page });
  const facets = rt.changeLog.facets();
  const set = (k) => (e) => { setF({ ...f, [k]: e && e.target ? e.target.value : e }); setPage(0); };
  return (
    <SkCard id="sk-changelog" icon="History" title="변경 이력" wide skills={['setting-change-log', 'data-audit-log', 'platform-date-time-picker']}
      desc="로그인 · 설정 저장 · 답변 전송 등 모든 기록. 비밀값은 가리고 바뀐 필드만 보여줍니다."
      actions={<>
        <SkBtn small onClick={() => skDownload('change-log.csv', rt.changeLog.exportCsv(filters), 'text/csv')}>CSV</SkBtn>
        <SkBtn small onClick={() => skDownload('change-log.json', rt.changeLog.exportJson(filters))}>JSON</SkBtn>
        {rt.changeLog.selectedCount > 0 && <SkBtn small danger onClick={async () => { await rt.changeLog.removeSelected(); rt.emit(); }}>선택 삭제 ({rt.changeLog.selectedCount})</SkBtn>}
      </>}>
      <div className="flex flex-wrap gap-2 mb-3">
        <input className="sb-input !py-1.5 !text-[12px] w-40" placeholder="검색" value={f.query} onChange={set('query')} />
        {['action', 'module', 'actor'].map((k) => (
          <select key={k} className="sb-input !w-auto !py-1.5 !text-[12px]" value={f[k]} onChange={set(k)}>
            <option value="">{{ action: '모든 동작', module: '모든 메뉴', actor: '모든 사람' }[k]}</option>
            {(facets[k] || []).map((x) => <option key={x.value || x} value={x.value || x}>{x.label || x.value || x}</option>)}
          </select>
        ))}
        <SkDatePicker rt={rt} value={f.from} onChange={set('from')} placeholder="시작일" />
        <SkDatePicker rt={rt} value={f.to} onChange={set('to')} placeholder="종료일" />
      </div>
      <div className="flex flex-col">
        {p.rows.length === 0 && <div className="text-[12.5px] ink-3 py-4 text-center">기록이 없습니다</div>}
        {p.rows.map((r) => (
          <div key={r.id} className="flex items-start gap-2.5 py-2 border-t sb-border">
            <input type="checkbox" className="mt-1" checked={!!r.selected} onChange={(e) => { rt.changeLog.toggle(r.id, e.target.checked); rt.emit(); }} />
            <div className="min-w-0 flex-1">
              <div className="text-[12.5px] ink-1"><b style={{ color: r.notable ? 'var(--warning)' : 'var(--ink-1)' }}>{r.action}</b> · {r.summary}</div>
              <div className="text-[11px] ink-3">{skFmt(r.at)} · {r.actor || '-'} · {r.module}</div>
              {r.changes.length > 0 && (
                <div className="mt-1 flex flex-col gap-0.5">
                  {r.changes.slice(0, 4).map((c) => (
                    <div key={c.field} className="text-[11px] sb-mono"><span className="ink-3">{c.field}:</span> <s className="ink-3">{String(c.before ?? '∅')}</s> → <span className="ink-1">{String(c.after ?? '∅')}</span></div>
                  ))}
                </div>
              )}
            </div>
          </div>
        ))}
      </div>
      <div className="flex items-center justify-between mt-2 text-[11.5px] ink-3">
        <span className="sb-num">{p.total}건</span>
        <div className="flex gap-1.5">
          <SkBtn small disabled={page === 0} onClick={() => setPage(page - 1)}>이전</SkBtn>
          <span className="sb-num self-center">{page + 1} / {p.pages}</span>
          <SkBtn small disabled={page + 1 >= p.pages} onClick={() => setPage(page + 1)}>다음</SkBtn>
        </div>
      </div>
    </SkCard>
  );
}

/* ---- data-backup-restore ---- */
function SkBackupCard({ rt }) {
  const [info, setInfo] = React.useState(null);
  const [text, setText] = React.useState('');
  const [mode, setMode] = React.useState('merge');
  const [msg, setMsg] = React.useState(null);
  return (
    <SkCard id="sk-backup" icon="Save" title="백업 · 복원" skills={['data-backup-restore', 'data-records-store']}
      desc="워크스페이스 전체를 JSON 하나로. 비밀키는 내보낼 때 비워지고, 복원은 미리보기 후 확인을 거쳐야 실행됩니다.">
      <div className="flex flex-wrap gap-2">
        <SkBtn primary onClick={() => { const ex = rt.backup.export({}); if (ex.ok === false) return setMsg(skResult(ex)); skDownload(ex.fileName, ex.text); setMsg({ tone: 'ok', text: '내보냄 · ' + Math.round(ex.bytes / 1024) + ' KB' }); }}>
          <Icon name="ArrowDownRight" size={14} />내보내기
        </SkBtn>
        <label className="sb-btn sb-btn-ghost !px-3 !py-2 !text-[12.5px] cursor-pointer">
          <Icon name="ArrowUpRight" size={14} />파일 열기
          <input type="file" accept="application/json,.json" hidden onChange={async (e) => {
            const f = e.target.files[0]; if (!f) return;
            const t = await skReadFile(f); setText(t);
            const i = rt.backup.inspect(t); setInfo(i); setMsg(i.ok === false ? skResult(i) : null);
          }} />
        </label>
      </div>
      {info && info.ok !== false && (
        <div className="mt-3 p-3 rounded-lg text-[12px] ink-2" style={{ background: 'var(--surface-3)' }}>
          <div>레코드: {Object.entries(info.counts || {}).map(([m, n]) => m + ' ' + n + '건').join(' · ') || '없음'}</div>
          <div>직원 {(info.members || []).length || info.members || 0}명 · 설정 포함: {info.hasConfig ? '예' : '아니요'}</div>
          {(info.warnings || []).length > 0 && <div style={{ color: 'var(--warning)' }}>경고: {info.warnings.join(', ')}</div>}
          <div className="flex items-center gap-2 mt-2">
            <select className="sb-input !w-auto !py-1 !text-[12px]" value={mode} onChange={(e) => setMode(e.target.value)}>
              {rt.modules['data-backup-restore'].MODES.map((m) => <option key={m} value={m}>{m}</option>)}
            </select>
            <SkBtn small primary onClick={async () => {
              const plan = await rt.backup.plan(text, { mode });
              if (plan.ok === false) return setMsg(skResult(plan));
              const ok = await skConfirm(rt, '복원 실행', '모드: ' + mode + '\n' + JSON.stringify(plan.summary || plan, null, 1).slice(0, 400), '복원', true);
              if (!ok) return;
              const r = await rt.backup.restore(text, { mode, confirmed: true, restoreConfig: false });
              await rt.sync.syncAll();
              setMsg(skResult(r, '복원했습니다'));
            }}>미리보기 후 복원</SkBtn>
          </div>
        </div>
      )}
      {msg && <SkNote tone={msg.tone}>{msg.text}</SkNote>}
    </SkCard>
  );
}

/* ---- data-records-store · data-realtime-sync · data-pending-queue · data-client-errors · data-file-storage · data-audit-log ---- */
function SkDataCard({ rt }) {
  const s = rt.sync.state;
  const es = rt.errors.stats;
  const files = Object.entries(rt.bucket.list());
  const errRows = (() => { try { return JSON.parse(localStorage.getItem('velrano:errors') || '[]'); } catch { return []; } })();
  const Stat = ({ k, v }) => <div className="p-2.5 rounded-lg" style={{ background: 'var(--surface-3)' }}><div className="text-[10.5px] ink-3 font-bold">{k}</div><div className="text-[14px] font-extrabold ink-1 sb-num">{v}</div></div>;
  return (
    <SkCard id="sk-data" icon="Database" title="데이터 계층 상태" skills={['data-records-store', 'data-realtime-sync', 'data-pending-queue', 'data-client-errors', 'data-file-storage', 'data-audit-log']}
      desc="문의는 레코드 저장소에 쌓이고, 다른 탭과 실시간으로 맞춰지며, 실패한 쓰기는 큐에 남습니다. 브라우저 오류는 자동 수집됩니다.">
      <div className="grid grid-cols-3 gap-2">
        <Stat k="레코드 (문의)" v={rt.cache.read('inquiries').length} />
        <Stat k={'동기화 · ' + s.mode} v={s.status} />
        <Stat k="미전송 큐" v={rt.queue.length} />
        <Stat k="감사 로그 대기" v={rt.audit.pending} />
        <Stat k="수집된 오류" v={es.sent + ' / 억제 ' + es.suppressed} />
        <Stat k="보관 파일" v={files.length} />
      </div>
      <div className="flex flex-wrap gap-2 mt-3">
        <SkBtn small onClick={async () => { await rt.sync.syncAll(); rt.toaster.ok('동기화했습니다'); }}>지금 동기화</SkBtn>
        <SkBtn small onClick={async () => { const r = await rt.queue.flush(); rt.toaster.info('큐 전송: ' + r.sent + '건, 남음 ' + r.kept + '건'); }}>큐 전송</SkBtn>
        <SkBtn small onClick={async () => { await rt.audit.flush(); rt.toaster.ok('감사 로그를 기록했습니다'); }}>로그 기록</SkBtn>
        <SkBtn small onClick={() => { rt.errors.report(new Error('수동 테스트 오류 · ' + new Date().toLocaleTimeString())); }}>오류 보고 테스트</SkBtn>
      </div>
      {s.lastAt && <div className="text-[11px] ink-3 mt-2">마지막 동기화 {skFmt(s.lastAt)}{s.error ? ' · 오류: ' + (s.error.message || s.error) : ''}</div>}
      {errRows.length > 0 && (
        <div className="mt-3 max-h-[110px] overflow-y-auto sb-scroll">
          {errRows.slice(0, 6).map((e, i) => <div key={i} className="text-[11px] sb-mono ink-3 truncate">{skFmt(e.at || e.created_at)} · {e.message || e.msg}</div>)}
        </div>
      )}
    </SkCard>
  );
}

/* ---- integration-* ---- */
function SkIntegrationsCard({ rt }) {
  const [tab, setTab] = React.useState('gemini');
  const tabs = [['gemini', 'Gemini AI'], ['translate', '번역'], ['kv', 'Cloudflare KV'], ['github', 'GitHub 백업'], ['drive', 'Google Drive'], ['hooks', '웹훅']];
  return (
    <SkCard id="sk-int" icon="PlugZap" title="외부 연동" wide
      skills={['integration-gemini-ai', 'integration-cloud-translate', 'integration-cloudflare-kv', 'integration-github-backup', 'integration-google-drive', 'integration-webhook-connector']}
      desc="키를 넣으면 바로 동작합니다. AI 키는 AES-GCM 으로 봉인되어 저장됩니다. 키가 없으면 각 스킬이 무엇이 빠졌는지 그대로 알려줍니다.">
      <div className="flex flex-wrap gap-1.5 mb-4">
        {tabs.map(([id, l]) => <SkBtn key={id} small primary={tab === id} onClick={() => setTab(id)}>{l}</SkBtn>)}
      </div>
      {tab === 'gemini' && <SkGemini rt={rt} />}
      {tab === 'translate' && <SkTranslate rt={rt} />}
      {tab === 'kv' && <SkCfgForm rt={rt} skill={rt.kv} keys={[['CF_ACCOUNT_ID', 'Account ID'], ['CF_API_TOKEN', 'API Token', true], ['CF_KV_NAMESPACE', 'KV Namespace ID'], ['CF_WORKER_URL', 'Worker URL (선택)'], ['CF_PAGES_PROJECT', 'Pages 프로젝트 (선택)']]}
        actions={[['토큰 확인', () => rt.kv.verify()], ['스냅샷 저장', () => rt.kv.put({})], ['스냅샷 읽기', () => rt.kv.get({})]]} />}
      {tab === 'github' && <SkCfgForm rt={rt} skill={rt.github} keys={[['GH_OWNER', 'Owner'], ['GH_REPO', 'Repository'], ['GH_BRANCH', 'Branch'], ['GH_PATH', '파일 경로 (예: backup/velrano.json)'], ['GH_TOKEN', 'Token', true]]}
        actions={[['연결 확인', () => rt.github.test()], ['백업 올리기', () => rt.github.push({ message: 'VELRANO SYSTEM backup' })], ['백업 받기', () => rt.github.pull()]]} />}
      {tab === 'drive' && <SkCfgForm rt={rt} skill={rt.drive} missingFn={() => rt.drive.missing()} keys={[['GDRIVE_CLIENT_ID', 'OAuth Client ID'], ['GDRIVE_FOLDER_ID', '폴더 ID']]}
        actions={[['Google 로그인', () => rt.drive.token({ interactive: true })], ['폴더 확인', () => rt.drive.test()]]} />}
      {tab === 'hooks' && <SkHooks rt={rt} />}
    </SkCard>
  );
}

function SkResultLine({ r }) {
  if (!r) return null;
  const tone = r.ok === false ? 'danger' : 'ok';
  const text = r.ok === false ? (r.reason || 'error') + (r.message ? ' · ' + r.message : r.error && r.error.message ? ' · ' + r.error.message : '')
    : JSON.stringify(r, (k, v) => (k === 'data' || k === 'token' ? undefined : v)).slice(0, 220);
  return <SkNote tone={tone}>{text}</SkNote>;
}

function SkCfgForm({ rt, skill, keys, actions, missingFn }) {
  const [d, setD] = React.useState(() => Object.fromEntries(keys.map(([k]) => [k, rt.cfg[k] || ''])));
  const [r, setR] = React.useState(null);
  const conf = missingFn ? { ok: missingFn().length === 0, missing: missingFn() } : skill.configured();
  return (
    <div>
      <div className="grid sm:grid-cols-2 gap-2.5">
        {keys.map(([k, l, secret]) => (
          <SkField key={k} label={l}><input className="sb-input !py-1.5 !text-[12.5px]" type={secret ? 'password' : 'text'} value={d[k]} onChange={(e) => setD({ ...d, [k]: e.target.value })} /></SkField>
        ))}
      </div>
      <div className="flex flex-wrap items-center gap-2 mt-3">
        <span className="text-[11.5px] font-semibold" style={{ color: conf.ok ? 'var(--good)' : 'var(--warning)' }}>{conf.ok ? '설정 완료' : '빠진 값: ' + conf.missing.join(', ')}</span>
        <div className="flex-1" />
        <SkBtn small primary onClick={async () => { await rt.save(d); rt.audit.push({ action: 'update', module: 'settings', label: 'integration:' + keys[0][0] }); setR({ ok: true, saved: true }); }}>저장</SkBtn>
        {actions.map(([l, fn]) => <SkBtn key={l} small onClick={async () => { try { setR(await fn()); } catch (e) { setR({ ok: false, reason: 'error', message: e.message }); } }}>{l}</SkBtn>)}
      </div>
      <SkResultLine r={r} />
    </div>
  );
}

function SkGemini({ rt }) {
  const [key, setKey] = React.useState('');
  const [masked, setMasked] = React.useState('');
  const [prompt, setPrompt] = React.useState('고객 문의에 공손하게 답하는 한 문장을 써 주세요.');
  const [r, setR] = React.useState(null);
  React.useEffect(() => { rt.gemini.mask().then(setMasked).catch(() => {}); }, [rt, rt.cfg.GEMINI_API_KEY]);
  return (
    <div>
      <div className="grid sm:grid-cols-[1fr_200px] gap-2.5">
        <SkField label={'API 키 ' + (masked ? '(저장됨: ' + masked + ')' : '')}><input className="sb-input !py-1.5 !text-[12.5px]" type="password" value={key} onChange={(e) => setKey(e.target.value)} placeholder="AIza…" /></SkField>
        <SkField label="모델">
          <select className="sb-input !py-1.5 !text-[12.5px]" value={rt.gemini.model()} onChange={(e) => rt.gemini.setModel(e.target.value)}>
            {rt.gemini.MODELS.map((m) => <option key={m} value={m}>{m}</option>)}
          </select>
        </SkField>
      </div>
      <div className="flex flex-wrap items-center gap-2 mt-3">
        <label className="flex items-center gap-2 text-[12.5px] ink-2"><Switch checked={rt.gemini.enabled()} label="사용" onChange={(on) => rt.gemini.setEnabled(on)} />사용</label>
        <div className="flex-1" />
        <SkBtn small primary onClick={async () => { setR(await rt.gemini.setKey(key) || { ok: true }); setKey(''); }}>키 봉인 저장</SkBtn>
        <SkBtn small onClick={async () => setR(await rt.gemini.test())}>연결 테스트</SkBtn>
      </div>
      <div className="flex gap-2 mt-3">
        <input className="sb-input !py-1.5 !text-[12.5px]" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
        <SkBtn small onClick={async () => setR(await rt.gemini.call(prompt, { maxOutputTokens: 200 }))}>실행</SkBtn>
      </div>
      {r && r.ok !== false && r.text ? <SkNote tone="ok">{r.text}</SkNote> : <SkResultLine r={r} />}
    </div>
  );
}

function SkTranslate({ rt }) {
  const [key, setKey] = React.useState('');
  const [text, setText] = React.useState('도착 시간이 18:40 으로 바뀌었어요.');
  const [to, setTo] = React.useState('en');
  const [r, setR] = React.useState(null);
  return (
    <div>
      <div className="flex gap-2">
        <input className="sb-input !py-1.5 !text-[12.5px]" type="password" value={key} onChange={(e) => setKey(e.target.value)} placeholder="Google Cloud Translation API 키" />
        <SkBtn small primary onClick={async () => { setR(await rt.translate.setKey(key) || { ok: true }); setKey(''); }}>저장</SkBtn>
        <label className="flex items-center gap-2 text-[12.5px] ink-2 shrink-0"><Switch checked={rt.translate.enabled()} label="사용" onChange={(on) => rt.translate.setEnabled(on)} />사용</label>
      </div>
      <div className="flex gap-2 mt-3">
        <input className="sb-input !py-1.5 !text-[12.5px]" value={text} onChange={(e) => setText(e.target.value)} />
        <select className="sb-input !w-auto !py-1.5 !text-[12.5px]" value={to} onChange={(e) => setTo(e.target.value)}>
          {rt.lang.list().map((l) => <option key={l} value={l}>{l}</option>)}
        </select>
        <SkBtn small onClick={async () => { try { const out = await rt.translate.one(text, to); setR(typeof out === 'string' ? { ok: true, text: out } : out); } catch (e) { setR({ ok: false, reason: e.reason || 'error', message: e.message }); } }}>번역</SkBtn>
      </div>
      {r && r.text ? <SkNote tone="ok">{r.text}</SkNote> : <SkResultLine r={r} />}
    </div>
  );
}

function SkHooks({ rt }) {
  const [d, setD] = React.useState({ name: '', url: '', events: [] });
  const [r, setR] = React.useState(null);
  return (
    <div>
      <div className="flex flex-col gap-1.5 mb-3">
        {rt.hooks.rows().length === 0 && <div className="text-[12px] ink-3">등록된 웹훅이 없습니다. 답변 전송 · 설정 변경 같은 이벤트가 등록한 주소로 POST 됩니다.</div>}
        {rt.hooks.rows().map((h) => (
          <div key={h.id} className="flex items-center gap-2 p-2 rounded-lg" style={{ background: 'var(--surface-3)' }}>
            <Switch checked={h.enabled !== false} label="사용" onChange={async () => { await rt.hooks.toggle(h.id); }} />
            <div className="min-w-0 flex-1"><div className="text-[12.5px] font-bold ink-1">{h.name}</div><div className="text-[11px] ink-3 truncate sb-mono">{h.url}</div></div>
            <SkBtn small onClick={async () => setR(await rt.hooks.test(h.id))}>테스트</SkBtn>
            <button className="sb-icon-btn !w-7 !h-7" onClick={async () => { await rt.hooks.remove(h.id); }}><Icon name="Trash2" size={13} /></button>
          </div>
        ))}
      </div>
      <div className="grid sm:grid-cols-[160px_1fr_auto] gap-2">
        <input className="sb-input !py-1.5 !text-[12.5px]" placeholder="이름" value={d.name} onChange={(e) => setD({ ...d, name: e.target.value })} />
        <input className="sb-input !py-1.5 !text-[12.5px]" placeholder="https://…" value={d.url} onChange={(e) => setD({ ...d, url: e.target.value })} />
        <SkBtn small primary onClick={async () => {
          const v = rt.hooks.validate(d);
          if (v && v.ok === false) return setR(v);
          const res = await rt.hooks.add(d); setR(res); if (!res || res.ok !== false) setD({ name: '', url: '', events: [] });
        }}>추가</SkBtn>
      </div>
      <div className="text-[11px] ink-3 mt-2">같은 브라우저의 다른 탭에는 BroadcastChannel 버스로 즉시 전달됩니다{rt.bus.available ? ' (사용 가능)' : ' (이 브라우저는 미지원)'}.</div>
      <SkResultLine r={r} />
    </div>
  );
}

/* ---- auth-company-routing + auth-role-guard ---- */
function SkAccessCard({ rt }) {
  const s = rt.guard.summary(Object.keys(window.SkillRuntime.NAV_REGISTRY));
  return (
    <SkCard id="sk-access" icon="Lock" title="회사 경로 · 접근 요약" skills={['auth-company-routing', 'auth-role-guard', 'shell-sidebar-nav']}
      desc="이 배포의 회사는 URL 경로 /c/{slug} 로 정해집니다. 아래는 지금 로그인한 계정이 실제로 할 수 있는 일입니다.">
      <div className="text-[12.5px] ink-2 mb-3">회사 주소: <span className="sb-mono ink-1">{rt.company.urlOf(rt.workspace.slug, location.origin)}</span></div>
      <table className="w-full text-[12px]">
        <thead><tr className="ink-3"><th className="text-left py-1">메뉴</th><th className="text-left">허용 동작</th><th>표시</th></tr></thead>
        <tbody>
          {Object.entries(s.modules || {}).map(([m, x]) => (
            <tr key={m} className="border-t sb-border">
              <td className="py-1.5 font-semibold ink-1">{rt.t('nav.' + m)}</td>
              <td className="ink-2">{(x.actions || []).join(', ') || '없음'}{x.master ? ' · 관리' : ''}</td>
              <td className="text-center">{x.visible ? '✓' : '—'}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="text-[11px] ink-3 mt-2">역할: {s.role} · 내보내기 {rt.guard.exportAllowed() ? '허용' : '불가'}</div>
    </SkCard>
  );
}

/* =================================================================
   메신저 › 내보내기 (export-pdf-print · export-word-doc)
   ================================================================= */
function SkExportBar({ rt, inquiries }) {
  if (!rt) return null;
  const rows = rt.inquiries().length ? rt.inquiries() : inquiries;
  const cols = [{ key: 'customer', label: '고객' }, { key: 'channel', label: '채널' }, { key: 'subject', label: '문의' }, { key: 'status', label: '상태' }, { key: 'at', label: '접수' }];
  const data = rows.map((q) => ({ customer: q.customer, channel: q.channel, subject: q.subject, status: q.status, at: new Date(q.createdAt).toLocaleString() }));
  const W = rt.modules['export-word-doc'];
  const body = () => '<h1>' + rt.brand.view().system + ' — 문의 목록</h1><p>' + new Date().toLocaleString() + ' · ' + data.length + '건</p>' + W.tableHtml(cols, data);
  const allowed = rt.printer.allowed();
  return (
    <div className="px-4 sm:px-6 pt-4 flex flex-wrap items-center gap-2">
      <span className="text-[12px] ink-3 mr-auto">문의 {data.length}건{rt.demo.on && rt.isAdmin() ? ' (샘플 포함)' : ''}</span>
      <SkBtn small disabled={!allowed} title={allowed ? '' : '관리자 · 매니저만'} onClick={() => rt.printer.printHtml(body(), { documentTitle: rt.printer.fileName([rt.workspace.name, 'inquiries'], 'pdf') })}>
        <Icon name="FileText" size={13} />PDF 인쇄
      </SkBtn>
      <SkBtn small disabled={!allowed} onClick={() => rt.word.download(body(), { fileName: rt.word.fileName([rt.workspace.name, 'inquiries'], 'doc'), title: '문의 목록' })}>
        <Icon name="FileText" size={13} />Word 내보내기
      </SkBtn>
    </div>
  );
}

/* =================================================================
   문의 상세 › 번역 · AI 초안 · Drive (integration-cloud-translate · gemini-ai · google-drive)
   ================================================================= */
function SkInquiryTools({ q, setDraft }) {
  const rt = window.__SKR;
  const [out, setOut] = React.useState(null);
  const [busy, setBusy] = React.useState('');
  if (!rt || !q) return null;
  const last = (q.messages || []).filter((m) => m.from !== 'agent').slice(-1)[0];
  const run = async (k, fn) => { setBusy(k); try { setOut(await fn()); } catch (e) { setOut({ ok: false, reason: e.reason || 'error', message: e.message }); } setBusy(''); };
  return (
    <div className="shrink-0 border-t sb-border px-3 pt-2.5">
      <div className="flex flex-wrap gap-1.5">
        <SkBtn small disabled={!last || !!busy} onClick={() => run('tr', async () => { const t = await rt.translate.one(last.text, rt.lang.current); return typeof t === 'string' ? { ok: true, text: t } : t; })}>
          <Icon name="Languages" size={12} />{busy === 'tr' ? '번역 중…' : '마지막 메시지 번역'}
        </SkBtn>
        <SkBtn small disabled={!last || !!busy} onClick={() => run('ai', async () => {
          const r = await rt.gemini.call('당신은 ' + rt.brand.view().system + ' 상담원입니다. 아래 고객 메시지에 정중하고 짧은 답변 초안을 고객의 언어로 쓰세요.\n\n' + last.text, { maxOutputTokens: 300 });
          if (r && r.ok !== false && r.text) setDraft(r.text);
          return r && r.text ? { ok: true, text: 'AI 초안을 입력창에 넣었습니다.' } : r;
        })}>
          <Icon name="Bot" size={12} />{busy === 'ai' ? '작성 중…' : 'AI 답변 초안'}
        </SkBtn>
        <label className="sb-btn sb-btn-ghost !px-2.5 !py-1.5 !text-[11.5px] cursor-pointer">
          <Icon name="Package" size={12} />Drive 업로드
          <input type="file" hidden onChange={(e) => { const f = e.target.files[0]; if (f) run('dr', async () => { const folder = await rt.drive.folderForTask(q.id, { create: true }); return rt.drive.upload(f, { folderId: folder && folder.id ? folder.id : rt.cfg.GDRIVE_FOLDER_ID, name: f.name }); }); }} />
        </label>
      </div>
      {out && (out.ok === false
        ? <SkNote tone="warn">{(out.reason || 'error') + (out.reason === 'noKey' || out.reason === 'disabled' || /clientId|folderId/.test(String(out.reason)) ? ' — 설정 › 외부 연동에서 키를 넣으세요' : out.message ? ' · ' + out.message : '')}</SkNote>
        : <SkNote tone="ok">{out.text || '완료'}</SkNote>)}
    </div>
  );
}

/* =================================================================
   스킬 라이브러리 — 46개 전부: 로드 여부 · 적용 위치 · 바로가기
   ================================================================= */
function SkLibraryView({ rt, setView }) {
  const [q, setQ] = React.useState('');
  const [cat, setCat] = React.useState(null);
  React.useEffect(() => { fetch('/skills.json', { cache: 'no-store' }).then((r) => r.json()).then(setCat).catch(() => setCat({ groups: [] })); }, []);
  const mods = (rt && rt.modules) || {};
  const term = q.trim().toLowerCase();
  const groups = ((cat && cat.groups) || []).map((g) => ({ ...g, skills: g.skills.filter((s) => !term || (s.name + s.description + (SK_APPLIED[s.slug] || [''])[0]).toLowerCase().includes(term)) })).filter((g) => g.skills.length);
  const loaded = Object.keys(SK_APPLIED).filter((s) => mods[s]).length;
  return (
    <div className="p-4 sm:p-6 max-w-6xl mx-auto">
      <div className="flex flex-wrap items-center gap-3 mb-4">
        <h2 className="text-lg font-bold ink-1">스킬 라이브러리</h2>
        <span className="text-[12px] font-bold px-2 py-1 rounded-lg" style={{ background: loaded === 46 ? 'rgba(27,175,122,.15)' : 'rgba(250,178,25,.15)', color: loaded === 46 ? 'var(--good)' : 'var(--warning)' }}>
          {loaded} / 46 적용됨
        </span>
        <div className="flex-1" />
        <input className="sb-input w-56 max-w-[50vw]" value={q} onChange={(e) => setQ(e.target.value)} placeholder="스킬 · 적용 위치 검색…" />
      </div>
      {!cat && <div className="ink-3 text-sm">불러오는 중…</div>}
      {groups.map((g) => (
        <section key={g.group} className="mb-6">
          <div className="text-[11px] font-bold tracking-[.12em] ink-3 mb-2">{g.group.toUpperCase()} · {g.skills.length}</div>
          <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
            {g.skills.map((s) => {
              const [where, anchor] = SK_APPLIED[s.slug] || ['-', null];
              const ok = !!mods[s.slug];
              return (
                <div key={s.slug} className="sb-card p-3.5 flex flex-col">
                  <div className="flex items-center gap-2 mb-1">
                    <span className="sb-dot" style={{ background: ok ? 'var(--good)' : 'var(--critical)' }} />
                    <span className="font-bold ink-1 text-[13px] sb-mono truncate">{s.name}</span>
                  </div>
                  <div className="ink-3 text-[11.5px] leading-relaxed line-clamp-2 mb-2">{s.description}</div>
                  <div className="mt-auto flex items-center gap-2">
                    <span className="text-[11.5px] font-semibold flex-1" style={{ color: 'var(--accent)' }}>→ {where}</span>
                    {anchor && <SkBtn small onClick={() => { setView('settings'); setTimeout(() => skJump(anchor), 80); }}>열기</SkBtn>}
                  </div>
                </div>
              );
            })}
          </div>
        </section>
      ))}
    </div>
  );
}
