Files
inkling/src/renderer/inbox/components/RevisionHistoryModal.tsx
2026-05-09 20:51:13 +09:00

96 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}