From 5f5e91d65594c79dff0d62fbeb2ef4b1432f8b65 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 09:25:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EB=B0=B0=EC=88=98=EA=B4=80=20?= =?UTF-8?q?=EC=97=B0=EC=9E=A5(m)=EC=9D=84=20=EC=A0=95=EB=B3=B8=EC=97=90=20?= =?UTF-8?q?=EC=8B=A4=EC=9D=8C=20=E2=80=94=20=EC=88=98=EB=9F=89=EC=9D=B4=20?= =?UTF-8?q?=EA=B4=80=20=EA=B3=B5=EC=A2=85=EC=9D=84=20=EC=85=80=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B08 창 보고(2026-09-08): 관 11개가 `pipe_points.json` 에 있고 품셈 공종도 있는데 **연장(m)이 없어** 배수관 물량이 막혀 있었음. 관 길이는 횡단 기하가 **m 단위 올림까지** 끝낸 값 (`CulvertLayout.pipe.lengthM`)인데 **정본(`design`)에 안 실렸음**. **옮기거나 새로 짜지 않았음** — 그 계산은 **이미 서버가 Node 로 그대로 실행**하고 있었음 (`B06_Section_Server_Calc_Node` ← `Server_Calc_Prebuild.recompute_server_side`, [저장]·[확정]). 구조물 폐회로 면적이 실리는 **그 다리에 값 한 줄만** 더 실었음 — 계산을 두 벌로 짜지 않음 (CLAUDE.md 5장). B08 창이 낸 갈래 둘(서버로 이관 / 저장 때 남기기)은 둘 다 불필요했음. - `STRUCTURE_AREA_KEYS` → **`STRUCTURE_ROW_KEYS`** 로 이름을 고치고 `pipe_length_m` 추가. 이름이 「면적」이라 길이를 담으면 거짓말이 됨. - **면적을 못 내는 측점에서도 길이는 냄** — 설계선이 모자라 폐회로가 안 닫히는 자리에도 관은 서 있고 수량은 그 길이가 필요함(`return pipeRow`). - ⚠ **서버에 화이트리스트가 하나 더 있었음** — `_AREA_KEYS`(「Node 가 다른 키를 내도 안 흘림」). 거기 안 넣으면 Node 가 값을 내도 **조용히 버려짐**. 넣고, **두 목록을 대조하는 시험**을 둠. - 키 이름은 `pipe_length_m` 으로 **B08 창과 미리 못 박음** — 오늘 `back_len_cm` 이 엔진에서 `stone_back_length_cm` 로 읽혀 저장값이 영영 안 닿던 사고가 있었음. ⚠ 값이 정본에 실리려면 그 프로젝트에서 **[저장] 한 번**이 필요하고, **번들 재빌드** (`npm run build:server-calc`)도 필요함 — 번들은 git 밖이라 창마다 각자 돌려야 함. 시험 6건 — 패치로 받는지 · 음수 거부 · 면적과 **같은 목록**으로 실리는지 · 면적 실패 시에도 길이는 나오는지 · 이름을 두 번 적지 않는지 · **서버 화이트리스트와 TS 목록이 짝인지**. 전체 507 passed · 17 skipped. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Api_Types.ts | 4 +++ B06_Section/B06_Section_Schema.py | 4 +++ .../B06_Section_Server_Calc_Prebuild.py | 10 ++++++- B06_Section/B06_Section_Structure_Layouts.ts | 26 +++++++++++++++---- B06_Section/B06_Section_UI_Page_Persist.ts | 4 +-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index bbfe1d64..d5f83b05 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -467,6 +467,8 @@ export interface CrossDesign { /** 측점별 **암 절토 경사비**(1:n 의 n) — 사용자가 카드에 넣은 값(2026-09-07). * 0 은 「표준값을 씀」이다. 계산에 쓰이는 값이 아니라 **입력을 되싣는 자리**다. */ cut_slope_ratio_user?: number; + /** 배수관 연장(m) — 기하가 **m 단위 올림까지** 끝낸 값(2026-09-08). 수량(B08)이 읽는다. */ + pipe_length_m?: number; } export interface CrossDesignResponse { @@ -504,6 +506,8 @@ export interface CrossSectionPatch { display_half_width_m?: number; /** 측점별 암 절토 경사비(2026-09-07). 0 = 표준값으로 되돌림. */ cut_slope_ratio_user?: number; + /** 배수관 연장(m) — 구조물 면적과 같은 길로 정본에 실린다(2026-09-08). */ + pipe_length_m?: number; inlet_structure?: "auto" | "revet" | "I" | "L" | "U"; basin_adjust?: { innerWidthM: number; diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index 8cf4525f..3cbfc1b2 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -175,6 +175,10 @@ class CrossSectionPatch(BaseModel): fill_area_m2: float | None = Field(default=None, ge=0) cut_soil_area_m2: float | None = Field(default=None, ge=0) cut_rock_area_m2: float | None = Field(default=None, ge=0) + # 배수관 연장(m) — 브라우저 기하가 **m 단위 올림까지** 끝낸 값. 수량(B08)이 배수관 + # 공종을 세려면 정본에 있어야 한다(2026-09-08). 계산은 서버가 같은 코드를 Node 로 + # 돌려 내므로 한 벌이다(`B06_Section_Server_Calc_Node`). + pipe_length_m: float | None = Field(default=None, ge=0) class SectionConfirmRequest(BaseModel): diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index fb9d7fb4..399ecd57 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -52,7 +52,15 @@ ROOT = Path(__file__).resolve().parents[1] BUNDLE = ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js" _NPM_SCRIPT = "build:server-calc" # 정본에 얹는 값만 받는다 — Node 가 다른 키를 내도 설계 데이터에 흘리지 않는다. -_AREA_KEYS = ("cut_area_m2", "fill_area_m2", "cut_soil_area_m2", "cut_rock_area_m2") +# ⚠ **TS 쪽 `STRUCTURE_ROW_KEYS` 와 짝이다.** 한쪽만 늘리면 Node 가 값을 내도 여기서 조용히 +# 버려진다(2026-09-08 관 길이를 더하며 실제로 걸린 자리). 시험이 두 목록을 대조한다. +_AREA_KEYS = ( + "cut_area_m2", + "fill_area_m2", + "cut_soil_area_m2", + "cut_rock_area_m2", + "pipe_length_m", +) def _mass_haul_context() -> dict[str, Any]: diff --git a/B06_Section/B06_Section_Structure_Layouts.ts b/B06_Section/B06_Section_Structure_Layouts.ts index ec4b2613..57ba09c3 100644 --- a/B06_Section/B06_Section_Structure_Layouts.ts +++ b/B06_Section/B06_Section_Structure_Layouts.ts @@ -107,12 +107,20 @@ export function trimOfLayouts(layouts: StoredLayouts) { ); } -/** 정본에 얹는 면적 키 — 이 넷만 오간다. */ -export const STRUCTURE_AREA_KEYS = [ +/** + * 정본에 얹는 키 — 구조물이 선 측점에서만 나오는 값들. + * + * 면적 넷에 **관 길이**를 더했다(2026-09-08). 관 길이는 브라우저 기하가 **m 단위 올림까지** + * 끝낸 값인데 정본에 없어 **수량이 배수관 연장을 못 냈다**(B08 창 보고). 계산을 서버로 + * 옮기거나 새로 짜지 않고, **이미 서버가 Node 로 돌리는 이 다리에 한 줄 더 실었다** + * (CLAUDE.md 5장 — 계산은 한 벌). + */ +export const STRUCTURE_ROW_KEYS = [ "cut_area_m2", "fill_area_m2", "cut_soil_area_m2", "cut_rock_area_m2", + "pipe_length_m", ] as const; /** 측점 하나의 폐회로 면적 — 구조물이 없으면 null(표준 계산값이 이미 맞다). */ @@ -122,9 +130,16 @@ function areaRowOf( ): Record | null { const layouts = computeStoredLayouts(section, sections); if (!layouts) return null; + // 관 길이는 면적과 **따로** 낸다 — 폐회로 면적을 못 내는 측점(설계선이 모자란 자리)에도 + // 관은 서 있고, 수량은 그 길이를 필요로 한다(2026-09-08). + const pipeLengthM = layouts.culvert?.pipe?.lengthM; + const pipeRow: Record | null = + typeof pipeLengthM === "number" && pipeLengthM > 0 + ? { chainage_m: section.chainage_m, pipe_length_m: Number(pipeLengthM.toFixed(4)) } + : null; const trim = trimOfLayouts(layouts); const design = layouts.design; - if (!trim || !Array.isArray(design.design_line) || design.design_line.length < 2) return null; + if (!trim || !Array.isArray(design.design_line) || design.design_line.length < 2) return pipeRow; const ground = section.samples .filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number") .map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number })) @@ -136,9 +151,10 @@ function areaRowOf( rockBoundaryOffsetM: typeof design.rock_boundary_offset_m === "number" ? design.rock_boundary_offset_m : null, }); - if (!areas) return null; + if (!areas) return pipeRow; const round = (value: number): number => Number(value.toFixed(4)); const row: Record = { + ...(pipeRow ?? {}), chainage_m: section.chainage_m, cut_area_m2: round(areas.cutAreaM2), fill_area_m2: round(areas.fillAreaM2), @@ -173,7 +189,7 @@ export function applyStructureAreaRows( const design = sections.find((item) => item.chainage_m === row.chainage_m)?.design as Record | undefined; if (!design) continue; - for (const key of STRUCTURE_AREA_KEYS) { + for (const key of STRUCTURE_ROW_KEYS) { if (typeof row[key] === "number") design[key] = row[key]; } } diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index 9c7c0f9a..e45c50c3 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -29,7 +29,7 @@ import { } from "@util/common_util_cross_berm"; import { applyStructureAreaRows, - STRUCTURE_AREA_KEYS, + STRUCTURE_ROW_KEYS, structureAreaRows, } from "./B06_Section_Structure_Layouts"; import { crossDesignChoices } from "./B06_Section_Cross_Design_Session"; @@ -393,7 +393,7 @@ export function collectSectionEdits(ctx: SectionPersistContext): { const design = section.design as Record | undefined; if (!design) continue; let patch: CrossSectionPatch | null = null; - for (const key of STRUCTURE_AREA_KEYS) { + for (const key of STRUCTURE_ROW_KEYS) { if (typeof design[key] !== "number") continue; patch = patch ?? patchFor(section.chainage_m); patch[key] = design[key] as number;