// 단가 사전 — 검색 + 편집 (수정/추가/삭제 → 저장 시 /dict 보관 & 견적 즉시 반영) — window.EstDictViewer
const dvWon = window.EstCalc.won;
const dvBadge = window.EstEditor.gBadge;
const dvFmt = window.fmtNum || ((v) => (v == null ? '' : String(v)));

const cloneItem = (d) => ({ ...d, mat: d.mat ? { ...d.mat } : null, lab: d.lab ? { ...d.lab } : null, cmat: d.cmat ? { ...d.cmat } : null, clab: d.clab ? { ...d.clab } : null });
function setRecent(cur, raw) {
  const s = String(raw).replace(/[^\d.]/g, '');
  if (s === '') return null;
  const n = Math.round(parseFloat(s));
  if (isNaN(n)) return cur;
  return cur ? { ...cur, recent: n } : { recent: n, avg: n, median: n, min: n, max: n, n: 1 };
}

// 이익률 = 1 − 원가합/판가합 (판가=mat/lab, 원가=cmat/clab)
function dvMargin(d) {
  const sell = (d.mat ? d.mat.recent : 0) + (d.lab ? d.lab.recent : 0);
  const cost = (d.cmat ? d.cmat.recent : 0) + (d.clab ? d.clab.recent : 0);
  if (!sell || !cost) return null;
  return (1 - cost / sell) * 100;
}

// 수량 규칙: 품목 수량을 무엇에 비례시킬지. base ∈ 고정/평형/면적(㎡)/욕실수/방수/베란다수
const QR_BASES = [
  { k: 'fixed', label: '고정 (또는 체크 입력 수량)', short: '고정' },
  { k: 'py', label: '평형 비례 (평형 × 계수)', short: '평형' },
  { k: 'area', label: '공급면적(㎡) 비례', short: '면적' },
  { k: 'baths', label: '욕실 개수 비례', short: '욕실수' },
  { k: 'rooms', label: '방 개수 비례', short: '방수' },
  { k: 'verandas', label: '베란다 개수 비례', short: '베란다수' },
  { k: 'expr', label: '수식 (직접 — 조합·치수 등)', short: '수식' },
];
function qtyRuleOf(d) {
  if (d && d.qtyRule) return { base: 'fixed', factor: 1, roundup: true, expr: '', ...d.qtyRule };
  if (d && d.byArea) return { base: 'py', factor: d.byArea.factor, roundup: d.byArea.roundup !== false, expr: '' }; // 레거시 byArea
  return { base: 'fixed', factor: 1, roundup: true, expr: '' };
}
function qtyEval(r, ctx) {   // 규칙+맥락→수량. fixed/값없음이면 null(호출측이 기본값 사용)
  ctx = ctx || {};
  if (!r || r.base === 'fixed') return null;
  if (r.base === 'expr') {
    if (!r.expr || !String(r.expr).trim()) return null;
    try {
      const fn = new Function('평형', '면적', '방', '욕실', '베란다', '침실욕실가로', '침실욕실세로', '거실욕실가로', '거실욕실세로', '주방길이', '올림', '내림', 'return (' + r.expr + ');');
      const q = Number(fn(ctx.py || 0, ctx.area || 0, ctx.rooms || 0, ctx.baths || 0, ctx.verandas || 0, ctx.bw || 0, ctx.bh || 0, ctx.lw || 0, ctx.lh || 0, ctx.km || 0, Math.ceil, Math.floor));
      if (!isFinite(q)) return null;
      return r.roundup ? Math.ceil(q) : Math.round(q * 100) / 100;
    } catch (e) { return null; }
  }
  const v = ctx[r.base] || 0;
  if (!v) return null;
  const q = v * (Number(r.factor) || 0);
  return r.roundup ? Math.ceil(q) : Math.round(q * 100) / 100;
}
function qtyRuleLabel(d) {
  const r = qtyRuleOf(d);
  if (r.base === 'fixed') return '고정';
  if (r.base === 'expr') return '수식';
  const b = QR_BASES.find((x) => x.k === r.base);
  return (b ? b.short : r.base) + '×' + r.factor + (r.roundup ? '↑' : '');
}
window.qtyRuleOf = qtyRuleOf; window.qtyEval = qtyEval;

