fix(structure): 종단 알약 정보 창 2행 — 1행 이름 + 기준 측점 · 2행 컨테이너 첫 값만 「 · 」 이음 · 제목 · 빈 값 뺌(54-46)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
2026-10-06 15:35:05 +09:00
co-authored by Claude Opus 5.5
parent 4838489e96
commit 131271e9ae
3 changed files with 42 additions and 69 deletions
@@ -2,12 +2,12 @@
* B05_Profile_UI_Structures_Balloon.ts
* 종단 구조물 알약 정보 라벨 줄(54-14) — 화면 요소 없는 순수 규칙. B05 · B06 알약 레인이 같이 씀.
*
* · 줄 = 종류 이름(기타 = 손 이름) · 기준 측점(구간 = 시작 ~ 끝) · 컨테이너마다 첫 옵션 값 한 줄
* (「관: 관경 1000」) · 컨테이너가 없으면 앞 두 줄만 · 메모 · 그 밖 옵션은 안 씀
* · 2행 — 1행 = 종류 이름(기타 = 손 이름) + 기준 측점(구간 = 시작 ~ 끝) · 2행 = 컨테이너마다 첫 옵션
* 값만 「 · 」 로 이음(제목 · 옵션 이름 없음 · 빈 값은 건너뜀) · 값이 하나도 없으면 1행만 · 메모 · 그 밖 옵션은 안 씀
* · 컨테이너 = 좌측 패널 폼과 같은 상자 — 명세 종류(`STRUCTURE_SPECS`)는 명세 컨테이너(선택 컨테이너는
* 인스턴스마다) · 그 밖 종류는 정의 옵션 그룹(`StructureType.groups`)
* · 첫 옵션 = 지금 보이는 첫 입력 칸(자동 칸 · 갈래가 없어 가린 칸 · 끈 칸은 건너뜀)
* · 값 `auto` = 「(자동)」 · 빈 값 = 「—」(54-39 · 기본값으로 안 바꿈 · 기본값은 새 구조물 칸에 실제 값)
* · 값 `auto` = 「(자동)」 · 빈 값은 건너뜀(기본값으로 안 바꿈 · 기본값은 새 구조물 칸에 실제 값)
* ========================================================================== */
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
@@ -18,8 +18,6 @@ import {
branchOf,
choiceLabel,
choicesOf,
containerTitle,
itemLabel,
readState,
type Instance,
type SpecContainer,
@@ -28,12 +26,12 @@ import {
} from "../A00_Common/design/design_structure_spec";
const AUTO_MARK = "(자동)";
const NONE = "—";
const SEP = " · ";
/** 선택 컨테이너 개수를 밖에서 정함(배관 흙막이 = 유출 단 수 · 폼 `hooks.count` 와 같은 값). */
export type ContainerCount = (containerId: string) => number | null | undefined;
/** 라벨 줄 — [이름, 기준 측점, ...컨테이너 줄]. */
/** 라벨 줄 — [이름 기준 측점, 컨테이너 값 이음?]. */
export function balloonLines(
structure: StructureInstance,
type: StructureType | undefined,
@@ -41,13 +39,14 @@ export function balloonLines(
count?: ContainerCount,
): string[] {
const spec = STRUCTURE_SPECS[structure.type_id];
const rest = spec
? specLines(spec, structure, type, count)
: groupLines(structure.options ?? {}, type);
return [head.name, head.position, ...rest];
const values = (
spec ? specLines(spec, structure, type, count) : groupLines(structure.options ?? {}, type)
).filter(Boolean);
const first = `${head.name} ${head.position}`;
return values.length ? [first, values.join(SEP)] : [first];
}
/** 명세 컨테이너 줄 — 폼(`Drainage_Facility.setFacility` · `paintStructureSpec`)과 같은 값 · 이름. */
/** 명세 컨테이너 값 — 폼(`Drainage_Facility.setFacility` · `paintStructureSpec`)과 같은 값 · 이름(빈 값 = ""). */
function specLines(
spec: StructureSpec,
structure: StructureInstance,
@@ -61,19 +60,14 @@ function specLines(
names = pipeNames;
}
const state = formState(spec, structure.type_id, structure.options ?? {});
const groups = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name]));
return spec.containers.flatMap((c) => {
const title = containerTitle(c, names, groups[c.link]);
return instancesOf(c, state[c.id] ?? [], count).map((values, index) => {
return spec.containers.flatMap((c) =>
instancesOf(c, state[c.id] ?? [], count).map((values) => {
const item = c.items.find(
(entry) => entry.kind !== "auto" && (!entry.by || branchOf(entry, values)),
);
const head = c.required ? title : `${title} ${index + 1}`;
return item
? `${head}: ${itemLabel(c, item, names)} ${specValue(c, item, values, names)}`
: `${head}: ${NONE}`;
});
});
return item ? specValue(c, item, values, names) : "";
}),
);
}
/** 저장 값 → 폼 칸 값 — 폼(`Drainage_Facility.setFacility`)과 같은 손질(BOX 옛 설치 · 본체 규격 ·
@@ -116,10 +110,10 @@ function specValue(
};
const raw = (values[item.id] ?? "").trim();
if (raw === "auto") return AUTO_MARK;
return raw ? shown(raw) : NONE;
return raw ? shown(raw) : "";
}
/** 정의 옵션 그룹 줄 — 그룹마다 첫 옵션(정의에 있고 켜진 칸). */
/** 정의 옵션 그룹 값 — 그룹마다 첫 옵션(정의에 있고 켜진 칸 · 빈 값 = ""). */
function groupLines(
options: Record<string, string | number>,
type: StructureType | undefined,
@@ -129,11 +123,9 @@ function groupLines(
const option = group.options
.map((key) => byKey.get(key))
.find((entry) => entry && entry.visible !== false && entry.enabled !== false);
if (!option) return `${group.name}: ${NONE}`;
const unit = option.unit ?? "";
if (!option) return "";
const raw = options[option.key];
const value =
raw === "auto" ? AUTO_MARK : raw !== undefined && raw !== "" ? `${raw}${unit}` : NONE;
return `${group.name}: ${option.label} ${value}`;
if (raw === "auto") return AUTO_MARK;
return raw !== undefined && raw !== "" ? `${raw}${option.unit ?? ""}` : "";
});
}
@@ -263,7 +263,7 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
* 알약 **위**로 띄운다 — 아래로 펴면 도면 테이블에 가려 안 보인다(2026-08-18
* 사용자 지시). 2행 구조에서 1행(윗줄)은 좌측 위, 2행(아랫줄)은 우측 위로
* 전개해 서로 겹치지 않게 하고, 솔로 알약은 위 중앙이다.
* 줄 = 이름 · 기준 측점 · 컨테이너마다 첫 옵션 값(`balloonLines` · 54-14) · 그래프 바닥 ▼ 단추를 가리지
* 줄 = 2행 — 이름 + 기준 측점 · 컨테이너마다 첫 옵션 값 이음(`balloonLines` · 54-14 · 54-46) · 그래프 바닥 ▼ 단추를 가리지
* 않게 단추 높이만큼 더 위 · 위가 잘리면 알약 아래로. */
function buildBalloon(
lines: string[],
+20 -39
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
"""54-14 종단 구조물 알약 정보 라벨 — 중요한 것만.
• 줄 = 이름 · 기준 측점 · 컨테이너마다 첫 옵션 값 한 줄(컨테이너가 없으면 앞 두 줄만)
• 2행 — 1행 = 이름 + 기준 측점 · 2행 = 컨테이너마다 첫 옵션 값만 「 · 」 로 이음(54-46 · 제목 없음)
• 컨테이너 = 좌측 패널 폼과 같은 상자(명세 컨테이너 · 선택 컨테이너는 인스턴스마다 · 정의 옵션 그룹)
• 빈 값 = 등록부 기본값 「(기본)」 · 값 auto = 「(자동)」 · 그것도 없으면 「—」 · 메모 · 그 밖 옵션은 안 씀
• 빈 값은 건너뜀 · 값 auto = 「(자동)」 · 값이 하나도 없으면 1행만 · 메모 · 그 밖 옵션은 안 씀
• 라벨은 ▲▼ 단추층 위 · 단추 높이만큼 위로 비킴 · B05 · B06 은 같은 레인 — 실제 화면은 ORCA 로 따로 본다.
화면 그릇 없이 Node 헬퍼가 TS 를 트랜스파일해 돌린다(`helper_54_14_balloon_lines.cjs`).
"""
@@ -33,56 +33,37 @@ def out():
return json.loads(proc.stdout)
def test_배관_컨테이너마다_첫_값_한_줄(out):
assert out["pipeFull"] == [
"이름",
"5+0",
"관: 관경 1200",
"관 보호공(유입): 구조 집수정",
"관 보호공(유출): 구조 흙막이",
"흙막이 1: 형태 돌망태", # 구조 칸 없음(54-30)
"도수로/돌붙임 1: 구조 도수로",
]
def test_배관_컨테이너마다_첫_값만_한_줄로_이음(out):
# 흙막이 1 은 구조 칸 없음 — 형태(54-30)
assert out["pipeFull"] == ["이름 5+0", "1200 · 집수정 · 흙막이 · 돌망태 · 도수로"]
def test_빈_값은_등록부_기본값에_기본_표시(out):
assert out["pipeBlank"] == [
"이름",
"5+0",
"관: 관경 1000 (기본)",
"관 보호공(유입): 구조 흙막이 (기본)",
"관 보호공(유출): 구조 —",
]
def test_빈_값은_건너뜀(out):
assert out["pipeBlank"] == ["이름 5+0"]
def test_흙막이_상자_수는_밖에서_정한_단_수(out):
assert out["pipeCount"][-2:] == ["흙막이 1: 형태 —", "흙막이 2: 형태 —"]
assert len(out["pipeCount"]) == 2 + 3 + 2
# 저장 1 벌(형태 빈 값) · 모자란 둘째도 빈 값 — 둘 다 건너뜀
assert out["pipeCount"] == ["이름 5+0"]
def test_박스_본체_규격은_폭_높이로_되찾음(out):
assert out["boxPreset"] == [
"이름",
"5+0",
"본체: 본체 규격 3.0 × 3.0",
"유입 구조: 구조 날개벽",
"유출 구조: 구조 —",
]
assert out["boxBlank"][2] == "본체: 본체 규격 2.0 × 2.0 (기본)"
assert out["boxPreset"] == ["이름 5+0", "3.0 × 3.0 · 날개벽"]
assert out["boxBlank"] == ["이름 5+0"]
def test_정의_옵션_그룹은_그룹마다_첫_켜진_옵션(out):
assert out["underdrain"] == ["이름", "5+0", "맹암거: 높이 0.6m"]
assert out["underdrainBlank"] == ["이름", "5+0", "맹암거: 높이 —"]
# 끈 칸(높이)은 건너뜀 · 빈 값은 기본값
assert out["wall"] == ["이름", "5+0", "상세 내용: 길이 10m (기본)"]
assert out["wallAuto"] == ["이름", "5+0", "상세 내용: 길이 (자동)"]
assert out["underdrain"] == ["이름 5+0", "0.6m"]
assert out["underdrainBlank"] == ["이름 5+0"]
# 끈 칸(높이)은 건너뜀
assert out["wall"] == ["이름 5+0"]
assert out["wallAuto"] == ["이름 5+0", "(자동)"]
def test_컨테이너가_없으면_이름과_기준_측점만(out):
assert out["mirror"] == ["이름", "5+0"]
assert out["etcEmpty"] == ["이름", "5+0"]
assert out["etcNote"] == ["이름", "5+0", "상세 내용 1: 비고 시험 비고"]
def test_컨테이너가_없으면_1행만(out):
assert out["mirror"] == ["이름 5+0"]
assert out["etcEmpty"] == ["이름 5+0"]
assert out["etcNote"] == ["이름 5+0", "시험 비고"]
def test_라벨은_단추층_위로_비켜_뜬다():