From 09442ae16548b9d54860739a6daf74fe5035ad71 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 10:04:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(B06):=20=EB=8B=A8=EC=9D=B4=20=EB=AA=BB?= =?UTF-8?q?=20=EC=84=B0=EB=8B=A4=EB=8A=94=20=EC=95=88=EB=82=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95=EC=B0=BD=20=EB=B0=96=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EB=9D=84=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3-7 (2026-09-07). 안내는 이미 있었으나 `activeRevet` 이 그 벽일 때만 떠서, 좌측 「구조물 배치」 폼처럼 **조정창을 안 거친 경로**로 단 수를 바꾸면 개수만 조용히 잘렸음. 사용자는 [+]를 눌러도 아무 일이 없는 것으로 보임. - 조건을 없애고 **자리(측점·측·세워진 단 수)마다 한 번만** 내도록 바꿈. 다시 그릴 때마다 뜨면 시끄럽고, 결과가 달라지면(지형·조작) 키가 달라져 다시 알림. - 요청대로 다 선 자리는 기록을 지워, 나중에 또 모자라면 다시 알림. - 문구는 기존 `B06_Cross_Extra_Limit` 그대로 — 새 문구를 만들지 않음(중복 토스트 방지). 앞서 `Station_Controls.syncCount` 에 같은 안내를 붙였다가 **기존 것과 겹쳐 두 줄로 뜨는 것**을 실화면에서 보고 되돌렸음. 안내는 이 한 자리에만 둠. tsc 통과 · 시험 508 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_Section_UI_Cross_Culvert_Wire.ts | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts index 65969bc3..27c1484b 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts @@ -152,6 +152,28 @@ export interface CulvertLink { * 옆 측점에 서야 한다). 고정 기본 10m(5/5)는 소유 벽 제원조차 없을 때의 마지막 값이다. * 손으로 한 번 잡은 단은 그 값으로 고정된다 — 계곡부·능선부에서 단마다 연장이 다르다. */ +/** 「단이 못 섰다」 안내를 이미 낸 자리 — 다시 그릴 때마다 뜨지 않게 한 번만 낸다. + * 키에 세워진 단 수를 넣어, 지형·조작이 바뀌어 결과가 달라지면 다시 알린다. */ +const extraLimitNotified = new Set(); + +/** 요청 단 수보다 적게 섰음을 알린다 — 조정창을 열지 않은 경로(좌측 폼 등)에서도 뜬다. + * 종전에는 `activeRevet` 이 그 벽일 때만 떠서, 다른 길로 단 수를 바꾸면 조용히 잘렸다 + * (2026-09-07). 문구는 종전 그대로 쓴다. */ +function noticeExtraLimit(chainageM: number, side: "outlet" | "basin", built: number): void { + const key = `${chainageM.toFixed(2)}|${side}|${built}`; + if (extraLimitNotified.has(key)) return; + extraLimitNotified.add(key); + showToast(L("B06_Cross_Extra_Limit").replace("{n}", String(built)), "info"); +} + +/** 요청대로 다 선 자리는 안내 기록을 지운다 — 다음에 또 모자라면 다시 알린다. */ +function clearExtraLimitNotice(chainageM: number, side: "outlet" | "basin"): void { + const head = `${chainageM.toFixed(2)}|${side}|`; + for (const key of [...extraLimitNotified]) { + if (key.startsWith(head)) extraLimitNotified.delete(key); + } +} + export function tierSpanOf(section: CrossSection, key: string): StructureSpan { const stored = section.design?.extra_spans?.[key]; if (stored) return { beforeM: Math.max(stored.before_m, 0), afterM: Math.max(stored.after_m, 0) }; @@ -470,24 +492,18 @@ export function computeCardCulvert( // 유입측(집수정 계류측·독립 기슭막이)도 유출측과 같은 규칙으로 알린다 — 종전에는 // 조용히 잘려서 +를 눌러도 아무 일이 없는 것처럼 보였다(2026-08-29 사용자 지적: // 우측 독립 기슭막이에서 추가2가 안 서는데 토스트가 없다). - if (activeRevet === "inlet" || activeRevet?.startsWith("bextra")) { - showToast( - L("B06_Cross_Extra_Limit").replace("{n}", String(layout.basinExtras.length)), - "info", - ); - } + noticeExtraLimit(section.chainage_m, "basin", layout.basinExtras.length); extraWalls.syncCount(section.chainage_m, layout.basinExtras.length, "basin"); + } else if (extraWalls) { + clearExtraLimitNotice(section.chainage_m, "basin"); } if (extraWalls && extraWalls.countFor(section, "outlet") > layout.extraWalls.length) { // 요청 단 수보다 지형이 허락하는 단이 적다(벽이 원지반에 0.5m 이상 묻히면 성토 // 불필요 — 그 아래 단은 못 세운다). 조작 중일 때만 가능한 단 수를 토스트로 알린다. - if (activeRevet === "outlet" || activeRevet?.startsWith("extra")) { - showToast( - L("B06_Cross_Extra_Limit").replace("{n}", String(layout.extraWalls.length)), - "info", - ); - } + noticeExtraLimit(section.chainage_m, "outlet", layout.extraWalls.length); extraWalls.syncCount(section.chainage_m, layout.extraWalls.length, "outlet"); + } else if (extraWalls) { + clearExtraLimitNotice(section.chainage_m, "outlet"); } if (revetOffset) { const roles: Array<[RevetKey, WallAdjust]> = [ From 8f4c486a00085aea7d2eef3c390170c3344a7676 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 10:50:36 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(=EA=B0=9C=EB=B0=9C=ED=99=98=EA=B2=BD):?= =?UTF-8?q?=20vite=20=ED=94=84=EB=A1=9D=EC=8B=9C=20=EB=8C=80=EC=83=81?= =?UTF-8?q?=EC=9D=84=20127.0.0.1=20=EB=A1=9C=20=EB=B0=95=EC=9D=8C=20?= =?UTF-8?q?=E2=80=94=20/api=20=EA=B0=80=20IPv6=20=EB=A1=9C=20=ED=97=9B?= =?UTF-8?q?=EC=A7=9A=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 계획서 7-1. 화면의 **모든 `/api` 요청이 「Failed to fetch」** 가 되는데 서버는 멀쩡한 증상이 하루 종일 있었음. 원인은 프록시 대상이 `http://localhost:<포트>` 인 것 — Node 18+ 는 `localhost` 를 **IPv6(::1) 먼저** 물고, 백엔드는 `SERVER_HOST=0.0.0.0` (IPv4 전용)으로 떠서 서로 못 만남. 실측 — `127.0.0.1:8001` 200 · `[::1]:8001` 연결 실패. 고친 뒤(vite 재기동 포함) 화면에서 `/api/health` **200**, 로그인 POST **200**. 오늘 겪은 「저장 Failed to fetch」 두 번과 「화면 백지」 세 번이 이것으로 설명됨 — 부팅 때 API 가 죽으면 앱이 아무것도 안 그리고 콘솔 오류도 안 남았음. 공용 파일이라 배분 창(claude-b1) 승인을 받고 고침. 5173·5174 양쪽에 같이 적용됨. Co-Authored-By: Claude Opus 5 (1M context) --- vite.config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 2dbf28fe..c7e98dd9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,12 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); -const apiTarget = `http://localhost:${process.env.AISLO_API_PORT ?? "8000"}`; +// ⚠ `localhost` 로 두지 말 것 — Node 18+ 는 그것을 **IPv6(::1) 먼저** 물고, 백엔드는 +// `SERVER_HOST=0.0.0.0`(IPv4 전용)로 뜬다. 그러면 프록시가 `[::1]:<포트>` 로 붙으려다 +// 실패해 화면의 **모든 `/api` 요청이 「Failed to fetch」** 가 된다 — 서버는 멀쩡한데 +// 저장이 실패하고, 부팅 때 죽으면 화면이 통째로 백지가 된다(2026-09-07 실측: +// `127.0.0.1:8001` 200 · `[::1]:8001` 연결 실패). 숫자로 박아 그 갈림을 없앤다. +const apiTarget = `http://127.0.0.1:${process.env.AISLO_API_PORT ?? "8000"}`; /** * Vite 설정 — 임도 설계 웹앱 (Vanilla TS + Three.js)