feat(778-5 D-5): 마법사 단가산출 줄 고름 — 제안 · 블록 [확인] 뒤 확인한 것만 옮김(요구 621 · 622 · 623 · 624)
- 단가산출 칸 = 서버 줄 판정(빈 확인) 블록 표: 판정 · 우리 금액 · 엑셀 금액(참고) · 근거 꼬리표 · 만든 SK · [확인] 칩(고를 줄 0 일 때만) · [줄] 펼침 · [보기]
- 줄 표(새 Hopyo_SkLines) = 묶음 · 이름 · 규격 · 매개값 · 상태 칩(엑셀 칠과 한 규칙 unitTone) · 꼬리표 · 방법 · 금액 · 차 · 고름(후보 · 찾기 · 율 기준 · 뺌 + 까닭 · 지움) · 엑셀 보기 뛰기
- 고름 · 확인 = 캐시 m02-hopyo-sk · [확인한 것 옮기기] = 몸 {확인: [{번, 고름}]} · 서버가 다시 셈 · 자동 확인 · 「모두 확인」 없음
- 순수 꼴 = 새 logic/Hopyo_SkPick · 길 = 새 Hopyo_SkApi(Hopyo_Api 안 늘림)
- 마법사 [끝내기] 막힘 글 앞에 「단가산출 확인 안 한 블록 n · 고를 줄 m」(Hopyo_Wizard 한 곳)
- 걷음: 옛 고름 대기 기초 고름 · 줄 비교(Compare.compareView · compareBrief · Api SangeunCompare · CompareLook · CSS) — 쓰는 곳 없음 · 못옮김 창 잇기 만들기(옛 바탕 몸 = 서버가 걷어 422) → 보기만 + 줄 찾기 단추 lineFind 로 다시 씀 · 안 쓰는 문구 39
- 낡은 시험 고침: test_746_2(줄 비교 · 못옮김 요소 단추 시험 걷음 — 기능 걷음) · test_729_3(candLabel · 옛 잇기 몸 기대값 걷음) · test_m02_hopyo_screen(sangeunStep 인자) · 새 test_778_5_sk_pick_screen + helper_778_5_sk_pick.cjs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -367,11 +367,3 @@
|
||||
.m02-hopyo-side > .m02-side__row {
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
/* 고름 대기 후보 줄 비교(746-2) — 고르기 칸 아래 · 후보 단추 줄 + 표 */
|
||||
.m02-hopyo__cmp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -234,27 +234,6 @@ export interface SangeunCand {
|
||||
글: string;
|
||||
/** 729-4 — 기준단위 ↔ 엑셀 단위 성격 다름 = 환산 계수 필요(가 1 = 나 몇) */
|
||||
계수?: { 가: string; 나: string; 갈래: string } | null;
|
||||
/** 746-1 — 그 기초로 견준 줄 비교(앞 셋) · null = 견줌 못함(단위 성격 다름) */
|
||||
줄비교?: SangeunCompare | null;
|
||||
}
|
||||
|
||||
/** 줄 비교 꼴(746-1) */
|
||||
export type CompareLook = "같음" | "품다름" | "엑셀만" | "기초만";
|
||||
|
||||
/** 고름 대기 후보 줄 비교(746-1) — 줄마다 엑셀 · 기초 1 단위 품 */
|
||||
export interface SangeunCompare {
|
||||
줄: {
|
||||
갈래: "인력" | "장비" | "자재";
|
||||
이름: string;
|
||||
규격?: string;
|
||||
단위?: string;
|
||||
엑셀: number | null;
|
||||
기초: number | null;
|
||||
꼴: CompareLook;
|
||||
}[];
|
||||
수: Record<CompareLook, number>;
|
||||
못옮김: SangeunLost[];
|
||||
환산?: unknown;
|
||||
}
|
||||
|
||||
/** M01 요소 후보(746-1) — 못옮김 · 안 이은 기계 줄 · 고르면 `{노임|기계|자재: 키}` 로 이음 */
|
||||
|
||||
@@ -1,111 +1,15 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Compare.ts
|
||||
* 불러오기 고름 창 — 후보 블록 줄 비교 · M01 요소 후보(PLAN 746-2 · 요구 585 · 서버 746-1 `줄비교` · `후보`) —
|
||||
* · `compareView` = 고름 대기 후보(앞 셋)마다 줄 비교 표(줄 · 엑셀 품 · 기초 품 · 꼴 배지) + 못옮김 수 · 후보 바꿈 단추
|
||||
* · `elemChoices` = 못옮김 · 안 이은 기계 줄의 M01 요소 후보 셋 단추(누르면 부르는 쪽이 이음)
|
||||
* 보이기만 — 판정 · 순위 · 미리 고름은 서버 그대로 · 고르기는 부르는 쪽.
|
||||
* 불러오기 M01 요소 후보 단추(PLAN 746-2 · 요구 585 · 서버 746-1 `후보`) —
|
||||
* · `elemChoices` = 안 이은 줄의 M01 요소 후보 셋 단추(누르면 부르는 쪽이 이음)
|
||||
* · 옛 고름 대기 기초 줄 비교(`compareView`)는 778-5 D-5 줄 고름으로 걷음
|
||||
* 보이기만 — 판정 · 순위는 서버 그대로 · 고르기는 부르는 쪽.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { t as L, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { formatNumber, formatPrice } from "../common_util/common_util_money";
|
||||
import type {
|
||||
CompareLook,
|
||||
ElemCand,
|
||||
SangeunCand,
|
||||
SangeunCompare,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
|
||||
const LOOK: Record<CompareLook, { tone: BadgeTone; key: LocaleKey }> = {
|
||||
같음: { tone: "success", key: "M02_CmpSame" },
|
||||
품다름: { tone: "warning", key: "M02_CmpDiff" },
|
||||
엑셀만: { tone: "info", key: "M02_CmpExcel" },
|
||||
기초만: { tone: "neutral", key: "M02_CmpBase" },
|
||||
};
|
||||
const LOOKS = Object.keys(LOOK) as CompareLook[];
|
||||
const num = (n: number | null): string => (n === null ? "—" : formatNumber(n));
|
||||
|
||||
/** 선택 칸 글 뒤 요약 — 「같음 3 · 품 다름 1」 · 견줌 못함 = "" */
|
||||
export const compareBrief = (c: SangeunCompare | null | undefined): string =>
|
||||
c
|
||||
? LOOKS.filter((k) => c.수[k])
|
||||
.map((k) => `${L(LOOK[k].key)} ${c.수[k]}`)
|
||||
.join(" · ")
|
||||
: "";
|
||||
|
||||
/** 줄 비교 표 하나 */
|
||||
function compareTable(c: SangeunCompare): HTMLElement {
|
||||
const rows = c.줄.map((x) => [
|
||||
`${x.갈래} ${x.이름}${x.규격 ? ` ${x.규격}` : ""}`,
|
||||
`${num(x.엑셀)} ${x.단위 ?? ""}`.trim(),
|
||||
`${num(x.기초)} ${x.단위 ?? ""}`.trim(),
|
||||
createBadge(L(LOOK[x.꼴].key), LOOK[x.꼴].tone),
|
||||
]);
|
||||
return createTable(
|
||||
[
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_CmpColExcel"), width: "112px", align: "end" },
|
||||
{ label: L("M02_CmpColBase"), width: "112px", align: "end" },
|
||||
{ label: L("M02_CmpColLook"), width: "84px" },
|
||||
],
|
||||
rows,
|
||||
{ density: "data", empty: L("M02_CmpNone") },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고름 대기 후보 줄 비교 칸 — 앞 후보 줄 비교를 후보 단추로 바꿔 봄 · `show(로직)` 으로 밖(고르기 칸)이 바꿈.
|
||||
* 줄 비교 있는 후보가 하나도 없으면 null.
|
||||
*/
|
||||
export function compareView(
|
||||
cands: SangeunCand[],
|
||||
): { root: HTMLElement; show: (로직: string) => void } | null {
|
||||
const has = cands.filter((c) => c.줄비교);
|
||||
if (!has.length) return null;
|
||||
const root = el("div", { className: "m02-hopyo__cmp", attrs: { "data-cmp": "" } });
|
||||
const body = el("div");
|
||||
const seg = createSegment({
|
||||
options: has.map((c, i) => ({ value: c.로직, label: `${i + 1}. ${c.이름}`, title: c.이름 })),
|
||||
value: has[0]!.로직,
|
||||
size: 24,
|
||||
ariaLabel: L("M02_CmpTitle").replace("{name}", ""),
|
||||
onChange: (v) => paint(v),
|
||||
});
|
||||
const paint = (로직: string): void => {
|
||||
const c = has.find((x) => x.로직 === 로직) ?? has[0]!;
|
||||
const cmp = c.줄비교!;
|
||||
body.dataset.cmpOf = c.로직;
|
||||
const lost = cmp.못옮김.length;
|
||||
body.replaceChildren(
|
||||
createText(
|
||||
[
|
||||
L("M02_CmpTitle").replace("{name}", c.이름),
|
||||
compareBrief(cmp),
|
||||
lost ? L("M02_CmpLost").replace("{n}", String(lost)) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
"muted",
|
||||
12,
|
||||
),
|
||||
compareTable(cmp),
|
||||
);
|
||||
};
|
||||
root.append(seg.root, body);
|
||||
paint(has[0]!.로직);
|
||||
return {
|
||||
root,
|
||||
show: (로직) => {
|
||||
if (!has.some((c) => c.로직 === 로직)) return;
|
||||
seg.set(로직);
|
||||
paint(로직);
|
||||
},
|
||||
};
|
||||
}
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { formatPrice } from "../common_util/common_util_money";
|
||||
import type { ElemCand } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
|
||||
/** 요소 후보 단추 줄(셋까지) — 누르면 `pick(후보)` */
|
||||
export function elemChoices(cands: ElemCand[], pick: (c: ElemCand) => void): HTMLElement | null {
|
||||
|
||||
@@ -1,180 +1,93 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Lost.ts
|
||||
* 단가산출 못옮김 잇기 창(PLAN 729-3 · 729-4 이음 · 요구서 내역19 2장 3 · 4) — 실무 블록을 SK 로 옮길 때 우리 M01 에
|
||||
* 바로 못 옮긴 엑셀 줄(노임 · 기계 · 자재)을 줄마다 M01 요소로 이음 → SK 더한줄(엑셀 수량) · [뺌] = 놓음(까닭 남김).
|
||||
* · 고름 = 캐시 `m02-hopyo-lost:{층}:{주인}:{키}:{번}` `{id: 잇기}` — 서버는 부를 때마다 그 블록 잇기 전부로 다시 셈
|
||||
* (앞 잇기 안 남음) → [잇기 만들기] 는 캐시 모두를 보냄 · 바탕 = 그 SK 문서 바탕(로직 · 입력값 · 환산 그대로)
|
||||
* · 줄마다 M01 요소 후보 셋 단추(746-2 · 서버 `후보`) — 누르면 그 줄에 이음 고름(찾기와 같은 길)
|
||||
* · 남은 못옮김 = 서버 답 그대로 표 · 0 이면 그 SK 를 쓰는 일위대가 줄이 이어짐(남으면 만들기 409)
|
||||
* 계산은 서버 — 화면은 고름 · 보이기만 · 저장은 [잇기 만들기] 누를 때만.
|
||||
* 단가산출 줄 찾기 단추 · 옛 못옮김 창(PLAN 729-4 · 778-5 D-5).
|
||||
* · `lineFind` = 줄 묶음대로 찾아 넣기 창(`Find_Pick` 탭 하나 · 노임 = 인력 · 기계 = 기계 · 자재 = 재료) →
|
||||
* 고름 `{노임|기계|자재: 키}` — 마법사 단가산출 줄 표(`Hopyo_SkLines`)가 씀
|
||||
* · `openLost` = 옛 SK(기초 + 엑셀 차이)의 남은 못옮김 보기만 — 잇기는 마법사 단가산출 칸에서 그 블록 줄 고름 →
|
||||
* [확인] → [확인한 것 옮기기](서버 778-5 D-3 · 옛 `바탕` 몸 걷음)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { readTemplate } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import {
|
||||
makeSangeun,
|
||||
type HopyoScope,
|
||||
type SangeunBuilt,
|
||||
type SangeunLink,
|
||||
type SangeunLost,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { elemChoices } from "./M02_MasterTemplete_Hopyo_Compare";
|
||||
import type { HopyoScope, SangeunBuilt, SangeunLost } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import type { SkPick } from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const NONE = "우리에 없음";
|
||||
const KIND = { 노임: "labor", 기계: "machine", 자재: "material" } as const;
|
||||
|
||||
export const lostKey = (scope: HopyoScope, key: string, no: number): string =>
|
||||
`m02-hopyo-lost:${scope.layer}:${scope.owner ?? ""}:${key}:${no}`;
|
||||
|
||||
/** 캐시 잇기 `{id: 잇기}` */
|
||||
export function loadLinks(k: string): Record<string, SangeunLink> {
|
||||
try {
|
||||
const got = JSON.parse(sessionStorage.getItem(k) ?? "{}") as unknown;
|
||||
return got && typeof got === "object" ? (got as Record<string, SangeunLink>) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
/** 줄 찾기 단추 — 묶음 갈래 탭 하나 · 고르면 `done({갈래: 키})` */
|
||||
export function lineFind(opt: {
|
||||
scope: HopyoScope;
|
||||
갈래: "노임" | "기계" | "자재";
|
||||
이름: string;
|
||||
규격?: string;
|
||||
done: (p: SkPick) => void;
|
||||
}): HTMLButtonElement {
|
||||
const go = createButton({
|
||||
label: opt.갈래 === "기계" ? L("M02_LeftMachine") : L("M02_LeftFind"),
|
||||
size: 24,
|
||||
variant: "ghost",
|
||||
onClick: () =>
|
||||
openFindPicker({
|
||||
layer: opt.scope.layer,
|
||||
owner: opt.scope.owner,
|
||||
title: `${opt.이름} ${opt.규격 ?? ""}`.trim(),
|
||||
kinds: [KIND[opt.갈래]],
|
||||
query: opt.이름.split(/[\s(]/)[0] ?? "",
|
||||
logic: () => showToast(L("M02_GridLinkNoLogic"), "info"),
|
||||
element: (e) => opt.done({ [opt.갈래]: e.코드 } as SkPick),
|
||||
machine: (e) => opt.done({ 기계: e.코드 }),
|
||||
}),
|
||||
});
|
||||
go.dataset.skFind = opt.갈래;
|
||||
return go;
|
||||
}
|
||||
const saveLinks = (k: string, v: Record<string, SangeunLink>): void =>
|
||||
sessionStorage.setItem(k, JSON.stringify(v));
|
||||
|
||||
/** 고른 것 글 — 키 또는 뺌 */
|
||||
const linkText = (x: SangeunLink | undefined): string =>
|
||||
!x ? "" : x.뺌 ? `${L("M02_LostDrop")} · ${x.뺌}` : (x.노임 ?? x.기계 ?? x.자재 ?? "");
|
||||
|
||||
export interface LostOptions {
|
||||
scope: HopyoScope;
|
||||
key: string;
|
||||
/** 블록 번(단산N) */
|
||||
번: number;
|
||||
/** 그 블록 SK 키 — 바탕을 읽음 */
|
||||
/** 그 블록 SK 키 */
|
||||
sk: string;
|
||||
이름?: string;
|
||||
lost: SangeunLost[];
|
||||
/** 잇기 만든 뒤(서버 답) */
|
||||
/** 옛 꼴 그대로(부르는 쪽) — 보기만이라 안 부름 */
|
||||
done?: (got: SangeunBuilt) => void;
|
||||
}
|
||||
|
||||
/** 못옮김 잇기 창 */
|
||||
/** 옛 못옮김 창 — 보기만 · 잇기 = 마법사 단가산출 칸 */
|
||||
export function openLost(opt: LostOptions): void {
|
||||
const k = lostKey(opt.scope, opt.key, opt.번);
|
||||
let lost = opt.lost;
|
||||
openModal({
|
||||
title: `${L("M02_LostTitle")} — 단산${opt.번} ${opt.이름 ?? ""}`.trim(),
|
||||
closeLabel: L("M02_Close"),
|
||||
width: "960px",
|
||||
dialogClass: "m02-modal",
|
||||
mount: (body, _close, actions) => {
|
||||
const list = el("div", { attrs: { "data-lost": "" } });
|
||||
const put = (id: string, link: SangeunLink | null): void => {
|
||||
const all = loadLinks(k);
|
||||
if (link) all[id] = link;
|
||||
else delete all[id];
|
||||
saveLinks(k, all);
|
||||
draw();
|
||||
};
|
||||
const make = createButton({
|
||||
label: L("M02_LostMake"),
|
||||
variant: "filled",
|
||||
onClick: () => void run(),
|
||||
});
|
||||
make.dataset.lostMake = "";
|
||||
actions.append(make);
|
||||
body.append(createText(L("M02_LostHint"), "hint", 14), list);
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const links = Object.values(loadLinks(k));
|
||||
if (!links.length) return showToast(L("M02_WizSkPickFirst"), "warning");
|
||||
make.disabled = true;
|
||||
try {
|
||||
const doc = (await readTemplate(opt.scope.layer, "sangeun", opt.sk, null, opt.scope.owner))
|
||||
.문서 as { 바탕?: { 로직?: string } | null; 입력값?: Record<string, unknown>; 환산?: { 계수?: number } }; // prettier-ignore
|
||||
const pick = {
|
||||
...(doc.바탕?.로직 ? { 로직: doc.바탕.로직, 입력값: doc.입력값 ?? {} } : {}),
|
||||
...(doc.환산?.계수 ? { 환산: { 계수: doc.환산.계수 } } : {}),
|
||||
잇기: links,
|
||||
};
|
||||
const got = await makeSangeun(opt.scope, opt.key, { [opt.번]: pick });
|
||||
lost = got.만듦.find((x) => x.번 === opt.번)?.못옮김 ?? [];
|
||||
showToast(L("M02_LostLeft").replace("{n}", String(lost.length)), lost.length ? "warning" : "success"); // prettier-ignore
|
||||
opt.done?.(got);
|
||||
draw();
|
||||
} catch (error) {
|
||||
showToast(why(error), "error");
|
||||
} finally {
|
||||
make.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function draw(): void {
|
||||
const all = loadLinks(k);
|
||||
list.dataset.lost = String(lost.length);
|
||||
const rows = lost.map((x) => {
|
||||
const kind = x.갈래;
|
||||
const find = createButton({
|
||||
label: kind === "기계" ? L("M02_LeftMachine") : L("M02_LeftFind"),
|
||||
size: 24,
|
||||
variant: "ghost",
|
||||
onClick: () =>
|
||||
// 736-2 줄 갈래대로 탭 하나(노임 = 인력 · 기계 = 기계 · 자재 = 재료) → 이음 칸 `노임` · `기계` · `자재`
|
||||
openFindPicker({
|
||||
layer: opt.scope.layer,
|
||||
owner: opt.scope.owner,
|
||||
title: `${x.이름} ${x.규격 ?? ""}`.trim(),
|
||||
kinds: [kind === "노임" ? "labor" : kind === "기계" ? "machine" : "material"],
|
||||
query: x.이름.split(/[\s(]/)[0] ?? "",
|
||||
logic: () => showToast(L("M02_GridLinkNoLogic"), "info"),
|
||||
element: (e) => put(x.id, { id: x.id, [kind]: e.코드 }),
|
||||
machine: (e) => put(x.id, { id: x.id, 기계: e.코드 }),
|
||||
}),
|
||||
});
|
||||
const drop = createButton({
|
||||
label: NONE,
|
||||
size: 24,
|
||||
variant: "ghost",
|
||||
onClick: () => put(x.id, { id: x.id, 뺌: NONE }),
|
||||
});
|
||||
drop.title = L("M02_LostDropTip");
|
||||
const clear = createButton({
|
||||
label: L("M02_LostClear"),
|
||||
size: 24,
|
||||
variant: "link",
|
||||
onClick: () => put(x.id, null),
|
||||
});
|
||||
return [
|
||||
kind,
|
||||
x.이름,
|
||||
x.규격 ?? "",
|
||||
`${x.수량 ?? ""} ${x.단위 ?? ""}`.trim(),
|
||||
x.글 ?? "",
|
||||
elemChoices(x.후보 ?? [], (c) => put(x.id, { id: x.id, [kind]: c.키 })) ?? "",
|
||||
linkText(all[x.id]),
|
||||
el("div", { className: "ui-toolbar", children: [find, drop, ...(all[x.id] ? [clear] : [])] }), // prettier-ignore
|
||||
];
|
||||
});
|
||||
list.replaceChildren(
|
||||
createTable(
|
||||
[
|
||||
{ label: L("M02_LeftKind"), width: "56px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_LeftSpec") },
|
||||
{ label: L("M02_SangeunColQty"), width: "90px" },
|
||||
{ label: L("M02_LeftWhy") },
|
||||
{ label: L("M02_ElemTitle") },
|
||||
{ label: L("M02_LeftPicked") },
|
||||
{ label: L("M02_LeftPick"), width: "220px" },
|
||||
],
|
||||
rows,
|
||||
{ density: "data", empty: L("M02_LostZero") },
|
||||
),
|
||||
);
|
||||
}
|
||||
draw();
|
||||
mount: (body) => {
|
||||
const rows = opt.lost.map((x) => [
|
||||
x.갈래,
|
||||
x.이름,
|
||||
x.규격 ?? "",
|
||||
`${x.수량 ?? ""} ${x.단위 ?? ""}`.trim(),
|
||||
x.글 ?? "",
|
||||
]);
|
||||
body.append(
|
||||
createText(L("M02_LostHint").replace("{n}", String(opt.번)), "hint", 14),
|
||||
createTable(
|
||||
[
|
||||
{ label: L("M02_LeftKind"), width: "56px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_LeftSpec") },
|
||||
{ label: L("M02_SangeunColQty"), width: "90px" },
|
||||
{ label: L("M02_LeftWhy") },
|
||||
],
|
||||
rows,
|
||||
{ density: "data", empty: L("M02_LostZero") },
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_SkApi.ts
|
||||
* 마법사 단가산출 길(PLAN 778-5 D-5 · 서버 D-3 `M02_Hopyo_Sangeun.build`) — `POST …/hopyo/{키}/sangeun`
|
||||
* · 몸 `{확인: [{번, 고름?: {차례: 고름}}]}` — 빈 확인 = 판정만(안 만듦) · 확인한 블록만 단위 SK
|
||||
* · 답 = 블록마다 줄 판정 · 만듦 · 못만듦(꼴 = `logic/M02_MasterTemplete_Hopyo_SkPick`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import type { SkJudged, SkSure } from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
||||
|
||||
/** 줄 판정 · 확인한 블록 옮기기 — 맞춤 셈 전 409 · 틀린 고름 400 · 없는 블록 404 */
|
||||
export const judgeSangeun = (
|
||||
scope: HopyoScope,
|
||||
key: string,
|
||||
확인: SkSure[] = [],
|
||||
): Promise<SkJudged> =>
|
||||
call(at(scope, `/hopyo/${encodeURIComponent(key)}/sangeun`), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ 확인 }),
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_SkLines.ts
|
||||
* 마법사 단가산출 블록 펼침 = 줄 표(PLAN 778-5 D-5 · 요구 623 · 624) — 서버 줄 판정(D-3) + 캐시 고름 겹침.
|
||||
* · 칸 = 묶음 · 이름 · 규격 · 매개값 · 상태 칩(이어짐 · 고를 것 · 못 이음 · 밖 · 뺌 — 엑셀 보기 칠과 한 규칙
|
||||
* `SheetMap.unitTone`) + 꼬리표(「기본 — 건설업 작업반장」 · 「내가 고름」) · 방법 글 · 우리 금액 · 엑셀 금액 · 차(참고)
|
||||
* · 고름(밖 줄 빼고) = 서버 후보 단추 · [찾기](`Hopyo_Lost.lineFind` = 찾아 넣기 창) · 율 = 기준 고르기 ·
|
||||
* 하위 · 값 = 뺌만 · [뺌] + 까닭 · [고름 지움] — 고름은 부르는 쪽 캐시(`onPick`)
|
||||
* · [보기] = 엑셀 보기 단가산출 상세 그 행(779-4 뛰기)
|
||||
* 금액 · 판정은 서버 — 고른 줄 금액은 [확인한 것 옮기기] 뒤 서버가 다시 셈(그 전 「—」).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createTable, type Cell } from "@ui/ui_template_table";
|
||||
import { formatNumber, formatPrice } from "../common_util/common_util_money";
|
||||
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { lineFind } from "./M02_MasterTemplete_Hopyo_Lost";
|
||||
import { unitTone, type Tone } from "./logic/M02_MasterTemplete_Hopyo_SheetMap";
|
||||
import {
|
||||
BASES,
|
||||
GROUP,
|
||||
candPick,
|
||||
candText,
|
||||
dropOnly,
|
||||
lineView,
|
||||
pickable,
|
||||
type SkBlock,
|
||||
type SkLine,
|
||||
type SkPick,
|
||||
} from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
||||
|
||||
const BADGE: Record<Tone, BadgeTone> = {
|
||||
success: "success",
|
||||
warning: "warning",
|
||||
info: "info",
|
||||
danger: "danger",
|
||||
muted: "neutral",
|
||||
};
|
||||
const FIND = { L: "노임", X: "기계", M: "자재" } as const;
|
||||
const won = (v: number | null | undefined): string =>
|
||||
v === null || v === undefined ? "—" : formatPrice(v);
|
||||
|
||||
export interface SkLinesOptions {
|
||||
scope: HopyoScope;
|
||||
block: SkBlock;
|
||||
picks: Record<string, SkPick>;
|
||||
/** 고름 넣기 · 지움(null) */
|
||||
onPick: (차례: number, p: SkPick | null) => void;
|
||||
/** 엑셀 보기 그 행 */
|
||||
onSheet?: (행: number) => void;
|
||||
}
|
||||
|
||||
/** 상태 칩 + 꼬리표 */
|
||||
function stateCell(line: SkLine, pick: SkPick | undefined): HTMLElement {
|
||||
const v = lineView(line, pick);
|
||||
const diff = !v.mine && line.차 !== null && line.차 !== undefined && Math.abs(line.차) >= 1;
|
||||
const tone = v.상태 === "뺌" ? "muted" : unitTone(v.상태, diff);
|
||||
const chip = createBadge(v.상태, BADGE[tone]);
|
||||
chip.dataset.skState = v.상태;
|
||||
const tags = v.꼬리표.map((t) => {
|
||||
const tag = createBadge(t, t === "내가 고름" ? "accent" : "neutral");
|
||||
tag.dataset.skTag = t;
|
||||
return tag;
|
||||
});
|
||||
return el("div", { className: "ui-toolbar", children: [chip, ...tags] });
|
||||
}
|
||||
|
||||
/** 고름 칸 — 후보 · 찾기 · 율 기준 · 뺌 + 까닭 · 지움 */
|
||||
function pickCell(opt: SkLinesOptions, line: SkLine, pick: SkPick | undefined): HTMLElement {
|
||||
const box = el("div", { className: "ui-toolbar", attrs: { "data-sk-pick": String(line.차례) } });
|
||||
if (!pickable(line)) return box;
|
||||
const put = (p: SkPick | null): void => opt.onPick(line.차례, p);
|
||||
if (!dropOnly(line)) {
|
||||
for (const c of line.후보.slice(0, 3)) {
|
||||
const p = candPick(line.묶음, c);
|
||||
if (!p) continue;
|
||||
const b = createButton({ label: candText(c), size: 24, variant: "ghost", onClick: () => put(p) }); // prettier-ignore
|
||||
b.dataset.skCand = String(line.차례);
|
||||
b.title = c.키 ?? c.자재 ?? c.글 ?? "";
|
||||
box.append(b);
|
||||
}
|
||||
const kind = FIND[line.묶음 as keyof typeof FIND];
|
||||
if (kind) box.append(lineFind({ scope: opt.scope, 갈래: kind, 이름: line.이름 ?? "", 규격: line.규격, done: put })); // prettier-ignore
|
||||
if (line.묶음 === "R") {
|
||||
const base = createSelectField({
|
||||
options: [{ value: "", text: L("M02_SkBasePick") }].concat(
|
||||
BASES.map((b) => ({ value: b, text: b })),
|
||||
),
|
||||
compact: true,
|
||||
size: 24,
|
||||
ariaLabel: L("M02_SkBasePick"),
|
||||
onChange: (v) => v && put({ 기준: v }),
|
||||
});
|
||||
box.append(base.root);
|
||||
}
|
||||
}
|
||||
const why = createInputField({ compact: true, size: 24, placeholder: L("M02_SkDropWhy") });
|
||||
why.input.dataset.skWhy = String(line.차례);
|
||||
const drop = createButton({
|
||||
label: L("M02_SkDrop"),
|
||||
size: 24,
|
||||
variant: "ghost",
|
||||
onClick: () => put({ 뺌: why.input.value.trim() || L("M02_SkDropDefault") }),
|
||||
});
|
||||
drop.dataset.skDrop = String(line.차례);
|
||||
box.append(why.root, drop);
|
||||
if (pick) {
|
||||
const clear = createButton({ label: L("M02_SkClear"), size: 24, variant: "link", onClick: () => put(null) }); // prettier-ignore
|
||||
clear.dataset.skClear = String(line.차례);
|
||||
box.append(clear);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 블록 줄 표 */
|
||||
export function skLineTable(opt: SkLinesOptions): HTMLElement {
|
||||
const rows: Cell[][] = opt.block.줄.map((line) => {
|
||||
const pick = opt.picks[String(line.차례)];
|
||||
const v = lineView(line, pick);
|
||||
const param = Object.entries(line.매개값)
|
||||
.map(([k, n]) => `${k} ${formatNumber(n)}`)
|
||||
.join(" · ");
|
||||
const how = [line.방법, v.글].filter(Boolean).join(" · ");
|
||||
const sheet =
|
||||
line.행 !== null && opt.onSheet
|
||||
? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => opt.onSheet!(line.행!) }) // prettier-ignore
|
||||
: "";
|
||||
if (sheet) sheet.dataset.skSheet = String(line.행);
|
||||
return [
|
||||
GROUP[line.묶음] ?? line.묶음,
|
||||
line.이름 ?? "",
|
||||
line.규격,
|
||||
param,
|
||||
stateCell(line, pick),
|
||||
how,
|
||||
v.mine ? "—" : won(line.우리금액),
|
||||
won(line.엑셀금액),
|
||||
v.mine || line.차 === null || line.차 === undefined ? "—" : won(line.차),
|
||||
pickCell(opt, line, pick),
|
||||
sheet,
|
||||
];
|
||||
});
|
||||
const table = createTable(
|
||||
[
|
||||
{ label: L("M02_SkColGroup"), width: "48px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_LeftSpec") },
|
||||
{ label: L("M02_SkColParam") },
|
||||
{ label: L("M02_SkColState") },
|
||||
{ label: L("M02_SkColHow") },
|
||||
{ label: L("M02_SkColOurs"), align: "end" },
|
||||
{ label: L("M02_SkColExcel"), align: "end" },
|
||||
{ label: L("M02_SkColDiff"), align: "end" },
|
||||
{ label: L("M02_SkColPick") },
|
||||
{ label: L("M02_SkSheet"), width: "52px" },
|
||||
],
|
||||
rows,
|
||||
{ density: "data", empty: L("M02_SkNoLines") },
|
||||
);
|
||||
table.dataset.skLines = String(opt.block.번);
|
||||
return table;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 새로 올림) · 작업 중 파일 이어서
|
||||
* ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] →
|
||||
* 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 ·
|
||||
* 단가산출 체크면 단가산출 칸(`Hopyo_WizardSk` — 기초 · SK · 고름 대기 기초 고름 · 729-3) ·
|
||||
* 단가산출 체크면 단가산출 칸(`Hopyo_WizardSk` — 블록 줄 고름 · [확인] · 확인한 것만 SK · 778-5 D-5) ·
|
||||
* 안 이은 줄 띠(`Hopyo_Left` — 안 만든 호표의 남은 줄 · [고르기] = 줄마다 이을 것 · 729-3)
|
||||
* ③ [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 ·
|
||||
* 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
type LeftMap,
|
||||
type LeftScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Left";
|
||||
import { sangeunStep } from "./M02_MasterTemplete_Hopyo_WizardSk";
|
||||
import { sangeunStep, type SkStep } from "./M02_MasterTemplete_Hopyo_WizardSk";
|
||||
import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer";
|
||||
|
||||
const LAYERS: { layer: ManageLayer; text: string }[] = [
|
||||
@@ -211,6 +211,7 @@ export function buildHopyoWizard(): HopyoWizard {
|
||||
const band = el("div", { attrs: { "data-hopyo-left": "" } });
|
||||
let nos: number[] = [];
|
||||
let map: LeftMap | null = null;
|
||||
let sk: SkStep | null = null; // 단가산출 칸 — 막힘 글 「확인 안 한 블록 n · 고를 줄 m」
|
||||
const pick = createButton({
|
||||
label: L("M02_LeftOpen"),
|
||||
variant: "ghost",
|
||||
@@ -222,8 +223,10 @@ export function buildHopyoWizard(): HopyoWizard {
|
||||
function paint(got: LeftMap | null): void {
|
||||
map = got;
|
||||
const n = got ? leftCount(got) : null;
|
||||
const w = sk?.count();
|
||||
const skLeft = w?.블록 ? `${L("M02_SkLeft").replace("{n}", String(w.블록)).replace("{m}", String(w.줄))} · ` : ""; // prettier-ignore
|
||||
finish.disabled = !!n?.줄;
|
||||
finish.title = n?.줄 ? L("M02_LeftBlocked") : "";
|
||||
finish.title = n?.줄 ? skLeft + L("M02_LeftBlocked") : "";
|
||||
band.dataset.left = n ? String(n.줄) : "";
|
||||
band.replaceChildren(
|
||||
createBanner({
|
||||
@@ -231,7 +234,7 @@ export function buildHopyoWizard(): HopyoWizard {
|
||||
text: !n
|
||||
? L("M02_LeftUnknown")
|
||||
: n.줄
|
||||
? `${L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표))} · ${L("M02_LeftBlocked")}`
|
||||
? `${skLeft}${L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표))} · ${L("M02_LeftBlocked")}`
|
||||
: L("M02_LeftZero"),
|
||||
actions: [pick],
|
||||
}),
|
||||
@@ -248,14 +251,12 @@ export function buildHopyoWizard(): HopyoWizard {
|
||||
}
|
||||
const back = createButton({ label: "처음으로", variant: "link", onClick: () => void first() });
|
||||
back.title = "작업은 그대로 보관 — 처음 화면 「작업 중」에서 이어서";
|
||||
if (wantSangeun) sk = sangeunStep(scope, file.키, file.파일, () => paint(map));
|
||||
const wrong = file.어긋남 ? ` · 검산 어긋남 ${file.어긋남}` : "";
|
||||
body.replaceChildren(
|
||||
createText(`${file.파일} · ${textOf(layer)} 층 · 호표 ${file.호표수}${wrong}`, "muted", 14),
|
||||
...(wantSangeun
|
||||
? [
|
||||
createPanelHead({ title: L("M02_WizSkHead"), level: 4, divider: true }).root,
|
||||
sangeunStep(scope, file.키).root,
|
||||
]
|
||||
? [createPanelHead({ title: L("M02_WizSkHead"), level: 4, divider: true }).root, sk!.root]
|
||||
: []),
|
||||
createPanelHead({ title: L("M02_WizIlwiHead"), level: 4, divider: true }).root,
|
||||
createText(
|
||||
|
||||
@@ -1,238 +1,250 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_WizardSk.ts
|
||||
* 일위대가 불러오기 마법사 ② 단가산출 단계(PLAN 729-3 · 요구서 내역19 1장 7 · 2장 3) —
|
||||
* · [단가산출 만들기] = 서버 `sangeun`(산근 블록마다 매칭 → 같으면 기초 · 다르면 SK = 기초 + 엑셀 차이 · 맞는 기초 없음 = 고름 대기)
|
||||
* · 만든 줄 표 = 블록 · 이름 · 키 · 갈래(기초 · SK 차이 있음) · 이미 만든 블록은 「그대로」 수만
|
||||
* · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) ·
|
||||
* 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}`
|
||||
* 후보마다 줄 비교 표(746-2 · `Hopyo_Compare` · 서버 `줄비교`) — 고르기 칸 아래 · 후보 단추로 바꿔 봄
|
||||
* · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) ·
|
||||
* 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표
|
||||
* · 같은 내용(733-1) = 갈래 칸 꼬리표 「같은 것 씀」(이 층 같은 SK 를 씀) · 「위 층에 같은 것 — 연결 권함」(회사 · 마스터 · 알림만) ·
|
||||
* 수는 요약 글에
|
||||
* · 별칭 제안 = 이 층 별칭 장부에 남음 · 모두에게 쓰이려면 시스템 관리자가 M01 › 별칭 장부에서 승인(링크 · 716 길 그대로)
|
||||
* 계산 · 판정은 서버 — 화면은 보이기 · 고름만 · 자동 저장 없음(누를 때만 서버).
|
||||
* 일위대가 불러오기 마법사 ② 단가산출 칸(PLAN 778-5 D-5 · 요구 621 · 622 · 623 · 624) — 모두 줄로만 · 자동 없음.
|
||||
* · 열면 서버 줄 판정(`POST …/sangeun` 빈 확인 = 안 만듦 · D-3) → 블록 표: 단산N · 이름 · 판정(「옮길 수 있음」 ·
|
||||
* 「고를 줄 n」 — 캐시 고름 겹침) · 우리 금액 · 엑셀 금액(참고) · 근거 꼬리표(가까운 기초 이름 · 참고) · 만든 SK ·
|
||||
* [확인] 칩(고를 줄 0 일 때만 켜짐) · [줄] 펼침 = 줄 표(`Hopyo_SkLines`) · [보기] = 엑셀 보기 그 블록 첫 행
|
||||
* · 고름 · 확인 = 캐시 `m02-hopyo-sk:{층}:{주인}:{키}`(`SkPick` 꼴) — 누를 때 서버에 안 감 · 자동 저장 없음
|
||||
* · [확인한 것 옮기기 (n)] = 몸 `{확인: [{번, 고름}]}` → 서버가 M01 지금 값으로 다시 셈 · 단위 SK(확인 안 한 블록 안 만듦) ·
|
||||
* 만든 블록은 확인에서 뺌 · 못만듦 = 띠(까닭 그대로) · 「모두 확인」 단추 없음
|
||||
* · `count()` = 아직 안 옮긴 블록 · 고를 줄(마법사 [끝내기] 막힘 글)
|
||||
* ========================================================================== */
|
||||
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { tableRow, tableShell, type Column } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createToggle } from "@ui/ui_template_toggle";
|
||||
import { formatPrice } from "../common_util/common_util_money";
|
||||
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { judgeSangeun } from "./M02_MasterTemplete_Hopyo_SkApi";
|
||||
import { skLineTable } from "./M02_MasterTemplete_Hopyo_SkLines";
|
||||
import {
|
||||
makeSangeun,
|
||||
type HopyoScope,
|
||||
type SangeunBuilt,
|
||||
type SangeunPick,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { compareBrief, compareView } from "./M02_MasterTemplete_Hopyo_Compare";
|
||||
import { openLost } from "./M02_MasterTemplete_Hopyo_Lost";
|
||||
|
||||
type Made = SangeunBuilt["만듦"][number];
|
||||
GOOD,
|
||||
canConfirm,
|
||||
picksOf,
|
||||
readDraft,
|
||||
setPick,
|
||||
setSure,
|
||||
sureBody,
|
||||
tidy,
|
||||
verdictOf,
|
||||
waiting,
|
||||
type SkBlock,
|
||||
type SkDraft,
|
||||
type SkJudged,
|
||||
} from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const won = (v: number | null | undefined): string =>
|
||||
v === null || v === undefined ? "—" : formatPrice(v);
|
||||
|
||||
/** 고름 대기 후보 글 — 「이름 [단위] · 판정 · 글」 */
|
||||
export const candLabel = (c: { 이름: string; 단위: string; 판정: string; 글: string }): string =>
|
||||
`${c.이름} [${c.단위}] · ${c.판정}${c.글 && c.글 !== c.판정 ? ` · ${c.글}` : ""}`;
|
||||
export const skDraftKey = (scope: HopyoScope, key: string): string =>
|
||||
`m02-hopyo-sk:${scope.layer}:${scope.owner ?? ""}:${key}`;
|
||||
|
||||
/** 만든 갈래 꼬리표 — 기초 = 초록 · SK = 차이 있음 */
|
||||
const kindBadge = (kind: string): HTMLElement =>
|
||||
kind === "SK"
|
||||
? createBadge(L("M02_WizSkDiff"), "accent")
|
||||
: createBadge(L("M02_WizSkBase"), "success");
|
||||
export interface SkStep {
|
||||
root: HTMLElement;
|
||||
/** 아직 안 옮긴 블록 · 고를 줄 — 판정 전 null */
|
||||
count: () => { 블록: number; 줄: number } | null;
|
||||
}
|
||||
|
||||
/** ② 단가산출 단계 칸 — `root` 를 마법사 본 칸에 붙임 */
|
||||
export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement } {
|
||||
/** ② 단가산출 칸 — `파일` = 엑셀 보기 창 머리 · `onChange` = 판정 · 옮김 뒤(끝내기 글) */
|
||||
export function sangeunStep(
|
||||
scope: HopyoScope,
|
||||
key: string,
|
||||
파일: string,
|
||||
onChange?: () => void,
|
||||
): SkStep {
|
||||
const dk = skDraftKey(scope, key);
|
||||
const load = (): SkDraft => readDraft(sessionStorage.getItem(dk));
|
||||
const save = (d: SkDraft): void => sessionStorage.setItem(dk, JSON.stringify(d));
|
||||
let got: SkJudged | null = null;
|
||||
let fails: SkJudged["못만듦"] = [];
|
||||
const open = new Set<number>(); // 펼친 블록
|
||||
const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } });
|
||||
const make = createButton({
|
||||
label: L("M02_WizSkMake"),
|
||||
variant: "filled",
|
||||
onClick: () => void run(),
|
||||
});
|
||||
make.title = L("M02_WizSkMakeTip");
|
||||
make.dataset.hopyoSangeunMake = "";
|
||||
const again = createButton({ label: L("M02_SkJudge"), variant: "ghost", size: 28, onClick: () => void judge() }); // prettier-ignore
|
||||
again.dataset.skJudge = "";
|
||||
const move = createButton({ label: L("M02_SkMove").replace("{n}", "0"), variant: "filled", size: 28, disabled: true, onClick: () => void run() }); // prettier-ignore
|
||||
move.title = L("M02_SkMoveTip");
|
||||
move.dataset.skMove = "";
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo",
|
||||
children: [el("div", { className: "ui-toolbar", children: [make] }), out],
|
||||
children: [
|
||||
createText(L("M02_SkHint"), "hint", 14),
|
||||
out,
|
||||
el("div", { className: "ui-toolbar", children: [move, again] }),
|
||||
],
|
||||
});
|
||||
|
||||
const made = new Map<number, Made>(); // 이 칸에서 만든 줄(누를 때마다 더함)
|
||||
const reused = new Set<number>(); // 733-1 같은 것 씀 블록
|
||||
const above = new Map<number, string[]>(); // 733-1 위 층 같은 것 「층 키」
|
||||
async function run(picks?: SangeunPick): Promise<void> {
|
||||
make.disabled = true;
|
||||
const sheet = (행: number): void =>
|
||||
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
||||
.then((m) =>
|
||||
m.openHopyoSheet({ scope, key, 파일, canWrite: true, target: scope, 단산행: 행 }),
|
||||
)
|
||||
.catch((error: unknown) => showToast(why(error), "error"));
|
||||
|
||||
async function judge(): Promise<void> {
|
||||
again.disabled = true;
|
||||
out.replaceChildren(createText(L("M02_SkJudging"), "muted", 14));
|
||||
try {
|
||||
const got = await makeSangeun(scope, key, picks);
|
||||
draw(got);
|
||||
showToast(summary(got), got.고름대기.length ? "warning" : "success");
|
||||
got = await judgeSangeun(scope, key);
|
||||
fails = [];
|
||||
draw();
|
||||
} catch (error) {
|
||||
out.replaceChildren(createBanner({ tone: "warning", text: why(error) }));
|
||||
} finally {
|
||||
again.disabled = false;
|
||||
onChange?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
if (!got) return;
|
||||
const body = sureBody(load(), got.판정);
|
||||
if (!body.length) return showToast(L("M02_SkSureFirst"), "warning");
|
||||
move.disabled = true;
|
||||
try {
|
||||
const next = await judgeSangeun(scope, key, body);
|
||||
const made = new Set(next.만듦.map((x) => x.번));
|
||||
const d = load();
|
||||
save({ ...d, 확인: d.확인.filter((n) => !made.has(n)) });
|
||||
got = next;
|
||||
fails = next.못만듦;
|
||||
showToast(
|
||||
L("M02_SkMoved")
|
||||
.replace("{made}", String(made.size))
|
||||
.replace("{bad}", String(fails.length)),
|
||||
fails.length ? "warning" : "success",
|
||||
);
|
||||
draw();
|
||||
} catch (error) {
|
||||
showToast(why(error), "error");
|
||||
draw();
|
||||
} finally {
|
||||
make.disabled = false;
|
||||
onChange?.();
|
||||
}
|
||||
}
|
||||
|
||||
const summary = (got: SangeunBuilt): string =>
|
||||
L("M02_WizSkSummary")
|
||||
.replace("{b}", String(got.블록))
|
||||
.replace("{base}", String(got.기초))
|
||||
.replace("{sk}", String(got.SK))
|
||||
.replace("{keep}", String(got.그대로))
|
||||
.replace("{wait}", String(got.고름대기.length)) +
|
||||
(got.같은것씀?.length || got.위층같음?.length
|
||||
? L("M02_WizSkSameSum")
|
||||
.replace("{same}", String(got.같은것씀?.length ?? 0))
|
||||
.replace("{above}", String(new Set(got.위층같음?.map((x) => x.번)).size))
|
||||
: "");
|
||||
const cols: Column[] = [
|
||||
{ label: L("M02_SkColBlock"), width: "72px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_SangeunColUnit"), width: "56px" },
|
||||
{ label: L("M02_SkColVerdict") },
|
||||
{ label: L("M02_SkColOurs"), align: "end" },
|
||||
{ label: L("M02_SkColExcel"), align: "end" },
|
||||
{ label: L("M02_SkColBasis") },
|
||||
{ label: L("M02_SkColMade"), width: "110px" },
|
||||
{ label: L("M02_SkSure"), width: "72px" },
|
||||
{ label: "", width: "96px" },
|
||||
];
|
||||
|
||||
/** 갈래 칸 — 갈래 + 같은 것 씀 · 위 층 같은 것 꼬리표 */
|
||||
const kindCell = (x: Made): HTMLElement => {
|
||||
const tags: HTMLElement[] = [kindBadge(x.갈래)];
|
||||
if (reused.has(x.번)) {
|
||||
const tag = createBadge(L("M02_WizSkSame"), "info");
|
||||
tag.title = L("M02_WizSkSameTip");
|
||||
tag.dataset.wizSame = String(x.번);
|
||||
tags.push(tag);
|
||||
}
|
||||
const up = above.get(x.번);
|
||||
if (up?.length) {
|
||||
const tag = createBadge(L("M02_WizSkAbove"), "warning");
|
||||
tag.title = up.join(" · ");
|
||||
tag.dataset.wizAbove = String(x.번);
|
||||
tags.push(tag);
|
||||
}
|
||||
return el("div", { className: "ui-toolbar", children: tags });
|
||||
};
|
||||
|
||||
/** 못옮김 칸 — 수 + [잇기](남은 것 있을 때) */
|
||||
const lostCell = (x: Made, got: SangeunBuilt): HTMLElement => {
|
||||
const n = x.못옮김?.length ?? 0;
|
||||
if (!n) return el("span", { text: "0" });
|
||||
const go = createButton({
|
||||
label: `${L("M02_LostOpen")} (${n})`,
|
||||
/** 블록 한 줄 + 펼친 줄 표 */
|
||||
function blockRows(b: SkBlock, d: SkDraft): HTMLTableRowElement[] {
|
||||
const picks = picksOf(d, b.번);
|
||||
const word = verdictOf(b, picks);
|
||||
const verdict = createBadge(word, word === GOOD ? "success" : "info");
|
||||
verdict.dataset.skVerdict = String(b.번);
|
||||
const basis = b.근거 ? createBadge(`${L("M02_SkBasisTag")} ${b.근거.이름 ?? b.근거.로직}`, "neutral") : "—"; // prettier-ignore
|
||||
if (basis instanceof HTMLElement) basis.title = `${b.근거!.로직} · ${b.근거!.판정 ?? ""}`;
|
||||
const made = b.만든 ? createBadge(b.만든, "success") : "—";
|
||||
const ok = canConfirm(b, picks);
|
||||
const sure = createToggle({
|
||||
label: L("M02_SkSure"),
|
||||
kind: "chip",
|
||||
size: 24,
|
||||
onClick: () =>
|
||||
openLost({ scope, key, 번: x.번, sk: x.키, 이름: x.이름, lost: x.못옮김 ?? [],
|
||||
done: (next) => draw({ ...got, 만듦: next.만듦, 별칭: got.별칭 }) }), // prettier-ignore
|
||||
});
|
||||
go.dataset.wizLost = String(x.번);
|
||||
return go;
|
||||
};
|
||||
|
||||
function draw(got: SangeunBuilt): void {
|
||||
for (const x of got.남은못옮김 ?? []) if (!made.has(x.번)) made.set(x.번, { ...x, 갈래: "SK" });
|
||||
for (const x of got.만듦) made.set(x.번, x);
|
||||
for (const x of got.같은것씀 ?? []) reused.add(x.번);
|
||||
for (const x of got.위층같음 ?? []) {
|
||||
const at = x.층 === "system" ? L("M02_PickerSystem") : L("M02_PickerCompany");
|
||||
above.set(x.번, [...(above.get(x.번) ?? []), `${at} ${x.키}`]);
|
||||
}
|
||||
const table = createTable(
|
||||
[
|
||||
{ label: L("M02_WizSkBlock"), width: "80px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_WizSkKey"), width: "110px" },
|
||||
{ label: L("M02_WizSkKind"), width: "260px" },
|
||||
{ label: L("M02_LostCol"), width: "120px" },
|
||||
],
|
||||
[...made.values()]
|
||||
.sort((a, b) => a.번 - b.번)
|
||||
.map((x) => [`단산${x.번}`, x.이름, x.키, kindCell(x), lostCell(x, got)]),
|
||||
{ density: "data", empty: L("M02_WizSkNoneMade") },
|
||||
);
|
||||
const selects = new Map<number, HTMLSelectElement>();
|
||||
const coefs = new Map<number, HTMLInputElement>();
|
||||
const wait = createTable(
|
||||
[
|
||||
{ label: L("M02_WizSkBlock"), width: "80px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_SangeunColUnit"), width: "64px" },
|
||||
{ label: L("M02_LeftWhy") },
|
||||
{ label: L("M02_LeftBase"), width: "62%" },
|
||||
],
|
||||
got.고름대기.map((x) => {
|
||||
const pick = createSelectField({
|
||||
options: [{ value: "", text: L("M02_WizSkNoPick") }].concat(
|
||||
x.후보.map((c) => ({
|
||||
value: c.로직,
|
||||
text: candLabel(c) + (c.줄비교 ? ` · ${compareBrief(c.줄비교)}` : ""),
|
||||
})),
|
||||
),
|
||||
compact: true,
|
||||
size: 24,
|
||||
ariaLabel: `${L("M02_LeftBase")} 단산${x.번}`,
|
||||
});
|
||||
pick.select.dataset.wizWait = String(x.번);
|
||||
selects.set(x.번, pick.select);
|
||||
const coef = createInputField({ type: "number", min: 0, compact: true, size: 24,
|
||||
placeholder: L("M02_WizSkCoef") }); // prettier-ignore
|
||||
coef.input.dataset.wizCoef = String(x.번);
|
||||
const need = (): void => {
|
||||
const c = x.후보.find((y) => y.로직 === pick.select.value)?.계수;
|
||||
coef.root.hidden = !c;
|
||||
coef.input.title = c ? `${c.가} 1 = ${c.나} ?` : "";
|
||||
};
|
||||
const cmp = compareView(x.후보);
|
||||
pick.select.addEventListener("change", () => {
|
||||
need();
|
||||
cmp?.show(pick.select.value);
|
||||
});
|
||||
need();
|
||||
coefs.set(x.번, coef.input);
|
||||
return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { children: [el("div", { className: "ui-toolbar", children: [pick.root, coef.root] }), ...(cmp ? [cmp.root] : [])] })]; // prettier-ignore
|
||||
}),
|
||||
{ density: "data", empty: L("M02_WizSkNoWait") },
|
||||
);
|
||||
const again = createButton({
|
||||
label: L("M02_WizSkRemake"),
|
||||
variant: "filled",
|
||||
size: 28,
|
||||
onClick: () => {
|
||||
const picks: SangeunPick = {};
|
||||
for (const [no, s] of selects) {
|
||||
if (!s.value) continue;
|
||||
const c = Number(coefs.get(no)?.value);
|
||||
const need = got.고름대기.find((x) => x.번 === no)?.후보.find((y) => y.로직 === s.value)?.계수; // prettier-ignore
|
||||
if (need && !(c > 0)) return showToast(L("M02_WizSkCoefNeed").replace("{n}", String(no)), "warning"); // prettier-ignore
|
||||
picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) };
|
||||
}
|
||||
if (!Object.keys(picks).length) return showToast(L("M02_WizSkPickFirst"), "warning");
|
||||
void run(picks);
|
||||
checked: d.확인.includes(b.번),
|
||||
disabled: !ok,
|
||||
onChange: (on) => {
|
||||
save(setSure(load(), b, on));
|
||||
draw();
|
||||
},
|
||||
});
|
||||
again.dataset.wizRemake = "";
|
||||
const ledger = createButton({
|
||||
label: L("M02_WizSkLedger"),
|
||||
variant: "link",
|
||||
onClick: () => void window.open(`#/${ROUTES.M01_MASTER_DATA}`, "_blank"),
|
||||
sure.root.dataset.skSure = String(b.번);
|
||||
sure.root.title = ok ? L("M02_SkSureTip") : L("M02_SkSureOff");
|
||||
const lines = createButton({
|
||||
label: `${L("M02_SkOpen")} ${b.줄.length}`,
|
||||
size: 24,
|
||||
variant: open.has(b.번) ? "filled" : "ghost",
|
||||
onClick: () => {
|
||||
if (open.has(b.번)) open.delete(b.번);
|
||||
else open.add(b.번);
|
||||
draw();
|
||||
},
|
||||
});
|
||||
out.replaceChildren(
|
||||
createText(summary(got), "muted", 14),
|
||||
table,
|
||||
...(got.고름대기.length
|
||||
? [
|
||||
createText(L("M02_WizSkWaitHint"), "hint", 14),
|
||||
wait,
|
||||
el("div", { className: "ui-toolbar", children: [again] }),
|
||||
]
|
||||
: []),
|
||||
el("div", {
|
||||
className: "ui-toolbar",
|
||||
children: [
|
||||
createText(L("M02_WizSkAlias").replace("{n}", String(got.별칭 ?? 0)), "muted", 14),
|
||||
ledger,
|
||||
],
|
||||
}),
|
||||
);
|
||||
lines.dataset.skOpen = String(b.번);
|
||||
const first = b.줄.find((l) => l.행 !== null)?.행 ?? null;
|
||||
const view = first !== null ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => sheet(first) }) : ""; // prettier-ignore
|
||||
const head = tableRow(cols, [
|
||||
`단산${b.번}`,
|
||||
b.이름 ?? "",
|
||||
b.단위 ?? "",
|
||||
verdict,
|
||||
won(b.우리금액),
|
||||
won(b.엑셀금액),
|
||||
basis,
|
||||
made,
|
||||
sure.root,
|
||||
el("div", { className: "ui-toolbar", children: [lines, view] }),
|
||||
]);
|
||||
head.dataset.skBlock = String(b.번);
|
||||
if (!open.has(b.번)) return [head];
|
||||
const table = skLineTable({
|
||||
scope,
|
||||
block: b,
|
||||
picks,
|
||||
onPick: (at, p) => {
|
||||
save(tidy(setPick(load(), b.번, at, p), got!.판정));
|
||||
draw();
|
||||
},
|
||||
onSheet: sheet,
|
||||
});
|
||||
const sub = tableRow(cols, [{ content: table, colSpan: cols.length }]);
|
||||
sub.className = "is-sub";
|
||||
return [head, sub];
|
||||
}
|
||||
|
||||
return { root };
|
||||
function draw(): void {
|
||||
if (!got) return;
|
||||
const d = tidy(load(), got.판정);
|
||||
save(d);
|
||||
const blocks = got.판정;
|
||||
const good = blocks.filter((b) => canConfirm(b, picksOf(d, b.번))).length;
|
||||
const made = blocks.filter((b) => b.만든).length;
|
||||
const sum = L("M02_SkSummary")
|
||||
.replace("{b}", String(blocks.length))
|
||||
.replace("{ok}", String(good))
|
||||
.replace("{wait}", String(blocks.length - good))
|
||||
.replace("{made}", String(made))
|
||||
.replace("{sure}", String(d.확인.length));
|
||||
const { root: table, tbody } = tableShell(cols, { density: "data" });
|
||||
for (const b of blocks) tbody.append(...blockRows(b, d));
|
||||
const band = fails.length
|
||||
? [
|
||||
createBanner({
|
||||
tone: "warning",
|
||||
text: L("M02_SkFailed").replace("{n}", String(fails.length)),
|
||||
details: fails.map((x) => `단산${x.번} ${x.이름 ?? ""} — ${x.까닭}`).join("\n"),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
out.replaceChildren(
|
||||
createText(sum, "muted", 14),
|
||||
...band,
|
||||
blocks.length ? table : createText(L("M02_SkNone"), "muted", 14),
|
||||
);
|
||||
move.disabled = !d.확인.length;
|
||||
move.querySelector(".ui-btn__label")!.textContent = L("M02_SkMove").replace(
|
||||
"{n}",
|
||||
String(d.확인.length),
|
||||
);
|
||||
onChange?.();
|
||||
}
|
||||
|
||||
void judge();
|
||||
return {
|
||||
root,
|
||||
count: () => (got ? waiting(load(), got.판정) : null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts
|
||||
* 마법사 단가산출 줄 고름(PLAN 778-5 D-5 · 요구 621 · 622 · 623 · 624) — 순수 셈.
|
||||
* · 재료 = 서버 `POST …/hopyo/{키}/sangeun` 답 `판정 [{번 … 줄 […]}]`(D-3 `M02_Hopyo_Sangeun.build`)
|
||||
* · 고름 캐시 `{고름 {번: {차례: 고름}}, 확인 [번]}` — 줄 고름이 서버 상태를 덮음(꼬리표 「내가 고름」)
|
||||
* · 블록 [확인] = 남은 고를 줄 0 일 때만 · 몸 = `{확인: [{번, 고름?}]}`(확인한 블록만 · 자동 확인 없음)
|
||||
* · 금액 · 판정 정본은 서버 — 여기는 캐시를 겹쳐 보이기 · 몸 짓기만
|
||||
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
||||
* ========================================================================== */
|
||||
|
||||
export type SkGroup = "L" | "X" | "M" | "R" | "S" | "E";
|
||||
|
||||
/** 줄 고름 — 노임 LB · 기계 EQ · 자재 MT · 율 기준 · 뺌 + 까닭 */
|
||||
export type SkPick =
|
||||
| { 노임: string }
|
||||
| { 기계: string }
|
||||
| { 자재: string; 별도계상?: boolean }
|
||||
| { 기준: SkBase; 비목?: string }
|
||||
| { 뺌: string };
|
||||
|
||||
/** 율 기준 — 묶음 · 비목 · 줄 목록(그 엑셀 행 줄) */
|
||||
export type SkBase = string | { 행: number[] };
|
||||
|
||||
/** 서버 후보 — 노임 · 기계 = `키` · 자재 = `자재` · 율 = `기준` */
|
||||
export interface SkCand {
|
||||
키?: string;
|
||||
자재?: string;
|
||||
기준?: SkBase;
|
||||
비목?: string;
|
||||
갈래?: string;
|
||||
이름?: string | null;
|
||||
규격?: string;
|
||||
단위?: string;
|
||||
구분?: string;
|
||||
단가?: number | null;
|
||||
글?: string;
|
||||
}
|
||||
|
||||
export interface SkLine {
|
||||
차례: number;
|
||||
행: number | null;
|
||||
묶음: SkGroup;
|
||||
이름: string | null;
|
||||
규격: string;
|
||||
매개값: Record<string, number>;
|
||||
엑셀금액: number | null;
|
||||
상태: string;
|
||||
방법: string;
|
||||
단위: unknown;
|
||||
후보: SkCand[];
|
||||
꼬리표: string[];
|
||||
글: string;
|
||||
우리금액?: number | null;
|
||||
차?: number | null;
|
||||
}
|
||||
|
||||
export interface SkBlock {
|
||||
번: number;
|
||||
이름: string | null;
|
||||
단위: string | null;
|
||||
판정: string;
|
||||
고를줄: number;
|
||||
우리금액: number | null;
|
||||
엑셀금액: number | null;
|
||||
근거: { 로직: string; 이름: string | null; 판정: string | null } | null;
|
||||
만든: string | null;
|
||||
줄: SkLine[];
|
||||
}
|
||||
|
||||
export interface SkJudged {
|
||||
블록: number;
|
||||
판정: SkBlock[];
|
||||
만듦: { 번: number; 이름: string | null; 키: string; 갈래: string; 못옮김: unknown[] }[];
|
||||
못만듦: { 번: number; 이름: string | null; 까닭: string }[];
|
||||
그대로: number;
|
||||
같은것씀: { 번: number; 키: string }[];
|
||||
위층같음: { 번: number; 층: string; 주인: string | null; 키: string }[];
|
||||
}
|
||||
|
||||
/** 확인 목록 한 칸(서버 몸) */
|
||||
export interface SkSure {
|
||||
번: number;
|
||||
고름?: Record<number, SkPick>;
|
||||
}
|
||||
|
||||
/** 캐시 꼴 — 블록 번 · 줄 차례는 JSON 열쇠라 글 */
|
||||
export interface SkDraft {
|
||||
고름: Record<string, Record<string, SkPick>>;
|
||||
확인: number[];
|
||||
}
|
||||
|
||||
export const MINE = "내가 고름";
|
||||
export const GOOD = "옮길 수 있음";
|
||||
export const LINKED = "이어짐";
|
||||
export const PICK = "고를 것";
|
||||
export const OUT = "밖";
|
||||
export const DROP = "뺌";
|
||||
export const GROUP: Record<SkGroup, string> = {
|
||||
L: "노무",
|
||||
X: "기계",
|
||||
M: "자재",
|
||||
R: "율",
|
||||
S: "하위",
|
||||
E: "값",
|
||||
};
|
||||
/** 율 기준 고르기 칸 */
|
||||
export const BASES = ["인력", "재료", "노무비", "재료비", "경비"];
|
||||
const DONE = new Set([LINKED, OUT, DROP]);
|
||||
|
||||
export const emptyDraft = (): SkDraft => ({ 고름: {}, 확인: [] });
|
||||
|
||||
/** 캐시 글 → 꼴(틀리면 빈 것) */
|
||||
export function readDraft(raw: string | null): SkDraft {
|
||||
try {
|
||||
const got = JSON.parse(raw ?? "") as Partial<SkDraft> | null;
|
||||
const picks = got?.고름 && typeof got.고름 === "object" ? got.고름 : {};
|
||||
const sure = Array.isArray(got?.확인) ? got.확인.filter((n) => Number.isInteger(n)) : [];
|
||||
return { 고름: picks, 확인: sure };
|
||||
} catch {
|
||||
return emptyDraft();
|
||||
}
|
||||
}
|
||||
|
||||
/** 고른 것 글 — 키 · 율 기준 · 뺌 까닭 */
|
||||
export function pickText(p: SkPick): string {
|
||||
if ("뺌" in p) return `${DROP} · ${p.뺌}`;
|
||||
if ("기준" in p) {
|
||||
const base = typeof p.기준 === "string" ? p.기준 : `${p.기준.행.join(" · ")} 행`;
|
||||
return `기준 ${base}${p.비목 ? ` · ${p.비목}` : ""}`;
|
||||
}
|
||||
return "노임" in p ? p.노임 : "기계" in p ? p.기계 : p.자재;
|
||||
}
|
||||
|
||||
/** 줄 보이기 — 캐시 고름이 서버 상태를 덮음 */
|
||||
export function lineView(
|
||||
line: SkLine,
|
||||
pick: SkPick | undefined,
|
||||
): { 상태: string; 꼬리표: string[]; 글: string; mine: boolean } {
|
||||
if (!pick) return { 상태: line.상태, 꼬리표: line.꼬리표, 글: line.글, mine: false };
|
||||
return { 상태: "뺌" in pick ? DROP : LINKED, 꼬리표: [MINE], 글: pickText(pick), mine: true };
|
||||
}
|
||||
|
||||
/** 그 블록 캐시 고름 `{차례: 고름}` */
|
||||
export const picksOf = (d: SkDraft, 번: number): Record<string, SkPick> => d.고름[String(번)] ?? {};
|
||||
|
||||
/** 남은 고를 줄 수 — 이어짐 · 밖 · 뺌 아닌 줄(캐시 고름 겹침) */
|
||||
export const leftLines = (b: SkBlock, picks: Record<string, SkPick>): number =>
|
||||
b.줄.filter((l) => !DONE.has(lineView(l, picks[String(l.차례)]).상태)).length;
|
||||
|
||||
/** [확인] 켜짐 — 고를 줄 0 */
|
||||
export const canConfirm = (b: SkBlock, picks: Record<string, SkPick>): boolean =>
|
||||
leftLines(b, picks) === 0;
|
||||
|
||||
/** 블록 판정 글 — 캐시 겹친 뒤 「옮길 수 있음」 · 「고를 줄 n」 */
|
||||
export function verdictOf(b: SkBlock, picks: Record<string, SkPick>): string {
|
||||
const n = leftLines(b, picks);
|
||||
return n ? `고를 줄 ${n}` : GOOD;
|
||||
}
|
||||
|
||||
/** 줄 고를 수 있나 — 밖 줄 = 아님 · 하위 · 값 = 뺌만 */
|
||||
export const pickable = (l: SkLine): boolean => l.상태 !== OUT;
|
||||
export const dropOnly = (l: SkLine): boolean => l.묶음 === "S" || l.묶음 === "E";
|
||||
|
||||
/** 서버 후보 → 고름(그 줄 묶음 꼴 · 아니면 null) */
|
||||
export function candPick(group: SkGroup, c: SkCand): SkPick | null {
|
||||
if (group === "L" && c.키?.startsWith("LB")) return { 노임: c.키 };
|
||||
if (group === "X" && c.키?.startsWith("EQ")) return { 기계: c.키 };
|
||||
if (group === "M" && c.자재) return { 자재: c.자재 };
|
||||
if (group === "R" && c.기준 !== undefined && c.기준 !== null)
|
||||
return { 기준: c.기준, ...(c.비목 ? { 비목: c.비목 } : {}) };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 후보 글 — 「이름 규격 [단위]」 · 율 = 기준 */
|
||||
export function candText(c: SkCand): string {
|
||||
if (c.기준 !== undefined) return pickText({ 기준: c.기준, ...(c.비목 ? { 비목: c.비목 } : {}) });
|
||||
const spec = c.규격 ? ` ${c.규격}` : "";
|
||||
const unit = c.단위 ? ` [${c.단위}]` : "";
|
||||
return `${c.이름 ?? c.키 ?? c.자재 ?? ""}${spec}${unit}${c.구분 ? ` · ${c.구분}` : ""}`;
|
||||
}
|
||||
|
||||
/** 캐시 정리 — 고를 줄 남은 블록 · 이미 옮겨 판정 없는 블록은 확인에서 뺌 */
|
||||
export function tidy(d: SkDraft, blocks: SkBlock[]): SkDraft {
|
||||
const by = new Map(blocks.map((b) => [b.번, b]));
|
||||
const sure = d.확인.filter((n) => {
|
||||
const b = by.get(n);
|
||||
return !!b && canConfirm(b, picksOf(d, n));
|
||||
});
|
||||
return { ...d, 확인: [...new Set(sure)].sort((a, b) => a - b) };
|
||||
}
|
||||
|
||||
/** [확인한 것 옮기기] 몸 — 확인한 블록만 · 고름은 그 블록 줄에 있는 차례만 */
|
||||
export function sureBody(d: SkDraft, blocks: SkBlock[]): SkSure[] {
|
||||
const by = new Map(blocks.map((b) => [b.번, b]));
|
||||
return tidy(d, blocks).확인.map((n) => {
|
||||
const have = new Set(by.get(n)!.줄.map((l) => l.차례));
|
||||
const picks = Object.entries(picksOf(d, n)).filter(([at]) => have.has(Number(at)));
|
||||
return picks.length
|
||||
? { 번: n, 고름: Object.fromEntries(picks.map(([at, p]) => [Number(at), p])) }
|
||||
: { 번: n };
|
||||
});
|
||||
}
|
||||
|
||||
/** 마법사 [끝내기] 글 재료 — 아직 안 옮긴 블록 수 · 그 블록 고를 줄 합 */
|
||||
export function waiting(d: SkDraft, blocks: SkBlock[]): { 블록: number; 줄: number } {
|
||||
const open = blocks.filter((b) => !b.만든);
|
||||
return { 블록: open.length, 줄: open.reduce((n, b) => n + leftLines(b, picksOf(d, b.번)), 0) };
|
||||
}
|
||||
|
||||
/** 줄 고름 넣기 · 지움(null) — 새 캐시 */
|
||||
export function setPick(d: SkDraft, 번: number, 차례: number, p: SkPick | null): SkDraft {
|
||||
const mine = { ...picksOf(d, 번) };
|
||||
if (p) mine[String(차례)] = p;
|
||||
else delete mine[String(차례)];
|
||||
return { ...d, 고름: { ...d.고름, [String(번)]: mine } };
|
||||
}
|
||||
|
||||
/** [확인] 켬 · 끔 — 고를 줄 남으면 안 켬 */
|
||||
export function setSure(d: SkDraft, b: SkBlock, on: boolean): SkDraft {
|
||||
const rest = d.확인.filter((n) => n !== b.번);
|
||||
const ok = on && canConfirm(b, picksOf(d, b.번));
|
||||
return { ...d, 확인: ok ? [...rest, b.번].sort((x, y) => x - y) : rest };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 729-3 안 이은 줄 화면 헬퍼 — `Hopyo_Left` · `Hopyo_Check` · `Api_Fetch` · `Labor_Unlinked` · `Hopyo_WizardSk` 도우미를
|
||||
/* 729-3 안 이은 줄 화면 헬퍼 — `Hopyo_Left` · `Hopyo_Check` · `Api_Fetch` · `Labor_Unlinked` 도우미를
|
||||
* 트랜스파일해 Node 로 돌림 · fetch 대역(부른 길 · 몸 모음 · 409 몸) · sessionStorage 대역 ·
|
||||
* stdin = {one, made, left, fixes} · stdout = JSON. */
|
||||
const fs = require("fs");
|
||||
@@ -54,7 +54,6 @@ global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4)
|
||||
const left = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Left.ts"));
|
||||
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
|
||||
const grid = loadTs(path.join(M02, "M02_MasterTemplete_Labor_Unlinked.ts"));
|
||||
const sk = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_WizardSk.ts"));
|
||||
const scope = { layer: "personal", owner: null };
|
||||
const at = { scope, target: scope, key: "HP-000001" };
|
||||
|
||||
@@ -87,13 +86,6 @@ global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4)
|
||||
unlinkedCalls: calls.filter((x) => x.url.includes("/hopyo-unlinked")).map((x) => x.url),
|
||||
lastBody: calls.filter((x) => x.url.includes("/hopyo-unlinked")).pop().body,
|
||||
fixes: grid.unlinkedFixes({ 담은로직: input.fixes }).map((x) => x.이름),
|
||||
cand: sk.candLabel({ 이름: "되메우기", 단위: "㎥", 판정: "후보", 글: "후보" }),
|
||||
cand2: sk.candLabel({
|
||||
이름: "되메우기",
|
||||
단위: "㎥",
|
||||
판정: "다른 점",
|
||||
글: "보통인부 품 다름",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
})().catch((error) => {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 ·
|
||||
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } };
|
||||
const stub = new Proxy({}, { get: () => () => {} });
|
||||
const { loadTs } = makeLoader({
|
||||
root: ROOT,
|
||||
requireFor: (full) => (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith(".") && !request.endsWith(".css"))
|
||||
return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
|
||||
return stub;
|
||||
},
|
||||
});
|
||||
global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
|
||||
|
||||
(async () => {
|
||||
const { blocks } = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const calls = [];
|
||||
global.fetch = async (url, init = {}) => {
|
||||
calls.push({
|
||||
url,
|
||||
method: init.method ?? "GET",
|
||||
body: init.body ? JSON.parse(init.body) : null,
|
||||
});
|
||||
return { status: 200, ok: true, json: async () => ({ 판정: blocks, 만듦: [], 못만듦: [] }) };
|
||||
};
|
||||
const M02 = path.join(ROOT, "M02_MasterTemplete");
|
||||
const sk = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_SkPick.ts"));
|
||||
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SkApi.ts"));
|
||||
const [a, b, c] = blocks; // a = 고를 줄 2 · b = 옮길 수 있음 · c = 못 이음(하위)
|
||||
const out = {};
|
||||
let d = sk.emptyDraft();
|
||||
out.start = blocks.map((x) => ({
|
||||
번: x.번,
|
||||
판정: sk.verdictOf(x, {}),
|
||||
확인켜짐: sk.canConfirm(x, {}),
|
||||
}));
|
||||
out.emptyBody = sk.sureBody(d, blocks); // 아무것도 안 눌렀으면 몸 비움(자동 확인 없음)
|
||||
d = sk.setSure(d, a, true); // 고를 줄 남은 블록 = 안 켜짐
|
||||
out.sureBlocked = d.확인;
|
||||
d = sk.setPick(d, a.번, 1, { 노임: "LB000099" }); // 작업반장 → 다른 노임
|
||||
out.half = {
|
||||
판정: sk.verdictOf(a, sk.picksOf(d, a.번)),
|
||||
켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)),
|
||||
};
|
||||
d = sk.setPick(d, a.번, 2, { 자재: "MT000123" });
|
||||
d = sk.setPick(d, a.번, 3, { 뺌: "우리에 없음" });
|
||||
out.view = a.줄.map((l) => sk.lineView(l, sk.picksOf(d, a.번)[String(l.차례)]));
|
||||
out.full = {
|
||||
판정: sk.verdictOf(a, sk.picksOf(d, a.번)),
|
||||
켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)),
|
||||
};
|
||||
d = sk.setSure(d, a, true);
|
||||
d = sk.setSure(d, b, true);
|
||||
d = sk.setSure(d, c, true); // 못 이음 남음 = 안 켜짐
|
||||
out.sure = d.확인;
|
||||
out.body = sk.sureBody(d, blocks);
|
||||
out.wait = sk.waiting(d, blocks);
|
||||
// 고름 지우면 확인에서 빠짐(tidy)
|
||||
out.tidy = sk.tidy(sk.setPick(d, a.번, 2, null), blocks).확인;
|
||||
// 캐시 글 왕복 · 틀린 글
|
||||
out.round = sk.readDraft(JSON.stringify(d));
|
||||
out.bad = sk.readDraft("{엉터리");
|
||||
out.cand = {
|
||||
L: sk.candPick("L", { 키: "LB000001", 이름: "작업반장" }),
|
||||
X: sk.candPick("X", { 키: "EQ000019", 이름: "굴착기" }),
|
||||
M: sk.candPick("M", { 자재: "MT000001", 이름: "시멘트" }),
|
||||
R: sk.candPick("R", { 기준: "노무비", 비목: "경비" }),
|
||||
Mbad: sk.candPick("M", { 키: "LB000001" }),
|
||||
text: sk.candText({ 이름: "합판", 규격: "12mm", 단위: "장" }),
|
||||
};
|
||||
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001");
|
||||
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body);
|
||||
out.calls = calls;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error?.stack ?? error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -84,8 +84,6 @@ def test_남은_줄_고름_몸_막힘_판정():
|
||||
assert got["lastBody"]["줄"] == got["body"] and got["lastBody"]["번"] == 2
|
||||
# 일위대가표 띠 = 이음 · 비율 없는 고정 줄만
|
||||
assert got["fixes"] == ["안이음"]
|
||||
assert got["cand"] == "되메우기 [㎥] · 후보"
|
||||
assert got["cand2"] == "되메우기 [㎥] · 다른 점 · 보통인부 품 다름"
|
||||
|
||||
|
||||
def _text(name: str) -> str:
|
||||
@@ -98,15 +96,13 @@ def test_마법사_끝내기_막음_단가산출_칸_목록_열():
|
||||
assert end.index("await recount()") < end.index("showConfirmDialog(") # 끝내기 전 다시 셈
|
||||
assert "if (leftCount(got).줄)" in end and "openLeft(" in end # 남으면 막음 · 표
|
||||
assert "finish.disabled = !!n?.줄;" in wizard # 띠 · 남은 수 · [끝내기] 막음
|
||||
assert "sangeunStep(scope, file.키)" in wizard and "/sangeun`" not in wizard
|
||||
sk = _text("M02_MasterTemplete_Hopyo_WizardSk.ts")
|
||||
assert "makeSangeun(scope, key, picks)" in sk
|
||||
assert "picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) };" in sk # 계수 칸
|
||||
lost = _text(
|
||||
"M02_MasterTemplete_Hopyo_Lost.ts"
|
||||
) # 못옮김 잇기 = 캐시 모두 · 바탕 = SK 문서 바탕
|
||||
assert "const links = Object.values(loadLinks(k));" in lost and "잇기: links," in lost
|
||||
assert "로직: doc.바탕.로직, 입력값: doc.입력값 ?? {}" in lost
|
||||
assert "sangeunStep(scope, file.키, file.파일" in wizard and "/sangeun`" not in wizard
|
||||
sk = _text(
|
||||
"M02_MasterTemplete_Hopyo_WizardSk.ts"
|
||||
) # 778-5 D-5 — 줄 고름 · 확인한 것만(옛 기초 고름 걷음)
|
||||
assert "judgeSangeun(scope, key, body)" in sk
|
||||
lost = _text("M02_MasterTemplete_Hopyo_Lost.ts") # 옛 못옮김 창 = 보기만(옛 `바탕` 몸 걷음)
|
||||
assert "makeSangeun" not in lost and "loadLinks" not in lost
|
||||
assert "row.못옮김?.length && row.단산 !== undefined && row.SK" in _text(
|
||||
"M02_MasterTemplete_Hopyo_Left.ts"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""746-2 지킴 — 불러오기 고르기 창 후보 블록 줄 비교 · 요소 후보 화면(요구 585).
|
||||
|
||||
서버 없이 소스로 잡음: 고름 대기 칸이 줄 비교 표를 붙임 · 못옮김 창 · 안 이은 줄 창이 요소 후보 단추를 붙임 ·
|
||||
서버 없이 소스로 잡음: 안 이은 줄 창이 요소 후보 단추를 붙임(고름 대기 줄 비교 · 못옮김 잇기는 778-5 D-5 줄 고름으로 걷음) ·
|
||||
안 이은 줄 창의 기초 후보 칸에 요소 후보(`키`)가 섞여 들어가지 않음 · 서버 응답 칸 이름 · 부품 상자만 · 700 줄.
|
||||
"""
|
||||
|
||||
@@ -14,14 +14,7 @@ def _src(name: str) -> str:
|
||||
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_고름_대기_칸이_줄_비교_표를_붙임():
|
||||
sk = _src("WizardSk")
|
||||
assert "compareView(x.후보)" in sk and "cmp?.show(pick.select.value)" in sk
|
||||
assert "compareBrief(c.줄비교)" in sk # 선택 칸 글에 같음 · 품다름 수
|
||||
|
||||
|
||||
def test_못옮김_창_안_이은_줄_창이_요소_후보_단추를_붙임():
|
||||
assert "elemChoices(x.후보" in _src("Lost")
|
||||
def test_안_이은_줄_창이_요소_후보_단추를_붙임():
|
||||
left = _src("Left")
|
||||
assert "elemChoices(row.후보.filter(isElem)" in left
|
||||
assert (
|
||||
@@ -31,14 +24,8 @@ def test_못옮김_창_안_이은_줄_창이_요소_후보_단추를_붙임():
|
||||
|
||||
def test_서버_응답_칸_이름_그대로():
|
||||
api = _src("Api")
|
||||
for name in (
|
||||
"줄비교?: SangeunCompare | null",
|
||||
"엑셀: number | null",
|
||||
"기초: number | null",
|
||||
"못옮김: SangeunLost[]",
|
||||
):
|
||||
assert name in api
|
||||
assert 'CompareLook = "같음" | "품다름" | "엑셀만" | "기초만"' in api
|
||||
assert "못옮김: SangeunLost[]" in api and "export interface ElemCand" in api
|
||||
assert "SangeunCompare" not in api and "export function compareView" not in _src("Compare")
|
||||
|
||||
|
||||
def test_부품_상자만_날_요소_없음_700줄_안():
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""778-5 D-5 지킴 — 마법사 단가산출 줄 고름(제안 · 확인 뒤 옮김 · 요구 621 · 622 · 623 · 624).
|
||||
|
||||
서버 없이 잡음: 줄 고름 캐시가 서버 상태를 덮음(「내가 고름」) · [확인] = 고를 줄 0 일 때만 · 몸 = 이음 줄
|
||||
`{확인: [{번, 고름?: {차례: 고름}}]}`(확인한 블록만) · 자동 확인 없음(빈 캐시 = 빈 몸 · 「모두 확인」 단추 없음) ·
|
||||
화면 = 부품 상자만 · 700 줄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
|
||||
|
||||
|
||||
def _line(at, kind, state, **more):
|
||||
row = {"차례": at, "행": 100 + at, "묶음": kind, "이름": f"줄{at}", "규격": "", "매개값": {}}
|
||||
row |= {"엑셀금액": 1000.0, "상태": state, "방법": "", "단위": None, "후보": [], "꼬리표": []}
|
||||
return {**row, "글": "", "우리금액": None, "차": None, **more}
|
||||
|
||||
|
||||
def _block(no, lines, made=None):
|
||||
head = {"번": no, "이름": f"블록{no}", "단위": "㎥", "판정": "", "고를줄": 0, "우리금액": None}
|
||||
return head | {"엑셀금액": 5000.0, "근거": None, "만든": made, "줄": lines}
|
||||
|
||||
|
||||
BOSS = ["기본 — 건설업 작업반장"]
|
||||
BLOCKS = [
|
||||
_block(
|
||||
2,
|
||||
[
|
||||
_line(0, "L", "이어짐", 매개값={"품": 0.2}),
|
||||
_line(1, "L", "이어짐", 꼬리표=BOSS, 후보=[{"키": "LB000099"}]),
|
||||
_line(2, "M", "고를 것", 후보=[{"자재": "MT000123"}]),
|
||||
_line(3, "M", "못 이음"),
|
||||
_line(4, "R", "밖"),
|
||||
],
|
||||
),
|
||||
_block(5, [_line(0, "X", "이어짐"), _line(1, "R", "이어짐")]),
|
||||
_block(7, [_line(0, "S", "못 이음")], made=None),
|
||||
]
|
||||
|
||||
|
||||
def _run() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_778_5_sk_pick.cjs")],
|
||||
input=json.dumps({"blocks": BLOCKS}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_판정_확인_켜짐_조건():
|
||||
got = _run()
|
||||
assert got["start"] == [
|
||||
{"번": 2, "판정": "고를 줄 2", "확인켜짐": False},
|
||||
{"번": 5, "판정": "옮길 수 있음", "확인켜짐": True},
|
||||
{"번": 7, "판정": "고를 줄 1", "확인켜짐": False},
|
||||
]
|
||||
assert got["sureBlocked"] == [] # 고를 줄 남은 블록 = [확인] 안 켜짐
|
||||
assert got["half"] == {"판정": "고를 줄 2", "켜짐": False} # 이어진 줄 바꿈은 고를 줄 안 줄임
|
||||
assert got["full"] == {"판정": "옮길 수 있음", "켜짐": True}
|
||||
assert got["sure"] == [2, 5] # 하위 못 이음 블록 7 = 안 켜짐
|
||||
|
||||
|
||||
def test_줄_표_꼴_내가_고름이_덮음():
|
||||
view = _run()["view"]
|
||||
assert view[0] == {"상태": "이어짐", "꼬리표": [], "글": "", "mine": False}
|
||||
assert view[1] == {"상태": "이어짐", "꼬리표": ["내가 고름"], "글": "LB000099", "mine": True}
|
||||
assert view[2]["상태"] == "이어짐" and view[2]["글"] == "MT000123"
|
||||
assert view[3] == {
|
||||
"상태": "뺌",
|
||||
"꼬리표": ["내가 고름"],
|
||||
"글": "뺌 · 우리에 없음",
|
||||
"mine": True,
|
||||
}
|
||||
assert view[4]["상태"] == "밖"
|
||||
|
||||
|
||||
def test_몸_꼴_이음_줄_자동_확인_없음():
|
||||
got = _run()
|
||||
assert got["emptyBody"] == [] # 누른 것 없음 = 몸 비움
|
||||
picks = {"1": {"노임": "LB000099"}, "2": {"자재": "MT000123"}, "3": {"뺌": "우리에 없음"}}
|
||||
assert got["body"] == [{"번": 2, "고름": picks}, {"번": 5}]
|
||||
first, second = got["calls"]
|
||||
assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만
|
||||
assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun")
|
||||
assert second["body"] == {"확인": got["body"]}
|
||||
assert got["wait"] == {"블록": 3, "줄": 1} # 안 옮긴 블록 셋 · 블록 7 하위 줄
|
||||
assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐
|
||||
assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []}
|
||||
|
||||
|
||||
def test_후보_고름_꼴():
|
||||
cand = _run()["cand"]
|
||||
assert cand["L"] == {"노임": "LB000001"} and cand["X"] == {"기계": "EQ000019"}
|
||||
assert cand["M"] == {"자재": "MT000001"} and cand["R"] == {"기준": "노무비", "비목": "경비"}
|
||||
assert cand["Mbad"] is None and cand["text"] == "합판 12mm [장]"
|
||||
|
||||
|
||||
def _src(name: str) -> str:
|
||||
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_화면_자동_없음_부품_상자만():
|
||||
wiz = _src("WizardSk")
|
||||
assert "judgeSangeun(scope, key)" in wiz # 열 때 = 판정만
|
||||
assert "judgeSangeun(scope, key, body)" in wiz and "sureBody(load(), got.판정)" in wiz
|
||||
assert "모두" not in wiz.split("*/", 1)[1] and "setSure(" in wiz # 확인은 블록마다만
|
||||
assert 'kind: "chip"' in wiz and "disabled: !ok" in wiz # [확인] 칩 · 고를 줄 0 일 때만
|
||||
assert "단산행: 행" in wiz # 엑셀 보기 뛰기
|
||||
lines = _src("SkLines")
|
||||
assert "lineFind(" in lines and "unitTone(" in lines # 찾기 창 · 엑셀 칠과 한 규칙
|
||||
wizard = _src("Wizard")
|
||||
assert 'L("M02_SkLeft")' in wizard and "sk?.count()" in wizard # [끝내기] 막힘 글
|
||||
for name in ("WizardSk", "SkLines", "SkApi", "Lost", "Compare"):
|
||||
src = _src(name)
|
||||
assert len(src.splitlines()) <= 700, name
|
||||
assert not re.search(r'el\("(button|input|select)"|<button|<input', src), name
|
||||
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name
|
||||
logic = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SkPick.ts").read_text(encoding="utf-8")
|
||||
assert not re.search(r"sessionStorage\.|fetch\(|document\.", logic) # 순수
|
||||
@@ -361,7 +361,7 @@ def test_좌측_판_자리와_권한():
|
||||
assert "accessFor(layer, null, user).canWrite" in wizard # 결과 층 = 쓸 수 있는 층만
|
||||
assert 'let pick: ManageLayer = "personal";' in wizard # 기본 개인
|
||||
assert (
|
||||
"let wantSangeun = false;" in wizard and "sangeunStep(scope, file.키)" in wizard
|
||||
"let wantSangeun = false;" in wizard and "sangeunStep(scope, file.키, file.파일" in wizard
|
||||
) # 결과 단가산출 체크 → 단가산출 칸(729-3)
|
||||
|
||||
|
||||
|
||||
@@ -231,47 +231,60 @@ export const ui_locales_m2 = {
|
||||
M02_FindElementLater: ["일위대가 표 개편 뒤", "After the unit-cost table rework"],
|
||||
M02_FindOptions: ["옵션 {n}", "{n} options"],
|
||||
// 729-3 불러오기 마법사 — 단가산출 단계 · 안 이은 줄 · 일위대가표 띠
|
||||
M02_WizSkCheck: ["단가산출(산근 블록 → 기초 · SK)", "Unit-cost calcs (blocks → base · SK)"],
|
||||
M02_WizSkCheck: [
|
||||
"단가산출(블록 줄 → 확인한 것만 SK)",
|
||||
"Unit-cost calcs (lines → confirmed only)",
|
||||
],
|
||||
// 778-5 D-5 마법사 단가산출 줄 고름(제안 · 확인 뒤 옮김)
|
||||
M02_SkHint: [
|
||||
"블록마다 줄을 보고 고를 줄을 고른 뒤 [확인] → [확인한 것 옮기기] · 자동으로 옮기지 않음 · 금액 = M01 지금 값(엑셀 금액은 참고)",
|
||||
"Review each block, pick open lines, confirm, then move · nothing moves automatically · amounts = current M01 (Excel for reference)",
|
||||
],
|
||||
M02_SkSummary: [
|
||||
"블록 {b} · 옮길 수 있음 {ok} · 고를 줄 있음 {wait} · 옮김(SK) {made} · 확인 {sure}",
|
||||
"Blocks {b} · ready {ok} · open lines {wait} · moved (SK) {made} · confirmed {sure}",
|
||||
],
|
||||
M02_SkJudge: ["다시 판정", "Re-check"],
|
||||
M02_SkJudging: ["줄 판정 중…", "Checking lines…"],
|
||||
M02_SkMove: ["확인한 것 옮기기 ({n})", "Move confirmed ({n})"],
|
||||
M02_SkMoveTip: [
|
||||
"확인한 블록만 단위 SK 로 — 서버가 M01 지금 값으로 다시 셈",
|
||||
"Only confirmed blocks become SKs — the server recalculates with current M01",
|
||||
],
|
||||
M02_SkMoved: ["옮김 {made} · 못 옮김 {bad}", "Moved {made} · not moved {bad}"],
|
||||
M02_SkFailed: ["못 옮긴 블록 {n} — 까닭 펼쳐 봄", "{n} blocks not moved — see why"],
|
||||
M02_SkSureFirst: ["확인한 블록 없음 — 블록마다 [확인]", "No confirmed block yet"],
|
||||
M02_SkSure: ["확인", "Confirm"],
|
||||
M02_SkSureTip: ["이 블록을 옮길 목록에 넣음", "Add this block to the move list"],
|
||||
M02_SkSureOff: ["고를 줄이 남음 — 줄 표에서 먼저 고름", "Lines still open — pick them first"],
|
||||
M02_SkOpen: ["줄", "Lines"],
|
||||
M02_SkSheet: ["보기", "View"],
|
||||
M02_SkNone: ["단가산출 블록 없음", "No unit-cost blocks"],
|
||||
M02_SkNoLines: ["줄 없음", "No lines"],
|
||||
M02_SkBasisTag: ["근거(참고) —", "Basis (ref) —"],
|
||||
M02_SkLeft: [
|
||||
"단가산출 확인 안 한 블록 {n} · 고를 줄 {m}",
|
||||
"Unit-cost blocks not confirmed {n} · open lines {m}",
|
||||
],
|
||||
M02_SkColBlock: ["블록", "Block"],
|
||||
M02_SkColVerdict: ["판정", "Verdict"],
|
||||
M02_SkColOurs: ["우리 금액", "Our amount"],
|
||||
M02_SkColExcel: ["엑셀 금액(참고)", "Excel amount (ref)"],
|
||||
M02_SkColBasis: ["근거", "Basis"],
|
||||
M02_SkColMade: ["만든 SK", "SK made"],
|
||||
M02_SkColGroup: ["묶음", "Group"],
|
||||
M02_SkColParam: ["매개값", "Value"],
|
||||
M02_SkColState: ["상태", "State"],
|
||||
M02_SkColHow: ["방법", "Method"],
|
||||
M02_SkColDiff: ["차(참고)", "Diff (ref)"],
|
||||
M02_SkColPick: ["고름", "Pick"],
|
||||
M02_SkBasePick: ["기준 고름", "Pick base"],
|
||||
M02_SkDrop: ["뺌", "Drop"],
|
||||
M02_SkDropWhy: ["까닭", "Reason"],
|
||||
M02_SkDropDefault: ["우리에 없음", "Not in ours"],
|
||||
M02_SkClear: ["고름 지움", "Clear"],
|
||||
M02_WizSkHead: ["단가산출", "Unit-cost calcs"],
|
||||
M02_WizIlwiHead: ["일위대가", "Unit-price sheets"],
|
||||
M02_WizSkMake: ["단가산출 만들기", "Make unit-cost calcs"],
|
||||
M02_WizSkMakeTip: [
|
||||
"산근 블록마다 맞춤 → 같으면 기초 · 다르면 SK(기초 + 엑셀 차이) · 맞는 기초 없음 = 고름 대기 · 이미 만든 블록은 그대로",
|
||||
"Each block: same = base · different = SK (base + Excel diff) · no match = waits for a pick · made blocks kept",
|
||||
],
|
||||
M02_WizSkSummary: [
|
||||
"산근 블록 {b} · 기초 {base} · SK {sk} · 그대로 {keep} · 고름 대기 {wait}",
|
||||
"Blocks {b} · base {base} · SK {sk} · kept {keep} · waiting {wait}",
|
||||
],
|
||||
M02_WizSkBlock: ["블록", "Block"],
|
||||
M02_WizSkKey: ["키", "Key"],
|
||||
M02_WizSkKind: ["갈래", "Kind"],
|
||||
M02_WizSkBase: ["기초", "Base"],
|
||||
M02_WizSkDiff: ["SK · 차이 있음", "SK · differs"],
|
||||
M02_WizSkSame: ["같은 것 씀", "Reused"],
|
||||
M02_WizSkSameTip: [
|
||||
"이 층에 내용 같은 단가산출이 있어 새로 안 만들고 그것을 씀",
|
||||
"Same content already in this layer — reused, not created",
|
||||
],
|
||||
M02_WizSkAbove: ["위 층에 같은 것 — 연결 권함", "Same in upper layer — link suggested"],
|
||||
M02_WizSkSameSum: [
|
||||
" · 같은 것 씀 {same} · 위 층에 같은 것 {above}",
|
||||
" · reused {same} · same above {above}",
|
||||
],
|
||||
M02_WizSkNoneMade: ["새로 만든 단가산출 없음", "No new unit-cost calcs"],
|
||||
M02_WizSkNoWait: ["고름 대기 없음", "Nothing waiting"],
|
||||
M02_WizSkNoPick: ["고르지 않음", "Not picked"],
|
||||
M02_WizSkWaitHint: [
|
||||
"고름 대기 — 맞는 기초 없음 · 블록마다 기초를 골라 [고른 기초로 만들기] = 그 기초 + 엑셀 차이 SK",
|
||||
"Waiting — no matching base · pick a base per block, then make = that base + Excel diff",
|
||||
],
|
||||
M02_WizSkRemake: ["고른 기초로 만들기", "Make with picked bases"],
|
||||
M02_WizSkPickFirst: ["고른 기초 없음 — 블록마다 먼저 고름", "No base picked yet"],
|
||||
M02_WizSkAlias: [
|
||||
"별칭 제안 {n} 줄 — 이 층 별칭 장부에 남음 · 모두에게 쓰임 = 시스템 관리자 승인",
|
||||
"Alias proposals {n} — kept in this layer's ledger · system admin approves for everyone",
|
||||
],
|
||||
M02_WizSkLedger: ["M01 별칭 장부", "M01 alias ledger"],
|
||||
M02_LeftTitle: ["안 이은 줄", "Unlinked rows"],
|
||||
M02_LeftHint: [
|
||||
"만들면 M01 · 기초 · SK 에 안 이어질 엑셀 줄 — 줄마다 이을 것을 고름 · 고른 뒤 엑셀 보기 창 [저장] 또는 호표 [수정] 생성",
|
||||
@@ -315,37 +328,12 @@ export const ui_locales_m2 = {
|
||||
"Fixed rows link to labor, material or machine only",
|
||||
],
|
||||
M02_LibUnlinked: ["안 이은 줄", "Unlinked"],
|
||||
M02_WizSkCoef: ["환산 계수", "Coefficient"],
|
||||
M02_WizSkCoefNeed: [
|
||||
"단산{n} — 단위 성격 다름 · 환산 계수(0 보다 큼) 필요",
|
||||
"Block {n} — units differ · enter a coefficient",
|
||||
],
|
||||
M02_LostCol: ["못옮김", "Not moved"],
|
||||
M02_LostOpen: ["잇기", "Link"],
|
||||
M02_LostTitle: ["못옮김 잇기", "Link unmoved rows"],
|
||||
M02_LostHint: [
|
||||
"엑셀에만 있어 SK 로 못 옮긴 줄 — 줄마다 M01 노임 · 기계 · 자재로 이음(SK 더한줄 · 엑셀 수량) 또는 우리에 없음 · 고른 것 모두를 [잇기 만들기]로 보냄",
|
||||
"Excel-only rows not moved into the SK — link each to M01 labor, machine or material, then make",
|
||||
"옛 단가산출 SK 의 남은 못옮김(보기만) — 잇기 = 마법사 단가산출 칸에서 단산{n} 블록 줄 고름 → [확인] → [확인한 것 옮기기]",
|
||||
"Unmoved rows of an old SK (view only) — link in the wizard unit-cost step: pick lines of block {n}, confirm, then move",
|
||||
],
|
||||
M02_LostMake: ["잇기 만들기", "Make links"],
|
||||
M02_LostLeft: ["남은 못옮김 {n}", "{n} unmoved rows left"],
|
||||
M02_LostZero: ["못옮김 0", "Nothing unmoved"],
|
||||
M02_LostDrop: ["뺌", "Dropped"],
|
||||
M02_LostDropTip: [
|
||||
"우리 M01 에 없음 — SK 에 안 넣고 까닭만 남김(사용자가 고른 것)",
|
||||
"Not in M01 — kept out of the SK with a note",
|
||||
],
|
||||
M02_LostClear: ["고름 지움", "Clear"],
|
||||
M02_CmpTitle: ["줄 비교 — {name}", "Line compare — {name}"],
|
||||
M02_CmpNone: ["견줄 수 없음(단위 성격 다름)", "Cannot compare (units differ)"],
|
||||
M02_CmpSame: ["같음", "Same"],
|
||||
M02_CmpDiff: ["품 다름", "Qty differs"],
|
||||
M02_CmpExcel: ["엑셀만", "Excel only"],
|
||||
M02_CmpBase: ["기초만", "Base only"],
|
||||
M02_CmpColExcel: ["엑셀 품", "Excel qty"],
|
||||
M02_CmpColBase: ["기초 품", "Base qty"],
|
||||
M02_CmpColLook: ["꼴", "Result"],
|
||||
M02_CmpLost: ["못옮김 {n}", "{n} unmoved"],
|
||||
M02_ElemTitle: ["M01 요소 후보", "M01 element candidates"],
|
||||
M02_SheetIlwi: ["일위대가표", "Unit-price sheet"],
|
||||
M02_SheetList: ["단가산출표", "Price basis list"],
|
||||
|
||||
Reference in New Issue
Block a user