refactor(D03·D04): 진행 막대 · 고르는 카드 · 입력 · 고르기 · 체크 · 띠 알림을 부품으로 (PLAN 553-4)
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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("평면에서 측점을 누르면 그 자리 지반선 · 예상 횡단이 보입니다.");
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
/** 뒤에서 도는 일을 끝날 때까지 지켜봄 — 진행은 지형 칸에. */
|
||||
async function watch(kind: "terrain" | "analyze"): Promise<PlanJob | null> {
|
||||
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<void> {
|
||||
return null;
|
||||
} finally {
|
||||
busy = null;
|
||||
bar.hidden = true;
|
||||
bar.root.hidden = true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
const listId = `d04-companies-${year}`;
|
||||
const options = el("datalist", { attrs: { id: listId } });
|
||||
const found = new Map<string, number>();
|
||||
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<void> {
|
||||
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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user