From ed8e92f1fefbb53af4bdaa2f491b7c54c758a4ef Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 3 Jun 2026 22:18:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(analysis):=20DocumentList=EC=97=90=20?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EC=9C=A0=ED=98=95(sourceType)=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이력서/레포 등 소스 유형별로 분석 결과를 분리 표시할 수 있도록 클라이언트 사이드 sourceType 필터와 소스별 빈 상태 문구를 추가. Co-Authored-By: Claude Opus 4.8 --- .../src/features/analysis/ui/DocumentList.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 (
    - {data.map((doc) => ( + {docs.map((doc) => ( ))}
From 00b18c31ff7967b5621f1b9f2eb6cc8fac4a8267 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 3 Jun 2026 22:18:58 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(workspace):=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=EC=8A=A4=ED=8E=98=EC=9D=B4=EC=8A=A4/=ED=9E=88=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=A5=BC=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - workspace·history 페이지를 좌측 사이드바 단일 대시보드로 통합 - 사이드바 4탭: 홈/이력서/레포지토리/히스토리 (URL 기반 뷰 전환) - 홈 뷰: 면접 시작 CTA + 바로가기 + 요약 통계 - 이력서·레포지토리 관리를 별도 탭으로 분리, 각 탭에 소스별 분석 결과 표시 - 모든 탭을 /workspace/* 경로로 통일, 구 /history는 리다이렉트 - workspace-sidebar 위젯 신설, 미사용 workspace-profile-card 제거 Co-Authored-By: Claude Opus 4.8 --- frontend/src/app/router/index.tsx | 24 ++- frontend/src/pages/CLAUDE.md | 2 +- frontend/src/pages/History/index.ts | 1 - frontend/src/pages/History/ui/HistoryPage.tsx | 34 --- .../src/pages/Workspace/ui/HistoryView.tsx | 26 +++ frontend/src/pages/Workspace/ui/HomeView.tsx | 103 +++++++++ frontend/src/pages/Workspace/ui/ReposView.tsx | 26 +++ .../src/pages/Workspace/ui/ResumesView.tsx | 24 +++ .../src/pages/Workspace/ui/WorkspacePage.tsx | 106 ++++----- frontend/src/widgets/CLAUDE.md | 4 +- .../widgets/workspace-profile-card/index.ts | 1 - .../ui/WorkspaceProfileCard.tsx | 79 ------- .../src/widgets/workspace-sidebar/index.ts | 1 + .../workspace-sidebar/ui/WorkspaceSidebar.tsx | 204 ++++++++++++++++++ 14 files changed, 461 insertions(+), 174 deletions(-) delete mode 100644 frontend/src/pages/History/index.ts delete mode 100644 frontend/src/pages/History/ui/HistoryPage.tsx create mode 100644 frontend/src/pages/Workspace/ui/HistoryView.tsx create mode 100644 frontend/src/pages/Workspace/ui/HomeView.tsx create mode 100644 frontend/src/pages/Workspace/ui/ReposView.tsx create mode 100644 frontend/src/pages/Workspace/ui/ResumesView.tsx delete mode 100644 frontend/src/widgets/workspace-profile-card/index.ts delete mode 100644 frontend/src/widgets/workspace-profile-card/ui/WorkspaceProfileCard.tsx create mode 100644 frontend/src/widgets/workspace-sidebar/index.ts create mode 100644 frontend/src/widgets/workspace-sidebar/ui/WorkspaceSidebar.tsx 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/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/workspace-profile-card/index.ts b/frontend/src/widgets/workspace-profile-card/index.ts deleted file mode 100644 index 4f004789..00000000 --- a/frontend/src/widgets/workspace-profile-card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { WorkspaceProfileCard } from './ui/WorkspaceProfileCard' diff --git a/frontend/src/widgets/workspace-profile-card/ui/WorkspaceProfileCard.tsx b/frontend/src/widgets/workspace-profile-card/ui/WorkspaceProfileCard.tsx deleted file mode 100644 index 77754f28..00000000 --- a/frontend/src/widgets/workspace-profile-card/ui/WorkspaceProfileCard.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useAuth } from '@/features/auth' - -export function WorkspaceProfileCard() { - const { status, user } = useAuth() - - if (status === 'loading') return - 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 ( + + + + + ) +} From ee8e6d34f3c8af410ed170864785087a4c18ad4c Mon Sep 17 00:00:00 2001 From: Jaeho Date: Wed, 3 Jun 2026 22:19:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor(site-nav):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=ED=97=A4=EB=8D=94=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EB=8B=A8=EC=9D=BC=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?=EC=A0=90=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대시보드 사이드바 도입으로 중복된 히스토리 링크를 제거하고, 인증 시 헤더에는 프로필(→/workspace)과 로그아웃만 남김. Co-Authored-By: Claude Opus 4.8 --- frontend/src/widgets/site-nav/ui/SiteNav.tsx | 6 ------ 1 file changed, 6 deletions(-) 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' ? ( <> - - 히스토리 -