Files
SPipe/src/BackupModal.tsx
T
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

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>
);
};