feat(v0210): RevisionHistoryModal — 이력 목록 + 회수 confirm + chain 보존
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { EditableField } from './EditableField.js';
|
||||
import { IntentBanner } from './IntentBanner.js';
|
||||
import { pushTagUndo } from './TagUndoToast.js';
|
||||
import { MoveStatusModal, statusLabelWithParticle } from './MoveStatusModal.js';
|
||||
import { RevisionHistoryModal } from './RevisionHistoryModal.js';
|
||||
|
||||
interface Props {
|
||||
note: Note;
|
||||
@@ -524,6 +525,17 @@ export function NoteCard({ note, onDeleted, onUpdated, mode = 'inbox', onRestore
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRevisions && (
|
||||
<RevisionHistoryModal
|
||||
noteId={local.id}
|
||||
onClose={() => setShowRevisions(false)}
|
||||
onRestored={(newRawText) => {
|
||||
const updated = { ...local, rawText: newRawText, updatedAt: new Date().toISOString() };
|
||||
setLocal(updated);
|
||||
onUpdated(updated);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
95
src/renderer/inbox/components/RevisionHistoryModal.tsx
Normal file
95
src/renderer/inbox/components/RevisionHistoryModal.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user