diff --git a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts index 7f321904a..bce554803 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts @@ -48,7 +48,8 @@ export function createFacilityDefinition(root: HTMLElement): FacilityDefinition // 정의에 새로 든 선택지는 보기를 더하고, 정의 · 규칙 밖 보기는 가림(빈 보기 · 지금 값은 그대로) const have = new Set([...control.options].map((item) => item.value)); for (const choice of state.choices) - if (!have.has(choice)) control.add(new Option(choice, choice)); + if (!have.has(choice)) + control.add(new Option(type.spec_names?.[`${option.key}=${choice}`] ?? choice, choice)); for (const item of control.options) { item.hidden = !!item.value && !state.choices.includes(item.value) && item.value !== control.value; diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index ed31f4ce4..eaf354609 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -68,6 +68,8 @@ interface OptionShape { export function optionControl( option: OptionShape, stored: string | number | undefined, + /** 선택지 보일 글(정의 이름 표 · 52-12) — 값은 그대로 */ + label: (choice: string) => string = (c) => c, ): HTMLInputElement | HTMLSelectElement { const value = stored === undefined || stored === null ? "" : String(stored); // 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻. @@ -78,7 +80,7 @@ export function optionControl( const kept = value && !option.choices.includes(value) ? [value] : []; const element = select([ ["", blank], - ...[...option.choices, ...kept].map((c) => [c, c] as [string, string]), + ...[...option.choices, ...kept].map((c) => [c, label(c)] as [string, string]), ]); element.value = value; if (tip) element.title = tip; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index af789a5dd..c30ad9f16 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -80,7 +80,8 @@ export function renderOptionFields( shown.forEach((option) => { // 받은 값만 칸에 — 새 구조물은 부르는 쪽(패널)이 등록부 기본값을 `values` 로 실어 줌(PLAN 46-7). // 보기는 정의 전부로 세워 옛 값을 잃지 않고, 숨김 · 규칙 선택지는 `applyRules` 가 좁힘. - const input = optionControl(option, values[option.key]); + const name = (c: string): string => type.spec_names?.[`${option.key}=${c}`] ?? c; + const input = optionControl(option, values[option.key], name); const label = `${option.unit ? `${option.label} (${option.unit})` : option.label}${option.not_in_table ? " · 표에 안 쓰임" : ""}`; const wrap = field(label, input, option.default_basis); wrap.dataset.optionKeys = option.key; // 옵션 그룹 상자가 이 표식으로 칸을 옮김(PLAN 46-3) @@ -152,7 +153,11 @@ export function renderOptionFields( wrap.classList.toggle("is-rule-hidden", off); // 그룹 상자로 옮겨 가도 같은 표식 if (!off) optionInputs.push(entry); if (state && entry.input.tagName.toLowerCase() === "select") - syncChoices(entry.input as HTMLSelectElement, state.choices); + syncChoices( + entry.input as HTMLSelectElement, + state.choices, + (c) => type!.spec_names?.[`${entry.key}=${c}`] ?? c, + ); } } applyRules(null); @@ -161,14 +166,18 @@ export function renderOptionFields( } /** 고르기 칸 보기를 규칙 선택지로 — 빈 보기는 남기고, 지금 값이 목록 밖이면 그 값만 남겨 보임. */ -function syncChoices(element: HTMLSelectElement, choices: string[]): void { +function syncChoices( + element: HTMLSelectElement, + choices: string[], + label: (choice: string) => string, +): void { const now = [...element.children].slice(1).map((o) => (o as HTMLOptionElement).value); const keep = element.value && !choices.includes(element.value) ? [element.value] : []; const want = [...choices, ...keep]; if (now.length === want.length && now.every((v, i) => v === want[i])) return; const value = element.value; const blank = element.children[0]; - element.replaceChildren(blank, ...want.map((c) => new Option(c, c))); + element.replaceChildren(blank, ...want.map((c) => new Option(label(c), c))); element.value = value; } diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts index d16428ac1..1529bacc3 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts @@ -32,6 +32,9 @@ import { checkBox, textBox } from "./M02_SO_Def_Ui"; import { buildGroupBlock } from "./M02_SO_Groups"; import { to } from "./M02_SO_Text"; +/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */ +const OLD_EDITOR = false; + export interface DefScreenOptions { /** 시스템 관리자 — 아니면 읽기만 */ isAdmin: boolean; @@ -184,7 +187,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { } const edit = canEdit(); const extra = (): NonNullable => (d.종류[type.type_id] ??= {}); - // 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김 + // 이름만 고침(PLAN 52-11 · 52-12) — 명세 종류 = 명세 나무 · 나머지 = 정의 그룹 나무 const spec = STRUCTURE_SPECS[type.type_id]; detail.replaceChildren( el("div", { @@ -200,7 +203,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { edit, attr: "data-sod-kind-off", }), - ...(spec + ...(!OLD_EDITOR ? [] : [ checkBox( @@ -221,7 +224,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { }), ], }), - ...(spec + ...(!OLD_EDITOR ? [buildSpecBlock({ def: d, type, spec, edit, changed })] : [ buildOptionsBlock({ def: d, type, edit, open, changed }), diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts index 63157ee50..1728f1662 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts @@ -3,14 +3,23 @@ * 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴 * 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 · * 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음. + * 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목 › 칸 이름 › 선택지 이름 · 미리보기 = B05 칸 부품 + * (`field` · `optionControl` · 그룹 상자)으로 같은 차례. * 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침. * ========================================================================== */ 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 { 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"; -import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model"; +import { + groupNameRows, + setSpecName, + specNameRows, + type SpecNameRow, +} from "./M02_SO_Def_Spec_Model"; import { head, textBox } from "./M02_SO_Def_Ui"; import { to, type OptionsTextKey } from "./M02_SO_Text"; @@ -20,47 +29,90 @@ const TAG: Record, OptionsTextKey> = { select: "FormSelect", number: "FormNumber", auto: "FormAuto", + text: "FormText", + bool: "FormBool", }; +/** 정의 그룹 꼴 미리보기 — 그룹 밖 칸 · 그룹 상자(제목 = 그룹 이름) · 선택지 = 이름 표 글(값 그대로). */ +function groupPreview(def: StructDef, type: TypeDef): HTMLElement { + const names = def.종류[type.type_id]?.이름 ?? {}; + const options = type.options.filter((o) => o.enabled !== false); + const cell = (o: (typeof options)[number]): HTMLElement => + field( + o.unit ? `${o.label} (${o.unit})` : o.label, + optionControl( + { + input: o.input === "select" || o.input === "number" ? o.input : "text", + choices: o.choices ?? [], + default: null, + required: !!o.required, + }, + undefined, + (c) => names[`${o.key}=${c}`] ?? c, + ), + ); + const groups = def.그룹[type.type_id] ?? []; + const inGroup = new Set(groups.flatMap((g) => g.옵션)); + const grid = (cells: HTMLElement[]): HTMLElement => + el("div", { className: "b05-structure__grid", children: cells }); + return el("div", { + children: [ + grid(options.filter((o) => !inGroup.has(o.key)).map(cell)), + ...groups.map((g) => { + const box = group(g.이름); + box.body.append(grid(options.filter((o) => g.옵션.includes(o.key)).map(cell))); + return box.root; + }), + ], + }); +} + export function buildSpecBlock(opt: { def: StructDef; type: TypeDef; - spec: StructureSpec; + /** 없으면 정의 그룹 꼴(52-12) */ + spec: StructureSpec | undefined; edit: boolean; changed: () => void; }): HTMLElement { const { def, type, spec, edit, changed } = opt; - const rows = specNameRows(def, spec).map((row) => + const rows = (spec ? specNameRows(def, spec) : groupNameRows(def, type)).map((row) => el("div", { className: `m02-sod__specrow is-level-${row.level}`, attrs: { "data-sod-spec": row.nameKey }, children: [ - textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, { - edit, - attr: "data-sod-spec-name", - cls: "m02-sod__name", - }), + row.nameKey + ? textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, { + edit, + attr: "data-sod-spec-name", + cls: "m02-sod__name", + }) + : el("span", { text: to("SpecLoose") }), el("small", { className: "m02-opts__key", text: row.key }), ...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []), ], }), ); // 미리보기 — 값 없는 새 구조물 폼 · 잇기 줄 없음(조합은 프로젝트 몫) - const preview = createSpecForm(null); - const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름])); - preview.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups); - preview.root.inert = true; + let preview: HTMLElement; + 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); + preview = form.root; + } else preview = groupPreview(def, type); + preview.inert = true; return el("div", { className: "m02-sod__block", attrs: { "data-sod-spec-block": "" }, children: [ ...head(to("SpecHead"), to("SpecNote")), - ...rows, + ...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("SpecNone") })]), ...head(to("SpecPreview")), el("div", { className: "m02-sod__specpreview", attrs: { "data-sod-spec-preview": "" }, - children: [preview.root], + children: [preview], }), ], }); diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts index bd6bc693b..52634ecbc 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts @@ -6,25 +6,29 @@ * · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`) * · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기 * (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀 + * · 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목(`그룹.이름`) › 칸 이름(옵션 `label`) › + * 선택지 이름(이름 표 키 `{옵션}={값}` · 값은 그대로) — 그룹 밖 칸은 맨 앞 머리 줄 아래 * import 는 꼴만 — 시험이 이 파일만 옮겨 돌림. * ========================================================================== */ import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; -import type { StructDef } from "./M02_SO_Def_Model"; +import type { StructDef, TypeDef } from "./M02_SO_Def_Model"; export interface SpecNameRow { /** 0 = 컨테이너 · 1 = 항목 · 2 = 값 */ level: 0 | 1 | 2; - /** 이름 표 키 */ + /** 이름 표 키 — 빈 글 = 고칠 이름 없는 머리 줄(그룹 밖 칸) */ nameKey: string; /** 읽기만 표시 — 컨테이너 id · 항목 id · 값 */ key: string; /** 지금 이름 */ label: string; /** 읽기만 표시 — 필수/선택 · 꼴 */ - tag: "required" | "optional" | "select" | "number" | "auto" | null; + tag: "required" | "optional" | "select" | "number" | "auto" | "text" | "bool" | null; /** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */ group: string | null; + /** 정의 옵션 이름(`label`)에서 옴 — 고치면 그 옵션 이름에 씀 */ + option: string | null; } /** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */ @@ -48,6 +52,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] label: names[c.id] ?? group?.이름 ?? c.title, tag: c.required ? "required" : "optional", group: group ? group.id : null, + option: null, }); for (const item of c.items) { const itemKey = `${c.id}.${item.id}`; @@ -58,6 +63,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] label: names[itemKey] ?? item.label, tag: item.kind, group: null, + option: null, }); const choices = [ ...(item.choices ?? []), @@ -72,6 +78,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] label: names[choiceKey] ?? ch.label, tag: null, group: null, + option: null, }); } } @@ -79,8 +86,67 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] return rows; } -/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */ +/** 정의 그룹 꼴 종류(명세 없음) 이름 줄 — 그룹 밖 칸 · 그룹마다 제목 › 칸 › 선택지(B05 그리기 차례). */ +export function groupNameRows(def: StructDef, type: TypeDef): SpecNameRow[] { + const names = def.종류[type.type_id]?.이름 ?? {}; + const options = type.options.filter((o) => o.enabled !== false); + const groups = def.그룹[type.type_id] ?? []; + const inGroup = new Set(groups.flatMap((g) => g.옵션)); + const rows: SpecNameRow[] = []; + const optionRows = (keys: readonly string[]): void => { + for (const o of options.filter((x) => keys.includes(x.key))) { + rows.push({ + level: 1, + nameKey: o.key, + key: o.key, + label: o.label, + tag: o.input, + group: null, + option: o.key, + }); + for (const ch of o.input === "select" ? (o.choices ?? []) : []) { + const nameKey = `${o.key}=${ch}`; + rows.push({ + level: 2, + nameKey, + key: ch, + label: names[nameKey] ?? ch, + tag: null, + group: null, + option: null, + }); + } + } + }; + const loose = options.filter((o) => !inGroup.has(o.key)).map((o) => o.key); + if (loose.length) { + rows.push({ level: 0, nameKey: "", key: "", label: "", tag: null, group: null, option: null }); + optionRows(loose); + } + for (const g of groups) { + rows.push({ + level: 0, + nameKey: `#${g.id}`, + key: g.id, + label: g.이름, + tag: null, + group: g.id, + option: null, + }); + optionRows(g.옵션); + } + return rows; +} + +/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 옵션 칸은 옵션 이름 · 나머지는 이름 표. */ export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void { + const option = row.option + ? def.types.find((t) => t.type_id === typeId)?.options.find((o) => o.key === row.option) + : undefined; + if (option) { + option.label = value; + return; + } const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined; if (group) { group.이름 = value; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts index 3235e7762..a2c9f8c46 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts @@ -48,9 +48,11 @@ const TEXT = { FormAuto: ["자동", "Auto"], SpecHead: ["구조물 명세 이름", "Structure spec names"], SpecNote: [ - "컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림", - "Rename containers › items › values only — keys, required/optional, form and structure come from the spec", + "제목 › 칸 이름 › 선택지 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 칸 더하기 · 빼기는 읽기만 · [저장]하면 B05 · B06 이 이 이름으로 그림", + "Rename titles › fields › choices only — keys, required/optional, form and adding/removing fields are read-only", ], + SpecLoose: ["그룹 밖 칸", "Fields outside groups"], + SpecNone: ["고칠 이름 없음 — 칸이 없는 종류", "No names to edit — this kind has no fields"], SpecRequired: ["필수", "Required"], SpecOptional: ["선택", "Optional"], SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"], diff --git a/resources/tester/helper_m02_so_def_spec.cjs b/resources/tester/helper_m02_so_def_spec.cjs index ede10f95b..6634b447e 100644 --- a/resources/tester/helper_m02_so_def_spec.cjs +++ b/resources/tester/helper_m02_so_def_spec.cjs @@ -6,12 +6,20 @@ const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); +// 이웃 TS(`./…`)는 같은 길로 읽음 — 명세 파일이 서로 import 함 +const cache = {}; const load = (...parts) => { - const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), { + const file = path.join(ROOT, ...parts); + if (cache[file]) return cache[file].exports; + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; - const box = { exports: {} }; - new Function("exports", "module", "require", js)(box.exports, box, require); + const box = (cache[file] = { exports: {} }); + const near = (id) => + id.startsWith(".") + ? load(path.relative(ROOT, path.resolve(path.dirname(file), `${id}.ts`))) + : require(id); + new Function("exports", "module", "require", js)(box.exports, box, near); return box.exports; }; const m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts"); @@ -61,4 +69,27 @@ m.setSpecName( ); out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름; +// 정의 그룹 꼴(52-12) — 맹암거 = 그룹 제목 › 칸 · 옆도랑 = 그룹 밖 칸 › 선택지 +const g = real(); +const ud = g.types.find((t) => t.type_id === "cross_underdrain"); +const udRows = m.groupNameRows(g, ud); +out.udRows = udRows.map((r) => [r.level, r.nameKey, r.label]); +m.setSpecName(g, ud.type_id, udRows[0], "맹암거 본체"); +m.setSpecName(g, ud.type_id, udRows[1], "암거 높이 칸"); +out.udGroup = g.그룹.cross_underdrain[0].이름; +out.udLabel = ud.options[0].label; +const side = g.types.find((t) => t.type_id === "ditch_side"); +const sideRows = m.groupNameRows(g, side); +out.sideHead = sideRows[0]; +const lshape = sideRows.find((r) => r.nameKey === "form=L형"); +out.sideChoice = lshape && [lshape.level, lshape.label]; +m.setSpecName(g, side.type_id, lshape, "L 모양"); +out.sideName = g.종류.ditch_side.이름["form=L형"]; +out.sideChoices = side.options.find((o) => o.key === "form").choices; +out.sideReread = m.groupNameRows(g, side).find((r) => r.nameKey === "form=L형").label; +out.bareKind = m.groupNameRows( + g, + g.types.find((t) => t.type_id === "open_ditch"), +).length; + process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_m02_so_def_spec.py b/resources/tester/test_m02_so_def_spec.py index 3677816ff..e575dad1a 100644 --- a/resources/tester/test_m02_so_def_spec.py +++ b/resources/tester/test_m02_so_def_spec.py @@ -8,6 +8,8 @@ pytest 는 그 결과(JSON)를 판정. - 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀. - 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음. - 이름 표가 빈 종류는 명세 글로 읽음. + - 정의 그룹 꼴(52-12): 그룹 제목 › 칸 › 선택지 · 제목 = 그룹 이름 · 칸 = 옵션 이름 · + 선택지 = 이름 표(값 그대로) · 칸 없는 종류는 줄 없음. """ from __future__ import annotations @@ -56,3 +58,17 @@ def test_구조는_그대로(r) -> None: def test_이름_표_없는_종류(r) -> None: assert r["bareLabel"] == "흙막이" assert r["bareGroup"] == "유입" + + +def test_정의_그룹_꼴(r) -> None: + assert r["udRows"] == [ + [0, "#gcrossud", "맹암거"], + [1, "culvert_height_m", "암거 높이"], + [1, "culvert_slope_pct", "암거 경사"], + ] + assert r["udGroup"] == "맹암거 본체" and r["udLabel"] == "암거 높이 칸" + assert r["sideHead"]["nameKey"] == "" and r["sideHead"]["level"] == 0 + assert r["sideChoice"] == [2, "L형"] + assert r["sideName"] == "L 모양" and r["sideReread"] == "L 모양" + assert "L형" in r["sideChoices"] + assert r["bareKind"] == 0 diff --git a/resources/tester/ui_checks/52-11_정의_이름고치기.js b/resources/tester/ui_checks/52-11_정의_이름고치기.js index 6d4679afe..8cffe5b2b 100644 --- a/resources/tester/ui_checks/52-11_정의_이름고치기.js +++ b/resources/tester/ui_checks/52-11_정의_이름고치기.js @@ -1,4 +1,5 @@ /* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림 + * 52-12 — 정의 그룹 꼴 종류(맹암거 · 개거 · 바닥막이 …)도 같은 이름 나무 · 옛 편집기는 모든 종류에서 숨김 * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여 * M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자: * orca eval --expression "window.__m5211='m02'; $(cat 이 파일)" @@ -7,11 +8,14 @@ * 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 · * B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림 * 기대 수치(m02): - * - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기 + * - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류도 이름 나무 + * - 맹암거 = 그룹 제목 › 칸 2 · 고치면 미리보기 그룹 상자 제목 · 칸 이름 따라감 + * - 바닥막이 = 선택지 이름 고치면 미리보기 보기 글 따라감(값 그대로) · 개거 = 고칠 이름 없음 · 종류 이름만(왼쪽 목록 따라감) * - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님) * - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐 * - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로 * 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true + * group.* = 맹암거 그룹 제목 · 칸 이름 · 바닥막이 선택지 글이 새 구조물 폼에 그대로(값 그대로) */ (async () => { const MODE = window.__m5211 || "m02"; @@ -44,10 +48,74 @@ row.click(); if (!(await until(() => $$("[data-sod-kind]").length))) return JSON.stringify({ error: "종류 목록 없음" }); - // 명세 없는 종류 — 옛 편집기 그대로 + // 명세 없는 종류 — 옛 편집기 숨김 · 이름 나무(52-12) $('[data-sod-kind="retaining_wall"]').click(); await W(200); - out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") }; + out.other = { + base: !!$("[data-sod-base]"), + spec: !!$("[data-sod-spec-block]"), + blocks: $("[data-sod-detail]").children.length, + }; + const nameBox = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`); + const groupPreview = () => $("[data-sod-spec-preview]"); + // 맹암거 — 그룹 제목 › 칸 + $('[data-sod-kind="cross_underdrain"]').click(); + await W(200); + { + const keys = $$("[data-sod-spec]").map((r) => r.dataset.sodSpec); + const g0 = nameBox("#gcrossud").value; + const o0 = nameBox("culvert_height_m").value; + put(nameBox("#gcrossud"), "맹암거X"); + await W(150); + put(nameBox("culvert_height_m"), "암거 높이X"); + await W(150); + const pv = groupPreview(); + out.ud = { + keys, + legend: [...pv.querySelectorAll("legend")].some((l) => l.textContent === "맹암거X"), + label: pv.textContent.includes("암거 높이X (m)"), + old: !!$("[data-sod-option]") || !!$("[data-sod-base]"), + }; + put(nameBox("#gcrossud"), g0); + await W(150); + put(nameBox("culvert_height_m"), o0); + await W(150); + out.ud.restored = + nameBox("#gcrossud").value === g0 && nameBox("culvert_height_m").value === o0; + } + // 바닥막이 — 선택지 이름(값 그대로) + $('[data-sod-kind="bed_sill"]').click(); + await W(200); + { + const k = "form=돌붙임(메)"; + const c0 = nameBox(k).value; + put(nameBox(k), "돌붙임 메X"); + await W(150); + const opt = [...groupPreview().querySelectorAll("option")].find( + (o) => o.textContent === "돌붙임 메X", + ); + out.bed = { choice: !!opt, value: opt?.value }; + put(nameBox(k), c0); + await W(150); + out.bed.restored = nameBox(k).value === c0; + } + // 개거 — 칸 없음 · 종류 이름만 + $('[data-sod-kind="open_ditch"]').click(); + await W(200); + { + const kind = $("[data-sod-kind-name]"); + const n0 = kind.value; + out.ditch = { + rows: $$("[data-sod-spec]").length, + none: !!$("[data-sod-spec-block] p.m02-side__empty"), + }; + put(kind, "개거X"); + await W(200); + out.ditch.list = $('[data-sod-kind="open_ditch"]').textContent.includes("개거X"); + put($("[data-sod-kind-name]"), n0); + await W(200); + out.ditch.restored = $('[data-sod-kind="open_ditch"]').textContent.includes(n0); + } // 배관 — 명세 이름 나무 $('[data-sod-kind="pipe"]').click(); await W(200); @@ -102,8 +170,21 @@ out.restored = Object.entries(orig).every(([k, v]) => box(k).value === v); out.판After = (await getDef()).판; out.ok = - out.other.base && - !out.other.spec && + !out.other.base && + out.other.spec && + out.other.blocks === 2 && + out.ud.keys.join() === "#gcrossud,culvert_height_m,culvert_slope_pct" && + out.ud.legend && + out.ud.label && + !out.ud.old && + out.ud.restored && + out.bed.choice && + out.bed.value === "돌붙임(메)" && + out.bed.restored && + out.ditch.rows === 0 && + out.ditch.none && + out.ditch.list && + out.ditch.restored && out.spec.containers === 5 && out.spec.inputs === out.spec.rows && out.spec.nameInputs === out.spec.rows && @@ -149,7 +230,41 @@ ), }; (li.querySelector("button") ?? li).click(); // 재선택 = 해제 - out.ok = Object.values(out.named).every(Boolean); + await W(500); + // 정의 그룹 꼴(52-12) — 받아 둔 종류 객체만 고치고 새 구조물 폼(군 · 종류 고르기 · 놓지 않음)으로 봄 + const types = await api.fetchStructureTypes(true); + const ud = types.find((t) => t.type_id === "cross_underdrain"); + ud.groups[0].name = "맹암거X"; + ud.options[0].label = "암거 높이X"; + const bed = types.find((t) => t.type_id === "bed_sill"); + bed.spec_names = { ...(bed.spec_names ?? {}), "form=돌붙임(메)": "돌붙임 메X" }; + const count = $$(".b05-route__irregular-list li").length; + const [famSel, kindSel] = $$("select", sec); + const choose = async (fam, kind) => { + famSel.value = fam; + famSel.dispatchEvent(new Event("change", { bubbles: true })); + await W(300); + kindSel.value = kind; + kindSel.dispatchEvent(new Event("change", { bubbles: true })); + await W(500); + }; + await choose("A", "cross_underdrain"); + out.group = { + legend: $$("legend", sec).some( + (l) => l.offsetParent !== null && l.textContent.includes("맹암거X"), + ), + label: $$(".b05-route__field > span", sec).some( + (n) => n.offsetParent !== null && n.textContent === "암거 높이X (m)", + ), + }; + await choose("D", "bed_sill"); + const opt = $$("option", sec).find((o) => o.textContent === "돌붙임 메X"); + out.group.choice = !!opt && opt.value === "돌붙임(메)"; + famSel.value = ""; + famSel.dispatchEvent(new Event("change", { bubbles: true })); + await W(300); + out.group.noWrite = $$(".b05-route__irregular-list li").length === count; + out.ok = Object.values(out.named).every(Boolean) && Object.values(out.group).every(Boolean); } return JSON.stringify(out); })();