diff --git a/frontend/src/app/router/index.tsx b/frontend/src/app/router/index.tsx index ffdc4f54..1cb05589 100644 --- a/frontend/src/app/router/index.tsx +++ b/frontend/src/app/router/index.tsx @@ -1,4 +1,4 @@ -import { createBrowserRouter } from 'react-router-dom' +import { createBrowserRouter, Navigate } from 'react-router-dom' import { RequireAuth } from '@/features/auth' import HomePage from '@/pages/Home' import LoginPage from '@/pages/Login' @@ -7,7 +7,6 @@ import WorkspacePage from '@/pages/Workspace' import InterviewSetupPage from '@/pages/InterviewSetup' import InterviewSessionPage from '@/pages/InterviewSession' import SessionFeedbackPage from '@/pages/SessionFeedback' -import HistoryPage from '@/pages/History' import SharedFeedbackPage from '@/pages/SharedFeedback' export const router = createBrowserRouter([ @@ -23,6 +22,22 @@ export const router = createBrowserRouter([ ), }, + { + path: '/workspace/resumes', + element: ( + + + + ), + }, + { + path: '/workspace/repos', + element: ( + + + + ), + }, { path: '/sessions/new', element: ( @@ -48,13 +63,14 @@ export const router = createBrowserRouter([ ), }, { - path: '/history', + path: '/workspace/history', element: ( - + ), }, + { path: '/history', element: }, { path: '/design-system/*', lazy: async () => { diff --git a/frontend/src/features/analysis/ui/DocumentList.tsx b/frontend/src/features/analysis/ui/DocumentList.tsx index a5724168..c993a759 100644 --- a/frontend/src/features/analysis/ui/DocumentList.tsx +++ b/frontend/src/features/analysis/ui/DocumentList.tsx @@ -24,9 +24,11 @@ const SOURCE_LABEL: Record = { type Props = { filter?: DocumentFilter + // 클라이언트에서 소스 유형으로 한정 (탭별 분석 결과 분리용). + sourceType?: AnalysisSourceType } -export function DocumentList({ filter = {} }: Props) { +export function DocumentList({ filter = {}, sourceType }: Props) { const { data = [], isPending, isError, error } = useDocuments(filter) if (isPending) { @@ -39,12 +41,18 @@ export function DocumentList({ filter = {} }: Props) {

) } - if (data.length === 0) { + + const docs = sourceType + ? data.filter((doc) => doc.sourceType === sourceType) + : data + + if (docs.length === 0) { + const subject = sourceType ? SOURCE_LABEL[sourceType] : '이력서·레포' return (

아직 분석된 문서가 없습니다.

- 이력서·레포 분석이 완료되면 요약과 기술 스택이 여기에 표시됩니다. + {subject} 분석이 완료되면 요약과 기술 스택이 여기에 표시됩니다.

) @@ -52,7 +60,7 @@ export function DocumentList({ filter = {} }: Props) { return ( diff --git a/frontend/src/pages/CLAUDE.md b/frontend/src/pages/CLAUDE.md index f91c155c..6c4b58d0 100644 --- a/frontend/src/pages/CLAUDE.md +++ b/frontend/src/pages/CLAUDE.md @@ -40,7 +40,7 @@ pages/Workspace/ | `Workspace` | `/workspace` | `features/resume`, `features/repo` (도입 예정) | | `Interview` | `/sessions/new`, `/sessions/:id` | `features/interview` | | `Interview (Feedback)` | `/sessions/:id/feedback` | `features/feedback` | -| `History` | `/history`, `/history/:id` | `features/history` (도입 예정) | +| `History` | `/workspace/history` (구 `/history` → 리다이렉트) | `features/history` | --- diff --git a/frontend/src/pages/History/index.ts b/frontend/src/pages/History/index.ts deleted file mode 100644 index c77a6ed6..00000000 --- a/frontend/src/pages/History/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ui/HistoryPage' diff --git a/frontend/src/pages/History/ui/HistoryPage.tsx b/frontend/src/pages/History/ui/HistoryPage.tsx deleted file mode 100644 index 4a2484cc..00000000 --- a/frontend/src/pages/History/ui/HistoryPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { SiteNav } from '@/widgets/site-nav' -import { SiteFooter } from '@/widgets/site-footer' -import { - ScoreTrend, - SessionHistoryList, - StatsSummary, - useUserStats, -} from '@/features/history' - -export default function HistoryPage() { - const { data: stats } = useUserStats() - - return ( -
- -
-

면접 히스토리

- - {stats && ( -
- - -
- )} - -
-

지난 면접

- -
-
- -
- ) -} diff --git a/frontend/src/pages/Workspace/ui/HistoryView.tsx b/frontend/src/pages/Workspace/ui/HistoryView.tsx new file mode 100644 index 00000000..a4ee6c67 --- /dev/null +++ b/frontend/src/pages/Workspace/ui/HistoryView.tsx @@ -0,0 +1,26 @@ +import { + ScoreTrend, + SessionHistoryList, + StatsSummary, + useUserStats, +} from '@/features/history' + +export function HistoryView() { + const { data: stats } = useUserStats() + + return ( +
+ {stats && ( +
+ + +
+ )} + +
+

지난 면접

+ +
+
+ ) +} diff --git a/frontend/src/pages/Workspace/ui/HomeView.tsx b/frontend/src/pages/Workspace/ui/HomeView.tsx new file mode 100644 index 00000000..6cc1ec21 --- /dev/null +++ b/frontend/src/pages/Workspace/ui/HomeView.tsx @@ -0,0 +1,103 @@ +import { Link } from 'react-router-dom' +import { + ScoreTrend, + StatsSummary, + useUserStats, +} from '@/features/history' +import { Button } from '@/shared/ui/Button' + +export function HomeView() { + const { data: stats } = useUserStats() + const hasSessions = (stats?.totalSessionCount ?? 0) > 0 + + return ( +
+
+

+ 맞춤 모의 면접 +

+

+ 이력서·레포 기반 맞춤 면접을 시작하세요 +

+

+ 면접 모드와 직군을 고르면 AI가 질문을 생성하고, 실시간으로 답변을 + 주고받습니다. +

+
+ + + + + + +
+
+ +
+ + + +
+ + {hasSessions && stats && ( +
+

한눈에 보기

+
+ + +
+
+ )} +
+ ) +} + +function QuickLink({ + to, + title, + description, +}: { + to: string + title: string + description: string +}) { + return ( + +
+

+ {title} +

+

{description}

+
+ + → + + + ) +} diff --git a/frontend/src/pages/Workspace/ui/ReposView.tsx b/frontend/src/pages/Workspace/ui/ReposView.tsx new file mode 100644 index 00000000..697fba99 --- /dev/null +++ b/frontend/src/pages/Workspace/ui/ReposView.tsx @@ -0,0 +1,26 @@ +import { WorkspaceSection } from '@/widgets/workspace-section' +import { RepoList, RepoPicker } from '@/features/repo' +import { DocumentList } from '@/features/analysis' + +export function ReposView() { + return ( +
+ +
+ + +
+
+ + + + +
+ ) +} diff --git a/frontend/src/pages/Workspace/ui/ResumesView.tsx b/frontend/src/pages/Workspace/ui/ResumesView.tsx new file mode 100644 index 00000000..ccb769cc --- /dev/null +++ b/frontend/src/pages/Workspace/ui/ResumesView.tsx @@ -0,0 +1,24 @@ +import { WorkspaceSection } from '@/widgets/workspace-section' +import { ResumeList, ResumeUploader } from '@/features/resume' +import { DocumentList } from '@/features/analysis' + +export function ResumesView() { + return ( +
+ } + > + + + + + + +
+ ) +} diff --git a/frontend/src/pages/Workspace/ui/WorkspacePage.tsx b/frontend/src/pages/Workspace/ui/WorkspacePage.tsx index dd1dec0e..84368e6f 100644 --- a/frontend/src/pages/Workspace/ui/WorkspacePage.tsx +++ b/frontend/src/pages/Workspace/ui/WorkspacePage.tsx @@ -1,64 +1,66 @@ -import { Link } from 'react-router-dom' -import { SiteNav } from '@/widgets/site-nav' -import { SiteFooter } from '@/widgets/site-footer' -import { WorkspaceProfileCard } from '@/widgets/workspace-profile-card' -import { WorkspaceSection } from '@/widgets/workspace-section' -import { ResumeList, ResumeUploader } from '@/features/resume' -import { RepoList, RepoPicker } from '@/features/repo' -import { DocumentList } from '@/features/analysis' -import { Button } from '@/shared/ui/Button' +import { useLocation } from 'react-router-dom' +import { useAuth } from '@/features/auth' +import { WorkspaceSidebar } from '@/widgets/workspace-sidebar' import { useWorkspaceAnalysisStream } from '../model/useWorkspaceAnalysisStream' +import { HomeView } from './HomeView' +import { ResumesView } from './ResumesView' +import { ReposView } from './ReposView' +import { HistoryView } from './HistoryView' + +type View = 'home' | 'resumes' | 'repos' | 'history' + +function resolveView(pathname: string): View { + if (pathname.startsWith('/workspace/history')) return 'history' + if (pathname.startsWith('/workspace/resumes')) return 'resumes' + if (pathname.startsWith('/workspace/repos')) return 'repos' + return 'home' +} export default function WorkspacePage() { - // 분석 상태 실시간 구독 (SSE) — 완료되면 목록이 자동 갱신된다. + // 분석 상태 실시간 구독 (SSE) — 어떤 뷰에 있든 완료 시 목록이 자동 갱신된다. useWorkspaceAnalysisStream() - return ( -
- -
- - - - - - } - > -

- 면접 모드와 직군을 고르면 AI가 질문을 생성하고, 실시간으로 답변을 주고받습니다. -

-
+ const { user } = useAuth() + const { pathname } = useLocation() + const view = resolveView(pathname) - } - > - - + const meta = { + home: { + title: user ? `안녕하세요, ${user.githubUsername}님` : '대시보드', + description: '오늘도 맞춤 모의 면접으로 실전 감각을 키워보세요.', + }, + resumes: { + title: '이력서', + description: '이력서를 업로드하고 분석 결과를 확인하세요.', + }, + repos: { + title: '레포지토리', + description: 'GitHub 레포를 등록하고 분석 결과를 확인하세요.', + }, + history: { + title: '면접 히스토리', + description: '지난 면접 기록과 점수 추이를 확인하세요.', + }, + }[view] - -
- - -
-
+ return ( +
+ +
+
+
+

+ {meta.title} +

+

{meta.description}

+
- - - + {view === 'home' && } + {view === 'resumes' && } + {view === 'repos' && } + {view === 'history' && } +
-
) } diff --git a/frontend/src/widgets/CLAUDE.md b/frontend/src/widgets/CLAUDE.md index ad23c576..98aafc5a 100644 --- a/frontend/src/widgets/CLAUDE.md +++ b/frontend/src/widgets/CLAUDE.md @@ -93,8 +93,8 @@ widgets/{X} → pages/*, app/* ✗ | `home-quote` | 다크 그린 quote · 팀 크레딧 | `/` | | `home-faq` | FAQ 아코디언 (`
` 기반) | `/` | | `home-cta` | 풀-블리드 CTA 배너 | `/` | -| `workspace-profile-card` | `useAuth().user` 기반 프로필 카드 (avatar + 핸들 + email + 연결 상태) | `/workspace` | -| `workspace-section` | 타이틀·설명·우측 액션·children 슬롯 컨테이너 (도메인 무관) | `/workspace` | +| `workspace-section` | 타이틀·설명·우측 액션·children 슬롯 컨테이너 (도메인 무관) | `/workspace/resumes`, `/workspace/repos` | +| `workspace-sidebar` | 대시보드 좌측 사이드바 (브랜드 + 홈/이력서/레포지토리/히스토리 `NavLink` 전환 + 프로필·로그아웃). `lg` 미만에서는 상단 가로 바 | `/workspace`, `/workspace/resumes`, `/workspace/repos`, `/workspace/history` | --- diff --git a/frontend/src/widgets/site-nav/ui/SiteNav.tsx b/frontend/src/widgets/site-nav/ui/SiteNav.tsx index de6b7d68..989b617e 100644 --- a/frontend/src/widgets/site-nav/ui/SiteNav.tsx +++ b/frontend/src/widgets/site-nav/ui/SiteNav.tsx @@ -53,12 +53,6 @@ export function SiteNav() {
{status === 'authenticated' ? ( <> - - 히스토리 - - if (!user) return null - - return ( -
- -
-

- @{user.githubUsername} -

-

- {user.email ?? '이메일 미공개'} -

-

- - GitHub로 연결됨 -

-
-
- ) -} - -function Avatar({ url, name }: { url: string | null; name: string }) { - if (url) { - return ( - {name} - ) - } - const initial = name.charAt(0).toUpperCase() || '?' - return ( -
- {initial} -
- ) -} - -function GithubMark() { - return ( - - - - ) -} - -function ProfileSkeleton() { - return ( -
-
-
-
-
-
-
- ) -} diff --git a/frontend/src/widgets/workspace-sidebar/index.ts b/frontend/src/widgets/workspace-sidebar/index.ts new file mode 100644 index 00000000..907a994a --- /dev/null +++ b/frontend/src/widgets/workspace-sidebar/index.ts @@ -0,0 +1 @@ +export { WorkspaceSidebar } from './ui/WorkspaceSidebar' diff --git a/frontend/src/widgets/workspace-sidebar/ui/WorkspaceSidebar.tsx b/frontend/src/widgets/workspace-sidebar/ui/WorkspaceSidebar.tsx new file mode 100644 index 00000000..9e15d0ee --- /dev/null +++ b/frontend/src/widgets/workspace-sidebar/ui/WorkspaceSidebar.tsx @@ -0,0 +1,204 @@ +import type { ReactNode } from 'react' +import { Link, NavLink } from 'react-router-dom' +import { useAuth, useLogout } from '@/features/auth' + +type NavItem = { + to: string + label: string + icon: ReactNode + end?: boolean +} + +const navItems: NavItem[] = [ + { to: '/workspace', label: '홈', icon: , end: true }, + { to: '/workspace/resumes', label: '이력서', icon: }, + { to: '/workspace/repos', label: '레포지토리', icon: }, + { to: '/workspace/history', label: '히스토리', icon: }, +] + +export function WorkspaceSidebar() { + const { user } = useAuth() + const { logout, loggingOut } = useLogout() + + return ( + + ) +} + +function HomeIcon() { + return ( + + + + ) +} + +function ResumeIcon() { + return ( + + + + + + ) +} + +function RepoIcon() { + return ( + + + + + + + ) +} + +function HistoryIcon() { + return ( + + + + + ) +} + +function LogoutIcon() { + return ( + + + + + ) +}