merge: main_laptop_1 → sub_laptop_7 52-3 — 충돌 다섯 = 양쪽 다 살림
- 명세 목록 = 박스암거 · 세월교 · 물넘이 포장 · 노면공 · 기타 전부 - B05 시설 폼 = BOX 명세 폼(메인) + 물넘이 포장 명세 폼(52-3) · 옛 BOX 프리셋 · 월류 칸 뺌 - 병합 키 = BOX 명세 키 + 물넘이 포장 경사각도 · 도수로/돌붙임 머리 - 표 채우기 컨테이너 = 물넘이 포장 도수로/돌붙임 + 기타 상세 내용 - 구조물정의 = 양쪽 그룹(box_culvert · ford_pavement) · 판 12 · 직전 메인 판 11 은 이력 판_11 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/* =============================================================================
|
||||
* design_structure_box.ts
|
||||
* 횡단배수 › 박스암거 명세 — 사용자 구조물 표 02 고친 판(`docs/raw/구조물정의_표/02_횡단배수_박스암거.md`) 그대로.
|
||||
*
|
||||
* 컨테이너 셋(모두 필수): 본체 · 유입 구조 · 유출 구조. 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
|
||||
* 저장 키 = pipe_points.json 옛 키 그대로(B06 횡단 · B07 · B08 이 읽는 자리):
|
||||
* · 본체 = `body_width_m` · `body_height_m` 수 — 규격 칸은 저장 안 함(폭 · 높이로 되찾음 · `bodyOf`)
|
||||
* · 유입 · 유출 구조 = `wing_in` · `wing_out`(없음 · 날개벽 · 흙막이 · 옛 「있음」 = 날개벽)
|
||||
* — 엔진은 날개벽만 세움(`_wing_spec` ↔ `wingSpec` 짝)
|
||||
* 표 02 에 없는 옛 날개벽 치수 칸은 화면에서 빼고 값은 남김(지난 정의 값).
|
||||
* import 없음(꼴만) — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Instance, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
|
||||
export const BOX_CUSTOM = "사용자 입력";
|
||||
/** 본체 규격 → [가로, 세로](m) — 실무 관측 규격(2026-08-17 사용자 확정). */
|
||||
const BOX_PRESETS: Record<string, readonly [number, number]> = {
|
||||
"2.0×2.0": [2, 2],
|
||||
"3.0×3.0": [3, 3],
|
||||
};
|
||||
|
||||
/** 유입 · 유출 구조 — 같은 선택지 · 키만 다름. */
|
||||
const structureItem = (key: string): SpecItem => ({
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
key,
|
||||
choices: [
|
||||
{ value: "없음", label: "없음" },
|
||||
{ value: "날개벽", label: "날개벽" },
|
||||
{ value: "흙막이", label: "흙막이" },
|
||||
],
|
||||
});
|
||||
|
||||
/** 사용자 입력일 때만 서는 숫자 칸. */
|
||||
const customSide = (id: string, label: string, key: string): SpecItem => ({
|
||||
id,
|
||||
label,
|
||||
kind: "number",
|
||||
step: 0.1,
|
||||
by: { item: "size", map: { [BOX_CUSTOM]: { key } } },
|
||||
});
|
||||
|
||||
export const BOX_SPEC: StructureSpec = {
|
||||
typeId: "box_culvert",
|
||||
containers: [
|
||||
{
|
||||
id: "body",
|
||||
title: "본체",
|
||||
required: true,
|
||||
link: "gboxbody",
|
||||
items: [
|
||||
{
|
||||
id: "size",
|
||||
label: "본체 규격",
|
||||
kind: "select",
|
||||
choices: [
|
||||
...Object.keys(BOX_PRESETS).map((value) => ({
|
||||
value,
|
||||
label: value.replace("×", " × "),
|
||||
})),
|
||||
{ value: BOX_CUSTOM, label: BOX_CUSTOM },
|
||||
],
|
||||
},
|
||||
customSide("width", "가로 (m)", "body_width_m"),
|
||||
customSide("height", "세로 (m)", "body_height_m"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "inlet",
|
||||
title: "유입 구조",
|
||||
required: true,
|
||||
link: "gboxin",
|
||||
items: [structureItem("wing_in")],
|
||||
},
|
||||
{
|
||||
id: "outlet",
|
||||
title: "유출 구조",
|
||||
required: true,
|
||||
link: "gboxout",
|
||||
items: [structureItem("wing_out")],
|
||||
},
|
||||
],
|
||||
// 표 02 에 없는 옛 날개벽 치수 — 화면에서 빼고 값은 남김(지난 정의 값)
|
||||
retired: ["in", "out"].flatMap((side) =>
|
||||
["height_m", "length_m", "angle_deg"].map((field) => `wing_${side}_${field}`),
|
||||
),
|
||||
};
|
||||
|
||||
/** 옛 저장 → 폼에 줄 옵션 — 설치 「있음」 = 날개벽(같은 뜻 · 저장하면 새 값으로 굳음). */
|
||||
export function boxOptionsIn(
|
||||
options: Record<string, string | number>,
|
||||
): Record<string, string | number> {
|
||||
const out = { ...options };
|
||||
for (const key of ["wing_in", "wing_out"]) if (out[key] === "있음") out[key] = "날개벽";
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 저장 폭 · 높이 → 본체 칸 값 — 규격과 맞으면 그 규격 · 아니면 사용자 입력 · 없으면 빈 값. */
|
||||
export function bodyOf(options: Record<string, string | number>): Instance {
|
||||
const width = Number(options.body_width_m);
|
||||
const height = Number(options.body_height_m);
|
||||
if (!(width > 0 && height > 0)) return { size: "" };
|
||||
const preset = Object.entries(BOX_PRESETS).find(
|
||||
([, [w, h]]) => Math.abs(w - width) < 0.05 && Math.abs(h - height) < 0.05,
|
||||
);
|
||||
return preset
|
||||
? { size: preset[0] }
|
||||
: { size: BOX_CUSTOM, width: String(width), height: String(height) };
|
||||
}
|
||||
|
||||
/** 본체 칸 값 → 저장 폭 · 높이 — 규격이면 규격 수(사용자 입력은 명세 키가 적음). */
|
||||
export function bodyOptions(body: Instance | undefined): Record<string, number> {
|
||||
const preset = BOX_PRESETS[body?.size ?? ""];
|
||||
return preset ? { body_width_m: preset[0], body_height_m: preset[1] } : {};
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* 구조물 명세(`design_structure_spec`) 그리기 — B05 · B06 「구조물 배치」 칸이 같은 노드를 씀.
|
||||
*
|
||||
* · 필수 컨테이너 = 늘 보임(한 벌) · 선택 컨테이너 = [추가] 로 인스턴스를 늘리고 인스턴스마다 [빼기]
|
||||
* · 항목: 고르기(상위 값에 따른 선택지 · 저장 키) · 숫자(맞물림 = 길이 · 전 · 후) · 자동(읽기만)
|
||||
* · 항목: 고르기(상위 값에 따른 선택지 · 저장 키) · 숫자(맞물림 = 길이 · 전 · 후) · 글(손 입력) · 자동(읽기만)
|
||||
* · 인스턴스마다 「구조물 조합」 잇기 줄(`buildSummaryRow` · 고르기 모달 = 패널의 그룹 [고르기] 길 그대로) —
|
||||
* 값은 패널 `group_items` 에 `linkOf` 키로 둠(빼면 뒤 인스턴스 잇기를 한 칸씩 당김)
|
||||
* · 값은 폼이 쥠 — 저장은 패널이 `read()` 로 읽어 정본 자리(옵션)에 씀
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
linkOf,
|
||||
readState,
|
||||
solveSplit,
|
||||
TEXT_MAX,
|
||||
writeOptions,
|
||||
type SpecContainer,
|
||||
type SpecItem,
|
||||
@@ -111,6 +112,13 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
select.value = now;
|
||||
return select;
|
||||
}
|
||||
if (item.kind === "text") {
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.maxLength = TEXT_MAX;
|
||||
input.value = values[item.id] ?? "";
|
||||
return input;
|
||||
}
|
||||
const input = numberInput(String(item.step ?? 0.1), "0", hooks.hint?.(c, item, state) ?? "");
|
||||
if (item.kind === "auto") {
|
||||
input.type = "text";
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
import { BOX_SPEC } from "./design_structure_box";
|
||||
import { FORD_SPEC } from "./design_structure_ford";
|
||||
import { FORD_PAVEMENT_SPEC } from "./design_structure_ford_pavement";
|
||||
import { PAVEMENT_SPEC } from "./design_structure_road";
|
||||
import { ETC_SPEC, PAVEMENT_SPEC } from "./design_structure_road";
|
||||
|
||||
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
|
||||
const BASIN = "집수정";
|
||||
@@ -189,7 +190,9 @@ export const PIPE_SPEC: StructureSpec = {
|
||||
/** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */
|
||||
export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
|
||||
pipe: PIPE_SPEC,
|
||||
box_culvert: BOX_SPEC,
|
||||
ford_bridge: FORD_SPEC,
|
||||
ford_pavement: FORD_PAVEMENT_SPEC,
|
||||
pavement_concrete: PAVEMENT_SPEC,
|
||||
etc: ETC_SPEC,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* 기준 측점(패널 위치 칸) + 필수 컨테이너 「상세 내용」 한 벌 = 포장사양 고르기 · 높이(m) 손 입력.
|
||||
* 구간 길이 · 전 · 후는 정의 옵션 칸 그대로(구간 배치) · 표에 없는 옛 상세 칸은 화면에서 빼고 값은 남김.
|
||||
* 저장 = structures.json `options` — 새 키 `pave_spec` · `height_m`.
|
||||
* 기타(52-14) = 기준 측점 + 상세 종류 이름(정의 옵션 칸 `name`) + 선택 컨테이너 「상세 내용」(비고 글) ·
|
||||
* 저장 키 `etc_detail_{n}_note` · 잇기 `getcdetail{n}`.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -43,3 +45,18 @@ export const PAVEMENT_SPEC: StructureSpec = {
|
||||
"wire_mesh",
|
||||
],
|
||||
};
|
||||
|
||||
export const ETC_SPEC: StructureSpec = {
|
||||
typeId: "etc",
|
||||
containers: [
|
||||
{
|
||||
id: "detail",
|
||||
title: "상세 내용",
|
||||
required: false,
|
||||
prefix: "etc_detail",
|
||||
link: "getcdetail",
|
||||
items: [{ id: "note", label: "비고", kind: "text" }],
|
||||
},
|
||||
],
|
||||
retired: [],
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*
|
||||
* · 종류 = 기본 옵션(기준 측점 No + 거리 · 패널 위치 칸이 받음) + 컨테이너[] + 항목[]
|
||||
* · 컨테이너 = 제목 · 필수(늘 한 벌) / 선택([추가] · [빼기] · 여러 개)
|
||||
* · 항목 꼴 = 고르기 · 숫자 · 자동(계산이 채움 · 저장 안 함) · 맞물림(길이 = 전 + 후)
|
||||
* · 항목 꼴 = 고르기 · 숫자 · 글(손 입력 · 길이 상한 `TEXT_MAX`) · 자동(계산이 채움 · 저장 안 함) ·
|
||||
* 맞물림(길이 = 전 + 후)
|
||||
* · 상위 값에 따른 선택지 · 저장 키 = `by`(같은 컨테이너 안 상위 항목 값 → 갈래)
|
||||
* · 저장 = 정본 자리 옵션 그대로(관이면 pipe_points.json `options`) — 필수 컨테이너는 항목 `key` ·
|
||||
* 선택 컨테이너는 인스턴스마다 `{prefix}_{n}_{항목}`(n 은 1부터)
|
||||
@@ -31,7 +32,7 @@ export type SplitRole = "length" | "before" | "after";
|
||||
export interface SpecItem {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: "select" | "number" | "auto";
|
||||
kind: "select" | "number" | "text" | "auto";
|
||||
/** 저장 키(필수 컨테이너 · `by` 없을 때). */
|
||||
key?: string;
|
||||
choices?: SpecChoice[];
|
||||
@@ -64,6 +65,9 @@ export interface StructureSpec {
|
||||
retired: string[];
|
||||
}
|
||||
|
||||
/** 글 항목 길이 상한(자) — 칸 · 저장이 함께 자름. */
|
||||
export const TEXT_MAX = 100;
|
||||
|
||||
/** 컨테이너 인스턴스 한 벌 값 `{항목 id: 글}`. */
|
||||
export type Instance = Record<string, string>;
|
||||
export type SpecState = Record<string, Instance[]>;
|
||||
@@ -135,7 +139,8 @@ export function readState(
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 컨테이너 값 → 옵션(빈 칸 뺌 · 숫자 칸은 수 · 선택 컨테이너는 차례대로 1 부터 다시 매김). */
|
||||
/** 컨테이너 값 → 옵션(빈 칸 뺌 · 숫자 칸은 수 · 선택 컨테이너는 차례대로 1 부터 다시 매김 ·
|
||||
* 다 빈 선택 인스턴스는 첫 칸을 빈 글로 남겨 자리를 지킴 — 잇기 · 집계 줄 · 다시 열기). */
|
||||
export function writeOptions(
|
||||
spec: StructureSpec,
|
||||
state: SpecState,
|
||||
@@ -143,6 +148,7 @@ export function writeOptions(
|
||||
const out: Record<string, string | number> = {};
|
||||
for (const c of spec.containers) {
|
||||
(state[c.id] ?? []).forEach((values, index) => {
|
||||
const before = Object.keys(out).length;
|
||||
for (const item of c.items) {
|
||||
const key = keyOf(c, item, index + 1, values);
|
||||
const raw = (values[item.id] ?? "").trim();
|
||||
@@ -152,10 +158,13 @@ export function writeOptions(
|
||||
if (Number.isFinite(n)) out[key] = Math.round(n * 100) / 100;
|
||||
continue;
|
||||
}
|
||||
out[key] = raw;
|
||||
out[key] = item.kind === "text" ? raw.slice(0, TEXT_MAX) : raw;
|
||||
if (c.required)
|
||||
Object.assign(out, choicesOf(item, values).find((ch) => ch.value === raw)?.also);
|
||||
}
|
||||
const first = c.items.find((item) => item.kind !== "auto");
|
||||
if (!c.required && first && Object.keys(out).length === before)
|
||||
out[instanceKey(c, index + 1, first)] = "";
|
||||
});
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user