feat(M01-쉬운단가): Cards 를 Model·Pick 실물에 맞춤 - 잠금 판정 · 덧붙임 더하기 · 묶음 안 ▲▼ · PLAN Cards 검증완료
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
This commit is contained in:
@@ -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<HTMLElement, Map<string, string>>();
|
||||
const closedGroups = new WeakMap<HTMLElement, Set<Group>>();
|
||||
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<string, CalcLine> {
|
||||
@@ -144,20 +153,16 @@ function patchCard(ctx: Ctx, card: EasyCard, patch: Partial<EasyCard>): 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<void> {
|
||||
@@ -166,16 +171,13 @@ async function repick(ctx: Ctx, card: EasyCard): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user