96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import type { NoteRevision } from '@shared/types';
|
||
import { inboxApi } from '../api.js';
|
||
|
||
interface Props {
|
||
noteId: string;
|
||
onClose: () => void;
|
||
/** 회수 성공 후 부모 (NoteCard) 가 local rawText 를 갱신하도록 통지. */
|
||
onRestored: (newRawText: string) => void;
|
||
}
|
||
|
||
const overlayStyle: React.CSSProperties = {
|
||
position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh',
|
||
background: 'rgba(0,0,0,0.4)', display: 'flex', alignItems: 'center',
|
||
justifyContent: 'center', zIndex: 100
|
||
};
|
||
|
||
const modalStyle: React.CSSProperties = {
|
||
background: '#fff', borderRadius: 8, padding: 20, width: 520,
|
||
maxHeight: '70vh', overflow: 'auto', boxShadow: '0 4px 16px rgba(0,0,0,0.2)'
|
||
};
|
||
|
||
const rowStyle: React.CSSProperties = {
|
||
border: '1px solid #eee', borderRadius: 6, padding: 10, marginTop: 8
|
||
};
|
||
|
||
function formatDate(iso: string): string {
|
||
return new Date(iso).toLocaleString('ko-KR');
|
||
}
|
||
|
||
function editedByLabel(by: 'user' | 'capture'): string {
|
||
return by === 'capture' ? '캡처' : '사용자';
|
||
}
|
||
|
||
export function RevisionHistoryModal({ noteId, onClose, onRestored }: Props): React.ReactElement {
|
||
const [revs, setRevs] = useState<NoteRevision[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
void (async () => {
|
||
try {
|
||
const r = await inboxApi.listRevisions(noteId);
|
||
if (!cancelled) setRevs(r);
|
||
} catch (e) {
|
||
if (!cancelled) setError((e as Error).message);
|
||
} finally {
|
||
if (!cancelled) setLoading(false);
|
||
}
|
||
})();
|
||
return () => { cancelled = true; };
|
||
}, [noteId]);
|
||
|
||
async function onRestore(rev: NoteRevision) {
|
||
if (!window.confirm('이 버전으로 되돌릴까요? 현재 본문도 이력에 보존됩니다.')) return;
|
||
const r = await inboxApi.restoreRevision(noteId, rev.revId);
|
||
if (!r.ok) {
|
||
setError(r.reason ?? '복원 실패');
|
||
return;
|
||
}
|
||
onRestored(rev.rawText);
|
||
onClose();
|
||
}
|
||
|
||
return (
|
||
<div style={overlayStyle} onClick={onClose}>
|
||
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<h3 style={{ margin: 0, fontSize: 16 }}>이력 ({revs.length}건)</h3>
|
||
<button onClick={onClose} aria-label="닫기" style={{ background: 'none', border: 'none', fontSize: 18, cursor: 'pointer', color: '#888' }}>×</button>
|
||
</div>
|
||
{loading && <div style={{ marginTop: 10, fontSize: 12, color: '#888' }}>불러오는 중…</div>}
|
||
{error !== null && <div style={{ marginTop: 10, fontSize: 12, color: '#c93030' }}>{error}</div>}
|
||
{!loading && revs.map((rev) => (
|
||
<div key={rev.revId} style={rowStyle}>
|
||
<div style={{ fontSize: 11, color: '#888', display: 'flex', justifyContent: 'space-between' }}>
|
||
<span>{formatDate(rev.editedAt)} · {editedByLabel(rev.editedBy)}</span>
|
||
<button
|
||
onClick={() => { void onRestore(rev); }}
|
||
aria-label="회수"
|
||
style={{ background: 'none', border: '1px solid #0a4b80', color: '#0a4b80', cursor: 'pointer', fontSize: 11, padding: '2px 8px', borderRadius: 3 }}
|
||
>
|
||
회수
|
||
</button>
|
||
</div>
|
||
<pre style={{ margin: '6px 0 0 0', whiteSpace: 'pre-wrap', fontSize: 12, color: '#444', background: '#fafafa', padding: 6, borderRadius: 3 }}>
|
||
{rev.rawText}
|
||
</pre>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|