// 메인 앱
// 각 .jsx 는 Babel-standalone 이 전역 스코프 <script> 로 실행하므로, 여기서 destructure 한
// 이름(Field·CoverPage·tbBtn 등)이 다른 파일의 동명 함수와 충돌한다. IIFE 로 스코프를 가둔다.
(function () {
const { Field, ItemSearch, GroupSection } = window.EstEditor;
const { CoverPage, SummaryPage, DetailPage } = window.EstOutput;
const { SummarySidebar, PreviewArea, tbBtn, toastS } = window.EstSummary;
const { AIPanel } = window.EstAI;
const { DictViewer } = window.EstDictViewer;
const { ChecklistPage } = window.EstChecklist;
const { CheckMapEditor } = window.EstCheckMap;
const C = window.EstCalc;

const ORDER = window.GONGJONG_ORDER;
const todayStr = () => new Date().toISOString().slice(0, 10);
// 견적 생성 시 자동 포함되는 "기본 포함" 항목.
// 관리자가 단가 사전에서 켜고 끌 수 있음(d.base). 아직 설정 안 한 품목은 과거 70%+ 등장(basePct)을 기본값으로.
const BASE_PCT = 70;
const baseOn = (d) => (d.base !== undefined ? !!d.base : (d.basePct || 0) >= BASE_PCT);
const getBaseItems = () => window.DICT.filter(baseOn);
window.baseOn = baseOn;

// 견적 저장/리스트 (로컬 보관)
const SAVED_KEY = 'est:saved';
const readSaved = () => { try { return JSON.parse(localStorage.getItem(SAVED_KEY) || '[]'); } catch (e) { return []; } };
const writeSaved = (arr) => localStorage.setItem(SAVED_KEY, JSON.stringify(arr));
const savedLabel = (e) => `${e.site && e.site.name ? e.site.name : '무제'}${e.site && e.site.client ? ' · ' + e.site.client : ''} · ${e.site ? e.site.date : ''}`;

function blankEst() {
  return {
    site: { name: '', addr: '', py: '', client: '', manager: '', date: todayStr(), reqDate: todayStr(), validDate: '', valid: '견적일로부터 2주' },
    company: JSON.parse(JSON.stringify(window.COMPANY_DEFAULT)),
    remarks: window.REMARKS_DEFAULT,
    groups: [],
    purchases: [],   // 물품 구매 대행 (가전/가구 등) — 견적 총액 미포함, 갑지 안내만
    formula: JSON.parse(JSON.stringify(window.FORMULA)),
    negotiated: '',
  };
}

function loadCurrent() {
  try {
    const s = localStorage.getItem('est:current');
    if (s) {
      const e = JSON.parse(s);
      // 구버전 호환: 누락 필드 보강
      if (!e.company) e.company = JSON.parse(JSON.stringify(window.COMPANY_DEFAULT));
      if (e.remarks == null) e.remarks = window.REMARKS_DEFAULT;
      if (!e.formula) e.formula = JSON.parse(JSON.stringify(window.FORMULA));
      if (e.formula.profitRate == null) e.formula.profitRate = window.FORMULA.profitRate;
      if (!e.site.reqDate) e.site.reqDate = e.site.date;
      if (!e.purchases) e.purchases = [];
      return e;
    }
  } catch (err) {}
  return blankEst();
}

function App() {
  const [est, setEst] = useState(loadCurrent);
  const [mode, setMode] = useState('home'); // home | checklist | edit | preview | dict
  const [showSettings, setShowSettings] = useState(false);
  const [toast, setToast] = useState('');
  const BASE_ITEMS = useMemo(() => getBaseItems(), []);
  const dictCtlRef = useRef({});            // 단가 사전 저장 제어 (DictViewer가 등록)
  const [dictDirty, setDictDirty] = useState(false);

  useEffect(() => { localStorage.setItem('est:current', JSON.stringify(est)); }, [est]);

  const calc = useMemo(() => C.compute(est), [est]);
  const flash = (m) => { setToast(m); setTimeout(() => setToast(''), 1800); };

  /* ---- 데이터 조작 ---- */
  // 수량 규칙(qtyRule)로 자동 산출 (추가 시점). 현장정보(평형/욕실/방/베란다) 기준, 규칙 없으면 1
  const autoQty = (d) => {
    const py = parseFloat(String(est.site.py || '').replace(/[^\d.]/g, '')) || 0;
    const ctx = { py, area: py * 3.3, baths: parseInt(est.site.baths, 10) || 0, rooms: parseInt(est.site.rooms, 10) || 0, verandas: parseInt(est.site.verandas, 10) || 0, bw: Number(est.site.bw) || 0, bh: Number(est.site.bh) || 0, lw: Number(est.site.lw) || 0, lh: Number(est.site.lh) || 0, km: Number(est.site.km) || 0 };
    const q = window.qtyEval(window.qtyRuleOf(d), ctx);
    return q == null ? 1 : q;
  };
  const patchSite = (k, v) => setEst((e) => ({ ...e, site: { ...e.site, [k]: v } }));

  const addItem = (d, basis) => {
    setEst((e) => {
      const groups = [...e.groups];
      let gi = groups.findIndex((g) => g.gongjong === d.gongjong);
      if (gi < 0) { groups.push({ gongjong: d.gongjong, rows: [] }); gi = groups.length - 1; }
      else groups[gi] = { ...groups[gi], rows: [...groups[gi].rows] };
      const pick = (s) => s ? (s[basis] != null ? s[basis] : s.recent) : '';
      groups[gi].rows.push({ name: d.name, unit: d.unit, qty: autoQty(d), mat: d.mat ? pick(d.mat) : '', lab: d.lab ? pick(d.lab) : '', gubun: d.gubun || '', note: '' });
      groups.sort((a, b) => ORDER.indexOf(a.gongjong) - ORDER.indexOf(b.gongjong));
      return { ...e, groups };
    });
    flash(`${d.name} 추가됨`);
  };

  // AI 추출 등 여러 행 일괄 추가 (행마다 공종·수량·단가 지정)
  const addRows = (rows) => {
    setEst((e) => {
      const groups = e.groups.map((g) => ({ ...g, rows: [...g.rows] }));
      rows.forEach((r) => {
        let gi = groups.findIndex((g) => g.gongjong === r.gongjong);
        if (gi < 0) { groups.push({ gongjong: r.gongjong, rows: [] }); gi = groups.length - 1; }
        groups[gi].rows.push({
          name: r.name, unit: r.unit || 'EA', qty: r.qty || 1,
          mat: r.mat != null ? r.mat : '', lab: r.lab != null ? r.lab : '',
          gubun: r.gubun || '', note: r.note || '',
        });
      });
      groups.sort((a, b) => ORDER.indexOf(a.gongjong) - ORDER.indexOf(b.gongjong));
      return { ...e, groups };
    });
  };


  // 홈: 새 체크리스트 시작 (고객 정보 → 체크리스트 헤더로 시드, 항목 초기화)
  const startNewChecklist = (info) => {
    const header = { client: info.client || '', date: info.date || todayStr(), name: info.name || '', py: info.py || '', budget: info.budget || '' };
    localStorage.setItem('chk:state', JSON.stringify({ header, items: {} }));
    setMode('checklist');
  };
  const loadSavedEst = (s) => { setEst(JSON.parse(JSON.stringify(s.est))); setMode('preview'); flash('견적 불러옴 — 수정하려면 ✏ 수정'); };

  // 체크리스트 선택 내역 → 단가사전 매칭 → 편집 화면에 견적 생성
  const generateFromChecklist = ({ header, selected, basics, aiRows }) => {
    const py = parseFloat(String(header.py || '').replace(/[^\d.]/g, '')) || 0;
    // 체크리스트 카테고리 → 단가사전 공종 (그 공종 안에서 우선 매칭 → 철거/배선 오매칭 방지)
    const CAT_G = { admin: '공통공사', window: '샷시공사', aircon: '에어컨공사', demo: '철거공사', plumb: '설비공사', carpentry: '목공사', electric: '전기공사', floor: '바닥공사', tile: '타일공사', wall: '도배공사', film: '필름공사', balcony: '기타공사', middoor: '기타공사', bath: '욕실공사', furniture: '가구공사' };
    const norm = (s) => String(s || '').replace(/[\s_()/]/g, '').toLowerCase();
    const matchCL = (label, catId) => {
      const g = CAT_G[catId]; const n = norm(label);
      const inc = (d) => { const dn = norm(d.name); return dn.includes(n) || n.includes(dn); };
      let hit = window.DICT.filter((d) => (!g || d.gongjong === g) && inc(d)).sort((a, b) => (b.count || 0) - (a.count || 0));
      if (hit[0]) return hit[0];
      hit = window.DICT.filter(inc).sort((a, b) => (b.count || 0) - (a.count || 0));
      return hit[0] || null;
    };
    const groups = [];
    const pushRow = (gongjong, row) => {
      let gi = groups.findIndex((g) => g.gongjong === gongjong);
      if (gi < 0) { groups.push({ gongjong, rows: [] }); gi = groups.length - 1; }
      if (groups[gi].rows.some((r) => r.name === row.name)) return;   // 같은 품목 중복 방지
      groups[gi].rows.push(row);
    };
    const minOf = (d) => ({ name: d.name, gongjong: d.gongjong, unit: d.unit, mat: d.mat ? d.mat.recent : '', lab: d.lab ? d.lab.recent : '' });
    // 수량 규칙(qtyRule)로 산출: 평형/면적/욕실수/방수/베란다수 × 계수. 규칙 없으면(고정) fb 사용
    const B = basics || {};
    const qctx = { py, area: py * 3.3, baths: Number(B.baths) || 0, rooms: Number(B.rooms) || 0, verandas: Number(B.verandas) || 0, bw: Number(B.bw) || 0, bh: Number(B.bh) || 0, lw: Number(B.lw) || 0, lh: Number(B.lh) || 0, km: Number(B.km) || 0 };
    const aq = (d, fb) => { const q = window.qtyEval(window.qtyRuleOf(d), qctx); return (q == null) ? fb : q; };
    // 공통 기본 항목 자동 포함 (basePct≥70 — 폐기물·동선보양·자재양중·기본인건비 등). 안 맞는 건 편집에서 삭제
    BASE_ITEMS.forEach((d) => pushRow(d.gongjong, { name: d.name, unit: d.unit, qty: aq(d, 1), mat: d.mat ? d.mat.recent : '', lab: d.lab ? d.lab.recent : '', gubun: d.gubun || '', note: '' }));
    selected.forEach((sel) => {
      const mp = window.CHK_MAP && window.CHK_MAP[sel.id];   // 견적사 매핑(품목 후보 / 세트) 우선
      const mlist = Array.isArray(mp) ? mp : (mp ? [mp] : []);
      const note = [sel.opt, sel.memo].filter(Boolean).join(' · ');
      const qty = Number(sel.qty) || 1;
      let added = false;
      // 세트 연결 → 세트 멤버 전부를 각각 행으로
      mlist.filter((x) => x.set).forEach((se) => {
        window.DICT.filter((d) => (d.setName || '').trim() === se.set).forEach((d) => {
          pushRow(d.gongjong, { name: d.name, unit: d.unit, qty: aq(d, qty), mat: d.mat ? d.mat.recent : '', lab: d.lab ? d.lab.recent : '', gubun: d.gubun || '', note: (note ? note + ' · ' : '') + '패키지:' + se.set, chk: sel.label });
          added = true;
        });
      });
      // 개별 품목 후보 → 한 행(기본=첫째) + 교체 후보
      let cands = mlist.filter((x) => x.name).map((x) => window.DICT.find((d) => d.gongjong === x.gongjong && d.name === x.name)).filter(Boolean);
      if (cands.length === 0 && !added) { const a = matchCL(sel.label, sel.catId); if (a) cands = [a]; }   // 매핑 없으면 자동
      if (cands.length) {
        const m = cands[0];
        pushRow(m.gongjong, { name: m.name, unit: m.unit || sel.unit, qty: aq(m, qty), mat: m.mat ? m.mat.recent : '', lab: m.lab ? m.lab.recent : '', gubun: m.gubun || '', note, chk: sel.label, cands: cands.map(minOf) });
        added = true;
      }
      if (!added) {
        pushRow(CAT_G[sel.catId] || sel.catName, { name: sel.label, unit: sel.unit, qty, mat: '', lab: '', gubun: '', note: (note ? note + ' · ' : '') + '사전에 없음(단가 입력)', chk: sel.label });
      }
    });
    // 욕실 패키지: 선택한 패키지 멤버 전부를 개소 수량만큼 투입
    (B.pkgs || []).forEach((p) => {
      const n = Number(p.qty) || 0; if (!p.name || n <= 0) return;
      window.DICT.filter((d) => (d.setName || '').trim() === p.name).forEach((d) => {
        pushRow(d.gongjong, { name: d.name, unit: d.unit, qty: aq(d, n), mat: d.mat ? d.mat.recent : '', lab: d.lab ? d.lab.recent : '', gubun: d.gubun || '', note: '패키지:' + p.name, chk: p.name });
      });
    });
    // AI 상담 추출 항목 포함
    (aiRows || []).forEach((r) => pushRow(r.gongjong || (CAT_G[r.catId] || '기타공사'), { name: r.name, unit: r.unit || 'EA', qty: Number(r.qty) || 1, mat: r.mat != null ? r.mat : '', lab: r.lab != null ? r.lab : '', gubun: r.gubun || '', note: r.note || '', chk: r.name }));
    groups.sort((a, b) => ORDER.indexOf(a.gongjong) - ORDER.indexOf(b.gongjong));
    setEst((e) => ({
      ...e,
      site: { ...e.site, client: header.client || e.site.client, name: header.name || e.site.name, py: String(py || header.py || e.site.py || ''), date: header.date || e.site.date, rooms: B.rooms || e.site.rooms, baths: B.baths || e.site.baths, verandas: B.verandas || e.site.verandas, bw: B.bw || e.site.bw, bh: B.bh || e.site.bh, lw: B.lw || e.site.lw, lh: B.lh || e.site.lh, km: B.km || e.site.km },
      groups,
    }));
    setMode('edit');
    flash(`체크리스트 ${selected.length}개 → 단가사전 매칭 견적 생성`);
  };

  const addAllOf = (gongjong) => {
    const items = window.DICT.filter((d) => d.gongjong === gongjong).sort((a, b) => b.count - a.count);
    setEst((e) => {
      const groups = e.groups.map((g) => g.gongjong === gongjong
        ? { ...g, rows: items.map((d) => ({ name: d.name, unit: d.unit, qty: autoQty(d), mat: d.mat ? d.mat.recent : '', lab: d.lab ? d.lab.recent : '', gubun: d.gubun || '', note: '' })) }
        : g);
      return { ...e, groups };
    });
    flash(`${gongjong} ${items.length}개 품목 불러옴`);
  };

  const delGroup = (gongjong) => setEst((e) => ({ ...e, groups: e.groups.filter((g) => g.gongjong !== gongjong) }));
  const patchRow = (gongjong, i, patch) => setEst((e) => ({
    ...e, groups: e.groups.map((g) => g.gongjong !== gongjong ? g : { ...g, rows: g.rows.map((r, j) => j === i ? { ...r, ...patch } : r) }),
  }));
  const delRow = (gongjong, i) => setEst((e) => ({
    ...e, groups: e.groups.map((g) => g.gongjong !== gongjong ? g : { ...g, rows: g.rows.filter((_, j) => j !== i) }),
  }));

  // 물품 구매 대행 (가전/가구 등) — 견적 총액과 별개
  const addPurchase = () => setEst((e) => ({ ...e, purchases: [...(e.purchases || []), { name: '', qty: 1, price: '', note: '' }] }));
  const patchPurchase = (i, patch) => setEst((e) => ({ ...e, purchases: e.purchases.map((p, j) => (j === i ? { ...p, ...patch } : p)) }));
  const delPurchase = (i) => setEst((e) => ({ ...e, purchases: e.purchases.filter((_, j) => j !== i) }));

  const newEst = () => { if (confirm('현재 견적을 비우고 새로 시작할까요?')) { setEst(blankEst()); flash('새 견적 시작'); } };
  // 미리보기 "최종 저장" — 보관함(est:saved)에 시점 스냅샷 적재
  const finalSave = () => {
    const arr = readSaved();
    const snap = {
      id: 'q' + new Date().getTime(),
      label: savedLabel(est),
      savedAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
      amount: window.EstCalc.compute(est).supply,
      status: 'final',
      est: JSON.parse(JSON.stringify(est)),
    };
    const i = arr.findIndex((x) => x.label === snap.label);
    if (i >= 0) arr[i] = snap; else arr.unshift(snap);
    writeSaved(arr);
    flash('보관함에 최종 저장됨');
  };


  return (
    <div style={{ minHeight: '100vh' }}>
      <TopBar mode={mode} setMode={setMode} est={est} flash={flash} setEst={setEst} calc={calc}
        hasEst={est.groups.length > 0}
        onNew={newEst}
        onOpenArchive={() => setMode('archive')}
        onFinalSave={finalSave}
        dictDirty={dictDirty} onDictSave={() => dictCtlRef.current.save && dictCtlRef.current.save()} />
      {toast && <div className="no-print" style={toastS}>{toast}</div>}

      {mode === 'home' ? (
        <HomeScreen onStart={startNewChecklist} />
      ) : mode === 'archive' ? (
        <ArchivePage onOpen={loadSavedEst} onNew={() => setMode('home')} flash={flash} />
      ) : mode === 'checklist' ? (
        <ChecklistPage onGenerate={generateFromChecklist} flash={flash} />
      ) : mode === 'edit' ? (
        <div style={{ display: 'flex', gap: 18, alignItems: 'flex-start', maxWidth: 1320, margin: '0 auto', padding: '20px 24px 80px' }}>
          {/* 본문 */}
          <div style={{ flex: 1, minWidth: 0 }}>
            <SiteCard est={est} patchSite={patchSite} />
            <CompanyCard est={est} setEst={setEst} />
            <div style={{ margin: '18px 0 10px' }}>
              <ItemSearch onAdd={addItem} />
            </div>
            <DirectAdd onAdd={addRows} flash={flash} />

            {est.groups.length === 0 && (
              <div style={{ textAlign: 'center', padding: '50px 20px', color: 'var(--ink-3)', border: '1.5px dashed var(--line-strong)', borderRadius: 14 }}>
                <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--ink-2)', marginBottom: 6 }}>위 검색창에서 품목을 추가하세요 (없는 건 ＋ 직접 항목)</div>
                <div style={{ fontSize: 13 }}>과거 시공단가가 자동으로 채워집니다 — 수량만 조정하면 됩니다.</div>
              </div>
            )}

            {calc.groups.map((cg) => {
              const g = est.groups.find((x) => x.gongjong === cg.gongjong);
              return (
                <GroupSection key={cg.gongjong} group={g} calcGroup={cg}
                  onRow={(i, patch) => patchRow(cg.gongjong, i, patch)}
                  onDelRow={(i) => delRow(cg.gongjong, i)}
                  onAddAll={() => addAllOf(cg.gongjong)}
                  onDelGroup={() => delGroup(cg.gongjong)} />
              );
            })}

            <PurchaseCard est={est} add={addPurchase} patch={patchPurchase} del={delPurchase} />
          </div>

          {/* 요약 사이드바 */}
          <SummarySidebar est={est} calc={calc} setEst={setEst} showSettings={showSettings} setShowSettings={setShowSettings} />
        </div>
      ) : mode === 'dict' ? (
        <DictViewer ctlRef={dictCtlRef} onDirty={setDictDirty} />
      ) : mode === 'map' ? (
        <CheckMapEditor />
      ) : (
        <PreviewArea est={est} calc={calc} />
      )}
      {mode === 'preview' && <PreviewActions onFinal={finalSave} onBack={() => setMode('edit')} est={est} calc={calc} />}
      {mode === 'edit' && (
        <div className="no-print" style={{ position: 'fixed', right: 24, bottom: 24, zIndex: 55, display: 'flex', gap: 8 }}>
          <button onClick={() => setMode('checklist')} style={{ padding: '14px 18px', border: '1px solid var(--line-strong)', borderRadius: 12, background: 'var(--surface)', color: 'var(--ink-2)', fontWeight: 700, fontSize: 14, boxShadow: 'var(--shadow-lg)', cursor: 'pointer' }}>← 체크리스트</button>
          <button onClick={() => setMode('preview')} style={{ padding: '14px 26px', border: 'none', borderRadius: 12, background: 'var(--accent)', color: '#fff', fontWeight: 800, fontSize: 15, boxShadow: 'var(--shadow-lg)', cursor: 'pointer' }}>미리보기 →</button>
        </div>
      )}
    </div>
  );
}

/* ---------------- 미리보기 출력 액션 (우측 하단 플로팅) ---------------- */
function PreviewActions({ onFinal, onBack, est, calc }) {
  return (
    <div className="no-print" style={{ position: 'fixed', right: 24, bottom: 24, zIndex: 55, display: 'flex', gap: 8, alignItems: 'center', background: 'oklch(1 0 0 / 0.94)', backdropFilter: 'blur(8px)', border: '1px solid var(--line)', borderRadius: 12, padding: 8, boxShadow: 'var(--shadow-lg)' }}>
      <button onClick={onBack} style={{ padding: '11px 15px', border: '1px solid var(--line-strong)', borderRadius: 9, background: 'var(--surface)', color: 'var(--ink-2)', fontWeight: 700, fontSize: 13 }}>✏ 수정</button>
      <button onClick={onFinal} style={{ padding: '11px 18px', border: 'none', borderRadius: 9, background: 'var(--accent)', color: '#fff', fontWeight: 800, fontSize: 13.5 }}>✓ 최종 저장</button>
      <button onClick={() => window.exportExcel(est, calc)} style={{ ...tbBtn, padding: '11px 15px' }}>엑셀</button>
      <button onClick={() => window.print()} style={{ padding: '11px 18px', border: '1px solid var(--ink)', borderRadius: 9, background: 'var(--ink)', color: '#fff', fontWeight: 700, fontSize: 13.5 }}>인쇄 / PDF</button>
    </div>
  );
}

/* ---------------- 상단바 (모드별 맥락 액션 + 스텝퍼 + 관리자 격리) ---------------- */
const FLOW = [['checklist', '체크리스트'], ['edit', '편집'], ['preview', '미리보기']];
const isAdmin = (m) => m === 'dict' || m === 'map';

function Stepper({ flow, mode, setMode, hasEst }) {
  const idx = flow.findIndex(([m]) => m === mode);
  return (
    <div className="stepper">
      {flow.map(([m, label], i) => {
        const locked = i > 0 && !hasEst;
        let cls = 'step';
        if (idx < 0) cls += i === 0 ? '' : ' locked';
        else if (i < idx) cls += ' done';
        else if (i === idx) cls += ' active';
        else if (locked) cls += ' locked';
        return (
          <React.Fragment key={m}>
            <div className={cls}>
              <span className="st-n">{i < idx ? '✓' : i + 1}</span>{label}
            </div>
            {i < flow.length - 1 && <span className="step-sep">→</span>}
          </React.Fragment>
        );
      })}
    </div>
  );
}
function AdminSubnav({ mode, setMode }) {
  return (
    <div className="adminsub">
      <div className="as-tabs">
        <button className={mode === 'dict' ? 'on' : ''} onClick={() => setMode('dict')}>단가 사전</button>
        <button className={mode === 'map' ? 'on' : ''} onClick={() => setMode('map')}>체크 매핑</button>
      </div>
    </div>
  );
}
function GearMenu({ setMode }) {
  const [open, setOpen] = useState(false);
  useEffect(() => { const h = () => setOpen(false); if (open) { document.addEventListener('click', h); return () => document.removeEventListener('click', h); } }, [open]);
  return (
    <div className="gearwrap">
      <button className="gearbtn" onClick={(e) => { e.stopPropagation(); setOpen(!open); }}>⚙ 관리자 ▾</button>
      {open && (
        <div className="gearmenu open">
          <div className="gm-h">관리자 설정</div>
          <button className="gm-item" onClick={() => setMode('dict')}>📋 <b>단가 사전</b><span>단가·원가·이익률·패키지·수량규칙</span></button>
          <button className="gm-item" onClick={() => setMode('map')}>🔗 <b>체크 매핑</b><span>체크 항목 ↔ 단가 사전 품목 연결</span></button>
        </div>
      )}
    </div>
  );
}

function TopBar({ mode, setMode, est, flash, setEst, calc, hasEst, onNew, onOpenArchive, onFinalSave, onDictSave, dictDirty }) {
  const fileRef = useRef(null);
  const load = (e) => {
    const f = e.target.files[0]; if (!f) return;
    const r = new FileReader();
    r.onload = () => { try { setEst(JSON.parse(r.result)); flash('불러옴'); } catch (err) { alert('파일을 읽을 수 없습니다'); } };
    r.readAsText(f); e.target.value = '';
  };
  const exportXls = () => window.exportExcel(est, calc);
  const run = (k) => ({
    reset: () => { localStorage.setItem('chk:state', JSON.stringify({ ...JSON.parse(localStorage.getItem('chk:state') || '{}'), items: {} })); window.location.reload(); },
    preview: () => setMode('preview'),
    finalSave: onFinalSave,
    xls: exportXls,
    print: () => window.print(),
    import: () => fileRef.current.click(),
    new: onNew,
    dictSave: onDictSave,
    mapSave: () => window.CHKMAP_SAVE && window.CHKMAP_SAVE(),
  }[k] || (() => {}));

  const ACTIONS = {
    home: [],
    archive: [],
    checklist: [],
    edit: [],
    preview: [],
    dict: [{ k: 'dictSave', label: dictDirty ? '사전 저장' : '저장됨', solid: true }],
    map: [{ k: 'mapSave', label: '매핑 저장', solid: true }],
  };
  const btnStyle = (a) => ({
    ...tbBtn,
    ...(a.solid ? { background: 'var(--accent)', color: '#fff', border: '1px solid transparent' } : {}),
    ...(a.dark ? { background: 'var(--ink)', color: '#fff', border: '1px solid var(--ink)' } : {}),
  });
  const showArchiveBtn = ['home', 'checklist', 'edit', 'preview', 'archive'].includes(mode);

  return (
    <div className={'no-print' + (mode === 'checklist' ? ' tb-checklist' : '')} style={{ position: 'sticky', top: 0, zIndex: 50, background: 'oklch(1 0 0 / 0.86)', backdropFilter: 'blur(12px)', borderBottom: '1px solid var(--line)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, maxWidth: 1320, margin: '0 auto', padding: '11px 24px', position: 'relative' }}>
        {isAdmin(mode) ? (
          <>
            <button onClick={() => setMode('edit')} style={{ ...tbBtn, border: 'none', background: 'transparent' }}>← 견적으로</button>
            <AdminSubnav mode={mode} setMode={setMode} />
          </>
        ) : (
          <div onClick={() => setMode('home')} style={{ display: 'flex', alignItems: 'center', gap: 9, cursor: 'pointer' }}>
            <img src="symbol.svg" alt="아정당" style={{ height: 24 }} />
            <div style={{ fontWeight: 800, fontSize: 16 }}>아정당 견적서</div>
          </div>
        )}

        <span style={{ flex: 1 }} />

        <div style={{ display: 'flex', gap: 7, alignItems: 'center' }}>
          {ACTIONS[mode].map((a) => <button key={a.k} onClick={run(a.k)} style={btnStyle(a)}>{a.label}</button>)}
          <span className="tb-util" style={{ display: 'flex', gap: 7, alignItems: 'center' }}>
            {showArchiveBtn && <button onClick={onOpenArchive} className="nb-archive" data-on={mode === 'archive'}>📁 보관함</button>}
            {!isAdmin(mode) && !window.PUBLIC_DEMO && <GearMenu setMode={setMode} />}
          </span>
        </div>

        {/* 작성 흐름 스텝퍼만 절대 중앙 (관리자 서브탭은 좌측 인라인) */}
        {!isAdmin(mode) && (
          <div style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
            <div style={{ pointerEvents: 'auto' }}><Stepper flow={FLOW} mode={mode} setMode={setMode} hasEst={hasEst} /></div>
          </div>
        )}
        <input ref={fileRef} type="file" accept=".json" onChange={load} style={{ display: 'none' }} />
      </div>
    </div>
  );
}

/* ---------------- 현장 정보 ---------------- */
function SiteCard({ est, patchSite }) {
  return (
    <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 12, padding: '16px 18px' }}>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
        <Field label="현장명" value={est.site.name} onChange={(v) => patchSite('name', v)} ph="예: 분당 산운마을 44PY" />
        <Field label="고객명" value={est.site.client} onChange={(v) => patchSite('client', v)} ph="홍길동" w={140} />
        <Field label="평형(PY)" value={est.site.py} onChange={(v) => patchSite('py', v)} ph="44" w={90} mono />
      </div>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginTop: 12 }}>
        <Field label="주소" value={est.site.addr} onChange={(v) => patchSite('addr', v)} ph="시·군·구 ○○로 ○○" />
        <Field label="견적일" value={est.site.date} onChange={(v) => patchSite('date', v)} w={130} mono />
        <Field label="유효기한" value={est.site.validDate} onChange={(v) => patchSite('validDate', v)} ph="2026-00-00" w={130} mono />
        <Field label="담당자" value={est.site.manager} onChange={(v) => patchSite('manager', v)} ph="담당자명" w={110} />
      </div>
    </div>
  );
}

