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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util.common_util_structure_catalog import load_parts, resolve_catalog
|
from common_util.common_util_structure_catalog import load_parts, resolve_catalog
|
||||||
|
|
||||||
BRANCHES = ("횡단도", "카드이름", "형상", "3D")
|
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"}
|
OFF_VALUE = {"횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none"}
|
||||||
LISTS = ("both", "b06", "hidden")
|
LISTS = ("both", "b06", "hidden")
|
||||||
_FIELDS = {"목록", "형상칸", *BRANCHES}
|
_FIELDS = {"목록", "형상칸", *BRANCHES}
|
||||||
@@ -99,7 +110,7 @@ def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]
|
|||||||
if row is None:
|
if row is None:
|
||||||
continue
|
continue
|
||||||
if row["shape"] == "none" and row["station"] != "none":
|
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"]) # 앞 판 없음 = 새로 숨긴 것 없음
|
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]
|
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"]),
|
("형상", row["shape"]),
|
||||||
("3D", row["model3d"]),
|
("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:
|
for branch, part_id in used:
|
||||||
part = parts[branch].get(part_id)
|
part = parts[branch].get(part_id)
|
||||||
if part is None:
|
if part is None:
|
||||||
continue
|
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["정본"]:
|
if row["placement"] not in part["자리꼴"] or row["owner"] not in part["정본"]:
|
||||||
out.append(
|
out.append(f"「{row['name']}」 {branch} 「{name}」 — {place} · {owner} 에 안 맞음")
|
||||||
f"「{row['name']}」 {branch} 「{part_id}」 — 자리꼴 {row['placement']} · "
|
|
||||||
f"정본 {row['owner']} 에 안 맞음"
|
|
||||||
)
|
|
||||||
for key in part["읽는 칸"]:
|
for key in part["읽는 칸"]:
|
||||||
if key in keys and key in hidden:
|
if key in keys and key in hidden:
|
||||||
out.append(
|
out.append(
|
||||||
f"「{row['name']}」 {branch} 「{part_id}」 이 읽는 칸 「{key}」 — "
|
f"「{row['name']}」 {branch} 「{name}」 이 쓰는 칸 「{labels[key]}」 — "
|
||||||
"숨길 수 없음"
|
"숨길 수 없음"
|
||||||
)
|
)
|
||||||
return out
|
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 []}
|
now = {t["type_id"]: t for t in doc.get("types") or []}
|
||||||
for old in before.get("types") or []:
|
for old in before.get("types") or []:
|
||||||
tid = old["type_id"]
|
tid = old["type_id"]
|
||||||
|
name = old.get("name") or tid
|
||||||
t = now.get(tid)
|
t = now.get(tid)
|
||||||
if t is None:
|
if t is None:
|
||||||
out.append(f"종류 「{tid}」 — 지울 수 없음")
|
out.append(f"종류 「{name}」 — 지울 수 없음")
|
||||||
continue
|
continue
|
||||||
fixed = (
|
fixed = (
|
||||||
("자리꼴", old.get("placement"), t.get("placement")),
|
("자리꼴", 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:
|
for what, a, b in fixed:
|
||||||
if a != b:
|
if a != b:
|
||||||
out.append(f"「{tid}」 {what} — 만든 뒤 고정")
|
out.append(f"「{name}」 {what} — 만든 뒤 고정")
|
||||||
options = {o["key"]: o for o in t.get("options") or []}
|
options = {o["key"]: o for o in t.get("options") or []}
|
||||||
for o in old.get("options") or []:
|
for o in old.get("options") or []:
|
||||||
n = options.get(o["key"])
|
n = options.get(o["key"])
|
||||||
if o.get("input") != "select" or n is None:
|
if o.get("input") != "select" or n is None:
|
||||||
continue
|
continue
|
||||||
if sorted(o.get("choices") or []) != sorted(n.get("choices") or []):
|
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
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { buildSpecBlock } from "./M02_SO_Def_Spec";
|
|||||||
import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe";
|
import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe";
|
||||||
import { catalogProblems, type CheckDef } from "./M02_SO_Def_Check";
|
import { catalogProblems, type CheckDef } from "./M02_SO_Def_Check";
|
||||||
import { buildParts } from "./M02_SO_Def_Parts";
|
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 { buildReflectBlock, buildTable } from "./M02_SO_Def_Table";
|
||||||
import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
||||||
import { buildGroupBlock } from "./M02_SO_Groups";
|
import { buildGroupBlock } from "./M02_SO_Groups";
|
||||||
@@ -50,17 +50,6 @@ export interface DefScreenOptions {
|
|||||||
|
|
||||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
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(
|
function smallButton(
|
||||||
label: string,
|
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 panes = el("div", { className: "m02-opts__panes", children: [kindList, detail] });
|
||||||
|
|
||||||
const changed = (): void => {
|
const changed = (): void => {
|
||||||
dirty = true;
|
dirty = differs(work, base);
|
||||||
sync();
|
sync();
|
||||||
paint();
|
paint();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
resolveCatalog,
|
resolveCatalog,
|
||||||
type CatalogDefinition,
|
type CatalogDefinition,
|
||||||
} from "../../common_util/common_util_structure_catalog";
|
} from "../../common_util/common_util_structure_catalog";
|
||||||
|
import { OWNER_KO, PLACE_KO, shortName } from "./M02_SO_Def_Reflect";
|
||||||
|
|
||||||
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
|
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
|
||||||
interface Part {
|
interface Part {
|
||||||
@@ -52,6 +53,7 @@ export interface CheckDef extends CatalogDefinition {
|
|||||||
choices?: readonly string[];
|
choices?: readonly string[];
|
||||||
default?: unknown;
|
default?: unknown;
|
||||||
empty_means?: string | null;
|
empty_means?: string | null;
|
||||||
|
label?: string;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
표시?: Record<string, Record<string, boolean>>;
|
표시?: Record<string, Record<string, boolean>>;
|
||||||
@@ -123,7 +125,7 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
|
|||||||
const row = rows.get(t.type_id);
|
const row = rows.get(t.type_id);
|
||||||
if (!row) continue;
|
if (!row) continue;
|
||||||
if (row.shape === "none" && row.station !== "none")
|
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 kept = hiddenIn(before ?? doc, t.type_id); // 앞 판 없음 = 새로 숨긴 것 없음
|
||||||
const hidden = hiddenIn(doc, t.type_id).filter((key) => !kept.includes(key));
|
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)
|
.filter((o) => (o.default === null || o.default === undefined) && !o.empty_means)
|
||||||
.map((o) => o.key),
|
.map((o) => o.key),
|
||||||
);
|
);
|
||||||
|
const labelOf = (key: string): string =>
|
||||||
|
(t.options ?? []).find((o) => o.key === key)?.label ?? key;
|
||||||
const used: Array<[Branch, string]> = [
|
const used: Array<[Branch, string]> = [
|
||||||
["횡단도", row.station],
|
["횡단도", row.station],
|
||||||
["카드이름", row.title],
|
["카드이름", row.title],
|
||||||
@@ -141,13 +145,16 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
|
|||||||
for (const [branch, id] of used) {
|
for (const [branch, id] of used) {
|
||||||
const part = partOf(branch, id);
|
const part = partOf(branch, id);
|
||||||
if (!part) continue;
|
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))
|
if (!part.자리꼴.includes(row.placement) || !part.정본.includes(row.owner))
|
||||||
out.push(
|
out.push(`「${row.name}」 ${branch} 「${name}」 — ${place} · ${owner} 에 안 맞음`);
|
||||||
`「${row.name}」 ${branch} 「${id}」 — 자리꼴 ${row.placement} · 정본 ${row.owner} 에 안 맞음`,
|
|
||||||
);
|
|
||||||
for (const key of part["읽는 칸"])
|
for (const key of part["읽는 칸"])
|
||||||
if (keys.has(key) && hidden.includes(key))
|
if (keys.has(key) && hidden.includes(key))
|
||||||
out.push(`「${row.name}」 ${branch} 「${id}」 이 읽는 칸 「${key}」 — 숨길 수 없음`);
|
out.push(
|
||||||
|
`「${row.name}」 ${branch} 「${name}」 이 쓰는 칸 「${labelOf(key)}」 — 숨길 수 없음`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
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]));
|
const now = new Map((doc.types ?? []).map((t) => [t.type_id, t]));
|
||||||
for (const old of before.types ?? []) {
|
for (const old of before.types ?? []) {
|
||||||
const t = now.get(old.type_id);
|
const t = now.get(old.type_id);
|
||||||
|
const name = old.name ?? old.type_id;
|
||||||
if (!t) {
|
if (!t) {
|
||||||
out.push(`종류 「${old.type_id}」 — 지울 수 없음`);
|
out.push(`종류 「${name}」 — 지울 수 없음`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fixed: Array<[string, unknown, unknown]> = [
|
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.managed_by ?? null, t.managed_by ?? null],
|
||||||
["군 글자", old.group, t.group],
|
["군 글자", old.group, t.group],
|
||||||
];
|
];
|
||||||
for (const [what, a, b] of fixed)
|
for (const [what, a, b] of fixed) if (a !== b) out.push(`「${name}」 ${what} — 만든 뒤 고정`);
|
||||||
if (a !== b) out.push(`「${old.type_id}」 ${what} — 만든 뒤 고정`);
|
|
||||||
const options = new Map((t.options ?? []).map((o) => [o.key, o]));
|
const options = new Map((t.options ?? []).map((o) => [o.key, o]));
|
||||||
for (const o of old.options ?? []) {
|
for (const o of old.options ?? []) {
|
||||||
const n = options.get(o.key);
|
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 a = [...(o.choices ?? [])].sort().join("\u0000");
|
||||||
const b = [...(n.choices ?? [])].sort().join("\u0000");
|
const b = [...(n.choices ?? [])].sort().join("\u0000");
|
||||||
if (a !== b)
|
if (a !== b)
|
||||||
out.push(`「${old.type_id}.${o.key}」 선택지 값 — 고칠 수 없음(이름은 이름 표로)`);
|
out.push(`「${name}」 「${o.label ?? o.key}」 선택지 값 — 고칠 수 없음(이름만 고침)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -7,7 +7,15 @@
|
|||||||
|
|
||||||
import { el } from "@ui/ui_template_elements";
|
import { el } from "@ui/ui_template_elements";
|
||||||
import type { StructDef } from "./M02_SO_Def_Model";
|
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 { checkBox, head } from "./M02_SO_Def_Ui";
|
||||||
import { to } from "./M02_SO_Text";
|
import { to } from "./M02_SO_Text";
|
||||||
|
|
||||||
@@ -20,17 +28,26 @@ export function buildParts(opt: {
|
|||||||
}): HTMLElement {
|
}): HTMLElement {
|
||||||
const { def, edit, changed } = opt;
|
const { def, edit, changed } = opt;
|
||||||
const off = partsOff(def);
|
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) => [
|
const blocks = (Object.keys(PART_BOX) as Branch[]).flatMap((branch) => [
|
||||||
...head(branch),
|
...head(branch),
|
||||||
el("table", {
|
el("table", {
|
||||||
className: "m02-sod__table",
|
className: "m02-sod__table m02-sod__table--wrap",
|
||||||
attrs: { "data-sod-parts": branch },
|
attrs: { "data-sod-parts": branch },
|
||||||
children: [
|
children: [
|
||||||
el("thead", {
|
el("thead", {
|
||||||
children: [
|
children: [
|
||||||
el("tr", {
|
el("tr", {
|
||||||
children: ["", "ColName", "PartRead", "PartFits", "PartUsers", "PartOn"].map((key) =>
|
children: (["ColName", "PartRead", "PartFits", "PartUsers", "PartOn"] as const).map(
|
||||||
el("th", { text: key ? to(key as "ColName") : "" }),
|
(key) => el("th", { text: to(key) }),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -44,10 +61,24 @@ export function buildParts(opt: {
|
|||||||
attrs: { "data-sod-part": key },
|
attrs: { "data-sod-part": key },
|
||||||
className: off[key] === false ? "is-off" : "",
|
className: off[key] === false ? "is-off" : "",
|
||||||
children: [
|
children: [
|
||||||
el("td", { children: [el("small", { className: "m02-opts__key", text: id })] }),
|
// 영어 키는 툴팁만(322-2 다듬기) — 이름 = 짧은 이름 · 읽는 칸 = 정의 칸 이름
|
||||||
el("td", { text: part.이름 }),
|
el("td", { text: shortName(part.이름), attrs: { title: `${id} · ${part.이름}` } }),
|
||||||
el("td", { text: part["읽는 칸"].join(" · ") || "—" }),
|
el("td", {
|
||||||
el("td", { text: `${part.자리꼴.join(" · ")} / ${part.정본.join(" · ")}` }),
|
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", { text: users.map((row) => row.name).join(" · ") || "—" }),
|
||||||
el("td", {
|
el("td", {
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
type CatalogDefinition,
|
type CatalogDefinition,
|
||||||
type CatalogRow,
|
type CatalogRow,
|
||||||
} from "../../common_util/common_util_structure_catalog";
|
} 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";
|
export type ReflectField = "목록" | "횡단도" | "카드이름" | "형상" | "3D";
|
||||||
type Reflect = Partial<Record<ReflectField, string>> & { 형상칸?: Record<string, unknown> };
|
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;
|
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 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> = {
|
const ROW_KEY: Record<ReflectField, keyof CatalogRow> = {
|
||||||
목록: "list",
|
목록: "list",
|
||||||
@@ -174,3 +179,36 @@ export function setShown(
|
|||||||
}
|
}
|
||||||
if (!Object.keys(row).length) delete def.표시[typeId];
|
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 { createButton, createTag, el } from "@ui/ui_template_elements";
|
||||||
import type { CatalogRow } from "../../common_util/common_util_structure_catalog";
|
import type { CatalogRow } from "../../common_util/common_util_structure_catalog";
|
||||||
import { stepFamily, stepType, type StructDef, type TypeDef } from "./M02_SO_Def_Model";
|
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 {
|
import {
|
||||||
isSet,
|
isSet,
|
||||||
|
OWNER_KO,
|
||||||
PART_BOX,
|
PART_BOX,
|
||||||
|
PLACE_KO,
|
||||||
|
shortName,
|
||||||
resetRow,
|
resetRow,
|
||||||
rowsOf,
|
rowsOf,
|
||||||
setReflect,
|
setReflect,
|
||||||
@@ -28,14 +32,26 @@ const LIST_PAIRS = (): Pairs => [
|
|||||||
["b06", to("ListB06")],
|
["b06", to("ListB06")],
|
||||||
["hidden", to("ListHidden")],
|
["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 => [
|
const partPairs = (branch: Exclude<ReflectField, "목록">): Pairs => [
|
||||||
[
|
[branch === "카드이름" ? "name" : "none", OFF_TEXT[branch]()],
|
||||||
branch === "카드이름" ? "name" : "none",
|
...Object.entries(PART_BOX[branch]).map(([id, part]) => [id, shortName(part.이름)] as const),
|
||||||
branch === "카드이름" ? to("TitleName") : to("OffNone"),
|
|
||||||
],
|
|
||||||
...Object.entries(PART_BOX[branch]).map(([id, part]) => [id, `${id} · ${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 {
|
interface CellCtx {
|
||||||
def: StructDef;
|
def: StructDef;
|
||||||
@@ -58,6 +74,7 @@ function reflectCell(ctx: CellCtx, field: ReflectField): HTMLElement {
|
|||||||
},
|
},
|
||||||
{ edit, attr: `data-sod-reflect-${field}` },
|
{ edit, attr: `data-sod-reflect-${field}` },
|
||||||
);
|
);
|
||||||
|
tipOptions(box, field);
|
||||||
return mark(isSet(def, type.type_id, field), box);
|
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 {
|
function shapeOptsCell(ctx: CellCtx): HTMLElement {
|
||||||
const { def, type, row, edit, changed } = ctx;
|
const { def, type, row, edit, changed } = ctx;
|
||||||
if (row.shape !== "wall") return el("span", { className: "m02-side__empty", text: "—" });
|
if (row.shape !== "wall") return el("span", { className: "m02-side__empty", text: "—" });
|
||||||
const form = String(row.shapeOpts.form ?? "");
|
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(
|
return mark(
|
||||||
isSet(def, type.type_id, "형상칸"),
|
isSet(def, type.type_id, "형상칸"),
|
||||||
textBox(
|
box,
|
||||||
form,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
{
|
|
||||||
edit,
|
|
||||||
attr: "data-sod-shape-form",
|
|
||||||
place: to("WallForm"),
|
|
||||||
max: 20,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
checkBox(
|
checkBox(
|
||||||
to("Weep"),
|
to("Weep"),
|
||||||
row.shapeOpts.weep === true,
|
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). */
|
/** 알약 색(style.color). */
|
||||||
function colorCell(ctx: CellCtx): HTMLElement {
|
function colorCell(ctx: CellCtx): HTMLElement {
|
||||||
const box = el("input", {
|
const box = el("input", {
|
||||||
@@ -121,9 +127,11 @@ function colorCell(ctx: CellCtx): HTMLElement {
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixed = (text: string): HTMLElement =>
|
/** 자리꼴 · 정본 = 한국어 글 · 「고정」(키는 툴팁). */
|
||||||
|
const fixed = (text: string, key: string): HTMLElement =>
|
||||||
el("span", {
|
el("span", {
|
||||||
className: "m02-sod__fixed",
|
className: "m02-sod__fixed",
|
||||||
|
attrs: { title: key },
|
||||||
children: [el("span", { text }), createTag(to("Fixed"), "neutral")],
|
children: [el("span", { text }), createTag(to("Fixed"), "neutral")],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,16 +145,14 @@ export function reflectCells(
|
|||||||
const row = rowsOf(def).get(type.type_id);
|
const row = rowsOf(def).get(type.type_id);
|
||||||
if (!row) return [];
|
if (!row) return [];
|
||||||
const ctx = { def, type, row, edit, changed };
|
const ctx = { def, type, row, edit, changed };
|
||||||
const opts = shapeOptsCell(ctx);
|
|
||||||
bindForm(opts, ctx);
|
|
||||||
return [
|
return [
|
||||||
[to("ColList"), reflectCell(ctx, "목록")],
|
[to("ColList"), reflectCell(ctx, "목록")],
|
||||||
[to("ColPlacement"), fixed(row.placement)],
|
[to("ColPlacement"), fixed(PLACE_KO[row.placement] ?? row.placement, row.placement)],
|
||||||
[to("ColOwner"), fixed(row.owner)],
|
[to("ColOwner"), fixed(OWNER_KO[row.owner] ?? row.owner, row.owner)],
|
||||||
[to("ColStation"), reflectCell(ctx, "횡단도")],
|
[to("ColStation"), reflectCell(ctx, "횡단도")],
|
||||||
[to("ColTitle"), reflectCell(ctx, "카드이름")],
|
[to("ColTitle"), reflectCell(ctx, "카드이름")],
|
||||||
[to("ColShape"), reflectCell(ctx, "형상")],
|
[to("ColShape"), reflectCell(ctx, "형상")],
|
||||||
[to("ColShapeOpts"), opts],
|
[to("ColShapeOpts"), shapeOptsCell(ctx)],
|
||||||
[to("ColColor"), colorCell(ctx)],
|
[to("ColColor"), colorCell(ctx)],
|
||||||
[to("Col3D"), reflectCell(ctx, "3D")],
|
[to("Col3D"), reflectCell(ctx, "3D")],
|
||||||
];
|
];
|
||||||
@@ -232,8 +238,8 @@ export function buildTable(opt: {
|
|||||||
children: [
|
children: [
|
||||||
td(""),
|
td(""),
|
||||||
el("td", {
|
el("td", {
|
||||||
|
attrs: { title: type.type_id }, // 영어 키는 툴팁만(322-2 다듬기)
|
||||||
children: [
|
children: [
|
||||||
el("small", { className: "m02-opts__key", text: type.type_id }),
|
|
||||||
...(edit
|
...(edit
|
||||||
? [
|
? [
|
||||||
moveButton(
|
moveButton(
|
||||||
|
|||||||
@@ -432,3 +432,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-4);
|
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"],
|
ListB06: ["B06 만", "B06 only"],
|
||||||
ListHidden: ["안 보임", "Hidden"],
|
ListHidden: ["안 보임", "Hidden"],
|
||||||
OffNone: ["없음", "None"],
|
OffNone: ["없음", "None"],
|
||||||
|
StationNone: ["안 만듦", "Not made"],
|
||||||
TitleName: ["종류 이름", "Type name"],
|
TitleName: ["종류 이름", "Type name"],
|
||||||
Default: ["기본", "Default"],
|
Default: ["기본", "Default"],
|
||||||
Fixed: ["고정", "Fixed"],
|
Fixed: ["고정", "Fixed"],
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
"정본": ["pipe_points"]
|
"정본": ["pipe_points"]
|
||||||
},
|
},
|
||||||
"wall": {
|
"wall": {
|
||||||
"이름": "벽 스윕",
|
"이름": "벽",
|
||||||
"읽는 칸": [],
|
"읽는 칸": [],
|
||||||
"자리꼴": ["interval"],
|
"자리꼴": ["interval"],
|
||||||
"정본": ["pipe_points", "structures"]
|
"정본": ["pipe_points", "structures"]
|
||||||
|
|||||||
@@ -88,6 +88,24 @@ out.partOff = [clone(plain.부품), reflect.rowsOf(plain).get("retaining_wall").
|
|||||||
reflect.setPartOn(plain, "형상.wall", true);
|
reflect.setPartOn(plain, "형상.wall", true);
|
||||||
out.partOn = clone(plain.부품);
|
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 = {};
|
out.spec = {};
|
||||||
for (const [tid, sp] of Object.entries(STRUCTURE_SPECS)) {
|
for (const [tid, sp] of Object.entries(STRUCTURE_SPECS)) {
|
||||||
|
|||||||
@@ -123,27 +123,25 @@ def test_check_rules(ts: dict) -> None:
|
|||||||
"부품 「횡단도.span3」 — 참거짓",
|
"부품 「횡단도.span3」 — 참거짓",
|
||||||
"부품 「끔」 — 없는 부품",
|
"부품 「끔」 — 없는 부품",
|
||||||
]
|
]
|
||||||
assert got["rule218"] == ["「개거(겉도랑)」 형상 none — 횡단도도 none 이어야 함"]
|
assert got["rule218"] == ["「개거(겉도랑)」 형상이 「없음」이면 횡단도도 「안 만듦」"]
|
||||||
assert got["mismatch"] == [
|
assert got["mismatch"] == ["「옹벽」 형상 「배관 세트」 — 구간 · 구조물 에 안 맞음"]
|
||||||
"「옹벽」 형상 「pipe_set」 — 자리꼴 interval · 정본 structures 에 안 맞음"
|
|
||||||
]
|
|
||||||
assert got["hide_read"] == [
|
assert got["hide_read"] == [
|
||||||
"「배관」 형상 「pipe_set」 이 읽는 칸 「revet_foundation」 — 숨길 수 없음"
|
"「배관」 형상 「배관 세트」 이 쓰는 칸 「흙막이 기초」 — 숨길 수 없음"
|
||||||
]
|
]
|
||||||
assert got["hide_no_before"] == [] # 앞 판 없음 = 새로 숨긴 것 없음
|
assert got["hide_no_before"] == [] # 앞 판 없음 = 새로 숨긴 것 없음
|
||||||
assert got["fixed"] == [
|
assert got["fixed"] == [
|
||||||
# 표 줄도 함께 걸림 — 자리꼴 · 정본을 바꾸면 그 종류 부품이 안 맞음
|
# 표 줄도 함께 걸림 — 자리꼴 · 정본을 바꾸면 그 종류 부품이 안 맞음
|
||||||
"「BOX암거」 형상 「box」 — 자리꼴 interval · 정본 pipe_points 에 안 맞음",
|
"「BOX암거」 형상 「BOX 본체 · 날개벽」 — 구간 · 관 지점 에 안 맞음",
|
||||||
"「BOX암거」 3D 「box」 — 자리꼴 interval · 정본 pipe_points 에 안 맞음",
|
"「BOX암거」 3D 「BOX 본체 · 날개벽」 — 구간 · 관 지점 에 안 맞음",
|
||||||
"「기슭막이」 형상 「hidden_revet」 — 자리꼴 interval · 정본 structures 에 안 맞음",
|
"「기슭막이」 형상 「독립 기슭막이」 — 구간 · 구조물 에 안 맞음",
|
||||||
"「pipe.pipe_diameter_mm」 선택지 값 — 고칠 수 없음(이름은 이름 표로)",
|
"「배관」 「관경」 선택지 값 — 고칠 수 없음(이름만 고침)",
|
||||||
"「box_culvert」 자리꼴 — 만든 뒤 고정",
|
"「BOX암거」 자리꼴 — 만든 뒤 고정",
|
||||||
"「revetment」 정본 — 만든 뒤 고정",
|
"「기슭막이」 정본 — 만든 뒤 고정",
|
||||||
"「mirror」 군 글자 — 만든 뒤 고정",
|
"「반사경」 군 글자 — 만든 뒤 고정",
|
||||||
"종류 「etc」 — 지울 수 없음",
|
"종류 「기타」 — 지울 수 없음",
|
||||||
]
|
]
|
||||||
# 형상 부품을 끄면 벽 다섯은 형상 none — 횡단도도 none 으로 바꿔야 저장됨
|
# 형상 부품을 끄면 벽 다섯은 형상 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:
|
def test_reflect_edits(ts: dict) -> None:
|
||||||
@@ -165,6 +163,10 @@ def test_reflect_edits(ts: dict) -> None:
|
|||||||
assert r["reset"] == [None, None, True]
|
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:
|
def test_save_writes_list_and_enabled_together(ts: dict) -> None:
|
||||||
sync = ts["sync"]
|
sync = ts["sync"]
|
||||||
assert sync["same"] is True
|
assert sync["same"] is True
|
||||||
|
|||||||
Reference in New Issue
Block a user