feat(구조물표): 322-2 M02 표 화면 — 탭 셋(한눈 표 · 종류별 · 부품) · 반영 · 부품 · 알약 색 저장 · 목록 + enabled 함께 · 검사 짝(화면 = 서버 · 1-3 규칙 · 만든 뒤 고정) · 명세 폼 보임 따름 · 판 알림
- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로] - 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만 - 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만) - 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참 - 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만 - 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름 - 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번 - 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""구조물 표 검사(PLAN 322-2 · 1-3 규칙) — 반영 · 부품 꼴 · 표 줄과 부품이 맞음 · 만든 뒤 고정.
|
||||
|
||||
- 반영 = `{종류: {목록 · 횡단도 · 카드이름 · 형상 · 형상칸 · 3D}}` · 값 = 부품 상자에 있는 것
|
||||
- 부품 = `{"갈래.부품": false}`(끈 것만)
|
||||
- 표 줄(정의 ⊕ 처음값) — 형상 none 이면 횡단도도 none(218) · 부품 자리꼴 · 정본 = 그 종류 줄 ·
|
||||
부품이 읽는 칸 중 기본값 없는 칸(비워 두는 뜻 칸 빼고)은 새로 숨기지 못함(정의 「표시」 · 숨기면
|
||||
그림이 늘 기본값을 씀 · 앞 판에서 이미 숨긴 칸은 그대로 둠)
|
||||
- 앞 판이 없으면(옛 판 읽기) 꼴만 검사
|
||||
- 앞 판과 비교 — 종류 지우기 없음 · 자리꼴 · 정본 · 군 글자 고정 · 선택지 값 고정(이름은 이름 표로)
|
||||
화면 짝 = `M02_SO_Def_Check.ts`(같은 글)
|
||||
거울 시험 = `resources/tester/test_322_2_check_mirror.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_structure_catalog import load_parts, resolve_catalog
|
||||
|
||||
BRANCHES = ("횡단도", "카드이름", "형상", "3D")
|
||||
OFF_VALUE = {"횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none"}
|
||||
LISTS = ("both", "b06", "hidden")
|
||||
_FIELDS = {"목록", "형상칸", *BRANCHES}
|
||||
|
||||
|
||||
def _shape_errors(doc: dict[str, Any], known: set[str]) -> list[str]:
|
||||
"""반영 · 부품 꼴."""
|
||||
parts = load_parts()["parts"]
|
||||
out: list[str] = []
|
||||
reflect = doc.get("반영", {})
|
||||
if not isinstance(reflect, dict):
|
||||
out.append("반영은 {종류: 묶음} 이어야 함")
|
||||
else:
|
||||
for tid, row in reflect.items():
|
||||
if tid not in known or not isinstance(row, dict):
|
||||
out.append(f"반영 「{tid}」 — 없는 종류이거나 묶음 아님")
|
||||
continue
|
||||
for field, value in row.items():
|
||||
where = f"반영 「{tid}.{field}」"
|
||||
if field not in _FIELDS:
|
||||
out.append(f"{where} — 모르는 칸")
|
||||
elif field == "목록":
|
||||
if value not in LISTS:
|
||||
out.append(f"{where} — both · b06 · hidden 중 하나")
|
||||
elif field == "형상칸":
|
||||
ok = isinstance(value, dict) and all(
|
||||
(isinstance(v, str) and bool(v.strip()))
|
||||
if k == "form"
|
||||
else (k == "weep" and isinstance(v, bool))
|
||||
for k, v in value.items()
|
||||
)
|
||||
if not ok:
|
||||
out.append(f"{where} — form 글 · weep 참거짓")
|
||||
elif not isinstance(value, str) or (
|
||||
value != OFF_VALUE[field] and value not in parts[field]
|
||||
):
|
||||
shown = value if isinstance(value, str) else _js(value)
|
||||
out.append(f"{where} — 없는 부품 「{shown}」")
|
||||
off = doc.get("부품", {})
|
||||
if not isinstance(off, dict):
|
||||
out.append("부품은 {갈래.부품: 참거짓} 이어야 함")
|
||||
else:
|
||||
for key, value in off.items():
|
||||
branch, _, part = key.partition(".")
|
||||
if part not in parts.get(branch, {}):
|
||||
out.append(f"부품 「{key}」 — 없는 부품")
|
||||
elif not isinstance(value, bool):
|
||||
out.append(f"부품 「{key}」 — 참거짓")
|
||||
return out
|
||||
|
||||
|
||||
def _js(value: Any) -> str:
|
||||
"""TS `String(value)` 와 같은 글(검사 글 짝)."""
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (list, tuple)):
|
||||
return ",".join(_js(v) for v in value)
|
||||
if isinstance(value, dict):
|
||||
return "[object Object]"
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return str(int(value))
|
||||
return str(value)
|
||||
|
||||
|
||||
def _hidden_in(doc: dict[str, Any] | None, tid: str) -> list[str]:
|
||||
row = ((doc or {}).get("표시") or {}).get(tid) or {}
|
||||
return [key for key, shown in row.items() if shown is False]
|
||||
|
||||
|
||||
def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]:
|
||||
"""표 줄 — 218 규칙 · 부품 자리꼴 · 정본 · 읽는 칸 보임(새로 숨김만)."""
|
||||
parts = load_parts()["parts"]
|
||||
rows = resolve_catalog(doc)
|
||||
out: list[str] = []
|
||||
for t in doc.get("types") or []:
|
||||
row = rows.get(t["type_id"])
|
||||
if row is None:
|
||||
continue
|
||||
if row["shape"] == "none" and row["station"] != "none":
|
||||
out.append(f"「{row['name']}」 형상 none — 횡단도도 none 이어야 함")
|
||||
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]
|
||||
# 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
|
||||
keys = {
|
||||
o["key"]
|
||||
for o in t.get("options") or []
|
||||
if o.get("default") is None and not o.get("empty_means")
|
||||
}
|
||||
used = (
|
||||
("횡단도", row["station"]),
|
||||
("카드이름", row["title"]),
|
||||
("형상", row["shape"]),
|
||||
("3D", row["model3d"]),
|
||||
)
|
||||
for branch, part_id in used:
|
||||
part = parts[branch].get(part_id)
|
||||
if part is None:
|
||||
continue
|
||||
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']} 에 안 맞음"
|
||||
)
|
||||
for key in part["읽는 칸"]:
|
||||
if key in keys and key in hidden:
|
||||
out.append(
|
||||
f"「{row['name']}」 {branch} 「{part_id}」 이 읽는 칸 「{key}」 — "
|
||||
"숨길 수 없음"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _fixed_errors(doc: dict[str, Any], before: dict[str, Any]) -> list[str]:
|
||||
"""앞 판과 비교 — 만든 뒤 고정."""
|
||||
out: 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"]
|
||||
t = now.get(tid)
|
||||
if t is None:
|
||||
out.append(f"종류 「{tid}」 — 지울 수 없음")
|
||||
continue
|
||||
fixed = (
|
||||
("자리꼴", old.get("placement"), t.get("placement")),
|
||||
("정본", old.get("managed_by"), t.get("managed_by")),
|
||||
("군 글자", old.get("group"), t.get("group")),
|
||||
)
|
||||
for what, a, b in fixed:
|
||||
if a != b:
|
||||
out.append(f"「{tid}」 {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']}」 선택지 값 — 고칠 수 없음(이름은 이름 표로)")
|
||||
return out
|
||||
|
||||
|
||||
def catalog_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]:
|
||||
"""구조물 표 검사 — 틀린 것 글들(빈 목록 = 통과). `before` = 지금 판(없으면 고정 검사 안 함)."""
|
||||
known = {t["type_id"] for t in doc.get("types") or [] if isinstance(t, dict)}
|
||||
shape = _shape_errors(doc, known)
|
||||
if shape:
|
||||
return shape
|
||||
return _row_errors(doc, before) + _fixed_errors(doc, before) if before else []
|
||||
@@ -1,8 +1,10 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def.ts
|
||||
* 구조물 정의 편집 화면(PLAN 48-2 · 메인 칸) — 왼쪽 구조물군 · 종류 목록(이름 바꿈 · 순서 · 안 씀) ·
|
||||
* 오른쪽 고른 종류(이름 · 안 씀 · 기준 측점 표시 · 옵션 · 배치 격자 · 그룹 · 규칙) ·
|
||||
* 아래 [저장](판 +1 · 시스템 관리자만) · 지난 판 목록(누르면 그 판을 읽기만으로 봄).
|
||||
* 구조물 정의 편집 화면(PLAN 48-2 · 메인 칸) — 탭 셋(322-2): 「한눈 표」(종류마다 반영 한 줄 ·
|
||||
* `M02_SO_Def_Table`) · 「종류별」(왼쪽 구조물군 · 종류 목록 · 오른쪽 고른 종류 이름 · 반영 · 명세 이름 나무 ·
|
||||
* 줄마다 보임 · 미리보기) · 「부품」(`M02_SO_Def_Parts`) ·
|
||||
* 아래 검사(화면 = 서버 짝 `M02_SO_Def_Check`) · [저장](판 +1 · 목록 + enabled 함께 · 시스템 관리자만) ·
|
||||
* 지난 판 목록(누르면 그 판을 읽기만으로 봄).
|
||||
* 다른 역할은 읽기만. 고칠 사본은 저장 전까지 서버에 안 감 · 정본 = `구조물정의.json`(48-1).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -28,6 +30,10 @@ import { buildOptionsBlock } from "./M02_SO_Def_Options";
|
||||
import { buildRulesBlock } from "./M02_SO_Def_Rules";
|
||||
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 { buildReflectBlock, buildTable } from "./M02_SO_Def_Table";
|
||||
import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { buildGroupBlock } from "./M02_SO_Groups";
|
||||
import { to } from "./M02_SO_Text";
|
||||
@@ -44,10 +50,12 @@ 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);
|
||||
for (const part of [out.그룹, out.배치, out.종류, out.표시] as Record<string, unknown>[])
|
||||
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;
|
||||
@@ -88,6 +96,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
let current: string | null = null;
|
||||
const open = new Set<string>(); // 선택지를 펼친 옵션
|
||||
let versions: DefVersion[] | null = null;
|
||||
type Tab = "table" | "kinds" | "parts";
|
||||
let tab: Tab = "table";
|
||||
|
||||
const shown = (): StructDef => past ?? work;
|
||||
const canEdit = (): boolean => opt.isAdmin && !past;
|
||||
@@ -100,6 +110,13 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
attrs: { "data-sod-history": "" },
|
||||
});
|
||||
|
||||
/** 검사 글 — 이름 · 배치 · 규칙(`problems`) + 구조물 표(서버 짝 · 앞 판 = 지금 서버 판) */
|
||||
const allProblems = (): string[] =>
|
||||
past ? [] : [...problems(work), ...catalogProblems(work as CheckDef, base as CheckDef)];
|
||||
const tabBar = el("div", { className: "m02-sod__tabs", attrs: { "data-sod-tabs": "" } });
|
||||
const body = el("div", { className: "m02-sod__body" });
|
||||
const panes = el("div", { className: "m02-opts__panes", children: [kindList, detail] });
|
||||
|
||||
const changed = (): void => {
|
||||
dirty = true;
|
||||
sync();
|
||||
@@ -199,10 +216,13 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
cls: "m02-sod__name",
|
||||
}),
|
||||
el("small", { className: "m02-opts__key", text: type.type_id }),
|
||||
checkBox(to("Unused"), type.enabled === false, (v) => ((type.enabled = !v), changed()), {
|
||||
edit,
|
||||
attr: "data-sod-kind-off",
|
||||
}),
|
||||
// 안 씀 = 목록 B06 만(enabled 는 목록 짝 · 322-2) · 숨김(hidden)은 한눈 표 목록 칸에서
|
||||
checkBox(
|
||||
to("Unused"),
|
||||
type.enabled === false,
|
||||
(v) => (setReflect(d, type.type_id, "목록", v ? "b06" : "both"), changed()),
|
||||
{ edit, attr: "data-sod-kind-off" },
|
||||
),
|
||||
...(!OLD_EDITOR
|
||||
? []
|
||||
: [
|
||||
@@ -225,7 +245,10 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
],
|
||||
}),
|
||||
...(!OLD_EDITOR
|
||||
? [buildSpecBlock({ def: d, type, spec, edit, changed })]
|
||||
? [
|
||||
buildReflectBlock(d, type, edit, changed),
|
||||
buildSpecBlock({ def: d, base, type, spec, edit, changed }),
|
||||
]
|
||||
: [
|
||||
buildOptionsBlock({ def: d, type, edit, open, changed }),
|
||||
buildLayoutBlock({ def: d, type, edit, changed }),
|
||||
@@ -235,6 +258,27 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
);
|
||||
};
|
||||
|
||||
const paintTabs = (): void => {
|
||||
const pairs: Array<[Tab, string]> = [
|
||||
["table", to("TabTable")],
|
||||
["kinds", to("TabKinds")],
|
||||
["parts", to("TabParts")],
|
||||
];
|
||||
tabBar.replaceChildren(
|
||||
...pairs.map(([key, label]) => {
|
||||
const b = createButton({ label, variant: key === tab ? "filled" : "ghost", size: "sm" });
|
||||
b.setAttribute("data-sod-tab", key);
|
||||
b.addEventListener("click", () => ((tab = key), paint()));
|
||||
return b;
|
||||
}),
|
||||
);
|
||||
const edit = canEdit();
|
||||
if (tab === "table")
|
||||
body.replaceChildren(buildTable({ def: shown(), edit, changed, issues: allProblems() }));
|
||||
else if (tab === "parts") body.replaceChildren(buildParts({ def: shown(), edit, changed }));
|
||||
else body.replaceChildren(panes);
|
||||
};
|
||||
|
||||
const paintFoot = (): void => {
|
||||
banner.replaceChildren(
|
||||
...(past
|
||||
@@ -248,7 +292,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
]),
|
||||
);
|
||||
banner.classList.toggle("is-past", !!past);
|
||||
const list = past ? [] : problems(work);
|
||||
const list = allProblems();
|
||||
issues.replaceChildren(...list.map((t) => el("p", { className: "m02-sod__bad", text: t })));
|
||||
save.disabled = !opt.isAdmin || !!past || !dirty || list.length > 0;
|
||||
};
|
||||
@@ -256,6 +300,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
const paint = (): void => {
|
||||
paintKinds();
|
||||
paintDetail();
|
||||
paintTabs();
|
||||
paintFoot();
|
||||
};
|
||||
|
||||
@@ -273,7 +318,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
const save = createButton({ label: to("Save"), variant: "filled" });
|
||||
save.setAttribute("data-sod-save", "");
|
||||
save.addEventListener("click", async () => {
|
||||
if (problems(work).length) return;
|
||||
if (allProblems().length) return;
|
||||
save.disabled = true; // 연달아 눌러도 한 번만
|
||||
try {
|
||||
base = await saveStructureDef(base.판, tidy(work));
|
||||
@@ -339,7 +384,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
children: [
|
||||
el("p", { className: "m02-side__empty", text: to("DefNote") }),
|
||||
banner,
|
||||
el("div", { className: "m02-opts__panes", children: [kindList, detail] }),
|
||||
tabBar,
|
||||
body,
|
||||
issues,
|
||||
el("div", {
|
||||
className: "m02-side__row",
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Check.ts
|
||||
* 구조물 표 검사(PLAN 322-2 · 1-3 규칙) — 정의 `반영` · `부품` 꼴 · 표 줄과 부품이 맞음 · 만든 뒤 고정.
|
||||
*
|
||||
* · 반영 = `{종류: {목록 · 횡단도 · 카드이름 · 형상 · 형상칸 · 3D}}` · 값 = 부품 상자에 있는 것
|
||||
* · 부품 = `{"갈래.부품": false}`(끈 것만)
|
||||
* · 표 줄(정의 ⊕ 처음값) — 형상 none 이면 횡단도도 none(218) · 부품 자리꼴 · 정본 = 그 종류 줄 ·
|
||||
* 부품이 읽는 칸 중 기본값 없는 칸(비워 두는 뜻 칸 빼고)은 새로 숨기지 못함(정의 「표시」 · 숨기면
|
||||
* 그림이 늘 기본값을 씀 · 앞 판에서 이미 숨긴 칸은 그대로 둠)
|
||||
* · 앞 판이 없으면(옛 판 읽기) 꼴만 검사
|
||||
* · 앞 판과 비교 — 종류 지우기 없음 · 자리꼴 · 정본 · 군 글자 고정 · 선택지 값 고정(이름은 이름 표로)
|
||||
* 서버 짝 = `M02_SO_Check.py`(같은 글) · 거울 시험 = `resources/tester/test_322_2_check_mirror.py`.
|
||||
* ========================================================================== */
|
||||
|
||||
import PARTS from "../../common_util/common_util_structure_parts.json" with { type: "json" };
|
||||
import {
|
||||
resolveCatalog,
|
||||
type CatalogDefinition,
|
||||
} from "../../common_util/common_util_structure_catalog";
|
||||
|
||||
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
|
||||
interface Part {
|
||||
이름: string;
|
||||
"읽는 칸": string[];
|
||||
자리꼴: string[];
|
||||
정본: string[];
|
||||
}
|
||||
const PARTS_BY = (PARTS as unknown as { parts: Record<Branch, Record<string, Part>> }).parts;
|
||||
|
||||
export const BRANCHES: readonly Branch[] = ["횡단도", "카드이름", "형상", "3D"];
|
||||
/** 갈래마다 「없음」 값 — 카드이름은 종류 이름(name). */
|
||||
export const OFF_VALUE: Record<Branch, string> = {
|
||||
횡단도: "none",
|
||||
카드이름: "name",
|
||||
형상: "none",
|
||||
"3D": "none",
|
||||
};
|
||||
export const LISTS = ["both", "b06", "hidden"] as const;
|
||||
const FIELDS = new Set(["목록", "형상칸", ...BRANCHES]);
|
||||
|
||||
/** 검사가 읽는 정의 칸. */
|
||||
export interface CheckDef extends CatalogDefinition {
|
||||
types?: ReadonlyArray<{
|
||||
type_id: string;
|
||||
name?: string;
|
||||
group?: string;
|
||||
placement?: string;
|
||||
managed_by?: string | null;
|
||||
options?: ReadonlyArray<{
|
||||
key: string;
|
||||
input?: string;
|
||||
choices?: readonly string[];
|
||||
default?: unknown;
|
||||
empty_means?: string | null;
|
||||
}>;
|
||||
}>;
|
||||
표시?: Record<string, Record<string, boolean>>;
|
||||
}
|
||||
|
||||
const isObject = (v: unknown): v is Record<string, unknown> =>
|
||||
typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
/** 부품 상자에 있는 부품 — 제 칸만(물려받은 이름 아님). */
|
||||
const partOf = (branch: string, id: string): Part | undefined => {
|
||||
const box = (PARTS_BY as Record<string, Record<string, Part>>)[branch];
|
||||
return box && Object.hasOwn(box, id) ? box[id] : undefined;
|
||||
};
|
||||
|
||||
/** 반영 · 부품 꼴. */
|
||||
function shapeErrors(doc: CheckDef, known: Set<string>): string[] {
|
||||
const out: string[] = [];
|
||||
const reflect: unknown = doc.반영 ?? {};
|
||||
if (!isObject(reflect)) out.push("반영은 {종류: 묶음} 이어야 함");
|
||||
else
|
||||
for (const [tid, row] of Object.entries(reflect)) {
|
||||
if (!known.has(tid) || !isObject(row)) {
|
||||
out.push(`반영 「${tid}」 — 없는 종류이거나 묶음 아님`);
|
||||
continue;
|
||||
}
|
||||
for (const [field, value] of Object.entries(row)) {
|
||||
const where = `반영 「${tid}.${field}」`;
|
||||
if (!FIELDS.has(field)) out.push(`${where} — 모르는 칸`);
|
||||
else if (field === "목록") {
|
||||
if (!(LISTS as readonly unknown[]).includes(value))
|
||||
out.push(`${where} — both · b06 · hidden 중 하나`);
|
||||
} else if (field === "형상칸") {
|
||||
const ok =
|
||||
isObject(value) &&
|
||||
Object.entries(value).every(([k, v]) =>
|
||||
k === "form"
|
||||
? typeof v === "string" && !!v.trim()
|
||||
: k === "weep" && typeof v === "boolean",
|
||||
);
|
||||
if (!ok) out.push(`${where} — form 글 · weep 참거짓`);
|
||||
} else {
|
||||
const branch = field as Branch;
|
||||
if (typeof value !== "string" || (value !== OFF_VALUE[branch] && !partOf(branch, value)))
|
||||
out.push(`${where} — 없는 부품 「${String(value)}」`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const parts: unknown = doc.부품 ?? {};
|
||||
if (!isObject(parts)) out.push("부품은 {갈래.부품: 참거짓} 이어야 함");
|
||||
else
|
||||
for (const [key, value] of Object.entries(parts)) {
|
||||
const at = key.indexOf(".");
|
||||
if (at < 0 || !partOf(key.slice(0, at), key.slice(at + 1)))
|
||||
out.push(`부품 「${key}」 — 없는 부품`);
|
||||
else if (typeof value !== "boolean") out.push(`부품 「${key}」 — 참거짓`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const hiddenIn = (doc: CheckDef | null, tid: string): string[] =>
|
||||
Object.entries(doc?.표시?.[tid] ?? {})
|
||||
.filter(([, shown]) => shown === false)
|
||||
.map(([key]) => key);
|
||||
|
||||
/** 표 줄 — 218 규칙 · 부품 자리꼴 · 정본 · 읽는 칸 보임(새로 숨김만). */
|
||||
function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
|
||||
const out: string[] = [];
|
||||
const rows = resolveCatalog(doc);
|
||||
for (const t of doc.types ?? []) {
|
||||
const row = rows.get(t.type_id);
|
||||
if (!row) continue;
|
||||
if (row.shape === "none" && row.station !== "none")
|
||||
out.push(`「${row.name}」 형상 none — 횡단도도 none 이어야 함`);
|
||||
const kept = hiddenIn(before ?? doc, t.type_id); // 앞 판 없음 = 새로 숨긴 것 없음
|
||||
const hidden = hiddenIn(doc, t.type_id).filter((key) => !kept.includes(key));
|
||||
// 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
|
||||
const keys = new Set(
|
||||
(t.options ?? [])
|
||||
.filter((o) => (o.default === null || o.default === undefined) && !o.empty_means)
|
||||
.map((o) => o.key),
|
||||
);
|
||||
const used: Array<[Branch, string]> = [
|
||||
["횡단도", row.station],
|
||||
["카드이름", row.title],
|
||||
["형상", row.shape],
|
||||
["3D", row.model3d],
|
||||
];
|
||||
for (const [branch, id] of used) {
|
||||
const part = partOf(branch, id);
|
||||
if (!part) continue;
|
||||
if (!part.자리꼴.includes(row.placement) || !part.정본.includes(row.owner))
|
||||
out.push(
|
||||
`「${row.name}」 ${branch} 「${id}」 — 자리꼴 ${row.placement} · 정본 ${row.owner} 에 안 맞음`,
|
||||
);
|
||||
for (const key of part["읽는 칸"])
|
||||
if (keys.has(key) && hidden.includes(key))
|
||||
out.push(`「${row.name}」 ${branch} 「${id}」 이 읽는 칸 「${key}」 — 숨길 수 없음`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 앞 판과 비교 — 만든 뒤 고정. */
|
||||
function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
|
||||
const out: 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);
|
||||
if (!t) {
|
||||
out.push(`종류 「${old.type_id}」 — 지울 수 없음`);
|
||||
continue;
|
||||
}
|
||||
const fixed: Array<[string, unknown, unknown]> = [
|
||||
["자리꼴", old.placement, t.placement],
|
||||
["정본", 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} — 만든 뒤 고정`);
|
||||
const options = new Map((t.options ?? []).map((o) => [o.key, o]));
|
||||
for (const o of old.options ?? []) {
|
||||
const n = options.get(o.key);
|
||||
if (o.input !== "select" || !n) continue;
|
||||
const a = [...(o.choices ?? [])].sort().join("\u0000");
|
||||
const b = [...(n.choices ?? [])].sort().join("\u0000");
|
||||
if (a !== b)
|
||||
out.push(`「${old.type_id}.${o.key}」 선택지 값 — 고칠 수 없음(이름은 이름 표로)`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 구조물 표 검사 — 틀린 것 글들(빈 목록 = 통과). `before` = 지금 서버 판(없으면 고정 검사 안 함). */
|
||||
export function catalogProblems(doc: CheckDef, before: CheckDef | null): string[] {
|
||||
const known = new Set((doc.types ?? []).map((t) => t.type_id));
|
||||
const shape = shapeErrors(doc, known);
|
||||
if (shape.length) return shape;
|
||||
return before ? [...rowErrors(doc, before), ...fixedErrors(doc, before)] : [];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Parts.ts
|
||||
* 구조물 정의 「부품」 탭(PLAN 322-2) — 코드가 그리는 방법(부품 상자 `common_util_structure_parts.json`).
|
||||
* 갈래마다 부품 · 이름 · 읽는 칸 · 맞는 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 `부품` = 끈 것만).
|
||||
* 끈 부품을 쓰는 종류는 그 갈래가 없음(카드 이름은 종류 이름)으로 — 표 줄(`resolveCatalog`)이 따름.
|
||||
* ========================================================================== */
|
||||
|
||||
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 { checkBox, head } from "./M02_SO_Def_Ui";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
type Branch = keyof typeof PART_BOX;
|
||||
|
||||
export function buildParts(opt: {
|
||||
def: StructDef;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
}): HTMLElement {
|
||||
const { def, edit, changed } = opt;
|
||||
const off = partsOff(def);
|
||||
const blocks = (Object.keys(PART_BOX) as Branch[]).flatMap((branch) => [
|
||||
...head(branch),
|
||||
el("table", {
|
||||
className: "m02-sod__table",
|
||||
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") : "" }),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", {
|
||||
children: Object.entries(PART_BOX[branch]).map(([id, part]) => {
|
||||
const key = `${branch}.${id}`;
|
||||
// 끈 부품을 쓰던 종류도 보이게 — 끈 동안은 표가 none 으로 풀므로 켠 꼴로 셈
|
||||
const users = usersOf({ ...def, 부품: {} } as StructDef, branch, id);
|
||||
return el("tr", {
|
||||
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(" · ")}` }),
|
||||
el("td", { text: users.map((row) => row.name).join(" · ") || "—" }),
|
||||
el("td", {
|
||||
children: [
|
||||
checkBox(
|
||||
to("PartOn"),
|
||||
off[key] !== false,
|
||||
(on) => (setPartOn(def, key, on), changed()),
|
||||
{ edit, attr: "data-sod-part-on" },
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
return el("div", {
|
||||
className: "m02-sod__tablewrap",
|
||||
children: [el("p", { className: "m02-side__empty", text: to("PartsNote") }), ...blocks],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Reflect.ts
|
||||
* 구조물 표 편집(PLAN 322-2)의 순수 손질 — 정의 사본의 `반영` · `부품` · 목록 + enabled · 형상칸.
|
||||
*
|
||||
* · 지금 값 = 구조물 표 한 줄(`resolveCatalog` · 정의 반영 ⊕ 처음값 ⊕ 부품 끔)
|
||||
* · 반영에는 처음 값과 다른 칸만 남김(같으면 지움 = 「기본」) · [처음 값으로] = 그 종류 반영 지움
|
||||
* · 목록과 enabled 는 함께 씀 — enabled = 목록 both 일 때만 참(옛 짝 · 브레인 322-1 답 ①)
|
||||
* 화면 그리기 없음 — 표 · 종류별 · 부품 탭이 이 함수들로 사본을 고침.
|
||||
* ========================================================================== */
|
||||
|
||||
import PARTS from "../../common_util/common_util_structure_parts.json" with { type: "json" };
|
||||
import {
|
||||
resolveCatalog,
|
||||
type CatalogDefinition,
|
||||
type CatalogRow,
|
||||
} from "../../common_util/common_util_structure_catalog";
|
||||
import type { StructDef } from "./M02_SO_Def_Model";
|
||||
|
||||
export type ReflectField = "목록" | "횡단도" | "카드이름" | "형상" | "3D";
|
||||
type Reflect = Partial<Record<ReflectField, string>> & { 형상칸?: Record<string, unknown> };
|
||||
interface Part {
|
||||
이름: string;
|
||||
"읽는 칸": string[];
|
||||
자리꼴: string[];
|
||||
정본: string[];
|
||||
}
|
||||
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> }).처음값;
|
||||
|
||||
/** 칸 → 표 줄 열쇠. */
|
||||
const ROW_KEY: Record<ReflectField, keyof CatalogRow> = {
|
||||
목록: "list",
|
||||
횡단도: "station",
|
||||
카드이름: "title",
|
||||
형상: "shape",
|
||||
"3D": "model3d",
|
||||
};
|
||||
|
||||
export const reflectOf = (def: StructDef): Record<string, Reflect> =>
|
||||
((def as { 반영?: Record<string, Reflect> }).반영 ??= {});
|
||||
export const partsOff = (def: StructDef): Record<string, boolean> =>
|
||||
((def as { 부품?: Record<string, boolean> }).부품 ??= {});
|
||||
|
||||
/** 사본 전체 표 — 종류마다 한 줄. */
|
||||
export const rowsOf = (def: StructDef): Map<string, CatalogRow> =>
|
||||
resolveCatalog(def as unknown as CatalogDefinition);
|
||||
|
||||
/** 칸 지금 값. */
|
||||
export const valueOf = (row: CatalogRow, field: ReflectField): string =>
|
||||
String(row[ROW_KEY[field]]);
|
||||
|
||||
/** 반영이 없을 때(처음 값 · 목록은 enabled 짝) 그 칸 값. */
|
||||
export function defaultOf(def: StructDef, typeId: string, field: ReflectField): string {
|
||||
const copy = { ...def, 반영: { ...reflectOf(def) } } as unknown as StructDef;
|
||||
const own = { ...(reflectOf(copy)[typeId] ?? {}) };
|
||||
delete own[field];
|
||||
reflectOf(copy)[typeId] = own;
|
||||
const row = rowsOf(copy).get(typeId);
|
||||
return row ? valueOf(row, field) : "";
|
||||
}
|
||||
|
||||
/** 반영에 적힌 칸인가(아니면 「기본」). */
|
||||
export const isSet = (def: StructDef, typeId: string, field: ReflectField | "형상칸"): boolean =>
|
||||
field in (reflectOf(def)[typeId] ?? {});
|
||||
|
||||
const dropEmpty = (def: StructDef, typeId: string): void => {
|
||||
const all = reflectOf(def);
|
||||
if (all[typeId] && !Object.keys(all[typeId]).length) delete all[typeId];
|
||||
};
|
||||
|
||||
/** 칸 고침 — 처음 값과 같으면 반영에서 지움 · 목록이면 enabled 도(both 일 때만 참). */
|
||||
export function setReflect(
|
||||
def: StructDef,
|
||||
typeId: string,
|
||||
field: ReflectField,
|
||||
value: string,
|
||||
): void {
|
||||
const type = def.types.find((t) => t.type_id === typeId);
|
||||
if (field === "목록" && type) type.enabled = value === "both";
|
||||
const own = (reflectOf(def)[typeId] ??= {});
|
||||
delete own[field];
|
||||
if (defaultOf(def, typeId, field) !== value) own[field] = value;
|
||||
dropEmpty(def, typeId);
|
||||
}
|
||||
|
||||
/** 형상칸 한 칸 고침(form 글 · weep 참거짓) — 처음 값과 같으면 지움. */
|
||||
export function setShapeOpt(def: StructDef, typeId: string, key: string, value: unknown): void {
|
||||
const own = (reflectOf(def)[typeId] ??= {});
|
||||
const opts = { ...(own.형상칸 ?? {}) };
|
||||
const start = START[typeId]?.형상칸?.[key];
|
||||
if (value === start || value === "" || (value === false && start === undefined)) delete opts[key];
|
||||
else opts[key] = value;
|
||||
if (Object.keys(opts).length) own.형상칸 = opts;
|
||||
else delete own.형상칸;
|
||||
dropEmpty(def, typeId);
|
||||
}
|
||||
|
||||
/** [처음 값으로] — 그 종류 반영 지움 · enabled = 처음 목록 both 일 때만 참. */
|
||||
export function resetRow(def: StructDef, typeId: string): void {
|
||||
delete reflectOf(def)[typeId];
|
||||
const type = def.types.find((t) => t.type_id === typeId);
|
||||
if (type) type.enabled = START[typeId] ? START[typeId].목록 === "both" : type.enabled;
|
||||
}
|
||||
|
||||
/** [저장] 전 — 모든 종류 enabled = 지금 목록 both 일 때만 참(목록 + enabled 함께 씀). */
|
||||
export function syncEnabled(def: StructDef): void {
|
||||
const rows = rowsOf(def);
|
||||
for (const type of def.types) {
|
||||
const row = rows.get(type.type_id);
|
||||
if (row) type.enabled = row.list === "both";
|
||||
}
|
||||
}
|
||||
|
||||
/** 부품 켬끔 — 끔만 남김. */
|
||||
export function setPartOn(def: StructDef, key: string, on: boolean): void {
|
||||
const off = partsOff(def);
|
||||
if (on) delete off[key];
|
||||
else off[key] = false;
|
||||
}
|
||||
|
||||
/** 못 숨김 칸 — 종류가 지금 쓰는 부품들이 읽는 칸 중 기본값도 비워 두는 뜻도 없는 옵션
|
||||
* (숨기면 그림이 받을 값이 없음 · 검사 짝 `M02_SO_Def_Check`). */
|
||||
export function readKeys(def: StructDef, typeId: string): Set<string> {
|
||||
const row = rowsOf(def).get(typeId);
|
||||
if (!row) return new Set();
|
||||
const type = def.types.find((t) => t.type_id === typeId);
|
||||
const bare = new Set(
|
||||
(type?.options ?? [])
|
||||
.filter((o) => (o.default === null || o.default === undefined) && !o.empty_means)
|
||||
.map((o) => o.key),
|
||||
);
|
||||
const used: Array<[Branch, string]> = [
|
||||
["횡단도", row.station],
|
||||
["카드이름", row.title],
|
||||
["형상", row.shape],
|
||||
["3D", row.model3d],
|
||||
];
|
||||
return new Set(
|
||||
used
|
||||
.flatMap(([branch, id]) => PART_BOX[branch][id]?.["읽는 칸"] ?? [])
|
||||
.filter((key) => bare.has(key)),
|
||||
);
|
||||
}
|
||||
|
||||
/** 그 갈래 부품을 지금 쓰는 종류들. */
|
||||
export function usersOf(def: StructDef, branch: Branch, id: string): CatalogRow[] {
|
||||
const field = ROW_KEY[branch];
|
||||
return [...rowsOf(def).values()].filter((row) => row[field] === id);
|
||||
}
|
||||
|
||||
/** 정의 「표시」 숨김 키(그 종류). */
|
||||
export const hiddenKeys = (def: StructDef, typeId: string): Set<string> =>
|
||||
new Set(
|
||||
Object.entries(def.표시[typeId] ?? {})
|
||||
.filter(([, shown]) => shown === false)
|
||||
.map(([key]) => key),
|
||||
);
|
||||
|
||||
/** 옵션 키들 보임 켬끔 — 정의 옵션인 키만(숨김 = false 만 남김). */
|
||||
export function setShown(
|
||||
def: StructDef,
|
||||
typeId: string,
|
||||
keys: readonly string[],
|
||||
on: boolean,
|
||||
): void {
|
||||
const type = def.types.find((t) => t.type_id === typeId);
|
||||
const known = new Set((type?.options ?? []).map((o) => o.key));
|
||||
const row = (def.표시[typeId] ??= {});
|
||||
for (const key of keys.filter((k) => known.has(k))) {
|
||||
if (on) delete row[key];
|
||||
else row[key] = false;
|
||||
}
|
||||
if (!Object.keys(row).length) delete def.표시[typeId];
|
||||
}
|
||||
@@ -6,11 +6,13 @@
|
||||
* 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목 › 칸 이름 › 선택지 이름 · 미리보기 = B05 칸 부품
|
||||
* (`field` · `optionControl` · 그룹 상자)으로 같은 차례.
|
||||
* 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침.
|
||||
* 줄마다 보임(322-2 · 정의 「표시」 · 컨테이너 = 그 안 칸 전부) · 부품이 읽는 칸 = 「못 숨김」(앞 판에서
|
||||
* 이미 숨긴 칸은 다시 보이기만) · 미리보기도 숨김을 따름.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createTag, el } from "@ui/ui_template_elements";
|
||||
import { createSpecForm } from "../../A00_Common/design/design_structure_form";
|
||||
import type { StructureSpec } from "../../A00_Common/design/design_structure_spec";
|
||||
import { specItemKeys, type StructureSpec } from "../../A00_Common/design/design_structure_spec";
|
||||
import { group } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { field, optionControl } from "../../B05_Profile/B05_Profile_UI_Structures_Fields";
|
||||
import type { StructDef, TypeDef } from "./M02_SO_Def_Model";
|
||||
@@ -20,7 +22,8 @@ import {
|
||||
specNameRows,
|
||||
type SpecNameRow,
|
||||
} from "./M02_SO_Def_Spec_Model";
|
||||
import { head, textBox } from "./M02_SO_Def_Ui";
|
||||
import { hiddenKeys, readKeys, setShown } from "./M02_SO_Def_Reflect";
|
||||
import { checkBox, head, textBox } from "./M02_SO_Def_Ui";
|
||||
import { to, type OptionsTextKey } from "./M02_SO_Text";
|
||||
|
||||
const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
|
||||
@@ -36,7 +39,8 @@ const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
|
||||
/** 정의 그룹 꼴 미리보기 — 그룹 밖 칸 · 그룹 상자(제목 = 그룹 이름) · 선택지 = 이름 표 글(값 그대로). */
|
||||
function groupPreview(def: StructDef, type: TypeDef): HTMLElement {
|
||||
const names = def.종류[type.type_id]?.이름 ?? {};
|
||||
const options = type.options.filter((o) => o.enabled !== false);
|
||||
const hidden = hiddenKeys(def, type.type_id);
|
||||
const options = type.options.filter((o) => o.enabled !== false && !hidden.has(o.key));
|
||||
const cell = (o: (typeof options)[number]): HTMLElement =>
|
||||
field(
|
||||
o.unit ? `${o.label} (${o.unit})` : o.label,
|
||||
@@ -67,8 +71,45 @@ function groupPreview(def: StructDef, type: TypeDef): HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
/** 이름 줄이 쓰는 정의 옵션 키 — 명세 상자 = 그 안 칸 전부 · 칸 = 저장 · 갈래 키 · 그룹 = 그 옵션들. */
|
||||
function rowKeys(type: TypeDef, spec: StructureSpec | undefined, nameKey: string): string[] {
|
||||
if (!nameKey || nameKey.includes("=")) return [];
|
||||
if (!spec) {
|
||||
if (!nameKey.startsWith("#")) return [nameKey];
|
||||
return [];
|
||||
}
|
||||
const [cid, iid] = nameKey.split(".");
|
||||
const c = spec.containers.find((x) => x.id === cid);
|
||||
const items = (c?.items ?? []).filter((i) => !iid || i.id === iid);
|
||||
const own = new Set(type.options.map((o) => o.key));
|
||||
return items.flatMap(specItemKeys).filter((key) => own.has(key));
|
||||
}
|
||||
|
||||
/** 줄 보임 칸 — 키 없는 줄은 비움 · 부품이 읽는 보이는 칸은 못 숨김. */
|
||||
function shownBox(
|
||||
opt: { def: StructDef; base: StructDef; type: TypeDef; edit: boolean; changed: () => void },
|
||||
keys: string[],
|
||||
): HTMLElement[] {
|
||||
if (!keys.length) return [];
|
||||
const { def, base, type, edit, changed } = opt;
|
||||
const hidden = hiddenKeys(def, type.type_id);
|
||||
const kept = hiddenKeys(base, type.type_id);
|
||||
const read = readKeys(def, type.type_id);
|
||||
const shown = keys.some((key) => !hidden.has(key));
|
||||
const locked = shown && keys.some((key) => read.has(key) && !kept.has(key));
|
||||
return [
|
||||
checkBox(to("ShownCol"), shown, (v) => (setShown(def, type.type_id, keys, v), changed()), {
|
||||
edit: edit && !locked,
|
||||
attr: "data-sod-spec-shown",
|
||||
}),
|
||||
...(locked ? [createTag(to("CannotHide"), "neutral")] : []),
|
||||
];
|
||||
}
|
||||
|
||||
export function buildSpecBlock(opt: {
|
||||
def: StructDef;
|
||||
/** 지금 서버 판 — 이미 숨긴 칸(못 숨김 예외)을 가름 */
|
||||
base: StructDef;
|
||||
type: TypeDef;
|
||||
/** 없으면 정의 그룹 꼴(52-12) */
|
||||
spec: StructureSpec | undefined;
|
||||
@@ -90,6 +131,12 @@ export function buildSpecBlock(opt: {
|
||||
: el("span", { text: to("SpecLoose") }),
|
||||
el("small", { className: "m02-opts__key", text: row.key }),
|
||||
...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []),
|
||||
...shownBox(
|
||||
opt,
|
||||
row.group && !spec
|
||||
? ((def.그룹[type.type_id] ?? []).find((g) => g.id === row.group)?.옵션 ?? [])
|
||||
: rowKeys(type, spec, row.nameKey),
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -98,7 +145,14 @@ export function buildSpecBlock(opt: {
|
||||
if (spec) {
|
||||
const form = createSpecForm(null);
|
||||
const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름]));
|
||||
form.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups);
|
||||
form.write(
|
||||
spec,
|
||||
{},
|
||||
def.종류[type.type_id]?.이름 ?? {},
|
||||
groups,
|
||||
{},
|
||||
hiddenKeys(def, type.type_id),
|
||||
);
|
||||
preview = form.root;
|
||||
} else preview = groupPreview(def, type);
|
||||
preview.inert = true;
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Table.ts
|
||||
* 구조물 정의 「한눈 표」 탭(PLAN 322-2) — 종류마다 B05 · B06 반영 한 줄.
|
||||
* 군 · 종류(▲▼) · 이름 · 목록 · 자리꼴 · 정본(읽기만 「고정」) · 횡단도 · 카드 이름 · 형상(+ 형상칸) ·
|
||||
* 알약 색 · 3D · 검사. 흐린 칸 = 처음 값(「기본」) · 진한 칸 = 바꾼 값 · [처음 값으로] = 그 종류 반영 지움.
|
||||
* 칸 손질 = `M02_SO_Def_Reflect` · 검사 글 = `M02_SO_Def_Check`(서버 짝) · 「종류별」 탭이 같은 칸(`reflectCells`)을 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
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 {
|
||||
isSet,
|
||||
PART_BOX,
|
||||
resetRow,
|
||||
rowsOf,
|
||||
setReflect,
|
||||
setShapeOpt,
|
||||
valueOf,
|
||||
type ReflectField,
|
||||
} from "./M02_SO_Def_Reflect";
|
||||
import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
type Pairs = ReadonlyArray<readonly [string, string]>;
|
||||
const LIST_PAIRS = (): Pairs => [
|
||||
["both", to("ListBoth")],
|
||||
["b06", to("ListB06")],
|
||||
["hidden", to("ListHidden")],
|
||||
];
|
||||
/** 갈래 고르기 — 없음(카드 이름은 종류 이름) + 부품 상자 차례. */
|
||||
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),
|
||||
];
|
||||
|
||||
interface CellCtx {
|
||||
def: StructDef;
|
||||
type: TypeDef;
|
||||
row: CatalogRow;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
}
|
||||
|
||||
/** 반영 칸 하나 — 처음 값이면 흐리게 「기본」 · 바꿨으면 진하게. */
|
||||
function reflectCell(ctx: CellCtx, field: ReflectField): HTMLElement {
|
||||
const { def, type, row, edit, changed } = ctx;
|
||||
const pairs = field === "목록" ? LIST_PAIRS() : partPairs(field);
|
||||
const box = pickBox(
|
||||
pairs,
|
||||
valueOf(row, field),
|
||||
(v) => {
|
||||
setReflect(def, type.type_id, field, v);
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: `data-sod-reflect-${field}` },
|
||||
);
|
||||
return mark(isSet(def, type.type_id, field), box);
|
||||
}
|
||||
|
||||
/** 흐림(기본) · 진함(바꿈) 꼴. */
|
||||
function mark(changed: boolean, ...children: HTMLElement[]): HTMLElement {
|
||||
return el("div", {
|
||||
className: `m02-sod__cell ${changed ? "is-changed" : "is-default"}`,
|
||||
children: [...children, ...(changed ? [] : [createTag(to("Default"), "neutral")])],
|
||||
});
|
||||
}
|
||||
|
||||
/** 형상칸 — 벽(형상 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 ?? "");
|
||||
return mark(
|
||||
isSet(def, type.type_id, "형상칸"),
|
||||
textBox(
|
||||
form,
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
{
|
||||
edit,
|
||||
attr: "data-sod-shape-form",
|
||||
place: to("WallForm"),
|
||||
max: 20,
|
||||
},
|
||||
),
|
||||
checkBox(
|
||||
to("Weep"),
|
||||
row.shapeOpts.weep === true,
|
||||
(v) => (setShapeOpt(def, type.type_id, "weep", v), changed()),
|
||||
{ edit, attr: "data-sod-shape-weep" },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** 형상칸 글은 떠날 때 씀(초점 안 끊김). */
|
||||
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", {
|
||||
className: "m02-sod__color",
|
||||
attrs: { type: "color", "data-sod-color": "" },
|
||||
}) as HTMLInputElement;
|
||||
const style = (ctx.type.style ?? {}) as { color?: string };
|
||||
box.value = style.color ?? "#888888";
|
||||
box.disabled = !ctx.edit;
|
||||
box.addEventListener("change", () => {
|
||||
ctx.type.style = { ...style, color: box.value };
|
||||
ctx.changed();
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
const fixed = (text: string): HTMLElement =>
|
||||
el("span", {
|
||||
className: "m02-sod__fixed",
|
||||
children: [el("span", { text }), createTag(to("Fixed"), "neutral")],
|
||||
});
|
||||
|
||||
/** 「종류별」 탭이 쓰는 반영 칸들 — [글, 칸] 짝. */
|
||||
export function reflectCells(
|
||||
def: StructDef,
|
||||
type: TypeDef,
|
||||
edit: boolean,
|
||||
changed: () => void,
|
||||
): Array<[string, HTMLElement]> {
|
||||
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("ColStation"), reflectCell(ctx, "횡단도")],
|
||||
[to("ColTitle"), reflectCell(ctx, "카드이름")],
|
||||
[to("ColShape"), reflectCell(ctx, "형상")],
|
||||
[to("ColShapeOpts"), opts],
|
||||
[to("ColColor"), colorCell(ctx)],
|
||||
[to("Col3D"), reflectCell(ctx, "3D")],
|
||||
];
|
||||
}
|
||||
|
||||
function moveButton(label: string, run: () => boolean, off: boolean, changed: () => void) {
|
||||
const b = createButton({ label, variant: "ghost", size: "sm" });
|
||||
b.disabled = off;
|
||||
b.addEventListener("click", () => run() && changed());
|
||||
return b;
|
||||
}
|
||||
|
||||
/** 그 종류 검사 글 — 이름 · 키가 든 줄. */
|
||||
const issuesOf = (issues: readonly string[], type: TypeDef): string[] =>
|
||||
issues.filter((t) => t.includes(`「${type.name}」`) || t.includes(`「${type.type_id}`));
|
||||
|
||||
export function buildTable(opt: {
|
||||
def: StructDef;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
issues: readonly string[];
|
||||
}): HTMLElement {
|
||||
const { def, edit, changed, issues } = opt;
|
||||
const heads = [
|
||||
"ColFamily",
|
||||
"ColKind",
|
||||
"ColName",
|
||||
"ColList",
|
||||
"ColPlacement",
|
||||
"ColOwner",
|
||||
"ColStation",
|
||||
"ColTitle",
|
||||
"ColShape",
|
||||
"ColShapeOpts",
|
||||
"ColColor",
|
||||
"Col3D",
|
||||
"ColCheck",
|
||||
] as const;
|
||||
const td = (child: HTMLElement | string): HTMLElement =>
|
||||
el("td", typeof child === "string" ? { text: child } : { children: [child] });
|
||||
const body: HTMLElement[] = [];
|
||||
const families = Object.keys(def.구조물군);
|
||||
families.forEach((g, gi) => {
|
||||
const name = textBox(def.구조물군[g], (v) => (def.구조물군[g] = v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-family-name",
|
||||
});
|
||||
const familyRow = el("tr", {
|
||||
className: "m02-sod__familyrow",
|
||||
attrs: { "data-sod-table-family": g },
|
||||
children: [
|
||||
el("td", {
|
||||
attrs: { colspan: String(heads.length) },
|
||||
children: [
|
||||
el("strong", { text: g }),
|
||||
name,
|
||||
...(edit
|
||||
? [
|
||||
moveButton(to("GroupUp"), () => stepFamily(def, g, -1), gi === 0, changed),
|
||||
moveButton(
|
||||
to("GroupDown"),
|
||||
() => stepFamily(def, g, 1),
|
||||
gi === families.length - 1,
|
||||
changed,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
body.push(familyRow);
|
||||
const kinds = def.types.filter((t) => t.group === g);
|
||||
kinds.forEach((type, ti) => {
|
||||
const cells = new Map(reflectCells(def, type, edit, changed));
|
||||
const bad = issuesOf(issues, type);
|
||||
const reset = createButton({ label: to("Reset"), variant: "ghost", size: "sm" });
|
||||
reset.disabled = !edit || !(def as { 반영?: Record<string, unknown> }).반영?.[type.type_id];
|
||||
reset.addEventListener("click", () => (resetRow(def, type.type_id), changed()));
|
||||
body.push(
|
||||
el("tr", {
|
||||
attrs: { "data-sod-table-kind": type.type_id },
|
||||
children: [
|
||||
td(""),
|
||||
el("td", {
|
||||
children: [
|
||||
el("small", { className: "m02-opts__key", text: type.type_id }),
|
||||
...(edit
|
||||
? [
|
||||
moveButton(
|
||||
to("GroupUp"),
|
||||
() => stepType(def, type.type_id, -1),
|
||||
ti === 0,
|
||||
changed,
|
||||
),
|
||||
moveButton(
|
||||
to("GroupDown"),
|
||||
() => stepType(def, type.type_id, 1),
|
||||
ti === kinds.length - 1,
|
||||
changed,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
td(
|
||||
textBox(type.name, (v) => (type.name = v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-kind-name",
|
||||
}),
|
||||
),
|
||||
...[
|
||||
"ColList",
|
||||
"ColPlacement",
|
||||
"ColOwner",
|
||||
"ColStation",
|
||||
"ColTitle",
|
||||
"ColShape",
|
||||
"ColShapeOpts",
|
||||
"ColColor",
|
||||
"Col3D",
|
||||
].map((key) => td(cells.get(to(key as (typeof heads)[number])) ?? "")),
|
||||
el("td", {
|
||||
attrs: { "data-sod-table-check": "" },
|
||||
children: [
|
||||
bad.length
|
||||
? el("span", { className: "m02-sod__bad", text: bad.join(" · ") })
|
||||
: createTag(to("Ok"), "accent"),
|
||||
reset,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
return el("div", {
|
||||
className: "m02-sod__tablewrap",
|
||||
children: [
|
||||
el("p", { className: "m02-side__empty", text: to("TableNote") }),
|
||||
el("table", {
|
||||
className: "m02-sod__table",
|
||||
attrs: { "data-sod-table": "" },
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((h) => el("th", { text: to(h) })) })],
|
||||
}),
|
||||
el("tbody", { children: body }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 「종류별」 탭 머리 아래 반영 칸 묶음(한눈 표 한 줄과 같은 칸). */
|
||||
export function buildReflectBlock(
|
||||
def: StructDef,
|
||||
type: TypeDef,
|
||||
edit: boolean,
|
||||
changed: () => void,
|
||||
): HTMLElement {
|
||||
return el("div", {
|
||||
className: "m02-sod__block",
|
||||
attrs: { "data-sod-reflect-block": "" },
|
||||
children: [
|
||||
el("h4", { className: "m02-opts__group", text: to("ReflectHead") }),
|
||||
el("div", {
|
||||
className: "m02-sod__reflect",
|
||||
children: reflectCells(def, type, edit, changed).map(([label, cell]) =>
|
||||
el("label", {
|
||||
className: "m02-sod__reflectrow",
|
||||
children: [el("span", { text: label }), cell],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -18,10 +18,12 @@ from pydantic import ValidationError
|
||||
from B05_Profile import B05_Profile_Structures_Schema as schema
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Check import catalog_errors
|
||||
|
||||
FILE = "구조물정의.json"
|
||||
HISTORY = "구조물정의_이력"
|
||||
TOP = {"판", "schema_version", "comment", "구조물군", "동작", "types"}
|
||||
# 반영 · 부품 = 구조물 표 칸(322-2 · 없으면 처음값 · 검사 = `M02_SO_Check`)
|
||||
TOP = {"판", "schema_version", "comment", "구조물군", "동작", "types", "반영", "부품"}
|
||||
SECTIONS = {"종류": dict, "표시": dict, "그룹": dict, "배치": dict, "규칙": list}
|
||||
SPOT = ("줄", "칸", "너비")
|
||||
|
||||
@@ -171,8 +173,8 @@ def _rule_errors(doc: dict[str, Any], keys: dict[str, dict[str, Any]]) -> list[s
|
||||
return errors
|
||||
|
||||
|
||||
def check(doc: Any) -> list[str]:
|
||||
"""정의 한 벌 검사 — 틀린 것 목록(빈 목록 = 통과)."""
|
||||
def check(doc: Any, before: dict[str, Any] | None = None) -> list[str]:
|
||||
"""정의 한 벌 검사 — 틀린 것 목록(빈 목록 = 통과). `before` = 지금 판(만든 뒤 고정 · 322-2)."""
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Groups as groups
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Options as options
|
||||
|
||||
@@ -194,7 +196,8 @@ def check(doc: Any) -> list[str]:
|
||||
fn(doc[section], plain)
|
||||
except store.StoreError as e:
|
||||
errors += e.detail["errors"] if isinstance(e.detail, dict) else [str(e.detail)]
|
||||
return errors + _kind_errors(doc, keys) + _spot_errors(doc, keys) + _rule_errors(doc, keys)
|
||||
errors += _kind_errors(doc, keys) + _spot_errors(doc, keys) + _rule_errors(doc, keys)
|
||||
return errors + ([] if errors else catalog_errors(doc, before))
|
||||
|
||||
|
||||
def _commit(change: Callable[[dict[str, Any]], dict[str, Any]], version: Any) -> dict[str, Any]:
|
||||
@@ -206,7 +209,7 @@ def _commit(change: Callable[[dict[str, Any]], dict[str, Any]], version: Any) ->
|
||||
if str(version) != str(have):
|
||||
raise store.StoreError(409, {"stale": [FILE], "판": have})
|
||||
doc = {**change(current), "판": have}
|
||||
errors = check(doc)
|
||||
errors = check(doc, current)
|
||||
if errors:
|
||||
raise store.StoreError(422, {"errors": errors})
|
||||
atomic_write_json(history_path(have), current)
|
||||
|
||||
@@ -368,3 +368,67 @@
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ── 322-2 탭 셋 · 한눈 표 · 부품 ── */
|
||||
.m02-sod__tabs {
|
||||
display: flex;
|
||||
gap: var(--spacing-4);
|
||||
margin: var(--spacing-8) 0;
|
||||
}
|
||||
.m02-sod__tablewrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.m02-sod__table {
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-body-sm);
|
||||
min-width: 100%;
|
||||
}
|
||||
.m02-sod__table th,
|
||||
.m02-sod__table td {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--spacing-4);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.m02-sod__familyrow td {
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
.m02-sod__table tr.is-off {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.m02-sod__cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
.m02-sod__cell.is-default {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.m02-sod__cell.is-changed {
|
||||
font-weight: 600;
|
||||
}
|
||||
.m02-sod__fixed {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.m02-sod__color {
|
||||
width: 32px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
.m02-sod__reflect {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
.m02-sod__reflectrow {
|
||||
display: grid;
|
||||
grid-template-columns: 7em 1fr;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,48 @@ const TEXT = {
|
||||
Pick: ["왼쪽에서 구조물 종류를 고름", "Pick a structure type on the left"],
|
||||
Detail: ["B06 전용", "B06 only"],
|
||||
Save: ["저장", "Save"],
|
||||
TabTable: ["한눈 표", "Overview"],
|
||||
TabKinds: ["종류별", "By type"],
|
||||
TabParts: ["부품", "Parts"],
|
||||
TableNote: [
|
||||
"종류마다 B05 · B06 반영 한 줄 — 목록 · 횡단도 · 카드 이름 · 형상 · 3D 는 코드 부품 중에서 고름 · 흐린 칸 = 처음 값(기본) · 진한 칸 = 바꾼 값 · 자리꼴 · 정본은 만든 뒤 고정",
|
||||
"One row per type — pick parts for list · cross section · card name · shape · 3D; faded = default, bold = changed",
|
||||
],
|
||||
ColFamily: ["군", "Family"],
|
||||
ColKind: ["종류", "Type"],
|
||||
ColName: ["이름", "Name"],
|
||||
ColList: ["목록", "List"],
|
||||
ColPlacement: ["자리꼴", "Placement"],
|
||||
ColOwner: ["정본", "Owner"],
|
||||
ColStation: ["횡단도", "Cross section"],
|
||||
ColTitle: ["카드 이름", "Card name"],
|
||||
ColShape: ["형상", "Shape"],
|
||||
ColShapeOpts: ["형상칸", "Shape fields"],
|
||||
ColColor: ["알약 색", "Pill color"],
|
||||
Col3D: ["3D", "3D"],
|
||||
ColCheck: ["검사", "Check"],
|
||||
ListBoth: ["B05 · B06", "B05 · B06"],
|
||||
ListB06: ["B06 만", "B06 only"],
|
||||
ListHidden: ["안 보임", "Hidden"],
|
||||
OffNone: ["없음", "None"],
|
||||
TitleName: ["종류 이름", "Type name"],
|
||||
Default: ["기본", "Default"],
|
||||
Fixed: ["고정", "Fixed"],
|
||||
Reset: ["처음 값으로", "Reset"],
|
||||
Ok: ["통과", "OK"],
|
||||
WallForm: ["기본 형태", "Default form"],
|
||||
Weep: ["물빼기", "Weep holes"],
|
||||
ReflectHead: ["B05 · B06 반영", "B05 · B06 use"],
|
||||
ShownCol: ["보임", "Shown"],
|
||||
CannotHide: ["못 숨김", "Required"],
|
||||
PartsNote: [
|
||||
"코드가 그리는 방법(부품) — 끄면 그 부품을 쓰는 종류는 그 갈래가 없음으로 · 읽는 칸 = 그 부품이 쓰는 옵션(숨기지 못함)",
|
||||
"Code drawing parts — turning a part off makes its users fall back to none; read fields cannot be hidden",
|
||||
],
|
||||
PartRead: ["읽는 칸", "Reads"],
|
||||
PartFits: ["맞는 자리꼴 · 정본", "Fits"],
|
||||
PartUsers: ["쓰는 종류", "Used by"],
|
||||
PartOn: ["켬", "On"],
|
||||
Stale: [
|
||||
"그 사이 정의가 바뀜 — 새로 받았으니 다시 고쳐 주세요",
|
||||
"Changed meanwhile — reloaded, please edit again",
|
||||
|
||||
Reference in New Issue
Block a user