From 7e57a970f83df5f83a4f99ba66f39170f54020a3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 08:03:55 +0900 Subject: [PATCH] =?UTF-8?q?perf(522-2=C2=B7522-3):=20B07=20=ED=8F=89?= =?UTF-8?q?=EB=A9=B4=EB=8F=84=20=E2=9A=99=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=AB=20=ED=99=94=EB=A9=B4=EB=B6=80=ED=84=B0=20?= =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=EB=A7=8C=20=C2=B7=20=EB=B0=9B?= =?UTF-8?q?=EC=95=84=20=EB=91=94=20=EB=8F=84=EB=A9=B4=20=EC=9E=AC=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cad_host bare 옵션 = iframe 주소 ?bare · openwebcad main.tsx 그리기 전 setBare(true) — 도면 오기 전에도 리본 · 패널 없음 - 평면도 관리 모달 · B06 표 위 CAD = bare - 평면도 관리 = 페이지 목록(deps.drawings) 그대로 · 장 받기 = Prefetch.openDrawing · 같은 목록 다시 열기 = 요청 없음 · 고친 뒤만 목록 새로 + syncDrawings - test_130 흉내 갱신(처음 = 목록 안 받음 · 고친 뒤 맞춤 · 다시 열기 요청 없음 · ?bare) - 첫 열기 1.23~1.34 s → 0.31 s · 다시 열기 0.14~0.16 s · 요청 3 → 0 Co-Authored-By: Claude Opus 5.5 --- .../B06_Section_UI_Cross_Detail_Cad.ts | 1 + .../B07_DesignDetail_UI_PlanManage.ts | 38 ++++++++++++------ .../B07_DesignDetail_UI_Settings.ts | 2 +- B07_DesignDetail/openwebcad/src/main.tsx | 3 ++ .../tester/helper_130_b07_plan_manage.cjs | 39 ++++++++++++++++--- resources/tester/test_130_b07_plan_manage.py | 17 ++++++++ ui_template/cad_host/cad_host.ts | 4 +- 7 files changed, 86 insertions(+), 18 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts b/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts index c62aecddc..08865009c 100644 --- a/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts +++ b/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts @@ -333,6 +333,7 @@ export function mountNoteCad( let closed = false; const cad = createCadHost({ title: "표 위 CAD", + bare: true, onChanged: (dirty) => { if (!dirty || closed) return; cad diff --git a/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts b/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts index b4dbe70bd..3b552d98d 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts @@ -7,13 +7,13 @@ * 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 캔버스만 `bare`) · 확정 배지 · [CAD 에서 열기] * 고치기는 지금 서버 길 그대로(종류 파일) — 고칠 때마다 모달 안을 다시 받고, * 닫을 때 `onClosed(changed)` 로 좌측 목록 · 도면을 다시 읽음. + * 처음 열 때 = 페이지 목록 · 미리받은 장(`Prefetch.openDrawing`) 그대로 · 같은 목록으로 다시 열면 요청 없음(522-3). * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { createCadHost } from "@ui/cad_host/cad_host"; import { - fetchDesignDrawing, fetchDesignDrawingList, fetchPlanKinds, reorderPlanKinds, @@ -21,6 +21,7 @@ import { type DesignDrawingItem, type PlanKindsState, } from "./B07_DesignDetail_Api_Fetch"; +import { openDrawing, syncDrawings } from "./B07_DesignDetail_Prefetch"; import { groupPlanSheets, kindForm, @@ -36,8 +37,15 @@ export interface PlanManageControls { onClosed: (changed: boolean, openId?: string) => void; /** 머리 단추 — ⚙ 로 열 때 [원본에서 다시 그리기](280-5). */ actions?: HTMLElement[]; + /** 페이지가 받은 도면 목록 — 있으면 처음엔 목록을 다시 안 받음. */ + drawings?: DesignDrawingItem[]; } +/** 마지막으로 세운 종류 · 목록 — 같은 페이지 목록(`source`)으로 다시 열면 그대로 씀. */ +let seen: + | { source: DesignDrawingItem[] | undefined; state: PlanKindsState; list: DesignDrawingItem[] } + | undefined; + function el( tag: K, className: string, @@ -66,7 +74,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo let pickedKind = ""; let form: KindForm | undefined; let sheets: DesignDrawingItem[] = []; - const preview = createCadHost({ title: "평면도 미리보기" }); + const preview = createCadHost({ title: "평면도 미리보기", bare: true }); const side = el("div", "b07-plan-manage__side"); const head = el("div", "b07-plan-manage__head"); const view = el("div", "b07-plan-manage__view"); @@ -77,7 +85,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo projectId, onChanged: () => { changed = true; - void refresh(); + void refresh(true); }, }; @@ -171,7 +179,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo head.replaceChildren(el("strong", "", label)); preview.beginLoading(); try { - const response = await fetchDesignDrawing(projectId, drawing.id); + const response = await openDrawing(drawing); if (closed || picked !== drawing.id) return; const badge = el("span", "b07-plan-manage__badge", response.confirmed ? "확정" : "확정 전"); badge.dataset.confirmed = String(response.confirmed); @@ -198,15 +206,23 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo } }; - /** 종류 · 장 목록을 서버에서 다시 받아 왼쪽을 세우고 보던 장(없으면 첫 장)을 다시 봄. */ - const refresh = async (): Promise => { + /** 종류 · 장 목록으로 왼쪽을 세우고 보던 장(없으면 첫 장)을 다시 봄 · + * edited = 고친 뒤 — 서버에서 새로 받고 지문 바뀐 장은 미리받기에서 버림. */ + const refresh = async (edited = false): Promise => { try { - const [state, list] = await Promise.all([ - fetchPlanKinds(projectId), - fetchDesignDrawingList(projectId), - ]); + const source = controls.drawings; + const fresh = edited || !source; + const known = !fresh && seen?.source === source ? seen : undefined; + const [state, list] = known + ? [known.state, known.list] + : await Promise.all([ + fetchPlanKinds(projectId), + fresh ? fetchDesignDrawingList(projectId).then((got) => got.drawings) : source, + ]); if (closed) return; - sheets = list.drawings.filter((item) => item.kind === "plan"); + if (fresh) syncDrawings(projectId, list); + seen = { source, state, list }; + sheets = list.filter((item) => item.kind === "plan"); const all = groupPlanSheets(sheets).flatMap((group) => group.sheets); const keep = all.find((sheet) => sheet.drawing.id === picked) ?? all[0]; if (keep) pickedKind = keep.drawing.plan_kind ?? ""; diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts b/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts index c33e455d8..bcfa1243d 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts @@ -68,7 +68,7 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void redraw.classList.add("b07-settings__redraw"); if (kind === "plan" && deps.plan) { - const manager = openPlanManager({ ...deps.plan, actions: [redraw] }); + const manager = openPlanManager({ ...deps.plan, drawings: deps.drawings, actions: [redraw] }); closeModal = manager.close; return; } diff --git a/B07_DesignDetail/openwebcad/src/main.tsx b/B07_DesignDetail/openwebcad/src/main.tsx index 9e4b392d7..38280e159 100644 --- a/B07_DesignDetail/openwebcad/src/main.tsx +++ b/B07_DesignDetail/openwebcad/src/main.tsx @@ -31,6 +31,7 @@ import { isViewOnly, setActiveLayerId, setActiveToolActor, + setBare, setCanvas, setEntities, setHighlightedEntityIds, @@ -46,6 +47,8 @@ import { ActorEvent, type DrawEvent } from './tools/tool.types'; // 호스트 앱의 화이트/블랙 모드를 먼저 붙인 뒤 렌더한다. syncThemeFromHost(); +// 부모가 `?bare` 로 띄우면 첫 그림부터 캔버스만 — 도면을 넘길 때 다시 정함(522-2) +if (new URLSearchParams(window.location.search).has('bare')) setBare(true); ReactDOM.createRoot(document.getElementById('root') as HTMLDivElement).render( diff --git a/resources/tester/helper_130_b07_plan_manage.cjs b/resources/tester/helper_130_b07_plan_manage.cjs index 84f24c7ce..7129fb2e8 100644 --- a/resources/tester/helper_130_b07_plan_manage.cjs +++ b/resources/tester/helper_130_b07_plan_manage.cjs @@ -187,8 +187,11 @@ const sheets = () => ); const copy = () => JSON.parse(JSON.stringify(state)); const API = { - fetchPlanKinds: async () => copy(), - fetchDesignDrawingList: async () => ({ drawings: [{ id: "cover", kind: "cover" }, ...sheets()] }), + fetchPlanKinds: async () => (calls.push(["kinds"]), copy()), + fetchDesignDrawingList: async () => ( + calls.push(["list"]), + { drawings: [{ id: "cover", kind: "cover" }, ...sheets()] } + ), fetchDesignDrawing: async (_, id) => ( calls.push(["fetchDrawing", id]), { id, confirmed: confirmed.has(id), drawing: { entities: [{ id }], layers: [] } } @@ -232,6 +235,12 @@ const { loadTs } = makeLoader({ if (request === "@ui/ui_template_collapsible") return loadTs(path.join(ROOT, "ui_template", "ui_template_collapsible.ts")); if (request === "./B07_DesignDetail_Api_Fetch") return API; + // 522-3 미리받기 — 장 받기는 `openDrawing` · 고친 뒤 목록 맞춤은 `syncDrawings` + if (request === "./B07_DesignDetail_Prefetch") + return { + openDrawing: (item) => API.fetchDesignDrawing("p1", item.id), + syncDrawings: (_, list) => calls.push(["sync", list.length]), + }; if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); throw new Error(`stub 없음: ${request}`); }, @@ -245,9 +254,15 @@ const { openPlanManager } = loadTs(path.join(B07, "B07_DesignDetail_UI_PlanManag const out = {}; const closes = []; const selected = []; - const plan = { projectId: "p1", onClosed: (changed, openId) => closes.push([changed, openId]) }; + // 페이지가 받은 목록을 넘김(`openDrawingSettings` 와 같은 길 · 522-3) + const pageList = [{ id: "cover", kind: "cover", label: "표지", confirmed: false }, ...sheets()]; + const plan = { + projectId: "p1", + drawings: pageList, + onClosed: (changed, openId) => closes.push([changed, openId]), + }; const panel = buildDrawingSidePanel( - [{ id: "cover", kind: "cover", label: "표지", confirmed: false }, ...sheets()], + pageList, (drawing) => selected.push(drawing.id), undefined, false, @@ -288,6 +303,8 @@ const { openPlanManager } = loadTs(path.join(B07, "B07_DesignDetail_UI_PlanManag hasMarks: all(modal.body).some((n) => n.textContent === "대피소 · 사토장 자리"), preview: cad.loads.at(-1).drawing.entities[0].id, extra: cad.loads.at(-1).extra, + bare: cad.opts.bare, + calls: calls.map((c) => c[0]), }; // 종류 추가 — 「새 종류」 · 이름 · 조각 하나 @@ -297,7 +314,12 @@ const { openPlanManager } = loadTs(path.join(B07, "B07_DesignDetail_UI_PlanManag action().click(); await settle(); await settle(); - out.add = { kinds: kindRows(), sheets: sheetIds(), call: calls.find((c) => c[0] === "add") }; + out.add = { + kinds: kindRows(), + sheets: sheetIds(), + call: calls.find((c) => c[0] === "add"), + after: calls.slice(calls.findIndex((c) => c[0] === "add")).map((c) => c[0]), + }; // 업데이트 — 새 종류를 골라 이름 · 축척 바꿈 kindPick().change("k_new"); @@ -393,10 +415,17 @@ const { openPlanManager } = loadTs(path.join(B07, "B07_DesignDetail_UI_PlanManag out.close = { closes: [...closes], destroyed: cad.destroyed, open: modal.open }; // 다시 열어 고치지 않고 [CAD 에서 열기] → changed = false · 그 장 id + const callsBefore = calls.length; gear().click(); await settle(); await settle(); const modal2 = modals[1]; + out.reopen = { + calls: calls.slice(callsBefore).map((c) => c[0]), + kinds: modal2.body + .querySelectorAll(".b07-plan-manage__sheets .b07-plan-manage__row") + .map((row) => row.dataset.planKind), + }; modal2.body .querySelectorAll(".b07-plan-manage__sheet") .find((b) => b.dataset.drawingId === "plan_k_route") diff --git a/resources/tester/test_130_b07_plan_manage.py b/resources/tester/test_130_b07_plan_manage.py index 25d45793a..e635d36b7 100644 --- a/resources/tester/test_130_b07_plan_manage.py +++ b/resources/tester/test_130_b07_plan_manage.py @@ -9,6 +9,8 @@ - 장 단추 → 드롭다운 따라감 · 드롭다운 → 그 종류 첫 장 · 줄 끌어 놓기 = 종류 순서(놓는 즉시 · 140-2). - 미리보기 장 바꾸기 · 캔버스만(bare) · 확정 배지만(확정 풀기 없음 140-1) · 닫으면 changed=True · [CAD 에서 열기] = 그 장 id. + - 522 처음 = 페이지 목록 · 미리받기 장 · 고친 뒤만 목록 새로 + 미리받기 맞춤 · 같은 목록 다시 열기 = 요청 없음 · iframe `?bare`. + Node 헬퍼 `helper_130_b07_plan_manage.cjs` 가 화면 그릇 흉내로 돌림. 화면은 사용자 확인. """ @@ -59,6 +61,21 @@ def test_첫_장_미리보기는_보기_전용(out): assert extra["readOnly"] is True and extra["viewOnly"] is True assert extra["bare"] is True # 140-1 캔버스만(제목줄 · 리본 · 상태막대 없음) assert extra["recoveryScope"] == "b07-plan-preview" # 본 CAD 자동백업 칸과 안 겹침 + assert out["open"]["bare"] is True # 522-2 iframe 을 `?bare` 로 — 도면 오기 전에도 리본 없음 + + +def test_처음_열기는_페이지_목록(out): + # 522-3 목록은 페이지가 받은 것 · 장은 미리받기(`openDrawing`) — 목록 다시 안 받음 + assert out["open"]["calls"] == ["kinds", "fetchDrawing"] + + +def test_고친_뒤엔_목록_새로_맞춤(out): + assert out["add"]["after"][:4] == ["add", "kinds", "list", "sync"] + + +def test_같은_목록으로_다시_열면_요청_없음(out): + assert out["reopen"]["calls"] == ["fetchDrawing"] # 진짜 길은 미리받기 캐시 + assert out["reopen"]["kinds"] == ["k_route", "k_terrain"] # 고친 뒤 받은 순서 그대로 def test_종류_추가(out): diff --git a/ui_template/cad_host/cad_host.ts b/ui_template/cad_host/cad_host.ts index d4b2a19f8..87f000e92 100644 --- a/ui_template/cad_host/cad_host.ts +++ b/ui_template/cad_host/cad_host.ts @@ -64,6 +64,8 @@ export interface CadSaveResult { export interface CadHostOptions { /** iframe 제목(접근성). */ title: string; + /** 처음부터 캔버스만(`?bare`) — 도면이 오기 전에도 리본 · 패널을 안 그림(522-2). */ + bare?: boolean; /** CAD 편집 통지 — dirty 는 미저장 편집이 있는가. */ onChanged?: (dirty: boolean) => void; /** CAD 리본의 DXF·DWG 내보내기 단추. */ @@ -142,7 +144,7 @@ export function createCadHost {