feat(M02): 50-3 ④ 로직 고르기 창 서고 탭 · 참조 줄 표 · 줄 상세
- [로직 추가] 창 탭 단가산출 · 단가승격 · 사전조합 — 서고 탭은 층 단추(위 층만 · 기본 = 가장 가까운 층) + 그 층 목록 · 고르면 참조 줄 더함(같은 참조 두 번 막음) - 담은 로직 표 참조 줄 — 줄 가름 lineId · 키 · 이름 · 단위 = 가리킨 문서 것 · 구분 = 참조 키 머리 · [옵션] 없음(넣을 값은 승격 때 고정) - 줄 상세 — 단가승격 = 원 로직 키 + 원 로직 표 + 기준(1 단위) · 사전조합 = 안 줄 목록 한 층(구분 · 이름 · 수량 · 금액 넷) + 기준 · 아래 반영 합 - 문서 정리 · 보기 패널 · 다이어그램이 참조 줄을 받음(다이어그램은 단가산출 줄만) - 시험: 승격 줄 원 로직 · 표 · 사전조합 안 줄 · ui_checks 50-3-4 시나리오(목록 · calc 는 탭 안 가짜 · 공유 마스터 안 씀) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
@@ -11,8 +11,9 @@ import {
|
||||
type LogicSummary,
|
||||
type SubBrief,
|
||||
} from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { LaborCalc } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import type { LaborCalc, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import {
|
||||
call,
|
||||
getActiveLayer,
|
||||
getActiveOwner,
|
||||
type ItemLogic,
|
||||
@@ -118,9 +119,11 @@ export const fetchLibLogicSubs = (): Promise<SubBrief[]> =>
|
||||
|
||||
/** 손 입력 수량 — `null` 이면 면적산출 알약 연결(칸)을 따름 · 손이 칸보다 앞섬 */
|
||||
export type LaborQty = { 손: number } | null;
|
||||
/** 담은 로직 한 줄 — 입력값 = 그 로직 입력 이름만(빈 값 = 자동값) */
|
||||
/** 담은 로직 한 줄 — 입력값 = 그 로직 입력 이름만(빈 값 = 자동값) ·
|
||||
* 참조 줄(단가승격 · 사전조합 · 50-3) = `참조` 있고 `로직` 빈 글 · 줄 가름은 `lineId` */
|
||||
export interface LaborRow {
|
||||
로직: string;
|
||||
참조?: LaborRef;
|
||||
/** 사용자가 지은 이름(44-6 · 빈 것 = null) */
|
||||
별칭?: string | null;
|
||||
메모: string | null;
|
||||
@@ -154,7 +157,8 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc {
|
||||
열: d.열 ?? column,
|
||||
단위: d.단위 ?? null,
|
||||
담은로직: (d.담은로직 ?? []).map((r) => ({
|
||||
로직: r.로직,
|
||||
로직: r.로직 ?? "",
|
||||
...(r.참조 ? { 참조: r.참조 } : {}),
|
||||
별칭: r.별칭 ?? null,
|
||||
메모: r.메모 ?? null,
|
||||
입력값: r.입력값 ?? {},
|
||||
@@ -223,6 +227,24 @@ export const calcLabor = (
|
||||
...(수량 ? { 수량 } : {}),
|
||||
});
|
||||
|
||||
/** 서고 한 줄(50-3) — 키 = SP-· SC-###### · 제목 · 단위 = 문서 것 */
|
||||
export interface LibraryRow {
|
||||
이름: string;
|
||||
제목?: string | null;
|
||||
단위?: string | null;
|
||||
}
|
||||
/** 서고 목록 — 그 층(주인) 단가승격 `promote` · 사전조합 `precombo` 문서 */
|
||||
export const fetchLibrary = async (
|
||||
ref: Omit<LaborRef, "키">,
|
||||
kind: "promote" | "precombo",
|
||||
): Promise<LibraryRow[]> => {
|
||||
const q = ref.주인 && ref.층 !== "system" ? `?owner=${enc(ref.주인)}` : "";
|
||||
const got = await call<{ 양식: (LibraryRow & { 종류: string })[] }>(
|
||||
`/layers/${ref.층}/templates${q}`,
|
||||
);
|
||||
return got.양식.filter((r) => r.종류 === kind);
|
||||
};
|
||||
|
||||
/** 담은 로직 입력 목록 + 자동값(입력 모달 · `M02_Labor_Inputs`) */
|
||||
export const fetchLogicInputs = (
|
||||
keys: string[],
|
||||
|
||||
@@ -36,6 +36,17 @@ export interface CalcLine {
|
||||
결과?: unknown;
|
||||
/** 그 로직 재료 · 노무 · 경비 줄(M01 시험계산 표와 같은 모양 · 50-4) */
|
||||
줄?: LogicLine[];
|
||||
/** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④) */
|
||||
이름?: string | null;
|
||||
원로직?: string | null;
|
||||
안?: {
|
||||
참조: LaborRef;
|
||||
수량: number;
|
||||
이름?: string | null;
|
||||
단위?: string | null;
|
||||
기준: Money | null;
|
||||
까닭?: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CalcMaterial {
|
||||
|
||||
@@ -22,19 +22,23 @@ import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
|
||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import { recalcWorkbook } from "../A00_Common/spreadsheet/spreadsheet_recalc";
|
||||
import {
|
||||
getActiveLayer,
|
||||
getActiveOwner,
|
||||
readTemplate,
|
||||
saveTemplate,
|
||||
type ItemLogic,
|
||||
type Layer,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { linkAreaCell } from "./M02_MasterTemplete_AreaCellPick";
|
||||
import { MONEY_KEYS, settle } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { lineId, MONEY_KEYS, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import {
|
||||
areaLinks,
|
||||
calcLabor,
|
||||
fetchLogicInputs,
|
||||
normalizeLabor,
|
||||
type LaborCalc,
|
||||
type LaborCalcLine,
|
||||
type LaborCost,
|
||||
type LaborDoc,
|
||||
type LaborQty,
|
||||
type LaborRow,
|
||||
@@ -47,7 +51,7 @@ import {
|
||||
import { openLaborInput, type GoArea } from "./M02_MasterTemplete_Labor_Input";
|
||||
import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { kindOf, 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";
|
||||
@@ -135,53 +139,116 @@ export function mountLaborDetail(
|
||||
if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root);
|
||||
}
|
||||
|
||||
const lineOf = (key: string) => calc?.줄.find((l) => l.로직 === key);
|
||||
const lineOf = (key: string) => calc?.줄.find((l) => lineId(l) === key);
|
||||
const matOf = (code: string) => calc?.자재.find((m) => m.코드 === code);
|
||||
|
||||
/** 고른 줄 상세(50-4) — M01 시험계산 표(재료 · 노무 · 경비 줄 + 기준 합) + 반영 합 · 고른 줄 없으면 비움 */
|
||||
/** 고른 줄 상세(50-4) — 단가산출 = M01 시험계산 표(재료 · 노무 · 경비 줄 + 기준 합) ·
|
||||
* 단가승격 = 원 로직 표 + 원 로직 키 + 기준(1 단위) · 사전조합 = 안 줄 목록 한 층 + 기준 ·
|
||||
* 셋 다 아래 반영 합 · 고른 줄 없으면 비움 */
|
||||
const lineDet = el("div", {
|
||||
className: "m02-labor__linedet",
|
||||
attrs: { "data-labor-linedet": "" },
|
||||
});
|
||||
const sums = (title: string, money: Partial<LaborCost> | null | undefined): HTMLElement[] => [
|
||||
el("h4", { text: title }),
|
||||
el("dl", {
|
||||
className: "m01-logic__sums",
|
||||
children: MONEY_KEYS.flatMap((k) => [
|
||||
el("dt", { text: k === "계" ? tc("Prev_Sum") : k }),
|
||||
el("dd", {
|
||||
className: "m01-logic__money",
|
||||
text: money ? formatMoney(money[k] ?? 0) : "—",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
];
|
||||
/** 사전조합 안 줄 — 구분 · 이름 · 수량 · 금액 넷(= 그 줄 1 단위 기준 × 수량) */
|
||||
const innerTable = (inner: NonNullable<LaborCalcLine["안"]>): HTMLElement =>
|
||||
el("table", {
|
||||
className: "m01-logic__grid",
|
||||
attrs: { "data-labor-inner": "" },
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [tc("Sub"), tc("Col_Name"), tc("Labor_Qty"), ...MONEY_KEYS].map((h) =>
|
||||
el("th", { text: h === "계" ? tc("Prev_Sum") : h }),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", {
|
||||
children: inner.map((x) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: kindOf({ 로직: "", 참조: x.참조 }) }),
|
||||
el("td", {
|
||||
text: `${x.참조.키} ${x.이름 ?? ""}${x.까닭 ? ` · ${x.까닭}` : ""}`.trim(),
|
||||
}),
|
||||
el("td", { text: `${x.수량} ${x.단위 ?? ""}`.trim() }),
|
||||
...MONEY_KEYS.map((k) =>
|
||||
el("td", {
|
||||
className: "m01-logic__money",
|
||||
text: x.기준 ? formatMoney(x.기준[k] * x.수량) : "—",
|
||||
}),
|
||||
),
|
||||
],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
function showLine(): void {
|
||||
if (!picked) return void lineDet.replaceChildren();
|
||||
const logic = logics.get(picked);
|
||||
const head = el("h4", {
|
||||
text: tc("Labor_LineDetail").replace("{value}", `${picked} ${logic?.이름 ?? ""}`.trim()),
|
||||
});
|
||||
const row = cur.담은로직.find((r) => lineId(r) === picked);
|
||||
const line = lineOf(picked);
|
||||
const logic = logics.get(picked);
|
||||
const name = row?.참조 ? (line?.이름 ?? "") : (logic?.이름 ?? "");
|
||||
const head = el("h4", {
|
||||
text: tc("Labor_LineDetail").replace("{value}", `${row?.참조?.키 ?? picked} ${name}`.trim()),
|
||||
});
|
||||
if (!line)
|
||||
return void lineDet.replaceChildren(
|
||||
head,
|
||||
el("p", { className: "m01-logic__muted", text: fail ?? "…" }),
|
||||
);
|
||||
const answer: CalcAnswer = line.까닭
|
||||
? { ok: false, reason: line.까닭 }
|
||||
: { ok: true, lines: line.줄, sums: line.기준 ?? undefined, result: line.결과, middle: {} };
|
||||
const stop: CalcAnswer | null = line.까닭 ? { ok: false, reason: line.까닭 } : null;
|
||||
const unit = `${tc("Labor_LineBase")} (1 ${line.분모 ?? ""})`.trim();
|
||||
const body: HTMLElement[] = !row?.참조
|
||||
? answerView(
|
||||
stop ?? {
|
||||
ok: true,
|
||||
lines: line.줄,
|
||||
sums: line.기준 ?? undefined,
|
||||
result: line.결과,
|
||||
middle: {},
|
||||
},
|
||||
false,
|
||||
(logic?.결과단위 ?? "").startsWith("원"),
|
||||
)
|
||||
: line.안
|
||||
? [
|
||||
innerTable(line.안),
|
||||
...(stop ? answerView(stop, false, true) : []),
|
||||
...sums(unit, line.기준),
|
||||
]
|
||||
: [
|
||||
el("p", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${tc("Labor_FromLogic")} ${line.원로직 ?? "—"}`,
|
||||
}),
|
||||
...answerView(stop ?? { ok: true, lines: line.줄, middle: {} }, false, true),
|
||||
...sums(unit, line.기준),
|
||||
];
|
||||
// 반영 = 기준 × 수량 ÷ 배(표 아래 줄과 같음) · 수량 없으면 「—」 · 돈 아닌 로직은 없음
|
||||
const applied = line.기준
|
||||
? [
|
||||
el("h4", { text: tc("Labor_LineApplied") }),
|
||||
el("dl", {
|
||||
className: "m01-logic__sums",
|
||||
children: MONEY_KEYS.flatMap((k) => [
|
||||
el("dt", { text: k === "계" ? tc("Prev_Sum") : k }),
|
||||
el("dd", {
|
||||
className: "m01-logic__money",
|
||||
text: line.반영 ? formatMoney(line.반영[k]) : "—",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const money = (logic?.결과단위 ?? "").startsWith("원");
|
||||
lineDet.replaceChildren(head, ...answerView(answer, false, money), ...applied);
|
||||
const applied = line.기준 ? sums(tc("Labor_LineApplied"), line.반영) : [];
|
||||
lineDet.replaceChildren(head, ...body, ...applied);
|
||||
}
|
||||
|
||||
const drawDg = (): void => {
|
||||
if (opt.card || !dgBox.open) return; // 카드 갈래엔 다이어그램 없음
|
||||
const data = buildLaborDiagram({
|
||||
rows: cur.담은로직,
|
||||
rows: cur.담은로직.filter((r) => !r.참조), // ponytail: 참조 줄은 다이어그램에 안 그림 — 그릴 때 카드 꼴 더함
|
||||
logics: [...logics.values()],
|
||||
calc,
|
||||
mats: cur.자재.length,
|
||||
@@ -323,7 +390,7 @@ export function mountLaborDetail(
|
||||
|
||||
/** 아직 안 받은 로직 입력 목록만 받음 */
|
||||
const loadLogics = async (): Promise<void> => {
|
||||
const want = cur.담은로직.map((r) => r.로직).filter((k) => !logics.has(k));
|
||||
const want = cur.담은로직.map((r) => r.로직).filter((k) => k && !logics.has(k));
|
||||
if (!want.length) return;
|
||||
try {
|
||||
const got = await fetchLogicInputs(want);
|
||||
@@ -342,7 +409,7 @@ export function mountLaborDetail(
|
||||
/** 줄 고름(47-5) — 하이라이트(표만 · 다이어그램 안 다시 그림) + 줄 상세(50-4) + 원문 패널(47-4) ·
|
||||
* 고른 줄 다시 누름 = 닫음 · 화면 캐시 안 바뀜 */
|
||||
function pick(i: number): void {
|
||||
const key = cur.담은로직[i].로직;
|
||||
const key = lineId(cur.담은로직[i]) ?? "";
|
||||
picked = picked === key ? null : key;
|
||||
for (const tr of logicHost.querySelectorAll<HTMLElement>("[data-labor-logic],[data-labor-opt]"))
|
||||
tr.classList.toggle("is-selected", (tr.dataset.laborLogic ?? tr.dataset.laborOpt) === picked);
|
||||
@@ -350,7 +417,7 @@ export function mountLaborDetail(
|
||||
showSource();
|
||||
}
|
||||
function removeRow(i: number): void {
|
||||
if (cur.담은로직[i].로직 === picked) picked = null;
|
||||
if (lineId(cur.담은로직[i]) === picked) picked = null;
|
||||
cur.담은로직.splice(i, 1);
|
||||
changed();
|
||||
}
|
||||
@@ -386,9 +453,9 @@ export function mountLaborDetail(
|
||||
/** 수량산출 칸 누름 — 로직 줄 */
|
||||
function openQty(i: number): void {
|
||||
const row = cur.담은로직[i];
|
||||
const line = lineOf(row.로직);
|
||||
const line = lineOf(lineId(row) ?? "");
|
||||
openQtyFor(
|
||||
`${row.로직} ${logics.get(row.로직)?.이름 ?? ""}`.trim(),
|
||||
`${row.참조?.키 ?? row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(),
|
||||
row,
|
||||
line,
|
||||
line?.분모 ?? "",
|
||||
@@ -401,6 +468,18 @@ export function mountLaborDetail(
|
||||
openQtyFor(`${mat.코드} ${mat.이름 ?? ""}`.trim(), mat, matOf(mat.코드), mat.단위 ?? "");
|
||||
}
|
||||
|
||||
/** 서고 탭(50-3 ④) — 참조 줄 더함(같은 참조 두 번 막음 · 위 층만은 서버도 막음) */
|
||||
const library = {
|
||||
layer: opt.layer,
|
||||
owner: opt.layer === getActiveLayer() ? getActiveOwner() : null,
|
||||
pick: (ref: LaborRef, name: string) => {
|
||||
const id = lineId({ 참조: ref });
|
||||
if (cur.담은로직.some((r) => lineId(r) === id))
|
||||
return showToast(tc("Labor_AlreadyRef").replace("{value}", `${ref.키} ${name}`), "error");
|
||||
cur.담은로직.push({ 로직: "", 참조: ref, 별칭: null, 메모: null, 입력값: {}, 수량: null });
|
||||
changed();
|
||||
},
|
||||
};
|
||||
const addLogic = createButton({
|
||||
label: tc("AddLogic"),
|
||||
variant: "filled",
|
||||
@@ -410,7 +489,7 @@ export function mountLaborDetail(
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null });
|
||||
void loadLogics().then(() => changed());
|
||||
}),
|
||||
}, library),
|
||||
});
|
||||
const addMat = createButton({
|
||||
label: tc("Labor_AddMat"),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Labor_Pick.ts
|
||||
* [로직 더하기] 창 — 구분 → 상세구분 → 찾기로 사용자가 로직을 하나씩 고름(자동 추천 없음)
|
||||
* [로직 더하기] 창 — 구분 → 상세구분 → 찾기로 사용자가 로직을 하나씩 고름(자동 추천 없음) ·
|
||||
* 탭 단가산출 · 단가승격 · 사전조합(50-3 ④) — 서고 탭은 층 단추(위 층만 · 짝 `M02_Labor_Ref.readable`) +
|
||||
* 그 층 목록 · 고르면 참조 {층 · 주인 · 키} · 프로젝트 · 카드엔 서고 탭 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -12,10 +14,16 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import type { LogicSummary, SubBrief } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
fetchLibLogics as fetchLogics,
|
||||
fetchLibLogicSubs as fetchLogicSubs,
|
||||
fetchLibrary,
|
||||
type LibraryRow,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { ownOwner } from "./M02_MasterTemplete_Layer";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
@@ -23,7 +31,33 @@ import "./M02_MasterTemplete_Labor.css";
|
||||
const opts = (values: string[]): { value: string; text: string }[] =>
|
||||
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v }));
|
||||
|
||||
export function openLogicPicker(done: (logic: LogicSummary) => void): void {
|
||||
type Scope = Omit<LaborRef, "키">;
|
||||
/** 서고 탭 — 일위대가가 있는 층 · 주인(null = 자기 층) · 고르면 참조 */
|
||||
export interface LibraryPick {
|
||||
layer: Layer;
|
||||
owner: string | null;
|
||||
pick: (ref: LaborRef, name: string) => void;
|
||||
}
|
||||
|
||||
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
|
||||
async function upper(layer: Layer, owner: string | null): Promise<Scope[]> {
|
||||
if (layer === "project") return [];
|
||||
const own =
|
||||
layer === "system"
|
||||
? null
|
||||
: (owner ?? ownOwner(layer, await fetchSessionUser().catch(() => null)));
|
||||
const out: Scope[] = [{ 층: "system", 주인: null }];
|
||||
if (own) out.push({ 층: "company", 주인: own.split("/")[0] });
|
||||
if (own && layer === "personal") out.push({ 층: "personal", 주인: own });
|
||||
return out;
|
||||
}
|
||||
const LAYER_TEXT = {
|
||||
system: "Lib_System",
|
||||
company: "Lib_Company",
|
||||
personal: "Lib_Personal",
|
||||
} as const;
|
||||
|
||||
export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: LibraryPick): void {
|
||||
const subs: SubBrief[] = [];
|
||||
let all: LogicSummary[] = [];
|
||||
const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") });
|
||||
@@ -76,6 +110,108 @@ export function openLogicPicker(done: (logic: LogicSummary) => void): void {
|
||||
detail.select.disabled = d.length === 0;
|
||||
});
|
||||
|
||||
/* 서고 탭(50-3 ④) — 탭 · 층 단추 · 목록 */
|
||||
const filters = el("div", {
|
||||
className: "m01-logic__filters",
|
||||
children: [
|
||||
sub.root,
|
||||
detail.root,
|
||||
searchField.root,
|
||||
createButton({ label: tc("Pick_Find"), variant: "filled", onClick: find }),
|
||||
],
|
||||
});
|
||||
const tabs = el("div", { className: "ui-btn-group", attrs: { "data-pick-tabs": "" } });
|
||||
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
|
||||
let tab: "logic" | "promote" | "precombo" = "logic";
|
||||
let scopes: Scope[] = [];
|
||||
let scope: Scope | null = null;
|
||||
let seq = 0;
|
||||
const libList = async (): Promise<void> => {
|
||||
if (!lib || !scope || tab === "logic") return;
|
||||
const mine = ++seq;
|
||||
const at = scope;
|
||||
list.replaceChildren();
|
||||
hint.textContent = tc("Loading");
|
||||
let rows: LibraryRow[] = [];
|
||||
try {
|
||||
rows = await fetchLibrary(at, tab);
|
||||
} catch (error) {
|
||||
if (mine === seq)
|
||||
hint.textContent = error instanceof Error ? error.message : tc("Load_Failed");
|
||||
return;
|
||||
}
|
||||
if (mine !== seq) return;
|
||||
hint.textContent = rows.length ? `${rows.length}` : tc("Pick_None");
|
||||
list.replaceChildren(
|
||||
...rows.map((x) => {
|
||||
const row = el("button", {
|
||||
className: "m01-logic__pick-row m01-logic__pick-row--button",
|
||||
attrs: { type: "button", "data-pick": x.이름 },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-logic__stack",
|
||||
children: [
|
||||
el("strong", { text: x.제목 || x.이름 }),
|
||||
el("span", { className: "m01-logic__muted", text: x.이름 }),
|
||||
],
|
||||
}),
|
||||
el("span", { text: x.단위 ?? "" }),
|
||||
],
|
||||
});
|
||||
row.addEventListener("click", () => {
|
||||
close();
|
||||
lib.pick({ ...at, 키: x.이름 }, x.제목 || x.이름);
|
||||
});
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
};
|
||||
const drawLayers = (): void =>
|
||||
layerBar.replaceChildren(
|
||||
...scopes.map((x) =>
|
||||
createButton({
|
||||
label: tc(LAYER_TEXT[x.층 as keyof typeof LAYER_TEXT]),
|
||||
variant: x === scope ? "filled" : "ghost",
|
||||
onClick: () => {
|
||||
scope = x;
|
||||
drawLayers();
|
||||
void libList();
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
const drawTabs = (): void => {
|
||||
tabs.replaceChildren(
|
||||
...(["logic", "promote", "precombo"] as const).map((k) =>
|
||||
createButton({
|
||||
label: tc(
|
||||
k === "logic" ? "Kind_Logic" : k === "promote" ? "Kind_Promote" : "Lib_Precombo",
|
||||
),
|
||||
variant: k === tab ? "filled" : "ghost",
|
||||
onClick: () => {
|
||||
tab = k;
|
||||
drawTabs();
|
||||
filters.style.display = k === "logic" ? "" : "none"; // 공용 꼴 display 가 hidden 을 이김
|
||||
layerBar.style.display = k === "logic" ? "none" : "";
|
||||
if (k === "logic") {
|
||||
list.replaceChildren();
|
||||
hint.textContent = tc("Pick_Hint");
|
||||
} else void libList();
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
layerBar.style.display = "none";
|
||||
if (lib)
|
||||
void upper(lib.layer, lib.owner).then((got) => {
|
||||
scopes = got;
|
||||
scope = got[got.length - 1] ?? null; // 기본 = 가장 가까운 층
|
||||
if (!got.length) return;
|
||||
drawTabs();
|
||||
drawLayers();
|
||||
});
|
||||
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick m01-combo",
|
||||
attrs: { role: "dialog", "aria-label": tc("Pick_Title") },
|
||||
@@ -87,15 +223,9 @@ export function openLogicPicker(done: (logic: LogicSummary) => void): void {
|
||||
createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }),
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
className: "m01-logic__filters",
|
||||
children: [
|
||||
sub.root,
|
||||
detail.root,
|
||||
searchField.root,
|
||||
createButton({ label: tc("Pick_Find"), variant: "filled", onClick: find }),
|
||||
],
|
||||
}),
|
||||
tabs,
|
||||
filters,
|
||||
layerBar,
|
||||
hint,
|
||||
list,
|
||||
],
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
LaborMat,
|
||||
LaborRow,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { lineId } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
@@ -37,9 +38,13 @@ const HEAD: Record<(typeof COSTS)[number], Parameters<typeof tc>[0]> = {
|
||||
/** 옵션 값 줄이 걸치는 칸 수 — 구분 · 이름 · 별칭 · 수량산출 · 결과 단위 · 금액 넷 */
|
||||
const SPAN = 5 + COSTS.length;
|
||||
|
||||
/** 구분(50-4 · 저장 안 함) — 줄 모양(키 머리)에서 읽음 · SP- = 단가승격 · SC- = 일위조합(50-3) */
|
||||
export const kindOf = (key: string): string =>
|
||||
tc(key.startsWith("SP-") ? "Kind_Promote" : key.startsWith("SC-") ? "Kind_Combo" : "Kind_Logic");
|
||||
/** 구분(50-4 · 저장 안 함) — 줄 모양(참조 키 머리)에서 읽음 · SP- = 단가승격 · SC- = 일위조합(50-3) */
|
||||
export const kindOf = (row: Pick<LaborRow, "로직" | "참조">): string => {
|
||||
const key = row.참조?.키 ?? row.로직;
|
||||
return tc(
|
||||
key.startsWith("SP-") ? "Kind_Promote" : key.startsWith("SC-") ? "Kind_Combo" : "Kind_Logic",
|
||||
);
|
||||
};
|
||||
|
||||
const num = (v: number): string =>
|
||||
Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
||||
@@ -230,11 +235,13 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
// 옵션 · 삭제만(구조물 조합 카드 · 43-4)이어도 끝 칸 — 순서 단추는 `move` 가 있을 때만
|
||||
const tools = !!(move || remove || option);
|
||||
const lineOf = (key: string): LaborCalcLine | undefined =>
|
||||
data.calc?.줄.find((l) => l.로직 === key);
|
||||
data.calc?.줄.find((l) => lineId(l) === key);
|
||||
const trs = data.rows.flatMap((row, i) => {
|
||||
const id = lineId(row) ?? "";
|
||||
const logic = data.logics.get(row.로직);
|
||||
const line = lineOf(row.로직);
|
||||
const unit = logic?.결과단위 ?? "";
|
||||
const line = lineOf(id);
|
||||
// 참조 줄 — 키 · 이름 · 단위 = 가리킨 문서 것(calc 답) · 넣을 값 없음(승격 때 고정)
|
||||
const unit = row.참조 ? (line?.분모 ?? "") : (logic?.결과단위 ?? "");
|
||||
const values = valueLine(logic?.입력 ?? [], row.입력값);
|
||||
const note = values.text;
|
||||
const why = line?.까닭 ?? (data.fail && i === 0 ? data.fail : null);
|
||||
@@ -251,19 +258,21 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
run();
|
||||
},
|
||||
});
|
||||
const sel = data.selected === row.로직 ? " is-selected" : "";
|
||||
const sel = data.selected === id ? " is-selected" : "";
|
||||
const tr = el("tr", {
|
||||
className: (on ? "m02-labrow__row is-open" : "m02-labrow__row") + sel,
|
||||
attrs: { "data-labor-logic": row.로직 },
|
||||
attrs: { "data-labor-logic": id },
|
||||
children: [
|
||||
el("td", { className: "m02-labrow__no", text: String(i + 1) }),
|
||||
el("td", { className: "m02-labrow__kind", text: kindOf(row.로직) }),
|
||||
el("td", { className: "m02-labrow__kind", text: kindOf(row) }),
|
||||
titled(
|
||||
el("td", {
|
||||
className: "m02-labrow__name",
|
||||
children: [
|
||||
el("span", { className: "m02-labrow__key", text: row.로직 }),
|
||||
el("strong", { text: logic?.이름 ?? row.로직 }),
|
||||
el("span", { className: "m02-labrow__key", text: row.참조?.키 ?? row.로직 }),
|
||||
el("strong", {
|
||||
text: (row.참조 ? line?.이름 : logic?.이름) ?? row.참조?.키 ?? row.로직,
|
||||
}),
|
||||
...(values.empty
|
||||
? [
|
||||
el("small", {
|
||||
@@ -299,7 +308,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
...(move
|
||||
? [small(tc("Up"), () => move(i, -1)), small(tc("Down"), () => move(i, 1))]
|
||||
: []),
|
||||
...(option ? [small(tc("Labor_Option"), () => option(i))] : []),
|
||||
...(option && !row.참조 ? [small(tc("Labor_Option"), () => option(i))] : []),
|
||||
...(remove ? [small(tc("Remove"), () => remove(i), "danger")] : []),
|
||||
],
|
||||
}),
|
||||
@@ -314,7 +323,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
|
||||
// 옵션 값 줄(44-6) — 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글은 title) · 행 높이 안 커짐
|
||||
const opt = el("tr", {
|
||||
className: (on ? "m02-labrow__optrow is-open" : "m02-labrow__optrow") + sel,
|
||||
attrs: { "data-labor-opt": row.로직 },
|
||||
attrs: { "data-labor-opt": id },
|
||||
children: [
|
||||
el("td", { className: "m02-labrow__no", text: "" }),
|
||||
el("td", {
|
||||
|
||||
@@ -90,6 +90,13 @@ const TEXT = {
|
||||
Kind_Combo: ["일위조합", "Combination"],
|
||||
Labor_LineDetail: ["상세 — {value}", "Detail — {value}"],
|
||||
Labor_LineApplied: ["반영(수량 곱함)", "Applied (× quantity)"],
|
||||
Labor_LineBase: ["기준", "Base"],
|
||||
Labor_FromLogic: ["원 로직", "Source logic"],
|
||||
Lib_Precombo: ["사전조합", "Pre-combination"],
|
||||
Lib_System: ["마스터", "Master"],
|
||||
Lib_Company: ["회사", "Company"],
|
||||
Lib_Personal: ["개인", "Personal"],
|
||||
Labor_AlreadyRef: ["이미 담은 일위대가 조합 — {value}", "Already added — {value}"],
|
||||
Labor_Empty: ["빈 값 {n}", "{n} empty"],
|
||||
Labor_Lost: [
|
||||
"로직 입력이 바뀌어 저장 때 빠지는 값 — {value}",
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type LaborCalc,
|
||||
type LaborDoc,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { settle } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { lineId, settle } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
@@ -64,12 +64,13 @@ export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewH
|
||||
onRow ? () => onRow(row) : undefined;
|
||||
const logicOpen = (i: number): void => {
|
||||
const row = d.담은로직[i];
|
||||
const line = calc?.줄.find((l) => l.로직 === row.로직);
|
||||
const id = lineId(row) ?? "";
|
||||
const line = calc?.줄.find((l) => lineId(l) === id);
|
||||
pick({
|
||||
kind: "logic",
|
||||
key: row.로직,
|
||||
key: id,
|
||||
pill: line?.알약 ?? null,
|
||||
name: logics.get(row.로직)?.이름 ?? row.로직,
|
||||
name: (row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? row.참조?.키 ?? row.로직,
|
||||
unit: line?.분모 ?? "",
|
||||
})?.();
|
||||
};
|
||||
@@ -111,7 +112,7 @@ export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewH
|
||||
const got = await readTemplate(layer, "labor", column, null);
|
||||
if (mine !== seq) return;
|
||||
doc = normalizeLabor(got.문서, column);
|
||||
const want = doc.담은로직.map((r) => r.로직).filter((k) => !logics.has(k));
|
||||
const want = doc.담은로직.map((r) => r.로직).filter((k) => k && !logics.has(k));
|
||||
const [inputs, area] = await Promise.all([
|
||||
fetchLogicInputs(want),
|
||||
readTemplate(layer, "area", column, null).then(
|
||||
|
||||
@@ -163,6 +163,17 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
|
||||
assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "", "로직": [], "참조": ref}]
|
||||
|
||||
|
||||
def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world):
|
||||
sp = _promote(world["system"])
|
||||
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "하나")
|
||||
with store.at(world["system"]):
|
||||
doc = labor.clean({"열": "rv", "담은로직": [{"참조": {**SYS, "키": k}} for k in (sp, sc)]})
|
||||
promote, combo = labor.calc(doc, {})["줄"]
|
||||
assert promote["원로직"] == TEN and promote["줄"]
|
||||
assert sum(x["금액"] for x in promote["줄"]) == pytest.approx(float(_base(TEN)["계"]))
|
||||
assert "줄" not in combo and [x["참조"]["키"] for x in combo["안"]] == [sp]
|
||||
|
||||
|
||||
def test_펼침은_순환_깊이_없는_참조를_까닭으로_멈춤(world):
|
||||
folder = world["system"] / "precombo"
|
||||
for key, child in (("SC-000001", "SC-000002"), ("SC-000002", "SC-000001")):
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/* 50-3 ④ — 일위대가 상세 [로직 추가] 창 탭 단가산출 · 단가승격 · 사전조합 + 층 단추 · 참조 줄 표 · 줄 상세
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 ·
|
||||
* 나무에 item1(구조물 조합 · 일위대가 첫 줄 GC000198)이 있음
|
||||
* 부르는 법: orca eval --expression "$(tr '\n' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 공유 마스터에 서고 문서를 안 씀 — 이 탭 fetch 만 바꿔 끼움(끝나면 되돌림):
|
||||
* 층 목록 = 가짜 SP-990001 · SC-990001 더함 · calc = 참조 줄 빼고 진짜 셈 + 참조 줄 답(승격 = GC000198 줄 · 사전조합 안 줄 하나)
|
||||
* 하는 일: item1 → 일위대가 [수정] → [로직 추가] 탭 셋 → 단가승격 탭 · 마스터 → SP 고름 → 사전조합 탭 → SC 고름 →
|
||||
* 승격 줄 누름 · 조합 줄 누름 → 두 줄 삭제(저장 안 함) → item1 다시
|
||||
* 기대 수치:
|
||||
* - tabs = ["단가산출","단가승격","사전조합"] · layers = ["마스터"](마스터 층은 위 층 없음) · listed = ["SP-990001"] · ["SC-990001"]
|
||||
* - added.kinds 끝 둘 = ["단가승격","일위조합"] · added.keys 끝 둘 = ["SP-990001","SC-990001"] · 옵션 단추 없음(optBtn = 0)
|
||||
* - promote.from = "원 로직 GC000198" · promote.rows = GC000198 줄 수(4) · promote.sum = first 줄 기준 계
|
||||
* - combo.inner = 1 · combo.innerKind = "단가승격"
|
||||
* - removed = true(원래 줄 수) · askedLeave = false(저장 안 한 고침 없음) · back = true
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 8000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const n = (s) => Number(String(s ?? "").replace(/[^0-9.-]/g, "")) || 0;
|
||||
const SP = { 층: "system", 주인: null, 키: "SP-990001" };
|
||||
const SC = { 층: "system", 주인: null, 키: "SC-990001" };
|
||||
const rid = (r) => `참조|${r.층}|${r.주인 ?? ""}|${r.키}`;
|
||||
const real = window.fetch;
|
||||
let first = null;
|
||||
window.fetch = async (url, init) => {
|
||||
const u = String(url);
|
||||
if (/\/layers\/system\/templates$/.test(u)) {
|
||||
const got = await (await real(url, init)).json();
|
||||
got.양식.push(
|
||||
{ 종류: "promote", 이름: SP.키, 제목: "시험 승격", 단위: "㎡" },
|
||||
{ 종류: "precombo", 이름: SC.키, 제목: "시험 조합", 단위: "식" },
|
||||
);
|
||||
return new Response(JSON.stringify(got), { status: 200 });
|
||||
}
|
||||
if (/\/labor\/[^/]+\/calc/.test(u) && init?.body) {
|
||||
const body = JSON.parse(init.body);
|
||||
const refs = (body.doc?.담은로직 ?? []).filter((r) => r.참조);
|
||||
if (!refs.length) return real(url, init);
|
||||
body.doc.담은로직 = body.doc.담은로직.filter((r) => !r.참조);
|
||||
const got = await (await real(url, { ...init, body: JSON.stringify(body) })).json();
|
||||
first = got.줄[0];
|
||||
for (const r of refs) {
|
||||
const base = { ...first.기준 };
|
||||
const line = {
|
||||
참조: r.참조,
|
||||
이름: r.참조.키 === SP.키 ? "시험 승격" : "시험 조합",
|
||||
알약: rid(r.참조),
|
||||
분모: "㎡",
|
||||
배: 1,
|
||||
기준: base,
|
||||
반영: null,
|
||||
수량: null,
|
||||
수량출처: "없음",
|
||||
까닭: null,
|
||||
};
|
||||
if (r.참조.키 === SP.키) Object.assign(line, { 원로직: first.로직, 줄: first.줄 });
|
||||
else
|
||||
line.안 = [{ 참조: SP, 수량: 1, 이름: "시험 승격", 단위: "㎡", 기준: base, 까닭: null }];
|
||||
got.줄.push(line);
|
||||
}
|
||||
return new Response(JSON.stringify(got), { status: 200 });
|
||||
}
|
||||
return real(url, init);
|
||||
};
|
||||
const out = {};
|
||||
try {
|
||||
const node = await until(() => document.querySelector('[data-node="i|item1"]'));
|
||||
if (!node) return JSON.stringify({ error: "item1 없음" });
|
||||
node.click();
|
||||
const card = await until(() =>
|
||||
[...document.querySelectorAll(".m02-struct__card")].find((c) =>
|
||||
c.querySelector("table.m02-labrow tbody tr[data-labor-logic]"),
|
||||
),
|
||||
);
|
||||
if (!card) return JSON.stringify({ error: "일위대가 카드 없음" });
|
||||
[...card.querySelectorAll(".m02-struct__card-tools button")]
|
||||
.find((b) => b.innerText.trim() === "수정")
|
||||
?.click();
|
||||
const table = () =>
|
||||
[...document.querySelectorAll("table.m02-labrow")].find(
|
||||
(t) => !t.closest(".m02-struct__card") && t.querySelector("tr[data-labor-logic]"),
|
||||
);
|
||||
await until(() => table() && !/…/.test(table().querySelector("tbody").innerText), 12000);
|
||||
const host = table().closest("[data-labor]");
|
||||
const before = table().querySelectorAll("tbody tr[data-labor-logic]").length;
|
||||
const add = async (tabLabel, key) => {
|
||||
[...host.querySelectorAll(".m01-logic__section-head button")]
|
||||
.find((b) => b.innerText.trim() === "로직 추가")
|
||||
.click();
|
||||
const dlg = await until(
|
||||
() =>
|
||||
document.querySelector(".m01-logic__pick [data-pick-tabs] button") &&
|
||||
document.querySelector(".m01-logic__pick"),
|
||||
);
|
||||
const tabs = [...dlg.querySelectorAll("[data-pick-tabs] button")];
|
||||
out.tabs = tabs.map((b) => b.innerText.trim());
|
||||
tabs.find((b) => b.innerText.trim() === tabLabel).click();
|
||||
await until(() => dlg.querySelector(`[data-pick="${key}"]`));
|
||||
out.layers = [...dlg.querySelectorAll("[data-pick-layers] button")].map((b) =>
|
||||
b.innerText.trim(),
|
||||
);
|
||||
(out.listed ??= []).push([...dlg.querySelectorAll("[data-pick]")].map((b) => b.dataset.pick));
|
||||
dlg.querySelector(`[data-pick="${key}"]`).click();
|
||||
await until(
|
||||
() =>
|
||||
table()?.querySelector(`tr[data-labor-logic="${rid({ ...SP, 키: key })}"]`) &&
|
||||
!/…/.test(table().querySelector("tbody").innerText),
|
||||
12000,
|
||||
);
|
||||
};
|
||||
await add("단가승격", SP.키);
|
||||
await add("사전조합", SC.키);
|
||||
const rows = [...table().querySelectorAll("tbody tr[data-labor-logic]")];
|
||||
out.added = {
|
||||
kinds: rows.slice(-2).map((r) => r.querySelector(".m02-labrow__kind").innerText.trim()),
|
||||
keys: rows.slice(-2).map((r) => r.querySelector(".m02-labrow__key").innerText.trim()),
|
||||
optBtn: rows
|
||||
.slice(-2)
|
||||
.flatMap((r) =>
|
||||
[...r.querySelectorAll("button")].filter((b) => b.innerText.trim() === "옵션"),
|
||||
).length,
|
||||
};
|
||||
const open = async (ref) => {
|
||||
table()
|
||||
.querySelector(`tr[data-labor-logic="${rid(ref)}"] .m02-labrow__name`)
|
||||
.click();
|
||||
await w(300);
|
||||
return host.querySelector("[data-labor-linedet]");
|
||||
};
|
||||
let det = await open(SP);
|
||||
const grid = det.querySelector("table.m01-logic__grid");
|
||||
out.promote = {
|
||||
from: det.querySelector("p")?.innerText.trim(),
|
||||
rows: grid?.querySelectorAll("tbody tr").length ?? 0,
|
||||
sum: [...(grid?.querySelectorAll("tbody tr") ?? [])].reduce(
|
||||
(a, r) => a + n(r.lastElementChild.innerText),
|
||||
0,
|
||||
),
|
||||
firstBase: first?.기준?.계 ?? null,
|
||||
};
|
||||
det = await open(SC);
|
||||
const inner = det.querySelector("[data-labor-inner]");
|
||||
out.combo = {
|
||||
inner: inner?.querySelectorAll("tbody tr").length ?? 0,
|
||||
innerKind: inner?.querySelector("tbody td")?.innerText.trim(),
|
||||
};
|
||||
await open(SC); /* 닫음 */
|
||||
for (const ref of [SC, SP]) {
|
||||
const tr = table().querySelector(`tr[data-labor-logic="${rid(ref)}"]`);
|
||||
[...tr.querySelectorAll("button")].find((b) => b.innerText.trim() === "삭제").click();
|
||||
await w(600);
|
||||
}
|
||||
out.removed = table().querySelectorAll("tbody tr[data-labor-logic]").length === before;
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
document.querySelector('[data-node="i|item1"]')?.click();
|
||||
await w(800);
|
||||
const ask = [...document.querySelectorAll(".ui-modal button, dialog button")].find((b) =>
|
||||
/확인|버림|떠나기/.test(b.innerText),
|
||||
);
|
||||
out.askedLeave = !!ask; /* 저장 안 한 고침이 남았으면 물음 — 지운 뒤라 없어야 함 */
|
||||
ask?.click();
|
||||
await w(1200);
|
||||
out.back = !!document.querySelector(".m02-struct__card table.m02-labrow");
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user