SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,65 @@
|
||||
// 백업 기록: list the automatic/manual backups, restore one (undoable) or download it.
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { deleteBackup, listBackups, MAX_BACKUPS, type BackupRecord } from './backupStore';
|
||||
|
||||
interface Props {
|
||||
onRestore: (rec: BackupRecord) => void;
|
||||
onBackupNow: () => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const BackupModal: React.FC<Props> = ({ onRestore, onBackupNow, onClose }) => {
|
||||
const [items, setItems] = useState<BackupRecord[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const reload = () => listBackups().then(setItems).catch((e) => setError(e instanceof Error ? e.message : String(e)));
|
||||
useEffect(() => { void reload(); }, []);
|
||||
const download = (b: BackupRecord) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(new Blob([b.inp], { type: 'text/plain;charset=utf-8' }));
|
||||
a.download = `${b.fileName.replace(/\.[^.]+$/, '')}_백업_${b.savedAt.slice(0, 16).replace(/[-:T]/g, '')}.inp`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
};
|
||||
return (
|
||||
<div role="dialog" aria-modal="true" aria-label="백업 기록" style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.35)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={onClose}>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{ width: 'min(640px, 96vw)', maxHeight: '86vh', background: 'var(--bg-panel)', borderRadius: 8, boxShadow: '0 12px 40px rgba(0,0,0,.25)', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', padding: '10px 14px', borderBottom: '1px solid var(--border)' }}>
|
||||
<b style={{ flex: 1 }}>백업 기록</b>
|
||||
<button className="btn btn-ghost" onClick={onClose} aria-label="닫기"><X size={14} /></button>
|
||||
</div>
|
||||
<div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10, fontSize: 12, overflow: 'auto' }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<span style={{ flex: 1, color: 'var(--text-secondary)' }}>
|
||||
모델이 수정된 상태면 10분마다 이 브라우저에 자동으로 백업합니다 (최근 {MAX_BACKUPS}개). 복원은 실행 취소(Ctrl+Z)로 되돌릴 수 있습니다.
|
||||
</span>
|
||||
<button className="btn btn-secondary" style={{ height: 26 }} onClick={() => void onBackupNow().then(reload)}>지금 백업</button>
|
||||
</div>
|
||||
{error && <div style={{ color: 'var(--crit)' }}>{error}</div>}
|
||||
{items && !items.length && <div style={{ color: 'var(--text-muted)' }}>백업이 아직 없습니다.</div>}
|
||||
{items && items.length > 0 && (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead><tr>{['시각', '파일', '규모', '', ''].map((h, i) => <th key={i} style={{ textAlign: 'left', padding: '4px 6px', background: 'var(--bg-subtle)' }}>{h}</th>)}</tr></thead>
|
||||
<tbody>
|
||||
{items.map((b) => (
|
||||
<tr key={b.id} style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '4px 6px' }}>{new Date(b.savedAt).toLocaleString('ko-KR')}{b.reason === 'manual' ? ' (수동)' : ''}</td>
|
||||
<td style={{ padding: '4px 6px' }}>{b.fileName}</td>
|
||||
<td style={{ padding: '4px 6px' }}>절점 {b.nodes} · 링크 {b.links}</td>
|
||||
<td style={{ padding: '4px 6px' }}>
|
||||
<button className="btn btn-secondary" style={{ height: 22, fontSize: 11 }} onClick={() => { onRestore(b); onClose(); }}>복원</button>{' '}
|
||||
<button className="btn btn-ghost" style={{ height: 22, fontSize: 11 }} onClick={() => download(b)}>.inp</button>
|
||||
</td>
|
||||
<td style={{ padding: '4px 6px' }}>
|
||||
<button className="btn btn-ghost" style={{ height: 22, fontSize: 11 }} aria-label="백업 삭제" onClick={() => void deleteBackup(b.id).then(reload)}><X size={11} /></button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user