refactor(E01·E02): 빠진 횡단 띠 · 지반 카드 · 입력 · 고르기 · 안내 글 · 상태 글을 부품으로 (PLAN 553-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
/* 빠진 횡단 띠 — E02 · E03 화면 위(E01_StabRegister_UI_Missing.ts) */
|
||||
.e-missing {
|
||||
margin: 0 0 8px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--color-warning);
|
||||
border-left-width: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.e-missing > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.e-missing > p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
/* =============================================================================
|
||||
* E01_StabRegister_UI_Missing.ts
|
||||
* 빠진 횡단 띠(요구 351 · PLAN 532-5) — E02 · E03 화면 위 「횡단 없는 측점 N 개 — 해석에서 빠짐」.
|
||||
* 목록 = 서버 `missing_stations`(E01 복사본에 횡단 파일이 없는 측점 누가거리) · 누르면 펼침.
|
||||
* 목록 = 서버 `missing_stations`(E01 복사본에 횡단 파일이 없는 측점 누가거리) · 「자세히」로 펼침.
|
||||
* 모양 = 띠 알림 부품(경고).
|
||||
* ========================================================================== */
|
||||
|
||||
import "./E01_StabRegister_UI_Missing.css";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
|
||||
@@ -12,13 +13,13 @@ import { t } from "@ui/ui_template_locale";
|
||||
export function buildMissingBanner(missing: number[] | undefined): HTMLElement {
|
||||
const list = missing ?? [];
|
||||
if (!list.length) return el("div");
|
||||
const box = el("details", {
|
||||
className: "e-missing",
|
||||
attrs: { "data-role": "missing-stations", "data-count": String(list.length) },
|
||||
const box = createBanner({
|
||||
tone: "warning",
|
||||
text: t("E_Missing_Head").replace("{n}", String(list.length)),
|
||||
details: el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }),
|
||||
});
|
||||
box.append(
|
||||
el("summary", { text: t("E_Missing_Head").replace("{n}", String(list.length)) }),
|
||||
el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }),
|
||||
);
|
||||
box.classList.add("e-missing");
|
||||
box.dataset.role = "missing-stations";
|
||||
box.dataset.count = String(list.length);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
|
||||
import { buildCards, type E02View } from "./E02_StabGround_UI_Tables";
|
||||
import { buildCards, hook, type E02View } from "./E02_StabGround_UI_Tables";
|
||||
import {
|
||||
checkGround,
|
||||
type CheckError,
|
||||
@@ -74,7 +75,7 @@ async function request(path: string, init?: RequestInit): Promise<GroundResponse
|
||||
export function renderE02StabGround(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const main = el("div", { className: "e02-page" });
|
||||
const status = el("p", { className: "e02-status" });
|
||||
const status = el("div", { className: "e02-status" });
|
||||
const saveButton = createButton({
|
||||
label: t("Common_Btn_Save"),
|
||||
variant: "filled",
|
||||
@@ -89,10 +90,9 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
const actions = el("div", { className: "ui-sidebar-actions" });
|
||||
actions.append(resetButton, saveButton);
|
||||
const panel = el("div", { className: "ui-sidebar-panel e02-panel" });
|
||||
panel.append(el("p", { className: "e02-hint", text: t("E02_Note") }), status, actions);
|
||||
panel.append(hook(createText(t("E02_Note")), "e02-hint"), status, actions);
|
||||
mountGroupStep(root, ROUTES.E02_STAB_GROUND, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId)
|
||||
return void main.append(el("p", { className: "e02-empty", text: t("E02_NoProject") }));
|
||||
if (!projectId) return void main.append(hook(createText(t("E02_NoProject")), "e02-empty"));
|
||||
|
||||
let view: E02View | null = null;
|
||||
let refreshStations = (): void => {};
|
||||
@@ -104,22 +104,28 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
if (view && !view.editable) lines.push(t("E02_ReadOnly"));
|
||||
if (dirty) lines.push(t("E02_Unsaved"));
|
||||
if (errors.length) lines.push(`${t("E02_Invalid")} ${errors.length}`);
|
||||
status.textContent = lines.join("\n");
|
||||
status.replaceChildren(
|
||||
...(lines.length
|
||||
? [createText(lines.join("\n"), errors.length > 0 ? "error" : "warning")]
|
||||
: []),
|
||||
);
|
||||
status.classList.toggle("is-error", errors.length > 0);
|
||||
};
|
||||
|
||||
/** 틀린 칸 표시 — 칸마다 is-invalid + data-error(마우스 글) */
|
||||
/** 틀린 칸 표시 — 칸마다 is-error(부품 오류 상태) + data-error(마우스 글) */
|
||||
const mark = (): CheckError[] => {
|
||||
if (!view) return [];
|
||||
const errors = checkGround(view.ground, view.defaults.limits);
|
||||
for (const node of main.querySelectorAll<HTMLElement>(".is-invalid")) {
|
||||
node.classList.remove("is-invalid");
|
||||
for (const node of main.querySelectorAll<HTMLElement>("[data-error]")) {
|
||||
node.classList.remove("is-error");
|
||||
node.removeAttribute("aria-invalid");
|
||||
delete node.dataset.error;
|
||||
}
|
||||
for (const error of errors) {
|
||||
const node = main.querySelector<HTMLElement>(`[data-path="${error.path}"]`);
|
||||
if (!node) continue;
|
||||
node.classList.add("is-invalid");
|
||||
node.classList.add("is-error");
|
||||
node.setAttribute("aria-invalid", "true");
|
||||
node.dataset.error = errorText(error, view.defaults);
|
||||
}
|
||||
paintStatus(errors);
|
||||
@@ -242,10 +248,10 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
adopt(body, true);
|
||||
} catch (error) {
|
||||
main.replaceChildren(
|
||||
el("p", {
|
||||
className: "e02-empty",
|
||||
text: error instanceof Error ? error.message : t("Common_Status_Error"),
|
||||
}),
|
||||
hook(
|
||||
createText(error instanceof Error ? error.message : t("Common_Status_Error")),
|
||||
"e02-empty",
|
||||
),
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.e02-page > .ui-banner {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e02-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(560px, 100%), 1fr));
|
||||
@@ -17,33 +21,19 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* 카드 모양은 카드 부품(머리 카드) — 여기는 자리만 */
|
||||
.e02-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.e02-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.e02-card .ui-card__body {
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e02-card__title,
|
||||
.e02-card__sub {
|
||||
margin: 0;
|
||||
margin: var(--spacing-8) 0 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.e02-card__sub {
|
||||
font-size: var(--text-body-sm);
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e02-card__actions {
|
||||
@@ -103,11 +93,6 @@
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.e02-input.is-invalid {
|
||||
border-color: var(--color-danger);
|
||||
box-shadow: 0 0 0 1px var(--color-danger);
|
||||
}
|
||||
|
||||
.e02-rowhead {
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-secondary);
|
||||
@@ -135,22 +120,9 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.e02-hint,
|
||||
.e02-empty {
|
||||
margin: 0;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.e02-status {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
white-space: pre-line;
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.e02-status.is-error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.e02-table .ui-btn {
|
||||
|
||||
@@ -6,8 +6,16 @@
|
||||
* 값 바꿈 = view.change()(캐시 · 검사) · 줄 더함 · 지움 · [기본값] = view.rebuild().
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createCard } from "@ui/ui_template_card";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
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";
|
||||
import {
|
||||
@@ -32,6 +40,11 @@ export interface E02View {
|
||||
}
|
||||
|
||||
const L = (key: string): string => t(key as LocaleKey);
|
||||
/** 부품이 낸 요소에 촬영 장면 · 시험이 잡는 이름만 얹음(CSS 없음) */
|
||||
export const hook = <T extends HTMLElement>(node: T, name: string): T => (
|
||||
node.classList.add(name),
|
||||
node
|
||||
);
|
||||
export const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
||||
const FIELD_LABEL: Record<(typeof MATERIAL_FIELDS)[number], LocaleKey> = {
|
||||
gamma_t: "E02_Col_GammaT",
|
||||
@@ -48,10 +61,8 @@ export function numInput(
|
||||
set: (v: number | null) => void,
|
||||
meta: { src?: string; def?: number | null; nullable?: boolean } = {},
|
||||
): HTMLInputElement {
|
||||
const input = el("input", {
|
||||
className: "ui-input ui-input--sm e02-input e02-input--num",
|
||||
}) as HTMLInputElement;
|
||||
input.type = "number";
|
||||
const input = createInputField({ type: "number", compact: true }).input;
|
||||
input.classList.add("e02-input", "e02-input--num");
|
||||
input.step = "any";
|
||||
input.value = value === null || value === undefined ? "" : String(value);
|
||||
input.dataset.path = path;
|
||||
@@ -75,10 +86,8 @@ export function selectInput(
|
||||
set: (v: string) => void,
|
||||
meta: { src?: string; def?: string } = {},
|
||||
): HTMLSelectElement {
|
||||
const select = el("select", {
|
||||
className: "ui-select ui-select--sm e02-input",
|
||||
}) as HTMLSelectElement;
|
||||
for (const option of options) select.append(new Option(option.text, option.value));
|
||||
const select = createSelectField({ options, compact: true }).select;
|
||||
select.classList.add("e02-input");
|
||||
select.value = value ?? "";
|
||||
select.dataset.path = path;
|
||||
if (meta.src) select.dataset.src = meta.src;
|
||||
@@ -95,10 +104,9 @@ export function card(
|
||||
title: LocaleKey,
|
||||
...body: HTMLElement[]
|
||||
): { section: HTMLElement; head: HTMLElement } {
|
||||
const head = el("div", { className: "e02-card__head" });
|
||||
head.append(el("h3", { className: "e02-card__title", text: t(title) }));
|
||||
const section = el("section", { className: "e02-card ui-sidebar-section" });
|
||||
section.append(head, ...body);
|
||||
const head = el("div", { className: "e02-card__actions" });
|
||||
const section = createCard({ title: t(title), actions: [head], body, pad: 16 });
|
||||
section.classList.add("e02-card", "ui-sidebar-section");
|
||||
return { section, head };
|
||||
}
|
||||
|
||||
@@ -108,9 +116,7 @@ export function headButtons(
|
||||
...buttons: HTMLButtonElement[]
|
||||
): void {
|
||||
if (!view.editable) return;
|
||||
const row = el("div", { className: "e02-card__actions" });
|
||||
row.append(...buttons);
|
||||
head.append(row);
|
||||
head.append(...buttons);
|
||||
}
|
||||
|
||||
export function table(headers: string[], rows: HTMLElement[][]): HTMLTableElement {
|
||||
@@ -141,9 +147,8 @@ function materialsCard(view: E02View): HTMLElement {
|
||||
Object.values(g.label_map).includes(id) || g.zones.some((z) => z.soil === id || z.rock === id);
|
||||
const rows = g.materials.map((m, i) => {
|
||||
const base = `materials.${i}`;
|
||||
const name = el("input", {
|
||||
className: "ui-input ui-input--sm e02-input e02-input--name",
|
||||
}) as HTMLInputElement;
|
||||
const name = createInputField({ compact: true }).input;
|
||||
name.classList.add("e02-input", "e02-input--name");
|
||||
name.value = m.name;
|
||||
name.dataset.path = `${base}.name`;
|
||||
name.disabled = !view.editable;
|
||||
@@ -293,15 +298,13 @@ function zonesCard(view: E02View): { section: HTMLElement; refresh: () => void }
|
||||
],
|
||||
rows,
|
||||
)
|
||||
: el("p", { className: "e02-empty", text: t("E02_Zone_Empty") });
|
||||
: hook(createText(t("E02_Zone_Empty")), "e02-empty");
|
||||
|
||||
const stationsHost = el("div", { className: "e02-stations" });
|
||||
const refresh = (): void => {
|
||||
stationsHost.replaceChildren();
|
||||
if (!g.stations.length)
|
||||
return void stationsHost.append(
|
||||
el("p", { className: "e02-empty", text: t("E02_Stations_Empty") }),
|
||||
);
|
||||
return void stationsHost.append(hook(createText(t("E02_Stations_Empty")), "e02-empty"));
|
||||
const names = new Map(g.materials.map((m) => [m.id, m.name]));
|
||||
const interval = inferStationInterval(g.stations);
|
||||
const fmt = (v: number | null): string => (v === null ? "-" : v.toFixed(2));
|
||||
@@ -332,7 +335,7 @@ function zonesCard(view: E02View): { section: HTMLElement; refresh: () => void }
|
||||
|
||||
const { section, head } = card(
|
||||
"E02_Card_Zones",
|
||||
el("p", { className: "e02-hint", text: t("E02_Zone_Hint") }),
|
||||
hook(createText(t("E02_Zone_Hint")), "e02-hint"),
|
||||
zoneBody,
|
||||
el("h4", { className: "e02-card__sub", text: t("E02_Card_Stations") }),
|
||||
stationsHost,
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { WATER_KEYS, WATER_RULES, WET_MODES, type WetMode } from "./logic/E02_StabGround_Check";
|
||||
import {
|
||||
card,
|
||||
clone,
|
||||
headButtons,
|
||||
hook,
|
||||
numInput,
|
||||
selectInput,
|
||||
table,
|
||||
@@ -40,7 +42,7 @@ function removeButton(view: E02View, onClick: () => void): HTMLButtonElement {
|
||||
/** 사용자 수위선 구간 표 — 줄마다 시작 · 끝 · 점(offset · 깊이) · [점 추가] · [지움] */
|
||||
function linesBody(view: E02View): HTMLElement {
|
||||
const water = view.ground.groundwater;
|
||||
if (!water.lines.length) return el("p", { className: "e02-empty", text: t("E02_Line_Empty") });
|
||||
if (!water.lines.length) return hook(createText(t("E02_Line_Empty")), "e02-empty");
|
||||
const rows = water.lines.map((line, i) => {
|
||||
const base = `groundwater.lines.${i}`;
|
||||
const points = el("div", { className: "e02-points" });
|
||||
@@ -136,11 +138,11 @@ export function waterCard(view: E02View): HTMLElement {
|
||||
),
|
||||
);
|
||||
if (water.wet_mode === "line")
|
||||
body.push(el("p", { className: "e02-hint", text: t("E02_Line_Hint") }), linesBody(view));
|
||||
body.push(hook(createText(t("E02_Line_Hint")), "e02-hint"), linesBody(view));
|
||||
|
||||
const { section, head } = card(
|
||||
"E02_Card_Water",
|
||||
el("p", { className: "e02-hint", text: t("E02_Water_Hint") }),
|
||||
hook(createText(t("E02_Water_Hint")), "e02-hint"),
|
||||
grid,
|
||||
...body,
|
||||
);
|
||||
|
||||
@@ -57,3 +57,23 @@ 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
|
||||
|
||||
|
||||
def test_e01_missing_is_banner_part():
|
||||
ts = _src("E01_StabRegister/E01_StabRegister_UI_Missing.ts")
|
||||
assert "createBanner" in ts and '"details"' not in ts
|
||||
assert 'dataset.role = "missing-stations"' in ts # 시험 · 촬영이 잡음
|
||||
assert not (ROOT / "E01_StabRegister/E01_StabRegister_UI_Missing.css").exists()
|
||||
|
||||
|
||||
def test_e02_parts():
|
||||
tables = _src("E02_StabGround/E02_StabGround_UI_Tables.ts")
|
||||
page = _src("E02_StabGround/E02_StabGround_UI_Page.ts")
|
||||
css = _src("E02_StabGround/E02_StabGround_UI_Style.css")
|
||||
assert 'el("input"' not in tables and 'el("select"' not in tables
|
||||
assert "createInputField(" in tables and "createSelectField(" in tables
|
||||
assert "createCard({ title" in tables
|
||||
assert "is-invalid" not in page + css and "is-error" in page
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user