Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 45 additions & 43 deletions frontend/src/pages/Workspace/ui/WorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,53 @@
import { useAuth, useLogout } from '@/features/auth'
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'

//이 컴포넌트는 아직 초기 프로토타입 입니다.
export default function WorkspacePage() {
const { user } = useAuth()
const { logout, loggingOut } = useLogout()

return (
<main style={{ padding: '2rem' }}>
<h1>Workspace</h1>
<p>이력서·레포 관리 화면이 들어갈 자리입니다.</p>
<div className="min-h-svh bg-bg text-fg flex flex-col">
<SiteNav />
<main className="flex-1 mx-auto w-full max-w-content px-6 lg:px-12 py-10 space-y-10">
<WorkspaceProfileCard />

<WorkspaceSection
title="내 이력서"
description="PDF 이력서를 업로드하면 AI가 분석해 면접 질문 풀에 반영합니다."
>
<EmptyResumeSlot />
</WorkspaceSection>

{user ? (
<section
style={{
margin: '1rem 0',
padding: '1rem',
border: '1px solid #ddd',
borderRadius: 8,
display: 'inline-flex',
gap: 12,
alignItems: 'center',
}}
<WorkspaceSection
title="내 GitHub 레포지토리"
description="등록한 레포를 기반으로 코드 맥락에 맞는 질문이 생성됩니다."
>
{user.avatarUrl ? (
<img
src={user.avatarUrl}
alt={user.githubUsername}
width={40}
height={40}
style={{ borderRadius: '50%' }}
/>
) : null}
<div style={{ textAlign: 'left' }}>
<div>
<strong>@{user.githubUsername}</strong>
</div>
<div style={{ color: '#666', fontSize: 14 }}>
{user.email ?? 'email 없음'}
</div>
</div>
</section>
) : null}
<EmptyRepoSlot />
</WorkspaceSection>
</main>
<SiteFooter />
</div>
)
}

<div>
<button type="button" onClick={logout} disabled={loggingOut}>
{loggingOut ? '로그아웃 중…' : '로그아웃'}
</button>
</div>
</main>
function EmptyResumeSlot() {
return (
<div className="rounded-xl border border-dashed border-border-strong bg-surface p-10 text-center">
<p className="text-body text-fg-muted">아직 등록된 이력서가 없습니다.</p>
<p className="text-caption text-fg-subtle mt-2">
다음 단계에서 PDF 업로드가 가능해집니다.
</p>
</div>
)
}

function EmptyRepoSlot() {
return (
<div className="rounded-xl border border-dashed border-border-strong bg-surface p-10 text-center">
<p className="text-body text-fg-muted">아직 등록된 레포지토리가 없습니다.</p>
<p className="text-caption text-fg-subtle mt-2">
다음 단계에서 GitHub 레포 가져오기가 가능해집니다.
</p>
</div>
)
}
4 changes: 3 additions & 1 deletion frontend/src/widgets/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ widgets/{X} → pages/*, app/* ✗
|---|---|---|
| `site-*` | 사이트 전역 (여러 페이지 재사용) | `site-nav`, `site-footer` |
| `home-*` | 홈페이지 전용 | `home-hero`, `home-services`, `home-faq` |
| `workspace-*` (예정) | 워크스페이스 페이지 전용 | `workspace-sidebar` |
| `workspace-*` | 워크스페이스 페이지 전용 | `workspace-profile-card`, `workspace-section` |
| `interview-*` (예정) | 면접 페이지 전용 | `interview-control`, `interview-transcript` |

> prefix 가 페이지 종속을 명시적으로 표현. 한 페이지에서만 쓰이는 widget 도 정상.
Expand All @@ -93,6 +93,8 @@ widgets/{X} → pages/*, app/* ✗
| `home-quote` | 다크 그린 quote · 팀 크레딧 | `/` |
| `home-faq` | FAQ 아코디언 (`<details>` 기반) | `/` |
| `home-cta` | 풀-블리드 CTA 배너 | `/` |
| `workspace-profile-card` | `useAuth().user` 기반 프로필 카드 (avatar + 핸들 + email + 연결 상태) | `/workspace` |
| `workspace-section` | 타이틀·설명·우측 액션·children 슬롯 컨테이너 (도메인 무관) | `/workspace` |

---

Expand Down
1 change: 1 addition & 0 deletions frontend/src/widgets/workspace-profile-card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { WorkspaceProfileCard } from './ui/WorkspaceProfileCard'
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useAuth } from '@/features/auth'

export function WorkspaceProfileCard() {
const { status, user } = useAuth()

if (status === 'loading') return <ProfileSkeleton />
if (!user) return null

return (
<section className="rounded-2xl bg-surface-raised border border-border shadow-sm p-6 flex items-center gap-5">
<Avatar url={user.avatarUrl} name={user.githubUsername} />
<div className="min-w-0 flex-1">
<h2 className="font-heading font-bold text-h4 text-fg-strong truncate">
@{user.githubUsername}
</h2>
<p className="text-caption text-fg-muted mt-1 truncate">
{user.email ?? '이메일 미공개'}
</p>
<p className="text-caption text-fg-subtle mt-2 inline-flex items-center gap-1.5">
<GithubMark />
GitHub로 연결됨
</p>
</div>
</section>
)
}

function Avatar({ url, name }: { url: string | null; name: string }) {
if (url) {
return (
<img
src={url}
alt={name}
width={64}
height={64}
className="w-16 h-16 rounded-full object-cover border border-border"
/>
)
}
const initial = name.charAt(0).toUpperCase() || '?'
return (
<div
aria-hidden
className="w-16 h-16 rounded-full bg-sage-100 text-fg-strong font-heading font-bold text-h5 flex items-center justify-center"
>
{initial}
</div>
)
}

function GithubMark() {
return (
<svg
aria-hidden
viewBox="0 0 16 16"
width="12"
height="12"
fill="currentColor"
>
<path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 0 0 5.47 7.59c.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.42 7.42 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
</svg>
)
}

function ProfileSkeleton() {
return (
<section
aria-busy="true"
aria-label="프로필 로딩 중"
className="rounded-2xl bg-surface-raised border border-border shadow-sm p-6 flex items-center gap-5"
>
<div className="w-16 h-16 rounded-full bg-sage-100 animate-pulse" />
<div className="flex-1 space-y-3">
<div className="h-5 w-40 rounded bg-sage-100 animate-pulse" />
<div className="h-3 w-56 rounded bg-sage-100 animate-pulse" />
</div>
</section>
)
}
1 change: 1 addition & 0 deletions frontend/src/widgets/workspace-section/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { WorkspaceSection } from './ui/WorkspaceSection'
33 changes: 33 additions & 0 deletions frontend/src/widgets/workspace-section/ui/WorkspaceSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ReactNode } from 'react'

//단순 위젯 수준은 model분리를 추후에 분리를 고려합니다.
type Props = {
title: string
description?: string
action?: ReactNode
children: ReactNode
}

export function WorkspaceSection({
title,
description,
action,
children,
}: Props) {
return (
<section>
<header className="flex items-end justify-between gap-4 mb-4">
<div>
<h3 className="font-heading font-bold text-h5 text-fg-strong">
{title}
</h3>
{description ? (
<p className="text-body text-fg-muted mt-1">{description}</p>
) : null}
</div>
{action}
</header>
{children}
</section>
)
}