- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
66 lines
4.3 KiB
TypeScript
66 lines
4.3 KiB
TypeScript
// 백업 기록: 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>
|
|
);
|
|
};
|