feat(780-2): 엑셀 보기 단가산출 탭 오른쪽 = 단가산출 목록 — 판 바꾸기 · 거르기 · 목록 ↔ 시트 · 두 벌 맞춤(요구 627)
- 새 Hopyo_SheetSk: 탭(역할) 따라 오른쪽 판 바꿈 · 일위대가표 = 호표 목록 그대로 · 단가산출표 · 상세 = 마법사 단가산출 칸 좁은 꼴 한 벌(처음 올 때 지음 · 다른 층 · 권한 없음 = 보기만) · 시트 칸 → 목록 강조 · 목록 줄 → 지금 탭 블록 자리 - WizardSk: 좁은 꼴 옵션 · 부름 칸(일위대가 n 곳 · 일위대가 밖) · 거르기(전체 · 안 · 밖 · 고를 줄 있음) · 같은 캐시 두 벌 맞춤 알림 · 옮김 뒤 HOPYO_BUILT(열린 엑셀 보기 칠 다시) - SkPick: 줄 0 블록 = 「줄 없음」 확인 막음 · 거르기 · 끝내기 글 재료에 일위대가 밖 수 - SheetNav: role · blockAt · spotOf 내보냄 · SheetMap: 판정 없음 글 → 「일위대가 밖」 - Wizard [끝내기]: 막지 않음 · 물음창에 안 옮긴 단가산출 n(일위대가 밖 m) - 시험: 새 test_627_sheet_sk(판 바꿈 · 목록 지음 · 줄 0 막음 · 캐시 공유) · test_778_5 끝내기 재료 꼴에 밖 칸 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,10 @@
|
||||
* · 고름 · 확인 = 캐시 `m02-hopyo-sk:{층}:{주인}:{키}`(`SkPick` 꼴) — 누를 때 서버에 안 감 · 자동 저장 없음
|
||||
* · [확인한 것 옮기기 (n)] = 몸 `{확인: [{번, 고름}]}` → 서버가 M01 지금 값으로 다시 셈 · 단위 SK(확인 안 한 블록 안 만듦) ·
|
||||
* 만든 블록은 확인에서 뺌 · 못만듦 = 띠(까닭 그대로) · 「모두 확인」 단추 없음
|
||||
* · `count()` = 아직 안 옮긴 블록 · 고를 줄(마법사 [끝내기] 막힘 글)
|
||||
* · `count()` = 아직 안 옮긴 블록 · 그중 일위대가 밖 · 고를 줄(마법사 [끝내기] 글)
|
||||
* · 780-2(요구 627) — 부름 칸(「일위대가 n 곳」 · 「일위대가 밖」) · 거르기(전체 · 안 · 밖 · 고를 줄 있음) ·
|
||||
* 좁은 꼴(`narrow` = 엑셀 보기 오른쪽 · 칸 여섯 · 블록 줄 누름 = `onRow` · `mark(번)` 강조) · `readOnly` = 옮기기 숨김 ·
|
||||
* 두 벌 맞춤 = 알림 `SK_SYNC`(같은 캐시 열쇠 · 고름 바뀜 → 다시 그림 · 옮김 → 받은 판정 그대로) · 옮김 뒤 `HOPYO_BUILT`
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
@@ -17,6 +20,8 @@ import { t as L } from "@ui/ui_template_locale";
|
||||
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 { createSegment } from "@ui/ui_template_segment";
|
||||
import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check";
|
||||
import { formatPrice } from "../common_util/common_util_money";
|
||||
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { judgeSangeun } from "./M02_MasterTemplete_Hopyo_SkApi";
|
||||
@@ -24,6 +29,8 @@ import { skLineTable } from "./M02_MasterTemplete_Hopyo_SkLines";
|
||||
import {
|
||||
GOOD,
|
||||
canConfirm,
|
||||
filterBlocks,
|
||||
outside,
|
||||
picksOf,
|
||||
readDraft,
|
||||
setPick,
|
||||
@@ -34,6 +41,7 @@ import {
|
||||
waiting,
|
||||
type SkBlock,
|
||||
type SkDraft,
|
||||
type SkFilter,
|
||||
type SkJudged,
|
||||
} from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
||||
|
||||
@@ -46,26 +54,63 @@ export const skDraftKey = (scope: HopyoScope, key: string): string =>
|
||||
|
||||
export interface SkStep {
|
||||
root: HTMLElement;
|
||||
/** 아직 안 옮긴 블록 · 고를 줄 — 판정 전 null */
|
||||
count: () => { 블록: number; 줄: number } | null;
|
||||
/** 아직 안 옮긴 블록 · 그중 일위대가 밖 · 고를 줄 — 판정 전 null */
|
||||
count: () => { 블록: number; 밖: number; 줄: number } | null;
|
||||
/** 그 블록 펼치고 화면에 보임(안 이은 줄 창 [단가산출 걸음에서 확인]) */
|
||||
show: (번: number) => void;
|
||||
/** 그 블록 줄 강조 · 보이게(펼치지 않음 · 거른 줄이면 거르기 풂) — null = 강조 지움 */
|
||||
mark: (번: number | null) => void;
|
||||
}
|
||||
|
||||
export interface SkStepOptions {
|
||||
/** 좁은 꼴(엑셀 보기 오른쪽) — 칸 여섯 · 금액은 줄 title */
|
||||
narrow?: boolean;
|
||||
/** 블록 줄 누름(단추 밖) */
|
||||
onRow?: (번: number) => void;
|
||||
/** 옮기기 숨김(만들 층 ≠ 파일 층 · 쓰기 권한 없음) */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
/** 두 벌(마법사 칸 · 엑셀 보기 오른쪽) 맞춤 알림 — detail `{dk, from, got?}` */
|
||||
export const SK_SYNC = "m02-hopyo-sk";
|
||||
type SyncDetail = { dk: string; from: object; got?: SkJudged };
|
||||
|
||||
/** ② 단가산출 칸 — `파일` = 엑셀 보기 창 머리 · `onChange` = 판정 · 옮김 뒤(끝내기 글) */
|
||||
export function sangeunStep(
|
||||
scope: HopyoScope,
|
||||
key: string,
|
||||
파일: string,
|
||||
onChange?: () => void,
|
||||
o: SkStepOptions = {},
|
||||
): SkStep {
|
||||
const dk = skDraftKey(scope, key);
|
||||
const me = {};
|
||||
const load = (): SkDraft => readDraft(sessionStorage.getItem(dk));
|
||||
const save = (d: SkDraft): void => sessionStorage.setItem(dk, JSON.stringify(d));
|
||||
const tell = (more: Partial<SyncDetail> = {}): void =>
|
||||
void document.dispatchEvent(new CustomEvent<SyncDetail>(SK_SYNC, { detail: { dk, from: me, ...more } })); // prettier-ignore
|
||||
let got: SkJudged | null = null;
|
||||
let fails: SkJudged["못만듦"] = [];
|
||||
let current: number | null = null; // 강조 블록
|
||||
let filter: SkFilter = "all";
|
||||
const open = new Set<number>(); // 펼친 블록
|
||||
const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } });
|
||||
const seg = createSegment({
|
||||
options: [
|
||||
{ value: "all", label: L("M02_SkFilterAll") },
|
||||
{ value: "in", label: L("M02_SkFilterIn") },
|
||||
{ value: "out", label: L("M02_SkFilterOut") },
|
||||
{ value: "open", label: L("M02_SkFilterOpen") },
|
||||
],
|
||||
value: filter,
|
||||
size: 24,
|
||||
ariaLabel: L("M02_SkFilter"),
|
||||
onChange: (v) => {
|
||||
filter = v as SkFilter;
|
||||
draw();
|
||||
},
|
||||
});
|
||||
seg.root.dataset.skFilter = "";
|
||||
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
|
||||
@@ -74,11 +119,24 @@ export function sangeunStep(
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo",
|
||||
children: [
|
||||
createText(L("M02_SkHint"), "hint", 14),
|
||||
...(o.narrow ? [] : [createText(L("M02_SkHint"), "hint", 14)]),
|
||||
seg.root,
|
||||
out,
|
||||
el("div", { className: "ui-toolbar", children: [move, again] }),
|
||||
el("div", { className: "ui-toolbar", children: o.readOnly ? [again] : [move, again] }),
|
||||
],
|
||||
});
|
||||
/** 다른 벌이 고름 · 옮김을 알림 — 떨어진 벌은 듣기 그침 */
|
||||
const onSync = (e: Event): void => {
|
||||
const x = (e as CustomEvent<SyncDetail>).detail;
|
||||
if (!root.isConnected) return document.removeEventListener(SK_SYNC, onSync);
|
||||
if (x.dk !== dk || x.from === me || !got) return;
|
||||
if (x.got) {
|
||||
got = x.got;
|
||||
fails = [];
|
||||
}
|
||||
draw();
|
||||
};
|
||||
document.addEventListener(SK_SYNC, onSync);
|
||||
|
||||
const sheet = (행: number): void =>
|
||||
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
||||
@@ -114,6 +172,8 @@ export function sangeunStep(
|
||||
save({ ...d, 확인: d.확인.filter((n) => !made.has(n)) });
|
||||
got = next;
|
||||
fails = next.못만듦;
|
||||
tell({ got: next });
|
||||
if (made.size) document.dispatchEvent(new Event(HOPYO_BUILT)); // 열린 엑셀 보기 칠 · 좌측 나무 다시
|
||||
showToast(
|
||||
L("M02_SkMoved")
|
||||
.replace("{made}", String(made.size))
|
||||
@@ -129,18 +189,39 @@ export function sangeunStep(
|
||||
}
|
||||
}
|
||||
|
||||
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 cols: Column[] = o.narrow
|
||||
? [
|
||||
{ label: L("M02_SkColBlock") },
|
||||
{ label: L("M02_SkColCall"), width: "92px" },
|
||||
{ label: L("M02_SkColVerdict"), width: "92px" },
|
||||
{ label: L("M02_SkColMade"), width: "92px" },
|
||||
{ label: L("M02_SkSure"), width: "64px" },
|
||||
{ label: "", width: "60px" },
|
||||
]
|
||||
: [
|
||||
{ label: L("M02_SkColBlock"), width: "72px" },
|
||||
{ label: L("M02_SangeunColName") },
|
||||
{ label: L("M02_SangeunColUnit"), width: "56px" },
|
||||
{ label: L("M02_SkColCall"), width: "104px" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
/** 부름 칸 — 「일위대가 n 곳」(title 호표 번) · 「일위대가 밖」 · 옛 서버(칸 없음) = — */
|
||||
function callCell(b: SkBlock): HTMLElement | string {
|
||||
if (!b.부름) return "—";
|
||||
const out = outside(b);
|
||||
const word = out ? L("M02_SkCallOut") : L("M02_SkCallIn").replace("{n}", String(b.부름.length)); // prettier-ignore
|
||||
const badge = createBadge(word, out ? "neutral" : "info");
|
||||
badge.title = b.부름.map((n) => `제${n}호표`).join(" · ");
|
||||
badge.dataset.skCall = out ? "out" : "in";
|
||||
return badge;
|
||||
}
|
||||
|
||||
/** 블록 한 줄 + 펼친 줄 표 */
|
||||
function blockRows(b: SkBlock, d: SkDraft): HTMLTableRowElement[] {
|
||||
@@ -161,10 +242,13 @@ export function sangeunStep(
|
||||
onChange: (on) => {
|
||||
save(setSure(load(), b, on));
|
||||
draw();
|
||||
tell();
|
||||
},
|
||||
});
|
||||
sure.root.dataset.skSure = String(b.번);
|
||||
sure.root.title = ok ? L("M02_SkSureTip") : L("M02_SkSureOff");
|
||||
sure.root.title = ok
|
||||
? L("M02_SkSureTip")
|
||||
: L(b.줄.length ? "M02_SkSureOff" : "M02_SkSureEmpty");
|
||||
const lines = createButton({
|
||||
label: `${L("M02_SkOpen")} ${b.줄.length}`,
|
||||
size: 24,
|
||||
@@ -177,20 +261,34 @@ export function sangeunStep(
|
||||
});
|
||||
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] }),
|
||||
]);
|
||||
const view = first !== null && !o.narrow ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => sheet(first) }) : ""; // prettier-ignore
|
||||
const name = `단산${b.번} ${b.이름 ?? ""}${b.단위 ? ` [${b.단위}]` : ""}`;
|
||||
const head = tableRow(
|
||||
cols,
|
||||
o.narrow
|
||||
? [name, callCell(b), verdict, made, sure.root, lines]
|
||||
: [
|
||||
`단산${b.번}`,
|
||||
b.이름 ?? "",
|
||||
b.단위 ?? "",
|
||||
callCell(b),
|
||||
verdict,
|
||||
won(b.우리금액),
|
||||
won(b.엑셀금액),
|
||||
basis,
|
||||
made,
|
||||
sure.root,
|
||||
el("div", { className: "ui-toolbar", children: [lines, view] }),
|
||||
],
|
||||
);
|
||||
head.dataset.skBlock = String(b.번);
|
||||
if (b.번 === current) head.classList.add("is-current");
|
||||
if (o.narrow) {
|
||||
head.title = `${name} · ${L("M02_SkColOurs")} ${won(b.우리금액)} · ${L("M02_SkColExcel")} ${won(b.엑셀금액)}`; // prettier-ignore
|
||||
head.addEventListener("click", (e) => {
|
||||
if (!(e.target as Element).closest("button, label, input")) o.onRow?.(b.번);
|
||||
});
|
||||
}
|
||||
if (!open.has(b.번)) return [head];
|
||||
const table = skLineTable({
|
||||
scope,
|
||||
@@ -199,6 +297,7 @@ export function sangeunStep(
|
||||
onPick: (at, p) => {
|
||||
save(tidy(setPick(load(), b.번, at, p), got!.판정));
|
||||
draw();
|
||||
tell();
|
||||
},
|
||||
onSheet: sheet,
|
||||
});
|
||||
@@ -221,7 +320,8 @@ export function sangeunStep(
|
||||
.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 shown = filterBlocks(blocks, d, filter);
|
||||
for (const b of shown) tbody.append(...blockRows(b, d));
|
||||
const band = fails.length
|
||||
? [
|
||||
createBanner({
|
||||
@@ -231,10 +331,12 @@ export function sangeunStep(
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const far = blocks.filter(outside).length;
|
||||
const where = blocks.some((b) => b.부름) ? ` · ${L("M02_SkSummaryIn").replace("{in}", String(blocks.length - far)).replace("{out}", String(far))}` : ""; // prettier-ignore
|
||||
out.replaceChildren(
|
||||
createText(sum, "muted", 14),
|
||||
createText(sum + where, "muted", 14),
|
||||
...band,
|
||||
blocks.length ? table : createText(L("M02_SkNone"), "muted", 14),
|
||||
shown.length ? table : createText(L("M02_SkNone"), "muted", 14),
|
||||
);
|
||||
move.disabled = !d.확인.length;
|
||||
move.querySelector(".ui-btn__label")!.textContent = L("M02_SkMove").replace(
|
||||
@@ -253,5 +355,18 @@ export function sangeunStep(
|
||||
draw();
|
||||
out.querySelector(`[data-sk-block="${번}"]`)?.scrollIntoView({ block: "center" });
|
||||
},
|
||||
mark: (번) => {
|
||||
out.querySelector("tr.is-current")?.classList.remove("is-current");
|
||||
current = 번;
|
||||
if (번 === null || !got) return;
|
||||
if (!out.querySelector(`[data-sk-block="${번}"]`) && filter !== "all") {
|
||||
filter = "all";
|
||||
seg.set("all");
|
||||
draw();
|
||||
}
|
||||
const row = out.querySelector(`[data-sk-block="${번}"]`);
|
||||
row?.classList.add("is-current");
|
||||
row?.scrollIntoView({ block: "nearest" });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user