feat(M02): 359-3 실무자료 확인 창 표 = 일위대가 상세 꼴 · 금액 미리 셈 · 드롭다운 추가 · 로직없음 · 사용안함
- 줄 표 = 로직 표(차례 · 구분 · 이름 · 별칭 · 수량산출 · 결과 단위 · 노무비 · 재료비 · 경비 · 합계) + 자재 표(세 칸 · 가격 다섯) + 품 비교 · 단추 - 금액 칸 = 위 엑셀 · 아래 우리(줄마다 서버 calc 미리 셈 · 저장 안 함 · 최신 응답만) - 미리 셈 문서 꼴 = logic/M02_MasterTemplete_Hopyo_Draft.ts(정본 = M02_Hopyo_Build · 수량 = 엑셀 × 배 · 별도계상) - 드롭다운 글 추가 · 로직없음 · 사용안함(저장 값 그대로) · [엑셀에서 보기] → [엑셀 보기] · [사전조합 생성] - Check.ts 쪼갬(표 그리기 = Hopyo_CheckRows.ts) · 쓰지 않는 [← 호표 목록] 지움 - 시험 test_m02_hopyo_draft_calc(북부청 실물 줄 → calc 200 · 반영합 · 저장 안 함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -55,34 +55,49 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 확인 창 줄 표(338 · 339) — 체크 · 갈래 · 수량 · 단위 · 상태 = 줄바꿈 없음(폭 = 글 폭) · 단추 = 세로로 쌓아 좁게 ·
|
||||
품명 · 규격 · 우리 로직 · 옵션 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */
|
||||
/* 확인 창 줄 표(338 · 339 · 359-3) — 일위대가 상세 꼴(m02-labrow) · 체크 칸 · 수량 · 단위 = 줄바꿈 없음 ·
|
||||
단추 = 세로로 쌓아 좁게 · 이름 · 별칭 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */
|
||||
.m02-hopyo__table.m02-hopyo__lines :is(th, td) {
|
||||
padding: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-hopyo__table.m02-hopyo__lines
|
||||
:is(th, td):is(:nth-child(1), :nth-child(2), :nth-child(5), :nth-child(6), :nth-child(7)) {
|
||||
.m02-hopyo__table.m02-hopyo__lines td:first-child,
|
||||
.m02-hopyo__lines :is(.m02-labrow__qty, .m02-labrow__unit) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 품명 · 규격 — 낱말 두셋은 한 줄에 */
|
||||
.m02-hopyo__table.m02-hopyo__lines td:nth-child(3),
|
||||
/* 이름 칸 대상 줄 — 키 · 이름 · 배 칸 · 몫 한 줄(상세 이름 칸의 한 줄 말줄임을 풂) */
|
||||
.m02-hopyo__lines .m02-labrow__name > .m02-hopyo__part {
|
||||
display: flex;
|
||||
max-width: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.m02-hopyo__lines .m02-labrow__name > .m02-hopyo__state,
|
||||
.m02-hopyo__lines .m02-labrow__kind > .m02-hopyo__state {
|
||||
display: inline-block;
|
||||
margin-left: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 품 비교 — 항목마다 한 줄(글의 줄바꿈 그대로) */
|
||||
.m02-hopyo__table.m02-hopyo__lines td.m02-hopyo__compare {
|
||||
min-width: 9em;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.m02-hopyo__subhead {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__table td:nth-child(1) {
|
||||
min-width: 7em;
|
||||
}
|
||||
|
||||
.m02-hopyo__table.m02-hopyo__lines td:nth-child(4),
|
||||
.m02-hopyo-ledger__table td:nth-child(2) {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
/* 옵션 · 품 비교 — 항목마다 한 줄(글의 줄바꿈 그대로) */
|
||||
.m02-hopyo__table.m02-hopyo__lines td:is(:nth-child(9), :nth-child(10)) {
|
||||
min-width: 9em;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* 단추 칸 — 단추 · 넣음/뺌을 세로로 쌓아 칸 폭 = 가장 긴 단추 */
|
||||
.m02-hopyo__table.m02-hopyo__lines td.m02-hopyo__acts {
|
||||
width: 1%;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Check.ts
|
||||
* 일위대가 실무자료 확인 창(PLAN 338-3 · 설계 4-1) — 호표 하나의 줄마다 서버 맞춤(상태 · 체크 제안 ·
|
||||
* 우리 로직 · 옵션 · 품 비교)을 보이고, 사용자가 체크한 줄만 [확인하고 사전조합 만들기]로 서버에 보냄.
|
||||
* · 자리 = 실무자료 본 칸(호표 누름) · [로직 추가] 「실무 호표」 모달 · 엑셀 보기 창 펼침 [확인 창 열기]
|
||||
* 일위대가 실무자료 확인 창(PLAN 338-3 · 설계 4-1 · 359-3) — 호표 하나의 줄마다 서버 맞춤(상태 · 체크 제안 ·
|
||||
* 우리 로직 · 옵션 · 품 비교)을 보이고, 사용자가 체크한 줄만 [사전조합 생성]으로 서버에 보냄.
|
||||
* · 자리 = 엑셀 보기 창 호표 [수정] · [로직 추가] 「실무 호표」 모달
|
||||
* · 머리 = 호표 · 「제안 n / 줄 m」 · 숫자 판정 셈 중이면 3 초마다 다시 읽음(끝나면 멈춤 · 만진 줄은 그대로)
|
||||
* · 줄 = [✓] · 갈래 · 품명 · 규격 · 수량 · 단위 · 상태(맞음 · 별칭 · 다른 점 · 후보) · 우리 로직(여럿 · 몫 · 배) ·
|
||||
* 옵션(칸=값 · 출처 · 못 가름) · 품 비교(엑셀 → 우리 ✓ / ≠) · [고르기] [원문 보기] [엑셀에서 보기] · 뺌 까닭
|
||||
* · 줄 표 = 구조물 조합 일위대가 상세 꼴(로직 표 · 자재 표 · 품 비교 · 단추 — `M02_MasterTemplete_Hopyo_CheckRows`)
|
||||
* · 금액 = 줄마다 서버 calc 미리 셈(`logic/M02_MasterTemplete_Hopyo_Draft` 문서 · 저장 안 함 · 줄마다 최신 응답만)
|
||||
* · 대상 · 옵션을 바꾸면 서버 `guess`(저장 안 함 · 줄마다 최신 응답만)가 우리 품을 다시 셈
|
||||
* · 캐시 = sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}`(조작 즉시 · 만들기 성공하면 지움) · 자동 저장 없음
|
||||
* · 화면 잠금은 [확인하고 사전조합 만들기] 때만 · 쓰기 단추 = 만들 층 쓰기 권한만.
|
||||
* 계산은 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) — 화면은 보이기 · 체크 · 고름만.
|
||||
* · 화면 잠금은 [사전조합 생성] 때만 · 쓰기 단추 = 만들 층 쓰기 권한만.
|
||||
* 계산은 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build` · `/labor/{열}/calc`) — 화면은 보이기 · 체크 · 고름만.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -36,22 +36,23 @@ import {
|
||||
type HopyoPart,
|
||||
type HopyoScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { builtWord, openHopyoChoose, targetText } from "./M02_MasterTemplete_Hopyo_Choose";
|
||||
import {
|
||||
builtWord,
|
||||
inputsText,
|
||||
openHopyoChoose,
|
||||
sourceText,
|
||||
targetText,
|
||||
} from "./M02_MasterTemplete_Hopyo_Choose";
|
||||
checkTables,
|
||||
type LinePrice,
|
||||
type RowsContext,
|
||||
} from "./M02_MasterTemplete_Hopyo_CheckRows";
|
||||
import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet";
|
||||
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { calcLabor, fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
|
||||
import { draftDoc } from "./logic/M02_MasterTemplete_Hopyo_Draft";
|
||||
import "./M02_MasterTemplete_Hopyo.css";
|
||||
|
||||
export const POLL_MS = 3000;
|
||||
/** 사전조합을 만들었음(document 알림) — 좌측 「일위대가 사전조합」 목록 다시 */
|
||||
export const HOPYO_BUILT = "m02-hopyo-built";
|
||||
const OUT = ["우리에 없음", "안 씀"] as const;
|
||||
/** 미리 셈 문서 열 이름(서버 calc 는 보낸 문서로 셈 · 저장 안 함) */
|
||||
const DRAFT_COLUMN = "hopyo-draft";
|
||||
|
||||
/** 확인 창 캐시 — 사용자가 만진 것만(서버 제안 위에 덮어 보임) */
|
||||
export interface CheckDraft {
|
||||
@@ -204,13 +205,11 @@ export interface CheckOptions {
|
||||
target?: HopyoScope;
|
||||
/** 만들 층 쓰기 권한 — 없으면 체크 · 만들기 없음(보기만) */
|
||||
canWrite: boolean;
|
||||
/** [← 호표 목록](본 칸) */
|
||||
onBack?: () => void;
|
||||
/** 만들었음 — [로직 추가] 참조 줄 · 모달 닫기 */
|
||||
onBuilt?: (got: HopyoBuilt, hopyo: HopyoOne["호표"]) => void;
|
||||
}
|
||||
|
||||
/** 확인 창 몸 — 본 칸 · 모달이 같이 씀 */
|
||||
/** 확인 창 몸 — 모달(`openHopyoCheck`)에 담김 */
|
||||
export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo m02-hopyo-check",
|
||||
@@ -224,6 +223,9 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
let seq = 0;
|
||||
let timer = 0;
|
||||
const ask = newest(); // 줄마다 최신 미리 셈만
|
||||
const askPrice = newest(); // 줄마다 최신 금액 미리 셈만
|
||||
const priced = new Map<number, { sig: string; got: LinePrice }>();
|
||||
let pending = false;
|
||||
const keep = (): void => saveDraft(k, d);
|
||||
|
||||
async function load(quiet = false): Promise<void> {
|
||||
@@ -313,161 +315,76 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
function partCell(line: HopyoLine): HTMLElement {
|
||||
const parts = partsOf(line, d);
|
||||
return el("td", {
|
||||
className: "m02-hopyo__parts",
|
||||
children: parts.map((p, at) => {
|
||||
const times = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__times",
|
||||
attrs: { type: "number", step: "any", min: "0", value: p.배 == null ? "" : String(p.배) },
|
||||
}) as HTMLInputElement;
|
||||
times.title = "배 — 우리 수량 = 엑셀 수량 × 배";
|
||||
times.disabled = !opt.canWrite;
|
||||
times.addEventListener("change", () => {
|
||||
const v = times.value === "" ? null : Number(times.value);
|
||||
d.대상[line.차례] = parts.map((x, i) => (i === at ? { ...x, 배: v } : x));
|
||||
keep();
|
||||
draw();
|
||||
});
|
||||
const share = p.몫 != null ? ` · 몫 ${p.몫}` : "";
|
||||
return el("div", {
|
||||
className: "m02-hopyo__part",
|
||||
children: [
|
||||
el("span", {
|
||||
text: `${targetText(p)} ${p.로직 ?? p.자재 ?? ""}${share}`.trim(),
|
||||
attrs: { title: p.어디 ?? "" },
|
||||
}),
|
||||
times,
|
||||
...(p.로직
|
||||
? [
|
||||
createButton({
|
||||
label: "옵션",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => editOptions(line, at),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
}),
|
||||
/** 줄마다 금액 미리 셈(359-3) — 서버 calc(저장 안 함 · 줄마다 최신 응답만 · 문서가 같으면 다시 안 셈) */
|
||||
function price(line: HopyoLine): LinePrice | null {
|
||||
const doc = draftDoc(line, partsOf(line, d));
|
||||
if (!doc) return null;
|
||||
const sig = JSON.stringify(doc);
|
||||
const have = priced.get(line.차례);
|
||||
if (have?.sig === sig) return have.got;
|
||||
const slot = { sig, got: {} as LinePrice };
|
||||
priced.set(line.차례, slot);
|
||||
const mine = askPrice(line.차례);
|
||||
calcLabor(target.layer, DRAFT_COLUMN, doc, {}, target.owner).then(
|
||||
(calc) => mine() && ((slot.got = { calc }), soon()),
|
||||
(error: unknown) => mine() && ((slot.got = { fail: why(error) }), soon()),
|
||||
);
|
||||
return slot.got;
|
||||
}
|
||||
/** 응답 여럿 = 한 번 그림 */
|
||||
function soon(): void {
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
requestAnimationFrame(() => {
|
||||
pending = false;
|
||||
if (root.isConnected) draw();
|
||||
});
|
||||
}
|
||||
|
||||
function lineRow(line: HopyoLine): HTMLElement {
|
||||
const m = line.맞춤;
|
||||
const parts = partsOf(line, d);
|
||||
const box = el("input", { attrs: { type: "checkbox", "data-check": String(line.차례) } });
|
||||
const input = box as HTMLInputElement;
|
||||
input.checked = isChecked(line, d) && checkable(line, d);
|
||||
input.disabled = !opt.canWrite || !checkable(line, d);
|
||||
input.addEventListener("change", () => {
|
||||
d.체크[line.차례] = input.checked;
|
||||
const ctx = (): RowsContext => ({
|
||||
canWrite: opt.canWrite,
|
||||
d,
|
||||
price,
|
||||
check: (line, on) => {
|
||||
d.체크[line.차례] = on;
|
||||
keep();
|
||||
draw();
|
||||
});
|
||||
const out = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { title: "뺌 — 사전조합에 안 넣고 장부에 남김" },
|
||||
children: [
|
||||
el("option", { text: "넣음", attrs: { value: "" } }),
|
||||
...OUT.map((x) => el("option", { text: `뺌 — ${x}`, attrs: { value: x } })),
|
||||
],
|
||||
}) as HTMLSelectElement;
|
||||
out.value = d.뺌[line.차례] ?? "";
|
||||
out.disabled = !opt.canWrite || m.상태 === "%";
|
||||
out.addEventListener("change", () => {
|
||||
if (out.value) d.뺌[line.차례] = out.value;
|
||||
},
|
||||
out: (line, value) => {
|
||||
if (value) d.뺌[line.차례] = value;
|
||||
else delete d.뺌[line.차례];
|
||||
d.체크[line.차례] = !!out.value || d.체크[line.차례];
|
||||
d.체크[line.차례] = !!value || d.체크[line.차례];
|
||||
keep();
|
||||
draw();
|
||||
});
|
||||
const sheet = (행: [number, number]): void =>
|
||||
openSheet({
|
||||
scope: opt.scope,
|
||||
key: opt.key,
|
||||
파일: got?.파일 ?? "",
|
||||
canWrite: opt.canWrite,
|
||||
target,
|
||||
단산행: 행,
|
||||
});
|
||||
const acts = [
|
||||
...(opt.canWrite && m.상태 !== "%"
|
||||
? [
|
||||
createButton({
|
||||
label: "고르기",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
openHopyoChoose({
|
||||
line,
|
||||
openSheet: sheet,
|
||||
done: (ts) =>
|
||||
setParts(
|
||||
line,
|
||||
ts.map((t) => ({ ...t, 입력값: t.입력값 ?? {}, 몫: t.몫 ?? null })),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
createButton({
|
||||
label: "원문 보기",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => void showSource(line),
|
||||
},
|
||||
times: (line, at, value) => {
|
||||
d.대상[line.차례] = partsOf(line, d).map((x, i) => (i === at ? { ...x, 배: value } : x));
|
||||
keep();
|
||||
draw();
|
||||
},
|
||||
choose: (line) =>
|
||||
openHopyoChoose({
|
||||
line,
|
||||
openSheet: sheetAt,
|
||||
done: (ts) =>
|
||||
setParts(
|
||||
line,
|
||||
ts.map((t) => ({ ...t, 입력값: t.입력값 ?? {}, 몫: t.몫 ?? null })),
|
||||
),
|
||||
}),
|
||||
...(line.근거
|
||||
? [
|
||||
createButton({
|
||||
label: "엑셀에서 보기",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => sheet(line.근거!.행),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
out,
|
||||
];
|
||||
const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n");
|
||||
const opts = parts
|
||||
.filter((p) => p.로직)
|
||||
.flatMap((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처).split(/, (?=[^,=]+=)| · /))
|
||||
.filter(Boolean);
|
||||
if (m.갈리는칸?.length) opts.push(`⚠ 못 가름: ${m.갈리는칸.join(", ")}`);
|
||||
const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : "";
|
||||
return el("tr", {
|
||||
className: `m02-hopyo__line is-${input.checked ? "in" : "out"}`,
|
||||
attrs: { "data-line": String(line.차례), "data-state": m.상태 },
|
||||
children: [
|
||||
el("td", { children: [box] }),
|
||||
td(line.갈래),
|
||||
td(line.이름),
|
||||
td(line.규격),
|
||||
td(line.수량 === null ? "" : String(line.수량), "m02-hopyo__num"),
|
||||
td(line.단위),
|
||||
el("td", {
|
||||
attrs: { title: tags },
|
||||
children: [
|
||||
el("span", {
|
||||
className: `m02-hopyo__state is-${m.상태.replace(" ", "")}`,
|
||||
text: `${m.상태}${alias}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
partCell(line),
|
||||
td(opts.join("\n")), // 옵션 · 품 비교 = 항목마다 한 줄
|
||||
td(
|
||||
compareText(d.품[line.차례] ?? m.품)
|
||||
.split(" · ")
|
||||
.join("\n"),
|
||||
),
|
||||
el("td", { className: "m02-hopyo__acts", children: acts }),
|
||||
],
|
||||
source: (line) => void showSource(line),
|
||||
sheet: sheetAt,
|
||||
options: editOptions,
|
||||
});
|
||||
const sheetAt = (행: [number, number]): void =>
|
||||
openSheet({
|
||||
scope: opt.scope,
|
||||
key: opt.key,
|
||||
파일: got?.파일 ?? "",
|
||||
canWrite: opt.canWrite,
|
||||
target,
|
||||
단산행: 행,
|
||||
});
|
||||
}
|
||||
|
||||
async function make(h: HopyoOne["호표"]): Promise<void> {
|
||||
const whole = !!h.통째 && (d.통째 ?? h.통째.체크);
|
||||
@@ -483,8 +400,8 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
.map(([a, b]) => `${a} ${b}`)
|
||||
.join(" · ")})`
|
||||
: "") +
|
||||
" — 확인하고 사전조합을 만듦";
|
||||
if (!(await showConfirmDialog(ask, "확인하고 만들기"))) return;
|
||||
" — 사전조합 생성";
|
||||
if (!(await showConfirmDialog(ask, "생성"))) return;
|
||||
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: opt.no };
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
@@ -575,19 +492,15 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
draw();
|
||||
});
|
||||
const whole = !!h.통째 && (d.통째 ?? h.통째.체크);
|
||||
const go = createButton({ label: "확인하고 사전조합 만들기", variant: "filled", size: "sm" });
|
||||
const go = createButton({ label: "사전조합 생성", variant: "filled", size: "sm" });
|
||||
go.disabled = !whole && checked < 1;
|
||||
go.setAttribute("data-hopyo-make", "");
|
||||
go.addEventListener("click", () => void make(h));
|
||||
tools.push(all, go);
|
||||
}
|
||||
const back = opt.onBack
|
||||
? [createButton({ label: "← 호표 목록", variant: "ghost", size: "sm", onClick: opt.onBack })]
|
||||
: [];
|
||||
const head = el("div", {
|
||||
className: "m02-hopyo__bar",
|
||||
children: [
|
||||
...back,
|
||||
el("strong", { text: `제${h.번}호표 ${h.이름}` }),
|
||||
note(h.규격),
|
||||
note(h.단위),
|
||||
@@ -598,7 +511,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
: []),
|
||||
...(h.만든 ? [note(`만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}`)] : []),
|
||||
createButton({
|
||||
label: "엑셀에서 보기",
|
||||
label: "엑셀 보기",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
openSheet({
|
||||
@@ -638,38 +551,10 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
];
|
||||
})()
|
||||
: [];
|
||||
const heads = [
|
||||
"",
|
||||
"갈래",
|
||||
"품명",
|
||||
"규격",
|
||||
"수량",
|
||||
"단위",
|
||||
"상태",
|
||||
"우리 로직 · 배",
|
||||
"옵션",
|
||||
"품 비교",
|
||||
"",
|
||||
];
|
||||
root.replaceChildren(
|
||||
head,
|
||||
...whole,
|
||||
h.줄.length
|
||||
? el("div", {
|
||||
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-hopyo__table m02-hopyo__lines",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
||||
}),
|
||||
el("tbody", { children: h.줄.map(lineRow) }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
: note("구성 줄 없음"),
|
||||
...(h.줄.length ? checkTables(h.줄, ctx()) : [note("구성 줄 없음")]),
|
||||
...(built ? [builtBlock(built)] : []),
|
||||
);
|
||||
}
|
||||
@@ -678,7 +563,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
return root;
|
||||
}
|
||||
|
||||
/** 확인 창 모달 — [로직 추가] 「실무 호표」 · 엑셀 보기 창 [확인 창 열기] · 만들면 닫음 */
|
||||
/** 확인 창 모달 — [로직 추가] 「실무 호표」 · 엑셀 보기 창 호표 [수정] · 만들면 닫음 */
|
||||
export function openHopyoCheck(opt: CheckOptions & { title?: string }): void {
|
||||
openModal({
|
||||
title: opt.title ?? `확인 — 제${opt.no}호표`,
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_CheckRows.ts
|
||||
* 확인 창 줄 표(PLAN 359-3) — 구조물 조합 일위대가 상세 꼴(`Labor_Rows` logicTable · matTable)에 품 비교 · 단추만 더함.
|
||||
* · 로직 표 = [✓] · 차례 · 구분(엑셀 갈래 + 상태) · 이름(우리 로직 · 배 · 옵션 글) · 별칭(엑셀 품명 = 만들 때 별칭) ·
|
||||
* 수량산출(위 엑셀 / 아래 우리 = 엑셀 × 배) · 결과 단위 · 노무비 · 재료비 · 경비 · 합계 · 품 비교 · 단추
|
||||
* · 자재 표 = [✓] · 차례 · 코드 · 이름 · 규격 · 수량산출 · 단위 · 세 칸(자재구분 · 별도계상 · 운반비 · 글만) ·
|
||||
* 가격 다섯 · 엑셀 / 우리 금액 · 단추
|
||||
* · 금액 칸 = 위 작게 엑셀 · 아래 우리(서버 calc 미리 셈 · 저장 안 함) · 셈 중 「…」
|
||||
* · 단추 = [고르기] · [원문 보기] · [엑셀 보기] · [옵션](로직 대상마다) · 드롭다운 추가 · 로직없음 · 사용안함
|
||||
* (화면 글만 · 저장 값 = 빈 값 · 「우리에 없음」 · 「안 씀」 그대로)
|
||||
* 줄 표 = `tableOf`(대상이 모두 자재 · 대상 없는 자재 · 별도계상 줄 = 자재 표).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { HopyoLine, HopyoPart } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import {
|
||||
checkable,
|
||||
compareText,
|
||||
isChecked,
|
||||
partsOf,
|
||||
type CheckDraft,
|
||||
} from "./M02_MasterTemplete_Hopyo_Check";
|
||||
import { inputsText, sourceText } from "./M02_MasterTemplete_Hopyo_Choose";
|
||||
import type { LaborCalc } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks";
|
||||
import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { tableOf } from "./logic/M02_MasterTemplete_Hopyo_Draft";
|
||||
|
||||
/** 드롭다운 — 저장 값(서버 뺌 까닭) → 화면 글 */
|
||||
export const OUT_TEXT: [string, string][] = [
|
||||
["", "추가"],
|
||||
["우리에 없음", "로직없음"],
|
||||
["안 씀", "사용안함"],
|
||||
];
|
||||
const COSTS = ["노무비", "재료비", "경비", "계"] as const;
|
||||
const COST_HEAD = {
|
||||
노무비: "Prev_Labor",
|
||||
재료비: "Prev_Material",
|
||||
경비: "Prev_Expense",
|
||||
계: "Prev_Sum",
|
||||
} as const;
|
||||
|
||||
/** 줄 하나 미리 셈 — 셈 중 = 둘 다 없음 */
|
||||
export interface LinePrice {
|
||||
calc?: LaborCalc;
|
||||
fail?: string;
|
||||
}
|
||||
|
||||
export interface RowsContext {
|
||||
canWrite: boolean;
|
||||
d: CheckDraft;
|
||||
price: (line: HopyoLine) => LinePrice | null;
|
||||
check: (line: HopyoLine, on: boolean) => void;
|
||||
out: (line: HopyoLine, value: string) => void;
|
||||
times: (line: HopyoLine, at: number, value: number | null) => void;
|
||||
choose: (line: HopyoLine) => void;
|
||||
source: (line: HopyoLine) => void;
|
||||
sheet: (행: [number, number]) => void;
|
||||
options: (line: HopyoLine, at: number) => void;
|
||||
}
|
||||
|
||||
const num = (v: number): string =>
|
||||
Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 });
|
||||
const small = (text: string, cls = "m02-labrow__note"): HTMLElement =>
|
||||
el("small", { className: cls, text, attrs: { title: text } });
|
||||
|
||||
/** 금액 칸 — 위 엑셀(작게) · 아래 우리 */
|
||||
function pairCell(
|
||||
excel: number | null | undefined,
|
||||
ours: number | null | undefined,
|
||||
wait: boolean,
|
||||
label: string,
|
||||
): HTMLElement {
|
||||
const td = moneyCell(excel, ours, wait, label);
|
||||
td.querySelector(".m02-labrow__base")?.setAttribute("title", "엑셀");
|
||||
td.querySelector(".m02-labrow__applied")?.setAttribute("title", "우리(미리 셈)");
|
||||
return td;
|
||||
}
|
||||
|
||||
/** 수량산출 — 위 엑셀 수량 · 아래 우리(엑셀 × 배 · 대상마다) */
|
||||
function qtyCell(line: HopyoLine, parts: HopyoPart[]): HTMLElement {
|
||||
const ours = parts
|
||||
.map((p) => (p.배 === null || line.수량 === null ? "—" : num(line.수량 * p.배)))
|
||||
.join(" · ");
|
||||
return el("td", {
|
||||
className: "m02-labrow__qty",
|
||||
attrs: { "data-label": tc("Labor_QtyCol") },
|
||||
children: [
|
||||
el("small", {
|
||||
className: "m02-labrow__base",
|
||||
text: line.수량 === null ? "—" : `${num(line.수량)} ${line.단위}`.trim(),
|
||||
attrs: { title: "엑셀" },
|
||||
}),
|
||||
el("span", { className: "m02-labrow__applied", text: ours || "—", attrs: { title: "우리" } }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const stateChip = (line: HopyoLine): HTMLElement => {
|
||||
const m = line.맞춤;
|
||||
const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : "";
|
||||
const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n");
|
||||
return el("span", {
|
||||
className: `m02-hopyo__state is-${m.상태.replace(" ", "")}`,
|
||||
text: `${m.상태}${alias}`,
|
||||
attrs: { title: tags },
|
||||
});
|
||||
};
|
||||
|
||||
/** 대상마다 한 줄 — 키 · 이름 · × 배 칸 · 몫 */
|
||||
function partLines(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement[] {
|
||||
return parts.map((p, at) => {
|
||||
const times = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__times",
|
||||
attrs: { type: "number", step: "any", min: "0", value: p.배 == null ? "" : String(p.배) },
|
||||
}) as HTMLInputElement;
|
||||
times.title = "배 — 우리 수량 = 엑셀 수량 × 배";
|
||||
times.disabled = !ctx.canWrite;
|
||||
times.addEventListener("change", () =>
|
||||
ctx.times(line, at, times.value === "" ? null : Number(times.value)),
|
||||
);
|
||||
const key = p.로직 ?? p.자재 ?? (p.호표 !== undefined ? `제${p.호표}호표` : "");
|
||||
return el("div", {
|
||||
className: "m02-hopyo__part",
|
||||
attrs: { title: p.어디 ?? "" },
|
||||
children: [
|
||||
el("span", { className: "m02-labrow__key", text: key }),
|
||||
el("strong", { text: p.이름 ?? "" }),
|
||||
times,
|
||||
...(p.몫 != null ? [small(`몫 ${p.몫}`)] : []),
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 이름 칸 덧글 — 옵션(칸=값 · 출처) · 못 가름 · 셈 못 함 까닭 */
|
||||
function nameNotes(line: HopyoLine, parts: HopyoPart[], price: LinePrice | null): HTMLElement[] {
|
||||
const opts = parts
|
||||
.filter((p) => p.로직)
|
||||
.map((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처))
|
||||
.filter(Boolean);
|
||||
if (line.맞춤.갈리는칸?.length) opts.push(`⚠ 못 가름: ${line.맞춤.갈리는칸.join(", ")}`);
|
||||
const why = [
|
||||
price?.fail,
|
||||
...(price?.calc?.줄 ?? []).map((l) => l.까닭),
|
||||
...(price?.calc?.자재 ?? []).map((m) => m.까닭),
|
||||
].filter(Boolean) as string[];
|
||||
return [
|
||||
...opts.map((t) => small(t, "m02-labrow__note m02-hopyo__opts")),
|
||||
...why.map((t) => small(`셈 못 함: ${t}`, "m02-labrow__empty")),
|
||||
...(parts.some((p) => p.호표 !== undefined) ? [small("하위 호표 = 만든 뒤 셈")] : []),
|
||||
];
|
||||
}
|
||||
|
||||
function checkCell(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
||||
const box = el("input", {
|
||||
attrs: { type: "checkbox", "data-check": String(line.차례) },
|
||||
}) as HTMLInputElement;
|
||||
box.checked = isChecked(line, ctx.d) && checkable(line, ctx.d);
|
||||
box.disabled = !ctx.canWrite || !checkable(line, ctx.d);
|
||||
box.addEventListener("change", () => ctx.check(line, box.checked));
|
||||
return el("td", { children: [box] });
|
||||
}
|
||||
|
||||
function actsCell(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement {
|
||||
const m = line.맞춤;
|
||||
const ghost = (label: string, run: () => void): HTMLElement =>
|
||||
createButton({ label, variant: "ghost", size: "sm", onClick: run });
|
||||
const out = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { title: "로직없음 · 사용안함 — 사전조합에 안 넣고 장부에 남김", "data-out": "" },
|
||||
children: OUT_TEXT.map(([value, text]) => el("option", { text, attrs: { value } })),
|
||||
}) as HTMLSelectElement;
|
||||
out.value = ctx.d.뺌[line.차례] ?? "";
|
||||
out.disabled = !ctx.canWrite || m.상태 === "%";
|
||||
out.addEventListener("change", () => ctx.out(line, out.value));
|
||||
const options = parts.flatMap((p, at) => {
|
||||
if (!p.로직) return [];
|
||||
const b = ghost("옵션", () => ctx.options(line, at));
|
||||
b.title = `${p.로직} 옵션`;
|
||||
return [b];
|
||||
});
|
||||
return el("td", {
|
||||
className: "m02-hopyo__acts",
|
||||
children: [
|
||||
...(ctx.canWrite && m.상태 !== "%" ? [ghost("고르기", () => ctx.choose(line))] : []),
|
||||
ghost("원문 보기", () => ctx.source(line)),
|
||||
...(line.근거 ? [ghost("엑셀 보기", () => ctx.sheet(line.근거!.행))] : []),
|
||||
...options,
|
||||
out,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const rowOf = (line: HopyoLine, ctx: RowsContext, cells: HTMLElement[]): HTMLElement => {
|
||||
const on = isChecked(line, ctx.d) && checkable(line, ctx.d);
|
||||
return el("tr", {
|
||||
className: `m02-labrow__row m02-hopyo__line is-${on ? "in" : "out"}`,
|
||||
attrs: { "data-line": String(line.차례), "data-state": line.맞춤.상태 },
|
||||
children: [checkCell(line, ctx), ...cells, actsCell(line, partsOf(line, ctx.d), ctx)],
|
||||
});
|
||||
};
|
||||
|
||||
function logicRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
||||
const parts = partsOf(line, ctx.d);
|
||||
const price = ctx.price(line);
|
||||
const wait = !!price && !price.calc && !price.fail;
|
||||
const ours = price?.calc?.반영합;
|
||||
const unit = price?.calc?.줄[0]?.분모 ?? parts[0]?.단위 ?? line.단위;
|
||||
const compare = compareText(ctx.d.품[line.차례] ?? line.맞춤.품);
|
||||
return rowOf(line, ctx, [
|
||||
el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }),
|
||||
el("td", { className: "m02-labrow__kind", children: [line.갈래, stateChip(line)] }),
|
||||
el("td", {
|
||||
className: "m02-labrow__name m02-hopyo__parts",
|
||||
children: [
|
||||
...(parts.length ? partLines(line, parts, ctx) : [small("—", "m02-labrow__muted")]),
|
||||
...nameNotes(line, parts, price),
|
||||
],
|
||||
}),
|
||||
el("td", {
|
||||
className: "m02-labrow__alias",
|
||||
attrs: { title: `${line.이름} ${line.규격}`.trim() },
|
||||
children: [line.이름, ...(line.규격 ? [small(line.규격)] : [])],
|
||||
}),
|
||||
qtyCell(line, parts),
|
||||
el("td", { className: "m02-labrow__unit", text: unit ?? "" }),
|
||||
...COSTS.map((c) =>
|
||||
pairCell(line.금액[c], price?.calc ? (ours?.[c] ?? null) : null, wait, tc(COST_HEAD[c])),
|
||||
),
|
||||
el("td", { className: "m02-hopyo__compare", text: compare.split(" · ").join("\n") }),
|
||||
]);
|
||||
}
|
||||
|
||||
function matRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
|
||||
const parts = partsOf(line, ctx.d);
|
||||
const price = ctx.price(line);
|
||||
const wait = !!price && !price.calc && !price.fail;
|
||||
const mats = price?.calc?.자재 ?? [];
|
||||
const first = mats[0];
|
||||
const ours = mats.length ? mats.reduce((s, m) => s + (m.반영 ?? 0), 0) : null;
|
||||
const show = (v: number | null | undefined): string =>
|
||||
wait ? "…" : v === null || v === undefined ? "—" : v.toLocaleString("ko-KR");
|
||||
const p = parts[0];
|
||||
return rowOf(line, ctx, [
|
||||
el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }),
|
||||
el("td", {
|
||||
className: "m02-labrow__code",
|
||||
text: parts.map((x) => x.자재 ?? "").join(" · ") || "—",
|
||||
}),
|
||||
el("td", {
|
||||
className: "m02-labrow__name m02-hopyo__parts",
|
||||
children: [
|
||||
stateChip(line),
|
||||
...(parts.length ? partLines(line, parts, ctx) : []),
|
||||
small(`엑셀 ${line.이름}`.trim()),
|
||||
...nameNotes(line, parts, price),
|
||||
],
|
||||
}),
|
||||
el("td", {
|
||||
className: "m02-labrow__spec",
|
||||
text: p?.규격 ?? line.규격,
|
||||
attrs: { title: p?.규격 ?? line.규격 },
|
||||
}),
|
||||
qtyCell(line, parts),
|
||||
el("td", { className: "m02-labrow__unit", text: p?.단위 ?? line.단위 }),
|
||||
...markCells({ 별도계상: line.갈래 === "별도계상" ? "별도계상" : "직접계산" }),
|
||||
...PRICE_SLOTS.map((slot) =>
|
||||
el("td", {
|
||||
className: `m02-labrow__num m02-labrow__price${first?.기준칸 === slot ? " is-base-price" : ""}`,
|
||||
text: show(first?.가격?.[slot]),
|
||||
attrs: { "data-label": slot, "data-price-slot": slot },
|
||||
}),
|
||||
),
|
||||
pairCell(line.금액.계, price?.calc ? ours : null, wait, tc("Labor_AppliedSum")),
|
||||
]);
|
||||
}
|
||||
|
||||
const grid = (heads: string[], rows: HTMLElement[], kind: string): HTMLElement =>
|
||||
el("div", {
|
||||
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-labrow m02-hopyo__table m02-hopyo__lines",
|
||||
attrs: { "data-hopyo-table": kind },
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
||||
}),
|
||||
el("tbody", { children: rows }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
/** 줄 표 둘 — 로직 표 · 자재 표(줄 없는 표는 안 그림) */
|
||||
export function checkTables(lines: HopyoLine[], ctx: RowsContext): HTMLElement[] {
|
||||
const logic = lines.filter((l) => tableOf(l, partsOf(l, ctx.d)) === "logic");
|
||||
const mat = lines.filter((l) => !logic.includes(l));
|
||||
const out: HTMLElement[] = [];
|
||||
if (logic.length)
|
||||
out.push(
|
||||
grid(
|
||||
[
|
||||
"",
|
||||
tc("Col_Order"),
|
||||
tc("Sub"),
|
||||
tc("Col_Name"),
|
||||
tc("Col_Alias"),
|
||||
tc("Labor_QtyCol"),
|
||||
tc("Col_Unit"),
|
||||
...COSTS.map((c) => tc(COST_HEAD[c])),
|
||||
"품 비교",
|
||||
"",
|
||||
],
|
||||
logic.map((l) => logicRow(l, ctx)),
|
||||
"logic",
|
||||
),
|
||||
);
|
||||
if (mat.length)
|
||||
out.push(
|
||||
el("h4", { className: "m02-hopyo__subhead", text: "자재" }),
|
||||
grid(
|
||||
[
|
||||
"",
|
||||
tc("Col_Order"),
|
||||
tc("Labor_Code"),
|
||||
tc("Col_Name"),
|
||||
tc("Labor_Spec"),
|
||||
tc("Labor_QtyCol"),
|
||||
tc("Labor_Unit"),
|
||||
...markHeads(),
|
||||
...PRICE_SLOTS,
|
||||
`엑셀 / 우리 ${tc("Labor_AppliedSum")}`,
|
||||
"",
|
||||
],
|
||||
mat.map((l) => matRow(l, ctx)),
|
||||
"mat",
|
||||
),
|
||||
);
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Draft.ts
|
||||
* 확인 창 금액 미리 보기 꼴(PLAN 359-3) — 엑셀 줄 하나 + 지금 대상 → 일위대가 문서 한 벌(서버 `/labor/{열}/calc` 에
|
||||
* 보냄 · 저장 안 함 · 금액은 서버가 셈) · 줄이 들어갈 표(로직 표 · 자재 표).
|
||||
* 정본 = 서버 `M02_Hopyo_Build.Builder.build` 의 `put`([사전조합 생성] 때 다시 셈) — 같은 규칙:
|
||||
* 우리 수량 = 엑셀 수량 × 배 · 로직 = {로직, 입력값, 수량 {손}} · 자재 = {코드, 수량 {손}, 별도계상 =
|
||||
* 갈래 별도계상이면 「별도계상」 아니면 「직접계산」} · 같은 자재 = 수량 더함.
|
||||
* 미리 보기만 다른 것: 호표 대상(하위 사전조합 — 만든 뒤 셈) · 배 없는 대상은 뺌 ·
|
||||
* 같은 로직 둘째는 뺌(Build = 한 줄 사전조합으로 감쌈 · 한 문서에 같은 로직 둘 = 서버 400).
|
||||
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { HopyoLine, HopyoPart } from "../M02_MasterTemplete_Hopyo_Api";
|
||||
import type { LaborDoc, LaborMat, LaborRow } from "../M02_MasterTemplete_Labor_Api";
|
||||
|
||||
export type CheckTable = "logic" | "mat";
|
||||
|
||||
/** 엑셀 갈래 중 자재 표로 가는 것(대상 없을 때) */
|
||||
const MAT_KINDS = ["자재", "별도계상"];
|
||||
|
||||
/** 줄이 들어갈 표 — 대상이 모두 자재면 자재 표 · 대상 없으면 갈래(자재 · 별도계상) · 그 밖 로직 표 */
|
||||
export function tableOf(line: Pick<HopyoLine, "갈래">, parts: HopyoPart[]): CheckTable {
|
||||
if (parts.length) return parts.every((p) => !!p.자재) ? "mat" : "logic";
|
||||
return MAT_KINDS.includes(line.갈래) ? "mat" : "logic";
|
||||
}
|
||||
|
||||
/** 엑셀 줄 + 대상 → 미리 셈 문서 · 셀 것이 없으면 null(호표 대상뿐 · 배 없음 · 대상 없음) */
|
||||
export function draftDoc(
|
||||
line: Pick<HopyoLine, "수량" | "갈래">,
|
||||
parts: HopyoPart[],
|
||||
): LaborDoc | null {
|
||||
const rows: LaborRow[] = [];
|
||||
const mats = new Map<string, LaborMat>();
|
||||
const apart = line.갈래 === "별도계상" ? "별도계상" : "직접계산";
|
||||
for (const p of parts) {
|
||||
if (p.배 === null || p.호표 !== undefined) continue;
|
||||
const qty = (line.수량 ?? 0) * p.배;
|
||||
if (p.자재) {
|
||||
const have = mats.get(p.자재)?.수량;
|
||||
const hand = (have ? have.손 : 0) + qty;
|
||||
mats.set(p.자재, {
|
||||
코드: p.자재,
|
||||
이름: null,
|
||||
규격: null,
|
||||
단위: null,
|
||||
수량: { 손: hand },
|
||||
별도계상: apart,
|
||||
});
|
||||
} else if (p.로직 && !rows.some((r) => r.로직 === p.로직)) {
|
||||
const values = (p.입력값 ?? {}) as LaborRow["입력값"];
|
||||
rows.push({ 로직: p.로직, 별칭: null, 메모: null, 입력값: values, 수량: { 손: qty } });
|
||||
}
|
||||
}
|
||||
if (!rows.length && !mats.size) return null;
|
||||
return { 종류: "일위대가", 열: "", 단위: null, 담은로직: rows, 자재: [...mats.values()] };
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const STUB_FILES = [
|
||||
"M02_MasterTemplete_Labor_Api",
|
||||
"M02_MasterTemplete_Labor_Input",
|
||||
"M01_MasterData_UI_LogicLab_Source",
|
||||
"M02_MasterTemplete_Hopyo_CheckRows", // 표 그리기(DOM · Labor_Rows) — 359-3
|
||||
];
|
||||
const opened = [];
|
||||
const stub = new Proxy(
|
||||
@@ -74,6 +75,17 @@ let out0 = null;
|
||||
const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts"));
|
||||
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
|
||||
const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts"));
|
||||
const draft = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_Draft.ts"));
|
||||
if (input.drafts) {
|
||||
// 359-3 — 실물 줄 → 미리 셈 문서 · 표(서버 calc 에 그대로 보냄)
|
||||
const d = check.loadDraft("drafts");
|
||||
out0 = input.drafts.map((l) => {
|
||||
const parts = check.partsOf(l, d);
|
||||
return { 차례: l.차례, table: draft.tableOf(l, parts), doc: draft.draftDoc(l, parts) };
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out0));
|
||||
return;
|
||||
}
|
||||
if (input.real) {
|
||||
// 실물 맞춤 응답(338-4) — 서버 줄을 그대로 먹여 체크 · 몸 · 빠질 줄 · 품 비교 · 글
|
||||
const lines = input.real;
|
||||
@@ -177,6 +189,32 @@ let out0 = null;
|
||||
entry: ledger.entryOf({ ...rows[0], 갈래: "우리에 없음" }),
|
||||
};
|
||||
})(),
|
||||
draft: (() => {
|
||||
// 359-3 — 미리 셈 문서(우리 수량 = 엑셀 × 배 · 별도계상 · 같은 자재 더함 · 같은 로직 둘째 · 호표 · 배 없음 뺌)
|
||||
const L = { 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 2 };
|
||||
const M = { 자재: "MT1", 입력값: {}, 배: 1 };
|
||||
return {
|
||||
logic: draft.draftDoc({ 수량: 0.5, 갈래: "단가산출" }, [
|
||||
L,
|
||||
{ ...L, 배: 3 },
|
||||
{ 호표: 46, 입력값: {}, 배: 1 },
|
||||
{ 로직: "GF000101", 입력값: {}, 배: null },
|
||||
]),
|
||||
mat: draft.draftDoc({ 수량: 2, 갈래: "별도계상" }, [M, { ...M, 배: 0.5 }]),
|
||||
direct: draft.draftDoc({ 수량: null, 갈래: "자재" }, [M]).자재[0],
|
||||
none: [
|
||||
draft.draftDoc({ 수량: 1, 갈래: "단가산출" }, []),
|
||||
draft.draftDoc({ 수량: 1, 갈래: "호표" }, [{ 호표: 3, 입력값: {}, 배: 1 }]),
|
||||
],
|
||||
table: [
|
||||
draft.tableOf({ 갈래: "단가산출" }, [M]),
|
||||
draft.tableOf({ 갈래: "자재" }, [L]),
|
||||
draft.tableOf({ 갈래: "자재" }, [L, M]),
|
||||
draft.tableOf({ 갈래: "별도계상" }, []),
|
||||
draft.tableOf({ 갈래: "%" }, []),
|
||||
],
|
||||
};
|
||||
})(),
|
||||
sheet: {
|
||||
order: sheet.buildOrder([
|
||||
{ 번: 45, 아래: [46] },
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""확인 창 금액 미리 셈 이음매(PLAN 359-3) — 북부청 실물 호표 줄 → 화면 문서 꼴
|
||||
(`logic/M02_MasterTemplete_Hopyo_Draft.ts` · Node) → 서버 `/labor/{열}/calc`(저장 안 함).
|
||||
|
||||
체크 제안 로직 줄(제1~10호표) = 200 · 반영합 있음 · 우리 수량 = 엑셀 수량 × 배 ·
|
||||
자재 줄 = 자재 표 · 셈 뒤 층 폴더에 문서 안 생김.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester import test_m02_hopyo_excel as base
|
||||
from resources.tester.test_m02_hopyo_excel import _post
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
client = base.client # 공용 고정물(임시 층)
|
||||
CALC = "/api/m02/layers/personal/labor/hopyo-draft/calc"
|
||||
|
||||
|
||||
def _drafts(lines: list[dict]) -> list[dict]:
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_m02_hopyo_check.cjs")],
|
||||
input=json.dumps({"drafts": lines}, 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_실물_줄_미리_셈(client):
|
||||
assert _post(client).status_code == 200
|
||||
folder = layers.personal_dir(7, 42)
|
||||
fit.run_job(folder, "HP-000001") # 셈 끝(338 숫자 판정)
|
||||
before = sorted(p.name for p in folder.rglob("*") if p.is_file())
|
||||
lines = [
|
||||
{**line, "차례": no * 1000 + line["차례"]} # 호표 여럿 줄 한 번에(차례 = 열쇠만)
|
||||
for no in range(1, 11)
|
||||
for line in client.get(f"/api/m02/layers/personal/hopyo/HP-000001/{no}").json()["호표"][
|
||||
"줄"
|
||||
]
|
||||
]
|
||||
drafts = {x["차례"]: x for x in _drafts(lines)}
|
||||
sure = [x for x in lines if x["맞춤"]["체크"] and any("로직" in p for p in x["맞춤"]["대상"])]
|
||||
assert len(sure) >= 5, [x["맞춤"]["상태"] for x in lines]
|
||||
for line in sure:
|
||||
draft = drafts[line["차례"]]
|
||||
assert draft["doc"] is not None, line["이름"]
|
||||
res = client.post(CALC, json={"doc": draft["doc"], "수량": {}})
|
||||
assert res.status_code == 200, (line["이름"], res.text)
|
||||
got = res.json()
|
||||
if draft["table"] == "logic":
|
||||
row = draft["doc"]["담은로직"][0]
|
||||
part = line["맞춤"]["대상"][0]
|
||||
assert row["수량"]["손"] == line["수량"] * part["배"]
|
||||
assert got["반영합"]["계"] > 0, line["이름"]
|
||||
for line in lines: # 자재 · 별도계상 줄(대상 없음) = 자재 표
|
||||
if line["갈래"] in ("자재", "별도계상") and not line["맞춤"]["대상"]:
|
||||
assert drafts[line["차례"]]["table"] == "mat"
|
||||
after = sorted(p.name for p in folder.rglob("*") if p.is_file())
|
||||
assert after == before # 미리 셈 = 저장 안 함
|
||||
@@ -26,6 +26,7 @@ NEW = (
|
||||
"Hopyo_Side",
|
||||
"Hopyo_Sheet",
|
||||
"Hopyo_Check",
|
||||
"Hopyo_CheckRows",
|
||||
"Hopyo_Choose",
|
||||
"Hopyo_Ledger",
|
||||
)
|
||||
@@ -282,7 +283,8 @@ def test_옛_즉시_확정과_바로_만들기_없음():
|
||||
users = sorted(name for name, text in texts.items() if "buildHopyo(" in text)
|
||||
assert users == ["Hopyo_Check", "Hopyo_Sheet"] # 확인 창 · 엑셀 보기 [저장] 물음 뒤
|
||||
check = texts["Hopyo_Check"]
|
||||
assert '"확인하고 사전조합 만들기"' in check and "맞음 · 별칭 모두 체크" in check
|
||||
assert '"사전조합 생성"' in check and "맞음 · 별칭 모두 체크" in check
|
||||
assert "확인하고" not in check # 359-4 명사형
|
||||
assert "showLoadingOverlay()" in check and "dropDraft(k)" in check
|
||||
assert "m02-hopyo-check:" in check and "POLL_MS" in check
|
||||
sheet = texts["Hopyo_Sheet"]
|
||||
@@ -352,7 +354,8 @@ def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
|
||||
assert "await showSheet(got);" in side # 올린 직후 자동
|
||||
assert lazy in side.split("async function showSheet(")[1].split("async function showLedger(")[0]
|
||||
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
|
||||
assert lazy in check and '"엑셀에서 보기"' in check and "호표: opt.no" in check
|
||||
assert lazy in check and 'label: "엑셀 보기"' in check and "호표: opt.no" in check
|
||||
assert "엑셀에서 보기" not in check
|
||||
assert "단산행: 행" in check
|
||||
pick = _text("M02_MasterTemplete_Hopyo_Pick.ts")
|
||||
assert lazy in pick
|
||||
@@ -398,16 +401,14 @@ def test_확인_창_손질_339():
|
||||
② 만들기 뒤 좌측 사전조합 목록 다시 ③ 엑셀 보기 창 목록 셈 끝까지 다시 읽음 · 펼침 표 숫자 판정 칸."""
|
||||
css = _text("M02_MasterTemplete_Hopyo.css")
|
||||
assert "word-break: keep-all;" in css and "white-space: pre-line;" in css
|
||||
assert (
|
||||
".m02-hopyo__lines .m02-hopyo__acts > *" in css
|
||||
and "text-overflow"
|
||||
not in css.split(".m02-hopyo__lines td:is(:nth-child(9)")[-1].split("}")[0]
|
||||
)
|
||||
assert ".m02-hopyo__lines .m02-hopyo__acts > *" in css
|
||||
assert "nth-child(9)" not in css # 359-3 — 칸 차례 대신 칸 이름
|
||||
assert ".m02-hopyo-ledger__table td:nth-child(1)" in css
|
||||
assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts")
|
||||
rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts")
|
||||
assert r'text: compare.split(" · ").join("\n")' in rows # 품 비교 = 항목마다 한 줄
|
||||
assert "m02-hopyo__opts" in rows # 옵션 = 항목마다 한 줄
|
||||
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
|
||||
assert r'td(opts.join("\n"))' in check and r'.split(" · ")' in check
|
||||
assert r'.join("\n"),' in check # 품 비교 = 항목마다 한 줄(판정 글 함수는 그대로)
|
||||
make = check.split("async function make(")[1].split("function builtBlock(")[0]
|
||||
assert make.index("document.dispatchEvent(new Event(HOPYO_BUILT));") < make.index("opt.onBuilt")
|
||||
side = _text("M02_MasterTemplete_UI_Side.ts")
|
||||
@@ -431,3 +432,44 @@ def test_엑셀_보기_머리_막대_셈_끝까지_다시_읽음_359():
|
||||
assert drop.index("deleteHopyo(") < drop.index("modal.close();") < drop.index("opt.onDeleted")
|
||||
poll = sheet.split("function poll(")[1].split("async function load(")[0]
|
||||
assert "paintHead(next);" in poll and "paintList();" in poll and "poll(next);" in poll
|
||||
|
||||
|
||||
def test_확인_창_표_일위대가_상세_꼴_359():
|
||||
"""359-3 — 로직 표 · 자재 표(일위대가 상세 꼴) + 품 비교 · 단추 · 드롭다운 글(저장 값 그대로) ·
|
||||
금액 = 줄마다 서버 calc 미리 셈(`logic/` 문서 꼴 · Build 와 같은 수량 규칙)."""
|
||||
got = _check()["draft"]
|
||||
assert got["logic"] == {
|
||||
"종류": "일위대가",
|
||||
"열": "",
|
||||
"단위": None,
|
||||
"담은로직": [ # 같은 로직 둘째 · 호표 · 배 없음 = 뺌
|
||||
{"로직": "GC000198", "별칭": None, "메모": None, "입력값": {"소량감률": 50},
|
||||
"수량": {"손": 1}},
|
||||
],
|
||||
"자재": [],
|
||||
} # fmt: skip
|
||||
assert got["mat"]["자재"] == [ # 같은 자재 = 2 × 1 + 2 × 0.5
|
||||
{"코드": "MT1", "이름": None, "규격": None, "단위": None, "수량": {"손": 3},
|
||||
"별도계상": "별도계상"},
|
||||
] # fmt: skip
|
||||
assert got["direct"]["별도계상"] == "직접계산" and got["direct"]["수량"] == {"손": 0}
|
||||
assert got["none"] == [None, None]
|
||||
assert got["table"] == ["mat", "logic", "logic", "mat", "logic"]
|
||||
rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts")
|
||||
assert '["", "추가"],' in rows and '["우리에 없음", "로직없음"],' in rows
|
||||
assert '["안 씀", "사용안함"],' in rows and "뺌 — " not in rows
|
||||
for word in ('"고르기"', '"원문 보기"', '"엑셀 보기"', '"옵션"', '"품 비교"'):
|
||||
assert word in rows, word
|
||||
assert 'import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";' in rows
|
||||
assert "...markCells({ 별도계상:" in rows and "...markHeads()," in rows
|
||||
for head in ("Col_Order", "Sub", "Col_Name", "Col_Alias", "Labor_QtyCol", "Col_Unit"):
|
||||
assert f'tc("{head}")' in rows, head
|
||||
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
|
||||
assert "calcLabor(target.layer, DRAFT_COLUMN, doc, {}, target.owner)" in check
|
||||
assert "checkTables(h.줄, ctx())" in check and "draftDoc(line, partsOf(line, d))" in check
|
||||
assert "function lineRow(" not in check and "function partCell(" not in check
|
||||
draft = _text("logic/M02_MasterTemplete_Hopyo_Draft.ts")
|
||||
assert "M02_Hopyo_Build" in draft # 정본 짝 표시
|
||||
code = draft.split("*/", 1)[1] # 머리 글 밖
|
||||
for gone in ("document", "fetch(", "sessionStorage", "@ui/"):
|
||||
assert gone not in code, gone
|
||||
|
||||
Reference in New Issue
Block a user