/* ---------------- 갑지 정보 (회사 · 비고) ---------------- */
function CompanyCard({ est, setEst }) {
  const [open, setOpen] = useState(false);
  const co = est.company || {};
  const patchCo = (k, v) => setEst((e) => ({ ...e, company: { ...e.company, [k]: v } }));
  return (
    <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 12, padding: open ? '14px 18px' : '0', marginTop: 12, overflow: 'hidden' }}>
      <button onClick={() => setOpen(!open)} style={{ width: '100%', textAlign: 'left', border: 'none', background: 'transparent', padding: open ? '0 0 12px' : '13px 18px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 13.5, fontWeight: 700, color: 'var(--ink-2)' }}>
        <span>갑지 정보</span><span style={{ color: 'var(--ink-3)' }}>{open ? '▲' : '▼'}</span>
      </button>
      {open && (
        <div>
          <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            <Field label="상호" value={co.name} onChange={(v) => patchCo('name', v)} />
            <Field label="대표" value={co.reps} onChange={(v) => patchCo('reps', v)} w={180} />
          </div>
          <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginTop: 12 }}>
            <Field label="건설업등록번호" value={co.regno} onChange={(v) => patchCo('regno', v)} w={170} />
            <Field label="주소" value={co.addr} onChange={(v) => patchCo('addr', v)} />
            <Field label="대표전화" value={co.tel} onChange={(v) => patchCo('tel', v)} w={150} />
          </div>
          <label style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 12 }}>
            <span style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink-3)' }}>비고 (REMARKS) — 갑지 하단</span>
            <textarea value={est.remarks || ''} onChange={(e) => setEst((x) => ({ ...x, remarks: e.target.value }))} rows={3}
              style={{ padding: '8px 10px', border: '1px solid var(--line-strong)', borderRadius: 7, outline: 'none', fontSize: 12.5, lineHeight: 1.6, resize: 'vertical', fontFamily: 'inherit' }} />
          </label>
        </div>
      )}
    </div>
  );
}

