feat(b09): 호표별 보기 → 박스보기 · 접기 없앰 · 목록표에서 고른 호표 붉은 외곽선(박스 · 표 줄 구간 전체)(181-2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -48,3 +48,28 @@
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-hand td:first-child {
|
||||
background-image: linear-gradient(to right, var(--color-primary) 3px, transparent 3px);
|
||||
}
|
||||
|
||||
/* 181-2 표 보기 — 목록표에서 고른 호표 줄 구간 전체 붉은 외곽선(머리 accent 밑줄은 덮음) */
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-pick td {
|
||||
--pick-t: inset 0 0 0 0 transparent;
|
||||
--pick-b: inset 0 0 0 0 transparent;
|
||||
--pick-l: inset 0 0 0 0 transparent;
|
||||
--pick-r: inset 0 0 0 0 transparent;
|
||||
box-shadow: var(--pick-t), var(--pick-b), var(--pick-l), var(--pick-r);
|
||||
}
|
||||
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-pick.is-pick-top td {
|
||||
--pick-t: inset 0 2px 0 var(--color-danger);
|
||||
}
|
||||
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-pick.is-pick-end td {
|
||||
--pick-b: inset 0 -2px 0 var(--color-danger);
|
||||
}
|
||||
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-pick td:first-child {
|
||||
--pick-l: inset 2px 0 0 var(--color-danger);
|
||||
}
|
||||
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-pick td:last-child {
|
||||
--pick-r: inset -2px 0 0 var(--color-danger);
|
||||
}
|
||||
|
||||
@@ -192,7 +192,8 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
const goto = (link: Link | null): void => {
|
||||
if (link) show(link.tab, link.key);
|
||||
};
|
||||
const makeTable = (): DataTable => {
|
||||
// extra = 줄 class 곁값(181-2 표 보기 호표 구간)
|
||||
const makeTable = (extra?: (index: number) => string | null): DataTable => {
|
||||
const made = createDataTable({
|
||||
columns: spec.columns,
|
||||
rows: spec.rows,
|
||||
@@ -202,7 +203,9 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
caption: TABS.find(([id]) => id === tab)?.[1],
|
||||
editable: spec.editable,
|
||||
onEdit: spec.onEdit,
|
||||
rowClass: spec.rowClass,
|
||||
rowClass: extra
|
||||
? (i) => [spec.rowClass?.(i), extra(i)].filter(Boolean).join(" ") || null
|
||||
: spec.rowClass,
|
||||
onCellClick: (i, key) => {
|
||||
if (spec.onCell) spec.onCell(i, key);
|
||||
if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Tabs_Groups.ts
|
||||
* B09 묶음 탭 셋(PLAN 171-3) — 일위대가표 · 중기사용료 · 단가산출근거 = 공용 묶음 카드(171-2) 기본 ·
|
||||
* [표로 보기] = 공용 표 그대로.
|
||||
* [표로 보기] = 공용 표 그대로(맞은편 단추 = [박스보기]).
|
||||
*
|
||||
* 카드 = 탭 표 줄(`TabTable.rows`)을 호표(줄 키 앞머리)마다 묶음 — 값 · 풍선 · 누름 이동은 표와 같음
|
||||
* 머리 줄 = 묶음 첫 줄 · 합계 줄 = 키 끝 `|합계` · `|총계` · 그 밖 = 몸 줄(표 줄 번호 그대로)
|
||||
* 목록표에서 온 호표(181-2) = 박스 붉은 외곽선 · 표 보기 = 그 호표 줄 구간 전체 붉은 외곽선
|
||||
* 보기 · 가로 열 수 기억 = `b_page_state`(`b09-group-view` · `b09-group-cols` · 탭마다) · 못 읽으면 카드 · 2
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -29,8 +30,7 @@ export const GROUP_TABS: TabId[] = ["dg", "machine_blocks", "sg"];
|
||||
export const isGroupTab = (tab: TabId): boolean => GROUP_TABS.includes(tab);
|
||||
|
||||
/** 단추 글 = 바꿀 보기 이름 */
|
||||
export const viewLabel = (view: GroupView): string =>
|
||||
view === "cards" ? "표로 보기" : "호표별 보기";
|
||||
export const viewLabel = (view: GroupView): string => (view === "cards" ? "표로 보기" : "박스보기");
|
||||
|
||||
export const flipView = (view: GroupView): GroupView => (view === "cards" ? "table" : "cards");
|
||||
|
||||
@@ -146,6 +146,22 @@ export function tabGroups(tab: TabId, spec: TabTable): GroupCard[] {
|
||||
return cards;
|
||||
}
|
||||
|
||||
/** 고른 호표의 표 줄 구간 [첫 줄, 끝 줄](키 `|` 앞머리가 같은 줄) · 없으면 null */
|
||||
export function pickRange(rows: DataRow[], focus: string | undefined): [number, number] | null {
|
||||
if (focus === undefined) return null;
|
||||
const head = focus.split("|")[0];
|
||||
const at = rows.flatMap((r, i) => (str(r.key).split("|")[0] === head ? [i] : []));
|
||||
return at.length ? [at[0], at[at.length - 1]] : null;
|
||||
}
|
||||
|
||||
/** 표 줄 class — 구간 안 `is-pick` · 첫 줄 `is-pick-top` · 끝 줄 `is-pick-end` */
|
||||
export function pickClass(range: [number, number] | null, i: number): string | null {
|
||||
if (!range || i < range[0] || i > range[1]) return null;
|
||||
return ["is-pick", i === range[0] && "is-pick-top", i === range[1] && "is-pick-end"]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/** 표 열과 같은 숫자 꼴(열 소수 자리) */
|
||||
function numberFormat(spec: TabTable, key: string): (v: number | null | undefined) => string {
|
||||
const c = spec.columns.find((col) => col.key === key);
|
||||
@@ -168,7 +184,7 @@ export function renderGroupPanel(o: {
|
||||
hint: HTMLElement | null;
|
||||
focus?: string;
|
||||
goto: (link: Link | null) => void;
|
||||
makeTable: () => DataTable;
|
||||
makeTable: (rowClass?: (index: number) => string | null) => DataTable;
|
||||
redraw: () => void;
|
||||
}): HTMLElement[] {
|
||||
const view = chosen.get(o.tab) ?? readGroupView(o.tab);
|
||||
@@ -201,7 +217,8 @@ export function renderGroupPanel(o: {
|
||||
cards.dataset.b09Table = o.tab;
|
||||
return [bar, cards];
|
||||
}
|
||||
const table = o.makeTable();
|
||||
const range = pickRange(o.spec.rows, o.focus);
|
||||
const table = o.makeTable((i) => pickClass(range, i));
|
||||
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
|
||||
return [bar, table.root];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""157-2 B09 단가산출근거 탭 보기 둘 · 157-3 산근 규격 칸 「이름 규격」.
|
||||
|
||||
- 157-2 보기 B(호표별 카드) = 보기 A(표) 줄 그대로 호표마다 묶음 — 같은 값 · 같은 차례(171-3 공용 묶음 카드로 갈음)
|
||||
- 단추 = 보기 바꿈(글 「호표별 보기」 ↔ 「표로 보기」) · 기억 못 읽음 · 모르는 값 = 카드(171-3 컨테이너 기본)
|
||||
- 단추 = 보기 바꿈(글 「박스보기」 ↔ 「표로 보기」 · 181-2) · 기억 못 읽음 · 모르는 값 = 카드(171-3 컨테이너 기본)
|
||||
- 157-3 규격 = 기계 · 직종 원문번호 → 「이름 규격」(M02 85-1 도우미) · 번호는 풍선(tip)
|
||||
"""
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_157_2_단추_바꿈():
|
||||
"\nconsole.log(JSON.stringify([viewLabel('table'), viewLabel('cards'),"
|
||||
" flipView('table'), flipView(flipView('table'))]));"
|
||||
)
|
||||
assert out == ["호표별 보기", "표로 보기", "cards", "table"]
|
||||
assert out == ["박스보기", "표로 보기", "cards", "table"]
|
||||
|
||||
|
||||
def test_157_2_기억_실패_때_카드():
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- 머리 = 호표 · 명칭 · 규격 · 단위 · 합계 / 몸 = 이름 · 규격 · 수량 · 단위 · 합계 · 비고 / 합계 줄 = 노무 · 재료 · 경비 · 계
|
||||
- 줄 금액 = 합계만 · 비목 셋 = 풍선 · 숫자 오른쪽 · 글자 14px 한 벌 · 줄 간격 좁게(공용 카드 16px 끼움 없음)
|
||||
- 가로 열 수 1~4(기본 2 · 고르면 알림) · 묶음 12 넘으면 요약 보기(머리 · 합계만 · 머리 누름 = 펼침 · 모두 펼침 · 접기)
|
||||
- 가로 열 수 1~4(기본 2 · 고르면 알림) · 접기 없음(181-2) · 이동해 온 박스 = `is-pick`(붉은 외곽선) · 머리 누름 = 이동
|
||||
- 157-2 산근 카드 파일 = 이 양식으로 갈음(지움)
|
||||
"""
|
||||
|
||||
@@ -43,11 +43,9 @@ def test_도우미_열수_요약_풍선():
|
||||
out = _run(
|
||||
"\nconsole.log(JSON.stringify({"
|
||||
" cols: [0, 1, 2, 3, 4, 5, '3', null, undefined].map(clampColumns),"
|
||||
" summary: [12, 13].map(isSummary),"
|
||||
" tip: [partsTip([1000, 0, null]), partsTip([null, null, null]), partsTip(null)] }));"
|
||||
)
|
||||
assert out["cols"] == [2, 1, 2, 3, 4, 2, 2, 2, 2]
|
||||
assert out["summary"] == [False, True]
|
||||
assert out["tip"] == ["노무 1,000 · 재료 0 · 경비 -", None, None]
|
||||
|
||||
|
||||
@@ -91,7 +89,7 @@ def test_카드_머리_몸_합계_줄():
|
||||
assert out["foot"] == ["노무 100", "재료 20", "경비 3", "계 123", "빈 줄 1"]
|
||||
assert out["link"] == [True, False]
|
||||
assert out["cols"] == "2" # 기본 2
|
||||
assert out["summary"] == ["1", "2", "3", "4"] # 12 이하 = 요약 단추 없음
|
||||
assert out["summary"] == ["1", "2", "3", "4"] # 가로 열 수 단추뿐(요약 · 접기 단추 없음)
|
||||
|
||||
|
||||
def test_가로_열수_고름_알림_누름_이동():
|
||||
@@ -111,29 +109,35 @@ def test_가로_열수_고름_알림_누름_이동():
|
||||
assert out["picks"] == [21, 20] # 줄 · 머리 누름 = 표 줄 번호 그대로(갈 곳은 부르는 쪽 link)
|
||||
|
||||
|
||||
def test_묶음_12_넘으면_요약_보기():
|
||||
def test_묶음_많아도_접힘_없음_고른_박스_붉은_외곽선():
|
||||
out = _run(
|
||||
"\nconst root = make(13, { focus: 52 });"
|
||||
"\nconst cards = root.all('.ui-group-card');"
|
||||
"\nconst closed = () => cards.map((c) => c.classList.contains('is-closed'));"
|
||||
"\nconst first = closed();"
|
||||
"\nconst focused = root.querySelector('[data-at=\"52\"]');"
|
||||
"\nconst first = cards.map((c) => c.classList.contains('is-pick'));"
|
||||
"\ncards[0].querySelector('.ui-group-card__head').click();"
|
||||
"\nconst opened = closed()[0];"
|
||||
"\nconst btn = (label) => root.all('.ui-btn').find((b) => b.textContent === label);"
|
||||
"\nbtn('모두 펼침').click(); const all = closed();"
|
||||
"\nbtn('모두 접기').click(); const none = closed();"
|
||||
"\nconsole.log(JSON.stringify({ first, opened, all, none, picks,"
|
||||
"\nconsole.log(JSON.stringify({ first, picks,"
|
||||
" closed: cards.map((c) => c.classList.contains('is-closed')),"
|
||||
" buttons: root.all('.ui-btn').map((b) => b.textContent),"
|
||||
" foot: cards.every((c) => c.querySelector('.ui-group-card__foot')),"
|
||||
" focus: [focused.classList.contains('is-focus'), !!focused.scrolled] }));"
|
||||
" focus: [focused.classList.contains('is-focus'), cards[4].scrolled === true] }));"
|
||||
)
|
||||
# 이동해 온 줄(5번째 묶음 줄) = 그 묶음만 펼침 · 강조 · 스크롤
|
||||
assert out["first"] == [True] * 4 + [False] + [True] * 8
|
||||
# 이동해 온 줄(5번째 묶음 줄) = 그 박스만 붉은 외곽선 · 그 줄 강조 · 박스 스크롤
|
||||
assert out["first"] == [False] * 4 + [True] + [False] * 8
|
||||
assert out["focus"] == [True, True]
|
||||
assert out["opened"] is False # 머리 누름 = 펼침
|
||||
assert out["picks"] == [] # 요약 보기 머리 누름은 이동 아님
|
||||
assert out["all"] == [False] * 13 and out["none"] == [True] * 13
|
||||
assert out["foot"] is True # 접어도 합계 줄은 보임(몸만 숨김)
|
||||
assert out["closed"] == [False] * 13 # 13 묶음도 접힘 없음
|
||||
assert out["picks"] == [10] # 머리 누름 = 이동(표 줄 번호 그대로)
|
||||
assert out["buttons"] == ["1", "2", "3", "4"] # 모두 펼침 · 접기 단추 없음
|
||||
assert out["foot"] is True
|
||||
|
||||
|
||||
def test_고른_박스_없으면_외곽선_없음():
|
||||
out = _run(
|
||||
"\nconst root = make(3);"
|
||||
"\nconsole.log(JSON.stringify(root.all('.ui-group-card').map("
|
||||
"(c) => c.classList.contains('is-pick'))));"
|
||||
)
|
||||
assert out == [False, False, False]
|
||||
|
||||
|
||||
def test_양식_글자_간격_숫자():
|
||||
@@ -146,6 +150,8 @@ def test_양식_글자_간격_숫자():
|
||||
"font-size: inherit;",
|
||||
]
|
||||
assert "padding: 1px var(--spacing-4);" in css # 줄 간격 좁게
|
||||
assert "is-closed" not in css and "is-closed" not in (ROOT / COMMON).read_text("utf-8")
|
||||
assert "border-color: var(--color-danger);" in css # 고른 박스 = B06 고름 꼴(181-2)
|
||||
assert "spacing-16" not in css and "card-padding" not in css # 공용 카드 16px 끼움 없음
|
||||
assert "grid-template-columns: repeat(var(--group-cols, 2), minmax(0, 1fr));" in css
|
||||
assert "text-align: right;" in css
|
||||
|
||||
@@ -151,9 +151,10 @@ def test_화면_보기_바꿈_누름_이동():
|
||||
"\nview.querySelector('[data-at=\"2\"]').click();"
|
||||
"\nview.querySelector('[data-at=\"0\"]').click();"
|
||||
"\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');"
|
||||
"\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');"
|
||||
"\nbar.children[0].click();"
|
||||
"\nconst [bar2, view2] = renderGroupPanel(o);"
|
||||
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, redraws,"
|
||||
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws,"
|
||||
" label2: bar2.children[0].textContent, tag2: view2.tagName, focused,"
|
||||
" kept: Object.fromEntries(kept) }));",
|
||||
RAW,
|
||||
@@ -161,8 +162,9 @@ def test_화면_보기_바꿈_누름_이동():
|
||||
assert out["label"] == "표로 보기" and out["table"] == "dg" # 기본 = 카드
|
||||
assert out["gone"] == [{"tab": "labor", "key": "L00005"}, None] # 줄 누름 = 표와 같은 이동
|
||||
assert out["lit"] is True # 이동해 온 줄(키 DG1|1 = 표 3번째 줄) 강조
|
||||
assert out["box"] is True # 그 호표 박스 = 붉은 외곽선(181-2)
|
||||
assert out["redraws"] == 1
|
||||
assert out["label2"] == "호표별 보기" and out["tag2"] == "TABLE" and out["focused"] == [3]
|
||||
assert out["label2"] == "박스보기" and out["tag2"] == "TABLE" and out["focused"] == [3]
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "table"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""181-2 박스보기 — 이름 · 접기 없음 · 목록표에서 고른 호표 = 박스 붉은 외곽선 · 표 보기 = 줄 구간 전체.
|
||||
|
||||
- 단추 글 「박스보기」 ↔ 「표로 보기」 · 요약(접기) 보기 지움
|
||||
- 표 보기 = 고른 호표 줄 구간(키 `|` 앞머리 같은 줄) 모두 `is-pick` · 첫 줄 `is-pick-top` · 끝 줄 `is-pick-end`
|
||||
- 붉은 색 = `--color-danger`(B06 횡단 카드 고름 꼴)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from resources.tester.helper_b09_group_cards import COMMON, GROUPS, TABS, run # noqa: E402
|
||||
|
||||
ROWS = [ # 호표 A(3줄) · 호표 B(4줄) · 호표 C(1줄)
|
||||
{"key": "A"}, {"key": "A|1"}, {"key": "A|합계"},
|
||||
{"key": "B"}, {"key": "B|1"}, {"key": "B|2"}, {"key": "B|총계"},
|
||||
{"key": "C"},
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def test_구간_첫_끝_줄():
|
||||
out = run(
|
||||
[COMMON, TABS, GROUPS],
|
||||
f"\nconst rows = {json.dumps(ROWS)};"
|
||||
"\nconst show = (focus) => { const r = pickRange(rows, focus);"
|
||||
" return [r, rows.map((_, i) => pickClass(r, i))]; };"
|
||||
"\nconsole.log(JSON.stringify({ a: show('A|1'), b: show('B'), c: show('C'),"
|
||||
" none: show(undefined), unknown: show('Z|1') }));",
|
||||
)
|
||||
assert out["a"][0] == [0, 2]
|
||||
assert out["a"][1] == [
|
||||
"is-pick is-pick-top", "is-pick", "is-pick is-pick-end", None, None, None, None, None,
|
||||
] # fmt: skip
|
||||
assert out["b"][0] == [3, 6]
|
||||
assert out["b"][1][3:7] == [
|
||||
"is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end",
|
||||
] # fmt: skip
|
||||
assert out["b"][1][:3] == [None] * 3 # 다른 호표 없음
|
||||
assert out["c"][0] == [7, 7] and out["c"][1][7] == "is-pick is-pick-top is-pick-end"
|
||||
assert out["none"] == [None, [None] * 8] and out["unknown"] == [None, [None] * 8]
|
||||
|
||||
|
||||
def test_표_보기_줄_class_넘김():
|
||||
out = run(
|
||||
[COMMON, TABS, GROUPS],
|
||||
f"\nconst spec = {{ columns: [{{ key: 'key' }}], rows: {json.dumps(ROWS)} }};"
|
||||
"\nlet extra = null;"
|
||||
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {},"
|
||||
" makeTable: (e) => { extra = e; return { root: document.createElement('table'),"
|
||||
" focusRow: () => {} }; } };"
|
||||
"\nchosen.set('dg', 'table');"
|
||||
"\nrenderGroupPanel(o);"
|
||||
"\nconsole.log(JSON.stringify(spec.rows.map((_, i) => extra(i))));",
|
||||
)
|
||||
assert out == (
|
||||
[None] * 3 + ["is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end"] + [None]
|
||||
)
|
||||
|
||||
|
||||
def test_글_접기_없음_CSS_잇기():
|
||||
groups = (ROOT / GROUPS).read_text("utf-8")
|
||||
assert '"표로 보기" : "박스보기"' in groups and "호표별 보기" not in groups
|
||||
common = (ROOT / COMMON).read_text("utf-8")
|
||||
for gone in ("isSummary", "GROUP_SUMMARY_OVER", "is-closed", "모두 펼침", "모두 접기"):
|
||||
assert gone not in common
|
||||
assert 'box?.classList.add("is-pick")' in common
|
||||
page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
|
||||
for sel in ("tr.is-pick td", "tr.is-pick.is-pick-top td", "tr.is-pick.is-pick-end td"):
|
||||
assert sel in page
|
||||
assert page.count("var(--color-danger)") == 4 # 위 · 아래 · 왼 · 오른 2px
|
||||
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert "extra?: (index: number) => string | null" in shell
|
||||
@@ -47,6 +47,12 @@
|
||||
border-radius: var(--radius-cards);
|
||||
}
|
||||
|
||||
/* 목록표에서 고른 호표 — B06 횡단 카드 고름과 같은 붉은 외곽선(181-2) */
|
||||
.ui-group-card.is-pick {
|
||||
border-color: var(--color-danger);
|
||||
box-shadow: 0 0 0 2px var(--color-danger);
|
||||
}
|
||||
|
||||
.ui-group-card__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -76,14 +82,6 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ui-group-card.is-closed .ui-group-card__body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-group-card.is-closed .ui-group-card__head {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-group-card__lines {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* 머리 = 호표 · 명칭 · 규격 · 단위 · 합계 / 몸 = 줄(이름 · 규격 · 수량 · 단위 · 합계 · 비고) /
|
||||
* 합계 줄 = 노무 · 재료 · 경비 · 계 · 줄 금액 = 합계만(비목 셋 = 풍선)
|
||||
* 글자 14px 한 벌 · 줄 간격 좁게 · 숫자 오른쪽 · 가로 열 수 1~4(기억은 부르는 쪽)
|
||||
* 묶음 12 넘으면 요약 보기(머리 · 합계만 · 머리 누름 = 펼침 · 모두 펼침 · 접기)
|
||||
* 줄 번호(`at`) = 부르는 쪽 표 줄 번호 — 누름 = `onPick(at)` · 이동해 온 줄 = 펼침 + 스크롤 + 강조
|
||||
* 접기 없음 — 묶음 모두 펼친 박스
|
||||
* 줄 번호(`at`) = 부르는 쪽 표 줄 번호 — 누름 = `onPick(at)` · 이동해 온 줄 = 그 박스 붉은 외곽선
|
||||
* (`is-pick`) + 스크롤 + 그 줄 강조(`is-focus`)
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_group_cards.css";
|
||||
@@ -68,7 +69,6 @@ export interface GroupCardsOptions {
|
||||
actions?: HTMLElement[];
|
||||
}
|
||||
|
||||
export const GROUP_SUMMARY_OVER = 12;
|
||||
export const GROUP_COLUMNS = [1, 2, 3, 4] as const;
|
||||
export const PART_LABELS = ["노무", "재료", "경비"] as const;
|
||||
|
||||
@@ -76,9 +76,6 @@ export const PART_LABELS = ["노무", "재료", "경비"] as const;
|
||||
export const clampColumns = (value: unknown): number =>
|
||||
GROUP_COLUMNS.includes(value as 1) ? (value as number) : 2;
|
||||
|
||||
/** 요약 보기 — 묶음 12 넘음 */
|
||||
export const isSummary = (count: number): boolean => count > GROUP_SUMMARY_OVER;
|
||||
|
||||
const won = new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 1 });
|
||||
const plain = (value: number | null | undefined): string =>
|
||||
value === null || value === undefined || !Number.isFinite(value) ? "" : won.format(value);
|
||||
@@ -173,7 +170,7 @@ function card(g: GroupCard, o: Required<Pick<GroupCardsOptions, "money" | "qty">
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 묶음 카드 판 — 머리 띠(가로 열 수 · 요약 단추 · 부르는 쪽 단추) + 카드 격자 */
|
||||
/** 묶음 카드 판 — 머리 띠(가로 열 수 · 부르는 쪽 단추) + 카드 격자 */
|
||||
export function createGroupCards(opts: GroupCardsOptions): HTMLElement {
|
||||
const fmt = { money: opts.money ?? plain, qty: opts.qty ?? plain };
|
||||
const root = document.createElement("div");
|
||||
@@ -184,9 +181,6 @@ export function createGroupCards(opts: GroupCardsOptions): HTMLElement {
|
||||
grid.style.setProperty("--group-cols", String(columns));
|
||||
const cards = opts.groups.map((g) => card(g, fmt));
|
||||
grid.append(...cards);
|
||||
const summary = isSummary(opts.groups.length);
|
||||
const setOpen = (box: Element, open: boolean) => box.classList.toggle("is-closed", !open);
|
||||
if (summary) cards.forEach((c) => setOpen(c, false));
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "ui-group-cards__bar";
|
||||
@@ -215,14 +209,6 @@ export function createGroupCards(opts: GroupCardsOptions): HTMLElement {
|
||||
});
|
||||
pick.append(...colButtons);
|
||||
bar.append(pick, span("ui-group-cards__count", `묶음 ${opts.groups.length}`));
|
||||
if (summary) {
|
||||
const all = (open: boolean) => () => cards.forEach((c) => setOpen(c, open));
|
||||
bar.append(
|
||||
span("ui-group-cards__count", "요약 보기 — 머리를 누르면 펼침"),
|
||||
createButton({ label: "모두 펼침", variant: "ghost", size: "sm", onClick: all(true) }),
|
||||
createButton({ label: "모두 접기", variant: "ghost", size: "sm", onClick: all(false) }),
|
||||
);
|
||||
}
|
||||
const tail = document.createElement("div");
|
||||
tail.className = "ui-group-cards__actions";
|
||||
tail.append(...(opts.actions ?? []));
|
||||
@@ -232,18 +218,15 @@ export function createGroupCards(opts: GroupCardsOptions): HTMLElement {
|
||||
grid.addEventListener("click", (event) => {
|
||||
const el = (event.target as Element).closest<HTMLElement>("[data-at]");
|
||||
if (!el || !grid.contains(el)) return;
|
||||
const box = el.closest(".ui-group-card");
|
||||
if (summary && box && el.classList.contains("ui-group-card__head"))
|
||||
return setOpen(box, box.classList.contains("is-closed"));
|
||||
opts.onPick?.(Number(el.dataset.at));
|
||||
});
|
||||
|
||||
if (opts.focus !== undefined && opts.focus >= 0) {
|
||||
const el = grid.querySelector<HTMLElement>(`[data-at="${opts.focus}"]`);
|
||||
const box = el?.closest(".ui-group-card");
|
||||
if (box) setOpen(box, true);
|
||||
box?.classList.add("is-pick");
|
||||
el?.classList.add("is-focus");
|
||||
requestAnimationFrame(() => el?.scrollIntoView({ block: "center" }));
|
||||
requestAnimationFrame(() => (box ?? el)?.scrollIntoView({ block: "center" }));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user