- 나눈 조각으로 면 찾기(열린 영역 제외) · 영역 안 누름 = 고름 · 채움 강조 · 면적 · 둘레 - 선 고름 경사비율 · 닫힌 선 둘레 · 띠 고름 값 글 - 표 칸 누름 = 저장된 조각 그래프 고름 · 띠 강조 + 값 글 - 고른 칸 묶음 바깥 한 줄(겹선 없음) · 줄 물들임 없앰 · 합친 묶음 이름 칸 키 맞춤 - 259-2 · 259-3 이 같은 파일(상세 표 배선)이라 한 커밋 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
477 lines
20 KiB
TypeScript
477 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Detail_Table.ts
|
|
* [상세] 수량표 손질(179-4) — 칸 우클릭 메뉴 · 그래프에서 고르기 · 행 · 열 더함 / 지움 · 세션 초안.
|
|
*
|
|
* 칸 출처 = 횡단 설계 patch `detail_table`(세션 초안 `detail-table` → [저장] patch → 서버).
|
|
* 값은 서버(`B07_DesignDetail_Engine_Cross_Table_Edit`)가 그릴 때마다 다시 셈 — 모달 · B07 도면 표 한 벌.
|
|
* 조작 직후 그 칸 글만 `localCellValue` 로 즉시 — 짝(① 파이썬 · TS) · 거울 시험
|
|
* `resources/tester/test_179_4_detail_table.py`.
|
|
* 고르기: 면적 = 면적 띠 · 사토장(띠 → 표 키) 또는 닫힌 선 조각들 · 길이 = 선 조각(들) —
|
|
* 조각 = 선이 만나는 점에서 나눈 것(209-1 · `_Cross_Detail_Select`) · 저장 = 조각 두 끝 목록.
|
|
* 길이는 한 번 누름 = 바로 칸에 · Shift/Ctrl+누름 = 여럿 → Enter · [칸에 넣기].
|
|
* 닫힌 영역 안 누름 = 그 영역 둘레 조각(259-2) · 면적 고르기 중 띠 자리는 띠가 앞.
|
|
* 표 칸 누름(259-2) = 저장된 조각을 그림에서 다시 고름 · 띠 칸 = 값 글(`cellGraph` · `bandLabel`).
|
|
* ========================================================================== */
|
|
|
|
import { readState, writeState } from "../A00_Common/b_page_state";
|
|
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
|
import type {
|
|
CrossCellSource,
|
|
CrossDesign,
|
|
CrossDetailTable,
|
|
CrossSection,
|
|
} from "./B06_Section_Api_Fetch";
|
|
import type { CrossPickPiece, SectionSample } from "./B06_Section_Api_Types";
|
|
import type { CrossAxesOptions } from "./B06_Section_UI_Cross_Axes";
|
|
import { pickLines, piecesValue } from "./B06_Section_UI_Cross_Detail_Geom";
|
|
import { attachDetailSelect, PIECE_HIT_CLASS } from "./B06_Section_UI_Cross_Detail_Select";
|
|
|
|
/** 서버 `MAX_ROWS` · `MAX_COLUMNS` · `LINE_TOLERANCE_M` 와 같은 값. */
|
|
export const MAX_ROWS = 12;
|
|
export const MAX_COLUMNS = 4;
|
|
export const LINE_TOLERANCE_M = 0.05;
|
|
|
|
/* ── 세션 초안(측점키 0.01 m → 표 손질) ─────────────────────────────────── */
|
|
|
|
type Drafts = Record<string, CrossDetailTable>;
|
|
const tableKey = (chainageM: number): string => chainageM.toFixed(2);
|
|
|
|
/** [저장]이 patch 로 싣는 표 손질 — 측점(m) → 표 손질. */
|
|
export function readDetailTables(
|
|
projectId: string,
|
|
routeId: number,
|
|
): Map<number, CrossDetailTable> {
|
|
const raw = readState<Drafts>("detail-table", projectId, routeId) ?? {};
|
|
return new Map(Object.entries(raw).map(([key, table]) => [Number(key), table]));
|
|
}
|
|
|
|
/** 그 측점 표 손질 — 세션 초안 → 캐시 설계 → 빈 것(복사본). */
|
|
export function detailTableOf(
|
|
section: CrossSection,
|
|
projectId?: string,
|
|
routeId?: number | null,
|
|
): CrossDetailTable {
|
|
const draft =
|
|
projectId && routeId !== null && routeId !== undefined
|
|
? readState<Drafts>("detail-table", projectId, routeId)?.[tableKey(section.chainage_m)]
|
|
: undefined;
|
|
const table = draft ?? section.design?.detail_table;
|
|
return table ? structuredClone(table) : { cells: {}, rows: [], columns: [] };
|
|
}
|
|
|
|
/** 캐시 설계에도 얹음(모달 표 요청 · B05 [저장]이 같은 값을 실음) + 세션 초안. */
|
|
export function writeDetailTable(
|
|
section: CrossSection,
|
|
table: CrossDetailTable,
|
|
projectId?: string,
|
|
routeId?: number | null,
|
|
): void {
|
|
if (section.design) section.design.detail_table = table;
|
|
if (!projectId || routeId === null || routeId === undefined) return;
|
|
const all = readState<Drafts>("detail-table", projectId, routeId) ?? {};
|
|
all[tableKey(section.chainage_m)] = table;
|
|
writeState("detail-table", all, projectId, routeId);
|
|
}
|
|
|
|
/** 표 요청에 실을 설계 — 표 손질을 얹음(카드가 다시 그려져 설계 객체가 바뀌어도 초안 그대로). */
|
|
export function designForTable(
|
|
section: CrossSection,
|
|
projectId?: string,
|
|
routeId?: number | null,
|
|
): CrossDesign | undefined {
|
|
if (!section.design) return undefined;
|
|
return { ...section.design, detail_table: detailTableOf(section, projectId, routeId) };
|
|
}
|
|
|
|
/* ── 표 손질(순수) ──────────────────────────────────────────────────────── */
|
|
|
|
const nextId = (prefix: string, used: string[]): string => {
|
|
let n = 1;
|
|
while (used.includes(`${prefix}${n}`)) n += 1;
|
|
return `${prefix}${n}`;
|
|
};
|
|
|
|
export function setCell(table: CrossDetailTable, key: string, source: CrossCellSource | null) {
|
|
if (source) table.cells[key] = source;
|
|
else delete table.cells[key];
|
|
}
|
|
|
|
/** 행 더함 — `after` 줄 아래(null = 표 끝) · grouped = 그 줄 묶음 안. 새 id. */
|
|
export function addRow(table: CrossDetailTable, after: string | null, grouped: boolean): string {
|
|
const id = nextId(
|
|
"r",
|
|
table.rows.map((row) => row.id),
|
|
);
|
|
table.rows.push({ id, after, grouped, labels: ["", "", ""] });
|
|
return id;
|
|
}
|
|
|
|
export function addColumn(table: CrossDetailTable): string {
|
|
const id = nextId(
|
|
"c",
|
|
table.columns.map((column) => column.id),
|
|
);
|
|
table.columns.push({ id, label: "" });
|
|
return id;
|
|
}
|
|
|
|
/** 더한 행 지움 — 그 행 칸 · 열 칸도 지우고 그 아래 붙던 행은 지운 행 자리로. */
|
|
export function removeRow(table: CrossDetailTable, rowId: string): void {
|
|
const removed = table.rows.find((row) => row.id === rowId);
|
|
if (!removed) return;
|
|
table.rows = table.rows
|
|
.filter((row) => row.id !== rowId)
|
|
.map((row) => (row.after === rowId ? { ...row, after: removed.after } : row));
|
|
for (const key of Object.keys(table.cells))
|
|
if (key.startsWith(`r:${rowId}:`) || (key.startsWith("c:") && key.endsWith(`:${rowId}`)))
|
|
delete table.cells[key];
|
|
}
|
|
|
|
export function removeColumn(table: CrossDetailTable, columnId: string): void {
|
|
table.columns = table.columns.filter((column) => column.id !== columnId);
|
|
for (const key of Object.keys(table.cells))
|
|
if (key.startsWith(`c:${columnId}:`)) delete table.cells[key];
|
|
}
|
|
|
|
export function setRowLabel(table: CrossDetailTable, rowId: string, slot: number, text: string) {
|
|
const row = table.rows.find((entry) => entry.id === rowId);
|
|
if (!row) return;
|
|
const labels = [0, 1, 2].map((index) => row.labels[index] ?? "");
|
|
labels[slot] = text;
|
|
row.labels = labels;
|
|
}
|
|
|
|
export function setColumnLabel(table: CrossDetailTable, columnId: string, text: string) {
|
|
const column = table.columns.find((entry) => entry.id === columnId);
|
|
if (column) column.label = text;
|
|
}
|
|
|
|
/** 묶음 이름 칸이 덮는 줄 수 — 서버 `group_span` 과 같은 규칙. */
|
|
export function groupSpan(slots: ReadonlyArray<ReadonlyArray<string | null>>, index: number) {
|
|
let span = 1;
|
|
while (
|
|
index + span < slots.length &&
|
|
slots[index + span][0] === null &&
|
|
slots[index + span][1] !== null
|
|
)
|
|
span += 1;
|
|
return span;
|
|
}
|
|
|
|
/* ── 그래프 고르기 → 출처 ───────────────────────────────────────────────── */
|
|
|
|
/** 띠 · 사토장 클래스 → 표 키(서버 `AREA_KEYS` 의 표 키). */
|
|
const AREA_BANDS: ReadonlyArray<[string, string]> = [
|
|
["b06-chart__area--cut_soil", "cut_soil"],
|
|
["b06-chart__area--cut_rock", "cut_rock"],
|
|
["b06-chart__area--fill", "embankment"],
|
|
["b06-chart__spoil-fill", "spoil_fill"],
|
|
];
|
|
export const AREA_PICK_SELECTOR = ".b06-chart__area, .b06-chart__spoil-fill";
|
|
|
|
export function areaSourceOf(target: Element): CrossCellSource | null {
|
|
const hit = target.closest(AREA_PICK_SELECTOR);
|
|
const band = hit && AREA_BANDS.find(([name]) => hit.classList.contains(name));
|
|
return band ? { src: "area", key: band[1] } : null;
|
|
}
|
|
|
|
/** 그 칸이 그래프에서 무엇을 가리키나(259-2) — 저장된 조각 · 띠 키(저장 띠 출처 → 칸 키) · 없음. */
|
|
export function cellGraph(
|
|
table: CrossDetailTable,
|
|
key: string,
|
|
): { pieces: CrossPickPiece[] } | { band: string } | null {
|
|
const source = table.cells[key];
|
|
if (source && "pieces" in source) return { pieces: source.pieces };
|
|
if (source && source.src === "area" && "key" in source) return { band: source.key };
|
|
return null;
|
|
}
|
|
|
|
const BAND_NAME: Readonly<Record<string, string>> = {
|
|
cut_soil: "깍기(토사)",
|
|
cut_rock: "깍기(암석)",
|
|
ditch_soil: "측구(토사)",
|
|
ditch_rock: "측구(암석)",
|
|
embankment: "쌓기",
|
|
spoil_fill: "사토장",
|
|
};
|
|
|
|
/** 띠 값 글(259-2) — 「깍기(토사) 면적 X㎡」 · 띠 아님 · 값 없음 = null. */
|
|
export function bandLabel(key: string, design: CrossDesign | undefined): string | null {
|
|
const value = BAND_NAME[key] ? localCellValue({ src: "area", key }, design) : null;
|
|
return value === null ? null : `${BAND_NAME[key]} 면적 ${value.toFixed(2)}㎡`;
|
|
}
|
|
|
|
/* ── 칸 값(짝 ① · 서버 `cell_value` 거울) ─────────────────────────────────── */
|
|
|
|
const AREA_DESIGN: Readonly<Record<string, string>> = {
|
|
cut_soil: "cut_soil_area_m2",
|
|
cut_rock: "cut_rock_area_m2",
|
|
embankment: "fill_area_m2",
|
|
ditch_soil: "ditch_soil_area_m2",
|
|
ditch_rock: "ditch_rock_area_m2",
|
|
spoil_fill: "spoil_fill_area_m2",
|
|
};
|
|
|
|
const finite = (value: unknown): number | null =>
|
|
typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
|
|
function onLine([px, py]: [number, number], line: ReadonlyArray<[number, number]>): boolean {
|
|
for (let i = 0; i + 1 < line.length; i++) {
|
|
const [ax, ay] = line[i];
|
|
const [bx, by] = line[i + 1];
|
|
const dx = bx - ax;
|
|
const dy = by - ay;
|
|
const length2 = dx * dx + dy * dy;
|
|
const t =
|
|
length2 === 0 ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / length2));
|
|
if (Math.hypot(px - (ax + t * dx), py - (ay + t * dy)) <= LINE_TOLERANCE_M) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 조작 직후 칸 숫자(서버가 [저장] · 다시 받을 때 정본으로 다시 셈) — 글 · 못 셈 = null.
|
|
* `samples` = 측점 원지반(조각 검산 · 209-1). */
|
|
export function localCellValue(
|
|
source: CrossCellSource,
|
|
design: CrossDesign | undefined,
|
|
samples?: ReadonlyArray<SectionSample>,
|
|
) {
|
|
const record = (design ?? {}) as unknown as Record<string, unknown>;
|
|
if ("pieces" in source) return piecesValue(source.src, source.pieces, pickLines(design, samples));
|
|
if (source.src === "area") return finite(record[AREA_DESIGN[source.key] ?? ""]);
|
|
if (source.src !== "length") return null;
|
|
const line = (design?.design_line ?? [])
|
|
.map((p): [number, number] => [p.offset_m, p.elevation_m])
|
|
.filter((p) => p.every(Number.isFinite));
|
|
const { start, end } = source;
|
|
if (!onLine(start, line) || !onLine(end, line)) return null;
|
|
return Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
}
|
|
|
|
/* ── 우클릭 메뉴 ───────────────────────────────────────────────────────── */
|
|
|
|
/** 우클릭한 칸 — 값 칸 키 · 본문 줄 id · 더한 열 id. */
|
|
export interface CellTarget {
|
|
key: string | null;
|
|
rowId: string | null;
|
|
columnId: string | null;
|
|
}
|
|
|
|
export interface CellActions {
|
|
pick: (kind: "area" | "length") => void;
|
|
auto: () => void;
|
|
addRow: (after: string | null, grouped: boolean) => void;
|
|
addColumn: () => void;
|
|
removeRow: (rowId: string) => void;
|
|
removeColumn: (columnId: string) => void;
|
|
}
|
|
|
|
export function cellMenuItems(
|
|
table: CrossDetailTable,
|
|
target: CellTarget,
|
|
act: CellActions,
|
|
): MapContextMenuItem[] {
|
|
const items: MapContextMenuItem[] = [];
|
|
const { key, rowId, columnId } = target;
|
|
if (key) {
|
|
items.push(
|
|
["그래프에서 고르기 — 면적", () => act.pick("area")],
|
|
["그래프에서 고르기 — 길이", () => act.pick("length")],
|
|
);
|
|
if (table.cells[key]) items.push(["자동값으로", act.auto]);
|
|
}
|
|
items.push("-");
|
|
if (table.rows.length < MAX_ROWS) {
|
|
if (rowId) items.push(["행 더함 — 이 행 아래(묶음 안)", () => act.addRow(rowId, true)]);
|
|
items.push(["행 더함 — 표 끝", () => act.addRow(null, false)]);
|
|
}
|
|
if (table.columns.length < MAX_COLUMNS) items.push(["열 더함 — 오른쪽 끝", act.addColumn]);
|
|
items.push("-");
|
|
if (rowId?.startsWith("r")) items.push(["이 행 지움", () => act.removeRow(rowId)]);
|
|
if (columnId) items.push(["이 열 지움", () => act.removeColumn(columnId)]);
|
|
return items;
|
|
}
|
|
|
|
/* ── 모달 배선 ─────────────────────────────────────────────────────────── */
|
|
|
|
export interface DetailTableHost {
|
|
tableBox: HTMLElement;
|
|
cad: HTMLElement;
|
|
section: () => CrossSection;
|
|
projectId: string | undefined;
|
|
routeId: () => number | null | undefined;
|
|
axes: () => CrossAxesOptions | null;
|
|
/** 표 손질이 바뀜 — 서버 표를 다시 받음. */
|
|
changed: () => void;
|
|
}
|
|
|
|
let menu: ReturnType<typeof createMapContextMenu> | null = null;
|
|
|
|
const PICK_HINT = {
|
|
area: "면적 띠를 누름 · 또는 닫힌 영역 안 · 선들을 누름 → Enter · Esc 취소",
|
|
length: "선을 누름 · Shift/Ctrl+누름 = 여럿 → Enter · Esc 취소",
|
|
} as const;
|
|
|
|
/** 표 우클릭 · 그래프 고르기 · 더한 칸 글 손질을 검. detach = 떼기(모달 닫힐 때) ·
|
|
* redraw = 그림을 새로 그린 뒤(선 조각 · 판정 선 다시 · 209-1). */
|
|
export function attachDetailTable(host: DetailTableHost): {
|
|
detach: () => void;
|
|
redraw: () => void;
|
|
/** 칸 그래프 짝(259-2) — 저장된 조각 · 띠 키 · 없음. */
|
|
graph: (key: string) => ReturnType<typeof cellGraph>;
|
|
/** 표 칸 누름(259-2) — 저장된 조각을 그림에서 고름 · 띠면 값 글 · null = 비움. */
|
|
showCell: (key: string | null) => void;
|
|
} {
|
|
menu ??= createMapContextMenu("b06-detail");
|
|
const ctxMenu = menu;
|
|
if (!ctxMenu.element.isConnected) {
|
|
ctxMenu.element.style.position = "fixed"; // 화면 기준 자리(clientX · Y) · 모달 위
|
|
document.body.append(ctxMenu.element);
|
|
}
|
|
const { tableBox, cad } = host;
|
|
const edit = (change: (table: CrossDetailTable) => void): void => {
|
|
const section = host.section();
|
|
const table = detailTableOf(section, host.projectId, host.routeId());
|
|
change(table);
|
|
writeDetailTable(section, table, host.projectId, host.routeId());
|
|
host.changed();
|
|
};
|
|
|
|
let picking: { kind: "area" | "length"; key: string } | null = null;
|
|
const stopPick = (): void => {
|
|
picking = null;
|
|
delete cad.dataset.pick;
|
|
delete cad.dataset.pickHint;
|
|
select.setMode(null);
|
|
document.removeEventListener("keydown", onKey, true);
|
|
};
|
|
/** 고른 출처를 칸에 — 칸 글 즉시(짝 ①) · 세션 초안 → 서버 표 다시 받음. */
|
|
const commit = (source: CrossCellSource | null): void => {
|
|
if (!picking || !source) return;
|
|
const { key } = picking;
|
|
stopPick();
|
|
select.clear();
|
|
const section = host.section();
|
|
const shown = localCellValue(source, section.design, section.samples);
|
|
const td = tableBox.querySelector<HTMLElement>(`td[data-cell="${CSS.escape(key)}"]`);
|
|
if (td) td.textContent = shown === null ? "다시 고름" : shown.toFixed(2);
|
|
if ("key" in source) select.note(bandLabel(source.key, section.design)); // 띠도 값 글(259-2)
|
|
edit((table) => setCell(table, key, source));
|
|
};
|
|
const commitSelection = (): void => {
|
|
if (picking) commit(select.source(picking.kind));
|
|
};
|
|
const onKey = (event: KeyboardEvent): void => {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
commitSelection();
|
|
return;
|
|
}
|
|
if (event.key !== "Escape") return;
|
|
event.stopPropagation(); // 모달은 그대로 — 고르기만 취소
|
|
stopPick();
|
|
};
|
|
const startPick = (kind: "area" | "length", key: string): void => {
|
|
picking = { kind, key };
|
|
cad.dataset.pick = kind;
|
|
cad.dataset.pickHint = PICK_HINT[kind];
|
|
select.setMode(kind);
|
|
document.addEventListener("keydown", onKey, true);
|
|
};
|
|
// 선 조각 고름(209-1) — 길이 한 번 누름(더하기 아님) = 바로 칸에
|
|
const select = attachDetailSelect({
|
|
cad,
|
|
design: () => host.section().design,
|
|
axes: host.axes,
|
|
picked: (additive) => {
|
|
if (picking?.kind === "length" && !additive) commitSelection();
|
|
},
|
|
commit: commitSelection,
|
|
band: (event) => !!bandAt(event),
|
|
});
|
|
const bandAt = (event: MouseEvent): Element | undefined =>
|
|
document
|
|
.elementsFromPoint(event.clientX, event.clientY)
|
|
.find((el) => cad.contains(el) && el.closest(AREA_PICK_SELECTOR));
|
|
// 면적 띠 · 사토장 누름 = 그 띠 면적(179-4 그대로 · 영역보다 앞) — 선 조각 누름은 위가 맡음
|
|
const onPick = (event: MouseEvent): void => {
|
|
if (picking?.kind !== "area" || (event.target as Element).closest(`.${PIECE_HIT_CLASS}`))
|
|
return;
|
|
const hit = bandAt(event);
|
|
const source = hit ? areaSourceOf(hit) : null;
|
|
if (!source) return;
|
|
event.stopPropagation();
|
|
commit(source);
|
|
};
|
|
cad.addEventListener("click", onPick);
|
|
|
|
const onMenu = (event: MouseEvent): void => {
|
|
const el = (event.target as Element).closest<HTMLElement>(
|
|
"[data-cell], [data-row], [data-col]",
|
|
);
|
|
if (!el || !tableBox.contains(el)) return;
|
|
event.preventDefault();
|
|
const table = detailTableOf(host.section(), host.projectId, host.routeId());
|
|
const key = el.dataset.cell ?? null;
|
|
const items = cellMenuItems(
|
|
table,
|
|
{ key, rowId: el.dataset.row ?? null, columnId: el.dataset.col ?? null },
|
|
{
|
|
pick: (kind) => key && startPick(kind, key),
|
|
auto: () => key && edit((next) => setCell(next, key, null)),
|
|
addRow: (after, grouped) => edit((next) => void addRow(next, after, grouped)),
|
|
addColumn: () => edit((next) => void addColumn(next)),
|
|
removeRow: (rowId) => edit((next) => removeRow(next, rowId)),
|
|
removeColumn: (columnId) => edit((next) => removeColumn(next, columnId)),
|
|
},
|
|
);
|
|
ctxMenu.open(event.clientX, event.clientY, items);
|
|
};
|
|
tableBox.addEventListener("contextmenu", onMenu);
|
|
|
|
// 더한 칸 글 — 손 뗄 때(포커스 잃음) 한 번 · Enter = 끝
|
|
const onBlur = (event: FocusEvent): void => {
|
|
const el = event.target as HTMLElement;
|
|
if (!el.isContentEditable) return;
|
|
const text = (el.textContent ?? "").trim();
|
|
if (text === (el.dataset.shown ?? "")) return; // 안 고침
|
|
const { label, colLabel, cell } = el.dataset;
|
|
if (label) {
|
|
const [rowId, slot] = label.split(":");
|
|
edit((table) => setRowLabel(table, rowId, Number(slot), text));
|
|
} else if (colLabel) edit((table) => setColumnLabel(table, colLabel, text));
|
|
else if (cell) edit((table) => setCell(table, cell, text ? { src: "text", text } : null));
|
|
};
|
|
const onEnter = (event: KeyboardEvent): void => {
|
|
if (event.key === "Enter" && (event.target as HTMLElement).isContentEditable) {
|
|
event.preventDefault();
|
|
(event.target as HTMLElement).blur();
|
|
}
|
|
};
|
|
tableBox.addEventListener("focusout", onBlur);
|
|
tableBox.addEventListener("keydown", onEnter);
|
|
|
|
return {
|
|
detach: () => {
|
|
stopPick();
|
|
ctxMenu.close();
|
|
select.detach();
|
|
cad.removeEventListener("click", onPick);
|
|
tableBox.removeEventListener("contextmenu", onMenu);
|
|
tableBox.removeEventListener("focusout", onBlur);
|
|
tableBox.removeEventListener("keydown", onEnter);
|
|
},
|
|
redraw: () => select.redraw(),
|
|
graph: (key) => cellGraph(detailTableOf(host.section(), host.projectId, host.routeId()), key),
|
|
showCell: (key) => {
|
|
const target = key
|
|
? cellGraph(detailTableOf(host.section(), host.projectId, host.routeId()), key)
|
|
: null;
|
|
select.choose(target && "pieces" in target ? target.pieces : null);
|
|
// 저장 조각 = 조각 값 글 · 저장 띠 = 그 띠 · 저장 없음 = 칸 키 띠
|
|
const band = !target ? key : "band" in target ? target.band : null;
|
|
select.note(band ? bandLabel(band, host.section().design) : null);
|
|
},
|
|
};
|
|
}
|