From 45963535438118cabd35f929e0c93a588b9a98a6 Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Thu, 30 Apr 2026 14:14:35 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=98=A4=EC=9E=91=EB=8F=99=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/header/PostingActions.tsx | 5 +- src/hooks/useNavigationGuard.ts | 47 ++++++++++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/components/layout/header/PostingActions.tsx b/src/components/layout/header/PostingActions.tsx index 4f8e90a8..03da69d9 100644 --- a/src/components/layout/header/PostingActions.tsx +++ b/src/components/layout/header/PostingActions.tsx @@ -1,13 +1,12 @@ 'use client'; -import { usePathname, useRouter } from 'next/navigation'; +import { usePathname } from 'next/navigation'; import { Button, Icon } from '@/components/ui'; import { SendIcon } from '@/assets/icons'; import { useCreatePost, useUpdatePost } from '@/hooks'; function PostingActions() { - const router = useRouter(); const pathname = usePathname(); const editMatch = pathname.match(/^\/[^/]+\/board\/edit\/(\d+)$/); @@ -32,7 +31,7 @@ function PostingActions() { variant="secondary" size="md" className="typo-button1 text-text-strong px-4" - onClick={() => router.back()} + onClick={() => history.back()} > {isEditPage ? '수정 취소' : '작성 취소'} diff --git a/src/hooks/useNavigationGuard.ts b/src/hooks/useNavigationGuard.ts index 6ddf17ef..a9868571 100644 --- a/src/hooks/useNavigationGuard.ts +++ b/src/hooks/useNavigationGuard.ts @@ -23,10 +23,16 @@ function isGuardEntry() { function useNavigationGuard({ enabled }: UseNavigationGuardOptions) { const [open, setOpen] = useState(false); const router = useRouter(); + const enabledRef = useRef(enabled); const isLeaving = useRef(false); const guardUrl = useRef(''); const pendingUrl = useRef(null); const resetTimerId = useRef | null>(null); + const hasGuardEntry = useRef(false); + + useEffect(() => { + enabledRef.current = enabled; + }, [enabled]); // 네비게이션이 실제로 일어나지 않았을 경우 가드를 복원하는 안전장치 const scheduleGuardReset = (snapshot: string) => { @@ -38,40 +44,53 @@ function useNavigationGuard({ enabled }: UseNavigationGuardOptions) { guardUrl.current = location.href; if (!isGuardEntry()) { history.pushState(GUARD_STATE, '', location.href); + hasGuardEntry.current = true; } } }, NAVIGATION_TIMEOUT); }; + // enabled가 true가 될 때 guard entry를 push useEffect(() => { - if (!enabled) { - if (isGuardEntry() && !isLeaving.current) { - history.back(); - } - return; - } - - // allowNavigation()으로 이탈이 허용된 상태라면 guard를 재설정하지 않음 + if (!enabled) return; if (isLeaving.current) return; guardUrl.current = location.href; - if (!isGuardEntry()) { history.pushState(GUARD_STATE, '', location.href); } + hasGuardEntry.current = true; + }, [enabled]); + // 이벤트 리스너는 마운트 시 한 번만 등록하고, ref를 통해 최신 상태를 참조 + useEffect(() => { const handlePopState = () => { if (isLeaving.current) return; + // Next.js App Router는 pushState/replaceState 호출 시 popstate를 디스패치한다. + // guard entry 위에 있다면 우리 코드가 pushState한 것이므로 무시한다. + if (isGuardEntry()) return; + + // guard가 비활성 상태면 guard entry를 투명하게 건너뛴다 + if (!enabledRef.current) { + if (hasGuardEntry.current) { + hasGuardEntry.current = false; + history.back(); + } + return; + } + guardUrl.current = location.href; setOpen(true); }; const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (!enabledRef.current) return; e.preventDefault(); }; // 클릭을 캡처 단계에서 가로채서 Next.js Link 내비게이션을 차단 const handleClick = (e: MouseEvent) => { + if (!enabledRef.current) return; if (isLeaving.current) return; // ctrl/cmd/shift 등 새 탭/새 창 클릭은 무시 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; @@ -106,10 +125,11 @@ function useNavigationGuard({ enabled }: UseNavigationGuardOptions) { resetTimerId.current = null; } }; - }, [enabled]); + }, []); const onConfirm = () => { isLeaving.current = true; + hasGuardEntry.current = false; setOpen(false); const snapshot = location.href; @@ -133,11 +153,16 @@ function useNavigationGuard({ enabled }: UseNavigationGuardOptions) { return; } setOpen(false); - history.pushState(GUARD_STATE, '', guardUrl.current); + // 뒤로가기로 guard entry를 벗어난 경우에만 재설정 + if (!isGuardEntry()) { + history.pushState(GUARD_STATE, '', guardUrl.current); + hasGuardEntry.current = true; + } }; const allowNavigation = () => { isLeaving.current = true; + hasGuardEntry.current = false; pendingUrl.current = null; scheduleGuardReset(location.href); }; From aafc29995a36771afd92358683867326c6b1c4f4 Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Thu, 30 Apr 2026 14:15:11 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=EB=B9=88=20=EC=97=90=EB=94=94?= =?UTF-8?q?=ED=84=B0=EC=97=90=EC=84=9C=EB=8F=84=20'

'=20=EB=93=B1?= =?UTF-8?q?=EC=9D=98=20HTML=EC=9D=84=20=EB=B0=98=ED=99=98=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/board/PostEditorShell.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/board/PostEditorShell.tsx b/src/components/board/PostEditorShell.tsx index 22a0efdb..57de6ced 100644 --- a/src/components/board/PostEditorShell.tsx +++ b/src/components/board/PostEditorShell.tsx @@ -27,11 +27,14 @@ function PostEditorShell({ header, initialContent, align = 'start' }: PostEditor const files = usePostStore((s) => s.files); const snapshot = usePostStore((s) => s._snapshot); + // Tiptap은 빈 에디터에서도 '

' 등의 HTML을 반환하므로 태그를 제거 + const hasText = !!content.replace(/<[^>]*>/g, '').trim(); + const hasChanges = snapshot ? title !== snapshot.title || content !== snapshot.content || files.map((f) => f.id).join(',') !== snapshot.fileIds.join(',') - : title.length > 0 || content.length > 0 || files.length > 0; + : title.length > 0 || hasText || files.length > 0; const { open, onConfirm, onCancel, allowNavigation } = useNavigationGuard({ enabled: hasChanges, }); From b9b3b5be9c4093fc8a0048515dcefb467546214c Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Thu, 30 Apr 2026 14:33:21 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B2=A8=EB=B6=80=20=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EC=9D=98=20width=EA=B0=80=20=EB=B9=84?= =?UTF-8?q?=EC=A0=95=EC=83=81=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=8A=98?= =?UTF-8?q?=EC=96=B4=EB=82=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/board/Editor/index.tsx | 2 +- src/components/board/ImageList/ImageCard.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/board/Editor/index.tsx b/src/components/board/Editor/index.tsx index 954503e8..bc1ef049 100644 --- a/src/components/board/Editor/index.tsx +++ b/src/components/board/Editor/index.tsx @@ -50,7 +50,7 @@ export default function Editor({ initialContent }: EditorProps = {}) { if (!editor) return null; return ( -
+
{/* 숨겨진 파일 input — 슬래시 메뉴에서 각 ref를 통해 트리거 */} {item.uploaded === false && } From 79c30beaab41e5a6c7f4aff8bcbc6ae6c3c368b6 Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Thu, 30 Apr 2026 14:33:36 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20hasGuardEntry=EA=B0=80=20=EB=A6=AC?= =?UTF-8?q?=EB=A7=88=EC=9A=B4=ED=8A=B8=20=ED=9B=84=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=EB=8F=BC=20=EA=B0=80=EC=A7=9C=20=ED=9E=88=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=97=94=ED=8A=B8=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EB=86=93=EC=B9=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useNavigationGuard.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useNavigationGuard.ts b/src/hooks/useNavigationGuard.ts index a9868571..0176accf 100644 --- a/src/hooks/useNavigationGuard.ts +++ b/src/hooks/useNavigationGuard.ts @@ -52,7 +52,10 @@ function useNavigationGuard({ enabled }: UseNavigationGuardOptions) { // enabled가 true가 될 때 guard entry를 push useEffect(() => { - if (!enabled) return; + if (!enabled) { + hasGuardEntry.current = isGuardEntry(); + return; + } if (isLeaving.current) return; guardUrl.current = location.href; From 7b44beb60c794f689f9e8261f8c14b6cea573b27 Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Thu, 30 Apr 2026 14:54:19 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=20=EC=BD=94=EB=93=9C=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=EC=9D=B4=20=EB=A0=8C=EB=8D=94=EB=A7=81=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCodeHighlight.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hooks/useCodeHighlight.ts b/src/hooks/useCodeHighlight.ts index a755c931..cc56e92c 100644 --- a/src/hooks/useCodeHighlight.ts +++ b/src/hooks/useCodeHighlight.ts @@ -12,6 +12,10 @@ function useCodeHighlight(ref: RefObject, content: string) { const container = ref.current; if (!container) return; + container + .querySelectorAll('pre code[data-highlighted]') + .forEach((el) => el.removeAttribute('data-highlighted')); + const codeBlocks = container.querySelectorAll('pre code:not([data-highlighted])'); codeBlocks.forEach((codeEl) => { const text = codeEl.textContent ?? ''; From f3ef03fc8d1ecd74a80435d42109508b3724f7d7 Mon Sep 17 00:00:00 2001 From: nabbang6 Date: Thu, 30 Apr 2026 15:19:55 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20highlight=20=EA=B3=BC=EC=A0=95?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=82=B4=EC=9A=A9=EC=9D=84=20=EB=8D=AE?= =?UTF-8?q?=EC=96=B4=EC=93=B0=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCodeHighlight.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/hooks/useCodeHighlight.ts b/src/hooks/useCodeHighlight.ts index cc56e92c..41c2452a 100644 --- a/src/hooks/useCodeHighlight.ts +++ b/src/hooks/useCodeHighlight.ts @@ -18,6 +18,12 @@ function useCodeHighlight(ref: RefObject, content: string) { const codeBlocks = container.querySelectorAll('pre code:not([data-highlighted])'); codeBlocks.forEach((codeEl) => { + // 이미 하이라이팅된 경우 skip + if (codeEl.querySelector('span')) return; + + // 이미 처리된 경우 skip + if (codeEl.hasAttribute('data-highlighted')) return; + const text = codeEl.textContent ?? ''; if (!text.trim()) return; @@ -25,12 +31,23 @@ function useCodeHighlight(ref: RefObject, content: string) { .find((cls) => cls.startsWith('language-')) ?.slice('language-'.length); - const result = - lang && lowlight.registered(lang) - ? lowlight.highlight(lang, text) - : lowlight.highlightAuto(text); + let result; + + try { + result = + lang && lowlight.registered(lang) + ? lowlight.highlight(lang, text) + : lowlight.highlightAuto(text); + } catch (e) { + console.error('highlight 실패:', e); + return; + } + + const html = toHtml(result); + + if (!html || html.trim() === '') return; - codeEl.innerHTML = toHtml(result); + codeEl.innerHTML = html; codeEl.setAttribute('data-highlighted', ''); }); }, [ref, content]);