refactor(B07): 상세 설계 화면 페이지 자체 변종을 부품 상자 부품으로 — 도면 단추 · 장 번호 · 도구 단추 · 카드 · 띠 알림 · 값 목록 (PLAN 553-1)
- 도면 단추(날 ui-panel-btn) · 장 번호 · 도구 단추 = 단추 부품 · ⚙ = 아이콘 단추 꼴 · 평면도 줄 = 목록 줄 꼴 - 머리 = 패널 머리 + 배지 · 확정 건너뜀 = 띠 알림 · 빈 글 = 빈 상태 · 측점 정보 = 값 목록 · ⚙ 모달 그룹 상자 = 카드 · 종류 이름 칸 = 입력 - window.confirm 둘 → 확인창 부품 · 죽은 CSS 걷음 · 시험 헬퍼 부품 가짜 한 벌(helper_b07_part_stubs) · 153-1 옛 오류(removeAttribute) 고침 · 가이드 선택자 · 지킴 시험 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ export function buildGuideToggle(apply: (visible: boolean) => void): HTMLButtonE
|
||||
const button = createButton({
|
||||
label: labelOf(shown),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
size: 28, // ⚙ 설정 단추 줄 한 규격
|
||||
onClick: () => {
|
||||
shown = !shown;
|
||||
paint();
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
// 좌측 패널(도면 목록·설계 정보)은 파일이 700줄을 넘어 떼어냈다(2026-09-04).
|
||||
@@ -175,7 +176,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
|
||||
/** 장 배치 저장(140-3 · ⚙ 횡단 모달) — 서버 쓰기 → 화면 다시 세움 · 같은 장 번호를 다시 엶. */
|
||||
const saveCrossLayoutAndRebuild = async (next: CrossLayout, sheet: number) => {
|
||||
if (!projectId || !mayDiscardEdits()) return;
|
||||
if (!projectId || !(await mayDiscardEdits())) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await saveCrossLayout(projectId, next);
|
||||
@@ -218,13 +219,16 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
|
||||
/** 저장하지 않은 편집이 있으면 묻는다. 버리기로 해야 이동한다. */
|
||||
const mayDiscardEdits = (): boolean => {
|
||||
const mayDiscardEdits = async (): Promise<boolean> => {
|
||||
if (!cadDirty && !tabs.anyDirty()) return true;
|
||||
return window.confirm(
|
||||
"저장하지 않은 편집이 있습니다.\n" +
|
||||
return askConfirm({
|
||||
message:
|
||||
"저장하지 않은 편집이 있습니다.\n" +
|
||||
"지금 도면을 바꾸면 편집이 사라집니다. 버리고 이동할까요?\n\n" +
|
||||
"남기려면 [취소] — 고친 도면은 페이지를 옮길 때([<] [>]) 저장됩니다.",
|
||||
);
|
||||
okLabel: "버리고 이동",
|
||||
danger: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -389,13 +393,13 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
|
||||
/** [평면도 관리] 모달을 닫음 — 고쳤으면 목록 · 도면을 서버에서 다시 받아 화면을 다시 세운다. */
|
||||
const onPlanClosed = (changed: boolean, openId?: string): void => {
|
||||
const onPlanClosed = async (changed: boolean, openId?: string): Promise<void> => {
|
||||
if (!changed) {
|
||||
const target = drawings.find((item) => item.id === openId);
|
||||
if (target) selectDrawing(target);
|
||||
return;
|
||||
}
|
||||
if (!mayDiscardEdits()) return;
|
||||
if (!(await mayDiscardEdits())) return;
|
||||
reopenDrawingId = openId ?? currentDrawing?.id;
|
||||
cad.destroy();
|
||||
void renderB07DesignDetail(root);
|
||||
|
||||
@@ -6,8 +6,14 @@
|
||||
* DOM 만 만들고 상태는 갖지 않는다 — 고르기 동작은 인자로 받은 콜백에 넘긴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createKv } from "@ui/ui_template_kv";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createPanelHead } from "@ui/ui_template_panel_head";
|
||||
import type { CrossDesignInfo, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch";
|
||||
import { buildDevelopmentSection } from "./B07_DesignDetail_UI_Development";
|
||||
import { buildPlanSection } from "./B07_DesignDetail_UI_PlanKinds";
|
||||
@@ -66,41 +72,38 @@ export function buildDrawingSidePanel(
|
||||
const isFolded = (kind: string): boolean => folded.get(kind) ?? true; // 389-3 모든 묶음 기본 닫힘
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b07-drawing-list";
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "b07-drawing-list__heading";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = "설계 도면";
|
||||
const count = document.createElement("span");
|
||||
count.textContent = `${drawings.length}건`;
|
||||
heading.append(title, count);
|
||||
const heading = createPanelHead({
|
||||
title: "설계 도면",
|
||||
actions: [createBadge(`${drawings.length}건`, "neutral")],
|
||||
divider: true,
|
||||
}).root;
|
||||
heading.classList.add("b07-drawing-list__heading");
|
||||
panel.append(heading);
|
||||
// 확정을 건너뛴 상태라는 것을 **조용히 두지 않는다** — 그냥 열면 다음 사람이
|
||||
// 「왜 값이 없나」로 헤맨다(2026-09-09). 되돌리기는 B08 좌측 [확정 없이 다음으로] 옆.
|
||||
if (devBypass) {
|
||||
const notice = document.createElement("p");
|
||||
notice.className = "b07-drawing-list__bypass";
|
||||
notice.textContent = "확정을 건너뛴 상태입니다 — 개발 전용. 값이 비어 보일 수 있습니다.";
|
||||
panel.append(notice);
|
||||
panel.append(
|
||||
createBanner({
|
||||
tone: "warning",
|
||||
text: "확정을 건너뛴 상태입니다 — 개발 전용. 값이 비어 보일 수 있습니다.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage || drawings.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b07-drawing-list__empty";
|
||||
empty.textContent = errorMessage ?? "확정된 종·횡단 도면이 없습니다.";
|
||||
const empty = createEmpty({
|
||||
text: errorMessage ?? "확정된 종·횡단 도면이 없습니다.",
|
||||
small: true,
|
||||
});
|
||||
empty.classList.add("b07-drawing-list__empty");
|
||||
panel.append(empty);
|
||||
return panel;
|
||||
}
|
||||
|
||||
const drawingButton = (drawing: DesignDrawingItem, label: string): HTMLButtonElement => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b07-drawing-button ui-panel-btn";
|
||||
const button = listButton(label);
|
||||
button.dataset.drawingId = drawing.id;
|
||||
button.dataset.confirmed = String(drawing.confirmed);
|
||||
const name = document.createElement("span");
|
||||
name.className = "b07-drawing-button__name";
|
||||
name.textContent = label;
|
||||
button.append(name);
|
||||
button.addEventListener("click", () => onSelect(drawing));
|
||||
return button;
|
||||
};
|
||||
@@ -140,16 +143,10 @@ export function buildDrawingSidePanel(
|
||||
const [drawing] = items;
|
||||
// 서버가 빈 도각을 내주지 못한 경우에만 회색 버튼으로 남는다.
|
||||
if (!drawing) {
|
||||
const placeholder = document.createElement("button");
|
||||
placeholder.type = "button";
|
||||
placeholder.className = "b07-drawing-button ui-panel-btn";
|
||||
const placeholder = listButton(group.label);
|
||||
placeholder.disabled = true;
|
||||
placeholder.title = "준비 중";
|
||||
placeholder.dataset.pending = "true";
|
||||
const name = document.createElement("span");
|
||||
name.className = "b07-drawing-button__name";
|
||||
name.textContent = group.label;
|
||||
placeholder.append(name);
|
||||
panel.append(placeholder);
|
||||
continue;
|
||||
}
|
||||
@@ -183,9 +180,11 @@ export function buildDrawingSidePanel(
|
||||
section.append(button);
|
||||
}
|
||||
if (structure && items.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b07-drawing-list__empty";
|
||||
empty.textContent = "B05 · B06 구조물 배치에 연결된 구조물 도면이 없습니다.";
|
||||
const empty = createEmpty({
|
||||
text: "B05 · B06 구조물 배치에 연결된 구조물 도면이 없습니다.",
|
||||
small: true,
|
||||
});
|
||||
empty.classList.add("b07-drawing-list__empty");
|
||||
section.append(empty);
|
||||
}
|
||||
panel.append(section);
|
||||
@@ -194,10 +193,20 @@ export function buildDrawingSidePanel(
|
||||
return panel;
|
||||
}
|
||||
|
||||
/** 도면 단추 한 개 — 단추 부품(고스트 32) · 이름 칸 이름만 B07 자리 이름을 더함. */
|
||||
function listButton(label: string): HTMLButtonElement {
|
||||
const button = createButton({ label, variant: "ghost", size: 32 });
|
||||
button.classList.add("b07-drawing-button");
|
||||
button.children[button.children.length - 1]?.classList.add("b07-drawing-button__name");
|
||||
return button;
|
||||
}
|
||||
|
||||
/** 한 장 단추 안 오른쪽 ⚙(280-5) — 단추 안 단추 금지라 span · 누름 · Enter 가 도면 열기로 안 올라감. */
|
||||
function buttonGear(onSettings: () => void): HTMLElement {
|
||||
const gear = document.createElement("span");
|
||||
gear.className = "b07-drawing-button__settings";
|
||||
// 아이콘 단추 부품 꼴을 입힘 — 단추 안 단추는 못 둬서 span 으로 둠
|
||||
gear.className = "b07-drawing-button__settings ui-icon-btn ui-icon-btn--ghost";
|
||||
gear.dataset.size = "20";
|
||||
gear.setAttribute("role", "button");
|
||||
gear.tabIndex = 0;
|
||||
gear.textContent = "⚙";
|
||||
@@ -244,19 +253,6 @@ function ditchLabel(design: CrossDesignInfo): string {
|
||||
return `${ditch.top_width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`;
|
||||
}
|
||||
|
||||
function infoRow(label: string, value: string): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "b07-info__row";
|
||||
const key = document.createElement("span");
|
||||
key.className = "b07-info__key";
|
||||
key.textContent = label;
|
||||
const val = document.createElement("span");
|
||||
val.className = "b07-info__val";
|
||||
val.textContent = value;
|
||||
row.append(key, val);
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (B06 정본 값 · 확정 배지 없음 · 요구 343).
|
||||
*
|
||||
@@ -271,19 +267,16 @@ export function buildDesignInfoPanel(
|
||||
): HTMLElement {
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b07-info";
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "b07-info__heading";
|
||||
const stationName = document.createElement("strong");
|
||||
const scopeLabel = scope === "sheet" ? L("B07_Info_Sheet") : L("B07_Info_Station");
|
||||
stationName.textContent = `${scopeLabel} ${title}`;
|
||||
heading.append(stationName);
|
||||
panel.append(heading);
|
||||
panel.append(createPanelHead({ title: `${scopeLabel} ${title}`, level: 4 }).root);
|
||||
|
||||
if (!design) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "b07-info__empty";
|
||||
empty.textContent = scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign");
|
||||
panel.append(empty);
|
||||
panel.append(
|
||||
createEmpty({
|
||||
text: scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign"),
|
||||
small: true,
|
||||
}),
|
||||
);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -293,12 +286,15 @@ export function buildDesignInfoPanel(
|
||||
groundTitle.textContent = L("B07_Info_Ground_Title");
|
||||
ground.append(
|
||||
groundTitle,
|
||||
infoRow(L("B07_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])),
|
||||
infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)),
|
||||
infoRow(
|
||||
L("B07_Info_DitchSide"),
|
||||
design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"),
|
||||
),
|
||||
createKv([
|
||||
{ label: L("B07_Info_GroundType"), value: L(GROUND_TYPE_LABEL[design.ground_type]) },
|
||||
{ label: L("B07_Info_CutSide"), value: cutSideLabel(design.section_mode) },
|
||||
{
|
||||
label: L("B07_Info_DitchSide"),
|
||||
value:
|
||||
design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const plan = document.createElement("section");
|
||||
@@ -307,13 +303,15 @@ export function buildDesignInfoPanel(
|
||||
planTitle.textContent = L("B07_Info_Plan_Title");
|
||||
plan.append(
|
||||
planTitle,
|
||||
infoRow(L("B07_Info_DesignElevation"), `${design.design_elevation_m.toFixed(2)}m`),
|
||||
infoRow(L("B07_Info_CutSlope"), `1:${design.cut_slope_ratio}`),
|
||||
infoRow(L("B07_Info_FillSlope"), `1:${design.fill_slope_ratio}`),
|
||||
infoRow(L("B07_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`),
|
||||
infoRow(L("B07_Info_Ditch"), ditchLabel(design)),
|
||||
infoRow(L("B07_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`),
|
||||
infoRow(L("B07_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`),
|
||||
createKv([
|
||||
{ label: L("B07_Info_DesignElevation"), value: `${design.design_elevation_m.toFixed(2)}m` },
|
||||
{ label: L("B07_Info_CutSlope"), value: `1:${design.cut_slope_ratio}` },
|
||||
{ label: L("B07_Info_FillSlope"), value: `1:${design.fill_slope_ratio}` },
|
||||
{ label: L("B07_Info_RoadWidth"), value: `${design.roadbed_width_m.toFixed(2)}m` },
|
||||
{ label: L("B07_Info_Ditch"), value: ditchLabel(design) },
|
||||
{ label: L("B07_Info_CutArea"), value: `${design.cut_area_m2.toFixed(2)}㎡` },
|
||||
{ label: L("B07_Info_FillArea"), value: `${design.fill_area_m2.toFixed(2)}㎡` },
|
||||
]),
|
||||
);
|
||||
|
||||
panel.append(ground, plan);
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createSelectField, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import {
|
||||
addPlanKind,
|
||||
deletePlanKind,
|
||||
@@ -37,6 +43,19 @@ function el<K extends keyof HTMLElementTagNameMap>(
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 도구 단추 — 단추 부품(고스트 24) · 자리 이름 `b07-plan-tools__button` 을 더함. */
|
||||
export function toolButton(label: string, onClick?: () => void): HTMLButtonElement {
|
||||
const button = createButton({ label, variant: "ghost", size: 24, onClick });
|
||||
button.classList.add("b07-plan-tools__button");
|
||||
return button;
|
||||
}
|
||||
|
||||
/** 단추 글자만 바꿈(단추 부품의 이름 칸은 마지막 자식) */
|
||||
export function setButtonText(button: HTMLElement, text: string): void {
|
||||
const label = button.children[button.children.length - 1];
|
||||
if (label) label.textContent = text;
|
||||
}
|
||||
|
||||
/** 실패는 토스트로 알리고 삼킨다 — 성공했을 때만 화면을 다시 세운다. */
|
||||
export async function run(
|
||||
action: () => Promise<unknown>,
|
||||
@@ -120,10 +139,10 @@ export function kindForm(
|
||||
compact: true,
|
||||
ariaLabel: "등고선 간격",
|
||||
});
|
||||
const name = el("input", "b07-plan-tools__name");
|
||||
name.type = "text";
|
||||
name.placeholder = "종류 이름";
|
||||
const nameField = createInputField({ type: "text", placeholder: "종류 이름", compact: true });
|
||||
const name = nameField.input;
|
||||
name.maxLength = 40;
|
||||
name.classList.add("b07-plan-tools__name");
|
||||
const boxes = state.pieces.map((piece) => createCheckbox(piece.name, { value: piece.id }));
|
||||
const pieceList = el("div", "b07-plan-tools__pieces");
|
||||
const grouped = PIECE_GROUPS.map((ids) => boxes.filter(({ input }) => ids.includes(input.value)));
|
||||
@@ -136,10 +155,8 @@ export function kindForm(
|
||||
if (index) pieceList.append(el("hr", "b07-plan-tools__divider"));
|
||||
pieceList.append(...group.map((box) => box.root));
|
||||
});
|
||||
const action = el("button", "b07-plan-tools__button");
|
||||
action.type = "button";
|
||||
const remove = el("button", "b07-plan-tools__button", "지우기");
|
||||
remove.type = "button";
|
||||
const action = toolButton("");
|
||||
const remove = toolButton("지우기");
|
||||
// 유역도 양식은 그림이 정해져 있어 얹을 정보를 고르지 않는다.
|
||||
const showPieces = (): void => {
|
||||
const watershed = layout.select.value === "watershed";
|
||||
@@ -169,7 +186,7 @@ export function kindForm(
|
||||
layout.select.value = kind?.layout ?? "plan";
|
||||
layout.select.disabled = Boolean(kind);
|
||||
layout.root.hidden = kind?.layout === "plan"; // 평면도는 늘 같은 값 — 새 종류 · 유역도만 보임
|
||||
action.textContent = kind ? "업데이트" : "추가";
|
||||
setButtonText(action, kind ? "업데이트" : "추가");
|
||||
remove.hidden = !kind;
|
||||
showPieces();
|
||||
};
|
||||
@@ -178,9 +195,13 @@ export function kindForm(
|
||||
remove.addEventListener("click", () => {
|
||||
const kind = state.kinds.find((item) => item.id === pick.select.value);
|
||||
if (!kind) return;
|
||||
if (!window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`))
|
||||
return;
|
||||
void run(() => deletePlanKind(controls.projectId, kind.id), controls);
|
||||
void askConfirm({
|
||||
message: `「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`,
|
||||
okLabel: "지우기",
|
||||
danger: true,
|
||||
}).then((ok) => {
|
||||
if (ok) void run(() => deletePlanKind(controls.projectId, kind.id), controls);
|
||||
});
|
||||
});
|
||||
action.addEventListener("click", () => {
|
||||
const pieces = boxes.filter((box) => box.input.checked).map((box) => box.input.value);
|
||||
@@ -201,7 +222,7 @@ export function kindForm(
|
||||
actions.append(action, remove);
|
||||
const fields = el("div", "b07-plan-tools__fields");
|
||||
fields.append(pick.root, layout.root, scale.root, contour.root);
|
||||
form.append(fields, name, pieceList, actions);
|
||||
form.append(fields, nameField.root, pieceList, actions);
|
||||
return {
|
||||
root: form,
|
||||
pick: (kindId) => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 처음 열 때 = 페이지 목록 · 미리받은 장(`Prefetch.openDrawing`) 그대로 · 같은 목록으로 다시 열면 요청 없음(522-3).
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createCadHost } from "@ui/cad_host/cad_host";
|
||||
import {
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
kindForm,
|
||||
run,
|
||||
type KindForm,
|
||||
toolButton,
|
||||
type PlanKindsControls,
|
||||
} from "./B07_DesignDetail_UI_PlanKinds";
|
||||
|
||||
@@ -56,10 +57,10 @@ function el<K extends keyof HTMLElementTagNameMap>(
|
||||
return node;
|
||||
}
|
||||
|
||||
function button(className: string, text: string, onClick: () => void): HTMLButtonElement {
|
||||
const node = el("button", className, text);
|
||||
node.type = "button";
|
||||
node.addEventListener("click", onClick);
|
||||
/** 장 번호 · 이름 단추 — 단추 부품(고스트 22) · 자리 이름을 더함. */
|
||||
function sheetButton(className: string, text: string, onClick: () => void): HTMLButtonElement {
|
||||
const node = createButton({ label: text, variant: "ghost", size: 22, onClick });
|
||||
node.classList.add(...className.split(" "));
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -105,16 +106,18 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
const ids = state.kinds.map((kind) => kind.id);
|
||||
let dragId: string | null = null;
|
||||
for (const kind of state.kinds) {
|
||||
const row = el("div", "b07-plan-kind__head b07-plan-manage__row");
|
||||
// 목록 줄 부품 꼴(28) · 끌기 손잡이 — 줄 끌어 놓기는 아래 이벤트가 맡음
|
||||
const row = el("div", "b07-plan-kind__head b07-plan-manage__row ui-list-row ui-sidebar-row");
|
||||
row.dataset.size = "28";
|
||||
row.dataset.planKind = kind.id;
|
||||
row.draggable = true;
|
||||
const grip = el("span", "b07-plan-manage__grip", "≡");
|
||||
const grip = el("span", "b07-plan-manage__grip ui-list-row__grip");
|
||||
grip.title = "끌어 놓아 종류 순서 바꾸기";
|
||||
const group = kindSheets(kind.id);
|
||||
// 한 장이면 이름이 곧 장 단추 · 여러 장이면 이름 + 작은 장 단추
|
||||
const name =
|
||||
group.length === 1
|
||||
? button("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () =>
|
||||
? sheetButton("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () =>
|
||||
pickSheet(group[0].drawing, group[0].label),
|
||||
)
|
||||
: el("span", "b07-plan-kind__name", kind.name);
|
||||
@@ -125,7 +128,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
row.append(grip, name);
|
||||
if (group.length > 1)
|
||||
group.forEach(({ drawing, label }, index) => {
|
||||
const item = button("b07-plan-manage__sheet", String(index + 1), () =>
|
||||
const item = sheetButton("b07-plan-manage__sheet", String(index + 1), () =>
|
||||
pickSheet(drawing, label),
|
||||
);
|
||||
item.title = label;
|
||||
@@ -166,9 +169,10 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
side
|
||||
.querySelectorAll<HTMLElement>(".b07-plan-manage__sheet")
|
||||
.forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked)));
|
||||
side
|
||||
.querySelectorAll<HTMLElement>(".b07-plan-manage__row")
|
||||
.forEach((row) => (row.dataset.active = String(row.dataset.planKind === pickedKind)));
|
||||
side.querySelectorAll<HTMLElement>(".b07-plan-manage__row").forEach((row) => {
|
||||
row.dataset.active = String(row.dataset.planKind === pickedKind);
|
||||
row.classList.toggle("is-active", row.dataset.planKind === pickedKind);
|
||||
});
|
||||
};
|
||||
|
||||
/** 장 하나를 미리보기에 — 보기 전용 · 확정 풀기는 메인 [수정](140-1). */
|
||||
@@ -180,7 +184,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo
|
||||
try {
|
||||
const response = await openDrawing(drawing);
|
||||
if (closed || picked !== drawing.id) return;
|
||||
head.append(button("b07-plan-tools__button", "CAD 에서 열기", () => finish(drawing.id)));
|
||||
head.append(toolButton("CAD 에서 열기", () => finish(drawing.id)));
|
||||
preview.load(
|
||||
response.drawing,
|
||||
null,
|
||||
|
||||
@@ -15,7 +15,8 @@ import { openModal } from "@ui/ui_template_modal";
|
||||
import type { CrossLayout, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch";
|
||||
import { DRAWING_GROUPS, isCrossSheet } from "./B07_DesignDetail_UI_Panels";
|
||||
import { openPlanManager, type PlanManageControls } from "./B07_DesignDetail_UI_PlanManage";
|
||||
import { mountDrawingScale, settingsGroup } from "./B07_DesignDetail_UI_Settings_Scale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { groupBody, mountDrawingScale, settingsGroup } from "./B07_DesignDetail_UI_Settings_Scale";
|
||||
|
||||
type Kind = DesignDrawingItem["kind"];
|
||||
|
||||
@@ -43,9 +44,8 @@ const CROSS_SCALE_MIN = 0.5;
|
||||
const CROSS_SCALE_MAX = 2;
|
||||
|
||||
const note = (text: string): HTMLElement => {
|
||||
const line = document.createElement("p");
|
||||
line.className = "b07-settings__note";
|
||||
line.textContent = text;
|
||||
const line = createText(text, "muted", 14);
|
||||
line.classList.add("b07-settings__note");
|
||||
return line;
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void
|
||||
closeModal = close;
|
||||
if (mine) body.append(note(`지금 도면: ${current?.label}`));
|
||||
if (kind === "structure") body.append(settingsGroup("축척", note("M02 그림 — 축척 없음")));
|
||||
const confirm = createButton({ label: "확인", onClick: () => void apply() });
|
||||
const confirm = createButton({ label: "확인", size: 28, onClick: () => void apply() });
|
||||
const refresh = (): void => {
|
||||
confirm.disabled = scale?.read() === "invalid" || cross?.read() === "invalid";
|
||||
};
|
||||
@@ -100,13 +100,18 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void
|
||||
if (nextLayout) deps.cross.save(nextLayout, cross!.sheet);
|
||||
else if (nextScale) deps.plan?.onClosed(true, deps.current()?.id); // 목록 · 도면 다시 받음
|
||||
};
|
||||
const cancel = createButton({ label: "취소", variant: "ghost", onClick: close });
|
||||
const cancel = createButton({ label: "취소", variant: "ghost", size: 28, onClick: close });
|
||||
cancel.classList.add("b07-settings__cancel");
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "b07-settings__actions";
|
||||
if (cross) footer.append(deps.cross.guideToggle);
|
||||
if (scale) {
|
||||
const reset = createButton({ label: "기본값", variant: "ghost", onClick: scale.reset });
|
||||
const reset = createButton({
|
||||
label: "기본값",
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: scale.reset,
|
||||
});
|
||||
reset.title = "축척 칸을 기본으로 채움 — [확인]으로 씀";
|
||||
footer.append(reset);
|
||||
}
|
||||
@@ -178,7 +183,7 @@ function mountCross(
|
||||
table.input.value = String(got.table_scale ?? 1);
|
||||
for (const field of [cols, rows, table]) field.input.disabled = false;
|
||||
})
|
||||
.catch(() => layoutBox.append(note("배치를 받지 못했습니다.")));
|
||||
.catch(() => groupBody(layoutBox).append(note("배치를 받지 못했습니다.")));
|
||||
|
||||
return {
|
||||
sheet,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createCard,
|
||||
createInputField,
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
fetchDrawingScales,
|
||||
saveDrawingScales,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type { DrawingSettingsDeps } from "./B07_DesignDetail_UI_Settings";
|
||||
import { readScaleAxis, scaleToSend } from "./logic/B07_DesignDetail_Scale_Check";
|
||||
|
||||
@@ -38,16 +40,18 @@ const AXIS_LABEL: Record<Axis, string> = { x: "X(가로) 1/", y: "Y(세로) 1/",
|
||||
|
||||
const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL;
|
||||
|
||||
/** 모달 안 그룹 상자 — 제목(legend) + 칸. */
|
||||
export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLFieldSetElement {
|
||||
const box = document.createElement("fieldset");
|
||||
box.className = "b07-settings__group";
|
||||
const title = document.createElement("legend");
|
||||
title.textContent = legend;
|
||||
box.append(title, ...children);
|
||||
/** 모달 안 그룹 상자 — 카드 부품(제목 + 칸). 나중에 넣을 칸은 `groupBody(box)` 에. */
|
||||
export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLElement {
|
||||
const box = createCard({ title: legend, body: children, pad: 12 });
|
||||
box.classList.add("b07-settings__group");
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 그룹 상자의 칸 자리 — 카드 부품의 마지막 자식 */
|
||||
export function groupBody(box: HTMLElement): HTMLElement {
|
||||
return (box.children[box.children.length - 1] as HTMLElement | undefined) ?? box;
|
||||
}
|
||||
|
||||
/** 축척 칸 한 벌 — 모달 [확인] · [기본값] 이 씀. */
|
||||
export interface ScaleFields {
|
||||
/** null = 그대로(못 받음 포함) · "invalid" = 붉은 칸 있음 · 그 밖 = 보낼 값 */
|
||||
@@ -66,9 +70,8 @@ export function mountDrawingScale(
|
||||
): ScaleFields | undefined {
|
||||
if (!isScaleKind(kind) || !deps.plan) return undefined;
|
||||
const { projectId } = deps.plan;
|
||||
const status = document.createElement("p");
|
||||
status.className = "b07-settings__note";
|
||||
status.textContent = "축척 받는 중…";
|
||||
const status = createText("축척 받는 중…", "muted", 14);
|
||||
status.classList.add("b07-settings__note");
|
||||
const section = settingsGroup(
|
||||
kind === "cross" ? "축척 — 횡단도 장(표준 횡단면도 뺌)" : `축척 — ${LABEL[kind]}`,
|
||||
status,
|
||||
@@ -137,9 +140,8 @@ function buildScaleFields(
|
||||
});
|
||||
|
||||
// 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림
|
||||
const alertLine = document.createElement("p");
|
||||
alertLine.className = "b07-scale__error";
|
||||
alertLine.setAttribute("role", "alert");
|
||||
const alertLine = createText("", "error");
|
||||
alertLine.classList.add("b07-scale__error");
|
||||
alertLine.hidden = true;
|
||||
const check = (): void => {
|
||||
const errors = new Map<string, string[]>();
|
||||
@@ -159,7 +161,7 @@ function buildScaleFields(
|
||||
const row = document.createElement("div");
|
||||
row.className = "b07-scale__row";
|
||||
row.append(...fields.map((f) => f.field.root));
|
||||
section.append(row, alertLine);
|
||||
groupBody(section).append(row, alertLine);
|
||||
return {
|
||||
read: () => {
|
||||
const read = fields.map((f) => [f, f.read()] as const);
|
||||
|
||||
@@ -26,20 +26,6 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.b07-drawing-list__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--spacing-12);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.b07-drawing-list__heading span,
|
||||
.b07-drawing-list__empty {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* 횡단도는 2열 배치 — 단, **칸이 좁아지면 1열로 접는다**(2026-09-08).
|
||||
좌측 패널이 320 고정이던 동안은 2열이 늘 128.8px 라 문제가 없었으나, 공용
|
||||
오버레이가 좁은 폭에서 패널을 280·227·182 로 물리면서 칸이 108.8 → 82.4 → 60px
|
||||
@@ -76,16 +62,12 @@
|
||||
}
|
||||
|
||||
.b07-drawing-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
/* 격자 칸의 기본 최소폭은 글자 길이라, 이름이 길면 칸이 패널을 밀어내 단추가
|
||||
오른쪽으로 튀어나갔다(2026-09-04 실측 14px). 0으로 풀면 이름이 `…`로 잘린다. */
|
||||
min-width: 0;
|
||||
/* 크기 · 테두리 · 바탕 · 글자 = 공용 `.ui-panel-btn`(269) · 여기선 확정 여부 좌측 색 띠(미확정: 투명)만 */
|
||||
/* 크기 · 테두리 · 바탕 · 글자 = 단추 부품(고스트 32) · 여기선 확정 여부 좌측 색 띠(미확정: 투명)만 */
|
||||
border-left: 3px solid transparent;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.b07-drawing-button__name {
|
||||
@@ -101,29 +83,19 @@
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
/* ⚙ = 아이콘 단추 부품 꼴(span) · 자리만 단추 안 오른쪽 */
|
||||
.b07-drawing-button__settings {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
right: 4px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b07-drawing-button__settings:hover,
|
||||
.b07-drawing-button__settings:focus-visible {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.b07-drawing-button:hover,
|
||||
.b07-drawing-button[data-active="true"] {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
|
||||
.b07-drawing-button[data-active="true"] {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* 확정: 좌측 띠 + 측점 글자색을 함께 성공색으로 반영 */
|
||||
@@ -174,13 +146,6 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-info__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-info__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -193,27 +158,6 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.b07-info__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.b07-info__key {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.b07-info__val {
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.b07-info__empty {
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 횡단 장 배치(153-1 · 280-5 ⚙ 횡단 모달 · 523-1 그룹 상자) — 한 줄에 한 일:
|
||||
「장 배치」 배치 열 · 행 / 적용 · 「표」 표 배율 · 한 줄 안 같은 너비. */
|
||||
.b07-cross-layout {
|
||||
@@ -250,15 +194,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 확정을 건너뛴 개발 상태 알림 — 눈에 띄되 도면 목록을 밀어내지 않게 한 줄만. */
|
||||
.b07-drawing-list__bypass {
|
||||
margin: var(--spacing-4) 0 0;
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-left: 3px solid var(--color-warning, #d9a441);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 표준도 제원 입력 모양은 B08 로 이관(2026-09-13) — `B08_Quantity_UI_StructureSheet_Spec.ts` 가 스스로 넣음. */
|
||||
|
||||
/* 계획평면도 묶음 — 종류별 이름 줄 + 장 단추, 아래 [평면도 관리](PLAN 60 · 67 · 130장) */
|
||||
@@ -322,17 +257,7 @@
|
||||
}
|
||||
|
||||
/* 「평면도 장」 한 블록(140-2) — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서 */
|
||||
.b07-plan-manage__row {
|
||||
padding: 2px var(--spacing-4);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-buttons);
|
||||
}
|
||||
|
||||
.b07-plan-manage__row[data-active="true"] {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
|
||||
/* 줄 = 목록 줄 부품(28) · 여기선 끌어 놓을 자리 선만 */
|
||||
.b07-plan-manage__row[data-drop="before"] {
|
||||
box-shadow: inset 0 2px 0 var(--color-primary);
|
||||
}
|
||||
@@ -341,25 +266,13 @@
|
||||
box-shadow: inset 0 -2px 0 var(--color-primary);
|
||||
}
|
||||
|
||||
.b07-plan-manage__grip {
|
||||
color: var(--color-text-muted);
|
||||
cursor: grab;
|
||||
.b07-plan-manage__sheet.b07-plan-kind__name {
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.b07-plan-manage__sheet {
|
||||
min-width: 26px;
|
||||
padding: 2px var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-caption);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b07-plan-manage__sheet.b07-plan-kind__name {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.b07-plan-manage__sheet[data-confirmed="true"] {
|
||||
@@ -367,8 +280,8 @@
|
||||
}
|
||||
|
||||
.b07-plan-manage__sheet[data-active="true"] {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.b07-plan-tools__block {
|
||||
@@ -409,15 +322,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b07-plan-tools__button {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 목록에서 연 도면 탭 줄 + CAD(82-4) */
|
||||
.b07-cad-stack {
|
||||
display: flex;
|
||||
@@ -469,30 +373,6 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b07-settings__note {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.b07-settings__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: var(--spacing-8) var(--spacing-12) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.b07-settings__group > legend {
|
||||
padding: 0 var(--spacing-4);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.b07-settings__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -507,9 +387,6 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
|
||||
/* 단추 줄 한 규격 — 안내선 단추(좌측 패널 작은 꼴)도 같은 키 · 좁은 폭에 맞춘 옆 여백 */
|
||||
.b07-settings__actions .ui-btn {
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
font-size: var(--text-body-sm);
|
||||
border-radius: var(--radius-buttons);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -533,10 +410,8 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-scale__error {
|
||||
margin: 0;
|
||||
color: var(--color-danger);
|
||||
font-size: 12px;
|
||||
.b07-scale__error[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 받지 못한 도면(508-3b) — 미리받기 · 열기가 두 번 다 실패 · 누르면 다시 받음 */
|
||||
@@ -548,3 +423,15 @@ html.is-bm-route .b07-cad-stack > .cad-tabs {
|
||||
content: " · 다시 받기";
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 설정 모달 그룹 상자 = 카드 부품 · 제목은 옛 legend 크기로 작게(모달 h3 규칙이 키우지 않게) */
|
||||
.b07-settings__group > .ui-card__title {
|
||||
margin-bottom: var(--spacing-8);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.b07-settings__group > .ui-card__body {
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
const { partStubs } = require("./helper_b07_part_stubs.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const B07 = path.join(ROOT, "B07_DesignDetail");
|
||||
@@ -24,6 +25,8 @@ class FakeNode {
|
||||
const self = this;
|
||||
this.classList = {
|
||||
add: (...names) => names.forEach((n) => self._classes.add(n)),
|
||||
toggle: (name, force) =>
|
||||
(force ?? !self._classes.has(name)) ? self._classes.add(name) : self._classes.delete(name),
|
||||
contains: (name) => self._classes.has(name),
|
||||
};
|
||||
}
|
||||
@@ -88,7 +91,7 @@ const byText = (node, tag, text) =>
|
||||
|
||||
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
|
||||
const confirms = [];
|
||||
globalThis.window = { confirm: (message) => (confirms.push(message), true) };
|
||||
const parts = partStubs((tag) => new FakeNode(tag));
|
||||
|
||||
// ── 공용 부품 가짜 ─────────────────────────────────────────────────────────────
|
||||
const toasts = [];
|
||||
@@ -96,7 +99,9 @@ const modals = [];
|
||||
const cads = [];
|
||||
const selects = [];
|
||||
const STUBS = {
|
||||
...parts.modules,
|
||||
"@ui/ui_template_elements": {
|
||||
...parts.elements,
|
||||
showToast: (message, kind) => toasts.push([kind, message]),
|
||||
createSelectField: (opts) => {
|
||||
const root = new FakeNode("select");
|
||||
@@ -120,6 +125,7 @@ const STUBS = {
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_modal": {
|
||||
askConfirm: async (opts) => (confirms.push(opts.message), true),
|
||||
openModal: (opts) => {
|
||||
const body = new FakeNode("div");
|
||||
const modal = { opts, body, open: true };
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
const { partStubs } = require("./helper_b07_part_stubs.cjs");
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
@@ -46,6 +47,9 @@ class FakeNode {
|
||||
setAttribute(key, value) {
|
||||
this.attrs[key] = String(value);
|
||||
}
|
||||
removeAttribute(key) {
|
||||
delete this.attrs[key];
|
||||
}
|
||||
append(...kids) {
|
||||
kids.forEach((k) => this.children.push(k));
|
||||
}
|
||||
@@ -64,6 +68,7 @@ const all = (node) => [node, ...node.children.flatMap(all)];
|
||||
const byClass = (node, name) => all(node).filter((n) => n.classList.contains(name));
|
||||
|
||||
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
|
||||
const parts = partStubs((tag) => new FakeNode(tag));
|
||||
|
||||
const toasts = [];
|
||||
const modals = [];
|
||||
@@ -83,7 +88,8 @@ const { loadTs } = makeLoader({
|
||||
...loadTs(path.join(UI, "ui_template_elements.ts")),
|
||||
showToast: (message, kind) => toasts.push([kind, message]),
|
||||
};
|
||||
if (request === "@ui/ui_template_modal") return { openModal };
|
||||
if (request === "@ui/ui_template_modal") return { openModal, askConfirm: async () => true };
|
||||
if (parts.modules[request]) return parts.modules[request];
|
||||
if (request === "./B07_DesignDetail_UI_PlanManage") return {};
|
||||
if (request === "./B07_DesignDetail_Api_Fetch") return {};
|
||||
// 공용 카드(269) = 진짜 모듈
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
const { partStubs } = require("./helper_b07_part_stubs.cjs");
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
@@ -122,17 +123,11 @@ const SCALES = {
|
||||
};
|
||||
let answer = true;
|
||||
|
||||
const button = (opts) => {
|
||||
const node = new FakeNode("button");
|
||||
node.className = "ui-btn";
|
||||
node.textContent = opts.label;
|
||||
node.disabled = !!opts.disabled;
|
||||
if (opts.onClick) node.addEventListener("click", opts.onClick);
|
||||
return node;
|
||||
};
|
||||
const parts = partStubs((tag) => new FakeNode(tag));
|
||||
const STUBS = {
|
||||
...parts.modules,
|
||||
"@ui/ui_template_elements": {
|
||||
createButton: button,
|
||||
...parts.elements,
|
||||
createInputField: (opts) => {
|
||||
const root = new FakeNode("label");
|
||||
const input = new FakeNode("input");
|
||||
@@ -290,7 +285,7 @@ const drawings = [
|
||||
let current = drawings[4]; // cross_s1
|
||||
let redraws = 0;
|
||||
const saved = [];
|
||||
const guideToggle = button({ label: "안내선 숨기기" });
|
||||
const guideToggle = parts.elements.createButton({ label: "안내선 숨기기" });
|
||||
const closedCalls = [];
|
||||
const deps = {
|
||||
drawings,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
const { partStubs } = require("./helper_b07_part_stubs.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
@@ -115,14 +116,16 @@ const press = (root, section) => {
|
||||
};
|
||||
|
||||
const node = (tag = "div") => new FakeNode(tag);
|
||||
const parts = partStubs((tag) => new FakeNode(tag));
|
||||
const STUBS = {
|
||||
...parts.modules,
|
||||
"@ui/ui_template_modal": { askConfirm: async () => true },
|
||||
"@ui/ui_template_locale": {
|
||||
ui_locales: new Proxy({}, { get: (_, k) => [String(k)] }),
|
||||
currentLanguageIndex: 0,
|
||||
},
|
||||
"@ui/ui_template_elements": {
|
||||
createButton: ({ label }) => Object.assign(node("button"), { _text: label }),
|
||||
createInputField: () => ({ root: node(), input: node("input") }),
|
||||
...parts.elements,
|
||||
createSelectField: () => ({ root: node(), select: { value: "" } }),
|
||||
showToast: () => undefined,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* B07 시험 헬퍼 공용 — 템플릿 부품(단추 · 카드 · 배지 · 띠 알림 · 빈 상태 · 값 목록 · 패널 머리 · 보조 글)의
|
||||
* 기록용 가짜. 화면 그릇(FakeNode)은 헬퍼 것을 받아 씀 — 진짜 부품이 내는 클래스 · 자식 차례만 같게 흉내.
|
||||
*
|
||||
* const { partStubs } = require("./helper_b07_part_stubs.cjs");
|
||||
* const parts = partStubs((tag) => new FakeNode(tag));
|
||||
* STUBS["@ui/ui_template_elements"] = { ...parts.elements, 나머지 };
|
||||
* STUBS = { ...STUBS, ...parts.modules };
|
||||
*/
|
||||
function partStubs(makeNode) {
|
||||
const node = (tag, className, text) => {
|
||||
const made = makeNode(tag);
|
||||
if (className) made.className = className;
|
||||
if (text !== undefined) made.textContent = text;
|
||||
if (!made.dataset) made.dataset = {};
|
||||
return made;
|
||||
};
|
||||
/** 단추 부품 — `ui-btn ui-btn--<꼴>` · 마지막 자식 = 이름 칸(`ui-btn__label`) */
|
||||
const createButton = (opts) => {
|
||||
const button = node("button", `ui-btn ui-btn--${opts.variant ?? "filled"}`);
|
||||
if (opts.size !== undefined) button.dataset.size = String(opts.size);
|
||||
button.append(node("span", "ui-btn__label", opts.label));
|
||||
button.disabled = !!opts.disabled;
|
||||
if (opts.onClick) button.addEventListener("click", opts.onClick);
|
||||
return button;
|
||||
};
|
||||
const createInputField = (opts = {}) => {
|
||||
const input = node("input", opts.compact ? "ui-input ui-input--sm" : "ui-input");
|
||||
input.value = opts.value ?? "";
|
||||
input.placeholder = opts.placeholder ?? "";
|
||||
const root = node("div", "ui-field");
|
||||
root.append(input);
|
||||
return { root, input, setError: () => undefined };
|
||||
};
|
||||
const createCard = (opts) => {
|
||||
const card = node("div", "ui-card");
|
||||
if (opts.title) card.append(node("h3", "ui-card__title", opts.title));
|
||||
const body = node("div", "ui-card__body");
|
||||
body.append(...(opts.body ?? []));
|
||||
card.append(body);
|
||||
return card;
|
||||
};
|
||||
return {
|
||||
node,
|
||||
elements: { createButton, createInputField, createCard },
|
||||
modules: {
|
||||
"@ui/ui_template_badge": { createBadge: (text) => node("span", "ui-badge", text) },
|
||||
"@ui/ui_template_banner": { createBanner: (opts) => node("div", "ui-banner", opts.text) },
|
||||
"@ui/ui_template_empty": { createEmpty: (opts) => node("div", "ui-empty", opts.text) },
|
||||
"@ui/ui_template_kv": { createKv: () => node("dl", "ui-kv") },
|
||||
"@ui/ui_template_panel_head": {
|
||||
createPanelHead: (opts) => ({ root: node("header", "ui-panel-head", opts.title) }),
|
||||
},
|
||||
"@ui/ui_template_text": {
|
||||
createText: (text, kind = "muted") => node("p", `ui-text ui-text--${kind}`, text),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { partStubs };
|
||||
@@ -87,7 +87,7 @@ def test_종류_추가(out):
|
||||
def test_종류_업데이트(out):
|
||||
assert out["updateLabel"] == "업데이트"
|
||||
assert out["update"]["call"] == ["update", "k_new", "sub6임시2", [], 1000]
|
||||
assert out["update"]["rowText"] == "≡sub6임시2" # 한 장이면 이름이 곧 장 단추
|
||||
assert out["update"]["rowText"] == "sub6임시2" # 한 장이면 이름이 곧 장 단추 · 손잡이는 목록 줄 부품의 점(글 없음)
|
||||
|
||||
|
||||
def test_종류_지우기(out):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""180-3 B07 횡단 장 안내선 보이기 / 숨기기 단추.
|
||||
|
||||
- 끄는 것 = 점선 작도 영역 + 붉은 넘침 틀(화면 전용) · 인쇄 칸선 · 도면 개체는 그대로
|
||||
- 단추 = 배치 칸 줄 · ghost sm · 글 「안내선 숨기기」 ↔ 「안내선 보이기」 · 상태는 모듈 값(저장 안 함)
|
||||
- 단추 = 설정 단추 줄 · ghost 28 · 글 「안내선 숨기기」 ↔ 「안내선 보이기」 · 상태는 모듈 값(저장 안 함)
|
||||
- 숨김이면 넘침 토스트도 안 띄움 · 메시지 한 줄(`aislo:b08:guide-visible`) · cad_host `setGuideVisible`
|
||||
"""
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_단추_글_누름_상태_이어짐():
|
||||
out = json.loads(proc.stdout)
|
||||
assert out["first"] == {
|
||||
"text": "안내선 숨기기",
|
||||
"cls": "ui-btn ui-btn--ghost ui-btn--sm",
|
||||
"cls": "ui-btn ui-btn--ghost",
|
||||
"pressed": "false",
|
||||
"shown": True,
|
||||
}
|
||||
|
||||
@@ -120,3 +120,45 @@ def test_b04_css_common_values_only():
|
||||
assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭"
|
||||
body = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
assert not RAW_COLOR.search(body), f"{name}: 날 색"
|
||||
|
||||
|
||||
def test_b07_guide_selectors_alive():
|
||||
"""가이드 촬영 장면이 잡는 B07 자기 클래스(`b07-…`)는 소스에 살아 있어야 함 — CAD 앱 안(`cad-…`)은 다른 몫."""
|
||||
text = (ROOT / "A05_EduDetail/guide/B07/_scenes.json").read_text(encoding="utf-8")
|
||||
for name in set(re.findall(r"\.(b07-[a-z0-9_-]+)", text)):
|
||||
assert _defined("B07_DesignDetail", name), f"가이드 선택자 .{name} 가 B07 소스에 없음"
|
||||
|
||||
|
||||
def test_b07_uses_parts_not_raw_widgets():
|
||||
"""도면 단추 · 장 번호 · 도구 단추 · 그룹 상자 · 알림 · 값 줄이 부품으로 바뀜 · 시스템 확인창 없음."""
|
||||
ts = "".join(_src("B07_DesignDetail", ".ts").values())
|
||||
css = "".join(_src("B07_DesignDetail", ".css").values())
|
||||
for part in (
|
||||
"createButton",
|
||||
"createBadge",
|
||||
"createBanner",
|
||||
"createEmpty",
|
||||
"createKv",
|
||||
"createPanelHead",
|
||||
"createCard",
|
||||
"createText",
|
||||
"askConfirm",
|
||||
):
|
||||
assert part in ts, f"B07 이 부품 {part} 를 안 씀"
|
||||
assert "window.confirm" not in ts
|
||||
assert 'createElement("button")' not in ts and 'el("button"' not in ts
|
||||
assert "ui-panel-btn" not in ts and "ui-panel-btn" not in css # 옛 날 단추 꼴
|
||||
for old in (
|
||||
"b07-info__row",
|
||||
"b07-info__key",
|
||||
"b07-plan-tools__button {",
|
||||
"b07-settings__group {",
|
||||
):
|
||||
assert old not in css and old not in ts, f"옛 변종 {old} 가 남음"
|
||||
|
||||
|
||||
def test_b07_css_common_values_only():
|
||||
for name, css in _src("B07_DesignDetail", ".css").items():
|
||||
assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭"
|
||||
body = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
assert not RAW_COLOR.search(body), f"{name}: 날 색"
|
||||
|
||||
Reference in New Issue
Block a user