feat(742-1): 일위대가 안 줄 수량 = 사용자 입력 저장 · 기준단위 단가 / 수량 반영 금액 열
- 723-1 「안 줄 수량 null」 걷음 — SC 줄 · 자재 수량 {손} 저장(새 줄 1 · 빈 칸 = 「수량 입력」 · 금액 빠짐)
- 셈 차례: 층 = 알약 값 → 저장 수량 · 공사 굳힌 사본 = 굳힌 손 → 알약 → 저장(with_pills 가 {저장} 칸으로 옮김)
- 불러오기로 만든 일위대가 = 엑셀 수량을 저장 수량으로(본뜬.실무수량 같은 값 남김)
- 단가산출 안 일위대가 = 저장 수량으로 금액(수량 없음 없어짐) · B08 자재 · B09 내 자료집 저장도 같은 차례
- 일위대가표 수량 칸 = 문서에 저장(휘발 임시 칸 걷음) · 합계 열 「기준단위 단가」 · 「수량 반영 금액」
- 낡은 시험(723 · 175 · 204-2 · 702-3 · 703 · 717-4 · 719-1 · 719-2 · hopyo build · flow · labor ref)의 null 단언을
저장 수량 단언으로 고침 — 723-1 결정이 742-1(요구 584-1)로 바뀜 · 새 지킴 시험 test_742_1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,13 @@
|
||||
* M02 일위대가(서고 · 사전조합) 상세 = 일위대가표만(PLAN 723-2 · 내역 16 · 17 · 요구서_내역16 2장 3~5).
|
||||
* · 차례 = 정보(이름 · 구분 · 단위 · 본뜬) → 일위대가표(엑셀 꼴 격자 · `Labor_Grid.laborBlock`) · 오른쪽 원문 패널
|
||||
* · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄)
|
||||
* · 수량 칸 = 확인용 임시 입력(723-1 · 휘발 · 문서 · 저장 · dirty 안 건드림) → 그 줄 금액 · 합계 미리 보기
|
||||
* (짝 셈 `Labor_Calc.settle` · 서버 안 부름) · 문서 안 줄 수량은 늘 null(쓰는 쪽 알약으로 이음)
|
||||
* · 수량 칸 = 일위대가 1 단위에 드는 양(742-1 · 요구 584-1 · 문서에 저장 · 새 줄 1 · 빈 칸 = 「수량 입력」 ·
|
||||
* 금액 빠짐) → 그 줄 금액 · 합계 바로(짝 셈 `Labor_Calc.settle`) · 구조물 조합 · 공사 알약이 이어지면 그 값이 앞섬
|
||||
* · 합계 열 = 「기준단위 단가」(줄 1 단위) · 「수량 반영 금액」(× 수량)
|
||||
* · 머리 단추 = [항목 추가](736-1 · 자재는 찾기 창에서 고름 → 자재 목록) · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제
|
||||
* · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) ·
|
||||
* 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸)
|
||||
* · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 수량 걷고 다시 셈)
|
||||
* · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 다시 셈)
|
||||
* · 안 이은 고정 줄(729-3) = 격자 위 띠 · [연결](`Labor_Unlinked` — 이음만 · 저장은 [저장])
|
||||
* 구조물 조합 일위대가(카드 · 열 상세)는 그대로 `Labor_Detail`(면적 잇기 · 수량산출).
|
||||
* ========================================================================== */
|
||||
@@ -49,10 +50,13 @@ import "@ui/ui_template_general_layout.css";
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
|
||||
const TEXT = {
|
||||
TempHint: [
|
||||
"수량 칸 = 확인용 임시 입력(저장 안 함) — 실제 수량은 구조물 조합 · 프로젝트에서 이음",
|
||||
"Quantity = temporary check only (not saved) — real quantities link in structures · projects",
|
||||
QtyHint: [
|
||||
"수량 = 이 일위대가 1 단위에 드는 양(저장) — 구조물 조합 · 공사에서 알약 · 계산 값이 이어지면 그 값이 앞섬",
|
||||
"Quantity = amount per 1 unit of this item (saved) — linked pills · project values take precedence",
|
||||
],
|
||||
NeedQty: ["수량 입력", "Enter quantity"],
|
||||
BasePrice: ["기준단위 단가", "Unit price"],
|
||||
QtyAmount: ["수량 반영 금액", "Amount × qty"],
|
||||
Alias: ["별칭", "Alias"],
|
||||
AliasAsk: ["별칭(비우면 지움)", "Alias (empty = clear)"],
|
||||
RemoveAsk: ["「{value}」 줄을 지울까?", "Delete line “{value}”?"],
|
||||
@@ -61,9 +65,10 @@ const TEXT = {
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
||||
|
||||
/** 비율 고정 줄(705-3) — 수량 = 율(문서에 남음 · 임시 입력 아님) */
|
||||
/** 비율 고정 줄(705-3) — 수량 = 율 */
|
||||
const isRate = (row: LaborRow): boolean => !!row.고정?.비율;
|
||||
const matKey = (code: string): string => `자재|${code}`;
|
||||
/** 새 줄 수량(742-1) — 1 · 사용자가 고침 */
|
||||
const ONE = { 손: 1 } as const;
|
||||
|
||||
export function mountLaborTable(
|
||||
host: HTMLElement,
|
||||
@@ -71,14 +76,9 @@ export function mountLaborTable(
|
||||
opt: LaborDetailOptions,
|
||||
): LaborDetailHandle {
|
||||
const cur = normalizeLabor(doc, opt.column);
|
||||
// 723-1 — 옛 문서 손 수량은 안 씀(서버도 저장 때 걷음) · 비율 줄 율만
|
||||
for (const r of cur.담은로직) if (!isRate(r)) r.수량 = null;
|
||||
for (const m of cur.자재) m.수량 = null;
|
||||
const saved = JSON.stringify(cur);
|
||||
const edit = !opt.readOnly;
|
||||
const logics = new Map<string, ItemLogic>();
|
||||
/** 임시 수량(휘발) — 줄 id · 「자재|코드」 → 수 */
|
||||
const temp = new Map<string, number>();
|
||||
let calc: LaborCalc | null = null;
|
||||
let last: LaborCalc | null = null;
|
||||
let fail: string | null = null;
|
||||
@@ -95,19 +95,6 @@ export function mountLaborTable(
|
||||
return () => watchers.delete(fn);
|
||||
};
|
||||
|
||||
/** 셈 몸 = 문서 + 임시 수량(저장 안 함) */
|
||||
const withTemp = (): LaborDoc => ({
|
||||
...cur,
|
||||
담은로직: cur.담은로직.map((r) => {
|
||||
const v = temp.get(lineId(r) ?? "");
|
||||
return v === undefined ? r : { ...r, 수량: { 손: v } };
|
||||
}),
|
||||
자재: cur.자재.map((m) => {
|
||||
const v = temp.get(matKey(m.코드));
|
||||
return v === undefined ? m : { ...m, 수량: { 손: v } };
|
||||
}),
|
||||
});
|
||||
|
||||
const gridHost = el("div", { attrs: { "data-labor-grid": "", "data-labor-table": "" } });
|
||||
const source = createSourceView({
|
||||
path: "/m02/logic-lib/source",
|
||||
@@ -128,28 +115,30 @@ export function mountLaborTable(
|
||||
if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root);
|
||||
}
|
||||
|
||||
/** 일위대가표 — 줄 누름 · 우클릭 · 수량 칸(임시) · 빈 값 붉게 */
|
||||
/** 일위대가표 — 줄 누름 · 우클릭 · 수량 칸(저장) · 빈 값 · 빈 수량 붉게 */
|
||||
const draw = (): void => {
|
||||
const body = withTemp();
|
||||
const block = laborBlock(body, calc ?? last, logics);
|
||||
const block = laborBlock(cur, calc ?? last, logics);
|
||||
const empties = new Set<number>();
|
||||
block.lines.forEach((ln, n) => {
|
||||
const row = body.담은로직[n];
|
||||
if (!row) return void (ln.note = ln.note === tc("Grid_NoQty") ? "" : ln.note); // 자재 줄
|
||||
const empty = row.로직 ? valueLine(logics.get(row.로직)?.입력 ?? [], row.입력값).empty : 0;
|
||||
if (empty) empties.add(n);
|
||||
const row = cur.담은로직[n];
|
||||
if (ln.note === tc("Grid_NoQty")) ln.note = t("NeedQty"); // 742-1 빈 수량 = 사용자가 넣음
|
||||
const empty = row?.로직 ? valueLine(logics.get(row.로직)?.입력 ?? [], row.입력값).empty : 0;
|
||||
if (empty) ln.note = t("Empty").replace("{n}", String(empty));
|
||||
else if (ln.note === tc("Grid_NoQty")) ln.note = ""; // 수량 없음 = 서고에선 보통(723-1)
|
||||
if (empty || ln.note === t("NeedQty")) empties.add(n);
|
||||
});
|
||||
const have = calc ?? last;
|
||||
// 빈 줄 = 단가 모름(까닭) · 빈 값만 — 임시 수량 없는 줄은 안 셈
|
||||
block.blank = have ? block.lines.filter((l) => l.prices.sum == null).length + empties.size : 0;
|
||||
// 빈 줄 = 금액 모름(단가 까닭 · 빈 수량) · 빈 값
|
||||
block.blank = have
|
||||
? block.lines.filter((l, n) => l.amounts.sum == null || empties.has(n)).length
|
||||
: 0;
|
||||
if (fail && block.lines[0]) block.lines[0].note = fail;
|
||||
const g = hopyoGrid([block]);
|
||||
const at = (i: number): number | null => g.at.get(i)?.[1] ?? null;
|
||||
const label: Record<string, string> = { sum_price: t("BasePrice"), sum: t("QtyAmount") };
|
||||
const grid = createHopyoGrid(
|
||||
[block],
|
||||
{
|
||||
columns: g.columns.map((c) => (label[c.key] ? { ...c, label: label[c.key] } : c)),
|
||||
onRowClick: (i) => {
|
||||
const n = at(i);
|
||||
if (n !== null) openAt(n);
|
||||
@@ -163,13 +152,13 @@ export function mountLaborTable(
|
||||
: undefined,
|
||||
editable: (i, key) => {
|
||||
const n = at(i);
|
||||
if (key !== "qty" || n === null) return false;
|
||||
if (!edit || key !== "qty" || n === null) return false;
|
||||
const row = cur.담은로직[n];
|
||||
return !row || !isRate(row); // 임시 입력이라 읽기만 층도 됨
|
||||
return !row || !isRate(row);
|
||||
},
|
||||
onEdit: (i, _key, text) => {
|
||||
const n = at(i);
|
||||
if (n !== null) setTemp(n, text);
|
||||
if (n !== null) setQty(n, text);
|
||||
},
|
||||
cellClass: (i, key) => {
|
||||
const n = at(i);
|
||||
@@ -202,10 +191,9 @@ export function mountLaborTable(
|
||||
fail = null;
|
||||
draw();
|
||||
try {
|
||||
const body = withTemp();
|
||||
const got = opt.calc
|
||||
? await opt.calc(body)
|
||||
: await calcLabor(opt.layer, opt.column, body, undefined, opt.owner);
|
||||
? await opt.calc(cur)
|
||||
: await calcLabor(opt.layer, opt.column, cur, undefined, opt.owner);
|
||||
if (!alive || mine !== seq) return;
|
||||
calc = last = got;
|
||||
} catch (error) {
|
||||
@@ -215,34 +203,27 @@ export function mountLaborTable(
|
||||
draw();
|
||||
}, 150);
|
||||
};
|
||||
/** `qtyOnly` = 수량만(임시) — 짝 셈으로 바로 */
|
||||
/** `qtyOnly` = 수량만 — 짝 셈으로 바로 */
|
||||
const changed = (qtyOnly = false): void => {
|
||||
opt.onChanged(dirty());
|
||||
if (qtyOnly && calc) {
|
||||
calc = settle(calc, withTemp(), {});
|
||||
calc = settle(calc, cur, {});
|
||||
draw();
|
||||
} else recalc();
|
||||
};
|
||||
|
||||
/** 임시 수량(휘발) — 빈 글 = 지움 · 수 아님 = 알림 */
|
||||
function setTemp(n: number, text: string): void {
|
||||
const row = cur.담은로직[n];
|
||||
const key = row ? (lineId(row) ?? "") : matKey(cur.자재[n - cur.담은로직.length]?.코드 ?? "");
|
||||
/** 수량 칸(742-1 · 저장) — 빈 글 = 빈 수량(「수량 입력」) · 수 아님 · 음수 = 알림 */
|
||||
function setQty(n: number, text: string): void {
|
||||
const row = cur.담은로직[n] ?? cur.자재[n - cur.담은로직.length];
|
||||
const raw = text.replace(/,/g, "").trim();
|
||||
if (!raw) temp.delete(key);
|
||||
else if (Number.isFinite(Number(raw))) temp.set(key, Number(raw));
|
||||
else return showToast(t("NotNumber"), "error");
|
||||
calc = calc && settle(calc, withTemp(), {});
|
||||
draw();
|
||||
const v = Number(raw);
|
||||
if (!row) return;
|
||||
if (raw && !(Number.isFinite(v) && v >= 0)) return showToast(t("NotNumber"), "error");
|
||||
row.수량 = raw ? { 손: v } : null;
|
||||
changed(true);
|
||||
}
|
||||
/** 붙여넣기(705-2) 뒤 — 엑셀 수량은 임시 칸으로(문서엔 안 남김) */
|
||||
/** 붙여넣기(705-2) 뒤 — 엑셀 수량 = 저장 수량 */
|
||||
function afterPaste(): void {
|
||||
for (const r of cur.담은로직) {
|
||||
const v = r.수량?.손;
|
||||
if (isRate(r) || v == null) continue;
|
||||
temp.set(lineId(r) ?? "", v);
|
||||
r.수량 = null;
|
||||
}
|
||||
void loadLogics().then(() => changed());
|
||||
}
|
||||
|
||||
@@ -340,13 +321,8 @@ export function mountLaborTable(
|
||||
}
|
||||
function remove(n: number): void {
|
||||
const rows = cur.담은로직.length;
|
||||
if (n < rows) {
|
||||
temp.delete(lineId(cur.담은로직[n]) ?? "");
|
||||
cur.담은로직.splice(n, 1);
|
||||
} else {
|
||||
temp.delete(matKey(cur.자재[n - rows]?.코드 ?? ""));
|
||||
cur.자재.splice(n - rows, 1);
|
||||
}
|
||||
if (n < rows) cur.담은로직.splice(n, 1);
|
||||
else cur.자재.splice(n - rows, 1);
|
||||
changed();
|
||||
}
|
||||
|
||||
@@ -363,14 +339,21 @@ export function mountLaborTable(
|
||||
const id = lineId({ 참조: ref });
|
||||
if (cur.담은로직.some((r) => lineId(r) === id))
|
||||
return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error");
|
||||
cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: null });
|
||||
cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: { ...ONE } });
|
||||
changed();
|
||||
},
|
||||
fixed: (fx: LaborFixed, name: string) => {
|
||||
const id = lineId({ 고정: fx });
|
||||
if (cur.담은로직.some((r) => lineId(r) === id))
|
||||
return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error");
|
||||
cur.담은로직.push({ 로직: "", 고정: fx, 별칭: null, 메모: null, 입력값: {}, 수량: null });
|
||||
cur.담은로직.push({
|
||||
로직: "",
|
||||
고정: fx,
|
||||
별칭: null,
|
||||
메모: null,
|
||||
입력값: {},
|
||||
수량: { ...ONE },
|
||||
});
|
||||
changed();
|
||||
},
|
||||
/** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */
|
||||
@@ -384,7 +367,7 @@ export function mountLaborTable(
|
||||
? "Labor_InLogicMat"
|
||||
: cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat";
|
||||
if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error");
|
||||
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null });
|
||||
cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: { ...ONE } });
|
||||
changed();
|
||||
},
|
||||
};
|
||||
@@ -395,7 +378,7 @@ export function mountLaborTable(
|
||||
openLogicPicker((x) => {
|
||||
if (cur.담은로직.some((r) => r.로직 === x.키))
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null });
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: { ...ONE } });
|
||||
void loadLogics().then(() => changed());
|
||||
}, withHopyo(library)),
|
||||
});
|
||||
@@ -466,7 +449,7 @@ export function mountLaborTable(
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
createText(t("TempHint"), "hint"),
|
||||
createText(t("QtyHint"), "hint"),
|
||||
gridHost,
|
||||
],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user