Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts
T
eomsangdonandClaude Opus 5.5 09134bab84 refactor(M02): 띠 알림 · 빈 상태 · 보조 글 · 배지 · 탭 · 표 · 목록 줄 · 도구줄 · 체크를 부품으로 (PLAN 553-5)
- DataFlow: 표 넷 = 데이터표 · 함수 판 알림 · 선/알약 경고 · 끊긴 곳 = 띠 알림 · 알약 · 칸 종류 = 배지 · 고르는 줄 = 목록 줄 · 입력 넷 = 입력 · 도구줄 여섯 · 날 색 → 공통 값
- 구조물 옵션 · 구조물 · 도면 · 면적 · 품목 고르기: 판 띠 = 띠 알림 · 한눈/종류별/부품 = 탭 · 표 둘 = 데이터표 · 체크 둘 = 체크 · 카드 · 번호 = 카드 · 배지 · 도면 도구줄
- 일위대가 · 호표 · 실무자료 · 본 화면 · 서고: 알림 = 띠 알림 · 빈 상태 · 보조 글 · 도구줄 여섯 · 옛 보라 별칭 0
- M01 로직 입력칸 = 입력 부품(작은 칸) · 일위대가 상세 줄 수 고정 시험 693 · 지킴 시험 M02 몫 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:44:19 +09:00

