diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Check.py b/M02_MasterTemplete/StructOptions/M02_SO_Check.py index 9a865d192..a8a9c9b70 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Check.py +++ b/M02_MasterTemplete/StructOptions/M02_SO_Check.py @@ -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 diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts index 5e7512293..2d5636210 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts @@ -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; - for (const part of [out.그룹, out.배치, out.종류, out.표시, reflect] as Record[]) - 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(); }; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Check.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Check.ts index 1c4583774..f843d0031 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Check.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Check.ts @@ -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>; @@ -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; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Parts.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Parts.ts index 21fc3467b..7bc028558 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Parts.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Parts.ts @@ -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: [ diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Reflect.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Reflect.ts index b89de8151..295f569df 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Reflect.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Reflect.ts @@ -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 }; @@ -29,6 +29,11 @@ type Branch = Exclude; export const PART_BOX = (PARTS as unknown as { parts: Record> }).parts; const START = (PARTS as unknown as { 처음값: Record }).처음값; +/** 화면 글(322-2 다듬기 · 영어 키 안 보임) — 자리꼴 · 정본 · 부품 짧은 이름(괄호 설명은 툴팁). */ +export const PLACE_KO: Record = { point: "점", interval: "구간", site: "터" }; +export const OWNER_KO: Record = { pipe_points: "관 지점", structures: "구조물" }; +export const shortName = (full: string): string => full.replace(/\s*\(.*\)\s*$/, ""); + /** 칸 → 표 줄 열쇠. */ const ROW_KEY: Record = { 목록: "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; + for (const part of [out.그룹, out.배치, out.종류, out.표시, reflect] as Record[]) + 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)[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; + 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); +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Table.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Table.ts index 0cdb8c753..c5cd176b7 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Table.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Table.ts @@ -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, () => string> = { + 횡단도: () => to("StationNone"), + 카드이름: () => to("TitleName"), + 형상: () => to("OffNone"), + "3D": () => to("OffNone"), +}; const partPairs = (branch: Exclude): 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("[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( diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Style.css b/M02_MasterTemplete/StructOptions/M02_SO_Style.css index 726c63bc9..d654c0980 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Style.css +++ b/M02_MasterTemplete/StructOptions/M02_SO_Style.css @@ -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; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts index 7ebfdd906..097e3e226 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts @@ -149,6 +149,7 @@ const TEXT = { ListB06: ["B06 만", "B06 only"], ListHidden: ["안 보임", "Hidden"], OffNone: ["없음", "None"], + StationNone: ["안 만듦", "Not made"], TitleName: ["종류 이름", "Type name"], Default: ["기본", "Default"], Fixed: ["고정", "Fixed"], diff --git a/common_util/common_util_structure_parts.json b/common_util/common_util_structure_parts.json index 4eab9edb8..067c63f89 100644 --- a/common_util/common_util_structure_parts.json +++ b/common_util/common_util_structure_parts.json @@ -184,7 +184,7 @@ "정본": ["pipe_points"] }, "wall": { - "이름": "벽 스윕", + "이름": "벽", "읽는 칸": [], "자리꼴": ["interval"], "정본": ["pipe_points", "structures"] diff --git a/resources/tester/helper_322_2_check.cjs b/resources/tester/helper_322_2_check.cjs index 710dcf345..faf8f8b8a 100644 --- a/resources/tester/helper_322_2_check.cjs +++ b/resources/tester/helper_322_2_check.cjs @@ -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)) { diff --git a/resources/tester/test_322_2_check_mirror.py b/resources/tester/test_322_2_check_mirror.py index d3557175c..e4380b2dc 100644 --- a/resources/tester/test_322_2_check_mirror.py +++ b/resources/tester/test_322_2_check_mirror.py @@ -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