feat(view): reflect 시안 ① main screen and ⑤ save dialog
- Header follows the mockup: dark bar, project line, stage chips (② shows the pending change count), save status, 저장 / 엑셀 불러오기 / 프로젝트 열기 and the primary 선택 교각 다시 생성 · 변경 n button. - Left column: pier list with header row, 선택 n기 bulk bar, 입력값 and the 2단계 input card. The bottom input panel is gone. - First save opens 시안 ⑤ (project name, bridge, client, author, 좌표계, memo); later Ctrl+S / 저장 write the same file, as the spec's B5 requires. - Project info (client, author, 좌표계, memo) is kept in the project file. - Style layer uses the mockup's tokens (#18212B header, #1F5FA8 primary, #D5DBE2 borders, #E3EDF8 selected row, JetBrains Mono for numbers). - Not reflected: ② and ③ screens (not built yet), and the mockup's static front preview (the app draws the model in 3D). Not verified in a browser. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b62f694c5
commit
fae8d7b9b8
3 files changed
+193
-27
No files matched your search
+64
-7
@@ -7,7 +7,7 @@ import { buildPier, type PierModel } from './core/geometry';
|
||||
import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate';
|
||||
import { buildTemplateWorkbook } from './core/template';
|
||||
import { EDITABLE_FIELDS, overrideKeys, pendingKeys, revertDraft, type EditableKey, type PierValues } from './core/changes';
|
||||
import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile } from './core/project';
|
||||
import { parseProject, SCHEMA_VERSION, serializeProject, type ProjectFile, type ProjectInfo } from './core/project';
|
||||
import { clearBackup, readBackup, writeBackup, type Backup } from './view/backup';
|
||||
import type { Issue, PierInput, PierType } from './core/types';
|
||||
import { buildPierScene, viewPosition, type PartName, type PierScene, type ViewKind } from './view/scene';
|
||||
@@ -38,6 +38,7 @@ const VIEWS: { kind: ViewKind; label: string }[] = [
|
||||
];
|
||||
|
||||
const TYPES: PierType[] = ['T형', '2주형', '3주형'];
|
||||
const CRS = '국토좌표계 (x = 동, y = 북)';
|
||||
|
||||
interface Pending {
|
||||
fileName: string;
|
||||
@@ -52,7 +53,9 @@ const state = {
|
||||
view: 'iso' as ViewKind,
|
||||
visible: Object.fromEntries(PART_GROUPS.flatMap((g) => g.parts).map((p) => [p, true])) as Record<PartName, boolean>,
|
||||
scene: null as PierScene | null,
|
||||
project: { name: '', bridge: '' },
|
||||
project: { name: '', bridge: '', client: '', author: '', crs: CRS, memo: '' } as ProjectInfo,
|
||||
/** True once the project has a file on disk (first save via 시안 ⑤, or an opened file). Later saves go straight to it. */
|
||||
savedOnce: false,
|
||||
filter: 'all' as 'all' | PierType,
|
||||
pending: null as Pending | null,
|
||||
source: { fileName: '', sheets: [] as string[] },
|
||||
@@ -256,9 +259,13 @@ function renderHeader(): void {
|
||||
$('step1-status').textContent = pier?.model ? '생성됨' : '미생성';
|
||||
renderSaveStatus();
|
||||
const total = pendingTotal();
|
||||
const checked = state.piers.filter((p) => p.checked).length;
|
||||
$('step2-status').textContent = total ? `변경 ${total}건 · 재생성 필요` : '예정';
|
||||
const regen = $<HTMLButtonElement>('regen');
|
||||
regen.disabled = !state.piers.some((p) => p.checked);
|
||||
regen.disabled = checked === 0;
|
||||
regen.textContent = total ? `선택 교각 다시 생성 · 변경 ${total}` : '선택 교각 형상 생성';
|
||||
$('bulk-count').textContent = `선택 ${checked}기`;
|
||||
$<HTMLButtonElement>('bulk-regen').disabled = checked === 0;
|
||||
}
|
||||
|
||||
// --- results panel (시안 ① 입력값 · 계산 결과) ----------------------------------------
|
||||
@@ -461,7 +468,7 @@ function isDirty(): boolean {
|
||||
function currentProjectText(): string {
|
||||
return serializeProject({
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
project: { name: state.project.name, bridge: state.project.bridge, client: '', author: '', crs: '', memo: '' },
|
||||
project: { ...state.project },
|
||||
source: state.source,
|
||||
piers: state.piers.map((p) => ({ id: p.input.name, input: p.input, defaults: p.defaults, draft: p.draft })),
|
||||
history: state.history,
|
||||
@@ -481,11 +488,55 @@ function renderSaveStatus(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 저장 button and Ctrl+S: the first save opens the project dialog (시안 ⑤); later saves write the same file. */
|
||||
function saveProject(): void {
|
||||
if (state.piers.length === 0) return;
|
||||
if (!state.savedOnce) {
|
||||
openSaveDialog();
|
||||
return;
|
||||
}
|
||||
writeProjectFile();
|
||||
}
|
||||
|
||||
function projectFileName(): string {
|
||||
return `${state.project.bridge || state.project.name || '교각설계'}_교각설계.pzr.json`;
|
||||
}
|
||||
|
||||
function openSaveDialog(): void {
|
||||
$<HTMLInputElement>('sv-name').value = state.project.name;
|
||||
$<HTMLInputElement>('sv-bridge').value = state.project.bridge;
|
||||
$<HTMLInputElement>('sv-client').value = state.project.client;
|
||||
$<HTMLInputElement>('sv-author').value = state.project.author;
|
||||
$<HTMLTextAreaElement>('sv-memo').value = state.project.memo;
|
||||
$('sv-file').textContent = projectFileName();
|
||||
$<HTMLDialogElement>('save-dialog').showModal();
|
||||
$<HTMLInputElement>('sv-name').focus();
|
||||
}
|
||||
|
||||
function confirmSaveDialog(): void {
|
||||
const name = $<HTMLInputElement>('sv-name').value.trim();
|
||||
if (!name) {
|
||||
$<HTMLInputElement>('sv-name').focus();
|
||||
setStatus('프로젝트명을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
state.project = {
|
||||
name,
|
||||
bridge: $<HTMLInputElement>('sv-bridge').value.trim(),
|
||||
client: $<HTMLInputElement>('sv-client').value.trim(),
|
||||
author: $<HTMLInputElement>('sv-author').value.trim(),
|
||||
crs: CRS,
|
||||
memo: $<HTMLTextAreaElement>('sv-memo').value.trim(),
|
||||
};
|
||||
$<HTMLDialogElement>('save-dialog').close();
|
||||
writeProjectFile();
|
||||
}
|
||||
|
||||
function writeProjectFile(): void {
|
||||
state.savedOnce = true;
|
||||
state.history = [...state.history, { version: String(state.history.length + 1), note: '저장', at: new Date().toISOString() }];
|
||||
const text = currentProjectText();
|
||||
const name = `${state.project.bridge || state.project.name || '교각설계'}_교각설계.pzr.json`;
|
||||
const name = projectFileName();
|
||||
const url = URL.createObjectURL(new Blob([text], { type: 'application/json' }));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -501,7 +552,7 @@ function saveProject(): void {
|
||||
/** Opens a project from its text. A backup stays unsaved until the user saves it. */
|
||||
function openProjectText(text: string, saved: boolean): void {
|
||||
const parsed = parseProject(text);
|
||||
state.project = { name: parsed.project.name, bridge: parsed.project.bridge };
|
||||
state.project = { ...parsed.project };
|
||||
state.source = parsed.source;
|
||||
state.history = parsed.history;
|
||||
state.piers = parsed.piers.map((p) => ({
|
||||
@@ -520,6 +571,7 @@ function openProjectText(text: string, saved: boolean): void {
|
||||
async function openProjectFile(file: File): Promise<void> {
|
||||
try {
|
||||
openProjectText(await file.text(), true);
|
||||
state.savedOnce = true;
|
||||
clearBackup().catch(() => undefined);
|
||||
setStatus(`${file.name}을 열었습니다. ${state.piers.length}기를 원본 엑셀 없이 다시 생성했습니다.`);
|
||||
} catch (err) {
|
||||
@@ -680,7 +732,7 @@ function applyImport(): void {
|
||||
const p = state.pending;
|
||||
const name = $<HTMLInputElement>('project-name').value.trim();
|
||||
if (!p || !name) return;
|
||||
state.project = { name, bridge: $<HTMLInputElement>('bridge-name').value.trim() };
|
||||
state.project = { ...state.project, name, bridge: $<HTMLInputElement>('bridge-name').value.trim() };
|
||||
state.piers = p.entries.filter((e) => e.checked && selectable(p, e));
|
||||
state.active = state.piers[0]?.input.name ?? null;
|
||||
state.filter = 'all';
|
||||
@@ -688,6 +740,7 @@ function applyImport(): void {
|
||||
state.source = { fileName: p.fileName, sheets: p.sheets.map((sheet) => sheet.name) };
|
||||
state.history = [];
|
||||
state.savedSnapshot = null;
|
||||
state.savedOnce = false;
|
||||
$<HTMLDialogElement>('import-dialog').close();
|
||||
setupScene();
|
||||
renderAll();
|
||||
@@ -727,6 +780,10 @@ $<HTMLButtonElement>('dlg-close').addEventListener('click', () => $<HTMLDialogEl
|
||||
$<HTMLButtonElement>('dlg-cancel').addEventListener('click', () => $<HTMLDialogElement>('import-dialog').close());
|
||||
$<HTMLButtonElement>('dlg-apply').addEventListener('click', applyImport);
|
||||
$<HTMLButtonElement>('regen').addEventListener('click', regenerateChecked);
|
||||
$<HTMLButtonElement>('bulk-regen').addEventListener('click', regenerateChecked);
|
||||
$<HTMLButtonElement>('sv-save').addEventListener('click', confirmSaveDialog);
|
||||
$<HTMLButtonElement>('sv-cancel').addEventListener('click', () => $<HTMLDialogElement>('save-dialog').close());
|
||||
$<HTMLButtonElement>('sv-close').addEventListener('click', () => $<HTMLDialogElement>('save-dialog').close());
|
||||
$<HTMLButtonElement>('edit-regen').addEventListener('click', () => {
|
||||
const pier = activePier();
|
||||
if (pier) regenerateOne(pier.input.name);
|
||||
|
||||
Reference in new issue
Block a user