feat(M01): 인력 한 표 · 왼쪽 「전체」 + 구분 → 상세구분 두 단 거름 · 준용 모든 줄 고르기
- 인력 열 순서 = 키 … 옛이름 · 비고 · 준용 · 상태 읽기 전용 - 구분·상세구분 목록은 서버 /subs · 고른 거름은 sessionStorage 기억 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -34,7 +34,7 @@ const SIZE = 50;
|
||||
const MARKET = "재료_시중물가.json";
|
||||
const LINKED = "재료_품셈재료.json";
|
||||
const JOB = ["인력.json"];
|
||||
const NOTE = "비고";
|
||||
const NOTE = "비고"; // 끝 열(인력은 준용 앞) · 줄에 없으면 빈 칸 · 눌러 적음
|
||||
const SUPPLY = ["값", "조달"];
|
||||
const SOURCE = [...SUPPLY, "출처"];
|
||||
const NARA = "나라장터:";
|
||||
@@ -68,7 +68,7 @@ function hidden(file: string, col: string): boolean {
|
||||
/** 키(한 번 주면 안 바뀜) · 빌더가 원문에서 채운 칸(구분 · 상세구분 · 옛이름) — 읽기 전용. */
|
||||
const fixed = (file: string, col: string): boolean =>
|
||||
col === "키" ||
|
||||
(JOB.includes(file) && ["구분", "상세구분", "옛이름"].includes(col.split(SEP)[0]));
|
||||
(JOB.includes(file) && ["구분", "상세구분", "상태", "옛이름"].includes(col.split(SEP)[0]));
|
||||
|
||||
const HEADS: Record<string, string[]> = {
|
||||
[MARKET]: ["조달 값", "조달 기준", "M01_ProcureCol"],
|
||||
@@ -115,14 +115,16 @@ export function renderRows(
|
||||
for (const k of Object.keys(flatten(row)))
|
||||
if (!hidden(file, k) && k !== NOTE && !cols.includes(k)) cols.push(k);
|
||||
}
|
||||
const heads = (HEADS[file] ?? []).map((h) =>
|
||||
el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }),
|
||||
);
|
||||
const head = el("tr", {
|
||||
children: [
|
||||
el("th"),
|
||||
...cols.map((c) => el("th", { text: c })),
|
||||
...(HEADS[file] ?? []).map((h) =>
|
||||
el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }),
|
||||
),
|
||||
el("th", { text: NOTE }), // 비고 — 항상 끝 열(줄에 없으면 빈 칸 · 눌러 적음)
|
||||
...(JOB.includes(file) ? [] : heads),
|
||||
el("th", { text: NOTE }),
|
||||
...(JOB.includes(file) ? heads : []),
|
||||
],
|
||||
});
|
||||
const body = el("tbody");
|
||||
@@ -145,6 +147,7 @@ export function renderRows(
|
||||
onEdit: (text) => setAdd(file, i, withNote(row, text)),
|
||||
}),
|
||||
);
|
||||
if (JOB.includes(file)) tr.append(el("td"));
|
||||
body.append(tr);
|
||||
});
|
||||
|
||||
@@ -178,7 +181,6 @@ export function renderRows(
|
||||
: (next: Row): void => editRow(file, d.version, key, row, next);
|
||||
if (file === MARKET) tr.append(...marketCells(row, cur, put));
|
||||
if (file === LINKED) tr.append(linkedCell(row, cur, put));
|
||||
if (JOB.includes(file)) tr.append(wageCell(row, cur, put));
|
||||
tr.append(
|
||||
buildCell(show(cur[NOTE]), {
|
||||
changed: !same(cur[NOTE], row[NOTE]),
|
||||
@@ -188,6 +190,7 @@ export function renderRows(
|
||||
: (text) => editRow(file, d.version, key, row, withNote(cur, text)),
|
||||
}),
|
||||
);
|
||||
if (JOB.includes(file)) tr.append(wageCell(row, cur, put));
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
@@ -328,7 +331,7 @@ function linkedCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement {
|
||||
function wageCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement {
|
||||
const text = show(cur["준용"]);
|
||||
const td = buildCell(named(text), { changed: text !== show(row["준용"]) });
|
||||
if (put && row["값"] === null) {
|
||||
if (put) {
|
||||
const btn = rowBtn(L("M01_JobFind"));
|
||||
btn.addEventListener("click", () =>
|
||||
openPickModal({
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface SideHandle {
|
||||
setActive: (id: string | null) => void;
|
||||
}
|
||||
|
||||
const LABOR_FILTER = "m01.laborFilter";
|
||||
const LABOR_ID = "인력|";
|
||||
const MATERIAL_NAMES: Record<string, string> = { 나라장터자재: "나라장터", 오피넷유가: "유가" };
|
||||
|
||||
/** 파일 이름 → 목록 글자 — 「소요량_건설품셈_10장_창호…」 → 「건설품셈_10장_창호…」 */
|
||||
@@ -98,6 +100,78 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
|
||||
return button;
|
||||
};
|
||||
|
||||
/** 인력 — 「전체」 한 줄 + 구분 → 상세구분 두 단 거름(서버가 준 목록 · 고른 것은 기억) */
|
||||
const laborFilter = (
|
||||
file: FileInfo,
|
||||
open: (file: FileInfo, sub: string, label: string, detail?: string) => void,
|
||||
): HTMLElement => {
|
||||
const list = kinds.get("인력") ?? [];
|
||||
const saved = (() => {
|
||||
try {
|
||||
return JSON.parse(sessionStorage.getItem(LABOR_FILTER) ?? "[]") as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
const pickSub = list.find((k) => k.name === saved[0]);
|
||||
const choose = (name: string, values: string[], value: string): HTMLSelectElement => {
|
||||
const select = el("select", {
|
||||
className: "m01-side__select",
|
||||
attrs: { "aria-label": name },
|
||||
children: [L("M01_All"), ...values].map((v, i) =>
|
||||
el("option", { text: v, attrs: { value: i ? v : "" } }),
|
||||
),
|
||||
});
|
||||
select.value = value;
|
||||
return select;
|
||||
};
|
||||
const sub = choose(
|
||||
L("M01_LaborSub"),
|
||||
list.map((k) => k.name),
|
||||
pickSub?.name ?? "",
|
||||
);
|
||||
const detail = choose(L("M01_LaborDetail"), pickSub?.details ?? [], saved[1] ?? "");
|
||||
detail.disabled = !pickSub?.details.length;
|
||||
const run = (): void => {
|
||||
try {
|
||||
sessionStorage.setItem(LABOR_FILTER, JSON.stringify([sub.value, detail.value]));
|
||||
} catch {
|
||||
/* 기억 못 해도 거름은 됨 */
|
||||
}
|
||||
const label = [sub.value, detail.value].filter(Boolean).join(" · ") || L("M01_All");
|
||||
setActive(LABOR_ID);
|
||||
open(file, sub.value, label, detail.value);
|
||||
};
|
||||
sub.addEventListener("change", () => {
|
||||
const details = list.find((k) => k.name === sub.value)?.details ?? [];
|
||||
detail.replaceChildren(
|
||||
...[L("M01_All"), ...details].map((v, i) =>
|
||||
el("option", { text: v, attrs: { value: i ? v : "" } }),
|
||||
),
|
||||
);
|
||||
detail.disabled = details.length === 0;
|
||||
run();
|
||||
});
|
||||
detail.addEventListener("change", run);
|
||||
const all = make({ id: LABOR_ID, label: L("M01_All"), count: file.rows, run }, "", 0, () => {
|
||||
sub.value = detail.value = "";
|
||||
sub.dispatchEvent(new Event("change"));
|
||||
});
|
||||
return el("div", {
|
||||
children: [
|
||||
all,
|
||||
el("label", {
|
||||
className: "m01-side__filter",
|
||||
children: [el("span", { text: L("M01_LaborSub") }), sub],
|
||||
}),
|
||||
el("label", {
|
||||
className: "m01-side__filter",
|
||||
children: [el("span", { text: L("M01_LaborDetail") }), detail],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const drawGroup = (group: Group): void => {
|
||||
const files = lists.get(group) ?? [];
|
||||
const body = bodies.get(group);
|
||||
@@ -105,7 +179,11 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
|
||||
const open = (file: FileInfo, sub: string, label: string, detail = ""): void =>
|
||||
onOpen({ group, file, sub, detail, label });
|
||||
const one = files[0];
|
||||
if (group === "인력" || group === "기계") {
|
||||
if (group === "인력" && one) {
|
||||
body.replaceChildren(laborFilter(one, open));
|
||||
return;
|
||||
}
|
||||
if (group === "기계") {
|
||||
const list = kinds.get(group) ?? [];
|
||||
const all: TreeNode[] = one
|
||||
? [
|
||||
@@ -170,7 +248,7 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
|
||||
const subs = await fetchSubs(file);
|
||||
kinds.set(group, subs);
|
||||
await Promise.all(
|
||||
subs.map(async (k) =>
|
||||
(group === "기계" ? subs : []).map(async (k) =>
|
||||
counts.set(`${group}\n${k.name}`, (await fetchRows(file, 1, 1, "", false, k.name)).total),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -299,3 +299,15 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.m01-side__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
}
|
||||
|
||||
.m01-side__select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ export const ui_locales_m1 = {
|
||||
M01_JobTitle: ["준용 직종 고르기", "Pick substitute job"],
|
||||
M01_JobCol: ["준용", "Substitute"],
|
||||
M01_All: ["전체", "All"],
|
||||
M01_LaborSub: ["구분", "Category"],
|
||||
M01_LaborDetail: ["상세구분", "Detail"],
|
||||
M01_GroupLabor: ["인력", "Labor"],
|
||||
M01_GroupMaterial: ["재료", "Materials"],
|
||||
M01_GroupMachine: ["기계", "Machines"],
|
||||
|
||||
Reference in New Issue
Block a user