refactor(M02): 띠 알림 · 빈 상태 · 보조 글 · 배지 · 탭 · 표 · 목록 줄 · 도구줄 · 체크를 부품으로 (PLAN 553-5)

- DataFlow: 표 넷 = 데이터표 · 함수 판 알림 · 선/알약 경고 · 끊긴 곳 = 띠 알림 · 알약 · 칸 종류 = 배지 · 고르는 줄 = 목록 줄 · 입력 넷 = 입력 · 도구줄 여섯 · 날 색 → 공통 값
- 구조물 옵션 · 구조물 · 도면 · 면적 · 품목 고르기: 판 띠 = 띠 알림 · 한눈/종류별/부품 = 탭 · 표 둘 = 데이터표 · 체크 둘 = 체크 · 카드 · 번호 = 카드 · 배지 · 도면 도구줄
- 일위대가 · 호표 · 실무자료 · 본 화면 · 서고: 알림 = 띠 알림 · 빈 상태 · 보조 글 · 도구줄 여섯 · 옛 보라 별칭 0
- M01 로직 입력칸 = 입력 부품(작은 칸) · 일위대가 상세 줄 수 고정 시험 693 · 지킴 시험 M02 몫 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 23:44:19 +09:00
co-authored by Claude Opus 5.5
parent 022eff69f5
commit 09134bab84
86 changed files with 583 additions and 705 deletions
+28 -16
View File
@@ -38,6 +38,9 @@ import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
import { addFromTemplate, templates } from "./M02_SO_Def_Add";
import { buildGroupBlock } from "./M02_SO_Groups";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
import { createTabs } from "@ui/ui_template_tabs";
/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */
const OLD_EDITOR = false;
@@ -93,7 +96,11 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const canEdit = (): boolean => opt.isAdmin && !past;
const kindList = el("div", { className: "m02-opts__kinds", attrs: { "data-sod-kinds": "" } });
const detail = el("div", { className: "m02-opts__list", attrs: { "data-sod-detail": "" } });
const banner = el("div", { className: "m02-sod__banner", attrs: { "data-sod-banner": "" } });
// 판 띠 = 띠 알림 부품 꼴(정보 · 지난 판 = 경고)
const banner = el("div", {
className: "ui-banner ui-banner--info m02-sod__banner",
attrs: { "data-sod-banner": "" },
});
const issues = el("div", { className: "m02-sod__issues", attrs: { "data-sod-issues": "" } });
const historyBox = el("div", {
className: "m02-sod__history",
@@ -161,8 +168,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const off = t.enabled === false;
const groups = d.그룹[t.type_id]?.length ?? 0;
const b = el("button", {
className: `m02-opts__kind${t.type_id === current ? " is-active" : ""}${off ? " is-off" : ""}`,
attrs: { type: "button", "data-sod-kind": t.type_id },
className: `m02-opts__kind ui-list-row ui-sidebar-row${t.type_id === current ? " is-active" : ""}${off ? " is-off" : ""}`,
attrs: { type: "button", "data-sod-kind": t.type_id, "data-size": "32" },
children: [
el("span", { text: t.name }),
...(groups ? [createTag(to("GroupTag", String(groups)), "accent")] : []),
@@ -204,7 +211,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const d = shown();
const type = d.types.find((t) => t.type_id === current);
if (!type) {
detail.replaceChildren(el("p", { className: "m02-side__empty", text: to("Pick") }));
detail.replaceChildren(createEmpty({ text: to("Pick"), small: true }));
return;
}
const edit = canEdit();
@@ -220,7 +227,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
attr: "data-sod-kind-name",
cls: "m02-sod__name",
}),
el("small", { className: "m02-opts__key", text: type.type_id }),
el("small", { className: "ui-text ui-text--muted m02-opts__key", text: type.type_id }),
// 안 씀 = 목록 B06 만(enabled 는 목록 짝 · 322-2) · 숨김(hidden)은 한눈 표 목록 칸에서
checkBox(
to("Unused"),
@@ -269,14 +276,17 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
["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;
}),
);
// 탭 부품(보조 탭) — 칸마다 data-sod-tab 그대로
const tabs = createTabs({
kind: "pill",
tabs: pairs.map(([key, label]) => ({ key, label })),
value: tab,
onChange: (key) => ((tab = key as Tab), paint()),
});
tabs.root.querySelectorAll<HTMLElement>("[data-tab]").forEach((b) => {
b.setAttribute("data-sod-tab", b.dataset.tab ?? "");
});
tabBar.replaceChildren(tabs.root);
const edit = canEdit();
if (tab === "table")
body.replaceChildren(buildTable({ def: shown(), edit, changed, issues: allProblems() }));
@@ -297,8 +307,10 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
]),
);
banner.classList.toggle("is-past", !!past);
banner.classList.toggle("ui-banner--warning", !!past);
banner.classList.toggle("ui-banner--info", !past);
const list = allProblems();
issues.replaceChildren(...list.map((t) => el("p", { className: "m02-sod__bad", text: t })));
issues.replaceChildren(...list.map((t) => createText(t, "error")));
save.disabled = !opt.isAdmin || !!past || !dirty || list.length > 0;
};
@@ -366,7 +378,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
});
return b;
})
: [el("p", { className: "m02-side__empty", text: to("PastNone") })]),
: [createEmpty({ text: to("PastNone"), small: true })]),
);
};
const history = createButton({ label: to("PastList"), variant: "ghost" });
@@ -387,7 +399,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const node = el("div", {
className: "m02-opts m02-sod",
children: [
el("p", { className: "m02-side__empty", text: to("DefNote") }),
createText(to("DefNote")),
banner,
tabBar,
body,
@@ -20,6 +20,8 @@ import {
import { checkBox, head, moveButtons, pickBox, textBox } from "./M02_SO_Def_Ui";
import { buildPlaceSelect } from "./M02_SO_Groups";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
export interface OptionsBlockOptions {
def: StructDef;
@@ -113,8 +115,8 @@ function choicesBlock(opt: OptionsBlockOptions, o: OptionDef): HTMLElement {
return el("div", {
className: "m02-sod__choices",
children: [
el("p", { className: "m02-side__empty", text: to("ChoiceNote") }),
...(rows.length ? rows : [el("p", { className: "m02-sod__bad", text: to("ChoiceNone") })]),
createText(to("ChoiceNote")),
...(rows.length ? rows : [createText(to("ChoiceNone"), "error")]),
...(edit ? [add] : []),
],
});
@@ -171,7 +173,7 @@ function optionRow(opt: OptionsBlockOptions, o: OptionDef): HTMLElement {
attr: "data-sod-name",
cls: "m02-sod__name",
}),
el("small", { className: "m02-opts__key", text: o.key }),
el("small", { className: "ui-text ui-text--muted m02-opts__key", text: o.key }),
...(o.phase === "detail" ? [createTag(to("Detail"), "accent")] : []),
form,
textBox(o.unit ?? "", (v) => (o.unit = v.trim() || null), changed, {
@@ -224,7 +226,7 @@ export function buildOptionsBlock(opt: OptionsBlockOptions): HTMLElement {
...head(to("OptionHead"), to("OptionNote")),
...(type.options.length
? type.options.map((o) => optionRow(opt, o))
: [el("p", { className: "m02-side__empty", text: to("NoOptionKind") })]),
: [createEmpty({ text: to("NoOptionKind"), small: true })]),
...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
@@ -18,6 +18,7 @@ import {
} from "./M02_SO_Def_Reflect";
import { checkBox, head } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
type Branch = keyof typeof PART_BOX;
@@ -40,7 +41,7 @@ export function buildParts(opt: {
const blocks = (Object.keys(PART_BOX) as Branch[]).flatMap((branch) => [
...head(branch),
el("table", {
className: "m02-sod__table m02-sod__table--wrap",
className: "ui-table ui-table--data m02-sod__table m02-sod__table--wrap",
attrs: { "data-sod-parts": branch },
children: [
el("thead", {
@@ -99,6 +100,6 @@ export function buildParts(opt: {
]);
return el("div", {
className: "m02-sod__tablewrap",
children: [el("p", { className: "m02-side__empty", text: to("PartsNote") }), ...blocks],
children: [createText(to("PartsNote")), ...blocks],
});
}
@@ -16,6 +16,8 @@ import {
} from "./M02_SO_Def_Model";
import { head, pickBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createEmpty } from "@ui/ui_template_empty";
import { createCheckbox } from "@ui/ui_template_checkbox";
export interface RulesBlockOptions {
def: StructDef;
@@ -42,15 +44,18 @@ function multiPick(
className: "m02-sod__rulechoices",
attrs: { [attr]: "" },
children: all.map((c) => {
const box = el("input", { attrs: { type: "checkbox", value: c } }) as HTMLInputElement;
box.checked = on.has(c);
box.disabled = !edit;
box.addEventListener("change", () => {
if (box.checked) on.add(c);
else on.delete(c);
set(all.filter((x) => on.has(x)));
const { root } = createCheckbox(c, {
checked: on.has(c),
disabled: !edit,
value: c,
onChange: (checked) => {
if (checked) on.add(c);
else on.delete(c);
set(all.filter((x) => on.has(x)));
},
});
return el("label", { className: "m02-sod__check", children: [box, el("span", { text: c })] });
root.classList.add("m02-sod__check");
return root;
}),
});
}
@@ -227,7 +232,7 @@ export function buildRulesBlock(opt: RulesBlockOptions): HTMLElement {
...head(to("RuleHead"), to("RuleNote")),
...(mine.length
? mine.map((r) => ruleRow(opt, r, def.규칙.indexOf(r)))
: [el("p", { className: "m02-side__empty", text: to("RuleNone") })]),
: [createEmpty({ text: to("RuleNone"), small: true })]),
...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
@@ -28,6 +28,7 @@ import {
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";
import { createEmpty } from "@ui/ui_template_empty";
const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
required: "SpecRequired",
@@ -132,7 +133,7 @@ export function buildSpecBlock(opt: {
cls: "m02-sod__name",
})
: el("span", { text: to("SpecLoose") }),
el("small", { className: "m02-opts__key", text: row.key }),
el("small", { className: "ui-text ui-text--muted m02-opts__key", text: row.key }),
...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []),
...shownBox(
opt,
@@ -164,7 +165,7 @@ export function buildSpecBlock(opt: {
attrs: { "data-sod-spec-block": "" },
children: [
...head(to("SpecHead"), to("SpecNote")),
...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("SpecNone") })]),
...(rows.length ? rows : [createEmpty({ text: to("SpecNone"), small: true })]),
...head(to("SpecPreview")),
el("div", {
className: "m02-sod__specpreview",
@@ -25,6 +25,7 @@ import {
} from "./M02_SO_Def_Reflect";
import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
type Pairs = ReadonlyArray<readonly [string, string]>;
const LIST_PAIRS = (): Pairs => [
@@ -89,7 +90,7 @@ function mark(changed: boolean, ...children: HTMLElement[]): HTMLElement {
/** 형상칸 — 벽(형상 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: "—" });
if (row.shape !== "wall") return el("span", { className: "ui-text ui-text--muted", text: "—" });
const form = String(row.shapeOpts.form ?? "");
const forms = !form || REVET_FORMS.includes(form) ? REVET_FORMS : [form, ...REVET_FORMS];
const box = pickBox(
@@ -278,9 +279,7 @@ export function buildTable(opt: {
el("td", {
attrs: { "data-sod-table-check": "" },
children: [
bad.length
? el("span", { className: "m02-sod__bad", text: bad.join(" · ") })
: createTag(to("Ok"), "accent"),
bad.length ? createText(bad.join(" · "), "error") : createTag(to("Ok"), "accent"),
reset,
],
}),
@@ -292,9 +291,9 @@ export function buildTable(opt: {
return el("div", {
className: "m02-sod__tablewrap",
children: [
el("p", { className: "m02-side__empty", text: to("TableNote") }),
createText(to("TableNote")),
el("table", {
className: "m02-sod__table",
className: "ui-table ui-table--data m02-sod__table",
attrs: { "data-sod-table": "" },
children: [
el("thead", {
@@ -7,6 +7,8 @@
import { createButton, el } from "@ui/ui_template_elements";
import { step } from "./M02_SO_Def_Model";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createCheckbox } from "@ui/ui_template_checkbox";
/** 글 칸 — 글자마다 `set` · 떠날 때 `done` */
export function textBox(
@@ -69,13 +71,15 @@ export function checkBox(
set: (v: boolean) => void,
opt: { edit: boolean; attr?: string },
): HTMLLabelElement {
const box = el("input", {
attrs: { type: "checkbox", ...(opt.attr ? { [opt.attr]: "" } : {}) },
}) as HTMLInputElement;
box.checked = on;
box.disabled = !opt.edit;
box.addEventListener("change", () => set(box.checked));
return el("label", { className: "m02-sod__check", children: [box, el("span", { text: label })] });
// 체크 부품 — 속성(data-sod-*)은 칸에 그대로
const { root, input } = createCheckbox(label, {
checked: on,
disabled: !opt.edit,
onChange: set,
});
if (opt.attr) input.setAttribute(opt.attr, "");
root.classList.add("m02-sod__check");
return root;
}
/** 고르기 — [값, 글] 짝 */
@@ -117,5 +121,5 @@ export function moveButtons<T>(
/** 절 제목 + 안내 한 줄 */
export const head = (title: string, note?: string): HTMLElement[] => [
el("h4", { className: "m02-opts__group", text: title }),
...(note ? [el("p", { className: "m02-side__empty", text: note })] : []),
...(note ? [createText(note)] : []),
];
@@ -11,6 +11,8 @@ import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements";
import { SHAPE_KEYS, type GroupDef } from "./M02_SO_Def_Model";
import { checkBox, textBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
const NAME_MAX = 40;
@@ -109,7 +111,7 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
children: [
name,
el("small", {
className: "m02-opts__key",
className: "ui-text ui-text--muted m02-opts__key",
text: to("GroupCount", String(group.옵션.length)),
}),
...(opt.edit ? [up, down, del] : []),
@@ -129,11 +131,12 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
opt.changed();
});
return el("div", {
className: "m02-sog",
className: "ui-card m02-sog",
attrs: { "data-pad": "12" },
children: [
el("h4", { className: "m02-opts__group", text: to("GroupHead") }),
el("p", { className: "m02-side__empty", text: to("GroupNote") }),
...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("GroupNone") })]),
createText(to("GroupNote")),
...(rows.length ? rows : [createEmpty({ text: to("GroupNone"), small: true })]),
...(opt.edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
@@ -7,6 +7,7 @@
import { el } from "@ui/ui_template_elements";
import { openDefScreen } from "./M02_SO_Def";
import "@ui/ui_template_card.css";
import "./M02_SO_Style.css";
import { to } from "./M02_SO_Text";
@@ -37,28 +37,11 @@
font-size: var(--text-body-sm);
}
/* 종류 줄 = 목록 줄 부품(ui-list-row) · 여기는 글 · 꼬리표 늘어놓기만 */
.m02-opts__kind {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-4);
padding: var(--spacing-4) var(--spacing-8);
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: none;
color: var(--color-text);
font: inherit;
text-align: left;
cursor: pointer;
}
.m02-opts__kind:hover {
background: var(--color-mist-violet);
}
.m02-opts__kind.is-active {
border-color: var(--color-accent);
background: var(--color-mist-violet);
}
/* 옵션 줄 = 왼쪽 보이기 체크 · 라벨 · 키 · 표시 / 오른쪽 들어갈 자리(본체 · 그룹) */
@@ -81,14 +64,11 @@
}
/* 옵션 그룹(PLAN 46-2) — 그룹 줄 = 이름 · 옵션 수 · 위로 · 아래로 · 지우기 */
/* 겉 = 카드 부품(ui-card · 여백 12) */
.m02-sog {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--spacing-8) var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface-raised);
}
.m02-sog__row {
@@ -108,9 +88,9 @@
width: 11rem;
}
/* 키 글 = 보조 글 부품(ui-text) · 글줄 안에 놓임 */
.m02-opts__key {
color: var(--color-text-muted);
font-size: 11px;
display: inline-flex;
}
/* 구조물 정의 편집 화면(PLAN 48-2) — 판 띠 · 종류 머리 · 옵션 · 선택지 · 배치 격자 · 규칙 · 지난 판 */
@@ -118,19 +98,9 @@
max-width: 80rem;
}
/* 판 띠 = 띠 알림 부품(정보 · 지난 판 = 경고) · 여기는 한 줄 늘어놓기만 */
.m02-sod__banner {
display: flex;
align-items: center;
gap: var(--spacing-8);
color: var(--color-text-muted);
}
.m02-sod__banner.is-past {
padding: var(--spacing-4) var(--spacing-8);
border: 1px solid var(--color-accent);
border-radius: var(--radius-sm);
background: var(--color-mist-violet);
color: var(--color-text);
}
.m02-sod__family,
@@ -267,7 +237,7 @@
}
.m02-sod__cell.is-base {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
.m02-sod__cell.is-bad,
@@ -328,11 +298,6 @@
gap: var(--spacing-4);
}
.m02-sod__bad {
margin: 0;
color: var(--color-danger);
}
/* 구조물 명세 이름 고치기(PLAN 52-11) — 컨테이너 › 항목 › 값 들여쓰기 */
.m02-sod__specrow {
display: flex;
@@ -370,25 +335,20 @@
}
/* ── 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);
/* 표 = 표 부품 데이터표(ui-table--data) · 여기는 폭 · 줄바꿈만 */
table.m02-sod__table {
width: auto;
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 {
@@ -434,7 +394,7 @@
}
/* 부품 탭 — 긴 글은 줄바꿈해 표가 화면 안에(322-2 다듬기) */
.m02-sod__table--wrap {
table.m02-sod__table--wrap {
table-layout: fixed;
width: 100%;
}