feat(z01): 좌측 패널을 B03~B09 도킹 사이드바 양식으로(상자 둘 — 기초단가 · 마스터 트리) · 기초단가 다섯(노임·기계·자재·유가·요율) 표 + 칸 고치기(editable 만 · 못 고치는 칸은 까닭·산출근거) · 덮개 판정은 서버 state 만(원본 바뀜·주인 없음은 줄째 또렷이 · 단추 글자에 값) · 요율 알림 · 고친 것 모아 보기(서버 정렬) · 공용 오버레이에 진행단계·프로젝트 이름 끄기 선택(브레인)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -40,6 +40,64 @@ export function rowCells(row: Record<string, unknown>, columns: MasterColumn[]):
|
||||
);
|
||||
}
|
||||
|
||||
/** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */
|
||||
export interface TableMeta {
|
||||
editable?: string[];
|
||||
/** 열key → 왜 못 고치나 */
|
||||
locked?: Record<string, string>;
|
||||
/** 열key → 식 자체 */
|
||||
formula?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** 덮개 판정 낱말 셋 — 서버가 줌 · 이것만 씀(2026-09-15 브레인). */
|
||||
export type OverrideState = "edited" | "source_changed" | "orphan";
|
||||
|
||||
export interface CellInfo {
|
||||
editable: boolean;
|
||||
reason: string | null;
|
||||
overridden: boolean;
|
||||
/** 고쳐진 칸만 — 사람이 고칠 때의 원본 값 */
|
||||
original?: unknown;
|
||||
/** 고친 칸의 서버 판정 — 안 고친 칸 null · 서버가 state 를 안 주면 「missing」(때우지 않음) */
|
||||
state: OverrideState | "missing" | null;
|
||||
/** source_changed 칸만 — 지금 원본 값 */
|
||||
current?: unknown;
|
||||
rule: string | null;
|
||||
/** 이 줄 값을 넣은 식 — 줄마다 다를 때만 옴 */
|
||||
filled: string | null;
|
||||
}
|
||||
|
||||
/** 칸 하나의 고침 상태 — 줄의 `@overrides`·`@formula` 는 해당할 때만 실려 옴. */
|
||||
export function cellInfo(meta: TableMeta, row: Record<string, unknown>, key: string): CellInfo {
|
||||
const overrides = row["@overrides"] as
|
||||
Record<string, { original: unknown; current?: unknown; state?: OverrideState }> | undefined;
|
||||
const filled = row["@formula"] as Record<string, string> | undefined;
|
||||
const override = overrides?.[key];
|
||||
// 판정은 서버 state 만 — 화면이 값을 견줘 따로 정하면 판정이 두 벌이 됨(2026-09-15 브레인).
|
||||
const state = override ? (override.state ?? "missing") : null;
|
||||
return {
|
||||
editable: meta.editable?.includes(key) ?? false,
|
||||
reason: meta.locked?.[key] ?? null,
|
||||
overridden: override !== undefined,
|
||||
...(override ? { original: override.original } : {}),
|
||||
state,
|
||||
...(state === "source_changed" ? { current: override?.current } : {}),
|
||||
rule: meta.formula?.[key] ?? null,
|
||||
filled: filled?.[key] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const NUMBER_TEXT = /^-?[\d,]+(\.\d+)?$/;
|
||||
|
||||
/** 고친 글자를 보낼 값으로 — 수 칸은 쉼표를 떼 수로, 못 읽으면 undefined(저장 안 함). */
|
||||
export function parseCellInput(text: string, original: unknown): unknown {
|
||||
const trimmed = text.trim();
|
||||
const numeric = typeof original === "number" || (original == null && NUMBER_TEXT.test(trimmed));
|
||||
if (!numeric) return text;
|
||||
if (!NUMBER_TEXT.test(trimmed)) return undefined;
|
||||
return Number(trimmed.replaceAll(",", ""));
|
||||
}
|
||||
|
||||
export function pageCount(total: number, size: number): number {
|
||||
return Math.max(1, Math.ceil(total / size));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user