Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -178,7 +178,6 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null)
|
||||
settingCard(L("B01_Dashboard_MasterElements"), L("B01_Dashboard_PermSourceRead"), () =>
|
||||
navigateTo(ROUTES.M01_MASTER_DATA),
|
||||
),
|
||||
m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", "master", desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", "master", desc),
|
||||
m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc),
|
||||
m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc),
|
||||
@@ -193,10 +192,7 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null)
|
||||
L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"),
|
||||
L("B01_Dashboard_PermPersonalWrite"),
|
||||
].join(" · ");
|
||||
wrap.append(
|
||||
m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", root, desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc),
|
||||
);
|
||||
wrap.append(m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
|
||||
.b05-routeedit__cross-station {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
font-size: inherit; /* 44-2 — 제목(횡단)과 같은 크기 */
|
||||
}
|
||||
|
||||
/* 횡단 판 — 캔버스 그림이 아니라 **SVG 선**이다(2026-09-12 사용자 지시 ③).
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
.b05-routeedit__cross-key {
|
||||
fill: var(--color-text-secondary, #94a3b8);
|
||||
font-size: 11px;
|
||||
font-size: 5.5px; /* 44-2 — 지금(11px)의 절반 */
|
||||
}
|
||||
|
||||
.b05-routeedit__cross-key.is-design {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""M02 일위대가 문서 `labor/<열id>.json` — 정리 · 검사 · 계산 · 알약(PLAN 39-1a · 설계_정함 1장).
|
||||
|
||||
문서 = `{종류, 판, 열, 일위대가번, 단위, 본뜬, 담은로직: [{로직, 메모, 입력값, 수량}],
|
||||
문서 = `{종류, 판, 열, 일위대가번, 단위, 본뜬, 담은로직: [{로직, 별칭, 메모, 입력값, 수량}],
|
||||
자재: [{코드, 이름, 규격, 단위, 수량}], 계산값}` — 이름은 구조물 열 이름(따로 안 둠).
|
||||
수량 = `null`(면적산출 알약 칸 값 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」) | `{손: 수}`
|
||||
(손이 칸보다 앞섬) · 같은 로직 두 번 금지 · 입력값 = 그 로직 입력 이름만(빈 값 = 자동값).
|
||||
@@ -26,6 +26,7 @@ mm = mf.mp.mm
|
||||
COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비
|
||||
MATERIAL_HEAD = "자재|"
|
||||
_MAT_COLS = ("코드", "이름", "규격", "단위")
|
||||
ALIAS_MAX = 40 # 별칭(44-6) 글자 수 상한 — 화면 칸과 같음
|
||||
|
||||
|
||||
def _bad(text: str) -> store.StoreError:
|
||||
@@ -83,10 +84,16 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
given = one.get("입력값") or {}
|
||||
if not isinstance(given, dict):
|
||||
raise _bad(f"{where} · 입력값이 묶음 아님")
|
||||
memo = one.get("메모")
|
||||
memo, alias = one.get("메모"), one.get("별칭")
|
||||
if alias is not None and not isinstance(alias, str):
|
||||
raise _bad(f"{where} · 별칭은 글")
|
||||
alias = (alias or "").strip()
|
||||
if len(alias) > ALIAS_MAX:
|
||||
raise _bad(f"{where} · 별칭은 {ALIAS_MAX}자까지")
|
||||
lines.append(
|
||||
{
|
||||
"로직": code,
|
||||
"별칭": alias or None,
|
||||
"메모": str(memo) if memo else None,
|
||||
"입력값": {k: v for k, v in given.items() if k in names and v not in (None, "")},
|
||||
"수량": _qty(one.get("수량"), where),
|
||||
|
||||
@@ -78,6 +78,59 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 옵션 값 줄(44-6) — 이름 ~ 합계에 걸친 한 줄 말줄임(max-width 0 = 표 폭을 안 늘림) · 위 줄과 한 덩이 */
|
||||
.m02-labrow__opt {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.m02-labrow__row:has(+ .m02-labrow__optrow) td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.m02-labrow__optrow.is-open {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m02-labrow__optrow.is-open:hover,
|
||||
.m02-labrow__row.is-open:hover + .m02-labrow__optrow {
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
|
||||
/* 별칭 칸(44-6) — 칸 안 글상자 · 누르면 바로 고침 */
|
||||
.m02-labrow__alias {
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
.m02-labrow__aliasin {
|
||||
width: 100%;
|
||||
min-width: 6rem;
|
||||
padding: 2px var(--spacing-4);
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.m02-labrow__aliasin:hover,
|
||||
.m02-labrow__aliasin:focus {
|
||||
border-color: var(--color-primary);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* 총합계 줄 전체 합계 칸(44-6) — 테두리 · 색 */
|
||||
.m02-labrow__sum .m02-labrow__total {
|
||||
border: 2px solid var(--color-primary);
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 입력 모달 */
|
||||
.m02-labin {
|
||||
width: min(40rem, calc(100vw - 32px));
|
||||
@@ -234,6 +287,17 @@
|
||||
order: 3;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 옵션 값 줄 — 한 줄 통째 */
|
||||
.m02-labrow__optrow .m02-labrow__no,
|
||||
.m02-labrow__sum .m02-labrow__alias {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m02-labrow .m02-labrow__opt {
|
||||
grid-column: 1 / -1;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 수량산출 칸(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 · 누르면 손 입력 · 엑셀에서 고르기 */
|
||||
|
||||
@@ -121,6 +121,8 @@ export type LaborQty = { 손: number } | null;
|
||||
/** 담은 로직 한 줄 — 입력값 = 그 로직 입력 이름만(빈 값 = 자동값) */
|
||||
export interface LaborRow {
|
||||
로직: string;
|
||||
/** 사용자가 지은 이름(44-6 · 빈 것 = null) */
|
||||
별칭?: string | null;
|
||||
메모: string | null;
|
||||
입력값: Record<string, string | number>;
|
||||
수량: LaborQty;
|
||||
@@ -153,6 +155,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
단위: d.단위 ?? null,
|
||||
담은로직: (d.담은로직 ?? []).map((r) => ({
|
||||
로직: r.로직,
|
||||
별칭: r.별칭 ?? null,
|
||||
메모: r.메모 ?? null,
|
||||
입력값: r.입력값 ?? {},
|
||||
수량: r.수량 ?? null,
|
||||
|
||||
@@ -43,6 +43,7 @@ import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { keepKnown } from "./M02_Labor_Values_Model";
|
||||
import { pickMaterial } from "./M02_Material_Pick";
|
||||
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
|
||||
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css";
|
||||
import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css";
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
@@ -135,7 +136,13 @@ export function mountLaborDetail(
|
||||
{ rows: cur.담은로직, logics, calc, fail, cells: cells() },
|
||||
// 읽기만이면 순서 · 빼기 없음(줄 누름 = 보기 모달은 둠)
|
||||
edit
|
||||
? { open: openRow, qty: openQty, ...(opt.card ? {} : { move }), remove: removeRow }
|
||||
? {
|
||||
open: openRow,
|
||||
qty: openQty,
|
||||
...(opt.card ? {} : { move }),
|
||||
remove: removeRow,
|
||||
alias: setAlias,
|
||||
}
|
||||
: { open: openRow },
|
||||
),
|
||||
);
|
||||
@@ -262,6 +269,11 @@ export function mountLaborDetail(
|
||||
cur.담은로직.splice(i, 1);
|
||||
changed();
|
||||
}
|
||||
/** 별칭(44-6) — 화면 캐시만(저장은 [저장]) · 금액과 무관이라 다시 셈 · 다시 그림 없음 */
|
||||
function setAlias(i: number, value: string | null): void {
|
||||
cur.담은로직[i].별칭 = value;
|
||||
opt.onChanged(dirty());
|
||||
}
|
||||
function removeMat(i: number): void {
|
||||
cur.자재.splice(i, 1);
|
||||
changed();
|
||||
@@ -311,7 +323,7 @@ export function mountLaborDetail(
|
||||
openLogicPicker((x) => {
|
||||
if (cur.담은로직.some((r) => r.로직 === x.키))
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
cur.담은로직.push({ 로직: x.키, 메모: null, 입력값: {}, 수량: null });
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null });
|
||||
void loadLogics().then(() => changed());
|
||||
}),
|
||||
});
|
||||
@@ -330,15 +342,27 @@ export function mountLaborDetail(
|
||||
}),
|
||||
});
|
||||
|
||||
const unit = createInputField({ type: "text", label: tc("Labor_Unit") });
|
||||
unit.input.value = cur.단위 ?? "";
|
||||
unit.input.readOnly = !edit;
|
||||
unit.input.maxLength = 20;
|
||||
unit.input.setAttribute("data-labor-field", "단위");
|
||||
unit.input.addEventListener("input", () => {
|
||||
cur.단위 = unit.input.value.trim() || null;
|
||||
opt.onChanged(dirty());
|
||||
});
|
||||
// 단위 = 표준 단위 목록에서 고르기(지금 값이 목록에 없어도 선택지로 둠) · 저장은 [저장]
|
||||
const unit = {
|
||||
root: el("div", { className: "ui-field", attrs: { "data-labor-field": "단위" } }),
|
||||
};
|
||||
const unit0 = cur.단위;
|
||||
const drawUnit = (): void =>
|
||||
unit.root.replaceChildren(
|
||||
el("label", { className: "ui-field__label", text: tc("Labor_Unit") }),
|
||||
createUnitPick({
|
||||
value: cur.단위 || NO_UNIT,
|
||||
extra: [unit0, cur.단위].filter((u): u is string => !!u),
|
||||
readOnly: !edit,
|
||||
ariaLabel: tc("Labor_Unit"),
|
||||
onPick: (u) => {
|
||||
cur.단위 = u === NO_UNIT ? null : u;
|
||||
opt.onChanged(dirty());
|
||||
drawUnit();
|
||||
},
|
||||
}),
|
||||
);
|
||||
drawUnit();
|
||||
const origin = createInputField({ type: "text", label: tc("Labor_Origin") });
|
||||
origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self");
|
||||
origin.input.readOnly = true;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Labor_Rows.ts
|
||||
* 일위대가 담은 로직 표 · 자재목록 표(39-3) — 상세 · 보기 갈래(면적산출 오른쪽 패널 · 카드)가 같이 씀.
|
||||
* 담은 로직 = 차례 · 로직(키 · 이름 + 작은 글 「지역 서울 · 규격 25mm · 수량 B6 = 12.5㎡」 · 「빈 값 n」 붉게) ·
|
||||
* 결과 단위 · 금액 칸 넷(노무비 · 재료비 · 경비 · 합계 · 칸마다 위 기준 작게 / 아래 반영) · 맨 아래 합계 줄.
|
||||
* 담은 로직 = 차례 · 로직(키 · 이름 + 「빈 값 n」 붉게) · 별칭(44-6) · 수량산출(「12.5(B6)」) ·
|
||||
* 결과 단위 · 금액 칸 넷(노무비 · 재료비 · 경비 · 합계 · 칸마다 위 기준 작게 / 아래 반영) ·
|
||||
* 옵션 값(「지역 서울 · 규격 25mm」)은 줄 아래 이름 ~ 합계에 걸친 한 줄 · 맨 아래 합계 줄(전체 합계 강조).
|
||||
* 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3).
|
||||
* 금액은 calc 답(없으면 「…」).
|
||||
* 줄 누름 · 순서 단추는 `on` 이 있을 때만(보기 갈래 = 없음).
|
||||
@@ -31,11 +32,14 @@ const HEAD: Record<(typeof COSTS)[number], Parameters<typeof tc>[0]> = {
|
||||
계: "Prev_Sum",
|
||||
};
|
||||
|
||||
/** 옵션 값 줄이 걸치는 칸 수 — 이름 · 별칭 · 수량산출 · 결과 단위 · 금액 넷 */
|
||||
const SPAN = 4 + COSTS.length;
|
||||
|
||||
const num = (v: number): string =>
|
||||
Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
||||
|
||||
/** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」(칸 = 그 알약을 이은 면적산출 칸) ·
|
||||
* 이은 칸이 비었으면 「수량 A1 = 빈 칸」(수량 없음 · 서버와 같음) · 아니면 빈 글 */
|
||||
/** 수량 글 — 「수량 손 3 개소」 · 「수량 12.5 ㎡(B6)」(칸 = 그 알약을 이은 면적산출 칸 · 44-6) ·
|
||||
* 이은 칸이 비었으면 「수량 빈 칸(A1)」(수량 없음 · 서버와 같음) · 아니면 빈 글 */
|
||||
export function qtyText(
|
||||
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
|
||||
unit: string,
|
||||
@@ -44,12 +48,12 @@ export function qtyText(
|
||||
if (!line) return "";
|
||||
if (line.수량 === null || line.수량 === undefined) {
|
||||
const at = line.알약 ? cells.get(line.알약) : undefined;
|
||||
return at ? `${tc("Labor_Qty")} ${at} = ${tc("In_QtyEmpty")}` : "";
|
||||
return at ? `${tc("Labor_Qty")} ${tc("In_QtyEmpty")}(${at})` : "";
|
||||
}
|
||||
const v = `${num(line.수량)}${unit ? ` ${unit}` : ""}`;
|
||||
if (line.수량출처 === "손") return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`;
|
||||
const cell = (line.알약 && cells.get(line.알약)) || tc("In_QtyCell");
|
||||
return `${tc("Labor_Qty")} ${cell} = ${v}`;
|
||||
return `${tc("Labor_Qty")} ${v}(${cell})`;
|
||||
}
|
||||
|
||||
/** 결과단위 기준 수량 — 분모 앞 수 · 분모(「10 ㎡」 · 「1 ㎥」) · 분모 없으면 빈 글 */
|
||||
@@ -60,7 +64,7 @@ export function baseQtyText(
|
||||
return `${num(line.배 || 1)} ${line.분모}`;
|
||||
}
|
||||
|
||||
/** 지금 수량 짧은 글 — 「손 5 ㎥」 · 「B6 = 12.5」 · 「B6 = 빈 칸」 · 없으면 「—」 */
|
||||
/** 지금 수량 짧은 글 — 「손 5 ㎥」 · 「12.5(B6)」 · 「빈 칸(B6)」 · 없으면 「—」 */
|
||||
export function qtyShort(
|
||||
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
|
||||
unit: string,
|
||||
@@ -68,13 +72,13 @@ export function qtyShort(
|
||||
): string {
|
||||
const at = line?.알약 ? cells.get(line.알약) : undefined;
|
||||
if (!line || line.수량 === null || line.수량 === undefined)
|
||||
return at ? `${at} = ${tc("In_QtyEmpty")}` : "—";
|
||||
return at ? `${tc("In_QtyEmpty")}(${at})` : "—";
|
||||
if (line.수량출처 === "손")
|
||||
return `${tc("Labor_Hand")} ${num(line.수량)}${unit ? ` ${unit}` : ""}`;
|
||||
return `${at ?? tc("In_QtyCell")} = ${num(line.수량)}`;
|
||||
return `${num(line.수량)}(${at ?? tc("In_QtyCell")})`;
|
||||
}
|
||||
|
||||
/** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「손 5 ㎥」 · 「B6 = 12.5」 · 「—」) ·
|
||||
/** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「손 5 ㎥」 · 「12.5(B6)」 · 「—」) ·
|
||||
* `open` 이 있으면 단추(줄 누름과 따로) */
|
||||
function qtyCell(
|
||||
line: LaborCalcLine | undefined,
|
||||
@@ -155,6 +159,42 @@ export interface RowHandlers {
|
||||
qty?: (i: number) => void;
|
||||
move?: (i: number, d: number) => void;
|
||||
remove?: (i: number) => void;
|
||||
/** 별칭 칸 고침(44-6 · 글자마다 · 빈 글 = null) — 없으면 글만 보임 */
|
||||
alias?: (i: number, value: string | null) => void;
|
||||
}
|
||||
|
||||
/** 별칭 칸(44-6) — 고칠 수 있으면 칸 안 글상자(누르면 바로 고침 · 줄 누름과 따로) · 아니면 글 · 비면 빈 칸 */
|
||||
function aliasCell(
|
||||
value: string | null | undefined,
|
||||
set: ((v: string | null) => void) | null,
|
||||
): HTMLElement {
|
||||
if (!set)
|
||||
return el("td", {
|
||||
className: "m02-labrow__alias",
|
||||
text: value ?? "",
|
||||
attrs: { "data-label": tc("Col_Alias") },
|
||||
});
|
||||
const input = el("input", {
|
||||
className: "m02-labrow__aliasin",
|
||||
attrs: {
|
||||
type: "text",
|
||||
maxlength: "40", // 서버 `ALIAS_MAX` 와 같음
|
||||
title: tc("Alias_Edit"),
|
||||
"aria-label": tc("Col_Alias"),
|
||||
"data-labor-alias": "",
|
||||
},
|
||||
});
|
||||
input.value = value ?? "";
|
||||
input.addEventListener("click", (e) => e.stopPropagation()); // 줄 누름(입력 모달)과 따로
|
||||
input.addEventListener("input", () => set(input.value.trim() || null));
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") input.blur();
|
||||
});
|
||||
return el("td", {
|
||||
className: "m02-labrow__alias",
|
||||
attrs: { "data-label": tc("Col_Alias") },
|
||||
children: [input],
|
||||
});
|
||||
}
|
||||
|
||||
export interface LogicTableData {
|
||||
@@ -175,7 +215,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
const tools = !!(move || remove);
|
||||
const lineOf = (key: string): LaborCalcLine | undefined =>
|
||||
data.calc?.줄.find((l) => l.로직 === key);
|
||||
const trs = data.rows.map((row, i) => {
|
||||
const trs = data.rows.flatMap((row, i) => {
|
||||
const logic = data.logics.get(row.로직);
|
||||
const line = lineOf(row.로직);
|
||||
const unit = logic?.결과단위 ?? "";
|
||||
@@ -201,7 +241,6 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
children: [
|
||||
el("span", { className: "m02-labrow__key", text: row.로직 }),
|
||||
el("strong", { text: logic?.이름 ?? row.로직 }),
|
||||
...(note ? [el("small", { className: "m02-labrow__note", text: note })] : []),
|
||||
...(values.empty
|
||||
? [
|
||||
el("small", {
|
||||
@@ -221,6 +260,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
aliasCell(row.별칭, on?.alias ? (v) => on.alias?.(i, v) : null),
|
||||
qtyCell(line, data.cells, on?.qty ? () => on.qty?.(i) : null),
|
||||
el("td", { className: "m02-labrow__unit", text: unit }),
|
||||
...COSTS.map((c) => moneyCell(line?.기준?.[c], line?.반영?.[c], wait, tc(HEAD[c]))),
|
||||
@@ -240,7 +280,23 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
],
|
||||
});
|
||||
if (on) tr.addEventListener("click", () => on.open(i));
|
||||
return tr;
|
||||
if (!note) return [tr];
|
||||
// 옵션 값 줄(44-6) — 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글은 title) · 행 높이 안 커짐
|
||||
const opt = el("tr", {
|
||||
className: on ? "m02-labrow__optrow is-open" : "m02-labrow__optrow",
|
||||
attrs: { "data-labor-opt": row.로직 },
|
||||
children: [
|
||||
el("td", { className: "m02-labrow__no", text: "" }),
|
||||
el("td", {
|
||||
className: "m02-labrow__opt",
|
||||
text: note,
|
||||
attrs: { colspan: String(SPAN), title: note },
|
||||
}),
|
||||
...(tools ? [el("td", { text: "" })] : []),
|
||||
],
|
||||
});
|
||||
if (on) opt.addEventListener("click", () => on.open(i));
|
||||
return [tr, opt];
|
||||
});
|
||||
const sum = (part: Partial<LaborCost> | undefined, c: (typeof COSTS)[number]): number | null =>
|
||||
part?.[c] ?? null;
|
||||
@@ -251,11 +307,19 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
children: [
|
||||
el("td", { className: "m02-labrow__no", text: "" }),
|
||||
el("td", { className: "m02-labrow__name", text: tc("Prev_Sum") }),
|
||||
el("td", { className: "m02-labrow__alias", text: "" }),
|
||||
el("td", { className: "m02-labrow__qty", text: "" }),
|
||||
el("td", { className: "m02-labrow__unit", text: "" }),
|
||||
...COSTS.map((c) =>
|
||||
moneyCell(sum(data.calc?.기준합, c), sum(data.calc?.반영합, c), wait, tc(HEAD[c])),
|
||||
),
|
||||
...COSTS.map((c) => {
|
||||
const td = moneyCell(
|
||||
sum(data.calc?.기준합, c),
|
||||
sum(data.calc?.반영합, c),
|
||||
wait,
|
||||
tc(HEAD[c]),
|
||||
);
|
||||
if (c === "계") td.classList.add("m02-labrow__total"); // 전체 합계 강조(44-6)
|
||||
return td;
|
||||
}),
|
||||
...(tools ? [el("td", { text: "" })] : []),
|
||||
],
|
||||
}),
|
||||
@@ -264,6 +328,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
const heads = [
|
||||
tc("Col_Order"),
|
||||
tc("Col_Name"),
|
||||
tc("Col_Alias"),
|
||||
tc("Labor_QtyCol"),
|
||||
tc("Col_Unit"),
|
||||
...COSTS.map((c) => tc(HEAD[c])),
|
||||
|
||||
@@ -27,6 +27,8 @@ const TEXT = {
|
||||
Logics_Title: ["단가산출 로직", "Unit-price logics"],
|
||||
Col_Order: ["차례", "No."],
|
||||
Col_Name: ["이름", "Name"],
|
||||
Col_Alias: ["별칭", "Alias"],
|
||||
Alias_Edit: ["눌러서 별칭 고침", "Click to edit alias"],
|
||||
Col_Unit: ["결과 단위", "Result unit"],
|
||||
Col_Move: ["순서", "Move"],
|
||||
Up: ["올리기", "Up"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Layer.ts
|
||||
* M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시
|
||||
* `#/m02-master-template?open=구조물조합|일위대가|옵션|도면&root=master|company|personal&owner=…&company=…`
|
||||
* `#/m02-master-template?open=구조물조합|옵션|도면&root=master|company|personal&owner=…&company=…`
|
||||
* (옛 `?layer=` 도 root 로 읽음) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) ·
|
||||
* 회사 없음 안내 · 회사용 읽기만일 때 [개인용으로 복사] 막대.
|
||||
* ========================================================================== */
|
||||
@@ -16,7 +16,7 @@ import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_A
|
||||
export type ManageLayer = "system" | "company" | "personal";
|
||||
/** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */
|
||||
export type Root = "master" | "company" | "personal";
|
||||
export type OpenTarget = "구조물조합" | "일위대가" | "옵션" | "도면";
|
||||
export type OpenTarget = "구조물조합" | "옵션" | "도면";
|
||||
|
||||
export interface Entry {
|
||||
root: Root;
|
||||
@@ -27,7 +27,7 @@ export interface Entry {
|
||||
company: string | null;
|
||||
}
|
||||
|
||||
const OPENS: OpenTarget[] = ["구조물조합", "일위대가", "옵션", "도면"];
|
||||
const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면"];
|
||||
|
||||
export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root);
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
root.innerHTML = "";
|
||||
root.append(layout);
|
||||
void main.open(home);
|
||||
// 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 · 일위대가는 구조물 조합 하위(세 층) ·
|
||||
// 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 ·
|
||||
// 옵션 화면이 뒤에 열리면 편집 칸 차례(`seq`)가 늦게 온 집계표를 버림
|
||||
if (entry.open !== "구조물조합") side.reveal(entry.open);
|
||||
void tree.refresh();
|
||||
|
||||
@@ -94,7 +94,7 @@ export interface SideHandle {
|
||||
/** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */
|
||||
paint: () => void;
|
||||
/** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */
|
||||
reveal: (target: "옵션" | "도면" | "일위대가") => void;
|
||||
reveal: (target: "옵션" | "도면") => void;
|
||||
}
|
||||
|
||||
/** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */
|
||||
@@ -414,14 +414,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
}
|
||||
}
|
||||
|
||||
const reveal = (target: "옵션" | "도면" | "일위대가"): void => {
|
||||
// 일위대가 = 구조물 조합(처음부터 펼침) 안 일위대가 하위를 펼치고 「전체 항목」
|
||||
if (target === "일위대가") {
|
||||
columnSections[COLUMN_KINDS.findIndex((k) => k.kind === "labor")].classList.remove(
|
||||
"is-collapsed",
|
||||
);
|
||||
return void selectAll("labor");
|
||||
}
|
||||
const reveal = (target: "옵션" | "도면"): void => {
|
||||
extras?.[target].classList.remove("is-collapsed");
|
||||
if (target === "옵션") options.body.querySelector<HTMLElement>("button")?.click();
|
||||
};
|
||||
|
||||
@@ -70,31 +70,28 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_시스템_관리자_카드_다섯_고른_회사_해시():
|
||||
def test_시스템_관리자_카드_넷_고른_회사_해시():
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
|
||||
]
|
||||
# 회사가 없으면 해시에 안 실음
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == (
|
||||
f"{M02}?open=일위대가&root=master"
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_회사_관리자_개인_카드_둘():
|
||||
def test_회사_관리자_개인_카드_하나():
|
||||
company = [
|
||||
["B01_Dashboard_CardUnitCombo", WRITE_CO, f"{M02}?open=일위대가&root=company"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"],
|
||||
]
|
||||
assert _cards({"role": "ADMIN", "is_master": False}, 3) == company # 회사 인자 안 실음
|
||||
assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정
|
||||
assert _cards({"role": "USER", "is_master": False}) == [
|
||||
["B01_Dashboard_CardUnitCombo", READ_CO, f"{M02}?open=일위대가&root=personal"],
|
||||
["B01_Dashboard_CardStructCombo", READ_CO, f"{M02}?open=구조물조합&root=personal"],
|
||||
]
|
||||
|
||||
|
||||
@@ -80,3 +80,13 @@ def test_일위대가_카드는_상세_카드_갈래():
|
||||
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
|
||||
assert "const tools = !!(move || remove);" in rows and "compactTable" not in rows
|
||||
assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1]
|
||||
|
||||
|
||||
def test_일위대가_상세_단위는_표준_단위_고르기():
|
||||
"""44-7 — 상세 머리 「단위」 = 표준 단위 목록(UnitPick) · 지금 값이 목록에 없어도 선택지 ·
|
||||
고르면 캐시만(저장은 상세 [저장]) · 「-」 = 없음."""
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert 'import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";' in detail
|
||||
assert "extra: [unit0, cur.단위].filter((u): u is string => !!u)," in detail
|
||||
assert "cur.단위 = u === NO_UNIT ? null : u;" in detail
|
||||
assert "readOnly: !edit," in detail and "maxLength = 20" not in detail
|
||||
|
||||
@@ -39,7 +39,7 @@ def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
|
||||
for cid in ("rv", "c1"):
|
||||
store.create_structure(cid)
|
||||
got = store.read("labor", "rv")
|
||||
logic = [{"로직": TEN, "입력값": {}, "수량": {"손": 2}}]
|
||||
logic = [{"로직": TEN, "별칭": " 돌붙임 ", "입력값": {}, "수량": {"손": 2}}]
|
||||
store.write("labor", "rv", got["판"], {**got["문서"], "단위": "개소", "담은로직": logic})
|
||||
personal = layers.personal_dir(7, 42)
|
||||
items_store.ensure(personal)
|
||||
@@ -71,6 +71,7 @@ def test_같은_층_일위대가_하나_복사_번호_열은_받는_쪽(world):
|
||||
c1 = _labor(system, "c1")
|
||||
assert (c1["열"], c1["일위대가번"]) == ("c1", before["일위대가번"])
|
||||
assert (c1["단위"], c1["담은로직"][0]["로직"]) == ("개소", TEN)
|
||||
assert c1["담은로직"][0]["별칭"] == "돌붙임" # 별칭도 같이(44-6 · 앞뒤 빈칸 뗌)
|
||||
assert c1["계산값"]["반영합"]["계"] > 0 # 서버가 다시 셈
|
||||
|
||||
|
||||
@@ -80,6 +81,7 @@ def test_다른_층에서_내_개인으로_저장_뒤에도_덮어씀(world):
|
||||
for _ in range(2): # 저장된 데이터도 다시 바꿈
|
||||
assert client.post(url).status_code == 200
|
||||
assert _labor(personal, "a")["담은로직"][0]["로직"] == TEN
|
||||
assert _labor(personal, "a")["담은로직"][0]["별칭"] == "돌붙임"
|
||||
area = URL.format("personal", "area", "a") + "?from=rv&from_layer=system"
|
||||
assert client.post(area).status_code == 200
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것():
|
||||
)
|
||||
)
|
||||
assert got["담은로직"] == [
|
||||
{"로직": STONE, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None}
|
||||
{"로직": STONE, "별칭": None, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None}
|
||||
]
|
||||
assert got["자재"][0]["이름"] == "치 즐(0.2㎥)" and got["자재"][0]["수량"] == {"손": 2}
|
||||
assert (got["종류"], got["단위"], got["본뜬"]) == ("일위대가", "개소", None)
|
||||
@@ -47,6 +47,8 @@ def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것():
|
||||
_doc(담은로직=[{"로직": "GF999999"}]), # 없는 로직
|
||||
_doc(담은로직=[{"로직": STONE, "수량": {"손": "열"}}]), # 수 아님
|
||||
_doc(담은로직=[{"로직": STONE, "수량": 3}]), # 꼴 틀림
|
||||
_doc(담은로직=[{"로직": STONE, "별칭": 3}]), # 별칭 글 아님(44-6)
|
||||
_doc(담은로직=[{"로직": STONE, "별칭": "가" * 41}]), # 별칭 40자 넘음
|
||||
_doc(자재=[{"코드": "LB000001"}]), # 자재품목 아님
|
||||
_doc(자재=[{"코드": MAT}, {"코드": MAT}]), # 같은 자재 두 번
|
||||
"문서 아님",
|
||||
@@ -58,6 +60,13 @@ def test_틀린_문서는_400(bad):
|
||||
assert e.value.status == 400
|
||||
|
||||
|
||||
def test_별칭은_앞뒤_빈칸_떼고_빈_글은_null():
|
||||
got = labor.clean(
|
||||
_doc(담은로직=[{"로직": STONE, "별칭": " 돌붙임 "}, {"로직": TEN, "별칭": " "}])
|
||||
)
|
||||
assert [r["별칭"] for r in got["담은로직"]] == ["돌붙임", None]
|
||||
|
||||
|
||||
def test_빈_일위대가도_됨():
|
||||
got = labor.server(_doc())
|
||||
assert got["계산값"]["기준합"]["계"] == 0 and got["계산값"]["반영합"]["계"] == 0
|
||||
|
||||
@@ -41,10 +41,10 @@ def test_하위_차례_전체_항목_이름():
|
||||
assert '| "labor";' in api and "일위대가번: string;" in api
|
||||
assert "onAll?: (kind: AllKind) => void;" in side # 화면은 ItemList(39-5) · 페이지가 이음
|
||||
assert "if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return;" in side
|
||||
assert 'return void selectAll("labor");' in side # 해시 open=일위대가
|
||||
assert 'selectAll("labor")' not in side # 해시 open=일위대가 길 없앰(44-7)
|
||||
locale = (ROOT / "ui_template" / "ui_template_locale_m2.ts").read_text("utf-8")
|
||||
assert 'M02_KindLabor: ["일위대가"' in locale and 'M02_ItemsAll: ["전체 항목"' in locale
|
||||
assert 'B01_Dashboard_CardUnitCombo: ["일위대가",' in locale
|
||||
assert "B01_Dashboard_CardUnitCombo" not in locale # 대시보드 일위대가 카드 없앰(44-7)
|
||||
css = _text("M02_MasterTemplete_UI_Style.css")
|
||||
assert ".m02-side__rule {" in css and "border-top: 1px solid var(--color-border);" in css
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ console.log(JSON.stringify([
|
||||
|
||||
|
||||
def test_수량_글은_이은_칸_주소를_보임() -> None:
|
||||
"""`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 B6 = 20 ㎡」 — 없으면 「면적산출 칸」."""
|
||||
"""`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 20 ㎡(B6)」 — 없으면 「면적산출 칸」."""
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node 없음")
|
||||
@@ -154,11 +154,11 @@ def test_수량_글은_이은_칸_주소를_보임() -> None:
|
||||
assert out.returncode == 0, out.stderr
|
||||
got = json.loads(out.stdout)
|
||||
# 수량산출 열(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 짧은 글
|
||||
assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "B6 = 12.5", "B6 = 빈 칸", "—"]
|
||||
assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "12.5(B6)", "빈 칸(B6)", "—"]
|
||||
assert got[7:] == [
|
||||
"수량 B6 = 20 ㎡",
|
||||
"수량 면적산출 칸 = 20 ㎡",
|
||||
"수량 20 ㎡(B6)", # 44-6 — 수량(칸 주소)
|
||||
"수량 20 ㎡(면적산출 칸)",
|
||||
"수량 손 입력 3 ㎡",
|
||||
"수량 B6 = 빈 칸", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음)
|
||||
"수량 빈 칸(B6)", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음)
|
||||
"",
|
||||
]
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_주소_인자를_라우트에서_뗌():
|
||||
# 진입 해시 `?open=&root=&owner=`(PLAN 27-1d) · 옛 `?layer=` 도 root 로
|
||||
assert 'query.get("root") ?? query.get("layer")' in layer
|
||||
assert 'raw === "company" || raw === "personal" ? raw : "master"' in layer
|
||||
assert '["구조물조합", "일위대가", "옵션", "도면"]' in layer
|
||||
assert '["구조물조합", "옵션", "도면"]' in layer # 일위대가 = 구조물 조합 안(44-7)
|
||||
picker = _text(ROOT / "ui_template" / "ui_template_item_picker.ts")
|
||||
assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 환경 검증 프로젝트(OWNERS.md) · 로그인됨 · 계획노선 편집 닫힘
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 하는 일: [계획노선 편집] 열기 → 첫 측점 라벨 누름(앱 모듈의 라벨 상자 자리) → 횡단 판 글자 잼 → [취소]로 닫음
|
||||
* 기대 수치:
|
||||
* - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 5.5px(옛 11px 의 절반)
|
||||
* - station.fontSize = title.fontSize(측점 표기 = 제목 「횡단」 크기)
|
||||
* - 바꾸는 값 없음(취소로 닫음 · 노선 안 고침)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(200);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const btn = (text) =>
|
||||
[...document.querySelectorAll("button")].find(
|
||||
(b) => b.offsetParent && b.textContent.trim() === text,
|
||||
);
|
||||
btn("계획노선 편집")?.click();
|
||||
const canvas = await until(() =>
|
||||
[...document.querySelectorAll("canvas")].find(
|
||||
(c) => c.offsetParent && c.closest("[class*='routeedit']"),
|
||||
),
|
||||
);
|
||||
if (!canvas) return JSON.stringify({ ok: false, why: "편집 캔버스 없음" });
|
||||
// 앱이 실제로 부른 그 모듈 주소(@fs/<폴더>/…) — 같은 주소라야 라벨 상자 WeakMap 을 같이 씀
|
||||
const url = performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.find((n) => n.includes("B05_Profile_UI_RouteEdit_Input.ts"));
|
||||
const input = await import(url);
|
||||
const boxes = await until(() => {
|
||||
const b = input.stationLabelBoxes(canvas);
|
||||
return b.length ? b : null;
|
||||
});
|
||||
if (!boxes) return JSON.stringify({ ok: false, why: "측점 라벨 상자 없음" });
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const box = boxes[Math.floor(boxes.length / 2)];
|
||||
const at = { clientX: rect.left + box.x, clientY: rect.top + box.y, button: 0, bubbles: true };
|
||||
canvas.dispatchEvent(new PointerEvent("pointerdown", at));
|
||||
canvas.dispatchEvent(new PointerEvent("pointerup", at));
|
||||
const pane = await until(() =>
|
||||
[...document.querySelectorAll(".b05-routeedit__cross")].find((p) =>
|
||||
p.querySelector(".b05-routeedit__cross-key"),
|
||||
),
|
||||
);
|
||||
if (!pane) return JSON.stringify({ ok: false, why: "횡단 판 안 뜸", chainage: box.chainage });
|
||||
const fs = (el) => el && getComputedStyle(el).fontSize;
|
||||
const keys = [...pane.querySelectorAll(".b05-routeedit__cross-key")].map((t) => ({
|
||||
text: t.textContent.slice(0, 20),
|
||||
fontSize: fs(t),
|
||||
h: +t.getBoundingClientRect().height.toFixed(1),
|
||||
}));
|
||||
const title = pane.querySelector(".b05-routeedit__cross-title");
|
||||
const station = pane.querySelector(".b05-routeedit__cross-station");
|
||||
const out = {
|
||||
keys,
|
||||
title: { text: title.textContent, fontSize: fs(title) },
|
||||
station: { text: station.textContent, fontSize: fs(station) },
|
||||
};
|
||||
out.ok =
|
||||
keys.length === 3 &&
|
||||
keys.every((k) => k.fontSize === "5.5px") &&
|
||||
out.title.fontSize === out.station.fontSize;
|
||||
btn("취소")?.click();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
/* 44-7 — ① M02 일위대가 상세 머리 「단위」 = 표준 단위 드롭다운 ② 대시보드 「시스템 설정」 일위대가 카드 없음
|
||||
* 시작 화면: 아무 화면 · 시스템 관리자 로그인 · M02 마스터 층에 구조물 열 하나 이상
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안)
|
||||
* 기대 수치:
|
||||
* - cards = ["마스터 데이터", "구조물 조합", "구조물 옵션", "도면 양식"] · hasLabor = false
|
||||
* - pick = 1(단추) · textInput = 0(손글 칸 없음) · options > 10 · hasNow = true(지금 값이 선택지에 있음)
|
||||
* - 다른 단위 고름 → after = 고른 값 · dirty = true(다른 열로 가려면 버림 물음 · 「취소」)
|
||||
* - 처음 값 다시 고름 → back = 처음 값 · dirtyBack = false(물음 없이 옮겨 감 · 저장 안 함 · 바꾼 값 되돌림)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 6000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const out = {};
|
||||
// ② 대시보드 시스템 설정 카드
|
||||
location.hash = "#/b01-account";
|
||||
await until(() => document.querySelector(".b01-dashboard__setting-title"));
|
||||
out.cards = [...document.querySelectorAll(".b01-dashboard__setting-title")].map(
|
||||
(e) => e.textContent,
|
||||
);
|
||||
out.hasLabor = out.cards.includes("일위대가");
|
||||
// ① M02 일위대가 상세(좌측 일위대가 하위 첫 줄)
|
||||
location.hash = "#/m02-master-template?open=구조물조합&root=master";
|
||||
// 하위 줄은 다시 그려짐 — 누를 때마다 새로 찾음
|
||||
const row = () =>
|
||||
document
|
||||
.querySelector('[data-all="labor"]')
|
||||
?.parentElement?.querySelector(".m02-side__item:not([data-all])");
|
||||
if (!(await until(row))) return JSON.stringify({ ...out, error: "일위대가 하위 없음" });
|
||||
await w(500);
|
||||
row().click();
|
||||
const field = await until(() => document.querySelector('[data-labor-field="단위"]'));
|
||||
if (!field) return JSON.stringify({ ...out, error: "단위 칸 없음" });
|
||||
// 고침 표시 = 다른 열(하위 둘째 줄)로 갈 때 버림 물음이 뜨는지 — 뜨면 「취소」
|
||||
const asks = async () => {
|
||||
document
|
||||
.querySelector('[data-all="labor"]')
|
||||
.parentElement.querySelectorAll(".m02-side__item:not([data-all])")[1]
|
||||
.click();
|
||||
const ask = await until(() => document.querySelector(".ui-confirm"), 1500);
|
||||
[...(ask?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "취소")?.click();
|
||||
await w(200);
|
||||
return !!ask;
|
||||
};
|
||||
const btn = () => document.querySelector('[data-labor-field="단위"] .m02-df-unitpick > button');
|
||||
const items = async () => {
|
||||
btn().click();
|
||||
return until(() => {
|
||||
const li = document.querySelectorAll('[data-labor-field="단위"] .m02-df-unitpick__item');
|
||||
return li.length ? [...li] : null;
|
||||
});
|
||||
};
|
||||
out.pick = document.querySelectorAll('[data-labor-field="단위"] .m02-df-unitpick').length;
|
||||
out.textInput = field.querySelectorAll("input[type=text]:not(.m02-df-unitpick__filter)").length;
|
||||
const u0 = btn().textContent;
|
||||
out.before = u0;
|
||||
const list = await items();
|
||||
out.options = list.length;
|
||||
out.hasNow = list.some((b) => b.classList.contains("is-now"));
|
||||
const other = list.find((b) => !b.classList.contains("is-now") && !b.textContent.startsWith("-"));
|
||||
other.click();
|
||||
await w(200);
|
||||
out.after = btn().textContent;
|
||||
out.dirty = await asks();
|
||||
// 되돌림 — 처음 값(없음이면 「-」)
|
||||
const again = await items();
|
||||
again.find((b) => (u0 === "-" ? b.textContent.startsWith("-") : b.textContent === u0)).click();
|
||||
await w(200);
|
||||
out.back = btn().textContent;
|
||||
out.dirtyBack = await asks();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -4,7 +4,6 @@
|
||||
* ========================================================================== */
|
||||
|
||||
export const ui_locales_m2 = {
|
||||
B01_Dashboard_CardUnitCombo: ["일위대가", "Unit costs"],
|
||||
B01_Dashboard_CardStructCombo: ["구조물 조합", "Structure combos"],
|
||||
B01_Dashboard_CardStructOption: ["구조물 옵션", "Structure options"],
|
||||
B01_Dashboard_CardDrawing: ["도면 양식", "Drawing templates"],
|
||||
|
||||
Reference in New Issue
Block a user