diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaCellPick.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaCellPick.ts index d4f5be81f..bcc23497c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaCellPick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaCellPick.ts @@ -8,8 +8,6 @@ * ========================================================================== */ import { el } from "@ui/ui_template_elements"; -import { askShare } from "../A00_Common/spreadsheet/spreadsheet_link_ask"; -import { linksAt } from "../A00_Common/spreadsheet/spreadsheet_link_set"; import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { readTemplate, type Layer } from "./M02_MasterTemplete_Api_Fetch"; import "./M02_MasterTemplete_AreaPanel.css"; @@ -57,6 +55,11 @@ export function mountAreaSheet(host: HTMLElement, opts: AreaSheetOptions): AreaS host.replaceChildren(el("p", { className: "m02-cellpick__now", text: TEXT.loading })); const choose = async (): Promise => { const at = picked; + // 엔진 조각은 고를 때만(정적 import 금지 — 계약 `spreadsheet_types.ts` 머리) + const [{ askShare }, { linksAt }] = await Promise.all([ + import("../A00_Common/spreadsheet/spreadsheet_link_ask"), + import("../A00_Common/spreadsheet/spreadsheet_link_set"), + ]); const sheetId = book?.시트.find((s) => s.id === book?.활성)?.id ?? book?.시트[0]?.id; const others = book && sheetId ? linksAt(book, sheetId, at, opts.pill) : []; const choice = others.length diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts index 7219678a7..244349a77 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts @@ -8,8 +8,6 @@ import { createButton, createInputField, el } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; -import { askShare } from "../A00_Common/spreadsheet/spreadsheet_link_ask"; -import { linksAt, normA1 } from "../A00_Common/spreadsheet/spreadsheet_link_set"; import type { CalcValue, LinkInfo, SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; const TEXT = { @@ -72,6 +70,11 @@ export function openAreaLink(opts: AreaLinkOptions): void { }; /** 잇기 · 풀기 — 그 칸에 다른 알약이 있으면 함께 · 바꾸기 · 취소 물음(취소 = 모달 그대로) · 틀린 주소 false */ const done = async (cell: string | null): Promise => { + // 엔진 조각은 누를 때만(정적 import 금지 — 계약 `spreadsheet_types.ts` 머리) + const [{ askShare }, { linksAt, normA1 }] = await Promise.all([ + import("../A00_Common/spreadsheet/spreadsheet_link_ask"), + import("../A00_Common/spreadsheet/spreadsheet_link_set"), + ]); const a1 = cell === null ? null : normA1(cell); if (cell !== null && !a1) return false; const book = opts.sheet.getDoc(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 9c63bd843..098950a16 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -29,7 +29,7 @@ import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side"; import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; import type { SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; -import { recalcWorkbook } from "../A00_Common/spreadsheet/spreadsheet_recalc"; +type Recalc = (typeof import("../A00_Common/spreadsheet/spreadsheet_recalc"))["recalcWorkbook"]; import { getActiveLayer, getActiveOwner, @@ -122,7 +122,8 @@ export function mountLaborDetail( const given = (): Record => Object.fromEntries([...links].flatMap(([k, v]) => (v.값 === null ? [] : [[k, v.값]]))); /** 그 열 면적산출 문서 · 판 — [엑셀에서 고르기]로 이으면 바뀜(일위대가 [저장] 때 함께 저장) */ - let area: { doc: Workbook; 판: string } | null = null; + // 엔진(recalc)은 면적 문서와 함께 늦게 받음 — 정적 import 금지(계약 `spreadsheet_types.ts` 머리) + let area: { doc: Workbook; 판: string; recalc: Recalc } | null = null; let areaDirty = false; /** 알약 키 → 이름 · 단위(calc 답 `알약`) — 칸에 이을 때 연결 정보 */ let pills = new Map(); @@ -285,7 +286,7 @@ export function mountLaborDetail( showToast(tc("Labor_BadCell"), "error"); return false; } - next.계산값 = recalcWorkbook(next); // 식 칸 값도 바로 — 저장 때 서버가 다시 셈 + next.계산값 = area.recalc(next); // 식 칸 값도 바로 — 저장 때 서버가 다시 셈 area.doc = next; links = areaLinks(next); areaDirty = true; @@ -638,9 +639,12 @@ export function mountLaborDetail( loadLogics(), lib // 서고 모드엔 면적산출 없음 ? undefined - : readTemplate(opt.layer, "area", opt.column, null).then( - (got) => { - area = { doc: got.문서 as Workbook, 판: got.판 ?? "" }; + : Promise.all([ + readTemplate(opt.layer, "area", opt.column, null), + import("../A00_Common/spreadsheet/spreadsheet_recalc"), + ]).then( + ([got, { recalcWorkbook }]) => { + area = { doc: got.문서 as Workbook, 판: got.판 ?? "", recalc: recalcWorkbook }; links = areaLinks(got.문서); }, () => undefined, diff --git a/resources/tester/spreadsheet/spreadsheet_size.mjs b/resources/tester/spreadsheet/spreadsheet_size.mjs index 618be74c0..055f6f10d 100644 --- a/resources/tester/spreadsheet/spreadsheet_size.mjs +++ b/resources/tester/spreadsheet/spreadsheet_size.mjs @@ -1,8 +1,9 @@ // 스프레드시트 크기 재기 — gzip 크기(바이트). `test_spreadsheet_size.py` 가 부름. -// node spreadsheet_size.mjs <저장소 뿌리> → {"engine": {js, css}, "total": {js, css}, "static": [...]} +// node spreadsheet_size.mjs <저장소 뿌리> → {"engine": {js, css}, "total": {js, css}, "static": [...], "first": [...]} // engine = 엔진 파일만 줄여 묶은 크기 · total = 실제 앱 빌드에서 스프레드시트를 열 때 받는 조각 // (스프레드시트 조각 + 끌어오는 공용 조각 + css · 첫 화면 `index.html` 조각은 뺌). // static = 스프레드시트 조각을 정적으로 끌어오는 조각(비어야 함 — 늦게만 받음). +// first = 첫 화면 · M02 페이지 정적 사슬 조각 안에 든 스프레드시트 폴더 모듈(비어야 함 — 엔진 조각이 정적 사슬로 끌려오면 잡힘). import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -51,28 +52,39 @@ async function engineSize() { async function appSize() { const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "aislo-sheet-size-")); + const mods = new Map(); // 조각 파일 → 든 모듈 try { await build({ configLoader: "runner", configFile: path.join(root, "vite.config.ts"), logLevel: "silent", build: { outDir, emptyOutDir: true, manifest: true }, + plugins: [ + { + name: "sheet-size-modules", + generateBundle(_o, bundle) { + for (const c of Object.values(bundle)) + if (c.type === "chunk") mods.set(c.fileName, Object.keys(c.modules)); + }, + }, + ], }); const manifest = JSON.parse(fs.readFileSync(path.join(outDir, ".vite/manifest.json"), "utf8")); const key = Object.keys(manifest).find((k) => k.endsWith("spreadsheet/spreadsheet.ts")); const gz = (file) => gzipSync(fs.readFileSync(path.join(outDir, file))).length; // M02 가 이미 받은 조각은 새 몫이 아님 — 첫 화면 · M02 페이지 · M02 표(ui_template_sheet 등) // 의 정적 import 사슬. 산출근거 카드는 집계표를 연 뒤에만 열림. - const first = new Set(); - const mark = (k) => { - if (first.has(k)) return; - first.add(k); - for (const i of manifest[k].imports ?? []) mark(i); + const mark = (set, k) => { + if (set.has(k)) return; + set.add(k); + for (const i of manifest[k].imports ?? []) mark(set, i); }; const page = Object.keys(manifest).find((k) => k.endsWith("M02_MasterTemplete_UI_Page.ts")); - mark("index.html"); - mark(page); - for (const d of manifest[page].dynamicImports ?? []) if (d !== key) mark(d); + const opening = new Set(); // 첫 화면 · M02 페이지 정적 사슬만(늦은 불러오기 뺌) + mark(opening, "index.html"); + mark(opening, page); + const first = new Set(opening); + for (const d of manifest[page].dynamicImports ?? []) if (d !== key) mark(first, d); const size = { js: 0, css: 0 }; const seen = new Set(); const walk = (k) => { @@ -87,12 +99,18 @@ async function appSize() { const statics = Object.entries(manifest) .filter(([, v]) => (v.imports ?? []).includes(key)) .map(([k]) => k); - return { total: size, static: statics }; + const firstSheet = [...opening].flatMap((k) => + (mods.get(manifest[k].file) ?? []) + .map((id) => id.replaceAll("\\", "/")) + .filter((id) => id.includes("/A00_Common/spreadsheet/")) + .map((id) => id.slice(id.indexOf("A00_Common/"))), + ); + return { total: size, static: statics, first: firstSheet }; } finally { fs.rmSync(outDir, { recursive: true, force: true }); } } const engine = await engineSize(); -const { total, static: statics } = await appSize(); -console.log(JSON.stringify({ engine, total, static: statics })); +const { total, static: statics, first } = await appSize(); +console.log(JSON.stringify({ engine, total, static: statics, first })); diff --git a/resources/tester/spreadsheet/test_spreadsheet_size.py b/resources/tester/spreadsheet/test_spreadsheet_size.py index 57f4c888d..ad0d6a058 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_size.py +++ b/resources/tester/spreadsheet/test_spreadsheet_size.py @@ -38,6 +38,7 @@ def test_크기_예산(): f"gzip — 엔진 {engine} · 화면 {view} · css {total['css']} · 합 {total['js'] + total['css']}" ) assert size["static"] == [], f"스프레드시트 조각을 정적으로 끌어옴 — {size['static']}" + assert size["first"] == [], f"M02 첫 조각에 스프레드시트 모듈 — {size['first']}" assert engine <= BUDGET["engine"], f"엔진 {engine} B > {BUDGET['engine']}" assert view <= BUDGET["view"], f"화면 {view} B > {BUDGET['view']}" assert total["css"] <= BUDGET["css"], f"css {total['css']} B > {BUDGET['css']}" @@ -49,6 +50,12 @@ _STATIC = re.compile( r"^\s*import\s+(?!type\b)[^;]*?from\s+[\"'][^\"']*spreadsheet/spreadsheet", re.M ) _SIDE = re.compile(r"^\s*import\s+[\"'][^\"']*spreadsheet/spreadsheet", re.M) +# 흐름도 동기 계산 — M01 로직 실험실 `loadFlowMods` 의 import() 로만 열림 · 첫 묶음 막음은 번들 실측 시험(static · first)이 지킴 +_FLOW = { + "M02_MasterTemplete/DataFlow/M02_DF_Eval.ts", + "M02_MasterTemplete/DataFlow/M02_DF_Rule.ts", + "M02_MasterTemplete/DataFlow/M02_DF_FormulaView.ts", +} def test_M02_만_늦게_불러옴(): @@ -63,7 +70,7 @@ def test_M02_만_늦게_불러옴(): ).stdout.splitlines() for rel in listed: # 시험용 진입(`resources/tester/`)은 앱 번들 밖 - if rel.startswith(("A00_Common/spreadsheet/", "resources/tester/")): + if rel.startswith(("A00_Common/spreadsheet/", "resources/tester/")) or rel in _FLOW: continue if not (ROOT / rel).is_file(): continue