feat(M02): 단가산출 로직 줄 순서 — ▲▼ 작은 단추 + 손잡이 끌기 · 구조물 조합 카드도(85-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-08 12:07:39 +09:00
co-authored by Claude Opus 5.5
parent 875ceec7f7
commit 084b52001e
5 changed files with 92 additions and 13 deletions
@@ -90,7 +90,33 @@
white-space: nowrap;
}
/* 순서 칸 2행(51-14) — 위 [위로] [아래로] / 아래 [옵션]·[수정] [삭제] · 줄 사이도 단추 묶음 간격 */
/* 순서(85-3) — 손잡이 + ▲▼ 작은 단추 · 끌어 놓을 줄 위 선 */
.m02-labrow__order {
display: inline-flex;
align-items: center;
gap: 2px;
}
.m02-labrow__arrow.ui-btn {
min-width: 0;
height: 20px;
padding: 0 5px;
font-size: 10px;
line-height: 1;
}
.m02-labrow__grip {
padding: 0 4px;
color: var(--color-text-secondary);
cursor: grab;
user-select: none;
}
.m02-labrow__row.is-dragover td {
box-shadow: inset 0 2px 0 var(--color-primary);
}
/* 순서 칸 2행(51-14) — 위 손잡이 ▲▼ / 아래 [옵션]·[수정] [삭제] · 줄 사이도 단추 묶음 간격 */
.m02-labrow__tools {
display: inline-flex;
flex-direction: column;
@@ -186,7 +186,7 @@ export function mountLaborDetail(
option: openRow,
ref: openRef,
qty: openQty,
...(opt.card ? {} : { move }),
move: reorder,
remove: removeRow,
alias: setAlias,
}
@@ -310,10 +310,10 @@ export function mountLaborDetail(
}
};
function move(i: number, d: number): void {
const j = i + d;
if (j < 0 || j >= cur.담은로직.length) return;
[cur.담은로직[i], cur.담은로직[j]] = [cur.담은로직[j], cur.담은로직[i]];
/** 줄 순서(85-3) — from 줄을 to 자리로(▲▼ = 옆 자리 · 끌기 = 놓은 줄 자리) */
function reorder(from: number, to: number): void {
if (from === to || to < 0 || to >= cur.담은로직.length) return;
cur.담은로직.splice(to, 0, ...cur.담은로직.splice(from, 1));
changed();
}
/** 줄 고름(47-5) — 하이라이트 + 하위 행(51-2 · 표만 다시 그림 · 다이어그램 안 다시 그림) + 원문 패널(47-4) ·
@@ -148,6 +148,9 @@ function moneyCell(
}
/** 이름 칸 글마다 title = 전체 글(한 줄 말줄임 · 45-7) */
/** 끌고 있는 줄 차례(85-3) — 손잡이 dragstart ~ drop */
let dragFrom: number | null = null;
const titled = (td: HTMLElement): HTMLElement => {
for (const c of td.children) c.setAttribute("title", c.textContent ?? "");
return td;
@@ -179,7 +182,8 @@ export interface RowHandlers {
ref?: (i: number) => void;
/** 수량산출 칸 누름(손 입력 · 엑셀에서 고르기) — 없으면 줄 누름과 같음 */
qty?: (i: number) => void;
move?: (i: number, d: number) => void;
/** 줄 순서(85-3) — from 줄을 to 자리로(▲▼ = 옆 자리 · 손잡이 끌기 = 놓은 줄 자리) */
move?: (from: number, to: number) => void;
remove?: (i: number) => void;
/** 별칭 칸 고침(44-6 · 글자마다 · 빈 글 = null) — 없으면 글만 보임 */
alias?: (i: number, value: string | null) => void;
@@ -324,6 +328,32 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
run();
},
});
// 순서(85-3) — ▲▼ 작은 단추(글은 title · aria) + 손잡이 끌기(줄 전체는 안 끎 · 별칭 칸 글 고르기)
const arrow = (mark: string, label: string, to: number): HTMLElement => {
const b = small(mark, () => move?.(i, to));
b.classList.add("m02-labrow__arrow");
b.title = label;
b.setAttribute("aria-label", label);
if (to < 0 || to >= data.rows.length) (b as HTMLButtonElement).disabled = true;
return b;
};
const grip = (): HTMLElement => {
const g = el("span", {
className: "m02-labrow__grip",
text: "⠿",
attrs: { draggable: "true", title: tc("Labor_Drag"), "data-labor-grip": String(i) },
});
g.addEventListener("click", (e) => e.stopPropagation());
g.addEventListener("dragstart", (ev) => {
dragFrom = i;
ev.dataTransfer?.setData("text/plain", String(i));
if (ev.dataTransfer) ev.dataTransfer.effectAllowed = "move";
});
g.addEventListener("dragend", () => {
dragFrom = null;
});
return g;
};
const sel = data.selected === id ? " is-selected" : "";
const tr = el("tr", {
className: (on ? "m02-labrow__row is-open" : "m02-labrow__row") + sel,
@@ -373,10 +403,14 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
children: [
...(move
? [
group([
small(tc("Up"), () => move(i, -1)),
small(tc("Down"), () => move(i, 1)),
]),
el("div", {
className: "m02-labrow__order",
children: [
grip(),
arrow("▲", tc("Up"), i - 1),
arrow("▼", tc("Down"), i + 1),
],
}),
]
: []),
group([
@@ -403,6 +437,21 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
],
});
if (on) tr.addEventListener("click", () => on.open(i));
if (move) {
tr.addEventListener("dragover", (ev) => {
if (dragFrom === null || dragFrom === i) return;
ev.preventDefault();
tr.classList.add("is-dragover");
});
tr.addEventListener("dragleave", () => tr.classList.remove("is-dragover"));
tr.addEventListener("drop", (ev) => {
ev.preventDefault();
tr.classList.remove("is-dragover");
const from = dragFrom;
dragFrom = null;
if (from !== null && from !== i) move(from, i);
});
}
const subs = sel ? subRows(line, wait, tools) : [];
if (!note) return [tr, ...subs];
// 옵션 값 줄(44-6) — 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글은 title) · 행 높이 안 커짐
@@ -33,6 +33,7 @@ const TEXT = {
Col_Move: ["순서", "Move"],
Up: ["위로", "Up"],
Down: ["아래로", "Down"],
Labor_Drag: ["끌어 순서 바꿈", "Drag to reorder"],
Remove: ["삭제", "Delete"],
Labor_Option: ["옵션", "Options"],
Labor_Edit: ["수정", "Edit"],
+5 -2
View File
@@ -78,7 +78,7 @@ def test_일위대가_카드는_상세_카드_갈래():
assert "onChanged: (dirty) => laborCard.saver.mark(dirty && opts.editable())," in cards
assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로
assert "move: reorder," in detail # 85-3 — 카드도 순서(▲▼ · 끌기) · 빼기는 그대로
# 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음
# 51-11 사전조합 = 일위대가 상세와 같은 [로직 추가] · 같은 목록(걸러냄 없음)
assert "opt.card || promote ? null : addLogic" in detail
@@ -134,7 +134,10 @@ def test_일위대가_줄_고름과_옵션_단추():
) # 읽기만도 고름 · [옵션] = 보기 모달
assert "selected: picked" in detail
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
up, down = rows.index('small(tc("Up")'), rows.index('small(tc("Down")')
# 85-3 — ▲▼ 작은 단추(글은 title · aria) + 손잡이 끌기
up, down = rows.index('arrow("▲", tc("Up")'), rows.index('arrow("▼", tc("Down")')
assert rows.index("grip(),") < up and 'b.setAttribute("aria-label", label)' in rows
assert "move: reorder," in detail and "opt.card ? {} : { move }" not in detail # 카드도 순서
opt, rm = (
rows.index('small(tc("Labor_Option")'),
rows.index('small(tc("Remove"), () => remove(i), "danger")'),