diff --git a/frontend/src/app/router/ScrollToTop.tsx b/frontend/src/app/router/ScrollToTop.tsx new file mode 100644 index 00000000..22dbfe44 --- /dev/null +++ b/frontend/src/app/router/ScrollToTop.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react' +import { Outlet, useLocation } from 'react-router-dom' + +// createBrowserRouter 는 라우트 이동 시 스크롤을 자동으로 초기화하지 않는다. +// 푸터 등 페이지 하단 링크로 이동하면 직전 스크롤 위치가 그대로 유지돼 +// 새 페이지에서도 하단(푸터)을 보게 되므로, 경로 변경 시 상단으로 올린다. +// 해시 앵커(/#services 등)는 각 페이지가 직접 스크롤하므로 건드리지 않는다. +export function ScrollToTop() { + const { pathname, hash } = useLocation() + + useEffect(() => { + if (hash) return + window.scrollTo({ top: 0 }) + }, [pathname, hash]) + + return +} diff --git a/frontend/src/app/router/index.tsx b/frontend/src/app/router/index.tsx index f4e2c03f..6b6824f4 100644 --- a/frontend/src/app/router/index.tsx +++ b/frontend/src/app/router/index.tsx @@ -1,5 +1,6 @@ import { createBrowserRouter, Navigate } from 'react-router-dom' import { RequireAuth } from '@/features/auth' +import { ScrollToTop } from './ScrollToTop' import HomePage from '@/pages/Home' import LoginPage from '@/pages/Login' import AuthCallbackPage from '@/pages/AuthCallback' @@ -11,73 +12,78 @@ import SessionFeedbackPage from '@/pages/SessionFeedback' import SharedFeedbackPage from '@/pages/SharedFeedback' export const router = createBrowserRouter([ - { path: '/', element: }, - { path: '/login', element: }, - { path: '/practice/:track', element: }, - { path: '/share/:token', element: }, - { path: '/auth/callback', element: }, { - path: '/workspace', - element: ( - - - - ), - }, - { - path: '/workspace/resumes', - element: ( - - - - ), - }, - { - path: '/workspace/repos', - element: ( - - - - ), - }, - { - path: '/sessions/new', - element: ( - - - - ), - }, - { - path: '/sessions/:id', - element: ( - - - - ), - }, - { - path: '/sessions/:id/feedback', - element: ( - - - - ), - }, - { - path: '/workspace/history', - element: ( - - - - ), - }, - { path: '/history', element: }, - { - path: '/design-system/*', - lazy: async () => { - const mod = await import('@/pages/DesignSystem') - return { Component: mod.default } - }, + element: , + children: [ + { path: '/', element: }, + { path: '/login', element: }, + { path: '/practice/:track', element: }, + { path: '/share/:token', element: }, + { path: '/auth/callback', element: }, + { + path: '/workspace', + element: ( + + + + ), + }, + { + path: '/workspace/resumes', + element: ( + + + + ), + }, + { + path: '/workspace/repos', + element: ( + + + + ), + }, + { + path: '/sessions/new', + element: ( + + + + ), + }, + { + path: '/sessions/:id', + element: ( + + + + ), + }, + { + path: '/sessions/:id/feedback', + element: ( + + + + ), + }, + { + path: '/workspace/history', + element: ( + + + + ), + }, + { path: '/history', element: }, + { + path: '/design-system/*', + lazy: async () => { + const mod = await import('@/pages/DesignSystem') + return { Component: mod.default } + }, + }, + ], }, ]) diff --git a/frontend/src/features/interview/ui/live/ConversationThread.tsx b/frontend/src/features/interview/ui/live/ConversationThread.tsx index 841c565f..93b2120c 100644 --- a/frontend/src/features/interview/ui/live/ConversationThread.tsx +++ b/frontend/src/features/interview/ui/live/ConversationThread.tsx @@ -12,16 +12,19 @@ export function ConversationThread({ items: ThreadItem[] awaitingQuestion: boolean }) { - const bottomRef = useRef(null) + // 내부 스레드 컨테이너만 스크롤한다. scrollIntoView 는 스크롤 가능한 모든 + // 조상(=window)까지 스크롤해 페이지가 푸터로 끌려 내려가므로 사용하지 않는다. + const containerRef = useRef(null) useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + const el = containerRef.current + if (el) el.scrollTop = el.scrollHeight }, [items.length, awaitingQuestion]) // 가장 마지막 질문만 자동재생(초기 로드 시 과거 질문 일괄 재생 방지). const lastQuestionKey = [...items].reverse().find(isQuestion)?.key return ( - + {items.map((item) => isQuestion(item) ? ( @@ -30,7 +33,6 @@ export function ConversationThread({ ), )} {awaitingQuestion ? : null} - ) }