feat(core,view): input cards with change tracking (plan B1 · C2)

- Three values per pier: defaults (as imported), input (generated), draft
  (current cards). Overrides = draft vs defaults; pending = draft vs input.
- Editable now: 코핑 폭, 코핑 길이, 말뚝 길이. Changing a card does not touch the
  3D view until 다시 생성; the header and the card show 변경 n.
- Revert puts a field back to its imported default.
- Project files store defaults and draft (optional; v1 files still open).
- Tests: 코핑 폭 3100 → 3300 gives one override and one pending change;
  regenerating clears pending but keeps the override; revert; file round trip.
- Not verified in a browser (no debuggable Chrome in this session).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 09:48:47 +09:00
1 parent f5303e4c2b
commit 0b62f694c5
7 files changed
+239 -9

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
import type { PierInput } from './types';
/**
* Inputs the user can change on the 2단계 cards (spec §5.2 · plan C2). Each one maps to a field of PierInput.
* Extend this list as more cards are enabled; the tracking below works on any of these keys.
*/
export const EDITABLE_FIELDS = [
{ key: 'copingWidthMm', label: '코핑 폭 B', unit: 'mm' },
{ key: 'copingLengthMm', label: '코핑 길이 L', unit: 'mm' },
{ key: 'pileLengthMm', label: '말뚝 길이', unit: 'mm' },
] as const;
export type EditableKey = (typeof EDITABLE_FIELDS)[number]['key'];
/** Keys whose values differ between two inputs. */
export function changedKeys(a: PierInput, b: PierInput): EditableKey[] {
return EDITABLE_FIELDS.map((f) => f.key).filter((key) => a[key] !== b[key]);
}
/**
* The three values a pier carries while it is edited (spec §5.5, plan B1):
* - defaults: the values as imported from the sheet (기본값, shown as "기본 3100").
* - input: the values the 3D model was generated from (생성본).
* - draft: the values the user currently has in the cards (현재 입력).
*/
export interface PierValues {
defaults: PierInput;
input: PierInput;
draft: PierInput;
}
/** Overrides: draft differs from the imported default (주황 표시, "변경 n건" in the card). */
export function overrideKeys(p: PierValues): EditableKey[] {
return changedKeys(p.defaults, p.draft);
}
/** Pending: draft differs from the generated model, so the 3D view is out of date (미반영 변경). */
export function pendingKeys(p: PierValues): EditableKey[] {
return changedKeys(p.input, p.draft);
}
/** Puts one field back to its imported default in the draft. */
export function revertDraft(p: PierValues, key: EditableKey): PierInput {
return { ...p.draft, [key]: p.defaults[key] };
}
+10 -1
View File
@@ -14,8 +14,12 @@ export interface ProjectInfo {
export interface ProjectPier {
id: string;
/** Input as read from the spreadsheet. The 3D model is not stored; it is regenerated on open (spec §8). */
/** Generated input: the values the 3D model was built from. The model is not stored; it is regenerated on open (spec §8). */
input: PierInput;
/** Values as imported from the sheet (기본값). Missing in files saved before change tracking: input is used. */
defaults?: PierInput;
/** Values currently in the cards (현재 입력). Missing in older files: input is used. */
draft?: PierInput;
}
export interface ProjectFile {
@@ -54,6 +58,11 @@ export function parseProject(text: string): ProjectFile {
if (typeof input.name !== 'string' || !Array.isArray(input.ips) || !Array.isArray(input.bearings)) {
throw new Error(`교각 ${i + 1}(${String(input.name)}): 입력 형식이 맞지 않습니다.`);
}
for (const extra of [p.defaults, p.draft]) {
if (extra !== undefined && (typeof extra !== 'object' || extra === null || typeof extra.name !== 'string')) {
throw new Error(`교각 ${i + 1}(${input.name}): defaults 또는 draft 형식이 맞지 않습니다.`);
}
}
});
return obj as ProjectFile;
}