/* ---------------- 보관함 (est:saved 전용 페이지) ---------------- */
function ArchivePage({ onOpen, onNew, flash }) {
  const [list, setList] = useState(readSaved);
  const [q, setQ] = useState('');
  const C2 = window.EstCalc;
  const del = (id, e) => { e.stopPropagation(); if (!confirm('이 견적을 보관함에서 삭제할까요?')) return; const arr = readSaved().filter((x) => x.id !== id); writeSaved(arr); setList(arr); };
  const shown = list
    .filter((s) => !q.trim() || (s.label || '').toLowerCase().includes(q.trim().toLowerCase()))
    .slice().sort((a, b) => (b.savedAt || '').localeCompare(a.savedAt || ''));
  return (
    <div style={{ maxWidth: 980, margin: '0 auto', padding: '24px 24px 80px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
        <h2 style={{ margin: 0, fontSize: 20, fontWeight: 800 }}>📁 보관함 <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink-3)' }}>{list.length}건</span></h2>
        <span style={{ flex: 1 }} />
        <button onClick={onNew} style={{ padding: '9px 16px', border: 'none', borderRadius: 9, background: 'var(--accent)', color: '#fff', fontWeight: 800, fontSize: 13.5 }}>＋ 새 견적</button>
      </div>
      <div style={{ marginBottom: 14 }}>
        <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="고객·현장 검색" style={{ width: '100%', padding: '9px 12px', border: '1px solid var(--line-strong)', borderRadius: 9, outline: 'none', fontSize: 14 }} />
      </div>
      <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 12, overflow: 'hidden' }}>
        {shown.length === 0 && <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>보관된 견적이 없습니다. 미리보기에서 "✓ 최종 저장"하면 여기 쌓입니다.</div>}
        {shown.map((s) => (
          <div key={s.id} onClick={() => onOpen(s)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px', borderTop: '1px solid var(--line)', cursor: 'pointer' }}
            onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-2)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.label}</div>
              <div className="mono" style={{ fontSize: 11.5, color: 'var(--ink-3)' }}>{s.savedAt} · ₩ {C2.won(s.amount || 0)}</div>
            </div>
            <span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--accent-ink)' }}>열기 →</span>
            <button onClick={(e) => del(s.id, e)} style={{ border: 'none', background: 'transparent', color: 'var(--ink-3)', fontSize: 13 }}>✕</button>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---------------- 물품 구매 대행 (가전/가구 등, 견적 총액 미포함) ---------------- */
function PurchaseCard({ est, add, patch, del }) {
  const [open, setOpen] = useState(false);
  const fmt = window.fmtNum || ((v) => (v == null ? '' : String(v)));
  const list = est.purchases || [];
  const total = list.reduce((a, p) => a + C.num(p.price) * (C.num(p.qty) || 0), 0);
  const inp = { padding: '6px 8px', border: '1px solid var(--line-strong)', borderRadius: 7, outline: 'none', fontSize: 13 };
  return (
    <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 12, marginTop: 14, overflow: 'hidden' }}>
      <button onClick={() => setOpen(!open)} style={{ width: '100%', textAlign: 'left', border: 'none', background: 'var(--surface-2)', padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 9, fontSize: 13.5, fontWeight: 700, color: 'var(--ink-2)' }}>
        <span>🛒 물품 구매 대행 <span style={{ fontWeight: 600, color: 'var(--ink-3)' }}>(가전·가구 등 · 견적 총액 미포함)</span></span>
        <span style={{ flex: 1 }} />
        {list.length > 0 && <span className="mono" style={{ fontSize: 12.5, color: 'var(--ink-2)' }}>{list.length}건 · ₩ {C.won(total)}</span>}
        <span style={{ color: 'var(--ink-3)' }}>{open ? '▲' : '▼'}</span>
      </button>
      {open && (
        <div style={{ padding: '12px 16px' }}>
          {list.length === 0 && <div style={{ fontSize: 12.5, color: 'var(--ink-3)', padding: '4px 0 10px' }}>밀레 가전, 가구 등 인테리어 견적과 별도로 구매대행하는 품목을 적습니다. <b>견적 총액엔 안 들어가고</b> 갑지에 별도 안내됩니다.</div>}
          {list.map((p, i) => (
            <div key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '5px 0', borderTop: i ? '1px solid var(--line)' : 'none' }}>
              <input value={p.name} onChange={(e) => patch(i, { name: e.target.value })} placeholder="품목 (예: 밀레 식기세척기)" style={{ ...inp, flex: 1, fontWeight: 600 }} />
              <input value={p.qty} onChange={(e) => patch(i, { qty: e.target.value })} className="mono" style={{ ...inp, width: 50, textAlign: 'right' }} />
              <input value={fmt(p.price)} onChange={(e) => patch(i, { price: e.target.value.replace(/[^\d.]/g, '') })} className="mono" placeholder="단가" style={{ ...inp, width: 110, textAlign: 'right' }} />
              <span className="mono" style={{ width: 110, textAlign: 'right', fontSize: 12.5, color: 'var(--ink-2)' }}>₩ {C.won(C.num(p.price) * (C.num(p.qty) || 0))}</span>
              <button onClick={() => del(i)} style={{ width: 22, height: 22, border: 'none', background: 'transparent', color: 'var(--ink-3)' }}>✕</button>
            </div>
          ))}
          <button onClick={add} style={{ marginTop: 10, padding: '7px 13px', border: '1px dashed var(--line-strong)', background: 'var(--surface)', color: 'var(--ink-2)', borderRadius: 8, fontWeight: 700, fontSize: 12.5 }}>＋ 물품 추가</button>
          {list.length > 0 && <span style={{ marginLeft: 12, fontSize: 12.5, color: 'var(--ink-2)' }}>합계(내부 확인용) <b className="mono">₩ {C.won(total)}</b></span>}
        </div>
      )}
    </div>
  );
}