function DictViewer({ ctlRef, onDirty }) {
  const [all, setAll] = useState(() => window.DICT.map(cloneItem));
  const [q, setQ] = useState('');
  const [gj, setGj] = useState('전체');
  const [dirty, setDirty] = useState(false);
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState('');
  const [sel, setSel] = useState({});       // id -> 선택 여부
  const [grpName, setGrpName] = useState('');
  const [mkSet, setMkSet] = useState(false); // 세트 만들기 모드
  const [setFilter, setSetFilter] = useState(''); // 패키지 칩 필터
  const tabRef = useRef(null);
  const [tabSt, setTabSt] = useState({ s: false, e: false });   // s: 왼쪽으로 스크롤됨, e: 오른쪽 끝 도달
  const onTabScroll = () => { const el = tabRef.current; if (!el) return; setTabSt({ s: el.scrollLeft > 4, e: el.scrollLeft + el.clientWidth >= el.scrollWidth - 4 }); };
  useEffect(() => { onTabScroll(); const h = () => onTabScroll(); window.addEventListener('resize', h); return () => window.removeEventListener('resize', h); }, []);
  const scrollTabs = (dir) => { if (tabRef.current) tabRef.current.scrollBy({ left: dir * 260, behavior: 'smooth' }); };
  const [pkgQ, setPkgQ] = useState('');   // 패키지 모달 내 품목 검색(보조)
  const [pkgGj, setPkgGj] = useState(''); // 패키지 모달에서 선택한 공종
  const [showNew, setShowNew] = useState(false);   // 새 품목 모달
  const [ni, setNi] = useState({ gongjong: '기타공사', gubun: '', name: '', unit: 'EA', mat: '', lab: '', cmat: '', clab: '', setName: '' });
  const setN = (k, v) => setNi((x) => ({ ...x, [k]: v }));
  const [histItem, setHistItem] = useState(null);   // 가격 변경 이력 팝업 대상
  const [qrItem, setQrItem] = useState(null);       // 수량 규칙 팝업 대상
  const [gjEdit, setGjEdit] = useState(null);       // 공종 변경 팝오버 대상 행 id
  const [page, setPage] = useState(0);              // 표 페이지(10개씩)
  const [baseModal, setBaseModal] = useState(false); // ⭐ 기본 포함 항목 선택 팝업
  const [bmQ, setBmQ] = useState('');                 // 기본 포함 모달 검색
  const [bmGj, setBmGj] = useState('');               // 기본 포함 모달 공종 필터
  const [qr, setQr] = useState({ base: 'fixed', factor: 1, roundup: true });

  const bOn = window.baseOn || ((d) => (d.basePct || 0) >= 70);
  const results = useMemo(() => {
    let list = all;
    if (gj !== '전체') list = list.filter((d) => d.gongjong === gj);
    if (setFilter) list = list.filter((d) => (d.setName || '').trim() === setFilter);
    const t = q.trim().toLowerCase();
    if (t) {
      const toks = t.split(/\s+/);
      list = list.filter((d) => {
        const hay = (d.gongjong + ' ' + (d.gubun || '') + ' ' + d.name).toLowerCase();
        return toks.every((tok) => hay.includes(tok));
      });
    }
    // 사용횟수(과거 견적 등장) 많은 순 — 중요/자주 쓰는 품목이 위로
    return list.slice().sort((a, b) => (b.count || 0) - (a.count || 0));
  }, [all, q, gj, setFilter]);
  const baseCount = all.filter(bOn).length;

  // 현재 공종(전체면 전부)에 존재하는 세트명 목록
  const setNames = useMemo(() => {
    const seen = new Set(); const arr = [];
    all.forEach((d) => { const k = (d.setName || '').trim(); if (k && (gj === '전체' || d.gongjong === gj) && !seen.has(k)) { seen.add(k); arr.push(k); } });
    return arr;
  }, [all, gj]);
  const setCount = (n) => all.filter((d) => (d.setName || '').trim() === n).length;
  const allSetNames = useMemo(() => { const s = new Set(); all.forEach((d) => { const k = (d.setName || '').trim(); if (k) s.add(k); }); return [...s]; }, [all]);

  const PAGE = 10;
  const pageCount = Math.max(1, Math.ceil(results.length / PAGE));
  const pg = Math.min(page, pageCount - 1);
  useEffect(() => { setPage(0); }, [q, gj, setFilter]);
  const shown = results.slice(pg * PAGE, pg * PAGE + PAGE);
  // 패키지 모달 목록: 선택한 공종의 품목 → 검색으로 좁힘(보조). 공종 미선택+검색없음이면 이미 체크한 것만.
  const pkgItems = (() => {
    const t = pkgQ.trim().toLowerCase();
    let l = all;
    if (pkgGj) l = l.filter((d) => d.gongjong === pkgGj);
    if (t) { const toks = t.split(/\s+/); l = l.filter((d) => { const hay = (d.gongjong + ' ' + (d.gubun || '') + ' ' + d.name).toLowerCase(); return toks.every((tok) => hay.includes(tok)); }); }
    else if (!pkgGj) return all.filter((d) => sel[d.id]);
    return l.slice(0, 400);
  })();

  const edit = (id, patch) => { setAll((a) => a.map((d) => (d.id === id ? { ...d, ...patch } : d))); setDirty(true); setMsg(''); };
  const editPrice = (id, which, raw) => { setAll((a) => a.map((d) => (d.id === id ? { ...d, [which]: setRecent(d[which], raw) } : d))); setDirty(true); setMsg(''); };
  const del = (id) => { setAll((a) => a.filter((d) => d.id !== id)); setDirty(true); setMsg(''); };
  const toggleSel = (id) => setSel((s) => ({ ...s, [id]: !s[id] }));
  const selCount = Object.values(sel).filter(Boolean).length;
  const selectAllShown = () => setSel((s) => { const n = { ...s }; const allOn = shown.length > 0 && shown.every((d) => n[d.id]); shown.forEach((d) => { n[d.id] = !allOn; }); return n; });
  const groupSelected = () => {
    const name = grpName.trim();
    const ids = Object.keys(sel).filter((k) => sel[k]);
    if (!name || ids.length === 0) return;
    setAll((a) => a.map((d) => (sel[d.id] ? { ...d, setName: name } : d)));
    setDirty(true); setSel({}); setGrpName(''); setMkSet(false);
    setMsg(`${ids.length}개 품목을 '${name}' 패키지로 지정 — 사전 저장을 누르세요`);
  };
  const cancelMkSet = () => { setMkSet(false); setSel({}); setGrpName(''); };
  const openNew = () => { setNi({ gongjong: gj !== '전체' ? gj : '기타공사', gubun: '', name: '', unit: 'EA', mat: '', lab: '', cmat: '', clab: '', setName: '' }); setShowNew(true); };
  const saveNew = () => {
    const name = ni.name.trim(); if (!name) return;
    const id = Math.max(0, ...all.map((d) => d.id || 0)) + 1;
    const item = { id, gongjong: ni.gongjong, gubun: ni.gubun.trim(), name, unit: ni.unit.trim() || 'EA', mat: setRecent(null, ni.mat), lab: setRecent(null, ni.lab), cmat: setRecent(null, ni.cmat), clab: setRecent(null, ni.clab), count: 0, files: 0, basePct: 0, setName: ni.setName.trim() };
    setAll((a) => [item, ...a]); setDirty(true); setShowNew(false); setQ(name); setGj('전체');
    setMsg(`'${name}' 품목 추가됨 — 사전 저장을 누르세요`);
  };
  const save = async () => {
    setSaving(true); setMsg('');
    // 저장 시점에 단가가 바뀐 품목은 변경 이력(history)에 기록 — 월 단위 갱신 추적
    const today = new Date().toISOString().slice(0, 10);
    const prevById = {}; window.DICT.forEach((p) => { prevById[p.id] = p; });
    const allH = all.map((d) => {
      const prev = prevById[d.id];
      const cm = d.mat ? d.mat.recent : null, cl = d.lab ? d.lab.recent : null;
      const pm = prev && prev.mat ? prev.mat.recent : null, pl = prev && prev.lab ? prev.lab.recent : null;
      if (cm === pm && cl === pl) return d;   // 단가 변동 없음 → 이력 추가 안 함
      const hist = Array.isArray(d.history) ? d.history.slice() : [];
      const last = hist[hist.length - 1];
      if (!last || last.mat !== cm || last.lab !== cl) hist.push({ d: today, mat: cm, lab: cl });
      return { ...d, history: hist };
    });
    try {
      const r = await fetch('/dict', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(allH) });
      if (!r.ok) throw new Error('HTTP ' + r.status);
      window.DICT = allH; setAll(allH);   // 견적 화면(검색·기본항목)에 즉시 반영
      setDirty(false); setMsg(`저장됨 (${allH.length}품목) — 견적에 즉시 반영`);
    } catch (e) {
      setMsg('저장 실패: ' + (e.message || '') + ' — 시작.bat(서버)로 실행했는지 확인하세요.');
    } finally { setSaving(false); }
  };
  // 상단바(TopBar)의 "사전 저장" 버튼이 호출할 수 있게 save 등록 + dirty 동기화
  useEffect(() => { if (ctlRef) ctlRef.current.save = save; });
  useEffect(() => { if (onDirty) onDirty(dirty); }, [dirty]);
  useEffect(() => () => { if (onDirty) onDirty(false); }, []);

  const th = { textAlign: 'center', fontSize: 11, fontWeight: 700, color: 'var(--ink-3)', padding: '8px 8px', borderBottom: '1.5px solid var(--line-strong)', position: 'sticky', top: 0, background: 'var(--surface)', whiteSpace: 'nowrap' };
  const thG = { fontSize: 11, fontWeight: 700, color: 'var(--ink-3)', padding: '8px 8px', borderBottom: '1.5px solid var(--line-strong)', whiteSpace: 'nowrap', textAlign: 'center', verticalAlign: 'bottom', background: 'var(--surface)' };
  const thGrp = { fontSize: 9.5, fontWeight: 800, letterSpacing: '0.04em', color: 'var(--ink-3)', padding: '6px 8px 2px', textAlign: 'center', whiteSpace: 'nowrap' };
  const thG2 = { fontSize: 10, fontWeight: 700, color: 'var(--ink-3)', padding: '2px 8px 8px', borderBottom: '1.5px solid var(--line-strong)', textAlign: 'center', whiteSpace: 'nowrap' };
  const cell = { padding: '7px 8px', borderBottom: '1px solid var(--line)', verticalAlign: 'middle' };
  const inp = { width: '100%', border: '1px solid transparent', borderRadius: 6, padding: '6px 7px', outline: 'none', fontSize: 13.5, background: 'transparent' };
  const inpR = { ...inp, textAlign: 'right', fontFamily: 'var(--mono)' };

  return (
    <div style={{ maxWidth: 1320, margin: '0 auto', padding: '20px 24px 80px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
        <h2 style={{ margin: 0, fontSize: 20, fontWeight: 800 }}>단가 사전 <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink-3)' }}>{all.length.toLocaleString()}품목</span></h2>
        <span style={{ flex: 1 }} />
        <button onClick={() => { setBaseModal(true); setBmQ(''); setBmGj(''); }} style={{ padding: '7px 13px', fontSize: 12.5, fontWeight: 700, border: '1px solid var(--line-strong)', borderRadius: 8, background: 'var(--surface)', color: 'var(--ink-2)' }}>⭐ 기본 포함 항목</button>
        <button onClick={() => { setMkSet(true); setSel({}); setGrpName(''); setPkgQ(''); setPkgGj(''); }} style={{ padding: '7px 13px', fontSize: 12.5, fontWeight: 700, border: '1px solid var(--accent)', borderRadius: 8, background: 'var(--accent-soft)', color: 'var(--accent-ink)' }}>＋ 패키지 만들기</button>
        <button onClick={openNew} style={{ padding: '7px 13px', fontSize: 12.5, fontWeight: 700, border: '1px dashed var(--line-strong)', borderRadius: 8, background: 'var(--surface)', color: 'var(--ink-2)' }}>＋ 새 품목</button>
      </div>

      {/* 검색 */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', background: 'var(--surface)', border: '1.5px solid var(--accent)', borderRadius: 9, boxShadow: '0 0 0 4px var(--accent-soft)', marginBottom: 12 }}>
        <span style={{ color: 'var(--accent)', fontSize: 16 }}>⌕</span>
        <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="품목·공종·구분 검색"
          style={{ flex: 1, border: 'none', outline: 'none', padding: '12px 0', background: 'transparent', fontSize: 14 }} />
        {q && <button onClick={() => setQ('')} style={{ padding: '5px 10px', fontSize: 12, border: '1px solid var(--line-strong)', borderRadius: 7, background: 'var(--surface)', color: 'var(--ink-2)', fontWeight: 600 }}>지우기</button>}
      </div>

      {/* 공종 탭 (스크롤바 숨김 + ‹ › 버튼으로 넘김) */}
      <div style={{ position: 'relative', borderBottom: '1.5px solid var(--line)', marginBottom: setNames.length > 0 ? 0 : 12 }}>
        <div ref={tabRef} className="no-sb" onScroll={onTabScroll} style={{ display: 'flex', gap: 2, overflowX: 'auto', scrollBehavior: 'smooth' }}>
          {['전체', ...window.GONGJONG_ORDER].map((g) => {
            const on = gj === g;
            return (
              <button key={g} onClick={() => { setGj(g); setSetFilter(''); }}
                style={{ flex: 'none', padding: '9px 15px', fontSize: 13, fontWeight: on ? 800 : 600, border: 'none', borderBottom: '2.5px solid ' + (on ? 'var(--accent)' : 'transparent'), background: 'transparent', color: on ? 'var(--accent-ink)' : 'var(--ink-2)', marginBottom: -1.5, whiteSpace: 'nowrap', cursor: 'pointer' }}>{g}</button>
            );
          })}
        </div>
        {tabSt.s && <button onClick={() => scrollTabs(-1)} title="이전" style={{ position: 'absolute', left: 0, top: 0, bottom: 1, width: 38, border: 'none', cursor: 'pointer', color: 'var(--ink-2)', fontSize: 18, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', background: 'linear-gradient(to left, transparent, var(--bg) 60%)' }}>‹</button>}
        {!tabSt.e && <button onClick={() => scrollTabs(1)} title="다음" style={{ position: 'absolute', right: 0, top: 0, bottom: 1, width: 42, border: 'none', cursor: 'pointer', color: 'var(--ink-2)', fontSize: 18, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', background: 'linear-gradient(to right, transparent, var(--bg) 60%)' }}>›</button>}
      </div>
      {/* 패키지 칩 — 선택 공종에 만들어진 패키지가 있을 때만 그 아래 표시 */}
      {setNames.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', margin: '10px 0', padding: '8px 10px', background: 'var(--surface-2)', border: '1px solid var(--line)', borderRadius: 9 }}>
          <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-3)' }}>패키지:</span>
          {setNames.map((n) => (
            <button key={n} onClick={() => setSetFilter(setFilter === n ? '' : n)}
              style={{ padding: '4px 11px', fontSize: 12, borderRadius: 20, fontWeight: 700, border: '1px solid ' + (setFilter === n ? 'var(--accent)' : 'var(--accent-line)'), background: setFilter === n ? 'var(--accent)' : 'var(--accent-soft)', color: setFilter === n ? '#fff' : 'var(--accent-ink)' }}>
              🔗 {n} ({setCount(n)})
            </button>
          ))}
          {setFilter && <button onClick={() => setSetFilter('')} style={{ fontSize: 11.5, color: 'var(--ink-3)', border: 'none', background: 'transparent', textDecoration: 'underline' }}>필터 해제</button>}
        </div>
      )}

      {(dirty || msg) && (
        <div style={{ fontSize: 12, marginBottom: 6, display: 'flex', gap: 12, alignItems: 'center' }}>
          {dirty && <span style={{ color: 'var(--warn)', fontWeight: 600 }}>● 저장 안 한 변경 있음</span>}
          {msg && <span style={{ color: msg.startsWith('저장됨') ? 'var(--good)' : 'var(--danger)', fontWeight: 600 }}>{msg}</span>}
        </div>
      )}

      <div style={{ border: '1px solid var(--line)', borderRadius: 12, overflow: 'auto', background: 'var(--surface)' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
          <thead>
            <tr>
              <th rowSpan={2} style={{ ...thG, width: 86 }}>공종</th>
              <th rowSpan={2} style={thG}>품목</th>
              <th rowSpan={2} style={{ ...thG, width: 44 }}>단위</th>
              <th rowSpan={2} style={{ ...thG, width: 96 }} title="수량을 무엇에 비례시킬지. 클릭해서 설정">수량 규칙</th>
              <th rowSpan={2} style={{ ...thG, width: 50 }} title="과거 견적서에 등장한 총 횟수">사용</th>
              <th colSpan={2} className="dv-grp-sell" style={{ ...thGrp, color: 'var(--accent-ink)', borderLeft: '1px solid var(--line-strong)' }}>판가 · 고객가</th>
              <th colSpan={2} className="dv-grp-cost" style={{ ...thGrp, borderLeft: '1px solid var(--line-strong)' }}>원가 · 내부</th>
              <th rowSpan={2} style={{ ...thG, width: 64, borderLeft: '1px solid var(--line-strong)' }} title="이익률 = 1 − 원가합/판가합">이익률</th>
              <th rowSpan={2} style={{ ...thG, width: 52 }}></th>
            </tr>
            <tr>
              <th className="zone-sell" style={{ ...thG2, width: 82, borderLeft: '1px solid var(--line-strong)' }}>자재</th>
              <th className="zone-sell" style={{ ...thG2, width: 82 }}>노무</th>
              <th className="zone-cost" style={{ ...thG2, width: 82, borderLeft: '1px solid var(--line-strong)' }}>재료</th>
              <th className="zone-cost" style={{ ...thG2, width: 82 }}>노무</th>
            </tr>
          </thead>
          <tbody>
            {shown.map((d) => (
              <tr key={d.id}>
                <td style={{ ...cell, position: 'relative' }}>
                  <button className="gj-cell" onClick={() => setGjEdit(gjEdit === d.id ? null : d.id)} title="공종 변경 (클릭)">
                    <span style={dvBadge(d.gongjong)}>{d.gongjong.replace('공사', '')}</span>
                    <span className="caret">▼</span>
                  </button>
                  {gjEdit === d.id && (<>
                    <div onClick={() => setGjEdit(null)} style={{ position: 'fixed', inset: 0, zIndex: 25 }} />
                    <div className="dv-pop">
                      {window.GONGJONG_ORDER.map((g) => (
                        <button key={g} onClick={() => { edit(d.id, { gongjong: g }); setGjEdit(null); }}>{g}</button>
                      ))}
                    </div>
                  </>)}
                </td>
                <td style={cell}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <input value={d.name} onChange={(e) => edit(d.id, { name: e.target.value })} className="cell-input" style={{ ...inp, fontWeight: 700, fontSize: 14, flex: 1, minWidth: 0 }} placeholder="품목명" />
                    <input value={d.gubun || ''} onChange={(e) => edit(d.id, { gubun: e.target.value })} className="cell-input" style={{ ...inp, fontSize: 12, color: 'var(--ink-3)', width: 84, flex: 'none' }} placeholder="구분" />
                    {d.setName && <span className="pkg-chip">🔗 {d.setName}</span>}
                  </div>
                </td>
                <td style={{ ...cell, textAlign: 'center' }}><input value={d.unit} onChange={(e) => edit(d.id, { unit: e.target.value })} className="cell-input" style={{ ...inp, textAlign: 'center' }} /></td>
                <td style={{ ...cell, textAlign: 'center' }}>
                  {(() => { const fixed = qtyRuleOf(d).base === 'fixed'; return (
                    <button onClick={() => { setQrItem(d); setQr(qtyRuleOf(d)); }} title="수량 규칙 설정 (클릭)"
                      style={{ width: '100%', padding: '5px 8px', borderRadius: 7, fontSize: 11.5, fontWeight: 700, cursor: 'pointer', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', border: '1px solid ' + (fixed ? 'var(--line-strong)' : 'var(--accent-line)'), background: fixed ? 'var(--surface)' : 'var(--accent-soft)', color: fixed ? 'var(--ink-3)' : 'var(--accent-ink)' }}>{qtyRuleLabel(d)}</button>
                  ); })()}
                </td>
                <td style={{ ...cell, textAlign: 'right', fontSize: 12, color: 'var(--ink-3)' }} className="mono">{d.count || 0}</td>
                <td style={{ ...cell, borderLeft: '1px solid var(--line)' }} className="zone-sell"><input value={d.mat ? dvFmt(d.mat.recent) : ''} onChange={(e) => editPrice(d.id, 'mat', e.target.value)} className="mono cell-input" style={inpR} placeholder="-" /></td>
                <td style={cell} className="zone-sell"><input value={d.lab ? dvFmt(d.lab.recent) : ''} onChange={(e) => editPrice(d.id, 'lab', e.target.value)} className="mono cell-input" style={inpR} placeholder="-" /></td>
                <td style={{ ...cell, borderLeft: '1px solid var(--line)' }} className="zone-cost"><input value={d.cmat ? dvFmt(d.cmat.recent) : ''} onChange={(e) => editPrice(d.id, 'cmat', e.target.value)} className="mono cell-input" style={inpR} placeholder="-" title="재료 원가" /></td>
                <td style={cell} className="zone-cost"><input value={d.clab ? dvFmt(d.clab.recent) : ''} onChange={(e) => editPrice(d.id, 'clab', e.target.value)} className="mono cell-input" style={inpR} placeholder="-" title="노무 원가" /></td>
                <td style={{ ...cell, textAlign: 'center', borderLeft: '1px solid var(--line)' }}>{(() => { const m = dvMargin(d); if (m == null) return <span style={{ color: 'var(--ink-3)' }}>—</span>; const c = m >= 40 ? 'hi' : (m >= 15 ? 'mid' : 'lo'); return <span className={'margin-chip ' + c}>{m.toFixed(0)}%</span>; })()}</td>
                <td style={{ ...cell, textAlign: 'center', whiteSpace: 'nowrap' }}>
                  <button onClick={() => setHistItem(d)} title="가격 변경 이력" style={{ width: 22, height: 22, border: '1px solid transparent', background: 'transparent', borderRadius: 6, fontSize: 12, cursor: 'pointer' }}>📈</button>
                  <button onClick={() => del(d.id)} title="삭제" style={{ width: 22, height: 22, border: '1px solid transparent', background: 'transparent', color: 'var(--ink-3)', borderRadius: 6, fontSize: 12 }}>✕</button>
                </td>
              </tr>
            ))}
            {shown.length === 0 && <tr><td colSpan={11} style={{ ...cell, textAlign: 'center', color: 'var(--ink-3)', padding: '30px' }}>결과 없음.</td></tr>}
          </tbody>
        </table>
      </div>

      {/* 페이지네이션 (10개씩) */}
      {results.length > 0 && (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, margin: '14px 0 2px' }}>
          <button onClick={() => setPage(Math.max(0, pg - 1))} disabled={pg === 0}
            style={{ width: 38, height: 38, borderRadius: 9, border: '1px solid var(--line-strong)', background: 'var(--surface)', color: pg === 0 ? 'var(--line-strong)' : 'var(--ink-2)', fontSize: 18, fontWeight: 700, cursor: pg === 0 ? 'default' : 'pointer' }}>‹</button>
          <span className="mono" style={{ fontSize: 15, color: 'var(--ink)', fontWeight: 700 }}>{pg + 1} <span style={{ color: 'var(--ink-3)', fontWeight: 500 }}>/ {pageCount}</span></span>
          <button onClick={() => setPage(Math.min(pageCount - 1, pg + 1))} disabled={pg >= pageCount - 1}
            style={{ width: 38, height: 38, borderRadius: 9, border: '1px solid var(--line-strong)', background: 'var(--surface)', color: pg >= pageCount - 1 ? 'var(--line-strong)' : 'var(--ink-2)', fontSize: 18, fontWeight: 700, cursor: pg >= pageCount - 1 ? 'default' : 'pointer' }}>›</button>
          <span style={{ fontSize: 13, color: 'var(--ink-3)', marginLeft: 4 }}>총 {results.length.toLocaleString()}개</span>
        </div>
      )}

      <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 10, fontSize: 12, color: 'var(--ink-2)' }}>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><i style={{ width: 22, height: 14, borderRadius: 4, background: 'oklch(0.985 0.012 250)', border: '1px solid var(--accent-line)' }} /> 판가 · 고객가 (견적서에 나감)</span>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><i style={{ width: 22, height: 14, borderRadius: 4, background: 'oklch(0.965 0.006 285)', border: '1px solid var(--line)' }} /> 원가 · 내부 (마진 관리용 · 고객 미노출)</span>
      </div>

      {/* 기본 포함 항목 선택 모달 */}
      {baseModal && (() => {
        const t = bmQ.trim().toLowerCase();
        let bl = all;
        if (bmGj) bl = bl.filter((d) => d.gongjong === bmGj);
        if (t) { const toks = t.split(/\s+/); bl = bl.filter((d) => { const hay = (d.gongjong + ' ' + (d.gubun || '') + ' ' + d.name).toLowerCase(); return toks.every((tok) => hay.includes(tok)); }); }
        bl = bl.slice().sort((a, b) => (bOn(b) ? 1 : 0) - (bOn(a) ? 1 : 0) || (b.count || 0) - (a.count || 0)).slice(0, 500);
        const chipBm = (on) => ({ padding: '5px 11px', fontSize: 12, borderRadius: 16, fontWeight: 700, border: '1px solid ' + (on ? 'var(--accent)' : 'var(--line-strong)'), background: on ? 'var(--accent)' : 'var(--surface)', color: on ? '#fff' : 'var(--ink-2)', cursor: 'pointer' });
        return (
          <div onClick={() => setBaseModal(false)} style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(20,30,50,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
            <div onClick={(e) => e.stopPropagation()} style={{ width: 'min(680px, 96vw)', maxHeight: '86vh', display: 'flex', flexDirection: 'column', background: 'var(--surface)', borderRadius: 14, boxShadow: '0 20px 60px rgba(0,0,0,.3)', overflow: 'hidden' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '16px 20px', borderBottom: '1px solid var(--line)' }}>
                <span style={{ fontSize: 16, fontWeight: 800 }}>⭐ 기본 포함 항목</span>
                <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>체크한 품목은 견적 생성 시 자동으로 들어갑니다 (현재 {baseCount}개)</span>
                <span style={{ flex: 1 }} />
                <button onClick={() => setBaseModal(false)} style={{ border: 'none', background: 'transparent', fontSize: 18, color: 'var(--ink-3)', cursor: 'pointer' }}>✕</button>
              </div>
              <div style={{ padding: '12px 20px', borderBottom: '1px solid var(--line)', display: 'flex', flexDirection: 'column', gap: 8 }}>
                <input value={bmQ} onChange={(e) => setBmQ(e.target.value)} autoFocus placeholder="품목·공종 검색" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5 }} />
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
                  <button onClick={() => setBmGj('')} style={chipBm(bmGj === '')}>전체</button>
                  {window.GONGJONG_ORDER.map((g) => <button key={g} onClick={() => setBmGj(bmGj === g ? '' : g)} style={chipBm(bmGj === g)}>{g.replace('공사', '')}</button>)}
                </div>
              </div>
              <div style={{ flex: 1, overflow: 'auto', padding: '6px 12px' }}>
                {bl.map((d) => { const on = bOn(d); return (
                  <label key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 8px', borderRadius: 8, cursor: 'pointer' }}
                    onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-2)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                    <input type="checkbox" checked={on} onChange={() => edit(d.id, { base: !on })} style={{ width: 16, height: 16, accentColor: 'var(--accent)' }} />
                    <span style={dvBadge(d.gongjong)}>{d.gongjong.replace('공사', '')}</span>
                    <span style={{ flex: 1, fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.name}</span>
                    <span className="mono" style={{ fontSize: 11.5, color: 'var(--ink-3)' }}>{d.count || 0}회</span>
                  </label>
                ); })}
                {bl.length === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>결과 없음</div>}
              </div>
              <div style={{ padding: '12px 20px', borderTop: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 10 }}>
                <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>변경은 상단 <b>저장</b>을 눌러야 반영됩니다.</span>
                <span style={{ flex: 1 }} />
                <button onClick={() => setBaseModal(false)} style={{ padding: '8px 16px', border: 'none', borderRadius: 8, background: 'var(--accent)', color: '#fff', fontWeight: 700, fontSize: 13 }}>완료</button>
              </div>
            </div>
          </div>
        );
      })()}

      {/* 패키지 만들기 모달 */}
      {mkSet && (
        <div onClick={cancelMkSet} style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(20,30,50,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
          <div onClick={(e) => e.stopPropagation()} style={{ width: 'min(720px, 96vw)', maxHeight: '86vh', display: 'flex', flexDirection: 'column', background: 'var(--surface)', borderRadius: 14, boxShadow: '0 20px 60px rgba(0,0,0,.3)', overflow: 'hidden' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '16px 20px', borderBottom: '1px solid var(--line)' }}>
              <span style={{ fontSize: 16, fontWeight: 800 }}>🔗 패키지 만들기</span>
              <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>① 이름 → ② 공종 선택 → ③ 품목 체크</span>
              <span style={{ flex: 1 }} />
              <button onClick={cancelMkSet} style={{ border: 'none', background: 'transparent', fontSize: 18, color: 'var(--ink-3)', cursor: 'pointer' }}>✕</button>
            </div>
            <div style={{ padding: '14px 20px', display: 'flex', flexDirection: 'column', gap: 10, borderBottom: '1px solid var(--line)' }}>
              <input value={grpName} onChange={(e) => setGrpName(e.target.value)} autoFocus placeholder="패키지명 (예: 욕실 기본형(대림))"
                style={{ padding: '10px 12px', border: '1.5px solid var(--accent)', borderRadius: 9, outline: 'none', fontSize: 14, fontWeight: 600 }} />
              <div>
                <div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--ink-3)', marginBottom: 5 }}>② 공종 선택</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
                  {window.GONGJONG_ORDER.map((g) => { const on = pkgGj === g; return (
                    <button key={g} onClick={() => { setPkgGj(on ? '' : g); setPkgQ(''); }} style={{ padding: '5px 11px', fontSize: 12, borderRadius: 16, fontWeight: 700, border: '1px solid ' + (on ? 'var(--accent)' : 'var(--line-strong)'), background: on ? 'var(--accent)' : 'var(--surface)', color: on ? '#fff' : 'var(--ink-2)', cursor: 'pointer' }}>{g.replace('공사', '')}</button>
                  ); })}
                </div>
              </div>
              <input value={pkgQ} onChange={(e) => setPkgQ(e.target.value)} placeholder="🔎 품목 검색 (보조) — 선택한 공종 안에서 좁히기"
                style={{ padding: '8px 11px', border: '1px solid var(--line-strong)', borderRadius: 9, outline: 'none', fontSize: 13 }} />
            </div>
            <div style={{ flex: 1, overflow: 'auto', padding: '6px 12px', minHeight: 160 }}>
              {pkgItems.map((d) => (
                <label key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 8px', borderRadius: 7, cursor: 'pointer', background: sel[d.id] ? 'var(--accent-soft)' : 'transparent' }}>
                  <input type="checkbox" checked={!!sel[d.id]} onChange={() => toggleSel(d.id)} style={{ accentColor: 'var(--accent)' }} />
                  <span style={window.EstEditor.gBadge(d.gongjong)}>{d.gongjong.replace('공사', '')}</span>
                  <span style={{ flex: 1, fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.name}</span>
                  {d.setName && <span style={{ fontSize: 11, color: 'var(--accent-ink)', background: 'var(--accent-soft)', borderRadius: 6, padding: '1px 6px' }}>🔗 {d.setName}</span>}
                  <span className="mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>{d.mat ? dvFmt(d.mat.recent) : ''}</span>
                </label>
              ))}
              {pkgItems.length === 0 && <div style={{ padding: 30, textAlign: 'center', color: 'var(--ink-3)', fontSize: 13, lineHeight: 1.7 }}>{pkgGj ? '이 공종에 품목이 없습니다' : (selCount ? '다른 공종을 선택해 더 담거나 저장하세요.' : '위에서 ② 공종을 먼저 선택하세요.\n그 공종 품목이 여기 나오면 체크해서 담습니다.')}</div>}
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '14px 20px', borderTop: '1px solid var(--line)' }}>
              <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--accent-ink)' }}>선택 {selCount}개</span>
              {selCount > 0 && <button onClick={() => setSel({})} style={{ fontSize: 12, color: 'var(--ink-3)', border: 'none', background: 'transparent', textDecoration: 'underline', cursor: 'pointer' }}>선택 해제</button>}
              <span style={{ flex: 1 }} />
              <button onClick={cancelMkSet} style={{ padding: '9px 16px', border: '1px solid var(--line-strong)', background: 'var(--surface)', borderRadius: 9, fontSize: 13.5, color: 'var(--ink-2)', cursor: 'pointer' }}>취소</button>
              <button onClick={groupSelected} disabled={!grpName.trim() || selCount === 0}
                style={{ padding: '9px 20px', border: 'none', borderRadius: 9, fontWeight: 800, fontSize: 13.5, color: '#fff', cursor: (grpName.trim() && selCount > 0) ? 'pointer' : 'default', background: (grpName.trim() && selCount > 0) ? 'var(--accent)' : 'var(--line-strong)' }}>패키지 저장</button>
            </div>
          </div>
        </div>
      )}

      {/* 새 품목 모달 */}
      {showNew && (
        <div onClick={() => setShowNew(false)} style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(20,30,50,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
          <div onClick={(e) => e.stopPropagation()} style={{ width: 'min(560px, 96vw)', background: 'var(--surface)', borderRadius: 14, boxShadow: '0 20px 60px rgba(0,0,0,.3)', overflow: 'hidden' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '16px 20px', borderBottom: '1px solid var(--line)' }}>
              <span style={{ fontSize: 16, fontWeight: 800 }}>＋ 새 품목</span>
              <span style={{ fontSize: 12, color: 'var(--ink-3)' }}>단가 사전에 새 품목을 추가합니다.</span>
              <span style={{ flex: 1 }} />
              <button onClick={() => setShowNew(false)} style={{ border: 'none', background: 'transparent', fontSize: 18, color: 'var(--ink-3)', cursor: 'pointer' }}>✕</button>
            </div>
            <div style={{ padding: '18px 20px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>공종</span>
                <select value={ni.gongjong} onChange={(e) => setN('gongjong', e.target.value)} style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5 }}>
                  {window.GONGJONG_ORDER.map((g) => <option key={g} value={g}>{g}</option>)}
                </select>
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>구분</span>
                <input value={ni.gubun} onChange={(e) => setN('gubun', e.target.value)} placeholder="예: 철거, 위생도기" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5 }} />
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4, gridColumn: '1 / -1' }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>품목명 *</span>
                <input value={ni.name} autoFocus onChange={(e) => setN('name', e.target.value)} placeholder="품목명" style={{ padding: '9px 11px', border: '1.5px solid var(--accent)', borderRadius: 8, outline: 'none', fontSize: 14, fontWeight: 600 }} />
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>단위</span>
                <input value={ni.unit} onChange={(e) => setN('unit', e.target.value)} placeholder="EA" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5, textAlign: 'center' }} />
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>패키지(선택)</span>
                <select value={ni.setName} onChange={(e) => setN('setName', e.target.value)} style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5, background: 'var(--surface)' }}>
                  <option value="">없음</option>
                  {allSetNames.map((n) => <option key={n} value={n}>{n}</option>)}
                </select>
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>자재단가 <span style={{ color: 'var(--accent-ink)' }}>(판가)</span></span>
                <input value={ni.mat} onChange={(e) => setN('mat', e.target.value)} inputMode="numeric" placeholder="0" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5, textAlign: 'right', fontFamily: 'var(--mono)' }} />
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>노무단가 <span style={{ color: 'var(--accent-ink)' }}>(판가)</span></span>
                <input value={ni.lab} onChange={(e) => setN('lab', e.target.value)} inputMode="numeric" placeholder="0" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5, textAlign: 'right', fontFamily: 'var(--mono)' }} />
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>재료 원가</span>
                <input value={ni.cmat} onChange={(e) => setN('cmat', e.target.value)} inputMode="numeric" placeholder="0" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5, textAlign: 'right', fontFamily: 'var(--mono)' }} />
              </label>
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>노무 원가</span>
                <input value={ni.clab} onChange={(e) => setN('clab', e.target.value)} inputMode="numeric" placeholder="0" style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 13.5, textAlign: 'right', fontFamily: 'var(--mono)' }} />
              </label>
            </div>
            <div style={{ display: 'flex', gap: 10, padding: '14px 20px', borderTop: '1px solid var(--line)', justifyContent: 'flex-end' }}>
              <button onClick={() => setShowNew(false)} style={{ padding: '9px 16px', border: '1px solid var(--line-strong)', background: 'var(--surface)', borderRadius: 9, fontSize: 13.5, color: 'var(--ink-2)', cursor: 'pointer' }}>취소</button>
              <button onClick={saveNew} disabled={!ni.name.trim()} style={{ padding: '9px 20px', border: 'none', borderRadius: 9, fontWeight: 800, fontSize: 13.5, color: '#fff', cursor: ni.name.trim() ? 'pointer' : 'default', background: ni.name.trim() ? 'var(--accent)' : 'var(--line-strong)' }}>추가</button>
            </div>
          </div>
        </div>
      )}

      {/* 가격 변경 이력 모달 */}
      {histItem && (
        <div onClick={() => setHistItem(null)} style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(20,30,50,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
          <div onClick={(e) => e.stopPropagation()} style={{ width: 'min(560px, 96vw)', maxHeight: '80vh', display: 'flex', flexDirection: 'column', background: 'var(--surface)', borderRadius: 14, boxShadow: '0 20px 60px rgba(0,0,0,.3)', overflow: 'hidden' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '16px 20px', borderBottom: '1px solid var(--line)' }}>
              <span style={{ fontSize: 15, fontWeight: 800 }}>📈 가격 변경 이력</span>
              <span style={{ fontSize: 12.5, color: 'var(--ink-3)', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{histItem.name}</span>
              <button onClick={() => setHistItem(null)} style={{ border: 'none', background: 'transparent', fontSize: 18, color: 'var(--ink-3)', cursor: 'pointer' }}>✕</button>
            </div>
            <div style={{ flex: 1, overflow: 'auto', padding: '12px 20px' }}>
              {(histItem.mat || histItem.lab) && (
                <div style={{ marginBottom: 14, padding: '10px 12px', background: 'var(--surface-2)', border: '1px solid var(--line)', borderRadius: 9 }}>
                  <div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--ink-3)', marginBottom: 6 }}>과거 견적 분포 (작성 시 추출값 · 판가 기준)</div>
                  {['mat', 'lab'].map((k) => { const p = histItem[k]; if (!p) return null; return (
                    <div key={k} style={{ fontSize: 12.5, display: 'flex', gap: 10, padding: '2px 0' }}>
                      <span style={{ width: 30, color: 'var(--ink-3)' }}>{k === 'mat' ? '자재' : '노무'}</span>
                      <span className="mono">중앙 {dvFmt(p.median)} · 최소 {dvFmt(p.min)} · 최대 {dvFmt(p.max)} · {p.n || 0}회</span>
                    </div>
                  ); })}
                </div>
              )}
              {(!histItem.history || histItem.history.length === 0) ? (
                <div style={{ padding: '24px 0', textAlign: 'center', color: 'var(--ink-3)', fontSize: 13, lineHeight: 1.7 }}>아직 변경 이력이 없습니다.<br />단가를 수정한 뒤 <b>사전 저장</b>을 누르면 이때부터 기록됩니다.</div>
              ) : (
                <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5 }}>
                  <thead><tr>
                    {['변경 시점', '자재단가', '노무단가', '증감(자재/노무)'].map((h, i) => <th key={h} style={{ textAlign: i === 0 ? 'left' : 'right', padding: '7px 8px', borderBottom: '1.5px solid var(--line-strong)', color: 'var(--ink-3)', fontSize: 11, fontWeight: 700 }}>{h}</th>)}
                  </tr></thead>
                  <tbody>
                    {histItem.history.map((h, i) => {
                      const pv = histItem.history[i - 1];
                      const d1 = pv && pv.mat && h.mat != null ? ((h.mat - pv.mat) / pv.mat) * 100 : null;
                      const d2 = pv && pv.lab && h.lab != null ? ((h.lab - pv.lab) / pv.lab) * 100 : null;
                      const dchip = (v) => v == null ? '—' : (v === 0 ? '0%' : (v > 0 ? '▲' : '▼') + Math.abs(v).toFixed(1) + '%');
                      const dcol = (v) => v == null || v === 0 ? 'var(--ink-3)' : (v > 0 ? 'var(--danger)' : 'var(--good)');
                      return (
                        <tr key={i}>
                          <td style={{ padding: '7px 8px', borderBottom: '1px solid var(--line)' }}>{h.d}{i === 0 ? ' (최초)' : ''}</td>
                          <td className="mono" style={{ padding: '7px 8px', borderBottom: '1px solid var(--line)', textAlign: 'right' }}>{h.mat != null ? dvFmt(h.mat) : '-'}</td>
                          <td className="mono" style={{ padding: '7px 8px', borderBottom: '1px solid var(--line)', textAlign: 'right' }}>{h.lab != null ? dvFmt(h.lab) : '-'}</td>
                          <td style={{ padding: '7px 8px', borderBottom: '1px solid var(--line)', textAlign: 'right', fontSize: 11.5, fontWeight: 700 }}>
                            <span style={{ color: dcol(d1) }}>{dchip(d1)}</span> <span style={{ color: 'var(--line-strong)' }}>/</span> <span style={{ color: dcol(d2) }}>{dchip(d2)}</span>
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              )}
            </div>
          </div>
        </div>
      )}
      {/* 수량 규칙 모달 */}
      {qrItem && (() => {
        const sample = { py: 33, area: Math.round(33 * 3.3 * 10) / 10, baths: 2, rooms: 3, verandas: 1, bw: 1950, bh: 2645, lw: 1800, lh: 2440, km: 3 };
        const isFixed = qr.base === 'fixed';
        const isExpr = qr.base === 'expr';
        const preview = qtyEval({ ...qr, factor: Number(qr.factor) || 0 }, sample);
        const saveQr = () => { edit(qrItem.id, { qtyRule: { base: qr.base, factor: isFixed ? 1 : (Number(qr.factor) || 1), roundup: !!qr.roundup, expr: isExpr ? (qr.expr || '') : '' }, byArea: null }); setMsg(`'${qrItem.name}' 수량 규칙 변경 — 사전 저장을 누르세요`); setQrItem(null); };
        return (
          <div onClick={() => setQrItem(null)} style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(20,30,50,.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
            <div onClick={(e) => e.stopPropagation()} style={{ width: 'min(480px, 96vw)', background: 'var(--surface)', borderRadius: 14, boxShadow: '0 20px 60px rgba(0,0,0,.3)', overflow: 'hidden' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '16px 20px', borderBottom: '1px solid var(--line)' }}>
                <span style={{ fontSize: 15, fontWeight: 800 }}>📐 수량 규칙</span>
                <span style={{ fontSize: 12.5, color: 'var(--ink-3)', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{qrItem.name}</span>
                <button onClick={() => setQrItem(null)} style={{ border: 'none', background: 'transparent', fontSize: 18, color: 'var(--ink-3)', cursor: 'pointer' }}>✕</button>
              </div>
              <div style={{ padding: '18px 20px', display: 'flex', flexDirection: 'column', gap: 14 }}>
                <label style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
                  <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-3)' }}>수량 기준 — 이 품목 수량을 무엇에 비례시킬까요?</span>
                  <select value={qr.base} onChange={(e) => setQr((q) => ({ ...q, base: e.target.value }))} style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, fontSize: 13.5, background: '#fff' }}>
                    {QR_BASES.map((b) => <option key={b.k} value={b.k}>{b.label}</option>)}
                  </select>
                </label>
                {isExpr && (
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-3)' }}>수식 — 변수로 직접 계산 (조합·치수 등)</span>
                    <input value={qr.expr || ''} onChange={(e) => setQr((q) => ({ ...q, expr: e.target.value }))} placeholder="예: 방+욕실+1"
                      style={{ padding: '9px 11px', border: '1px solid var(--accent)', borderRadius: 8, fontSize: 13, fontFamily: 'var(--mono)', outline: 'none' }} />
                    <div style={{ fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.7 }}>변수: <b>평형 면적 방 욕실 베란다 침실욕실가로 침실욕실세로 거실욕실가로 거실욕실세로 주방길이</b> (치수=mm) · 함수: 올림() 내림()<br />예) 문틀철거=<b>방+욕실+1</b> · 거실욕실벽타일=<b>올림((거실욕실가로+거실욕실세로)*2*2.2/0.36*1.2)</b></div>
                    <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13 }}><input type="checkbox" checked={!!qr.roundup} onChange={(e) => setQr((q) => ({ ...q, roundup: e.target.checked }))} style={{ accentColor: 'var(--accent)' }} />결과 올림</label>
                  </div>
                )}
                {!isFixed && !isExpr && (
                  <div style={{ display: 'flex', gap: 14, alignItems: 'flex-end' }}>
                    <label style={{ display: 'flex', flexDirection: 'column', gap: 5, flex: 1 }}>
                      <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-3)' }}>계수 (× 곱할 값)</span>
                      <input value={qr.factor} onChange={(e) => setQr((q) => ({ ...q, factor: e.target.value }))} inputMode="decimal" placeholder="예: 0.9, 1.3, 1"
                        style={{ padding: '9px 11px', border: '1px solid var(--line-strong)', borderRadius: 8, fontSize: 13.5, textAlign: 'right', fontFamily: 'var(--mono)' }} />
                    </label>
                    <label style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '9px 4px', fontSize: 13 }}>
                      <input type="checkbox" checked={!!qr.roundup} onChange={(e) => setQr((q) => ({ ...q, roundup: e.target.checked }))} style={{ accentColor: 'var(--accent)' }} />소수점 올림
                    </label>
                  </div>
                )}
                <div style={{ background: 'var(--accent-soft)', border: '1px solid var(--accent-line)', borderRadius: 9, padding: '11px 13px', fontSize: 12.5, color: 'var(--accent-ink)', lineHeight: 1.6 }}>
                  {isFixed
                    ? '고정 — 체크리스트에서 입력한 수량(기본 1)을 그대로 사용합니다.'
                    : <span>미리보기 (평형33·면적108.9·욕실2·방3·베란다1·치수 예시 기준)<br /><b style={{ fontSize: 16 }}>→ 수량 {preview == null ? (isExpr ? '수식 확인 필요' : '-') : preview}</b></span>}
                </div>
              </div>
              <div style={{ display: 'flex', gap: 10, padding: '14px 20px', borderTop: '1px solid var(--line)', justifyContent: 'flex-end' }}>
                <button onClick={() => setQrItem(null)} style={{ padding: '9px 16px', border: '1px solid var(--line-strong)', background: 'var(--surface)', borderRadius: 9, fontSize: 13.5, color: 'var(--ink-2)', cursor: 'pointer' }}>취소</button>
                <button onClick={saveQr} style={{ padding: '9px 20px', border: 'none', borderRadius: 9, fontWeight: 800, fontSize: 13.5, color: '#fff', background: 'var(--accent)', cursor: 'pointer' }}>적용</button>
              </div>
            </div>
          </div>
        );
      })()}
    </div>
  );
}

window.EstDictViewer = { DictViewer };
