fix(구조물표): 322-2 다듬기(브레인 화면 확인) — 화면에 영어 키 안 보임 · 검사 글 한국어(화면 · 서버 짝) · 부품 탭 읽는 칸 = 정의 이름 · 줄바꿈 · 한눈 표 폭 줄임 · 바꿨다 되돌리면 [저장] 꺼짐

- 자리꼴 점 · 구간 · 터 · 정본 관 지점 · 구조물(키는 툴팁) · 고르기 글 = 부품 짧은 이름(괄호 설명 · 키는 툴팁) · 횡단도 없음 = 「안 만듦」 · 「벽 스윕」 → 「벽」 · 종류 키는 툴팁
- 검사 글: 「형상이 「없음」이면 횡단도도 「안 만듦」」 · 부품 짧은 이름 · 칸 이름 · 종류 이름(M02_SO_Def_Check.ts = M02_SO_Check.py)
- 부품 탭: 읽는 칸 = 그 부품을 쓰는 종류의 칸 이름(단위) · 표 줄바꿈(table-layout fixed)
- 한눈 표 형상칸 「기본 형태」 = 그림 형태 고르기(REVET_FORMS)
- [저장] 켬 = 저장할 꼴(tidy · 목록 짝 enabled)끼리 지금 판과 다를 때만(M02_SO_Def_Reflect.differs)
- 시험: 322-2 검사 글 · 되돌림 견본

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 16:29:30 +09:00
co-authored by Claude Opus 5.5
parent 6c87207f76
commit 0da2fa0767
11 changed files with 207 additions and 89 deletions
@@ -13,11 +13,22 @@
from __future__ import annotations
import re
from typing import Any
from common_util.common_util_structure_catalog import load_parts, resolve_catalog
BRANCHES = ("횡단도", "카드이름", "형상", "3D")
# 화면 글(322-2 다듬기 · TS `M02_SO_Def_Reflect` 짝) — 자리꼴 · 정본 한국어 · 부품 짧은 이름
PLACE_KO = {"point": "점", "interval": "구간", "site": "터"}
OWNER_KO = {"pipe_points": "관 지점", "structures": "구조물"}
def short_name(full: str) -> str:
"""괄호 설명을 뗀 부품 이름(TS `shortName` 짝)."""
return re.sub(r"\s*\(.*\)\s*$", "", full)
OFF_VALUE = {"횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none"}
LISTS = ("both", "b06", "hidden")
_FIELDS = {"목록", "형상칸", *BRANCHES}
@@ -99,7 +110,7 @@ def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]
if row is None:
continue
if row["shape"] == "none" and row["station"] != "none":
out.append(f"「{row['name']}」 형상 none — 횡단도도 none 이어야 함")
out.append(f"「{row['name']}」 형상이 「없음」이면 횡단도도 「안 만듦」")
kept = _hidden_in(before or doc, t["type_id"]) # 앞 판 없음 = 새로 숨긴 것 없음
hidden = [key for key in _hidden_in(doc, t["type_id"]) if key not in kept]
# 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
@@ -114,19 +125,20 @@ def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]
("형상", row["shape"]),
("3D", row["model3d"]),
)
labels = {o["key"]: o.get("label") or o["key"] for o in t.get("options") or []}
for branch, part_id in used:
part = parts[branch].get(part_id)
if part is None:
continue
name = short_name(part["이름"])
place = PLACE_KO.get(row["placement"], row["placement"])
owner = OWNER_KO.get(row["owner"], row["owner"])
if row["placement"] not in part["자리꼴"] or row["owner"] not in part["정본"]:
out.append(
f"「{row['name']}」 {branch} 「{part_id}」 — 자리꼴 {row['placement']} · "
f"정본 {row['owner']} 에 안 맞음"
)
out.append(f"「{row['name']}」 {branch} 「{name}」 — {place} · {owner} 에 안 맞음")
for key in part["읽는 칸"]:
if key in keys and key in hidden:
out.append(
f"「{row['name']}」 {branch} 「{part_id}」 이 읽는 칸 「{key}」 — "
f"「{row['name']}」 {branch} 「{name}」 이 쓰는 칸 「{labels[key]}」 — "
"숨길 수 없음"
)
return out
@@ -138,9 +150,10 @@ def _fixed_errors(doc: dict[str, Any], before: dict[str, Any]) -> list[str]:
now = {t["type_id"]: t for t in doc.get("types") or []}
for old in before.get("types") or []:
tid = old["type_id"]
name = old.get("name") or tid
t = now.get(tid)
if t is None:
out.append(f"종류 「{tid}」 — 지울 수 없음")
out.append(f"종류 「{name}」 — 지울 수 없음")
continue
fixed = (
("자리꼴", old.get("placement"), t.get("placement")),
@@ -149,14 +162,15 @@ def _fixed_errors(doc: dict[str, Any], before: dict[str, Any]) -> list[str]:
)
for what, a, b in fixed:
if a != b:
out.append(f"「{tid}」 {what} — 만든 뒤 고정")
out.append(f"「{name}」 {what} — 만든 뒤 고정")
options = {o["key"]: o for o in t.get("options") or []}
for o in old.get("options") or []:
n = options.get(o["key"])
if o.get("input") != "select" or n is None:
continue
if sorted(o.get("choices") or []) != sorted(n.get("choices") or []):
out.append(f"「{tid}.{o['key']}」 선택지 값 — 고칠 수 없음(이름은 이름 표로)")
label = o.get("label") or o["key"]
out.append(f"「{name}」 「{label}」 선택지 값 — 고칠 수 없음(이름만 고침)")
return out
+2 -13
View File
@@ -32,7 +32,7 @@ import { buildSpecBlock } from "./M02_SO_Def_Spec";
import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe";
import { catalogProblems, type CheckDef } from "./M02_SO_Def_Check";
import { buildParts } from "./M02_SO_Def_Parts";
import { setReflect, syncEnabled } from "./M02_SO_Def_Reflect";
import { differs, setReflect, tidy } from "./M02_SO_Def_Reflect";
import { buildReflectBlock, buildTable } from "./M02_SO_Def_Table";
import { checkBox, textBox } from "./M02_SO_Def_Ui";
import { buildGroupBlock } from "./M02_SO_Groups";
@@ -50,17 +50,6 @@ export interface DefScreenOptions {
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
/** 저장할 꼴 — 목록 + enabled 함께(322-2) · 빈 묶음 걷어냄(그룹 목록 · 배치 · 종류 덧칸 · 표시 · 반영) */
function tidy(def: StructDef): StructDef {
const out = clone(def);
syncEnabled(out);
const reflect = (out.반영 ?? {}) as Record<string, unknown>;
for (const part of [out.그룹, out.배치, out.종류, out.표시, reflect] as Record<string, unknown>[])
for (const [k, v] of Object.entries(part))
if (Array.isArray(v) ? !v.length : !Object.keys(v as object).length) delete part[k];
return out;
}
/** 작은 단추 */
function smallButton(
label: string,
@@ -118,7 +107,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const panes = el("div", { className: "m02-opts__panes", children: [kindList, detail] });
const changed = (): void => {
dirty = true;
dirty = differs(work, base);
sync();
paint();
};
@@ -17,6 +17,7 @@ import {
resolveCatalog,
type CatalogDefinition,
} from "../../common_util/common_util_structure_catalog";
import { OWNER_KO, PLACE_KO, shortName } from "./M02_SO_Def_Reflect";
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
interface Part {
@@ -52,6 +53,7 @@ export interface CheckDef extends CatalogDefinition {
choices?: readonly string[];
default?: unknown;
empty_means?: string | null;
label?: string;
}>;
}>;
표시?: Record<string, Record<string, boolean>>;
@@ -123,7 +125,7 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
const row = rows.get(t.type_id);
if (!row) continue;
if (row.shape === "none" && row.station !== "none")
out.push(`「${row.name}」 형상 none — 횡단도도 none 이어야 함`);
out.push(`「${row.name}」 형상이 「없음」이면 횡단도도 「안 만듦」`);
const kept = hiddenIn(before ?? doc, t.type_id); // 앞 판 없음 = 새로 숨긴 것 없음
const hidden = hiddenIn(doc, t.type_id).filter((key) => !kept.includes(key));
// 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
@@ -132,6 +134,8 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
.filter((o) => (o.default === null || o.default === undefined) && !o.empty_means)
.map((o) => o.key),
);
const labelOf = (key: string): string =>
(t.options ?? []).find((o) => o.key === key)?.label ?? key;
const used: Array<[Branch, string]> = [
["횡단도", row.station],
["카드이름", row.title],
@@ -141,13 +145,16 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
for (const [branch, id] of used) {
const part = partOf(branch, id);
if (!part) continue;
const name = shortName(part.이름);
const place = PLACE_KO[row.placement] ?? row.placement;
const owner = OWNER_KO[row.owner] ?? row.owner;
if (!part.자리꼴.includes(row.placement) || !part.정본.includes(row.owner))
out.push(
`「${row.name}」 ${branch} 「${id}」 — 자리꼴 ${row.placement} · 정본 ${row.owner} 에 안 맞음`,
);
out.push(`「${row.name}」 ${branch} 「${name}」 — ${place} · ${owner} 에 안 맞음`);
for (const key of part["읽는 칸"])
if (keys.has(key) && hidden.includes(key))
out.push(`「${row.name}」 ${branch} 「${id}」 이 읽는 칸 「${key}」 — 숨길 수 없음`);
out.push(
`「${row.name}」 ${branch} 「${name}」 이 쓰는 칸 「${labelOf(key)}」 — 숨길 수 없음`,
);
}
}
return out;
@@ -159,8 +166,9 @@ function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
const now = new Map((doc.types ?? []).map((t) => [t.type_id, t]));
for (const old of before.types ?? []) {
const t = now.get(old.type_id);
const name = old.name ?? old.type_id;
if (!t) {
out.push(`종류 「${old.type_id}」 — 지울 수 없음`);
out.push(`종류 「${name}」 — 지울 수 없음`);
continue;
}
const fixed: Array<[string, unknown, unknown]> = [
@@ -168,8 +176,7 @@ function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
["정본", old.managed_by ?? null, t.managed_by ?? null],
["군 글자", old.group, t.group],
];
for (const [what, a, b] of fixed)
if (a !== b) out.push(`「${old.type_id}」 ${what} — 만든 뒤 고정`);
for (const [what, a, b] of fixed) if (a !== b) out.push(`「${name}」 ${what} — 만든 뒤 고정`);
const options = new Map((t.options ?? []).map((o) => [o.key, o]));
for (const o of old.options ?? []) {
const n = options.get(o.key);
@@ -177,7 +184,7 @@ function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
const a = [...(o.choices ?? [])].sort().join("\u0000");
const b = [...(n.choices ?? [])].sort().join("\u0000");
if (a !== b)
out.push(`「${old.type_id}.${o.key}」 선택지 값 — 고칠 수 없음(이름은 이름 표로)`);
out.push(`「${name}」 「${o.label ?? o.key}」 선택지 값 — 고칠 수 없음(이름만 고침)`);
}
}
return out;
@@ -7,7 +7,15 @@
import { el } from "@ui/ui_template_elements";
import type { StructDef } from "./M02_SO_Def_Model";
import { PART_BOX, partsOff, setPartOn, usersOf } from "./M02_SO_Def_Reflect";
import {
OWNER_KO,
PART_BOX,
partsOff,
PLACE_KO,
setPartOn,
shortName,
usersOf,
} from "./M02_SO_Def_Reflect";
import { checkBox, head } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
@@ -20,17 +28,26 @@ export function buildParts(opt: {
}): HTMLElement {
const { def, edit, changed } = opt;
const off = partsOff(def);
/** 옵션 키 → 정의 이름(단위) — 그 부품을 쓰는 종류의 칸 이름 먼저 · 없으면 아무 종류 · 그래도 없으면 키. */
const labelOf = (key: string, users: readonly string[]): string => {
const types = [
...def.types.filter((t) => users.includes(t.type_id)),
...def.types.filter((t) => !users.includes(t.type_id)),
];
const o = types.flatMap((t) => t.options).find((x) => x.key === key);
return o ? (o.unit ? `${o.label} (${o.unit})` : o.label) : key;
};
const blocks = (Object.keys(PART_BOX) as Branch[]).flatMap((branch) => [
...head(branch),
el("table", {
className: "m02-sod__table",
className: "m02-sod__table m02-sod__table--wrap",
attrs: { "data-sod-parts": branch },
children: [
el("thead", {
children: [
el("tr", {
children: ["", "ColName", "PartRead", "PartFits", "PartUsers", "PartOn"].map((key) =>
el("th", { text: key ? to(key as "ColName") : "" }),
children: (["ColName", "PartRead", "PartFits", "PartUsers", "PartOn"] as const).map(
(key) => el("th", { text: to(key) }),
),
}),
],
@@ -44,10 +61,24 @@ export function buildParts(opt: {
attrs: { "data-sod-part": key },
className: off[key] === false ? "is-off" : "",
children: [
el("td", { children: [el("small", { className: "m02-opts__key", text: id })] }),
el("td", { text: part.이름 }),
el("td", { text: part["읽는 칸"].join(" · ") || "—" }),
el("td", { text: `${part.자리꼴.join(" · ")} / ${part.정본.join(" · ")}` }),
// 영어 키는 툴팁만(322-2 다듬기) — 이름 = 짧은 이름 · 읽는 칸 = 정의 칸 이름
el("td", { text: shortName(part.이름), attrs: { title: `${id} · ${part.이름}` } }),
el("td", {
text:
part["읽는 칸"]
.map((k) =>
labelOf(
k,
users.map((row) => row.typeId),
),
)
.join(" · ") || "—",
}),
el("td", {
text: `${part.자리꼴.map((p) => PLACE_KO[p] ?? p).join(" · ")} / ${part.정본
.map((o) => OWNER_KO[o] ?? o)
.join(" · ")}`,
}),
el("td", { text: users.map((row) => row.name).join(" · ") || "—" }),
el("td", {
children: [
@@ -14,7 +14,7 @@ import {
type CatalogDefinition,
type CatalogRow,
} from "../../common_util/common_util_structure_catalog";
import type { StructDef } from "./M02_SO_Def_Model";
import { clone, type StructDef } from "./M02_SO_Def_Model";
export type ReflectField = "목록" | "횡단도" | "카드이름" | "형상" | "3D";
type Reflect = Partial<Record<ReflectField, string>> & { 형상칸?: Record<string, unknown> };
@@ -29,6 +29,11 @@ type Branch = Exclude<ReflectField, "목록">;
export const PART_BOX = (PARTS as unknown as { parts: Record<Branch, Record<string, Part>> }).parts;
const START = (PARTS as unknown as { 처음값: Record<string, Reflect> }).처음값;
/** 화면 글(322-2 다듬기 · 영어 키 안 보임) — 자리꼴 · 정본 · 부품 짧은 이름(괄호 설명은 툴팁). */
export const PLACE_KO: Record<string, string> = { point: "점", interval: "구간", site: "터" };
export const OWNER_KO: Record<string, string> = { pipe_points: "관 지점", structures: "구조물" };
export const shortName = (full: string): string => full.replace(/\s*\(.*\)\s*$/, "");
/** 칸 → 표 줄 열쇠. */
const ROW_KEY: Record<ReflectField, keyof CatalogRow> = {
목록: "list",
@@ -174,3 +179,36 @@ export function setShown(
}
if (!Object.keys(row).length) delete def.표시[typeId];
}
/** 저장할 꼴 — 목록 + enabled 함께(322-2) · 빈 묶음 걷어냄(그룹 목록 · 배치 · 종류 덧칸 · 표시 · 반영) */
export function tidy(def: StructDef): StructDef {
const out = clone(def);
syncEnabled(out);
const reflect = (out.반영 ?? {}) as Record<string, unknown>;
for (const part of [out.그룹, out.배치, out.종류, out.표시, reflect] as Record<string, unknown>[])
for (const [k, v] of Object.entries(part))
if (Array.isArray(v) ? !v.length : !Object.keys(v as object).length) delete part[k];
return out;
}
/** 열쇠 차례 무관 글 — 같은 값이면 같은 글. */
const stable = (value: unknown): string =>
Array.isArray(value)
? `[${value.map(stable).join(",")}]`
: value && typeof value === "object"
? `{${Object.keys(value)
.sort()
.map((k) => `${JSON.stringify(k)}:${stable((value as Record<string, unknown>)[k])}`)
.join(",")}}`
: JSON.stringify(value ?? null);
/** 저장할 것이 있나 — 저장할 꼴(tidy)끼리 견줌 · 빈 반영 · 부품 칸은 없는 셈(바꿨다 되돌리면 꺼짐 · 322-2). */
export function differs(work: StructDef, base: StructDef): boolean {
const canon = (def: StructDef): string => {
const out = tidy(def) as Record<string, unknown>;
for (const key of ["반영", "부품"])
if (out[key] && !Object.keys(out[key] as object).length) delete out[key];
return stable(out);
};
return canon(work) !== canon(base);
}
@@ -9,9 +9,13 @@
import { createButton, createTag, el } from "@ui/ui_template_elements";
import type { CatalogRow } from "../../common_util/common_util_structure_catalog";
import { stepFamily, stepType, type StructDef, type TypeDef } from "./M02_SO_Def_Model";
import { REVET_FORMS } from "../../B06_Section/B06_Section_UI_Cross_Culvert_Const";
import {
isSet,
OWNER_KO,
PART_BOX,
PLACE_KO,
shortName,
resetRow,
rowsOf,
setReflect,
@@ -28,14 +32,26 @@ const LIST_PAIRS = (): Pairs => [
["b06", to("ListB06")],
["hidden", to("ListHidden")],
];
/** 갈래 고르기 — 없음(카드 이름은 종류 이름) + 부품 상자 차례. */
/** 갈래 고르기 — 없음(횡단도 = 안 만듦 · 카드 이름 = 종류 이름) + 부품 상자 차례 · 글 = 짧은 이름
* (영어 키 · 괄호 설명은 툴팁 · 322-2 다듬기). */
const OFF_TEXT: Record<Exclude<ReflectField, "목록">, () => string> = {
횡단도: () => to("StationNone"),
카드이름: () => to("TitleName"),
형상: () => to("OffNone"),
"3D": () => to("OffNone"),
};
const partPairs = (branch: Exclude<ReflectField, "목록">): Pairs => [
[
branch === "카드이름" ? "name" : "none",
branch === "카드이름" ? to("TitleName") : to("OffNone"),
],
...Object.entries(PART_BOX[branch]).map(([id, part]) => [id, `${id} · ${part.이름}`] as const),
[branch === "카드이름" ? "name" : "none", OFF_TEXT[branch]()],
...Object.entries(PART_BOX[branch]).map(([id, part]) => [id, shortName(part.이름)] as const),
];
/** 고르기 항목 툴팁 = 키 · 부품 긴 이름. */
function tipOptions(box: HTMLSelectElement, branch: ReflectField): void {
if (branch === "목록") return;
for (const option of Array.from(box.options)) {
const part = PART_BOX[branch][option.value];
option.title = part ? `${option.value} · ${part.이름}` : option.value;
}
}
interface CellCtx {
def: StructDef;
@@ -58,6 +74,7 @@ function reflectCell(ctx: CellCtx, field: ReflectField): HTMLElement {
},
{ edit, attr: `data-sod-reflect-${field}` },
);
tipOptions(box, field);
return mark(isSet(def, type.type_id, field), box);
}
@@ -69,24 +86,22 @@ function mark(changed: boolean, ...children: HTMLElement[]): HTMLElement {
});
}
/** 형상칸 — 벽(형상 wall)만: 기본 형태 글 · 물빼기. */
/** 형상칸 — 벽(형상 wall)만: 기본 형태(그림 형태 고르기) · 물빼기. */
function shapeOptsCell(ctx: CellCtx): HTMLElement {
const { def, type, row, edit, changed } = ctx;
if (row.shape !== "wall") return el("span", { className: "m02-side__empty", text: "—" });
const form = String(row.shapeOpts.form ?? "");
const forms = !form || REVET_FORMS.includes(form) ? REVET_FORMS : [form, ...REVET_FORMS];
const box = pickBox(
forms.map((f) => [f, f] as const),
form,
(v) => (setShapeOpt(def, type.type_id, "form", v), changed()),
{ edit, attr: "data-sod-shape-form" },
);
box.title = to("WallForm");
return mark(
isSet(def, type.type_id, "형상칸"),
textBox(
form,
() => undefined,
() => undefined,
{
edit,
attr: "data-sod-shape-form",
place: to("WallForm"),
max: 20,
},
),
box,
checkBox(
to("Weep"),
row.shapeOpts.weep === true,
@@ -96,15 +111,6 @@ function shapeOptsCell(ctx: CellCtx): HTMLElement {
);
}
/** 형상칸 글은 떠날 때 씀(초점 안 끊김). */
function bindForm(node: HTMLElement, ctx: CellCtx): void {
const box = node.querySelector<HTMLInputElement>("[data-sod-shape-form]");
box?.addEventListener("change", () => {
setShapeOpt(ctx.def, ctx.type.type_id, "form", box.value.trim());
ctx.changed();
});
}
/** 알약 색(style.color). */
function colorCell(ctx: CellCtx): HTMLElement {
const box = el("input", {
@@ -121,9 +127,11 @@ function colorCell(ctx: CellCtx): HTMLElement {
return box;
}
const fixed = (text: string): HTMLElement =>
/** 자리꼴 · 정본 = 한국어 글 · 「고정」(키는 툴팁). */
const fixed = (text: string, key: string): HTMLElement =>
el("span", {
className: "m02-sod__fixed",
attrs: { title: key },
children: [el("span", { text }), createTag(to("Fixed"), "neutral")],
});
@@ -137,16 +145,14 @@ export function reflectCells(
const row = rowsOf(def).get(type.type_id);
if (!row) return [];
const ctx = { def, type, row, edit, changed };
const opts = shapeOptsCell(ctx);
bindForm(opts, ctx);
return [
[to("ColList"), reflectCell(ctx, "목록")],
[to("ColPlacement"), fixed(row.placement)],
[to("ColOwner"), fixed(row.owner)],
[to("ColPlacement"), fixed(PLACE_KO[row.placement] ?? row.placement, row.placement)],
[to("ColOwner"), fixed(OWNER_KO[row.owner] ?? row.owner, row.owner)],
[to("ColStation"), reflectCell(ctx, "횡단도")],
[to("ColTitle"), reflectCell(ctx, "카드이름")],
[to("ColShape"), reflectCell(ctx, "형상")],
[to("ColShapeOpts"), opts],
[to("ColShapeOpts"), shapeOptsCell(ctx)],
[to("ColColor"), colorCell(ctx)],
[to("Col3D"), reflectCell(ctx, "3D")],
];
@@ -232,8 +238,8 @@ export function buildTable(opt: {
children: [
td(""),
el("td", {
attrs: { title: type.type_id }, // 영어 키는 툴팁만(322-2 다듬기)
children: [
el("small", { className: "m02-opts__key", text: type.type_id }),
...(edit
? [
moveButton(
@@ -432,3 +432,15 @@
align-items: center;
gap: var(--spacing-4);
}
/* 부품 탭 — 긴 글은 줄바꿈해 표가 화면 안에(322-2 다듬기) */
.m02-sod__table--wrap {
table-layout: fixed;
width: 100%;
}
.m02-sod__table--wrap td,
.m02-sod__table--wrap th {
white-space: normal;
word-break: keep-all;
overflow-wrap: anywhere;
}
@@ -149,6 +149,7 @@ const TEXT = {
ListB06: ["B06 만", "B06 only"],
ListHidden: ["안 보임", "Hidden"],
OffNone: ["없음", "None"],
StationNone: ["안 만듦", "Not made"],
TitleName: ["종류 이름", "Type name"],
Default: ["기본", "Default"],
Fixed: ["고정", "Fixed"],
+1 -1
View File
@@ -184,7 +184,7 @@
"정본": ["pipe_points"]
},
"wall": {
"이름": "벽 스윕",
"이름": "벽",
"읽는 칸": [],
"자리꼴": ["interval"],
"정본": ["pipe_points", "structures"]
+18
View File
@@ -88,6 +88,24 @@ out.partOff = [clone(plain.부품), reflect.rowsOf(plain).get("retaining_wall").
reflect.setPartOn(plain, "형상.wall", true);
out.partOn = clone(plain.부품);
// 바꿨다 되돌리면 저장할 것 없음(322-2 다듬기) — 목록 · 횡단도 · 형상칸 · 보임 · 부품
const base = clone(input.definition);
const work = clone(input.definition);
const steps = [];
reflect.setReflect(work, "masonry_wet", "목록", "both");
steps.push(reflect.differs(work, base));
reflect.setReflect(work, "masonry_wet", "목록", "hidden");
reflect.setReflect(work, "open_ditch", "횡단도", "anchor");
reflect.setReflect(work, "open_ditch", "횡단도", "none");
reflect.setShapeOpt(work, "soil_guard", "form", "콘크리트");
reflect.setShapeOpt(work, "soil_guard", "form", "통나무·목재틀");
reflect.setShown(work, "pipe", ["wing_wall_type"], false);
reflect.setShown(work, "pipe", ["wing_wall_type"], true);
reflect.setPartOn(work, "3D.chute", false);
reflect.setPartOn(work, "3D.chute", true);
steps.push(reflect.differs(work, base));
out.revert = steps;
// 명세 폼 숨김 — 저장 · 갈래 키가 모두 숨김인 칸 · 키 있는 칸이 모두 숨긴 상자
out.spec = {};
for (const [tid, sp] of Object.entries(STRUCTURE_SPECS)) {
+16 -14
View File
@@ -123,27 +123,25 @@ def test_check_rules(ts: dict) -> None:
"부품 「횡단도.span3」 — 참거짓",
"부품 「끔」 — 없는 부품",
]
assert got["rule218"] == ["「개거(겉도랑)」 형상 none — 횡단도도 none 이어야 함"]
assert got["mismatch"] == [
"「옹벽」 형상 「pipe_set」 — 자리꼴 interval · 정본 structures 에 안 맞음"
]
assert got["rule218"] == ["「개거(겉도랑)」 형상이 「없음」이면 횡단도도 「안 만듦」"]
assert got["mismatch"] == ["「옹벽」 형상 「배관 세트」 — 구간 · 구조물 에 안 맞음"]
assert got["hide_read"] == [
"「배관」 형상 「pipe_set」 이 읽는 칸 「revet_foundation」 — 숨길 수 없음"
"「배관」 형상 「배관 세트」 이 쓰는 칸 「흙막이 기초」 — 숨길 수 없음"
]
assert got["hide_no_before"] == [] # 앞 판 없음 = 새로 숨긴 것 없음
assert got["fixed"] == [
# 표 줄도 함께 걸림 — 자리꼴 · 정본을 바꾸면 그 종류 부품이 안 맞음
"「BOX암거」 형상 「box」 — 자리꼴 interval · 정본 pipe_points 에 안 맞음",
"「BOX암거」 3D 「box」 — 자리꼴 interval · 정본 pipe_points 에 안 맞음",
"「기슭막이」 형상 「hidden_revet」 — 자리꼴 interval · 정본 structures 에 안 맞음",
"「pipe.pipe_diameter_mm」 선택지 값 — 고칠 수 없음(이름은 이름 표로)",
"「box_culvert」 자리꼴 — 만든 뒤 고정",
"「revetment」 정본 — 만든 뒤 고정",
"「mirror」 군 글자 — 만든 뒤 고정",
"종류 「etc」 — 지울 수 없음",
"「BOX암거」 형상 「BOX 본체 · 날개벽」 — 구간 · 관 지점 에 안 맞음",
"「BOX암거」 3D 「BOX 본체 · 날개벽」 — 구간 · 관 지점 에 안 맞음",
"「기슭막이」 형상 「독립 기슭막이」 — 구간 · 구조물 에 안 맞음",
"「배관」 「관경」 선택지 값 — 고칠 수 없음(이름만 고침)",
"「BOX암거」 자리꼴 — 만든 뒤 고정",
"「기슭막이」 정본 — 만든 뒤 고정",
"「반사경」 군 글자 — 만든 뒤 고정",
"종류 「기타」 — 지울 수 없음",
]
# 형상 부품을 끄면 벽 다섯은 형상 none — 횡단도도 none 으로 바꿔야 저장됨
assert len(got["parts_off"]) == 5 and all("횡단도도 none" in t for t in got["parts_off"])
assert len(got["parts_off"]) == 5 and all("횡단도도 「안 만듦」" in t for t in got["parts_off"])
def test_reflect_edits(ts: dict) -> None:
@@ -165,6 +163,10 @@ def test_reflect_edits(ts: dict) -> None:
assert r["reset"] == [None, None, True]
def test_revert_means_nothing_to_save(ts: dict) -> None:
assert ts["revert"] == [True, False] # 바꾸면 저장할 것 · 되돌리면 없음
def test_save_writes_list_and_enabled_together(ts: dict) -> None:
sync = ts["sync"]
assert sync["same"] is True