From 710169fd00d7065c65f0225dc0a4efbc85ee47d3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 01:09:58 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(B05):=20[3D=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8]=EA=B0=80=20=EB=A7=8C=EB=93=A0=20=EC=BD=94?= =?UTF-8?q?=EB=A6=AC=EB=8F=84=EB=A5=BC=20=EB=B0=94=EB=A1=9C=20=EC=98=AC?= =?UTF-8?q?=EB=A6=AC=EA=B3=A0=20=EB=B3=B4=EA=B4=80=ED=95=A8=EC=97=90?= =?UTF-8?q?=EB=8F=84=20=EB=8B=B4=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보조 창 지적 — [3D 업데이트]를 눌러도 브라우저가 만들기만 하고 PUT 이 없었음. 저장 시점이 B05->B06 이동뿐이라(2026-08-23 확정), 누른 뒤 새로고침하면 저장본이 낡은 채라 다음 사람이 또 만들어야 했음. - 버튼 경로 끝에서 saveCorridorIfDirty 를 부름. - saveCorridorIfDirty 가 PUT 성공 뒤 그 봉투를 브라우저 보관함에도 담음(옛 열쇠 자리는 치움). 그러지 않으면 다음 진입이 새 열쇠 주소로 16.9MB 를 한 번 더 받았음. typecheck 통과. 실측 확인은 보조 창이 자기 프로젝트에서(저장본이 최신인 자리) 진행. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Corridor.ts | 12 ++++++++++-- B05_Profile/B05_Profile_UI_Page.ts | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Corridor.ts b/B05_Profile/B05_Profile_UI_Corridor.ts index 58864d84..7ce3ea85 100644 --- a/B05_Profile/B05_Profile_UI_Corridor.ts +++ b/B05_Profile/B05_Profile_UI_Corridor.ts @@ -224,8 +224,16 @@ function markDirty(projectId: string, routeId: number): void { export async function saveCorridorIfDirty(projectId: string, routeId: number): Promise { const entry = cache.get(keyOf(projectId, routeId)); if (!entry || !entry.dirty) return; - const ok = await putStored(projectId, routeId, serialize(entry.build, entry.hash)); - if (ok) entry.dirty = false; + const envelope = serialize(entry.build, entry.hash); + const ok = await putStored(projectId, routeId, envelope); + if (!ok) return; + entry.dirty = false; + // 방금 올린 것을 **보관함에도** 담는다(2026-09-06) — 그러지 않으면 다음 진입이 열쇠가 + // 바뀐 주소로 한 번 더 받아 온다. 담아 두면 네트워크 없이 선다. + const url = `${corridorUrlPrefix(projectId, routeId)}?hash=${encodeURIComponent(entry.hash)}`; + const bytes = new TextEncoder().encode(JSON.stringify(envelope)).buffer as ArrayBuffer; + await purgeAssetsWithPrefix(projectId, corridorUrlPrefix(projectId, routeId)); + await writeCachedBytes(projectId, url, bytes); } /** Page 훅 — 현재 종횡단 정본 그대로 코리도를 확보해 뷰어에 반영(실패 시 제거). diff --git a/B05_Profile/B05_Profile_UI_Page.ts b/B05_Profile/B05_Profile_UI_Page.ts index a841c47a..bdc2df3b 100644 --- a/B05_Profile/B05_Profile_UI_Page.ts +++ b/B05_Profile/B05_Profile_UI_Page.ts @@ -322,6 +322,10 @@ export async function renderB05Route(root: HTMLElement): Promise { ); // 측점 바·라벨·램프도 계획고를 따라 움직여야 한다(2026-08-23 지적 ③). renderStationLines(currentSectionDetail); + // 만든 것을 **바로 올려 둔다**(2026-09-06) — 예전에는 B05→B06 이동 때만 올려서, + // 누른 뒤 새로고침하면 저장본이 낡은 채라 다음 사람이 또 만들어야 했다. + // 브라우저 보관함이 생긴 지금은 올려 두면 다음 진입이 네트워크 없이 선다. + void saveCorridorIfDirty(activeProjectId, latest.route.id); }, onMovePoint: viewer.beginMoveSelected, onDeletePoint: viewer.markers.deleteSelected, From fe14ad02ee71a375edc37d961ec96cb04be9e035 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 01:19:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(B05):=20=EA=B3=84=ED=9A=8D=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EC=B4=88=EA=B8=B0=20=ED=8F=B4=EB=A6=AC=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=9E=90=EB=8F=99=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EC=B2=B4=EC=9D=B8=EC=97=90=EC=84=9C=20=EC=84=B8=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN 0-10. 그동안은 화면이 route/plan 을 처음 부를 때 만들어져, 화면을 안 열면 값이 없었음 — 초기값은 서버가 낸다는 0-4 원칙과 어긋났음. - 파일입력 자동설계 체인이 예상노선 정본을 남긴 직후(2.6단계) 초기 폴리라인도 세움. 실패는 비치명적 — 없으면 화면이 처음 열릴 때 만들어짐. - _ensure_planned_initial 이 **낡음도 본다** — 예상노선 파일이 더 나중에 쓰였으면 다시 만듦. 파일을 다시 올리면 예상노선이 새로 깔리는데 초기본이 옛 노선인 채로 남으면 노선 초기화가 옛 자리로 돌아갔음. 자체검증 — 예상노선 파일 시각을 최신으로 만든 뒤 route/plan 을 부르니 초기 폴리라인이 다시 만들어짐(노드 28 · 곡선 13 그대로). 시험 409 통과·17 건너뜀. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_Service_Chain.py | 30 ++++++++++++++++++++ B05_Profile/B05_Profile_Router_Replan.py | 13 +++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index 618764d9..ec0b0431 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -205,6 +205,12 @@ async def run_auto_design_chain( if not expected_path.is_file(): write_route_csv(expected_path, points) + # 2.6) 계획노선 **초기 폴리라인**도 여기서 세운다(2026-09-06 PLAN 0-10). + # 예상노선은 점 묶음이라 그대로는 설계선이 못 된다 — 지식DB 의 R 기준으로 + # 곡선을 끼운 폴리라인이 불변 초기 데이터다. 화면이 처음 열릴 때 만들면 + # 「화면을 안 열면 값이 없다」가 되므로 초기값은 서버가 낸다(0-4 원칙). + await _ensure_initial_polyline(project_id, project_root, points) + # 3) B05 경로 계산 request = RouteSolveRequest( filter_key=str(defaults["source_filter"]), @@ -327,6 +333,30 @@ async def run_auto_design_chain( clear_designing(project_root) +async def _ensure_initial_polyline( + project_id: UUID, project_root: Path, points: list[dict[str, float]] +) -> None: + """계획노선 초기 폴리라인을 세운다 — 이미 있으면 그대로 둔다. 실패는 비치명적.""" + import asyncio + + from B05_Profile.B05_Profile_Router_Replan import _ensure_planned_initial, _min_plan_radius_m + + try: + radius_m = await _min_plan_radius_m(project_id) + summary = await asyncio.to_thread(_ensure_planned_initial, project_root, radius_m) + if summary: + logger.info( + "초기 계획노선 폴리라인: project_id=%s 노드 %d · 곡선 %d · 위반 %d (R %.1fm)", + project_id, + summary["nodes"], + summary["curves"], + summary["violations"], + radius_m, + ) + except Exception: # noqa: BLE001 — 없으면 화면이 처음 열릴 때 만든다 + logger.exception("초기 계획노선 폴리라인 생성 실패(계속 진행): %s", project_id) + + async def run_redesign_chain( project_id: UUID, surface_model_id: int, diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 0af08e06..8c68c3a4 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -175,11 +175,18 @@ def _write_planned_polyline(path: Path, points: list[tuple[float, float]], radiu def _ensure_planned_initial(project_root: Path, radius_m: float) -> dict | None: - """계획노선 **초기 폴리라인**이 없으면 예상노선을 폴리라인화해 세운다.""" + """계획노선 **초기 폴리라인**이 없거나 낡았으면 예상노선을 폴리라인화해 세운다. + + 「낡았다」 = 예상노선 파일이 더 나중에 쓰였다. 파일을 다시 올리면 예상노선이 새로 + 깔리는데 초기본이 옛 노선인 채로 남으면 노선 초기화가 옛 자리로 돌아간다. + """ target = planned_route_initial_path(project_root) + source = expected_route_csv_path(project_root) if target.is_file(): - return None - points = [(x, y) for x, y in _vertices_of(expected_route_csv_path(project_root))] + if not source.is_file() or source.stat().st_mtime <= target.stat().st_mtime: + return None + logger.info("예상노선이 새로 깔려 초기 폴리라인을 다시 만듭니다: %s", target) + points = [(x, y) for x, y in _vertices_of(source)] if len(points) < 2: return None summary = _write_planned_polyline(target, points, radius_m) From 71694163de3a8816cfc4aa8654ac81c48fe1a5bc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 01:37:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(B06):=20=EB=B2=BD=EC=9D=84=20=EC=95=88?= =?UTF-8?q?=20=EA=B7=B8=EB=A6=AC=EB=8A=94=20=EC=B9=B4=EB=93=9C=EC=97=90=20?= =?UTF-8?q?[=EC=97=B0=EB=8F=99]=20=EB=B2=84=ED=8A=BC=EC=9D=B4=20=EB=9C=A8?= =?UTF-8?q?=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 링크 카드는 소유 측점의 벽을 빌려 그리는데, 빌릴 벽이 없는 카드에도 버튼이 떠 눌러도 아무 일이 없었음 — revetlink 에 detached 만 쌓였음(보조 창 실측: 0·20·40·60·80m 카드). linkState 가 drawnWallKeys().length 도 함께 보게 함. 계획서 3-4 실화면 확인도 함께 마침(공용 브라우저, 용화): - 벽이 있고 연동이 켜진 카드 820.00m 에서 [연동]을 끄자 revetx 에 820.00:outlet h=1.8 이 적힘 — 푸는 순간의 높이가 그 측점 값으로 굳음. - 그 뒤 소유 측점 821.62m 높이를 4.2m 로 올려도 820.00m 은 1.8m 그대로 — 따로 놂. ※ 앞서 안 굳는 것으로 본 것은 측정 오류였음. 패널이 카드마다 하나씩 67개인데 첫 번째 패널만 봤고, 벽이 없는 카드를 골랐음(보조 창이 가려냄). 시험 409 통과·17 건너뜀, typecheck 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Cross_View_Structure.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/B06_Section/B06_Section_UI_Cross_View_Structure.ts b/B06_Section/B06_Section_UI_Cross_View_Structure.ts index d00ff419..bdfbe4af 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Structure.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Structure.ts @@ -186,8 +186,13 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe const role = spanRoleOf(key, ctx.inletIsBasin()); if (role) ctx.structureSpan?.update(ctx.section, role, patch); }, + // 벽을 **그리지 않는 카드에는 [연동]을 내지 않는다**(2026-09-06). 링크 카드는 소유 + // 측점의 벽을 빌려 그리는데, 빌릴 벽이 없는 카드에도 버튼이 떠 눌러도 아무 일이 + // 없었다 — `revetlink` 에 `detached` 만 쌓였다(보조 창 실측: 0·20·40·60·80m 카드). linkState: () => - ctx.isLinked ? { linked: ctx.revetLink?.linkedFor(ctx.section) ?? true } : null, + ctx.isLinked && ctx.drawnWallKeys().length + ? { linked: ctx.revetLink?.linkedFor(ctx.section) ?? true } + : null, setLinked: (linked) => { // 연동을 **푸는 순간** 지금 그려진 높이를 이 측점 값으로 굳힌다(2026-09-06 사용자 // 지시). 예전에는 위치(4축)만 갈리고 높이는 소유 측점 값을 계속 따라가, 소유