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;