From a3815bfaa154d108066c9ad2e93a760552b0cc97 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 10:06:03 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(472-5=20=EA=B2=B9=EC=B9=A8=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC):=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=95=9C=20=EB=B2=8C=20=E2=80=94=20=EA=B4=80=20?= =?UTF-8?q?=C2=B7=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=3D=20=EA=B0=80=EA=B2=8C?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=ED=95=9C=20=EB=B2=88=20=C2=B7=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=B8=A1=EC=A0=90=20=3D=20=EC=9E=90?= =?UTF-8?q?=EA=B8=B0=20=ED=9B=85=20=C2=B7=20=EB=AC=B6=EC=9D=8C=20=3D=20?= =?UTF-8?q?=EC=B6=9C=EC=B2=98=EB=A7=88=EB=8B=A4=20=ED=95=9C=20=EB=B2=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - announceDesignStore = 관 · 구조물만(추가 측점은 _Api_PlusStations 훅이 이미 알림 · 페이지 설정은 5-C 훅) - 계산 묶음 먹이기 — 되돌리는 동안 구독은 안 받음(되살림 훅 한 길) · 묶음 입력과 같은 값 다시 알림은 안 넣음 (옛: 추가 측점 되돌리기에 묶음 두 번) - 시험 test_472_5_shop_owner 알림 수(옛 코드 실패 확인) · helper_429 가짜 묶음이 받은 값을 입력으로(까닭: 실제 묶음과 같게) Co-Authored-By: Claude Opus 5.5 --- .../design/design_pipeline_facilities.ts | 14 +++++--- A00_Common/design/design_store.ts | 7 ++-- resources/tester/helper_429_save_path.cjs | 7 +++- resources/tester/helper_472_5_shop.mjs | 33 ++++++++++++++++++- resources/tester/helper_472_5_shop_entry.ts | 2 ++ resources/tester/test_472_5_shop_owner.py | 15 +++++++-- 6 files changed, 67 insertions(+), 11 deletions(-) diff --git a/A00_Common/design/design_pipeline_facilities.ts b/A00_Common/design/design_pipeline_facilities.ts index 8aa74f311..914939d7f 100644 --- a/A00_Common/design/design_pipeline_facilities.ts +++ b/A00_Common/design/design_pipeline_facilities.ts @@ -3,12 +3,12 @@ * 계산 묶음 관 · 구조물 · 추가 측점 먹이기(PLAN 415 · 429 ⑨) — 가게 초안이 바뀌면 묶음에 넣음(비면 저장분). * · [저장] 도중 = 안 넣음(H2) — 초안이 비는 사이 열 때 읽은 옛 저장분으로 돌지 않게 · 끝나면 `resync` * (저장 목록 다시 읽기 · 입력 맞춤) 한 번. - * · 되돌리기 · 다시하기 = 세션만 되쓰고 가게 알림이 없음 → 묶음 입력과 견줘 다르면 넣음(415-2 · 노선 초안은 - * 노선 길이 맡음). + * · 되돌리기 · 다시하기 = 아래 되살림 훅 한 길 — 묶음 입력과 견줘 다르면 넣음(415-2 · 노선 초안은 노선 길이 + * 맡음) · 그 사이 가게 알림(`announceDesignStore` · 472-5)은 구독이 받지 않음(두 번 안 넣음). * 저장 창구는 늦게 불러옴(묶음 시험이 화면 모듈을 안 끌게). * ========================================================================== */ -import { onPageHistory } from "../../B05_Profile/B05_Profile_UI_Profile_History"; +import { onPageHistory, pageReplaying } from "../../B05_Profile/B05_Profile_UI_Profile_History"; import type { useDesignStore } from "./design_store"; import type { ChangeSource } from "./logic/design_pipeline_types"; @@ -57,7 +57,13 @@ export function feedFacilities(o: { saving() ? afterSave() : o.feed(source, value); const stops = [ ...FACILITIES.map(({ source, key }) => - o.store.subscribe(key, (value) => o.ready() && put(source, value ?? o.saved(key))), + o.store.subscribe(key, (value) => { + const now = value ?? o.saved(key); + // 묶음 입력과 같은 값 = 이미 받음(되살림 뒤 추가 측점 다시 알림 등) — 한 번만 넣음(472-5) + if (!o.ready() || pageReplaying() || JSON.stringify(now) === JSON.stringify(o.input(key))) + return; + put(source, now); + }), ), onPageHistory({ restore: () => { diff --git a/A00_Common/design/design_store.ts b/A00_Common/design/design_store.ts index 5bd9af3df..c1b6e0540 100644 --- a/A00_Common/design/design_store.ts +++ b/A00_Common/design/design_store.ts @@ -241,11 +241,12 @@ const listeners = new Map void>>(); * 노선을 모르면 `routeId = null` — 노선 칸(`alignmentDraft` · `sectionDetail`)은 비어 보이고 * 쓰기는 버려진다. 프로젝트를 모르면 프로젝트 칸이 그렇다. */ -/** 가게를 안 거친 세션 되씀(되돌리기 · 다시하기) 뒤 — 프로젝트 목록 칸을 듣는 쪽을 지금 값으로 부름 - * (472-5 · 관 · 구조물 사본이 옛 값으로 남아 다음 조작이 되돌림을 덮던 흠). */ +/** 가게를 안 거친 세션 되씀(되돌리기 · 다시하기) 뒤 — 관 · 구조물 칸 듣는 쪽을 지금 값으로 한 번 부름 + * (472-5 · 사본이 옛 값으로 남아 다음 조작이 되돌림을 덮던 흠). 추가 측점 · 페이지 설정은 자기 되살림 훅이 + * 알림(`_Api_PlusStations` · `_Api_PageSettings`) · 계산 묶음은 되살림 훅 한 길(구독은 되돌리는 동안 안 받음). */ export function announceDesignStore(projectId: string): void { const store = useDesignStore(projectId, null); - (["structures", "pipes", "plusStations"] as const).forEach((key) => { + (["structures", "pipes"] as const).forEach((key) => { const id = slotOf(key, projectId, null).id; const value = store.get(key); if (id) listeners.get(`${key}|${id}`)?.forEach((fn) => fn(value)); diff --git a/resources/tester/helper_429_save_path.cjs b/resources/tester/helper_429_save_path.cjs index 94402bd31..941f5a7ed 100644 --- a/resources/tester/helper_429_save_path.cjs +++ b/resources/tester/helper_429_save_path.cjs @@ -105,6 +105,7 @@ async function run() { const feeder = loader({ "../../B05_Profile/B05_Profile_UI_Profile_History": { onPageHistory: (hooks) => ((restore = hooks.restore), () => undefined), + pageReplaying: () => false, // 묶음 먹이기가 되돌리는 동안 구독을 거름(472-5) }, "./design_save": { savingDesign: () => false }, })(path.join(DESIGN, "design_pipeline_facilities.ts")); @@ -116,7 +117,11 @@ async function run() { saved: (key) => (key === "plusStations" ? [12] : []), input: (key) => inputNow[key] ?? null, routed: () => false, - feed: (source, value) => fed.push([source, value]), + // 묶음처럼 받은 값이 입력이 됨 — 같은 값 다시 알림은 안 넣음(472-5) + feed: (source, value) => { + fed.push([source, value]); + if (source === "plus") inputNow.plusStations = value; + }, resync: async () => undefined, }); fakeStore.set("plusStations", [12, 30]); diff --git a/resources/tester/helper_472_5_shop.mjs b/resources/tester/helper_472_5_shop.mjs index 1f04eae81..59ae6b96f 100644 --- a/resources/tester/helper_472_5_shop.mjs +++ b/resources/tester/helper_472_5_shop.mjs @@ -43,19 +43,49 @@ const pipes2 = [ ]; store.set("structures", A); store.set("pipes", pipes1); +m.writePlusDraft(P, [60]); const history = m.createProfileHistory(() => undefined, 100); store.set("structures", AB); store.set("pipes", pipes2); +m.writePlusDraft(P, [60, 80]); history.record(); -const heard = { structures: [], pipes: [] }; +const heard = { structures: [], pipes: [], plusStations: [] }; store.subscribe("structures", (v) => heard.structures.push(v)); store.subscribe("pipes", (v) => heard.pipes.push(v)); +store.subscribe("plusStations", (v) => heard.plusStations.push(v)); +// 계산 묶음 먹이기 — 되돌리기 한 번에 출처마다 한 번만(되살림 훅 한 길 · 472-5 겹침 정리) +const fed = []; +const input = { pipes: pipes2, structures: AB, plusStations: [60, 80] }; +const keyOf = { pipe: "pipes", structure: "structures", plus: "plusStations" }; +m.feedFacilities({ + projectId: P, + store, + ready: () => true, + gone: () => false, + saved: () => null, + input: (key) => input[key], + routed: () => false, + feed: (source, value) => { + fed.push(source); + input[keyOf[source]] = value; + }, + resync: async () => undefined, +}); +const tick = () => new Promise((done) => setTimeout(done, 5)); history.undo(); +await tick(); const afterUndo = { structures: heard.structures.at(-1) ?? null, pipes: heard.pipes.at(-1) ?? null, }; +const counts = { + structures: heard.structures.length, + pipes: heard.pipes.length, + plusStations: heard.plusStations.length, + fed: [...fed].sort(), +}; history.redo(); +await tick(); const afterRedo = { structures: heard.structures.at(-1) ?? null, pipes: heard.pipes.at(-1) ?? null, @@ -85,6 +115,7 @@ console.log( JSON.stringify({ afterUndo, afterRedo, + counts, echo, adopted, picked, diff --git a/resources/tester/helper_472_5_shop_entry.ts b/resources/tester/helper_472_5_shop_entry.ts index f830f139a..7996e05fa 100644 --- a/resources/tester/helper_472_5_shop_entry.ts +++ b/resources/tester/helper_472_5_shop_entry.ts @@ -4,3 +4,5 @@ export { pinDraftProject, useDesignStore } from "../../A00_Common/design/design_ export { adoptEditorPipes } from "../../B05_Profile/B05_Profile_UI_Drainage_Parts"; export { createPipeEditor } from "../../B05_Profile/B05_Profile_UI_Drainage_Pipes"; export { createFacilityStore } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility"; +export { feedFacilities } from "../../A00_Common/design/design_pipeline_facilities"; +export { writePlusDraft } from "../../B05_Profile/B05_Profile_Api_PlusStations"; diff --git a/resources/tester/test_472_5_shop_owner.py b/resources/tester/test_472_5_shop_owner.py index 02ac90f81..e288c3736 100644 --- a/resources/tester/test_472_5_shop_owner.py +++ b/resources/tester/test_472_5_shop_owner.py @@ -1,8 +1,9 @@ """PLAN 472-5 5-A (요구 273) — 관 · 구조물 값 주인 하나(설계 가게) · 사본 갈림 막음. 못박는 것 - - 되돌리기 · 다시하기(세션 직접 되씀)가 가게 듣는 쪽을 되돌린 값으로 부름 — 옛 코드는 안 불러 - 종단 목록 · 횡단 판 · 배수 편집기가 옛 값을 쥐고 다음 조작이 되돌림을 덮었음(옛 코드 실패). + - 되돌리기 · 다시하기(세션 직접 되씀)가 가게 듣는 쪽을 되돌린 값으로 한 번씩 부름(5-C 훅 · 묶음과 + 안 겹침) — 옛 코드는 안 불러 종단 목록 · 횡단 판 · 배수 편집기가 옛 값을 쥐고 다음 조작이 + 되돌림을 덮었음(옛 코드 실패). - 배수 편집기 받기 — 제 쓰기 메아리는 안 받음 · 남이 고친 옵션 · 차례도 받음 · 고른 관(id) 유지. - 배선 — 종단 보기 = 관(숨어도) · 구조물(보일 때) 구독 · 횡단 보기 = 남 고침(옵션만도) 다시 실음 · 횡단 판은 쓰기 전에 제 값을 적음(메아리 가름). @@ -75,6 +76,16 @@ def test_되돌리기가_가게_듣는_쪽을_부름(out) -> None: assert out["afterRedo"]["pipes"][0]["options"] == {"diameter_mm": 1000} +def test_되돌리기_알림은_한_번씩(out) -> None: + """겹침 정리 — 관 · 구조물 = 가게 알림 한 번 · 추가 측점 = 자기 훅 · 묶음 = 출처마다 한 번.""" + assert out["counts"] == { + "structures": 1, + "pipes": 1, + "plusStations": 1, + "fed": ["pipe", "plus", "structure"], + } + + def test_배수_편집기_남_고침만_받음(out) -> None: assert out["echo"] is False assert out["adopted"] is True From 629c23ac2268b7378b141ccd112ffd3043176ed6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 10:15:49 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(472-5=20=EA=B4=80=20=ED=8F=BC):=20?= =?UTF-8?q?=EC=97=B4=EB=A6=B0=20=EA=B4=80=20=ED=8F=BC=EC=9D=B4=20=EB=B0=96?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B0=94=EB=80=90=20=EA=B0=92(=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=C2=B7=20=ED=9A=A1=EB=8B=A8=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0)=EC=9C=BC=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 흠(화면에서 찾음): ↶ 뒤 종단 「구조물 배치」 관 폼 칸이 옛 값(관경 1200)을 쥐고, 다른 칸(동물이동형)을 바꾸면 옛 관경이 초안에 되쓰여 되돌림이 사라짐 - setPipeFacilities — 고른 관 값이 바뀌면 loadPipeForm(실시간 반영 중 · 임시 배치는 둠) - 시험 test_472_5_shop_owner 배선 줄 · 화면 검증_sub2: 관경 바꿈 → ↶ 폼 1000 → 동물이동형 바꿔도 관경 1000 · 횡단 보기 관경 1500 → 종단 폼 1500 · ↶ ↷ 따라감 Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_UI_Structures_Panel.ts | 8 ++++++++ resources/tester/test_472_5_shop_owner.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 207781293..81f728bd9 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -573,8 +573,16 @@ export function createStructuresSection( }, getStructures: () => [...structures], setPipeFacilities(pipes) { + const before = pipeFacilities; pipeFacilities = pipes.map((pipe) => ({ ...pipe })); followPipeList(panelContext, pipeFacilities); + // 열린 관 폼 — 값이 밖에서 바뀌면(되돌리기 · 다른 보기) 새 값으로 다시 엶 · 고치던 실시간 반영 · 임시 배치는 + // 둠(472-5 · 옛 칸 값이 다음 조작에 되쓰이던 흠) + const at = selectedPipeChainage; + if (at === null || applyingLive || tempPipeChainage !== null) return; + const near = (pipe: PipeFacilityItem) => Math.abs(pipe.chainage_m - at) < PIPE_MATCH_M; + const now = pipeFacilities.find(near); + if (now && JSON.stringify(now) !== JSON.stringify(before.find(near))) loadPipeForm(now); }, selectPipeByChainage(chainageM) { if (chainageM !== null) callbacks.onReveal?.(); diff --git a/resources/tester/test_472_5_shop_owner.py b/resources/tester/test_472_5_shop_owner.py index e288c3736..3ed88db8b 100644 --- a/resources/tester/test_472_5_shop_owner.py +++ b/resources/tester/test_472_5_shop_owner.py @@ -116,5 +116,9 @@ def test_배선() -> None: assert panel.index("seen.structures = JSON.stringify(structures);") < panel.index( "writePendingStructures(deps.projectId, structures);" ) + # 열린 관 폼 — 밖에서 바뀐 값(되돌리기 · 다른 보기)으로 다시 엶(옛 칸 값 되쓰던 흠) + form = _read("B05_Profile", "B05_Profile_UI_Structures_Panel.ts") + assert "if (at === null || applyingLive || tempPipeChainage !== null) return;" in form + assert "JSON.stringify(now) !== JSON.stringify(before.find(near))) loadPipeForm(now);" in form history = _read("B05_Profile", "B05_Profile_UI_Profile_History.ts") assert "announceDesignStore(projectId);" in history