/* ---------------- 직접 항목 추가 (이 견적에만, 단가 사전 미반영) ---------------- */
function DirectAdd({ onAdd, flash }) {
  const [open, setOpen] = useState(false);
  const [f, setF] = useState({ gongjong: '기타공사', name: '', unit: 'EA', qty: '1', mat: '', lab: '', note: '' });
  const set = (k, v) => setF((x) => ({ ...x, [k]: v }));
  const submit = () => {
    if (!f.name.trim()) return;
    onAdd([{ gongjong: f.gongjong, name: f.name.trim(), unit: f.unit || 'EA', qty: f.qty || 1, mat: f.mat, lab: f.lab, gubun: '직접', note: f.note }]);
    flash && flash(`'${f.name.trim()}' 추가됨 (이 견적에만)`);
    setF({ ...f, name: '', mat: '', lab: '', note: '' });
    setOpen(false);
  };
  const inp = { padding: '7px 9px', border: '1px solid var(--line-strong)', borderRadius: 7, outline: 'none', fontSize: 13 };
  return (
    <div style={{ margin: '0 0 12px' }}>
      <button onClick={() => setOpen(!open)} style={{ padding: '8px 14px', border: '1px dashed var(--line-strong)', background: 'var(--surface)', color: 'var(--ink-2)', borderRadius: 8, fontWeight: 700, fontSize: 13 }}>＋ 항목 추가 (해당 견적에만 반영)</button>
      {open && (
        <div style={{ marginTop: 8, background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 10, padding: '12px 14px' }}>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'flex-end' }}>
            <label style={{ display: 'flex', flexDirection: 'column', gap: 3 }}><span style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>공종</span>
              <select value={f.gongjong} onChange={(e) => set('gongjong', e.target.value)} style={{ ...inp }}>{ORDER.map((g) => <option key={g} value={g}>{g}</option>)}</select></label>
            <label style={{ display: 'flex', flexDirection: 'column', gap: 3, flex: 1, minWidth: 160 }}><span style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>품목명</span>
              <input value={f.name} onChange={(e) => set('name', e.target.value)} placeholder="예: 밀레 인덕션(특수)" style={inp} /></label>
            <label style={{ display: 'flex', flexDirection: 'column', gap: 3, width: 64 }}><span style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>단위</span>
              <input value={f.unit} onChange={(e) => set('unit', e.target.value)} style={inp} /></label>
            <label style={{ display: 'flex', flexDirection: 'column', gap: 3, width: 56 }}><span style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>수량</span>
              <input value={f.qty} onChange={(e) => set('qty', e.target.value)} className="mono" style={{ ...inp, textAlign: 'right' }} /></label>
            <label style={{ display: 'flex', flexDirection: 'column', gap: 3, width: 96 }}><span style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>자재단가</span>
              <input value={f.mat} onChange={(e) => set('mat', e.target.value.replace(/[^\d.]/g, ''))} className="mono" style={{ ...inp, textAlign: 'right' }} placeholder="0" /></label>
            <label style={{ display: 'flex', flexDirection: 'column', gap: 3, width: 96 }}><span style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>노무단가</span>
              <input value={f.lab} onChange={(e) => set('lab', e.target.value.replace(/[^\d.]/g, ''))} className="mono" style={{ ...inp, textAlign: 'right' }} placeholder="0" /></label>
            <button onClick={submit} style={{ padding: '8px 16px', border: 'none', background: 'var(--accent)', color: '#fff', borderRadius: 8, fontWeight: 700, fontSize: 13 }}>추가</button>
          </div>
          <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 8 }}>단가 사전에는 저장되지 않고 <b>이 견적에만</b> 적용됩니다. 견적서 미리보기에도 그대로 표시됩니다.</div>
        </div>
      )}
    </div>
  );
}

