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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
"정본": ["pipe_points"]
|
||||
},
|
||||
"wall": {
|
||||
"이름": "벽 스윕",
|
||||
"이름": "벽",
|
||||
"읽는 칸": [],
|
||||
"자리꼴": ["interval"],
|
||||
"정본": ["pipe_points", "structures"]
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user