feat(B06): 배수관 연장(m)을 정본에 실음 — 수량이 관 공종을 셀 수 있게
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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<string, number> | null {
|
||||
const layouts = computeStoredLayouts(section, sections);
|
||||
if (!layouts) return null;
|
||||
// 관 길이는 면적과 **따로** 낸다 — 폐회로 면적을 못 내는 측점(설계선이 모자란 자리)에도
|
||||
// 관은 서 있고, 수량은 그 길이를 필요로 한다(2026-09-08).
|
||||
const pipeLengthM = layouts.culvert?.pipe?.lengthM;
|
||||
const pipeRow: Record<string, number> | 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<string, number> = {
|
||||
...(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<string, unknown> | 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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, unknown> | 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;
|
||||
|
||||
Reference in New Issue
Block a user