diff --git a/src/app/doc/[uuid]/DocPageClient.tsx b/src/app/doc/[uuid]/DocPageClient.tsx index ba5586d..ab89807 100644 --- a/src/app/doc/[uuid]/DocPageClient.tsx +++ b/src/app/doc/[uuid]/DocPageClient.tsx @@ -1,9 +1,11 @@ "use client"; -import TopNavigationBar from "@/components/layout/NotePage/TopNavigationBar"; +import { useCallback, useEffect, useState } from "react"; + +import ChatMenu from "@/components/chat/ChatMenu"; import NoteExplorer from "@/components/layout/NotePage/NoteExplorer"; import NoteUI from "@/components/layout/NotePage/NoteUI"; -import ChatMenu from "@/components/chat/ChatMenu"; +import TopNavigationBar from "@/components/layout/NotePage/TopNavigationBar"; export default function DocPageClient({ user, @@ -12,12 +14,35 @@ export default function DocPageClient({ user: User; selectedNoteId: string; }) { + const [activeNoteId, setActiveNoteId] = useState(selectedNoteId); + + useEffect(() => { + setActiveNoteId(selectedNoteId); + }, [selectedNoteId]); + + const handleSelectNote = useCallback((noteId: string) => { + setActiveNoteId(noteId); + + if (typeof window !== "undefined") { + const nextUrl = `/doc/${noteId}`; + if (window.location.pathname !== nextUrl) { + window.history.replaceState(null, "", nextUrl); + } + } + }, []); + + const resolvedNoteId = activeNoteId || selectedNoteId; + return (
- +
- - + +
diff --git a/src/components/layout/NotePage/GithubCommitModal.tsx b/src/components/layout/NotePage/GithubCommitModal.tsx index 76a7384..4fb498b 100644 --- a/src/components/layout/NotePage/GithubCommitModal.tsx +++ b/src/components/layout/NotePage/GithubCommitModal.tsx @@ -1,13 +1,14 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useId, useMemo, useState } from "react"; import Portal from "@/components/common/Portal"; import { createGithubBranchAndCommit, + listGithubRepositoryFiles, listImportedRepositories, } from "@/libs/client/github"; -import { GithubBranchCommitFileInput, GithubImportedRepositoryDTO } from "@/types/github"; -import { FilePlus2, GitBranch, Loader2, Plus, Trash2, X } from "lucide-react"; +import { GithubImportedRepositoryDTO, GithubRepositoryFileDTO } from "@/types/github"; +import { CheckSquare, GitBranch, Loader2, Search, Square, X } from "lucide-react"; import toast from "react-hot-toast"; interface GithubCommitModalProps { @@ -16,36 +17,46 @@ interface GithubCommitModalProps { onClose: () => void; } -function createEmptyFileInput(): GithubBranchCommitFileInput { - return { - path: "", - content: "", - }; -} - export default function GithubCommitModal({ user, open, onClose, }: GithubCommitModalProps) { const [repositories, setRepositories] = useState([]); - const [selectedRepo, setSelectedRepo] = useState(""); + const [selectedRepo, setSelectedRepo] = useState(null); const [baseBranch, setBaseBranch] = useState("main"); const [branchName, setBranchName] = useState(""); const [commitMessage, setCommitMessage] = useState(""); - const [files, setFiles] = useState([createEmptyFileInput()]); + const [repositoryFiles, setRepositoryFiles] = useState([]); + const [selectedFileIds, setSelectedFileIds] = useState([]); + const [fileSearch, setFileSearch] = useState(""); const [loadingRepos, setLoadingRepos] = useState(false); + const [loadingFiles, setLoadingFiles] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const repoSelectId = useId(); + const baseBranchId = useId(); + const branchNameId = useId(); + const commitMessageId = useId(); + const fileSearchId = useId(); + + /** + * Resets the modal state when it closes or re-initializes. + */ + const resetState = useCallback(() => { + setSelectedRepo(null); + setBaseBranch("main"); + setBranchName(""); + setCommitMessage(""); + setRepositoryFiles([]); + setSelectedFileIds([]); + setFileSearch(""); + setError(null); + }, []); useEffect(() => { if (!open) { - setSelectedRepo(""); - setBaseBranch("main"); - setBranchName(""); - setCommitMessage(""); - setFiles([createEmptyFileInput()]); - setError(null); + resetState(); return; } @@ -54,8 +65,9 @@ export default function GithubCommitModal({ setLoadingRepos(true); const list = await listImportedRepositories(user.id); setRepositories(list); + setError(null); if (list.length > 0) { - setSelectedRepo(list[0].repositoryUrl); + setSelectedRepo(list[0]); } } catch (err) { console.error(err); @@ -66,63 +78,98 @@ export default function GithubCommitModal({ }; loadRepositories(); - }, [open, user.id]); + }, [open, resetState, user.id]); + + useEffect(() => { + if (!open || !selectedRepo) { + setRepositoryFiles([]); + setSelectedFileIds([]); + return; + } + + let cancelled = false; + + const loadRepositoryFiles = async () => { + try { + setLoadingFiles(true); + setError(null); + const files = await listGithubRepositoryFiles(user.id, selectedRepo); + if (!cancelled) { + setRepositoryFiles(files); + setSelectedFileIds([]); + setFileSearch(""); + } + } catch (err) { + console.error(err); + if (!cancelled) { + setRepositoryFiles([]); + setSelectedFileIds([]); + setError((err as Error).message ?? "파일 목록을 불러오지 못했습니다."); + } + } finally { + if (!cancelled) { + setLoadingFiles(false); + } + } + }; + + loadRepositoryFiles(); + + return () => { + cancelled = true; + }; + }, [open, selectedRepo, user.id]); const isSubmitDisabled = useMemo(() => { if (!selectedRepo || !baseBranch.trim() || !branchName.trim() || !commitMessage.trim()) { return true; } - return files.some((file) => !file.path.trim()); - }, [selectedRepo, baseBranch, branchName, commitMessage, files]); - - const handleFileChange = ( - index: number, - key: keyof GithubBranchCommitFileInput, - value: string - ) => { - setFiles((prev) => { - const next = [...prev]; - next[index] = { - ...next[index], - [key]: value, - }; - return next; - }); - }; + return selectedFileIds.length === 0; + }, [selectedRepo, baseBranch, branchName, commitMessage, selectedFileIds]); - const handleAddFile = () => { - setFiles((prev) => [...prev, createEmptyFileInput()]); - }; + /** + * Returns whether a file is currently selected for the commit. + * + * @param fileId The unique file identifier. + */ + const isFileSelected = (fileId: string) => selectedFileIds.includes(fileId); - const handleRemoveFile = (index: number) => { - setFiles((prev) => (prev.length === 1 ? prev : prev.filter((_, i) => i !== index))); + /** + * Toggles a file selection when the user interacts with it. + * + * @param fileId The unique file identifier to toggle. + */ + const handleToggleFile = (fileId: string) => { + setSelectedFileIds((prev) => + prev.includes(fileId) ? prev.filter((id) => id !== fileId) : [...prev, fileId] + ); }; + const filteredFiles = useMemo(() => { + const keyword = fileSearch.trim().toLowerCase(); + if (!keyword) { + return repositoryFiles; + } + return repositoryFiles.filter((file) => file.path.toLowerCase().includes(keyword)); + }, [fileSearch, repositoryFiles]); + const handleSubmit = async () => { - if (isSubmitDisabled) { + if (isSubmitDisabled || !selectedRepo) { setError("필수 항목을 모두 입력해주세요."); return; } - const payloadFiles: Record = {}; - files.forEach((file) => { - const path = file.path.trim(); - if (path) { - payloadFiles[path] = file.content ?? ""; - } - }); - try { setSubmitting(true); setError(null); await createGithubBranchAndCommit({ userId: user.id, - repositoryUrl: selectedRepo, + repositoryUrl: selectedRepo.repositoryUrl, baseBranch: baseBranch.trim(), branchName: branchName.trim(), commitMessage: commitMessage.trim(), - files: payloadFiles, + fileIds: selectedFileIds, }); toast.success("새 브랜치에 변경 사항을 푸시했습니다."); onClose(); @@ -167,11 +214,19 @@ export default function GithubCommitModal({
- + setBaseBranch(event.target.value)} + id={baseBranchId} />
- + setBranchName(event.target.value)} + id={branchNameId} placeholder="feature/my-change" />
- + setCommitMessage(event.target.value)} + id={commitMessageId} placeholder="Add new changes" />
-
-

- - 커밋할 파일 -

- -
+
+
+

+ 커밋할 파일 선택 +

+
-
- {files.map((file, index) => ( -
-
-
- - handleFileChange(index, "path", event.target.value)} - placeholder="docs/README.md" - /> -
- +
+ + setFileSearch(event.target.value)} + id={fileSearchId} + placeholder="파일 이름 또는 경로 검색" + /> +
+ +
+ {loadingFiles && ( +
+ 파일 목록을 불러오는 중입니다
+ )} -
- -