에디터 정리 및 버그 수정
This commit is contained in:
+152
-6
@@ -3,7 +3,7 @@
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { FilePlus2, Trash2, Eye, Pencil, ListChecks, Undo2, Redo2 } from "lucide-react";
|
||||
import {
|
||||
DndContext,
|
||||
@@ -72,6 +72,7 @@ import { EditorCanvas } from "./EditorCanvas";
|
||||
|
||||
export default function EditorPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const blocks = useEditorStore((state) => state.blocks);
|
||||
const selectedBlockId = useEditorStore((state) => state.selectedBlockId);
|
||||
const selectedListItemId = useEditorStore((state) => (state as any).selectedListItemId as string | null | undefined);
|
||||
@@ -121,6 +122,13 @@ export default function EditorPage() {
|
||||
(state) => (state as any).projectConfig as ProjectConfig,
|
||||
);
|
||||
const updateProjectConfig = useEditorStore((state) => state.updateProjectConfig);
|
||||
const resetHistory = useEditorStore((state) => (state as any).resetHistory as () => void);
|
||||
|
||||
const [authUserId, setAuthUserId] = useState<string | null>(null);
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
|
||||
const [initialSlugFromQuery, setInitialSlugFromQuery] = useState<string | null>(null);
|
||||
const [hasLoadedInitialProjectFromSlug, setHasLoadedInitialProjectFromSlug] = useState(false);
|
||||
|
||||
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
||||
const [editingText, setEditingText] = useState("");
|
||||
@@ -140,17 +148,105 @@ export default function EditorPage() {
|
||||
const canUndo = historyLength > 0;
|
||||
const canRedo = futureLength > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams) return;
|
||||
|
||||
const slugParam = searchParams.get("slug");
|
||||
if (slugParam && !initialSlugFromQuery) {
|
||||
setInitialSlugFromQuery(slugParam);
|
||||
}
|
||||
}, [searchParams, initialSlugFromQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialSlugFromQuery) return;
|
||||
const trimmed = initialSlugFromQuery.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
const currentSlug = (projectConfig?.slug ?? "").trim();
|
||||
if (currentSlug !== trimmed) {
|
||||
updateProjectConfig({ slug: trimmed });
|
||||
}
|
||||
}, [initialSlugFromQuery, projectConfig?.slug, updateProjectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authChecked) return;
|
||||
if (!initialSlugFromQuery) return;
|
||||
if (hasLoadedInitialProjectFromSlug) return;
|
||||
|
||||
const slug = initialSlugFromQuery.trim();
|
||||
if (!slug) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadFromServer = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${encodeURIComponent(slug)}`);
|
||||
if (!res.ok || cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (data && Array.isArray(data.contentJson)) {
|
||||
replaceBlocks(data.contentJson as any);
|
||||
if (data.title && typeof data.title === "string") {
|
||||
updateProjectConfig({ title: data.title, slug });
|
||||
} else {
|
||||
updateProjectConfig({ slug });
|
||||
}
|
||||
// 다른 slug/사용자에서 불러온 뒤에는 이전 프로젝트의 undo/redo 히스토리를 끊어준다.
|
||||
resetHistory();
|
||||
}
|
||||
} catch {
|
||||
// 서버 로드 실패는 조용히 무시하고, 사용자가 수동으로 불러오기 할 수 있도록 둔다.
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setHasLoadedInitialProjectFromSlug(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadFromServer();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [authChecked, initialSlugFromQuery, hasLoadedInitialProjectFromSlug, replaceBlocks]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/me");
|
||||
if (!res.ok && res.status === 401 && !cancelled) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
let data: any = null;
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data && typeof data.id === "string") {
|
||||
setAuthUserId(data.id);
|
||||
} else {
|
||||
setAuthUserId(null);
|
||||
}
|
||||
} else if (res.status === 401) {
|
||||
router.push("/login");
|
||||
setAuthUserId(null);
|
||||
}
|
||||
} catch {
|
||||
// 네트워크 오류 등은 일단 무시하고, 페이지 접근을 막지 않는다.
|
||||
if (!cancelled) {
|
||||
setAuthUserId(null);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setAuthChecked(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -353,6 +449,15 @@ export default function EditorPage() {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.contentJson)) {
|
||||
replaceBlocks(parsed.contentJson as any);
|
||||
if (parsed.title && typeof parsed.title === "string") {
|
||||
updateProjectConfig({ title: parsed.title, slug });
|
||||
} else {
|
||||
updateProjectConfig({ slug });
|
||||
}
|
||||
|
||||
// 다른 프로젝트(slug)를 불러왔으므로 이전 history/future 를 초기화한다.
|
||||
resetHistory();
|
||||
|
||||
setProjectMessage(`로컬에서 프로젝트를 불러왔습니다: ${slug}`);
|
||||
return;
|
||||
}
|
||||
@@ -372,6 +477,15 @@ export default function EditorPage() {
|
||||
const data = await response.json();
|
||||
if (data && Array.isArray(data.contentJson)) {
|
||||
replaceBlocks(data.contentJson as any);
|
||||
if (data.title && typeof data.title === "string") {
|
||||
updateProjectConfig({ title: data.title, slug });
|
||||
} else {
|
||||
updateProjectConfig({ slug });
|
||||
}
|
||||
|
||||
// 서버에서 다른 프로젝트를 불러온 뒤에는 이전 history/future 를 모두 비운다.
|
||||
resetHistory();
|
||||
|
||||
setProjectMessage(`프로젝트를 불러왔습니다: ${slug}`);
|
||||
} else {
|
||||
setProjectMessage("프로젝트 데이터 형식이 올바르지 않습니다.");
|
||||
@@ -586,6 +700,7 @@ export default function EditorPage() {
|
||||
const rootBlocks = blocks.filter((block) => !block.sectionId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authChecked) return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const slug = projectConfig?.slug?.trim();
|
||||
@@ -596,17 +711,35 @@ export default function EditorPage() {
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { blocks?: Block[]; projectConfig?: ProjectConfig };
|
||||
const parsed = JSON.parse(raw) as {
|
||||
blocks?: Block[];
|
||||
projectConfig?: ProjectConfig;
|
||||
savedByUserId?: string | null;
|
||||
};
|
||||
|
||||
const savedByUserId = parsed.savedByUserId ?? null;
|
||||
|
||||
if (authUserId) {
|
||||
if (savedByUserId && savedByUserId !== authUserId) {
|
||||
return;
|
||||
}
|
||||
} else if (savedByUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed.blocks)) {
|
||||
replaceBlocks(parsed.blocks as any);
|
||||
}
|
||||
if (parsed.projectConfig) {
|
||||
updateProjectConfig(parsed.projectConfig as ProjectConfig);
|
||||
}
|
||||
|
||||
// autosave 로 전체 프로젝트를 복원한 경우에도 이전 history/future 는 끊어준다.
|
||||
resetHistory();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}, [projectConfig?.slug, replaceBlocks, updateProjectConfig]);
|
||||
}, [authChecked, authUserId, projectConfig?.slug, replaceBlocks, updateProjectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -615,9 +748,22 @@ export default function EditorPage() {
|
||||
if (!slug) return;
|
||||
|
||||
const key = `pb:autosave:${slug}`;
|
||||
|
||||
try {
|
||||
const existing = window.localStorage.getItem(key);
|
||||
if (existing && !authChecked) {
|
||||
// 초기 렌더 시 이미 autosave 스냅샷이 있는 경우,
|
||||
// 인증 체크 및 복원(useEffect)이 끝나기 전까지는 기존 스냅샷을 덮어쓰지 않는다.
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// 기존 값 조회 에러는 무시하고, 아래 저장 시도는 계속 진행한다.
|
||||
}
|
||||
|
||||
const payload = {
|
||||
blocks,
|
||||
projectConfig,
|
||||
savedByUserId: authUserId ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -625,7 +771,7 @@ export default function EditorPage() {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}, [blocks, projectConfig]);
|
||||
}, [blocks, projectConfig, authUserId, authChecked]);
|
||||
|
||||
// 에디터 전역에서 Undo/Redo 단축키(Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z),
|
||||
// Delete/Backspace 기반 삭제, Cmd/Ctrl+D 기반 복제,
|
||||
|
||||
Reference in New Issue
Block a user