feat(b06,b07): 표 위 CAD 칸 미리 넣음 = 구조물 조합 이름 + 좌측 패널 옵션 줄 (166-1)
- 구조물마다 조합 줄 뒤에 좌측 패널 옵션 줄 — 폼과 같은 컨테이너 · 이름 · 보이는 칸(`structureOptionLines`) · 구조 갈래에 안 맞는 옛 값 뺌 · 필수 상자 빈 칸 = 폼 기본값 - B06 [상세] 모달 = 화면 TS 직접 · B07 = 같은 TS 를 Node 번들(B07_DesignDetail_Note_Options_Node · build:b07-note)로 받아 파이썬 짝이 붙임 · 실패는 조합 줄만 - 번들 낡음 판정에 A00_Common/design 더함 - 시험 test_166_1(진짜 명세 · B07 = 화면 같은 줄) · test_260 · test_265 기대값 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
This commit is contained in:
@@ -71,6 +71,8 @@ config/formula_node/
|
||||
config/spreadsheet_node/
|
||||
# 횡단도 덧그림 범위 번들 — `npm run build:b07-extent` 산출물.
|
||||
config/b07_extent_node/
|
||||
# 횡단도 표 위 CAD 칸 옵션 줄 번들 — `npm run build:b07-note` 산출물(166-1).
|
||||
config/b07_note_node/
|
||||
|
||||
# graphify 위키 산출물 — 창끼리 같은 위키를 찾게 **통째로** git 으로 나름
|
||||
# (2026-09-09 사용자 확정 「출력물 전체를 공유해도 됨」).
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
* 인스턴스마다) · 그 밖 종류는 정의 옵션 그룹(`StructureType.groups`)
|
||||
* · 첫 옵션 = 지금 보이는 첫 입력 칸(자동 칸 · 갈래가 없어 가린 칸 · 끈 칸은 건너뜀)
|
||||
* · 값 `auto` = 「(자동)」 · 빈 값은 건너뜀(기본값으로 안 바꿈 · 기본값은 새 구조물 칸에 실제 값)
|
||||
* · `structureOptionLines`(166-1) = 같은 컨테이너의 **보이는 칸 전부** 「제목: 이름 값 · …」 — B06 [상세] CAD 칸
|
||||
* 미리 넣음 · B07 짝(서버 Node `B07_DesignDetail_Note_Options_Node`)이 같이 씀
|
||||
* ========================================================================== */
|
||||
|
||||
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
||||
import { shapeFields, shapeKey } from "./B05_Profile_UI_Structures_Define";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { bodyOf, boxOptionsIn } from "../A00_Common/design/design_structure_box";
|
||||
import { withLegacyGuards } from "../A00_Common/design/design_structure_ford";
|
||||
@@ -18,6 +21,8 @@ import {
|
||||
branchOf,
|
||||
choiceLabel,
|
||||
choicesOf,
|
||||
containerTitle,
|
||||
itemLabel,
|
||||
readState,
|
||||
type Instance,
|
||||
type SpecContainer,
|
||||
@@ -46,6 +51,14 @@ export function balloonLines(
|
||||
return values.length ? [first, values.join(SEP)] : [first];
|
||||
}
|
||||
|
||||
/** 명세 이름 바꿈 표 — 배관은 정의 파일의 옛 이름 「흙막이 추가」보다 명세 이름 「흙막이」가 앞섬(54-1). */
|
||||
function specNames(structure: StructureInstance, type: StructureType | undefined) {
|
||||
const names = type?.spec_names ?? {};
|
||||
if (structure.type_id !== "pipe") return names;
|
||||
const { soil_add: _old, ...pipeNames } = names;
|
||||
return pipeNames;
|
||||
}
|
||||
|
||||
/** 명세 컨테이너 값 — 폼(`Drainage_Facility.setFacility` · `paintStructureSpec`)과 같은 값 · 이름(빈 값 = ""). */
|
||||
function specLines(
|
||||
spec: StructureSpec,
|
||||
@@ -53,12 +66,7 @@ function specLines(
|
||||
type: StructureType | undefined,
|
||||
count?: ContainerCount,
|
||||
): string[] {
|
||||
let names = type?.spec_names ?? {};
|
||||
if (structure.type_id === "pipe") {
|
||||
// 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1)
|
||||
const { soil_add: _old, ...pipeNames } = names;
|
||||
names = pipeNames;
|
||||
}
|
||||
const names = specNames(structure, type);
|
||||
const state = formState(spec, structure.type_id, structure.options ?? {});
|
||||
return spec.containers.flatMap((c) =>
|
||||
instancesOf(c, state[c.id] ?? [], count).map((values) => {
|
||||
@@ -129,3 +137,76 @@ function groupLines(
|
||||
return raw !== undefined && raw !== "" ? `${raw}${option.unit ?? ""}` : "";
|
||||
});
|
||||
}
|
||||
|
||||
/** 「이름 (단위)」 칸 이름 + 값 → 「이름 값단위」(예 높이 (m) + 1.3 = 높이 1.3m) · 단위 없는 이름은 그대로 띄움. */
|
||||
function nameValue(label: string, shown: string): string {
|
||||
const hit = /^(.*?)\s*\(([^()]+)\)$/.exec(label);
|
||||
return hit && shown !== AUTO_MARK ? `${hit[1]} ${shown}${hit[2]}` : `${label} ${shown}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* CAD 칸 미리 넣음 옵션 줄(166-1) — 좌측 패널 폼과 같은 컨테이너 · 이름 · 보이는 칸 전부 · 빈 칸 뺌.
|
||||
* 명세 종류 = 컨테이너(선택 컨테이너는 저장된 인스턴스마다 · 뒤에 번호)마다 「제목: 이름 값 · 이름 값」 ·
|
||||
* 그 밖 종류 = 정의 그룹마다(그룹 형상 칸 포함) + 그룹 밖 칸은 종류 이름 줄 · 값 하나 없는 상자는 줄 없음.
|
||||
*/
|
||||
export function structureOptionLines(
|
||||
structure: Pick<StructureInstance, "type_id" | "options">,
|
||||
type: StructureType | undefined,
|
||||
): string[] {
|
||||
const options = structure.options ?? {};
|
||||
const line = (title: string, parts: string[]): string[] => {
|
||||
const kept = parts.filter(Boolean);
|
||||
return kept.length ? [`${title}: ${kept.join(SEP)}`] : [];
|
||||
};
|
||||
const spec = STRUCTURE_SPECS[structure.type_id];
|
||||
if (spec) {
|
||||
const names = specNames(structure as StructureInstance, type);
|
||||
const state = formState(spec, structure.type_id, options);
|
||||
return spec.containers.flatMap((c) =>
|
||||
(state[c.id] ?? []).flatMap((values, index) => {
|
||||
const group = type?.groups?.find((g) => g.id === c.link)?.name;
|
||||
const title = containerTitle(c, names, group) + (c.required ? "" : ` ${index + 1}`);
|
||||
const parts = c.items
|
||||
.filter((item) => item.kind !== "auto" && (!item.by || branchOf(item, values)))
|
||||
.map((item) => {
|
||||
// 필수 상자 빈 칸 = 폼이 채워 보이는 기본값(177-2 · 예 동물이동형 유) · 선택 상자는 안내뿐이라 뺌
|
||||
const empty = !values[item.id]?.trim() && c.required && item.default;
|
||||
const filled = empty ? { ...values, [item.id]: item.default ?? "" } : values;
|
||||
const shown = specValue(c, item, filled, names);
|
||||
return shown ? nameValue(itemLabel(c, item, names), shown) : "";
|
||||
});
|
||||
return line(title, parts);
|
||||
}),
|
||||
);
|
||||
}
|
||||
const byKey = new Map((type?.options ?? []).map((option) => [option.key, option]));
|
||||
const valueOf = (key: string, unit?: string | null): string => {
|
||||
const raw = options[key];
|
||||
if (raw === undefined || raw === null || raw === "") return "";
|
||||
return raw === "auto" ? AUTO_MARK : `${raw}${unit ?? ""}`;
|
||||
};
|
||||
const optionParts = (keys: string[]): string[] =>
|
||||
keys.map((key) => {
|
||||
const option = byKey.get(key);
|
||||
if (!option || option.visible === false || option.enabled === false) return "";
|
||||
const shown = valueOf(key, option.unit);
|
||||
return shown ? `${option.label} ${shown}` : "";
|
||||
});
|
||||
const groups = type?.groups ?? [];
|
||||
const grouped = new Set(groups.flatMap((group) => group.options));
|
||||
return [
|
||||
...groups.flatMap((group) =>
|
||||
line(group.name, [
|
||||
...optionParts(group.options),
|
||||
...shapeFields(group.shape).map((field) => {
|
||||
const shown = valueOf(shapeKey(group.id, field.suffix));
|
||||
return shown ? nameValue(field.label, shown) : "";
|
||||
}),
|
||||
]),
|
||||
),
|
||||
...line(
|
||||
type?.name ?? structure.type_id,
|
||||
optionParts((type?.options ?? []).map((o) => o.key).filter((key) => !grouped.has(key))),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* (`_Engine_Cad_Table.cross_note_cad_entities`).
|
||||
* 처음 열 때 칸이 없으면 — 옛 글(`structure_note`)이 있으면 줄마다 Text 로 옮김 · 없으면 미리 넣음
|
||||
* (265-2 · 관 주인 측점 = 원형 마크 · 그 측점 구조물마다 지정된 구조물 조합 이름 줄 · 미지정 =
|
||||
* 「이름 조합 미지정」) · 그 밖 = 빈 도면.
|
||||
* 「이름 조합 미지정」 · 이어서 좌측 패널 옵션 줄 = `structureOptionLines` · 166-1) · 그 밖 = 빈 도면.
|
||||
* ⚠ 짝(①) = `B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py` — `noteEntities` ·
|
||||
* `structureNoteLines` · `stationNoLabel` 와 같은 입력 → 같은 엔티티(id 만 다름) · 모달을 안 연
|
||||
* 측점은 B07 이 그것으로 그림 · 거울 시험 `test_260_note_cad.py` · `test_265_note_cad.py`.
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
fetchStructureTypes,
|
||||
readPendingStructures,
|
||||
} from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { structureOptionLines } from "../B05_Profile/B05_Profile_UI_Structures_Balloon";
|
||||
import {
|
||||
readGroupItems,
|
||||
readSummaryItem,
|
||||
@@ -144,6 +145,8 @@ export interface NoteThing {
|
||||
at: number;
|
||||
summary_item?: unknown;
|
||||
group_items?: unknown;
|
||||
/** 좌측 패널 옵션 줄(166-1 · `structureOptionLines` · B07 짝은 서버 Node 가 같은 함수로 채움). */
|
||||
optionLines?: string[];
|
||||
}
|
||||
|
||||
/** 관 본체 잇기 자리(`design_structure_pipe` 「관」 link) — 옛 본체 `summary_item` 도 같은 자리. */
|
||||
@@ -156,18 +159,22 @@ export const thingsAt = (things: readonly NoteThing[], chainageM: number): NoteT
|
||||
/**
|
||||
* 구조물 조합 줄 — 구조물마다 지정된 조합(본체 → 그룹 차례) 이름 한 줄씩(이름 = 일위대가 이름 +
|
||||
* 조합 경로 옵션 값) · 같은 구조물의 같은 이름은 한 줄 · 미지정 = 「{구조물 이름} 조합 미지정」 ·
|
||||
* 배관은 본체 줄 없음(원형 마크) — 보호공 · 흙막이 · 도수로 조합만.
|
||||
* 배관은 본체 줄 없음(원형 마크) — 보호공 · 흙막이 · 도수로 조합만 · 이어서 그 구조물 좌측 패널 옵션 줄(166-1).
|
||||
*/
|
||||
export function structureNoteLines(things: readonly NoteThing[], chainageM: number): string[] {
|
||||
return thingsAt(things, chainageM).flatMap((thing) => {
|
||||
const own = readSummaryItem(thing.summary_item);
|
||||
const slots: Array<[string, SummaryItem]> = Object.entries(readGroupItems(thing.group_items));
|
||||
if (own) slots.unshift(["", own]);
|
||||
if (!slots.length) return thing.pipe ? [] : [`${thing.name} 조합 미지정`];
|
||||
const names = slots
|
||||
.filter(([gid]) => !(thing.pipe && (gid === "" || gid === PIPE_BODY_LINK)))
|
||||
.map(([, item]) => summaryLabel(item));
|
||||
return [...new Set(names)];
|
||||
const combos = slots.length
|
||||
? [...new Set(names)]
|
||||
: thing.pipe
|
||||
? []
|
||||
: [`${thing.name} 조합 미지정`];
|
||||
return [...combos, ...(thing.optionLines ?? [])];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,8 +244,10 @@ export async function loadNoteThings(projectId: string): Promise<NoteThing[]> {
|
||||
readPendingStructures(projectId) ?? fetchStructures(projectId).then((res) => res.structures),
|
||||
fetchStructureTypes(true),
|
||||
]);
|
||||
const names = new Map(types.map((type) => [type.type_id, type.name]));
|
||||
const nameOf = (kind: string): string => names.get(kind) ?? kind;
|
||||
const byId = new Map(types.map((type) => [type.type_id, type]));
|
||||
const nameOf = (kind: string): string => byId.get(kind)?.name ?? kind;
|
||||
const optionLines = (type_id: string, options?: Record<string, string | number>): string[] =>
|
||||
structureOptionLines({ type_id, options: options ?? {} }, byId.get(type_id));
|
||||
const piped = pipes.map((point): NoteThing => {
|
||||
const kind = point.facility ?? "pipe";
|
||||
const { summary_item, group_items } = point;
|
||||
@@ -248,13 +257,23 @@ export async function loadNoteThings(projectId: string): Promise<NoteThing[]> {
|
||||
at: point.chainage_m,
|
||||
summary_item,
|
||||
group_items,
|
||||
optionLines: optionLines(kind, point.options as Record<string, string | number>),
|
||||
};
|
||||
});
|
||||
const placed = structures.flatMap((item): NoteThing[] => {
|
||||
const at = item.chainage_m ?? item.start_m;
|
||||
if (typeof at !== "number" || !Number.isFinite(at)) return [];
|
||||
const { summary_item, group_items } = item;
|
||||
return [{ name: nameOf(item.type_id), pipe: false, at, summary_item, group_items }];
|
||||
return [
|
||||
{
|
||||
name: nameOf(item.type_id),
|
||||
pipe: false,
|
||||
at,
|
||||
summary_item,
|
||||
group_items,
|
||||
optionLines: optionLines(item.type_id, item.options),
|
||||
},
|
||||
];
|
||||
});
|
||||
return [...piped, ...placed];
|
||||
} catch {
|
||||
|
||||
@@ -7,20 +7,31 @@ B06 [상세] 모달을 한 번도 안 연 측점(칸 = 없음)에 B07 이 같은
|
||||
(빈 엔티티)은 덮지 않음 · 옛 글(`structure_note`)이 있으면 옛 글이 먼저
|
||||
(모달이 처음 열 때 옮기는 차례와 같음).
|
||||
그림 = 관 주인 측점 원형 마크(배수관 · Φ · L 정수) + 측점 · 그 측점 구조물마다 지정된
|
||||
구조물 조합 이름 줄(미지정 = 「이름 조합 미지정」).
|
||||
구조물 조합 이름 줄(미지정 = 「이름 조합 미지정」) + 그 구조물 좌측 패널 옵션 줄(166-1).
|
||||
옵션 줄은 파이썬으로 다시 짜지 않음 — 화면과 같은 TS(`structureOptionLines`)를 Node
|
||||
진입점(`B07_DesignDetail_Note_Options_Node.ts` · CLAUDE.md 5장 ②)으로 돌려 받음 ·
|
||||
실패는 비치명(조합 줄만).
|
||||
좌표 = 도면 mm(글 2.0 · 줄 3.0) · 칸 왼쪽 위 = (0, 0) · 아래로 음수.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_structure_types
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_definition, load_structure_types
|
||||
from B06_Section.B06_Section_Engine_Culvert import _culvert_set, pipe_points_file
|
||||
from common_util.common_util_drainage_pipes import PIPE_FACILITY_PIPE, parse_pipe_points
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
_OPTIONS_BUNDLE = ROOT / "config" / "b07_note_node" / "B07_DesignDetail_Note_Options_Node.js"
|
||||
_OPTIONS_NPM = "build:b07-note"
|
||||
|
||||
# TS `PIPE_MATCH_M`(`design_selection`) — 관 · 구조물 자리와 측점이 같은 자리로 보는 폭
|
||||
PIPE_MATCH_M = 0.51
|
||||
@@ -35,7 +46,8 @@ _GROUP_ID = re.compile(r"^g[0-9a-z]{1,16}$")
|
||||
|
||||
#: (관 누가거리, 세트, 옵션) — 원형 마크 재료
|
||||
Pipe = tuple[float, dict[str, Any], dict[str, Any]]
|
||||
#: 글 줄 재료 한 건 `{name, pipe, at, summary_item, group_items}` — TS `NoteThing`
|
||||
#: 글 줄 재료 한 건 `{name, pipe, at, summary_item, group_items, type_id, options, option_lines}`
|
||||
#: — TS `NoteThing`(option_lines = `optionLines`)
|
||||
Thing = dict[str, Any]
|
||||
|
||||
|
||||
@@ -98,22 +110,23 @@ def things_at(things: list[Thing], chainage_m: float) -> list[Thing]:
|
||||
def structure_note_lines(things: list[Thing], chainage_m: float) -> list[str]:
|
||||
"""구조물 조합 줄 — TS `structureNoteLines` 짝.
|
||||
|
||||
본체 → 그룹 차례 · 같은 이름 한 줄 · 미지정 = 「이름 조합 미지정」 · 배관 본체 뺌.
|
||||
본체 → 그룹 차례 · 같은 이름 한 줄 · 미지정 = 「이름 조합 미지정」 · 배관 본체 뺌 ·
|
||||
이어서 그 구조물 좌측 패널 옵션 줄(`option_lines` · 166-1).
|
||||
"""
|
||||
lines: list[str] = []
|
||||
for thing in things_at(things, chainage_m):
|
||||
own = _summary_item(thing.get("summary_item"))
|
||||
slots = ([("", own)] if own else []) + _group_items(thing.get("group_items"))
|
||||
if not slots:
|
||||
if not thing["pipe"]:
|
||||
lines.append(f"{thing['name']} 조합 미지정")
|
||||
continue
|
||||
names = [
|
||||
item["name"] or item["key"]
|
||||
for gid, item in slots
|
||||
if not (thing["pipe"] and gid in ("", _PIPE_BODY_LINK))
|
||||
]
|
||||
lines += list(dict.fromkeys(names))
|
||||
if slots:
|
||||
lines += list(dict.fromkeys(names))
|
||||
elif not thing["pipe"]:
|
||||
lines.append(f"{thing['name']} 조합 미지정")
|
||||
lines += [str(line) for line in thing.get("option_lines") or []]
|
||||
return lines
|
||||
|
||||
|
||||
@@ -207,6 +220,8 @@ def _things(points: list[Any], structures: list[Any], names: dict[str, str]) ->
|
||||
"at": float(point.chainage_m),
|
||||
"summary_item": point.summary_item,
|
||||
"group_items": point.group_items,
|
||||
"type_id": point.facility,
|
||||
"options": dict(point.options or {}),
|
||||
}
|
||||
for point in points
|
||||
]
|
||||
@@ -221,11 +236,46 @@ def _things(points: list[Any], structures: list[Any], names: dict[str, str]) ->
|
||||
"at": float(at),
|
||||
"summary_item": item.summary_item,
|
||||
"group_items": item.group_items,
|
||||
"type_id": item.type_id,
|
||||
"options": dict(item.options or {}),
|
||||
}
|
||||
)
|
||||
return things
|
||||
|
||||
|
||||
def with_option_lines(things: list[Thing]) -> list[Thing]:
|
||||
"""구조물마다 좌측 패널 옵션 줄(166-1) — 화면 TS 를 Node 로 한 번에 · 실패면 그대로."""
|
||||
if not things:
|
||||
return things
|
||||
try:
|
||||
# 지연 import — M02 정의 저장소가 B07 엔진 import 차례에 끼지 않게
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible
|
||||
|
||||
payload = {
|
||||
"things": [
|
||||
{"type_id": t["type_id"], "options": t.get("options") or {}} for t in things
|
||||
],
|
||||
"types": with_visible().model_dump(mode="json")["types"],
|
||||
"definition": load_definition(),
|
||||
}
|
||||
output = run_bundle_json(_OPTIONS_BUNDLE, _OPTIONS_NPM, payload)
|
||||
except Exception:
|
||||
logger.warning("B07 CAD 칸 옵션 줄 계산 실패 — 조합 줄만 넣음", exc_info=True)
|
||||
return things
|
||||
rows = output.get("lines") if isinstance(output, dict) else None
|
||||
if not isinstance(rows, list) or len(rows) != len(things):
|
||||
return things
|
||||
return [
|
||||
{
|
||||
**thing,
|
||||
"option_lines": [line for line in row if isinstance(line, str)]
|
||||
if isinstance(row, list)
|
||||
else [],
|
||||
}
|
||||
for thing, row in zip(things, rows)
|
||||
]
|
||||
|
||||
|
||||
def load_note_sources(project_root: Path) -> tuple[list[Pipe], list[Thing]]:
|
||||
"""정본 재료 — (원형 마크 배수관 · 관경 안 고름 = 없음, 글 줄 재료 = 관 지점 + 구조물 배치)."""
|
||||
try:
|
||||
@@ -242,7 +292,7 @@ def load_note_sources(project_root: Path) -> tuple[list[Pipe], list[Thing]]:
|
||||
pipes.append((float(point.chainage_m), spec, options))
|
||||
_revision, structures = load_structures(str(project_root))
|
||||
names = {kind.type_id: kind.name for kind in load_structure_types()}
|
||||
return pipes, _things(points, structures, names)
|
||||
return pipes, with_option_lines(_things(points, structures, names))
|
||||
|
||||
|
||||
def with_structure_note(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_Note_Options_Node.ts
|
||||
* B07 횡단도 표 위 CAD 칸 미리 넣음의 **좌측 패널 옵션 줄**을 서버가 내는 Node 진입점(166-1 ·
|
||||
* CLAUDE.md 5장 「계산 자리」 ②).
|
||||
*
|
||||
* 화면(B06 [상세] 모달 `loadNoteThings`)과 같은 함수(`structureOptionLines` · 정의 덧칸 `withDefinition`)를
|
||||
* 그대로 돌린다 — 파이썬 짝(`B07_DesignDetail_Engine_Cad_CulvertNote`)은 받은 줄을 구조물마다 조합 줄 뒤에 붙인다.
|
||||
*
|
||||
* 실행: node <번들> <입력.json> <출력.json>
|
||||
* 입력 { things: [{ type_id, options }], types: GET /projects/structure-types 의 types,
|
||||
* definition: 구조물 정의(GET /m02/structure-definition 과 같은 꼴) }
|
||||
* 출력 { lines: string[][] } — things 차례대로
|
||||
* 끝 코드: 0 성공 / 2 인자 오류
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import type { StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { structureOptionLines } from "../B05_Profile/B05_Profile_UI_Structures_Balloon";
|
||||
import {
|
||||
withDefinition,
|
||||
type StructureDefinition,
|
||||
} from "../B05_Profile/B05_Profile_UI_Structures_Define";
|
||||
|
||||
interface Input {
|
||||
things?: Array<{ type_id: string; options?: Record<string, string | number> }>;
|
||||
types?: StructureType[];
|
||||
definition?: StructureDefinition | null;
|
||||
}
|
||||
|
||||
const [inputPath, outputPath] = process.argv.slice(2);
|
||||
if (!inputPath || !outputPath) {
|
||||
console.error("사용: node <번들> <입력.json> <출력.json>");
|
||||
process.exit(2);
|
||||
}
|
||||
const input = JSON.parse(readFileSync(inputPath, "utf8")) as Input;
|
||||
const types = new Map(
|
||||
withDefinition(input.types ?? [], input.definition ?? null).map((type) => [type.type_id, type]),
|
||||
);
|
||||
const lines = (input.things ?? []).map((thing) =>
|
||||
structureOptionLines(
|
||||
{ type_id: thing.type_id, options: thing.options ?? {} },
|
||||
types.get(thing.type_id),
|
||||
),
|
||||
);
|
||||
writeFileSync(outputPath, JSON.stringify({ lines }));
|
||||
@@ -23,8 +23,9 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_DIRS = ("B05_Profile", "B06_Section", "B08_Quantity", "common_util")
|
||||
# 번들에 박히는 자료(JSON) — 벽 전면 기울기 표준경사 표·기슭막이 치수 표(2026-09-14 B5). 표만 바뀌어도 다시 빌드.
|
||||
SOURCE_DATA_DIRS = ("resources/data_masonry",)
|
||||
# 폴더째 훑지 않는 자리 — B07 은 CAD 앱(openwebcad · node_modules)이 함께 살아 맨 위 TS 만 본다.
|
||||
SOURCE_GLOBS = ("B07_DesignDetail/*.ts",)
|
||||
# 폴더째 훑지 않는 자리 — B07 은 CAD 앱(openwebcad · node_modules)이 함께 살아 맨 위 TS 만 본다 ·
|
||||
# 구조물 명세(A00 design)는 B07 CAD 칸 옵션 줄 번들이 끌어 씀(166-1).
|
||||
SOURCE_GLOBS = ("B07_DesignDetail/*.ts", "A00_Common/design/*.ts")
|
||||
# 번들 만들기·실행 상한(초). 실측 번들 실행 0.1초, 빌드 3초 수준이라 넉넉하다.
|
||||
BUILD_TIMEOUT_S = 300
|
||||
RUN_TIMEOUT_S = 600
|
||||
|
||||
+2
-1
@@ -5,12 +5,13 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node ./config/node_modules/vite/bin/vite.js --configLoader runner",
|
||||
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:formula && npm run build:spreadsheet && npm run build:b07-extent && npm run build:b07-cad",
|
||||
"build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:formula && npm run build:spreadsheet && npm run build:b07-extent && npm run build:b07-note && npm run build:b07-cad",
|
||||
"build:corridor": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B05_Profile/B05_Profile_Corridor_Node.ts --outDir ../config/corridor_node",
|
||||
"build:server-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B06_Section/B06_Section_Server_Calc_Node.ts --outDir ../config/server_calc_node",
|
||||
"build:formula": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../common_util/common_util_sheet_recalc_node.ts --outDir ../config/formula_node",
|
||||
"build:spreadsheet": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../common_util/common_util_spreadsheet_node.ts --outDir ../config/spreadsheet_node",
|
||||
"build:b07-extent": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Cross_Extent_Node.ts --outDir ../config/b07_extent_node",
|
||||
"build:b07-note": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B07_DesignDetail/B07_DesignDetail_Note_Options_Node.ts --outDir ../config/b07_note_node",
|
||||
"install:b07-cad": "npm --prefix B07_DesignDetail/openwebcad install",
|
||||
"build:b07-cad": "npm run install:b07-cad && npm --prefix B07_DesignDetail/openwebcad run build",
|
||||
"preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* 166-1 CAD 칸 미리 넣음 옵션 줄 — 진짜 `structureOptionLines`(명세 · 정의 덧칸)를 Node 로 돌림.
|
||||
* 표준입력 { things: [{type_id, options}], types, definition } → 표준출력 { lines: string[][] }
|
||||
* (B07 번들 `B07_DesignDetail_Note_Options_Node` 와 같은 입력 · 같은 함수 — 짝 = test_166_1_note_options.py).
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const { structureOptionLines } = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structures_Balloon.ts"),
|
||||
);
|
||||
const { withDefinition } = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structures_Define.ts"),
|
||||
);
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const types = new Map(
|
||||
withDefinition(input.types ?? [], input.definition ?? null).map((type) => [type.type_id, type]),
|
||||
);
|
||||
const lines = (input.things ?? []).map((thing) =>
|
||||
structureOptionLines(
|
||||
{ type_id: thing.type_id, options: thing.options ?? {} },
|
||||
types.get(thing.type_id),
|
||||
),
|
||||
);
|
||||
process.stdout.write(JSON.stringify({ lines }));
|
||||
@@ -22,6 +22,8 @@ const STUBS = {
|
||||
fetchStructureTypes: async () => DATA.types,
|
||||
readPendingStructures: () => null,
|
||||
},
|
||||
// 좌측 패널 옵션 줄(166-1)은 test_166_1 이 진짜 명세로 봄 — 여기는 조합 줄 · 마크만
|
||||
"../B05_Profile/B05_Profile_UI_Structures_Balloon": { structureOptionLines: () => [] },
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""166-1 횡단 [상세] CAD 칸 · B07 표 위 미리 넣음 = 구조물 조합 이름 + 좌측 패널 옵션 줄.
|
||||
|
||||
사용자 말: 「일단은 좌측패널 옵션과 구조물조합이름을 다 넣어주면 내가 추가로 선정하겠음」.
|
||||
옵션 줄 = 좌측 패널 폼과 같은 컨테이너 · 이름 · 보이는 칸(`structureOptionLines`) —
|
||||
관 보호공 구조 갈래에 맞지 않는 옛 값(집수정인데 흙막이 칸 값)은 안 씀 ·
|
||||
필수 상자 빈 칸 = 폼이 채워 보이는 기본값.
|
||||
B07 은 같은 TS 를 Node 번들(`B07_DesignDetail_Note_Options_Node`)로 돌려 받음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_definition # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_CulvertNote import ( # noqa: E402
|
||||
structure_note_lines,
|
||||
with_option_lines,
|
||||
)
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
PIPE = {
|
||||
"pipe_diameter_mm": 800,
|
||||
"pipe_kind": "파형강관",
|
||||
"inlet_type": "집수정",
|
||||
"inlet_structure": "L",
|
||||
"inlet_basin_height_m": 1.3,
|
||||
# 구조를 집수정으로 바꾸기 전 흙막이 값 — 폼이 안 보이는 옛 값
|
||||
"inlet_revet_form": "돌쌓기(찰)",
|
||||
"inlet_revet_length_m": 10,
|
||||
"outlet_type": "기슭막이",
|
||||
"outlet_revet_form": "돌쌓기(찰)",
|
||||
"outlet_revet_length_m": 10,
|
||||
"outlet_revet_before_m": 5,
|
||||
"outlet_revet_after_m": 5,
|
||||
"outlet_revet_height_m": 1.3,
|
||||
}
|
||||
WALL = {"wall_form": "돌쌓기(찰)", "length_m": 12, "height_m": 2.5, "before_m": 6, "after_m": 6}
|
||||
THINGS = [
|
||||
{"type_id": "pipe", "options": PIPE},
|
||||
{"type_id": "retaining_wall", "options": WALL},
|
||||
{"type_id": "hazard_sign", "options": {}},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts_lines() -> list[list[str]]:
|
||||
payload = {
|
||||
"things": THINGS,
|
||||
"types": with_visible().model_dump(mode="json")["types"],
|
||||
"definition": load_definition(),
|
||||
}
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_166_1_note_options.cjs")],
|
||||
input=json.dumps(payload, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)["lines"]
|
||||
|
||||
|
||||
def test_배관_옵션_줄은_폼_컨테이너_그대로(ts_lines):
|
||||
pipe = ts_lines[0]
|
||||
assert pipe[0] == "관: 관경 800"
|
||||
# 집수정 갈래 칸만 · 빈 동물이동형 = 폼 기본 「유」 · 흙막이 옛 값은 안 씀
|
||||
assert pipe[1] == "관 보호공(유입): 구조 집수정 · 형태 L형 · 높이 1.3m · 동물이동형 유"
|
||||
assert "10m" not in pipe[1]
|
||||
assert pipe[2].startswith("관 보호공(유출): 구조 흙막이 · 형태 돌 쌓기(찰) · 길이 10m")
|
||||
assert "기준측점 전 5m · 기준측점 후 5m" in pipe[2]
|
||||
|
||||
|
||||
def test_정의_그룹_종류는_그룹마다_한_줄_옵션_없으면_줄_없음(ts_lines):
|
||||
assert ts_lines[1] == [
|
||||
"옹벽: 형태 돌쌓기(찰) · 길이 12m · 높이 2.5m · 기준측점 전 6m · 기준측점 후 6m"
|
||||
]
|
||||
assert ts_lines[2] == []
|
||||
|
||||
|
||||
def test_B07_은_같은_TS_를_Node_번들로_받아_같은_줄(ts_lines):
|
||||
things = [{**thing, "name": thing["type_id"], "pipe": False, "at": 0.0} for thing in THINGS]
|
||||
got = with_option_lines(things)
|
||||
assert [thing["option_lines"] for thing in got] == ts_lines
|
||||
|
||||
|
||||
def test_조합_줄_뒤에_옵션_줄_미지정도_옵션_줄():
|
||||
thing = {
|
||||
"name": "옹벽",
|
||||
"pipe": False,
|
||||
"at": 40.0,
|
||||
"summary_item": None,
|
||||
"group_items": None,
|
||||
"option_lines": ["옹벽: 높이 2.5m"],
|
||||
}
|
||||
assert structure_note_lines([thing], 40.0) == ["옹벽 조합 미지정", "옹벽: 높이 2.5m"]
|
||||
|
||||
|
||||
def test_화면은_구조물마다_옵션_줄을_붙임():
|
||||
cad = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Detail_Cad.ts").read_text("utf-8")
|
||||
assert "structureOptionLines({ type_id, options: options ?? {} }, byId.get(type_id))" in cad
|
||||
assert "...(thing.optionLines ?? [])" in cad
|
||||
@@ -448,8 +448,9 @@ def test_칸_없음_만_미리_넣음_지운_칸_옛_글_설계없음_이웃은_
|
||||
seeded = with_structure_note(design, 1433.0, sources, "NO.71+13")
|
||||
assert seeded is not design and "structure_note_cad" not in design # 저장 설계는 안 바뀜
|
||||
labels = [e["shapeData"].get("label") for e in seeded["structure_note_cad"]["entities"]]
|
||||
# 관 조합 미지정 = 원형 마크 · 측점만(배관은 글 줄 없음)
|
||||
assert labels[2:] == ["배수관", "Φ1200m/m", "L=22m", "NO.71+13"]
|
||||
# 관 조합 미지정 = 원형 마크 · 측점(배관은 조합 글 줄 없음) · 이어서 좌측 패널 옵션 줄(166-1)
|
||||
assert labels[2:6] == ["배수관", "Φ1200m/m", "L=22m", "NO.71+13"]
|
||||
assert labels[6].startswith("관: 관경 1200") and all(": " in line for line in labels[6:])
|
||||
cleared = {**design, "structure_note_cad": {"entities": []}}
|
||||
assert with_structure_note(cleared, 1433.0, sources, "x") is cleared
|
||||
old = {**design, "structure_note": "옛 글"}
|
||||
|
||||
@@ -209,7 +209,10 @@ def test_파이썬_짝_재료는_정본_관_지점_구조물_배치(tmp_path):
|
||||
assert [(t["name"], t["at"]) for t in sources[1]] == [("배관", 40.2), ("옹벽", 40.0)]
|
||||
seeded = with_structure_note({"pipe_length_m": 8.4}, 40.0, sources, "NO.2")
|
||||
labels = _labels(seeded["structure_note_cad"]["entities"])
|
||||
assert labels == ["배수관", "Φ800m/m", "L=8m", "NO.2", _A["name"], _C["name"]]
|
||||
# 구조물마다 조합 줄 → 좌측 패널 옵션 줄(166-1) · 옵션 없는 옹벽은 조합 줄만
|
||||
assert labels[:5] == ["배수관", "Φ800m/m", "L=8m", "NO.2", _A["name"]]
|
||||
assert labels[5].startswith("관: 관경 800") and labels[-1] == _C["name"]
|
||||
assert all(": " in line for line in labels[5:-1])
|
||||
|
||||
|
||||
# ── 265-3 관 길이 정수 ─────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user