370 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M02_MasterTemplete_Hopyo_Ledger.ts
* 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶.
* · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만)
* · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안
* · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [로직 추가] · 배 · 몫 · [옵션] · [삭제] ·
* 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로)
* · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사)
* · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기.
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { loadingError, showLoading } from "@ui/ui_template_loading";
import {
readLedger,
saveLedger,
type HopyoScope,
type LedgerEntry,
type LedgerPair,
type LedgerRow,
} from "./M02_MasterTemplete_Hopyo_Api";
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
import "./M02_MasterTemplete_Hopyo.css";
export const LEDGER_TITLE = "별칭 장부";
/** 별칭 장부 뜻 한 줄(359-4) — 좌측 단추 title · 화면 머리 */
export const LEDGER_HINT =
"엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안";
const KINDS = ["짝", "우리에 없음", "안 씀"] as const;
/** 갈래 화면 글 — 확인 창 드롭다운과 같음(저장 값 그대로) */
export const KIND_TEXT: Record<string, string> = {
짝: "짝",
"우리에 없음": "로직없음",
"안 씀": "사용안함",
};
const LAYER_TEXT: Record<string, string> = { system: "마스터", company: "회사", personal: "개인" };
type Edits = Record<string, LedgerEntry | null>;
/** 화면 한 줄 — 서버 줄 위에 고친 것 */
export type LedgerView = LedgerRow & { 고친?: boolean };
export const ledgerKey = (scope: HopyoScope): string =>
`m02-hopyo-ledger:${scope.layer}:${scope.owner ?? ""}`;
function loadEdits(k: string): Edits {
try {
return JSON.parse(sessionStorage.getItem(k) ?? "{}") as Edits;
} catch {
return {};
}
}
function saveEdits(k: string, edits: Edits): void {
try {
if (Object.keys(edits).length) sessionStorage.setItem(k, JSON.stringify(edits));
else sessionStorage.removeItem(k);
} catch {
/* 캐시 못 씀 = 이 화면에만 남음 */
}
}
/** 쓰기 꼴 — 보기 칸(층 · 누가 · 때 …) 뺌 */
export const entryOf = (row: LedgerEntry): LedgerEntry => ({
원래: row.원래,
갈래: row.갈래,
짝:
row.갈래 === "짝"
? row.짝.map((p) => ({ ...p, 입력값: p.입력값 ?? {}, 몫: p.몫 ?? null }))
: [],
엑셀조항: row.엑셀조항 ?? [],
});
/** 서버 줄 + 고친 것 → 화면 줄 — 이 층 줄은 고친 값 · 지운 것 뺌 · 위 층에서 옮긴 줄은 이 층 줄로 더함 */
export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): LedgerView[] {
const mine = rows.filter((r) => r.고침);
const out: LedgerView[] = [];
for (const r of mine) {
if (!(r.열쇠 in edits)) out.push(r);
else if (edits[r.열쇠]) out.push({ ...r, ...edits[r.열쇠]!, 고친: true });
}
for (const [key, entry] of Object.entries(edits)) {
if (!entry || mine.some((r) => r.열쇠 === key)) continue;
out.push({
...entry,
열쇠: key,
누가: null,
때: null,
처음파일: null,
층: scope.layer,
주인: scope.owner,
고침: true,
고친: true,
});
}
return [...out, ...rows.filter((r) => !r.고침)];
}
/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */
export const pairText = (p: LedgerPair): string =>
[
`${p.로직 ?? p.자재 ?? ""} × ${p.배}`,
p.몫 != null ? `몫 ${p.몫}` : "",
Object.entries(p.입력값 ?? {})
.map(([k, v]) => `${k}=${String(v)}`)
.join(", "),
]
.filter(Boolean)
.join(" · ");
/** 찾기 · 갈래 거름 */
export const filterRows = (rows: LedgerView[], q: string, kind: string): LedgerView[] => {
const want = q.trim().toLowerCase();
return rows.filter(
(r) =>
(!kind || r.갈래 === kind) &&
(!want ||
`${r.원래.품명} ${r.원래.규격} ${r.짝.map(pairText).join(" ")}`
.toLowerCase()
.includes(want)),
);
};
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const td = (text: string, cls = ""): HTMLElement =>
el("td", { text, className: cls, attrs: { title: text } });
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
export interface LedgerOptions {
scope: HopyoScope;
/** 이 층 쓰기 권한 — 없으면 보기만 */
canWrite: boolean;
}
export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
const root = el("div", {
className: "m02-hopyo m02-hopyo-ledger",
attrs: { "data-hopyo-ledger": "" },
});
const k = ledgerKey(opt.scope);
let edits = loadEdits(k);
let rows: LedgerRow[] = [];
const search = el("input", {
className: "ui-input ui-input--sm m02-hopyo__search",
attrs: { type: "search", placeholder: "품명 · 규격 · 로직 찾기" },
}) as HTMLInputElement;
const kind = el("select", {
className: "ui-input ui-input--sm",
children: [
el("option", { text: "갈래 전체", attrs: { value: "" } }),
...KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })),
],
}) as HTMLSelectElement;
const save = createButton({ label: "저장", variant: "filled", size: "sm" });
const drop = createButton({ label: "변경 취소", variant: "ghost", size: "sm" });
const body = el("tbody");
const keep = (): void => {
saveEdits(k, edits);
paint();
};
const edit = (r: LedgerView, next: Partial<LedgerEntry>): void => {
edits[r.열쇠] = entryOf({ ...entryOf(r), ...next });
keep();
};
function pairCell(r: LedgerView, mine: boolean): HTMLElement {
const pairs: HTMLElement[] = r.짝.map((p, at) => {
const parts: HTMLElement[] = [el("span", { text: pairText(p) })];
if (mine) {
const times = el("input", {
className: "ui-input ui-input--sm m02-hopyo__times",
attrs: { type: "number", step: "any", min: "0", value: String(p.배), title: "배" },
}) as HTMLInputElement;
times.addEventListener("change", () =>
edit(r, { 짝: r.짝.map((x, i) => (i === at ? { ...x, 배: Number(times.value) } : x)) }),
);
parts.push(times);
if (p.로직)
parts.push(
createButton({
label: "옵션",
variant: "ghost",
size: "sm",
onClick: () =>
void fetchLogicInputs([p.로직!]).then(
(got) =>
openLaborInput({
title: p.로직!,
logic: got.로직[0] ?? null,
values: p.입력값 as never,
memo: null,
readOnly: false,
done: (res) =>
edit(r, {
짝: r.짝.map((x, i) => (i === at ? { ...x, 입력값: res.입력값 } : x)),
}),
}),
(error) => showToast(why(error), "error"),
),
}),
createButton({
label: "×",
variant: "ghost",
size: "sm",
onClick: () => edit(r, { 짝: r.짝.filter((_, i) => i !== at) }),
}),
);
}
return el("div", { className: "m02-hopyo__part", children: parts });
});
if (mine && r.갈래 === "짝")
pairs.push(
createButton({
label: "로직 추가",
variant: "ghost",
size: "sm",
onClick: () =>
openLogicPicker((x) =>
edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }),
),
}),
);
return el("td", { className: "m02-hopyo__parts", children: pairs });
}
function rowOf(r: LedgerView): HTMLElement {
const mine = opt.canWrite && r.고침;
const kindCell = mine
? (() => {
const s = el("select", {
className: "ui-input ui-input--sm",
children: KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })),
}) as HTMLSelectElement;
s.value = r.갈래;
s.addEventListener("change", () =>
edit(r, { 갈래: s.value as LedgerEntry["갈래"], 짝: s.value === "짝" ? r.짝 : [] }),
);
return el("td", { children: [s] });
})()
: td(KIND_TEXT[r.갈래] ?? r.갈래);
const acts: HTMLElement[] = [];
if (mine)
acts.push(
createButton({
label: "삭제",
variant: "danger",
size: "sm",
onClick: () => {
edits[r.열쇠] = null;
keep();
},
}),
);
else if (opt.canWrite && !rows.some((x) => x.고침 && x.열쇠 === r.열쇠) && !edits[r.열쇠])
acts.push(
createButton({
label: "내 층 복사",
variant: "ghost",
size: "sm",
onClick: () => {
edits[r.열쇠] = entryOf(r);
keep();
},
}),
);
const when = [r.누가?.이름, r.때?.replace("T", " ").slice(0, 16), r.처음파일]
.filter(Boolean)
.join(" · ");
return el("tr", {
className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`,
attrs: { "data-ledger": r.열쇠 },
children: [
td(r.원래.품명),
td(r.원래.규격),
td(r.원래.단위),
kindCell,
pairCell(r, mine),
td(when),
td(r.고침 ? "내 층" : (LAYER_TEXT[r.층] ?? r.층)),
el("td", { className: "m02-hopyo__acts", children: acts }),
],
});
}
function paint(): void {
const all = mergeRows(rows, edits, opt.scope);
const shown = filterRows(all, search.value, kind.value);
const n = Object.keys(edits).length;
save.querySelector(".ui-btn__label")!.textContent = n ? `저장 (${n})` : "저장";
save.disabled = drop.disabled = !n;
body.replaceChildren(
...(shown.length
? shown.map(rowOf)
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "8" } })] })]),
);
}
search.addEventListener("input", paint);
kind.addEventListener("change", paint);
drop.addEventListener("click", () => {
edits = {};
keep();
});
save.addEventListener("click", async () => {
save.disabled = true;
try {
rows = await saveLedger(opt.scope, edits);
edits = {};
showToast("장부 저장", "success");
keep();
} catch (error) {
showToast(`저장 못 함 — ${why(error)}`, "error");
paint();
}
});
async function load(): Promise<void> {
const stop = showLoading(root);
try {
rows = await readLedger(opt.scope);
} catch (error) {
root.replaceChildren(loadingError(why(error), () => void load()));
return;
} finally {
stop();
}
root.replaceChildren(
el("div", {
className: "ui-toolbar m02-hopyo__bar",
children: [
el("strong", { text: LEDGER_TITLE }),
note(`${LEDGER_HINT} · 사전조합 생성 때 남음 · 아래 층이 이김`),
search,
kind,
...(opt.canWrite ? [save, drop] : []),
],
}),
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-ledger__table",
children: [
el("thead", {
children: [
el("tr", {
children: [
"엑셀 품명",
"규격",
"단위",
"갈래",
"우리 로직 · 배 · 몫 · 옵션",
"누가 · 때 · 처음 파일",
"층",
"",
].map((t) => el("th", { text: t })),
}),
],
}),
body,
],
}),
],
}),
);
paint();
}
void load();
return root;
}