import type { FckByMember, Material } from './rebar/common'; import type { PierInput } from './types'; /** Project file schema (spec §8). Bump only with a migration. */ export const SCHEMA_VERSION = 1; export interface ProjectInfo { name: string; bridge: string; client: string; author: string; crs: string; memo: string; } export interface ProjectPier { id: string; /** 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 { schemaVersion: number; project: ProjectInfo; source: { fileName: string; sheets: string[] }; piers: ProjectPier[]; history: { version: string; note: string; at: string }[]; /** ③ 공통 설정 (fck · fy). Absent in older files: the defaults are used. */ common?: Material; /** ② 부재별 fck (MPa). Absent in older files: every member uses the 공통 fck. */ fckByMember?: FckByMember; } /** Serialises a project to the pretty-printed JSON stored in <교량명>_교각설계.pzr.json. */ export function serializeProject(p: ProjectFile): string { return `${JSON.stringify(p, null, 2)}\n`; } /** * Parses and checks a project file. Throws with a message that names the first problem, so a damaged file is * refused instead of being half-loaded. */ export function parseProject(text: string): ProjectFile { let raw: unknown; try { raw = JSON.parse(text); } catch { throw new Error('프로젝트 파일이 JSON으로 읽히지 않습니다.'); } const obj = raw as Partial; if (!obj || typeof obj !== 'object') throw new Error('프로젝트 파일 형식이 아닙니다.'); if (obj.schemaVersion !== SCHEMA_VERSION) { throw new Error(`지원하지 않는 파일 버전입니다 (schemaVersion ${String(obj.schemaVersion)}, 지원: ${SCHEMA_VERSION}).`); } if (!Array.isArray(obj.piers)) throw new Error('piers 목록이 없습니다.'); if (obj.fckByMember !== undefined) { for (const [member, value] of Object.entries(obj.fckByMember)) { if (!['column', 'coping', 'footing'].includes(member) || typeof value !== 'number' || !(value > 0)) { throw new Error(`부재별 fck 값이 맞지 않습니다 (${member}: ${String(value)}).`); } } } obj.piers.forEach((p, i) => { if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`); const input = p.input; 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; }