refactor(B08·B09): 자체 변종 → 부품 상자 — 탭 줄 · 분할 고르기 · 배지 칩 · 도구줄 · 떠 있는 판 · 보조 글 (PLAN 553-3)
- 위 · 보조 탭 줄 = 부품 탭(fillTabs · 밑줄 · 알약) · B09 좌측 스위치 = 분할 고르기 - 연결 칩 · 검사 탭 이름 · 끌기 태그 = 배지 · 이정표 도구 · 머리 줄 · 저장 줄 = 도구줄 · 구간 고침 · 행 고름 창 = 떠 있는 판 - 안내 · 빈 줄 · 상태 글 = 보조 글 · 운반 칸 = 카드 · 인쇄 미리보기 좌측 줄 = B09 dock 한 벌 · 쪽 체크 = 라디오 · 체크 부품 - 시험 흉내 틀 · 문구 맞춤 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
/* M02 에 없는 조합 안내 — 빨간 글 */
|
||||
.b08-sheets__combo > .ui-general-block__empty {
|
||||
.b08-sheets__combo > .ui-text {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
@@ -75,31 +75,10 @@
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 칩 = 부품 배지(알약) · 색은 상태마다 부품 색 · 없어진 원본 = 빨강 점선 */
|
||||
.b08-link__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
max-width: 100%;
|
||||
padding: 2px var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-missing {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 칩 테두리 상태 색(170-3) — 연결됨 · 마스터가 바뀜 · 고침 있음 · 원본 없어짐(빨강 점선) */
|
||||
.b08-link__chip.is-linked {
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-changed {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-edited {
|
||||
border-color: var(--color-info);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-gone {
|
||||
@@ -128,17 +107,3 @@
|
||||
.b08-link__badges:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 배지(163-4) — 고침 있음 · 마스터가 바뀜(경고) */
|
||||
.b08-link__badge {
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b08-link__badge.is-warn {
|
||||
border-color: var(--color-warning);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import "./B08_Quantity_UI_Drawing.css";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { PART_OF, projectItems, withCopyValues } from "@ui/ui_template_item_picker_project";
|
||||
@@ -37,10 +38,7 @@ function L(key: keyof typeof ui_locales): string {
|
||||
}
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const node = document.createElement("p");
|
||||
node.className = "ui-general-block__empty";
|
||||
node.textContent = text;
|
||||
return node;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
export interface SheetsRequest {
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
* 자료 읽기 · 표 짜기 = `B08_Quantity_Earthwork_Tables`(화면 없음 · 인쇄 미리보기도 씀 · 302-3)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { createCard } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { footNote } from "./B08_Quantity_UI_Foot";
|
||||
import {
|
||||
EARTHWORK_SHEETS,
|
||||
@@ -29,10 +31,7 @@ import {
|
||||
import { onOptionsChange } from "./B08_Quantity_UI_Options";
|
||||
|
||||
export function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
/** 표 한 장 — 공용 컨테이너(카드) 안에 공용 표 · 아래 줄 · 맨 아래 안내 · 경고 줄(202-1) */
|
||||
@@ -78,26 +77,18 @@ export async function renderEarthwork(host: HTMLElement, projectId: string): Pro
|
||||
const { table, interval, applyRock } = loaded;
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-earthwork__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.className = "b08-quantity__tabs b08-earthwork__tabs";
|
||||
const view = document.createElement("div");
|
||||
let current: SheetId = "summary";
|
||||
const select = (picked: SheetId): void => {
|
||||
current = picked;
|
||||
tabs.replaceChildren(
|
||||
...SHEETS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: id === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== picked && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === picked));
|
||||
button.dataset.b08EarthworkSheet = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: SHEETS.map(({ id, label }) => ({ key: id, label })),
|
||||
value: picked,
|
||||
kind: "pill",
|
||||
data: "b08EarthworkSheet",
|
||||
onChange: (id) => select(id as SheetId),
|
||||
});
|
||||
if (picked === "sheet") renderSheet(view, table, interval);
|
||||
else if (picked === "summary") renderEarthworkSummary(view, table);
|
||||
else if (picked === "haul") renderHaulDistance(view, table);
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
|
||||
export function footNote(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty b08-quantity__foot";
|
||||
p.textContent = text;
|
||||
const p = createText(text, "muted");
|
||||
p.classList.add("b08-quantity__foot");
|
||||
attachHoverInfo(p, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null));
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
색은 테마 토큰만(밝음 · 어두움 같이) · 테두리는 입력 · 선택 칸(공용 ui-input · ui-select)만
|
||||
· 굵게 + 붉은 계열은 중요 결과(.is-key)만. */
|
||||
|
||||
/* 보조 탭 + [저장] 한 줄 = 부품 도구줄 · 여기는 아래 틈만 */
|
||||
.b08-hydro__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
margin-bottom: var(--spacing-16);
|
||||
}
|
||||
|
||||
@@ -16,10 +13,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b08-hydro__toolbar > .ui-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.b08-hydro__sheet {
|
||||
container-type: inline-size; /* 2열 ↔ 1열 = 창이 아닌 이 칸 폭 기준 */
|
||||
color: var(--color-text-body);
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { clearState, writeState } from "../A00_Common/b_page_state";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import {
|
||||
hydroCalc,
|
||||
@@ -36,10 +39,7 @@ import { buildHydroSheet, type HydroInputs, type HydroResult } from "./B08_Quant
|
||||
export { request };
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
/** `onSave` = 페이지 이동 전 저장 등록(481-2) — 고친 측점 없으면 요청 없이 참. */
|
||||
@@ -68,17 +68,15 @@ export async function renderHydro(
|
||||
const inputsOf = (station: HydroStation): HydroInputs => hydroInputs(sheet, station, drafts);
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-hydro__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.className = "b08-quantity__tabs b08-hydro__tabs";
|
||||
const save = createButton({
|
||||
label: "저장",
|
||||
variant: "filled",
|
||||
onClick: () => void saveAll(),
|
||||
});
|
||||
save.dataset.b08HydroSave = "";
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "b08-hydro__toolbar";
|
||||
toolbar.append(tabs, save);
|
||||
const toolbar = createToolbar({ items: [tabs, save], size: 32 });
|
||||
toolbar.classList.add("b08-hydro__toolbar");
|
||||
const page = document.createElement("div");
|
||||
host.replaceChildren(toolbar, page);
|
||||
let seq = 0;
|
||||
@@ -87,22 +85,17 @@ export async function renderHydro(
|
||||
const open = (index: number): void => {
|
||||
openIndex = index;
|
||||
const station = sheet.stations[index];
|
||||
tabs.replaceChildren(
|
||||
...sheet.stations.map((s, i) => {
|
||||
fillTabs(tabs, {
|
||||
tabs: sheet.stations.map((s, i) => {
|
||||
const label = stationLabel(s.chainage_m, interval);
|
||||
const button = createButton({
|
||||
label: s.kind ? `${label} ${s.kind}` : label,
|
||||
variant: i === index ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => i !== index && open(i),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(i === index));
|
||||
button.dataset.b08HydroStation = String(s.chainage_m);
|
||||
button.title = s.kind || s.facility;
|
||||
return button;
|
||||
return { key: String(i), label: s.kind ? `${label} ${s.kind}` : label };
|
||||
}),
|
||||
);
|
||||
value: String(index),
|
||||
kind: "pill",
|
||||
data: "b08HydroStation",
|
||||
title: (key) => sheet.stations[Number(key)].kind || sheet.stations[Number(key)].facility,
|
||||
onChange: (key) => open(Number(key)),
|
||||
});
|
||||
const draw = (inputs: HydroInputs): void => {
|
||||
const view = buildHydroSheet(
|
||||
stationLabel(station.chainage_m, interval),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import {
|
||||
projectItems,
|
||||
structureFolder,
|
||||
@@ -102,6 +103,22 @@ async function update(projectId: string, info: ProjectItemInfo): Promise<boolean
|
||||
}
|
||||
}
|
||||
|
||||
type ChipState = "is-linked" | "is-missing" | "is-changed" | "is-edited" | "is-gone";
|
||||
const CHIP_TONE: Record<ChipState, BadgeTone> = {
|
||||
"is-linked": "success",
|
||||
"is-missing": "danger",
|
||||
"is-changed": "warning",
|
||||
"is-edited": "info",
|
||||
"is-gone": "danger",
|
||||
};
|
||||
|
||||
/** 칩 = 부품 배지(알약 20) 꼴 · 색 = 상태(연결 초록 · 미연결 빨강 · 바뀜 노랑 · 고침 파랑 · 없어짐 빨강 점선) */
|
||||
function toneChip(chip: HTMLElement, state: ChipState): void {
|
||||
const look = createBadge("", CHIP_TONE[state], { size: 20 });
|
||||
chip.className = `${look.className} b08-link__chip ${state}`;
|
||||
chip.dataset.size = look.dataset.size ?? "20";
|
||||
}
|
||||
|
||||
/** 칩 배지 · 테두리 색 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */
|
||||
async function paintCopyState(
|
||||
projectId: string,
|
||||
@@ -116,12 +133,8 @@ async function paintCopyState(
|
||||
.then((rows) => rows.find((row) => row.폴더 === folder) ?? null)
|
||||
.catch(() => null);
|
||||
if (!info) return null;
|
||||
const badge = (key: keyof typeof ui_locales, warn: boolean): HTMLElement => {
|
||||
const node = document.createElement("span");
|
||||
node.className = warn ? "b08-link__badge is-warn" : "b08-link__badge";
|
||||
node.textContent = L(key);
|
||||
return node;
|
||||
};
|
||||
const badge = (key: keyof typeof ui_locales, warn: boolean): HTMLElement =>
|
||||
createBadge(L(key), warn ? "warning" : "neutral", { size: 18 });
|
||||
if (info.고침) badges.append(badge("B08_Quantity_Link_Edited", false));
|
||||
if (info.원본없음) badges.append(badge("B08_Quantity_Link_Gone", true));
|
||||
else if (info.마스터바뀜) badges.append(badge("B08_Quantity_Link_Changed", true));
|
||||
@@ -133,7 +146,7 @@ async function paintCopyState(
|
||||
: info.고침
|
||||
? "is-edited"
|
||||
: null;
|
||||
if (tone) chip.classList.replace("is-linked", tone);
|
||||
if (tone) toneChip(chip, tone);
|
||||
button.disabled = !info.마스터바뀜 || !!info.원본없음;
|
||||
button.title = button.disabled
|
||||
? L("B08_Quantity_Link_UpdateSame")
|
||||
@@ -173,7 +186,7 @@ export function linkSection(
|
||||
line.append(head);
|
||||
for (const slot of facility.slots) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing";
|
||||
toneChip(chip, slot.linked ? "is-linked" : "is-missing");
|
||||
chip.dataset.b08LinkSlot = slot.copy;
|
||||
const name = document.createElement("span");
|
||||
name.className = "b08-link__name";
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import {
|
||||
blankUnitRows,
|
||||
@@ -53,10 +55,7 @@ const HINTS: Record<MaterialSheetId, string> = {
|
||||
const HIDDEN = "값 없는 줄은 숨김";
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
/** 글 → 값 · 빈칸 = null(일부러 비움) · 수가 아니면 undefined(무시) */
|
||||
@@ -138,8 +137,8 @@ export async function renderMaterialSheet(
|
||||
|
||||
const save = createButton({ label: "저장", variant: "filled", onClick: () => void saveAll() });
|
||||
save.dataset.b08MaterialSave = "";
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "b08-material__toolbar";
|
||||
const toolbar = createToolbar({ items: ["spacer", save], size: 32 });
|
||||
toolbar.classList.add("b08-material__toolbar");
|
||||
const notes = document.createElement("div");
|
||||
notes.append(footNote(sheet === "concrete" ? HINTS[sheet] : `${HINTS[sheet]} · ${HIDDEN}`));
|
||||
if (sheet !== "quantity") {
|
||||
@@ -158,7 +157,6 @@ export async function renderMaterialSheet(
|
||||
const concreteNote = footNote(""); // 콘크리트 전용 빈 까닭(163-5) — 조작마다 다시
|
||||
concreteNote.dataset.b08MaterialConcreteWhy = "";
|
||||
if (sheet === "concrete") notes.append(concreteNote);
|
||||
toolbar.append(save);
|
||||
|
||||
let built = materialTable(sheet, sheets, overrides);
|
||||
const table = createDataTable({
|
||||
|
||||
@@ -41,12 +41,6 @@
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.b08-milestone__bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-milestone__image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
@@ -61,8 +55,6 @@
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.b08-milestone__diagram {
|
||||
@@ -157,14 +149,13 @@
|
||||
.b08-milestone__leg-edit {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.b08-milestone__leg-edit > .ui-float-panel__body {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.b08-milestone__leg-edit .ui-input {
|
||||
@@ -251,14 +242,7 @@
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
/* 157 경로 탭 머리 줄 — 거래처 이름 · 주소(왼) + [그림 변경] · [초기화](오른) · 글자 = 메인 탭 크기 */
|
||||
.b08-milestone__route-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
/* 157 경로 탭 머리 줄 = 부품 도구줄(이름 · 주소 왼 + 단추 오른) */
|
||||
.b08-milestone__route-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
import "./B08_Quantity_UI_Milestone.css";
|
||||
import { createDataTable, type DataColumn } from "@ui/ui_template_data_table";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import type { Row } from "../B09_Estimation/B09_Estimation_Calc_Base";
|
||||
import { openHaulModal } from "../B09_Estimation/B09_Estimation_UI_Haul_Modal";
|
||||
import { openPriceLink } from "../B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link";
|
||||
@@ -146,10 +148,11 @@ function tableView(projectId: string): MilestoneView {
|
||||
};
|
||||
let mode = "false:false";
|
||||
let table = makeTable(milestoneColumns(false, false));
|
||||
const none = el("p", {
|
||||
className: "ui-general-block__empty",
|
||||
text: "경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.",
|
||||
});
|
||||
const none = createText(
|
||||
"경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.",
|
||||
"muted",
|
||||
14,
|
||||
);
|
||||
const refresh = (): void => {
|
||||
const data = liveMilestone(projectId);
|
||||
const { options, rows, mode: next } = milestoneTable(data, siteInfo()?.mid_default_m ?? null);
|
||||
@@ -179,8 +182,7 @@ function tableView(projectId: string): MilestoneView {
|
||||
|
||||
export function renderMilestone(host: HTMLElement, projectId: string): void {
|
||||
const tabs = el("div", {
|
||||
className: "b08-quantity__tabs ui-subtabs b08-structure__tabs",
|
||||
attrs: { role: "tablist" },
|
||||
className: "b08-quantity__tabs b08-structure__tabs",
|
||||
});
|
||||
const view = el("div");
|
||||
let current = "table";
|
||||
@@ -188,22 +190,19 @@ export function renderMilestone(host: HTMLElement, projectId: string): void {
|
||||
|
||||
const drawTabs = (): void => {
|
||||
const routes = liveMilestone(projectId).routes;
|
||||
const tab = (id: string, label: string): HTMLButtonElement => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: id === current ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== current && open(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === current));
|
||||
button.dataset.b08MilestoneTab = id;
|
||||
return button;
|
||||
};
|
||||
tabs.replaceChildren(
|
||||
tab("table", "거리이정표"),
|
||||
...routes.map((r, i) => tab(r.id, r.name.trim() || r.rows[0]?.item || `거래처 ${i + 1}`)),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: [
|
||||
{ key: "table", label: "거리이정표" },
|
||||
...routes.map((r, i) => ({
|
||||
key: r.id,
|
||||
label: r.name.trim() || r.rows[0]?.item || `거래처 ${i + 1}`,
|
||||
})),
|
||||
],
|
||||
value: current,
|
||||
kind: "pill",
|
||||
data: "b08MilestoneTab",
|
||||
onChange: open,
|
||||
});
|
||||
};
|
||||
|
||||
function open(id: string): void {
|
||||
@@ -213,10 +212,7 @@ export function renderMilestone(host: HTMLElement, projectId: string): void {
|
||||
view.replaceChildren(body.root);
|
||||
}
|
||||
|
||||
host.replaceChildren(
|
||||
tabs,
|
||||
el("p", { className: "ui-general-block__empty", text: "거리이정표를 불러오는 중…" }),
|
||||
);
|
||||
host.replaceChildren(tabs, createText("거리이정표를 불러오는 중…", "muted", 14));
|
||||
loadMilestone(projectId).then(
|
||||
() => {
|
||||
host.replaceChildren(tabs, view);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import {
|
||||
midKm,
|
||||
@@ -94,7 +96,10 @@ function editLegAt(
|
||||
km.placeholder = fmtKm(leg.auto_km); // 빈칸 = 자동 값
|
||||
km.title = `자동 값 ${fmtKm(leg.auto_km)} km`;
|
||||
}
|
||||
const box = el("div", { className: "b08-milestone__leg-edit", children: [road.root, km] });
|
||||
const pop = createFloatPanel({ popover: true });
|
||||
pop.body.append(road.root, km);
|
||||
const box = pop.root;
|
||||
box.classList.add("b08-milestone__leg-edit");
|
||||
box.dataset.b08MilestoneLegEdit = `${from}:${i}`;
|
||||
const at = anchor.getBoundingClientRect(); // 팬 · 줌 뒤 글 자리 그대로
|
||||
box.style.left = `${at.left}px`;
|
||||
@@ -300,7 +305,8 @@ export function createDiagram(
|
||||
onClick: () => openPlaceModal(projectId, { add: true }, addRoute),
|
||||
});
|
||||
addElem.dataset.b08MilestoneAdd = "open";
|
||||
const tools = el("div", { className: "b08-milestone__tools", children: [arrange, addElem] });
|
||||
const tools = createToolbar({ items: [arrange, addElem], size: 32, glass: true });
|
||||
tools.classList.add("b08-milestone__tools");
|
||||
|
||||
/** 장소 그림(254-3 · 256-1) blob 주소 — 상자마다 키(이름#시각)가 바뀔 때만 다시 받음 */
|
||||
const picUrls = new Map<string, { key: string; url: Promise<string> }>();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
ROADS,
|
||||
roadOf,
|
||||
@@ -113,7 +114,7 @@ export function legsEditor(
|
||||
};
|
||||
}
|
||||
|
||||
const hint = (text: string): HTMLElement => el("p", { className: "b08-options__hint", text });
|
||||
const hint = (text: string): HTMLElement => createText(text, "muted");
|
||||
|
||||
/** 시점 → 중점 — 도로 구분 · 거리 늘 손 입력(빈칸 = 노선 ÷ 2) */
|
||||
function openMidLeg(projectId: string): void {
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
type MapContextMenuItem,
|
||||
} from "@ui/ui_template_context_menu";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import type { MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainOf, ownerOf, reachesSite } from "./B08_Quantity_Milestone_Layout";
|
||||
import { DEST_TEXT } from "./B08_Quantity_Milestone_Table";
|
||||
@@ -76,6 +78,13 @@ export interface MilestoneView {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/** 머리 줄 — 이름 · 주소(왼) + 단추(오른) = 부품 도구줄 */
|
||||
function routeHead(title: HTMLElement, buttons: HTMLElement[]): HTMLElement {
|
||||
const bar = createToolbar({ items: [title, "spacer", ...buttons], size: 32 });
|
||||
bar.classList.add("b08-milestone__route-head");
|
||||
return bar;
|
||||
}
|
||||
|
||||
export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
// 157 머리 줄 = 거래처 이름 · 주소(왼) + [그림 변경] · [초기화](오른) · 글자 = 메인 탭 크기
|
||||
const name = el("strong", { className: "b08-milestone__route-name" });
|
||||
@@ -155,11 +164,11 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
return [routeId, ...chain].map((id) => segmentPath(projectId, id)).filter((p) => p.length >= 2);
|
||||
};
|
||||
// 255-2 길찾기 실패 = 안내 한 줄(까닭 줄 = 「 — 」)
|
||||
const status = el("p", { className: "b08-options__hint" });
|
||||
const status = createText("", "muted");
|
||||
status.dataset.b08MilestoneRouteStatus = "";
|
||||
status.hidden = true;
|
||||
// 373 안 이은 기준점에 이음 = 시점까지 안 닿음 안내(이은 거래처만)
|
||||
const open = el("p", { className: "b08-options__hint" });
|
||||
const open = createText("", "muted");
|
||||
open.dataset.b08MilestoneRouteOpen = "";
|
||||
open.hidden = true;
|
||||
/** 길 선 여럿 다시 길찾기(그림 캡처 없음 · 캡처는 [저장] 때) → 선 다시 · 실패 안내 */
|
||||
@@ -207,13 +216,12 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
className: "b08-milestone__route ui-card",
|
||||
attrs: { "data-b08-milestone-route": routeId },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "b08-milestone__route-head",
|
||||
children: [
|
||||
el("div", { className: "b08-milestone__route-title", children: [name, address] }),
|
||||
el("div", { className: "b08-milestone__bar", children: [place, change, reset, file] }),
|
||||
],
|
||||
}),
|
||||
routeHead(el("div", { className: "b08-milestone__route-title", children: [name, address] }), [
|
||||
place,
|
||||
change,
|
||||
reset,
|
||||
file,
|
||||
]),
|
||||
status,
|
||||
open,
|
||||
img,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { clampPct } from "./B08_Quantity_Options_Calc";
|
||||
@@ -97,10 +98,7 @@ function card(id: string, title: keyof typeof ui_locales, ...body: HTMLElement[]
|
||||
}
|
||||
|
||||
function hint(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "b08-options__hint";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted");
|
||||
}
|
||||
|
||||
/** 좌측 패널 — 맨 끝 `.ui-sidebar-actions` 줄([저장] · [?] 자리). `onSaved` = 열린 탭 다시 그림.
|
||||
|
||||
@@ -7,16 +7,22 @@
|
||||
padding: var(--spacing-16) var(--spacing-24); /* M02 본문(.m02-main)과 같은 값 */
|
||||
}
|
||||
|
||||
.b08-quantity .ui-general-block__empty {
|
||||
color: var(--color-text-secondary); /* muted 는 밝은 테마에서 대비 3:1 — 4.5:1 로 */
|
||||
/* 부품 보조 글은 display:flex 라 hidden 을 이김 — 숨김 켜고 끄는 글이 많아 한 번에 */
|
||||
.b08-quantity .ui-text[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 탭 줄 모양 = 부품 탭(밑줄 · 보조 알약) · 여기는 줄바꿈 · 아래 틈만 */
|
||||
.b08-quantity__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-quantity > .b08-quantity__tabs {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-quantity__tabs.ui-subtabs {
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* ☰ 들여쓰기(7-2) */
|
||||
@@ -31,6 +37,7 @@ html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child {
|
||||
|
||||
/* 236-1 표 아래 안내 · 경고 줄 — 늘 한 줄(줄임표 · 잘리면 공용 호버로 전문) */
|
||||
.b08-quantity__foot {
|
||||
display: block; /* 부품 보조 글은 flex — 줄임표는 블록에서 */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -39,19 +46,11 @@ html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child {
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 자재 집계표 셋(121) — 안내 글 · [저장] 한 줄 */
|
||||
/* 자재 집계표 셋(121) — [저장] 한 줄 = 부품 도구줄 · 아래 틈만 */
|
||||
.b08-material__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-16);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-material__toolbar > .ui-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 구조물 · 자재 집계표 머리 — 좁은 열(44~100)에서 말줄임 대신 줄바꿈(120-2 · 131 · 공용 표 CSS 는 그대로) */
|
||||
[data-b08-summary="table"] thead th,
|
||||
[data-b08-material] thead th {
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./B08_Quantity_UI_Page.css";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { moveStage, WORKFLOW_STEP_ROUTES, type StageStep } from "../A00_Common/b_workflow_nav";
|
||||
import { renderStructureSummary } from "./B08_Quantity_UI_Summary";
|
||||
import { renderMaterialSheet } from "./B08_Quantity_UI_Material";
|
||||
@@ -53,33 +54,22 @@ type SheetId = (typeof SHEETS)[number]["id"];
|
||||
type TabSave = (host: HTMLElement, save: () => Promise<boolean>) => void;
|
||||
|
||||
function pendingNote(): HTMLElement {
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "ui-general-block__empty";
|
||||
pending.textContent = L("B08_Quantity_Tab_Pending");
|
||||
return pending;
|
||||
return createText(L("B08_Quantity_Tab_Pending"), "muted", 14);
|
||||
}
|
||||
|
||||
/** 자재 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */
|
||||
function renderMaterials(host: HTMLElement, projectId: string | null, onSave: TabSave): void {
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-structure__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.className = "b08-quantity__tabs b08-structure__tabs";
|
||||
const view = document.createElement("div");
|
||||
const select = (picked: SheetId): void => {
|
||||
tabs.replaceChildren(
|
||||
...SHEETS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label: L(label),
|
||||
variant: id === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== picked && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === picked));
|
||||
button.dataset.b08StructureSheet = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: SHEETS.map(({ id, label }) => ({ key: id, label: L(label) })),
|
||||
value: picked,
|
||||
kind: "pill",
|
||||
data: "b08StructureSheet",
|
||||
onChange: (id) => select(id as SheetId),
|
||||
});
|
||||
const body = document.createElement("div");
|
||||
view.replaceChildren(body);
|
||||
if (!projectId) body.append(pendingNote());
|
||||
@@ -95,13 +85,21 @@ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null, onSa
|
||||
host.replaceChildren(view);
|
||||
if (tab === "material") return renderMaterials(view, projectId, onSave);
|
||||
if (tab === "summary" && projectId) {
|
||||
// 보조 탭 줄 자리(170-4) — 보이지 않는 sm 단추 한 줄 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐
|
||||
// 보조 탭 줄 자리(170-4) — 보이지 않는 보조 탭 한 칸 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐
|
||||
const blank = document.createElement("div");
|
||||
blank.className = "b08-quantity__tabs ui-subtabs b08-quantity__tabs--blank";
|
||||
blank.className = "b08-quantity__tabs b08-quantity__tabs--blank";
|
||||
fillTabs(blank, {
|
||||
tabs: [{ key: SHEETS[0].id, label: L(SHEETS[0].label) }],
|
||||
value: SHEETS[0].id,
|
||||
kind: "pill",
|
||||
data: "b08StructureSheet",
|
||||
onChange: () => undefined,
|
||||
}).forEach((button) => {
|
||||
button.tabIndex = -1;
|
||||
delete button.dataset.b08StructureSheet;
|
||||
});
|
||||
blank.setAttribute("aria-hidden", "true");
|
||||
const ghost = createButton({ label: L(SHEETS[0].label), variant: "ghost", size: "sm" });
|
||||
ghost.tabIndex = -1;
|
||||
blank.append(ghost);
|
||||
blank.setAttribute("role", "presentation");
|
||||
const body = document.createElement("div");
|
||||
view.append(blank, body);
|
||||
return void renderStructureSummary(body, projectId);
|
||||
@@ -118,7 +116,6 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
|
||||
const tabBar = document.createElement("div");
|
||||
tabBar.className = "b08-quantity__tabs";
|
||||
tabBar.setAttribute("role", "tablist");
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b08-quantity__panel";
|
||||
panel.setAttribute("role", "tabpanel");
|
||||
@@ -128,19 +125,14 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
const onTabSave: TabSave = (host, save) => (tabSave = { host, save });
|
||||
const select = (tab: TabId): void => {
|
||||
current = tab;
|
||||
tabBar.replaceChildren(
|
||||
...TABS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label: L(label),
|
||||
variant: id === tab ? "filled" : "ghost",
|
||||
onClick: () => id !== tab && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === tab));
|
||||
button.dataset.b08Tab = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabBar, {
|
||||
tabs: TABS.map(({ id, label }) => ({ key: id, label: L(label) })),
|
||||
value: tab,
|
||||
kind: "line",
|
||||
size: 32,
|
||||
data: "b08Tab",
|
||||
onChange: (id) => select(id as TabId),
|
||||
});
|
||||
renderTab(panel, tab, projectId, onTabSave);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createCard, showToast } from "@ui/ui_template_elements";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
@@ -39,10 +40,7 @@ function L(key: keyof typeof ui_locales): string {
|
||||
}
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const node = document.createElement("p");
|
||||
node.className = "ui-general-block__empty";
|
||||
node.textContent = text;
|
||||
return node;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
function buildTable(
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
@@ -60,9 +62,8 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
|
||||
refresh.classList.toggle("is-pending", Boolean(next.stale));
|
||||
const made = checkItems(sheets, next);
|
||||
items = made.items;
|
||||
const head = document.createElement("p");
|
||||
head.className = "b09-check__head";
|
||||
head.textContent = `연결 없음 ${items.length}곳`;
|
||||
const head = createText(`연결 없음 ${items.length}곳`, "muted");
|
||||
head.classList.add("b09-check__head");
|
||||
const rows = document.createElement("div");
|
||||
rows.className = "ui-sidebar-list";
|
||||
rows.append(
|
||||
@@ -73,18 +74,16 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
|
||||
row.dataset.i = String(i);
|
||||
const name = document.createElement("span");
|
||||
name.textContent = item.name;
|
||||
const tab = document.createElement("span");
|
||||
tab.className = "b09-check__tab";
|
||||
tab.textContent = item.tab;
|
||||
const tab = createBadge(item.tab, "neutral", { size: 18 });
|
||||
tab.classList.add("b09-check__tab");
|
||||
row.append(name, tab);
|
||||
row.addEventListener("click", () => goto(item.link));
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
const notes = made.notes.map((text) => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "b09-check__note";
|
||||
p.textContent = text;
|
||||
const p = createText(text, "muted");
|
||||
p.classList.add("b09-check__note");
|
||||
return p;
|
||||
});
|
||||
list.replaceChildren(head, ...(items.length ? [rows] : []), ...notes);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* 스위치 값 = pref global(계정 공용 · [저장] 필요 없음) · 바닥 고정 = 공용 `.ui-sidebar-actions` 한 벌
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { DRAFT } from "./B09_Estimation_UI_Tabs";
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ function setLinkMode(mode: LinkMode): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 단추 둘 스위치 — 켜진 쪽 filled · aria-pressed */
|
||||
/** 단추 둘 스위치 — 부품 「분할 고르기」(32) · 고른 쪽 aria-checked */
|
||||
function switchRow<T extends string>(
|
||||
name: string,
|
||||
values: T[],
|
||||
@@ -48,32 +48,14 @@ function switchRow<T extends string>(
|
||||
current: () => T,
|
||||
pick: (value: T) => void,
|
||||
): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ui-btn-group b09-dock__row";
|
||||
row.dataset.b09Switch = name;
|
||||
const buttons = values.map((value) => {
|
||||
const button = createButton({
|
||||
label: label(value),
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
if (value === current()) return;
|
||||
pick(value);
|
||||
sync();
|
||||
},
|
||||
});
|
||||
button.dataset.value = value;
|
||||
return button;
|
||||
const segment = createSegment({
|
||||
options: values.map((value) => ({ value, label: label(value) })),
|
||||
value: current(),
|
||||
size: 32,
|
||||
onChange: (value) => pick(value as T),
|
||||
});
|
||||
const sync = (): void =>
|
||||
buttons.forEach((button, k) => {
|
||||
const on = values[k] === current();
|
||||
button.classList.toggle("ui-btn--filled", on);
|
||||
button.classList.toggle("ui-btn--ghost", !on);
|
||||
button.setAttribute("aria-pressed", String(on));
|
||||
});
|
||||
sync();
|
||||
row.append(...buttons);
|
||||
return row;
|
||||
segment.root.dataset.b09Switch = name;
|
||||
return segment.root;
|
||||
}
|
||||
|
||||
/** 하단 고정 줄 — onView = 보기 바뀜(묶음 탭이면 다시 그림) */
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
.b09-haulstep {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface-raised);
|
||||
.b09-haulstep.ui-card {
|
||||
padding: 0; /* 부품 카드 · 머리 · 바닥 줄이 가장자리까지 */
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ const group = (label: string, rows: StepRow[], ats?: number[]): HTMLElement[] =>
|
||||
/** 칸 하나 — 제목 줄(제목 · 곁글) · 몸 · 바닥(굵게) */
|
||||
function box(title: string, sub: string, body: HTMLElement[], foot: HTMLElement[]): HTMLElement {
|
||||
return el("section", {
|
||||
className: "b09-haulstep",
|
||||
className: "b09-haulstep ui-card",
|
||||
children: [
|
||||
el("div", {
|
||||
className: "b09-haulstep__head",
|
||||
|
||||
@@ -8,22 +8,17 @@
|
||||
padding: var(--spacing-16) var(--spacing-24); /* B08 본문과 같은 값 */
|
||||
}
|
||||
|
||||
.b09-estimation .ui-general-block__empty,
|
||||
.b09-estimation__hint {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b09-estimation__hint {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 탭 줄 모양 = 부품 탭(밑줄 · 보조 알약) · 여기는 줄바꿈 · 아래 틈만(B08 과 같은 값) */
|
||||
.b09-estimation__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8); /* B08 위 · 보조 탭 줄과 같은 값(181-1) */
|
||||
}
|
||||
|
||||
.b09-estimation > .b09-estimation__tabs:first-child {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b09-estimation__tabs.ui-subtabs {
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* ☰ 들여쓰기(7-2) */
|
||||
@@ -59,12 +54,13 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
}
|
||||
|
||||
/* 202-1 안내 줄 = 표 · 카드 판 아래 · 203 줄 수 변수만큼 보임 · 넘치면 영역 안 스크롤 */
|
||||
.b09-estimation .ui-data-table + .ui-general-block__empty,
|
||||
.b09-estimation .ui-group-cards + .ui-general-block__empty {
|
||||
.b09-estimation .ui-data-table + .ui-text,
|
||||
.b09-estimation .ui-group-cards + .ui-text {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
}
|
||||
|
||||
.b09-estimation__line-hint {
|
||||
display: block; /* 부품 보조 글은 flex — 줄 수 · 스크롤은 블록에서 */
|
||||
min-width: 0;
|
||||
max-height: calc(var(--b09-hint-lines) * 1lh);
|
||||
overflow-y: auto;
|
||||
@@ -192,15 +188,11 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
/* 연결 점검 목록 — 이름 · 탭(작게) · 알림 글 */
|
||||
.b09-check__head,
|
||||
.b09-check__note {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b09-check__tab {
|
||||
flex: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 줄 끌기(204-1) — 놓을 자리 · 레벨 미리 보기 선 · 못 놓는 자리 = 빨간 점선 */
|
||||
@@ -232,15 +224,11 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
|
||||
.b09-estimation__drop-tag {
|
||||
.b09-estimation__drop > .b09-estimation__drop-tag.ui-badge {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
padding: 1px var(--spacing-8);
|
||||
border: 1px solid;
|
||||
border-radius: var(--radius-pills);
|
||||
border: 1px solid; /* 놓을 자리 색(--drop-c)을 따름 */
|
||||
background: var(--color-surface-raised);
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b09-estimation [data-b09-table="bill"] tbody tr.is-drop-into td {
|
||||
@@ -266,44 +254,43 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
.b09-dock .b09-dock__row {
|
||||
display: flex;
|
||||
gap: var(--btn-group-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 보기 · 행 누름 스위치 = 부품 분할 고르기 · 줄 폭을 나눠 가짐 */
|
||||
.b09-dock > .ui-segment {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.b09-dock > .ui-segment > .ui-segment__item {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 줄 폭을 나눠 가짐 · 가로 넘침 없음(min-width 0 = 공용 단추 줄의 auto 보다 앞섬 · 글 줄임표) — 인쇄 미리보기 좌측 줄도 같은 규칙 */
|
||||
.b09-dock .b09-dock__row > .ui-btn:not(.ui-help-btn, .ui-step-nav-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: auto;
|
||||
min-width: 0;
|
||||
padding-inline: var(--spacing-4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임 · 창 안 b09 표 꼴 그대로 · 빨강 테두리) */
|
||||
.b09-dock .b09-dock__row > .ui-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임) = 부품 떠 있는 판 · 여기는 자리 · 크기만 */
|
||||
.b09-estimation.b09-popup {
|
||||
position: fixed;
|
||||
z-index: var(--z-modal);
|
||||
gap: var(--spacing-4);
|
||||
width: max-content;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.b09-popup__head {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b09-popup__title {
|
||||
margin-right: auto;
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.b09-popup .ui-data-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { createDataTable, type DataTableOptions } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { placeHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { tableOptionsOf } from "./B09_Estimation_UI_Builders";
|
||||
import { rowIndex, tabName, type Link, type TabId, type TabTable } from "./B09_Estimation_UI_Tabs";
|
||||
@@ -135,35 +136,28 @@ export function openRowPopup(o: PopupOpen): void {
|
||||
if (!spec || !range) return o.go(o.link); // 보일 줄 없음 = 옮김
|
||||
listen();
|
||||
const level = stack.length;
|
||||
const box = document.createElement("div");
|
||||
box.className = "b09-estimation b09-popup ui-popup";
|
||||
// 떠 있는 판(부품) — 머리 = 제목 + [탭 이동] + 닫기 ✕ · 몸 = 표 조각
|
||||
const panel = createFloatPanel({ title: tabName(tab), onClose: () => closePopups(level) });
|
||||
const box = panel.root;
|
||||
box.classList.add("b09-estimation", "b09-popup", "ui-popup");
|
||||
box.setAttribute("role", "dialog");
|
||||
box.setAttribute("aria-label", tabName(tab));
|
||||
box.dataset.b09Popup = tab;
|
||||
const title = document.createElement("span");
|
||||
title.className = "b09-popup__title";
|
||||
title.textContent = tabName(tab);
|
||||
const head = box.querySelector<HTMLElement>(".ui-float-panel__head") as HTMLElement;
|
||||
head.classList.add("b09-popup__head");
|
||||
const closeButton = head.querySelector<HTMLElement>(".ui-icon-btn") as HTMLElement;
|
||||
closeButton.dataset.b09PopupClose = "";
|
||||
const goButton = createButton({
|
||||
label: "탭 이동",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
closePopups();
|
||||
o.go(o.link);
|
||||
},
|
||||
});
|
||||
goButton.dataset.b09PopupGo = "";
|
||||
const closeButton = createButton({
|
||||
label: "×",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => closePopups(level),
|
||||
});
|
||||
closeButton.dataset.b09PopupClose = "";
|
||||
closeButton.setAttribute("aria-label", "닫기");
|
||||
const head = document.createElement("div");
|
||||
head.className = "b09-popup__head";
|
||||
head.append(title, goButton, closeButton);
|
||||
closeButton.before(goButton);
|
||||
const from = range[0];
|
||||
const table = createDataTable({
|
||||
...popupOptions(spec, tab, range),
|
||||
@@ -181,7 +175,7 @@ export function openRowPopup(o: PopupOpen): void {
|
||||
},
|
||||
});
|
||||
table.root.dataset.b09Table = tab;
|
||||
box.append(head, table.root);
|
||||
panel.body.append(table.root);
|
||||
document.body.append(box);
|
||||
stack.push(box);
|
||||
const view = { width: window.innerWidth, height: window.innerHeight };
|
||||
|
||||
@@ -14,6 +14,7 @@ import "@ui/ui_template_general_layout.css";
|
||||
import "./B09_Estimation_UI_Page.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
createButton,
|
||||
hideLoadingOverlay,
|
||||
@@ -79,10 +80,7 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
}
|
||||
|
||||
function note(textValue: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = textValue;
|
||||
return p;
|
||||
return createText(textValue, "muted", 14);
|
||||
}
|
||||
|
||||
/** 산근(145-1) = 서버 단독 — 받은 그대로 두는 칸(+ sg_hand = 손 줄 로직 → 산근 키 · 166-2) */
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_TabBar.ts
|
||||
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(md) · 아래 탭(sm) · B08 수량산출 페이지 꼴.
|
||||
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(밑줄 · 32) · 아래 탭(보조 알약) · B08 수량산출 페이지 꼴(부품 「탭」).
|
||||
*
|
||||
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 sm 단추 한 줄(높이만 채움)
|
||||
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 보조 탭 한 칸(높이만 채움)
|
||||
* 위 탭 누름 = 그 묶음에서 마지막으로 본 아래 탭(이 화면 안 Map · 저장 안 함) · 없으면 첫 아래 탭
|
||||
* 탭 이동은 `draw(탭)` — 이동이 묶음을 넘어도 위 탭이 따라 켜짐
|
||||
* 보기 단추 · 가로 열 수(202-1) = 아래 줄 오른쪽 맞춤 자리(`setTools`) · 빈 줄에서도 보임
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { groupOf, tabName, TAB_GROUPS, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
export interface TabBars {
|
||||
@@ -20,58 +21,49 @@ export interface TabBars {
|
||||
setTools: (tools: HTMLElement[]) => void;
|
||||
}
|
||||
|
||||
function tabButton(label: string, on: boolean, size: "sm" | undefined, pick: () => void) {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: on ? "filled" : "ghost",
|
||||
...(size ? { size } : {}),
|
||||
onClick: () => !on && pick(),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(on));
|
||||
return button;
|
||||
}
|
||||
|
||||
export function createTabBars(onPick: (tab: TabId) => void): TabBars {
|
||||
const top = document.createElement("div");
|
||||
top.className = "b09-estimation__tabs";
|
||||
top.setAttribute("role", "tablist");
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "b09-estimation__tabs ui-subtabs";
|
||||
const tools = document.createElement("div");
|
||||
tools.className = "b09-estimation__tools ui-subtabs__tools";
|
||||
sub.className = "b09-estimation__tabs";
|
||||
const tools = createToolbar({ items: [], size: 24 });
|
||||
tools.classList.add("b09-estimation__tools", "ui-subtabs__tools");
|
||||
const lastSeen = new Map<TabId, TabId>();
|
||||
|
||||
const draw = (tab: TabId): void => {
|
||||
const [, picked] = groupOf(tab);
|
||||
lastSeen.set(picked[0], tab);
|
||||
top.replaceChildren(
|
||||
...TAB_GROUPS.map(([label, tabs]) => {
|
||||
const button = tabButton(label, tabs === picked, undefined, () =>
|
||||
onPick(lastSeen.get(tabs[0]) ?? tabs[0]),
|
||||
);
|
||||
button.dataset.b09Group = tabs[0];
|
||||
if (tabs.length === 1) button.dataset.b09Tab = tabs[0];
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
const groupTab = new Map(TAB_GROUPS.map(([, tabs]) => [tabs[0], tabs]));
|
||||
fillTabs(top, {
|
||||
tabs: TAB_GROUPS.map(([label, tabs]) => ({ key: tabs[0], label })),
|
||||
value: picked[0],
|
||||
kind: "line",
|
||||
size: 32,
|
||||
data: "b09Group",
|
||||
onChange: (key) => onPick(lastSeen.get(key as TabId) ?? (key as TabId)),
|
||||
}).forEach((button) => {
|
||||
const tabs = groupTab.get(button.dataset.tab as TabId);
|
||||
if (tabs?.length === 1) button.dataset.b09Tab = tabs[0];
|
||||
});
|
||||
const single = picked.length === 1;
|
||||
sub.classList.toggle("b09-estimation__tabs--blank", single);
|
||||
const buttons = fillTabs(sub, {
|
||||
tabs: single
|
||||
? [{ key: picked[0], label: tabName(picked[0]) }]
|
||||
: picked.map((id) => ({ key: id, label: tabName(id) })),
|
||||
value: single ? picked[0] : tab,
|
||||
kind: "pill",
|
||||
data: "b09Tab",
|
||||
onChange: (id) => onPick(id as TabId),
|
||||
});
|
||||
sub.setAttribute("role", single ? "presentation" : "tablist");
|
||||
if (single) {
|
||||
const ghost = createButton({ label: tabName(picked[0]), variant: "ghost", size: "sm" });
|
||||
ghost.tabIndex = -1;
|
||||
ghost.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
|
||||
return void sub.replaceChildren(ghost, tools);
|
||||
}
|
||||
sub.replaceChildren(
|
||||
...picked.map((id) => {
|
||||
const button = tabButton(tabName(id), id === tab, "sm", () => onPick(id));
|
||||
button.dataset.b09Tab = id;
|
||||
return button;
|
||||
}),
|
||||
tools,
|
||||
);
|
||||
if (single)
|
||||
for (const button of buttons) {
|
||||
button.tabIndex = -1;
|
||||
button.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
|
||||
delete button.dataset.b09Tab;
|
||||
}
|
||||
sub.append(tools);
|
||||
};
|
||||
const setTools = (items: HTMLElement[]): void => tools.replaceChildren(...items);
|
||||
return { top, sub, draw, setTools };
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 잡기 = 글 칸 · 단추 밖 아무 칸 · 6px 넘게 움직여야 끌기(그냥 누름 = 줄 고름)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
|
||||
@@ -68,8 +69,8 @@ export function attachDrag(root: HTMLElement, table: BillTable, done: (key: stri
|
||||
const step = parseFloat(getComputedStyle(tr).getPropertyValue("--dt-indent")) || 14;
|
||||
const line = document.createElement("div");
|
||||
line.className = "b09-estimation__drop";
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "b09-estimation__drop-tag";
|
||||
const tag = createBadge("", "neutral", { size: 18 });
|
||||
tag.classList.add("b09-estimation__drop-tag");
|
||||
line.append(tag);
|
||||
const rowEl = (i: number): HTMLElement | null => root.querySelector(`tr[data-i="${i}"]`);
|
||||
let into: HTMLElement | null = null;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { attachDrag } from "./B09_Estimation_UI_Tabs_Bill_Drag";
|
||||
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
@@ -149,9 +150,8 @@ export function levelBar(
|
||||
button.dataset.b09Level = label;
|
||||
return button;
|
||||
});
|
||||
const guide = document.createElement("p");
|
||||
guide.className = "ui-general-block__empty b09-estimation__line-hint";
|
||||
guide.textContent = GUIDE;
|
||||
const guide = createText(GUIDE, "muted");
|
||||
guide.classList.add("b09-estimation__line-hint");
|
||||
attachHoverInfo(guide, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null)); // 236-2 잘릴 때만
|
||||
const sync = (): void => {
|
||||
const acts = menuActs(table, index());
|
||||
|
||||
@@ -10,14 +10,7 @@
|
||||
}
|
||||
|
||||
.b09-print__status {
|
||||
margin: 0;
|
||||
min-height: 1.4em;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b09-print__status.is-error {
|
||||
color: var(--color-danger);
|
||||
min-height: 1.4em; /* 글이 비어도 줄 높이 유지 */
|
||||
}
|
||||
|
||||
.b09-print__frame {
|
||||
@@ -30,9 +23,7 @@
|
||||
}
|
||||
|
||||
.b09-print__areas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
flex-wrap: wrap; /* 부품 보조 탭 줄 */
|
||||
}
|
||||
|
||||
.b09-print__list {
|
||||
@@ -42,39 +33,3 @@
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.b09-print__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
font-size: var(--text-body-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 좌측 하단 두 줄(302-5 · B09 Dock 꼴) — 단추는 줄 폭을 나눠 가짐 · 가로 넘침 없음 */
|
||||
.b09-print__actions.ui-sidebar-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.b09-print__actions.ui-sidebar-actions > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.b09-print__actions .b09-print__row {
|
||||
display: flex;
|
||||
gap: var(--btn-group-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn:not(.ui-help-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-inline: var(--spacing-4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b09-print__actions .b09-print__row > .ui-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "../B09_Estimation_UI_Page.css"; // 좌측 하단 단추 줄(b09-dock) 규칙 한 벌
|
||||
import "./B09_Draft_Print_Page.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import type { MountedPage } from "../../A00_Common/router";
|
||||
import { navigateTo } from "../../A00_Common/router";
|
||||
import { workflowSteps } from "../../A00_Common/b_page_scaffold";
|
||||
import { fillTabs } from "../../A00_Common/b_page_tabs";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../../A00_Common/b_workflow_nav";
|
||||
import { inlineImages, pdfBody, xlsxBody } from "./B09_Draft_Print_Table";
|
||||
import type {
|
||||
@@ -114,8 +118,8 @@ function save(blob: Blob, name: string): void {
|
||||
export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
|
||||
const status = document.createElement("p");
|
||||
status.className = "b09-print__status";
|
||||
const status = createText("", "muted");
|
||||
status.className += " b09-print__status";
|
||||
status.dataset.b09PrintStatus = "";
|
||||
const frame = document.createElement("iframe");
|
||||
frame.className = "b09-print__frame";
|
||||
@@ -144,6 +148,8 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const say = (text: string, error = false) => {
|
||||
status.textContent = text;
|
||||
status.classList.toggle("is-error", error);
|
||||
status.classList.toggle("ui-text--error", error);
|
||||
status.classList.toggle("ui-text--muted", !error);
|
||||
};
|
||||
const picked = () => (area?.items ?? []).map((it) => it.id).filter((id) => !off.has(id));
|
||||
const clearFrame = () => {
|
||||
@@ -213,21 +219,16 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const drawList = () => {
|
||||
list.replaceChildren(
|
||||
...(area?.items ?? []).map((it) => {
|
||||
const row = document.createElement("label");
|
||||
row.className = "b09-print__item";
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.checked = !off.has(it.id);
|
||||
box.dataset.b09PrintItem = it.id;
|
||||
box.addEventListener("change", () => {
|
||||
if (box.checked) off.delete(it.id);
|
||||
else off.add(it.id);
|
||||
later();
|
||||
const row = createCheckbox(it.name, {
|
||||
checked: !off.has(it.id),
|
||||
onChange: (on) => {
|
||||
if (on) off.delete(it.id);
|
||||
else off.add(it.id);
|
||||
later();
|
||||
},
|
||||
});
|
||||
const name = document.createElement("span");
|
||||
name.textContent = it.name;
|
||||
row.append(box, name);
|
||||
return row;
|
||||
row.input.dataset.b09PrintItem = it.id;
|
||||
return row.root;
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -238,18 +239,13 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
off = new Set();
|
||||
const my = ++seq;
|
||||
clearFrame();
|
||||
areaRow.replaceChildren(
|
||||
...AREAS.map((a) => {
|
||||
const b = createButton({
|
||||
label: a.label,
|
||||
size: "sm",
|
||||
variant: a.id === id ? "filled" : "ghost",
|
||||
onClick: () => a.id !== areaId && pickArea(a.id),
|
||||
});
|
||||
b.dataset.b09PrintArea = a.id;
|
||||
return b;
|
||||
}),
|
||||
);
|
||||
fillTabs(areaRow, {
|
||||
tabs: AREAS.map((a) => ({ key: a.id, label: a.label })),
|
||||
value: id,
|
||||
kind: "pill",
|
||||
data: "b09PrintArea",
|
||||
onChange: (key) => pickArea(key as AreaId),
|
||||
});
|
||||
xlsxBtn.hidden = id === "drawings";
|
||||
drawList();
|
||||
if (!projectId) return say("현재 프로젝트가 없습니다.", true);
|
||||
@@ -270,14 +266,14 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
|
||||
// 두 줄(302-5) — 위 = 내려받기 · 아래 = [?](도움말 줄) · 돌아가기 — 패널 폭 넘침 없음
|
||||
const files = document.createElement("div");
|
||||
files.className = "b09-print__row";
|
||||
files.className = "b09-dock__row b09-print__row";
|
||||
files.append(pdfBtn, xlsxBtn);
|
||||
const nav = document.createElement("div");
|
||||
nav.className = "b09-print__row";
|
||||
nav.className = "b09-dock__row b09-print__row";
|
||||
nav.dataset.helpRow = "";
|
||||
nav.append(back);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-sidebar-actions b09-print__actions";
|
||||
actions.className = "ui-sidebar-actions b09-dock b09-print__actions";
|
||||
actions.append(files, nav);
|
||||
const left = document.createElement("div");
|
||||
left.className = "ui-sidebar-panel b09-print__panel";
|
||||
|
||||
@@ -87,6 +87,19 @@ let layoutOpts = null;
|
||||
let confirmAnswer = false;
|
||||
const store = new Map(); // 가짜 세션 캐시
|
||||
const common = {
|
||||
"../A00_Common/b_page_tabs": {
|
||||
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
|
||||
fillTabs: (host, o) => {
|
||||
const kids = o.tabs.map((t) =>
|
||||
Object.assign(new Node("button"), {
|
||||
onClick: () => t.key !== o.value && o.onChange(t.key),
|
||||
dataset: { [o.data]: t.key },
|
||||
}),
|
||||
);
|
||||
host.replaceChildren(...kids);
|
||||
return kids;
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_workflow_layout": {
|
||||
createWorkflowLayout: (opts) => {
|
||||
layoutOpts = opts;
|
||||
|
||||
@@ -81,6 +81,19 @@ const calls = [];
|
||||
let layoutOpts = null;
|
||||
const store = new Map(); // 가짜 세션 캐시 — 이름 → 값
|
||||
const common = {
|
||||
"../A00_Common/b_page_tabs": {
|
||||
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
|
||||
fillTabs: (host, o) => {
|
||||
const kids = o.tabs.map((t) =>
|
||||
Object.assign(new Node("button"), {
|
||||
onClick: () => t.key !== o.value && o.onChange(t.key),
|
||||
dataset: { [o.data]: t.key },
|
||||
}),
|
||||
);
|
||||
host.replaceChildren(...kids);
|
||||
return kids;
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_workflow_layout": {
|
||||
createWorkflowLayout: (opts) => {
|
||||
layoutOpts = opts;
|
||||
|
||||
@@ -15,7 +15,7 @@ COMMON = "ui_template/ui_template_group_cards.ts"
|
||||
TABS = "B09_Estimation/B09_Estimation_UI_Tabs.ts"
|
||||
GROUPS = "B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts"
|
||||
|
||||
# 기억(b_page_state) 대신 Map · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
|
||||
# 기억(b_page_state) 대신 Map · 탭 줄(fillTabs) 대신 가짜 button 줄 · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
|
||||
STUBS = r"""
|
||||
const kept = new Map();
|
||||
const readState = (name) => (kept.has(name) ? JSON.parse(kept.get(name)) : null);
|
||||
@@ -27,6 +27,50 @@ const createButton = (o) => {
|
||||
if (o.onClick) b.addEventListener("click", o.onClick);
|
||||
return b;
|
||||
};
|
||||
const fillTabs = (host, o) => {
|
||||
const kids = o.tabs.map((t) => {
|
||||
const b = document.createElement("button");
|
||||
b.textContent = t.label;
|
||||
b.dataset[o.data] = t.key;
|
||||
b.addEventListener("click", () => t.key !== o.value && o.onChange(t.key));
|
||||
return b;
|
||||
});
|
||||
host.replaceChildren(...kids);
|
||||
return kids;
|
||||
};
|
||||
const createSegment = (o) => {
|
||||
const root = document.createElement("div");
|
||||
let current = o.value;
|
||||
const items = o.options.map((x) => {
|
||||
const b = document.createElement("button");
|
||||
b.textContent = x.label;
|
||||
b.dataset.value = x.value;
|
||||
b.addEventListener("click", () => {
|
||||
if (current === x.value) return;
|
||||
current = x.value;
|
||||
mark();
|
||||
o.onChange(x.value);
|
||||
});
|
||||
return b;
|
||||
});
|
||||
const mark = () => items.forEach((b) => b.setAttribute("aria-checked", String(b.dataset.value === current)));
|
||||
mark();
|
||||
root.append(...items);
|
||||
return { root, get: () => current, set: (v) => { current = v; mark(); } };
|
||||
};
|
||||
const createText = (text, kind) => {
|
||||
const p = document.createElement("p");
|
||||
p.className = `ui-text ui-text--${kind}`;
|
||||
p.textContent = text;
|
||||
return p;
|
||||
};
|
||||
const createBadge = (text, tone) => {
|
||||
const span = document.createElement("span");
|
||||
span.className = `ui-badge ui-badge--${tone}`;
|
||||
span.textContent = text;
|
||||
return span;
|
||||
};
|
||||
const createToolbar = () => document.createElement("div");
|
||||
const hoverCalls = [];
|
||||
const attachHoverInfo = (root, text, selector) => { hoverCalls.push({ root, text, selector }); return () => {}; };
|
||||
"""
|
||||
|
||||
@@ -48,6 +48,7 @@ function createButton(o) {
|
||||
b.addEventListener("click", o.onClick);
|
||||
return b;
|
||||
}
|
||||
function createText(text) { const p = new FakeEl("p"); p.textContent = text; return p; }
|
||||
function attachHoverInfo(el, text) { hovers.push([el.textContent, text]); }
|
||||
|
||||
let user = {};
|
||||
|
||||
@@ -150,11 +150,13 @@ def test_B08_표_카드_차례_표_아래줄_안내():
|
||||
def test_B08_자재_구조물_안내_줄_표_아래():
|
||||
material = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Material.ts").read_text("utf-8")
|
||||
assert "createCard({ body: [toolbar, table.root, excel, notes] })" in material
|
||||
assert "toolbar.append(save);" in material # 저장 단추만 위 줄(안내는 아래)
|
||||
assert (
|
||||
'createToolbar({ items: ["spacer", save], size: 32 })' in material
|
||||
) # 저장 단추만 위 줄(안내는 아래)
|
||||
summary = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Summary.ts").read_text("utf-8")
|
||||
assert "createCard({ body: [table, ...parts] })" in summary
|
||||
css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.css").read_text("utf-8")
|
||||
assert "justify-content: flex-end" in _block(css, ".b08-material__toolbar")
|
||||
assert "margin-bottom" in _block(css, ".b08-material__toolbar") # 줄 모양 = 부품 도구줄(spacer)
|
||||
|
||||
|
||||
# ── 창 끝까지 셈(199-1)에 아래 줄 몫 ────────────────────────────────────────
|
||||
|
||||
@@ -161,7 +161,7 @@ def test_메인_탭_하위_탭_추가_삭제():
|
||||
"ui_template/ui_template_locale_b4.ts"
|
||||
)
|
||||
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts")
|
||||
assert 'tab("table", "거리이정표")' in ui and "routes.map((r, i) => tab(r.id," in ui
|
||||
assert '{ key: "table", label: "거리이정표" }' in ui and "...routes.map((r, i) => ({" in ui
|
||||
# 244-4 — 하위 탭 줄 [+ 추가] · [삭제] 없음(탭은 [+] 등록 · 항목 정보 [삭제]로만)
|
||||
assert 'label: "+ 추가"' not in ui and "removeCurrent" not in ui
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
|
||||
@@ -172,6 +172,8 @@ def test_157_도구_왼쪽_아래_경로_탭_지도_그림_변경_초기화():
|
||||
assert 'label: "그림 변경"' in route and 'label: "초기화"' in route
|
||||
assert "그림 지우기" not in route and "자동 그림으로 되돌리기" not in route.split("*/", 1)[1]
|
||||
assert "map.root.hidden = user;" in route and "editMap({" in route # 그림 없으면 길찾기 지도
|
||||
assert ".b08-milestone__route-head {" in css and "justify-content: space-between;" in css
|
||||
assert (
|
||||
'createToolbar({ items: [title, "spacer", ...buttons], size: 32 })' in route
|
||||
) # 머리 줄 = 부품 도구줄
|
||||
state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
|
||||
assert "export const hasUserImage" in state and "canRevertImage" not in state
|
||||
|
||||
@@ -434,9 +434,13 @@ def test_좌측_하단_두_줄_가로_넘침_없음():
|
||||
assert "files.append(pdfBtn, xlsxBtn)" in page
|
||||
assert "nav.dataset.helpRow" in page and "nav.append(back)" in page
|
||||
assert "actions.append(files, nav)" in page
|
||||
css = re.sub(r"\s+", " ", _text(DRAFT / "B09_Draft_Print_Page.css"))
|
||||
assert ".b09-print__actions.ui-sidebar-actions { flex-direction: column; }" in css
|
||||
btn = css[css.index(".ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn") :]
|
||||
# 553-3 — 좌측 하단 줄 = B09 dock 한 벌(복사 없음) · 줄 폭을 나눠 가짐 · 글 줄임표
|
||||
assert 'actions.className = "ui-sidebar-actions b09-dock b09-print__actions"' in page
|
||||
assert 'className = "b09-dock__row b09-print__row"' in page
|
||||
css = re.sub(r"\s+", " ", _text(ROOT / "B09_Estimation/B09_Estimation_UI_Page.css"))
|
||||
assert ".b09-dock.ui-sidebar-actions { flex-direction: column; }" in css
|
||||
btn = css[css.index(".b09-dock .b09-dock__row > .ui-btn:not(") :]
|
||||
btn = btn[: btn.index("}")]
|
||||
# 공용 단추 줄(min-width: auto = 글보다 안 좁아짐)보다 앞서 이기는 규칙 · 글 줄임표
|
||||
assert "flex: 1 1 0" in btn and "min-width: 0" in btn and "text-overflow: ellipsis" in btn
|
||||
assert ".b09-print__actions" not in _text(DRAFT / "B09_Draft_Print_Page.css")
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_좌측_스위치_누름():
|
||||
"\nlet redraws = 0;"
|
||||
"\nconst dock = createDock([document.createElement('button')], () => redraws++);"
|
||||
"\nconst row = dock.children[0];"
|
||||
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);"
|
||||
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-checked']);"
|
||||
"\nconst labels = row.children.map((b) => b.textContent);"
|
||||
"\nconst a = pressed();"
|
||||
"\nrow.children[0].click();" # 이미 박스 = 그대로
|
||||
|
||||
Reference in New Issue
Block a user