From cb02b1f1529cab7a3995eed40b08d679f19607f4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 22:02:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?test(552=C2=B7B06):=20=EC=8B=9C=ED=97=98=20?= =?UTF-8?q?=EB=91=98=20=EA=B3=A0=EC=B9=A8=20=E2=80=94=20=EB=B6=80=ED=92=88?= =?UTF-8?q?=20=EB=AA=A8=EC=9D=8C=20=EA=B2=AC=EB=B3=B8=20=EA=B8=80=20?= =?UTF-8?q?=E3=80=8C=EB=8D=94=ED=95=98=EA=B8=B0=E3=80=8D=20=E2=86=92=20?= =?UTF-8?q?=E3=80=8C=EC=B6=94=EA=B0=80=E3=80=8D=20=C2=B7=20=EB=8B=A4?= =?UTF-8?q?=EB=8B=A8=20=EC=84=B1=ED=86=A0=EB=B6=80=EC=84=A0=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=EC=9D=B4=20logic=20=EC=9C=BC=EB=A1=9C=20=EC=98=AE?= =?UTF-8?q?=EA=B8=B4=20B06=20=EA=B4=80=20=EB=AA=A8=EC=96=91=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EC=9D=BD=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui_template_ui_kit_parts.ts 아이콘 단추 견본 title 「더하기」가 보이는 글 금지어(PLAN 30-1)에 걸림 → 「추가」 - test_user_values_survive.py 다단 성토부선 시험 = 옛 B06_Section_UI_Cross_Culvert_{Geom,Tiers}.ts 경로 · e4befd158(518-2)에서 B06_Section/logic/B06_Section_Cross_Culvert_{Geom,Tiers}.ts 로 옮겨 감 · 판정 글은 그대로 통과 Co-Authored-By: Claude Opus 5.5 --- resources/tester/test_user_values_survive.py | 4 ++-- ui_template/ui_template_ui_kit_parts.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/tester/test_user_values_survive.py b/resources/tester/test_user_values_survive.py index ca94329e9..8b8f4fd80 100644 --- a/resources/tester/test_user_values_survive.py +++ b/resources/tester/test_user_values_survive.py @@ -236,9 +236,9 @@ def test_저장된_표준단면이_브라우저까지_간다(): # 값으로 재는 확인은 화면 실측에 있음(보조 창 b269ea34 81.77m — 단 0 → 1 에서 성토 # 10.9027 → 10.6743㎡). 계획서에 그 수치가 남아 있다. def test_다단_성토부선이_면적_트림에_들어간다(): - # 다단 · 묻힌 구간 거르기는 `_Culvert_Tiers` 로 나뉘어 있어 두 파일을 함께 읽음. + # 다단 · 묻힌 구간 거르기는 `logic/_Cross_Culvert_Tiers` 로 나뉘어 있어 두 파일을 함께 읽음. source = "".join( - (PROJECT_ROOT / "B06_Section" / f"B06_Section_UI_Cross_Culvert_{name}.ts").read_text( + (PROJECT_ROOT / "B06_Section" / "logic" / f"B06_Section_Cross_Culvert_{name}.ts").read_text( encoding="utf-8" ) for name in ("Geom", "Tiers") diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts index d77d99933..47660f9dc 100644 --- a/ui_template/ui_template_ui_kit_parts.ts +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -220,7 +220,7 @@ export const KIT_PARTS: KitPart[] = [ demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)))), sizes: (z) => createButton({ label: String(z), size: z, iconStart: "plus", onClick: noop }) }, { key: "icon-button", name: "아이콘 단추", group: "basic", kinds: "고스트 · 선 · 채움 · 켜짐(aria-pressed) · 글자 기호 · 여덟 단 · title 필수", file: "ui_template_button.ts", - demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "더하기", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s))), + demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "추가", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s))), sizes: (z) => createIconButton({ icon: "settings", title: `설정 ${z}`, size: z, variant: "outline" }) }, { key: "input", name: "입력", group: "basic", kinds: "글 · 수 · 단위 붙음 · 찾기 · 날짜 · 여러 줄 · 여덟 단", file: "ui_template_input.ts", demo: (s) => fieldDemo(s, () => { const f = createInputField({ placeholder: "폭", type: "number", unit: "m", size: 32 }); return { root: f.root, node: f.input, setError: f.setError }; }), From 0a3bab12238f55d1fd488dfc68eb6991c865d62c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 22:02:54 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor(D04):=20=EC=97=85=EC=B2=B4=20?= =?UTF-8?q?=ED=91=9C=EB=A5=BC=20=ED=91=9C=20=EB=B6=80=ED=92=88=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20(PLAN=20553-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts | 59 ++++++++++---------- D04_PlanConfirm/D04_PlanConfirm_UI_Style.css | 23 -------- resources/tester/test_553_4_de_parts.py | 2 + 3 files changed, 31 insertions(+), 53 deletions(-) diff --git a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts index aa595d520..654cc0f3b 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts +++ b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts @@ -25,6 +25,7 @@ import { import { createBanner } from "@ui/ui_template_banner"; import { createCheckbox } from "@ui/ui_template_checkbox"; import { createPanelCard } from "@ui/ui_template_collapsible"; +import { tableRow, tableShell, type Column } from "@ui/ui_template_table"; import { t } from "@ui/ui_template_locale"; import { stationLabel } from "../common_util/common_util_station"; import { clearState, readState, writeState } from "../A00_Common/b_page_state"; @@ -212,42 +213,40 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { function draw(): void { const list = current(); - const head = el("tr", { - children: [ - "D04_Col_Year", - "D04_Col_Length", - "D04_Col_Start", - "D04_Col_End", - "D04_Col_Chainage", - "D04_Col_Confirmed", - "D04_Col_Assignees", - ].map((key) => el("th", { text: t(key as Parameters[0]) })), - }); - const rows = summary.years.map((row) => { + const columns: Column[] = [ + "D04_Col_Year", + "D04_Col_Length", + "D04_Col_Start", + "D04_Col_End", + "D04_Col_Chainage", + "D04_Col_Confirmed", + "D04_Col_Assignees", + ].map((key, i) => ({ + label: t(key as Parameters[0]), + ...(i === 1 ? { align: "end" as const } : {}), + })); + const { root: tableRoot, tbody } = tableShell(columns); + tableRoot.querySelector("table")?.classList.add("d04-confirm__table"); + for (const row of summary.years) { const firms = list.filter((item) => item.year === row.year).map(firmChip); const chain = row.saved ? `${station(row.start_chainage_m)} ~ ${station(row.end_chainage_m)}` : t("D04_NotSaved"); - return el("tr", { - attrs: { "data-year": String(row.year) }, - children: [ - el("td", { text: String(row.year) }), - el("td", { text: fmt(row.length_m), attrs: { "data-num": "" } }), - el("td", { text: where(row.start) }), - el("td", { text: where(row.end) }), - el("td", { text: chain }), - el("td", { text: kstText(row.confirmed_at) }), - el("td", { children: editable ? [...firms, addForm(row.year)] : firms }), - ], - }); - }); - const table = el("table", { - className: "d04-confirm__table", - children: [el("thead", { children: [head] }), el("tbody", { children: rows })], - }); + const tr = tableRow(columns, [ + String(row.year), + fmt(row.length_m), + where(row.start), + where(row.end), + chain, + kstText(row.confirmed_at), + el("div", { children: editable ? [...firms, addForm(row.year)] : firms }), + ]); + tr.dataset.year = String(row.year); + tbody.append(tr); + } const notes = draft() ? [createBanner({ tone: "warning", text: t("D04_Dirty") })] : []; notes[0]?.classList.add("d04-confirm__dirty"); - main.replaceChildren(el("h2", { text: summary.name }), ...notes, table); + main.replaceChildren(el("h2", { text: summary.name }), ...notes, tableRoot); pickList.replaceChildren( ...summary.years diff --git a/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css b/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css index 2260840d3..53c4fb41b 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css +++ b/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css @@ -10,29 +10,6 @@ overflow: auto; } -.d04-confirm__table { - border-collapse: collapse; - font-size: var(--text-body-sm); - color: var(--color-text-body); -} - -.d04-confirm__table th, -.d04-confirm__table td { - padding: var(--spacing-8); - border: 1px solid var(--color-border); - text-align: left; - vertical-align: top; -} - -.d04-confirm__table thead th { - background: var(--color-surface); -} - -.d04-confirm__table td[data-num] { - text-align: right; - font-variant-numeric: tabular-nums; -} - .d04-confirm__firm, .d04-confirm__add { display: flex; diff --git a/resources/tester/test_553_4_de_parts.py b/resources/tester/test_553_4_de_parts.py index 9fc87bb68..f22a3a7e0 100644 --- a/resources/tester/test_553_4_de_parts.py +++ b/resources/tester/test_553_4_de_parts.py @@ -57,6 +57,8 @@ def test_d04_parts(): assert "input:checked" in ts # [확정] 이 고른 연도를 읽는 길 assert "border:" not in css.split(".d04-confirm__input")[1].split("}")[0] assert ".d04-confirm__dirty" not in css + assert 'el("table"' not in ts and "tableShell(columns)" in ts + assert "d04-confirm__table" not in css # 표 모양은 표 부품 · 이름은 촬영 장면용 걸이 def test_e01_missing_is_banner_part(): From eaf507973655589f87b8f76a6af62cf62cc0a2cf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 22:04:38 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor(E02):=20=EC=A7=80=EB=B0=98=20?= =?UTF-8?q?=ED=91=9C=20=EB=84=B7=EC=9D=84=20=ED=91=9C=20=EB=B6=80=ED=92=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=C2=B7=20=EC=B9=B8=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=A4=84=EC=9D=84=20=EC=9E=85=EB=A0=A5=20=EB=B6=80=ED=92=88=20?= =?UTF-8?q?=EA=BC=B4=EB=A1=9C=20(PLAN=20553-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- E02_StabGround/E02_StabGround_UI_Style.css | 37 +--------------------- E02_StabGround/E02_StabGround_UI_Tables.ts | 37 ++++++++++++---------- E02_StabGround/E02_StabGround_UI_Water.ts | 4 +-- resources/tester/test_553_4_de_parts.py | 3 ++ 4 files changed, 26 insertions(+), 55 deletions(-) diff --git a/E02_StabGround/E02_StabGround_UI_Style.css b/E02_StabGround/E02_StabGround_UI_Style.css index 3e715fb20..bade5d2fd 100644 --- a/E02_StabGround/E02_StabGround_UI_Style.css +++ b/E02_StabGround/E02_StabGround_UI_Style.css @@ -41,38 +41,11 @@ gap: var(--spacing-4); } -.e02-table { - width: 100%; - border-collapse: collapse; - font-size: var(--text-body-sm); - color: var(--color-text-body); -} - -.e02-table th { - padding: var(--spacing-4); - border-bottom: 1px solid var(--color-border); - color: var(--color-text-secondary); - font-weight: var(--font-weight-medium); - text-align: left; - white-space: nowrap; -} - -.e02-table td { - padding: 2px var(--spacing-4); - vertical-align: middle; -} - +/* 표 모양은 표 부품 — 여기는 읽기 표 줄 안 줄바꿈만 */ .e02-table--read td { - padding: 2px var(--spacing-8); - border-bottom: 1px solid var(--color-border); white-space: nowrap; } -.e02-table--read tr.is-zoned td { - background: var(--color-mist-violet); - color: var(--color-text); -} - .e02-stations { max-height: 320px; overflow: auto; @@ -112,14 +85,6 @@ font-size: var(--text-body-sm); } -.e02-field { - display: flex; - flex-direction: column; - gap: 2px; - font-size: var(--text-caption); - color: var(--color-text-secondary); -} - .e02-status { margin: var(--spacing-8) 0 0; white-space: pre-line; diff --git a/E02_StabGround/E02_StabGround_UI_Tables.ts b/E02_StabGround/E02_StabGround_UI_Tables.ts index 7f9967e9e..2c352bc21 100644 --- a/E02_StabGround/E02_StabGround_UI_Tables.ts +++ b/E02_StabGround/E02_StabGround_UI_Tables.ts @@ -15,6 +15,7 @@ import { } from "@ui/ui_template_elements"; import { createCard } from "@ui/ui_template_card"; import { createCheckbox } from "@ui/ui_template_checkbox"; +import { createTable } from "@ui/ui_template_table"; import { createText } from "@ui/ui_template_text"; import { t, type LocaleKey } from "@ui/ui_template_locale"; import { inferStationInterval, stationLabel } from "../common_util/common_util_station"; @@ -119,16 +120,15 @@ export function headButtons( head.append(...buttons); } -export function table(headers: string[], rows: HTMLElement[][]): HTMLTableElement { - const node = el("table", { className: "e02-table" }) as HTMLTableElement; - const tr = node.createTHead().insertRow(); - for (const text of headers) tr.append(el("th", { text })); - const body = node.createTBody(); - for (const cells of rows) { - const row = body.insertRow(); - for (const cell of cells) row.insertCell().append(cell); - } - return node; +/** 표 부품(데이터표) — 머리 글 "" = 줄 끝 단추 칸 · 돌려주는 것 = 표 감싸개(속 `table.e02-table`) */ +export function table(headers: string[], rows: HTMLElement[][]): HTMLElement { + const wrap = createTable( + headers.map((label) => ({ label })), + rows, + { density: "data" }, + ); + wrap.querySelector("table")?.classList.add("e02-table"); + return wrap; } function defaultMaterial(view: E02View, id: string): Material | undefined { @@ -200,9 +200,9 @@ function materialsCard(view: E02View): HTMLElement { const mapRow = el("div", { className: "e02-map" }); mapRow.append(el("span", { className: "e02-map__title", text: t("E02_LabelMap") })); for (const label of LABELS) { - const field = el("label", { className: "e02-field" }); + const field = el("label", { className: "ui-field e02-field" }); field.append( - el("span", { text: L(`E02_Label_${label}`) }), + el("span", { className: "ui-field__label", text: L(`E02_Label_${label}`) }), selectInput(view, `label_map.${label}`, layerOptions(g), g.label_map[label], (v) => { g.label_map[label] = v; view.rebuild(); @@ -327,8 +327,11 @@ function zonesCard(view: E02View): { section: HTMLElement; refresh: () => void } el("span", { text: fmt(r.soil_thickness_m) }), ]), ); - resolved.forEach((r, i) => node.tBodies[0].rows[i].classList.toggle("is-zoned", r.zoned)); - node.classList.add("e02-table--read"); + node.querySelectorAll("tbody tr").forEach((tr, i) => { + tr.classList.toggle("is-zoned", resolved[i].zoned); + tr.classList.toggle("is-active", resolved[i].zoned); + }); + node.querySelector("table")?.classList.add("e02-table--read"); stationsHost.append(node); }; refresh(); @@ -393,7 +396,7 @@ function safetyCard(view: E02View): HTMLElement { ]); const { section, head } = card( "E02_Card_Safety", - table(["", ...SLOPES.map((s) => L(`E02_Slope_${s}`))], rows), + table(["\u00a0", ...SLOPES.map((s) => L(`E02_Slope_${s}`))], rows), ); headButtons( view, @@ -416,8 +419,8 @@ function loadsCard(view: E02View): HTMLElement { const g = view.ground; const d = view.defaults; const field = (label: string, input: HTMLElement): HTMLElement => { - const node = el("label", { className: "e02-field" }); - node.append(el("span", { text: label }), input); + const node = el("label", { className: "ui-field e02-field" }); + node.append(el("span", { className: "ui-field__label", text: label }), input); return node; }; const kh = numInput(view, "seismic.kh", g.seismic.kh, (v) => (g.seismic.kh = v as number), { diff --git a/E02_StabGround/E02_StabGround_UI_Water.ts b/E02_StabGround/E02_StabGround_UI_Water.ts index 08d1fbba6..e2b8e720f 100644 --- a/E02_StabGround/E02_StabGround_UI_Water.ts +++ b/E02_StabGround/E02_StabGround_UI_Water.ts @@ -23,8 +23,8 @@ import { const L = (key: string): string => t(key as LocaleKey); function field(label: string, input: HTMLElement): HTMLElement { - const node = el("label", { className: "e02-field" }); - node.append(el("span", { text: label }), input); + const node = el("label", { className: "ui-field e02-field" }); + node.append(el("span", { className: "ui-field__label", text: label }), input); return node; } diff --git a/resources/tester/test_553_4_de_parts.py b/resources/tester/test_553_4_de_parts.py index f22a3a7e0..120e4582f 100644 --- a/resources/tester/test_553_4_de_parts.py +++ b/resources/tester/test_553_4_de_parts.py @@ -79,3 +79,6 @@ def test_e02_parts(): for dead in (".e02-card__head", ".e02-card__title", ".e02-hint", ".e02-empty"): assert dead not in css, dead _no_hex("E02_StabGround/E02_StabGround_UI_Style.css") + assert "createTable(" in tables and "createTHead" not in tables + assert 'className: "ui-field e02-field"' in tables # 칸 이름 줄은 입력 부품의 ui-field + assert ".e02-field" not in css and "border-collapse" not in css