/* ---------------- 홈 (새 체크리스트 시작 + 과거 목록) ---------------- */
function HomeScreen({ onStart }) {
  const [info, setInfo] = useState({ client: '', name: '', py: '', date: todayStr(), budget: '' });
  const set = (k, v) => setInfo((x) => ({ ...x, [k]: v }));
  const pyOk = parseFloat(String(info.py).replace(/[^\d.]/g, '')) > 0;
  const inp = { padding: '10px 12px', border: '1px solid var(--line-strong)', borderRadius: 8, outline: 'none', fontSize: 14 };
  const fld = (label, k, ph, wide, req) => (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: wide ? '1 1 100%' : '1 1 140px', minWidth: 120 }}>
      <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--ink-3)' }}>{label}{req && <span style={{ color: 'var(--accent-ink)' }}> *</span>}</span>
      <input value={info[k]} onChange={(e) => set(k, e.target.value)} placeholder={ph} style={{ ...inp, ...(req && !pyOk ? { border: '1px solid var(--accent)' } : {}) }} />
    </label>
  );
  const start = () => { if (!pyOk) { alert('평형(평수)을 먼저 입력해주세요. 수량 자동 산출의 기준입니다.'); return; } onStart(info); };
  return (
    <div style={{ maxWidth: 620, margin: '0 auto', padding: '64px 24px 80px' }}>
      <div style={{ textAlign: 'center', marginBottom: 24 }}>
        <div style={{ fontSize: 25, fontWeight: 800, letterSpacing: '-0.01em' }}>새 견적 시작</div>
        <div style={{ fontSize: 13.5, color: 'var(--ink-3)', marginTop: 8 }}>고객·현장 정보를 입력하고 상담 체크리스트로 넘어갑니다.</div>
      </div>
      <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 16, padding: '24px', boxShadow: 'var(--shadow)' }}>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
          {fld('평형', 'py', '예) 33', false, true)}
          {fld('고객명', 'client', '예) 박○○')}
          {fld('상담일', 'date', '26.06.05')}
          {fld('예산', 'budget', '8,000만')}
          {fld('현장 / 주소', 'name', '예) 고덕 베네루체 32평 A', true)}
        </div>
        <button onClick={start} disabled={!pyOk}
          style={{ marginTop: 20, width: '100%', padding: '15px', border: 'none', borderRadius: 11, background: pyOk ? 'var(--accent)' : 'var(--line-strong)', color: '#fff', fontWeight: 800, fontSize: 16, cursor: pyOk ? 'pointer' : 'default' }}>
          체크리스트 시작 →
        </button>
        {!pyOk && <div style={{ fontSize: 11.5, color: 'var(--ink-3)', textAlign: 'center', marginTop: 9 }}>평형은 필수예요 — 입력한 평형으로 품목 수량이 자동 계산됩니다.</div>}
      </div>
    </div>
  );
}

// 로드 시 저장된 단가 사전(/dict)이 있으면 시드(dict-data.js) 위에 덮어쓴 뒤 렌더
(async function boot() {
  // 로컬 서버(/dict)면 그걸로, 정적 배포면 번들된 JSON 파일로 폴백 → 둘 다 같은 데이터
  const tryJson = async (...urls) => {
    for (const u of urls) { try { const r = await fetch(u); if (r.status === 200) { const j = await r.json(); if (j) return j; } } catch (e) {} }
    return null;
  };
  const d = await tryJson('/dict', 'dict-store.json');
  if (Array.isArray(d) && d.length) window.DICT = d;
  const m = await tryJson('/chkmap', 'chkmap.json');
  window.CHK_MAP = (m && typeof m === 'object' && !Array.isArray(m)) ? m : {};
  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
})();
})();
