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:
@@ -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}호표`,
|
||||
|
||||
Reference in New Issue
Block a user