diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts index 4cdb60430..49a9d99f0 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts @@ -7,6 +7,8 @@ * ========================================================================== */ import "../B05_Profile/B05_Profile_UI_Style.css"; +import { createCheckbox } from "@ui/ui_template_checkbox"; +import { createSelectField } from "@ui/ui_template_input"; import { openModal } from "@ui/ui_template_modal"; import { createProgressCircle } from "@ui/ui_template_progress"; import { showToast } from "@ui/ui_template_elements"; @@ -51,17 +53,19 @@ function gridBounds(grid: DtmGrid): { export function openPlan3d(projectId: string, years: Plan3dYear[], firstYear: number): void { if (!years.length) return void showToast("3D 로 볼 연도 노선이 없습니다.", "warning"); let viewer: RouteViewer | null = createRouteViewer(); - const pick = document.createElement("select"); - pick.className = "d03-plan__3d-year"; - for (const item of years) - pick.append(new Option(`${item.year}년`, String(item.year), false, item.year === firstYear)); - const corridorBox = document.createElement("label"); - corridorBox.className = "d03-plan__3d-corridor"; - const corridorOn = Object.assign(document.createElement("input"), { - type: "checkbox", + const pick = createSelectField({ + options: years.map((item) => ({ value: String(item.year), text: `${item.year}년` })), + value: String(firstYear), + size: 28, + ariaLabel: "연도", + }).select; + pick.classList.add("d03-plan__3d-year"); + const corridor = createCheckbox(" 예상 코리도(표준 횡단 기본값 · 계획고 = 지반고)", { checked: true, }); - corridorBox.append(corridorOn, " 예상 코리도(표준 횡단 기본값 · 계획고 = 지반고)"); + const corridorBox = corridor.root; + corridorBox.classList.add("d03-plan__3d-corridor"); + const corridorOn = corridor.input; corridorOn.addEventListener("change", () => viewer?.setCorridorVisible(corridorOn.checked)); const top = document.createElement("div"); top.className = "d03-plan__3d-top"; diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts index 2f2c41eee..1232ffa41 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts @@ -8,6 +8,7 @@ import "../B05_Profile/B05_Profile_UI_Style_RouteEdit.css"; import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; import { createWorkflowPanelHandle } from "@ui/ui_template_overlay"; +import { createText } from "@ui/ui_template_text"; import { stationLabel } from "../common_util/common_util_station"; import type { DtmGrid } from "../common_util/common_util_surface_sampler"; import { buildCrossSvg } from "../B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw"; @@ -63,7 +64,7 @@ export function createPlanCross(): PlanCross { const clear = (text: string): void => { head.textContent = ""; - body.replaceChildren(Object.assign(document.createElement("p"), { textContent: text })); + body.replaceChildren(createText(text, "muted", 14)); delete root.dataset.chainage; }; clear("평면에서 측점을 누르면 그 자리 지반선 · 예상 횡단이 보입니다."); diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts index dbad0a984..185ed67b9 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts @@ -15,7 +15,10 @@ import "./D03_PlanAnalysis_UI_Style.css"; import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend"; +import { createCard } from "@ui/ui_template_card"; import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible"; +import { createProgressBar } from "@ui/ui_template_progress"; +import { createText } from "@ui/ui_template_text"; import { createButton, hideLoadingOverlay, @@ -118,12 +121,10 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { main.className = "d03-plan"; main.append(map.root, cross.root); - const terrainText = document.createElement("p"); - terrainText.className = "d03-plan__hint"; - const bar = document.createElement("progress"); - bar.className = "d03-plan__bar"; - bar.max = 100; - bar.hidden = true; + const terrainText = createText("", "muted"); + terrainText.classList.add("d03-plan__hint"); + const bar = createProgressBar(); + bar.root.hidden = true; const more = createButton({ label: "범위 밖 — 더 받기", variant: "filled", @@ -145,7 +146,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { actions.className = "ui-sidebar-actions"; actions.append(analyzeButton, saveButton, viewButton, undoButton); const terrainCard = createPanelCard("지형", false); - terrainCard.append(terrainText, bar, more); + terrainCard.append(terrainText, bar.root, more); const routeCard = createPanelCard("연도별 노선", false); routeCard.append(yearList); const panel = document.createElement("div"); @@ -334,29 +335,33 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { for (const mode of PLAN_MODES) { const route = result.routes[mode]; if (!route) continue; - const card = document.createElement("button"); - card.type = "button"; - card.className = "d03-plan__mode" + (row.mode === mode ? " is-picked" : ""); - card.dataset.mode = mode; const metrics = route.metrics; + const values = createText( + !metrics + ? "노선 없음" + : metrics.max_grade_pct === undefined + ? `연장 ${metrics.length_m.toFixed(1)} m · 기울기 · 위반 = 저장 때 셈` + : `연장 ${metrics.length_m.toFixed(1)} m · 최대 ${metrics.max_grade_pct.toFixed(1)} % · 위반 ${metrics.over_limit?.total ?? 0}`, + "muted", + ); + const card = createCard({ + title: MODE_LABEL[mode], + body: [values], + pad: 12, + selected: row.mode === mode, + onSelect: () => { + if (row.mode === mode) return; + remember(); + choices = { ...choices, [result.year]: mode }; + keepDraft(); + refresh(); + }, + }); + card.classList.add("d03-plan__mode"); + card.classList.toggle("is-picked", row.mode === mode); + card.dataset.mode = mode; card.disabled = !!route.error || !metrics; card.title = route.error ?? ""; - const name = document.createElement("strong"); - name.textContent = MODE_LABEL[mode]; - const values = document.createElement("span"); - values.textContent = !metrics - ? "노선 없음" - : metrics.max_grade_pct === undefined - ? `연장 ${metrics.length_m.toFixed(1)} m · 기울기 · 위반 = 저장 때 셈` - : `연장 ${metrics.length_m.toFixed(1)} m · 최대 ${metrics.max_grade_pct.toFixed(1)} % · 위반 ${metrics.over_limit?.total ?? 0}`; - card.append(name, values); - card.addEventListener("click", () => { - if (row.mode === mode) return; - remember(); - choices = { ...choices, [result.year]: mode }; - keepDraft(); - refresh(); - }); box.append(card); } return box; @@ -526,13 +531,13 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { /** 뒤에서 도는 일을 끝날 때까지 지켜봄 — 진행은 지형 칸에. */ async function watch(kind: "terrain" | "analyze"): Promise { busy = kind; - bar.hidden = false; + bar.root.hidden = false; refresh(); try { for (;;) { if (!root.isConnected) return null; const job = (await fetchPlanProgress(projectId))[kind]; - bar.value = job.percent; + bar.set(job.percent / 100); terrainText.textContent = `${kind === "terrain" ? "지형 받기" : "분석"} ${job.percent}% · ${job.message ?? ""}`; if (!job.running) { if (job.error) showToast(job.error, "error"); @@ -546,7 +551,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { return null; } finally { busy = null; - bar.hidden = true; + bar.root.hidden = true; refresh(); } } diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css index fc5773e7a..234fe200b 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css @@ -63,17 +63,10 @@ .d03-plan__cross-body p { margin: 12px 0; - color: var(--color-text-secondary); } .d03-plan__hint { margin: 0 0 6px; - font-size: 0.85rem; - color: var(--color-text-secondary); -} - -.d03-plan__bar { - width: 100%; } .d03-plan__years { @@ -93,37 +86,10 @@ font-size: 0.85rem; } -.d03-plan__mode { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; - padding: 6px 8px; - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - background: var(--color-surface); - color: var(--color-text); - font-size: 0.8rem; - text-align: left; - cursor: pointer; -} - -.d03-plan__mode.is-picked { - border-color: var(--color-primary); - box-shadow: inset 0 0 0 1px var(--color-primary); -} - -.d03-plan__mode:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.d03-plan__mode span { - color: var(--color-text-secondary); -} - .d03-plan__3d-top { display: flex; + gap: var(--spacing-8); + align-items: center; justify-content: flex-end; margin-bottom: 6px; } diff --git a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts index 6a3ee8416..aa595d520 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts +++ b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts @@ -14,6 +14,7 @@ import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_fro import { kstText } from "../common_util/common_util_time_kst"; import { createButton, + createInputField, createTag, el, hideLoadingOverlay, @@ -21,6 +22,8 @@ import { showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; +import { createBanner } from "@ui/ui_template_banner"; +import { createCheckbox } from "@ui/ui_template_checkbox"; import { createPanelCard } from "@ui/ui_template_collapsible"; import { t } from "@ui/ui_template_locale"; import { stationLabel } from "../common_util/common_util_station"; @@ -146,14 +149,11 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { const listId = `d04-companies-${year}`; const options = el("datalist", { attrs: { id: listId } }); const found = new Map(); - const firm = el("input", { - className: "d04-confirm__input", - attrs: { list: listId, placeholder: t("D04_Company") }, - }); - const mail = el("input", { - className: "d04-confirm__input", - attrs: { type: "email", placeholder: t("D04_Email") }, - }); + const firm = createInputField({ placeholder: t("D04_Company"), size: 28 }).input; + firm.setAttribute("list", listId); + const mail = createInputField({ type: "email", placeholder: t("D04_Email"), size: 28 }).input; + firm.classList.add("d04-confirm__input"); + mail.classList.add("d04-confirm__input"); firm.addEventListener("input", () => { const query = firm.value.trim(); if (!query) return; @@ -245,18 +245,20 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { className: "d04-confirm__table", children: [el("thead", { children: [head] }), el("tbody", { children: rows })], }); - const notes = draft() - ? [el("p", { className: "d04-confirm__dirty", text: t("D04_Dirty") })] - : []; + 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); pickList.replaceChildren( ...summary.years .filter((row) => row.saved) .map((row) => { - const box = el("input", { attrs: { type: "checkbox", value: String(row.year) } }); - box.checked = !row.confirmed_at; - return el("label", { className: "d04-confirm__pick", children: [box, ` ${row.year}`] }); + const pick = createCheckbox(String(row.year), { + checked: !row.confirmed_at, + value: String(row.year), + }); + pick.root.classList.add("d04-confirm__pick"); + return pick.root; }), ); } diff --git a/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css b/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css index 05556bae3..2260840d3 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css +++ b/D04_PlanConfirm/D04_PlanConfirm_UI_Style.css @@ -44,15 +44,6 @@ .d04-confirm__input { min-width: 10rem; - padding: var(--spacing-4) var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-canvas); - color: var(--color-text-body); -} - -.d04-confirm__dirty { - color: var(--color-warning, var(--color-text-secondary)); } .d04-confirm__picks { diff --git a/resources/tester/test_553_4_de_parts.py b/resources/tester/test_553_4_de_parts.py index e37297b87..7cc5d81d6 100644 --- a/resources/tester/test_553_4_de_parts.py +++ b/resources/tester/test_553_4_de_parts.py @@ -32,3 +32,28 @@ def test_d02_parts(): for dead in (".d02-info", "is-invalid", "font-size: var(--text-caption);\n}\n\n.d02-info"): assert dead not in css assert "data-axis" in ts or "dataset.axis" in ts + + +def test_d03_parts(): + ts = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts") + three = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts") + css = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css") + assert 'createElement("progress")' not in ts and "createProgressBar()" in ts + assert "createCard({" in ts and "onSelect:" in ts + assert 'classList.toggle("is-picked"' in ts # 촬영 장면이 `.d03-plan__mode.is-picked` 로 잡음 + assert "card.dataset.mode = mode" in ts + assert 'createElement("select")' not in three and "createSelectField" in three + assert 'createElement("input")' not in three and "createCheckbox" in three + for dead in (".d03-plan__bar", ".d03-plan__mode", "is-picked"): + assert dead not in css, dead + + +def test_d04_parts(): + ts = _src("D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts") + css = _src("D04_PlanConfirm/D04_PlanConfirm_UI_Style.css") + assert 'el("input"' not in ts + assert ts.count("createInputField(") == 2 and "createCheckbox(" in ts + assert 'createBanner({ tone: "warning"' in ts + assert "input:checked" in ts # [확정] 이 고른 연도를 읽는 길 + assert "border:" not in css.split(".d04-confirm__input")[1].split("}")[0] + assert ".d04-confirm__dirty" not in css