From 32f350acfaca3ee458a1964b5324ac6db08661ed Mon Sep 17 00:00:00 2001 From: dev-minsoo Date: Tue, 9 Jun 2026 23:12:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=80=EA=B2=BD=20=EC=9D=B4=EB=A0=A5?= =?UTF-8?q?=20=EB=B9=84=EA=B5=90=20=ED=99=94=EB=A9=B4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/revision-history.tsx | 407 +++++++++++++++------------- 1 file changed, 222 insertions(+), 185 deletions(-) diff --git a/src/components/revision-history.tsx b/src/components/revision-history.tsx index f3026cc..fe76a7d 100644 --- a/src/components/revision-history.tsx +++ b/src/components/revision-history.tsx @@ -1,7 +1,7 @@ "use client"; import { GitCompareArrows, RotateCcw, X } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState, type FormEvent } from "react"; import { restoreDocumentRevision } from "@/app/actions"; import { Badge } from "@/components/ui/badge"; @@ -13,20 +13,28 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { formatDate } from "@/lib/format"; import { buildSideBySideRows, diffStats, lineDiff, - type DiffLine, type DiffRow, type DiffSide, } from "@/lib/revision-diff"; +import { cn } from "@/lib/utils"; import type { DocumentRevision } from "@/types/devwiki"; type RevisionComparison = { revision: DocumentRevision; - diff: DiffLine[]; rows: DiffRow[]; stats: { added: number; @@ -35,6 +43,12 @@ type RevisionComparison = { isCurrentSnapshot: boolean; }; +type RevisionItem = { + revision: DocumentRevision; + previousBody: string; + isCurrentSnapshot: boolean; +}; + function diffSideClassName(type: DiffSide["type"]) { if (type === "added") { return "bg-teal-50 text-teal-950"; @@ -51,137 +65,212 @@ function diffSideClassName(type: DiffSide["type"]) { return "bg-background text-foreground"; } -function RevisionDiffModal({ +function RestoreRevisionForm({ + documentId, + revision, +}: { + documentId: string; + revision: DocumentRevision; +}) { + function confirmRestore(event: FormEvent) { + const confirmed = window.confirm( + `"${revision.title}" 스냅샷으로 문서를 복원할까요?\n\n현재 제목, 요약, 본문이 선택한 이력의 내용으로 바뀝니다.`, + ); + + if (!confirmed) { + event.preventDefault(); + } + } + + return ( +
+ + + +
+ ); +} + +function RevisionDiffDialog({ canRestore, comparison, documentId, - onClose, + onOpenChange, + open, }: { canRestore: boolean; - comparison: RevisionComparison; + comparison: RevisionComparison | null; documentId: string; - onClose: () => void; + onOpenChange: (open: boolean) => void; + open: boolean; }) { - const visibleRows = comparison.rows.slice(0, 600); - - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - onClose(); - } - } + if (!comparison) { + return null; + } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [onClose]); + const visibleRows = comparison.rows.slice(0, 700); + const currentLabel = comparison.isCurrentSnapshot ? "현재 문서" : "선택 스냅샷"; return ( -
-
event.stopPropagation()} + + -
-
-
-

+ +
+
+ {comparison.revision.editSummary || "변경 내용 비교"} -

- {comparison.isCurrentSnapshot ? ( - 현재 - ) : null} + + + {formatDate(comparison.revision.createdAt)} · 제목 스냅샷:{" "} + {comparison.revision.title} · 수정자:{" "} + {comparison.revision.editedByLabel ?? "알 수 없음"} +
-

- {formatDate(comparison.revision.createdAt)} · 제목 스냅샷:{" "} - {comparison.revision.title} -

-
-
- - +{comparison.stats.added} - - - -{comparison.stats.removed} - - +
+
+ + +{comparison.stats.added}줄 + + + -{comparison.stats.removed}줄 + +
+ + + +
-
+ -
- Before - After +
+
+
+
+ Before + + 이전 스냅샷 + +
+
+ After + + {currentLabel} + +
+
+ + {visibleRows.map((row) => ( +
+ {[row.before, row.after].map((side, sideIndex) => ( +
+ + {side.line ?? ""} + + + {side.text || " "} + +
+ ))} +
+ ))} +
-
- {visibleRows.map((row) => ( -
- {[row.before, row.after].map((side, sideIndex) => ( -
- - {side.line ?? ""} - - - {side.text || " "} - -
- ))} -
- ))} - {comparison.rows.length > visibleRows.length ? ( -

- 큰 diff는 앞 {visibleRows.length.toLocaleString("ko-KR")}줄만 - 표시합니다. + +

+ 붉은 줄은 이전 스냅샷에서 제거된 내용, 초록 줄은 선택한 스냅샷에 + 추가된 내용입니다. + {comparison.rows.length > visibleRows.length + ? ` 큰 diff는 앞 ${visibleRows.length.toLocaleString("ko-KR")}줄만 표시합니다.` + : ""} +

+ {canRestore && !comparison.isCurrentSnapshot ? ( + + ) : comparison.isCurrentSnapshot ? ( +

+ 현재 문서와 같은 스냅샷이라 복원할 필요가 없습니다.

) : null} -
+ + +
+ ); +} -
-

- 붉은 줄은 이전 버전에서 제거된 내용, 초록 줄은 이후 버전에 추가된 - 내용입니다. +function RevisionListItem({ + item, + onCompare, +}: { + item: RevisionItem; + onCompare: () => void; +}) { + return ( +

  • +
    +
    +

    + {item.revision.editSummary || item.revision.title}

    - {canRestore ? ( -
    - - - -
    + {item.isCurrentSnapshot ? ( + 현재 ) : null}
    +

    + {formatDate(item.revision.createdAt)} +

    +

    + 수정자: {item.revision.editedByLabel ?? "알 수 없음"} +

    -
  • + + {item.revision.summary ? ( +

    + {item.revision.summary} +

    + ) : null} + + + ); } @@ -199,14 +288,20 @@ export function RevisionHistory({ const [selectedRevisionId, setSelectedRevisionId] = useState( null, ); + const currentRevisionId = useMemo( + () => + revisions.find((revision) => revision.bodyMarkdown === currentBody)?.id ?? + null, + [currentBody, revisions], + ); const revisionItems = useMemo( () => revisions.map((revision, index) => ({ revision, previousBody: revisions[index + 1]?.bodyMarkdown ?? "", - isCurrentSnapshot: revision.bodyMarkdown === currentBody, + isCurrentSnapshot: revision.id === currentRevisionId, })), - [currentBody, revisions], + [currentRevisionId, revisions], ); const selectedRevisionItem = revisionItems.find( (item) => item.revision.id === selectedRevisionId, @@ -223,7 +318,6 @@ export function RevisionHistory({ return { revision: selectedRevisionItem.revision, - diff, rows: buildSideBySideRows(diff), stats: diffStats(diff), isCurrentSnapshot: selectedRevisionItem.isCurrentSnapshot, @@ -249,73 +343,13 @@ export function RevisionHistory({ {revisionItems.length ? ( -
      +
        {revisionItems.map((item) => ( -
      1. -
        -
        -

        - {item.revision.editSummary || item.revision.title} - {item.isCurrentSnapshot ? ( - - 현재 - - ) : null} -

        -

        - 제목 스냅샷: {item.revision.title} -

        -
        -
        - - {item.revision.summary ? ( -

        - {item.revision.summary} -

        - ) : null} - -

        - 수정자: {item.revision.editedByLabel ?? "알 수 없음"} -

        - -
        - - {canRestore ? ( -
        - - - -
        - ) : null} -
        -
      2. + item={item} + onCompare={() => setSelectedRevisionId(item.revision.id)} + /> ))}
      ) : ( @@ -325,14 +359,17 @@ export function RevisionHistory({ )} - {selectedComparison ? ( - setSelectedRevisionId(null)} - /> - ) : null} + { + if (!open) { + setSelectedRevisionId(null); + } + }} + /> ); }