diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Cards.ts b/M01_MasterData/EasyCalc/M01_EasyCalc_Cards.ts index 4e6d5aebd..3eb4f7ca8 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Cards.ts +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Cards.ts @@ -12,10 +12,11 @@ import { formatNumber } from "../M01_MasterData_UI_Logic_Edit"; import { formatMoney } from "../M01_MasterData_UI_Logic_Money"; import { addCard, + addPercent, checkCard, - moveCard, removeCard, updateCard, + withInputs, type CostKind, type EasyCard, type EasyDoc, @@ -98,7 +99,6 @@ interface Ctx { const badTexts = new WeakMap>(); const closedGroups = new WeakMap>(); -let blankSeq = 0; let picking = false; const groupOf = (card: EasyCard): Group => { @@ -108,8 +108,17 @@ const groupOf = (card: EasyCard): Group => { return kind === "인력" ? "person" : kind === "재료" ? "material" : "logic"; }; +/** Model 의 toRow 와 같은 나눔 — 줄 차례가 어긋나지 않게 */ const partOf = (card: EasyCard): "호표" | "덧줄" => - card.source?.part ?? (card.kind === "percent" ? "덧줄" : "호표"); + card.kind === "percent" + ? "덧줄" + : card.kind === "locked" + ? (card.source?.part ?? "덧줄") + : "호표"; + +/** 식 수량이거나 종류를 못 알아본 줄 — 숫자로 바꾸기 전까지 잠김 */ +const isLocked = (card: EasyCard): boolean => + card.qtyFormula !== null || (card.kind === "locked" && card.lockedWhy !== null); /** 카드 → 서버 계산 줄 — 호표 줄 차례 뒤에 덧줄 차례 · 줄 수가 안 맞으면(옛 응답) 없음 */ function lineMap(doc: EasyDoc, ans: CalcAnswer | null): Map { @@ -144,20 +153,16 @@ function patchCard(ctx: Ctx, card: EasyCard, patch: Partial): void { ctx.on.change(updateCard(ctx.doc, card.id, patch)); } -/** 같은 묶음 안 이웃을 지나갈 때까지 한 칸씩 — 묶음 밖 카드가 사이에 있어도 눈에 보이게 옮겨짐 */ -function moveInGroup(doc: EasyDoc, card: EasyCard, step: -1 | 1): EasyDoc { +/** 화면 묶음 안 이웃과 자리 바꿈 — 잠금 카드도 같은 묶음이라 Model.moveCard 의 묶음과 달라 여기서 바꿈 */ +function swapInGroup(doc: EasyDoc, card: EasyCard, step: -1 | 1): EasyDoc { const same = doc.cards.filter((c) => groupOf(c) === groupOf(card)); const neighbor = same[same.indexOf(card) + step]; if (!neighbor) return doc; - const at = (d: EasyDoc, id: string): number => d.cards.findIndex((c) => c.id === id); - let next = doc; - for (let n = doc.cards.length; n > 0; n--) { - if ((at(next, card.id) - at(next, neighbor.id)) * step >= 0) break; - const moved = moveCard(next, card.id, step); - if (moved === next) break; - next = moved; - } - return next; + const cards = doc.cards.slice(); + const a = cards.indexOf(card); + const b = cards.indexOf(neighbor); + [cards[a], cards[b]] = [cards[b], cards[a]]; + return { ...doc, cards }; } async function repick(ctx: Ctx, card: EasyCard): Promise { @@ -166,16 +171,13 @@ async function repick(ctx: Ctx, card: EasyCard): Promise { try { const pick = await openEasyPick(card.kind); if (!pick) return; - let next = updateCard(ctx.doc, card.id, { + const next = updateCard(ctx.doc, card.id, { name: pick.name, spec: pick.spec, unit: pick.unit, element: pick.element, }); - const have = new Set(next.inputs.map((i) => i.이름)); - const extra = (pick.needInputs ?? []).filter((i) => !have.has(i.이름)); - if (extra.length) next = { ...next, inputs: [...next.inputs, ...extra] }; - ctx.on.change(next); + ctx.on.change(withInputs(next, pick.needInputs ?? [])); } finally { picking = false; } @@ -193,22 +195,7 @@ async function addByPick(ctx: Ctx, kind: PickResult["kind"]): Promise { } function addBlankPercent(ctx: Ctx): void { - const card: EasyCard = { - id: `blank-${++blankSeq}`, - kind: "percent", - name: "", - spec: "", - unit: "%", - qty: null, - qtyFormula: null, - cost: "경비", - percentBase: "노무비", - element: null, - lockedWhy: null, - source: null, - raw: null, - }; - ctx.on.change({ ...ctx.doc, cards: [...ctx.doc.cards, card] }); + ctx.on.change(addPercent(ctx.doc, "", "노무비", null, "경비")); } async function lockToNumber(ctx: Ctx, card: EasyCard, value: number): Promise { @@ -309,7 +296,7 @@ function toolButtons(ctx: Ctx, card: EasyCard, group: EasyCard[]): HTMLElement | variant: "ghost", size: "sm", disabled: off, - onClick: () => ctx.on.change(moveInGroup(ctx.doc, card, step)), + onClick: () => ctx.on.change(swapInGroup(ctx.doc, card, step)), }); b.setAttribute("aria-label", label); return b; @@ -332,54 +319,55 @@ function toolButtons(ctx: Ctx, card: EasyCard, group: EasyCard[]): HTMLElement | /** 한 카드 = 문장 한 줄(좁으면 두 줄로 접힘) */ function cardView(ctx: Ctx, card: EasyCard, group: EasyCard[]): HTMLElement { const g = groupOf(card); - const locked = card.kind === "locked"; + const locked = isLocked(card); + const percent = card.kind === "percent"; + const plain = card.kind === "locked"; const name = nameWidget(ctx, card); + const value = locked ? lockedValue(ctx, card) : qtyInput(ctx, card); const parts: (HTMLElement | string)[] = [text("m01-easycards-tag", `【${tc(`Tag_${g}`)}】`)]; - if (g === "percent") { - const base = card.percentBase; + if (percent) { parts.push( selectBox( ctx, BASES, - locked ? null : base, + card.percentBase, (v) => tc(`Base_${v}`), (v) => patchCard(ctx, card, { percentBase: v }), tc("Slot"), ), tc("Pre_percent"), - locked ? lockedValue(ctx, card) : qtyInput(ctx, card), - locked ? "" : tc("Mid_percent"), + value, + tc("Mid_percent"), name, tc("Post_percent"), ); } else { parts.push( name, - g === "person" ? tc("Pre_person") : tc("Pre_pick"), - locked ? lockedValue(ctx, card) : qtyInput(ctx, card), + plain ? " " : g === "person" ? tc("Pre_person") : tc("Pre_pick"), + value, text("m01-easycards-unit", ` ${card.unit}`), - tc(`Post_${g}` as "Post_person"), + plain ? "" : tc(`Post_${g}` as "Post_person"), ); } - const slot = - g === "percent" && !locked - ? el("span", { - className: "m01-easycards-slot", - children: [ - ` · ${tc("Slot")} `, - selectBox( - ctx, - COSTS, - card.cost, - (v) => tc(`Base_${v}`), - (v) => patchCard(ctx, card, { cost: v }), - tc("Slot"), - ), - ], - }) - : null; + const slot = percent + ? el("span", { + className: "m01-easycards-slot", + children: [ + ` · ${tc("Slot")} `, + selectBox( + ctx, + COSTS, + card.cost, + (v) => tc(`Base_${v}`), + (v) => patchCard(ctx, card, { cost: v }), + tc("Slot"), + ), + ], + }) + : null; const head = el("div", { className: "m01-easycards-line",