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
12 changes: 5 additions & 7 deletions frontend/src/features/analysis/ui/DocumentList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { isApiError } from '@/shared/api'
import { Modal, StatusBadge, type StatusTone } from '@/shared/ui'
import { EmptyState, Modal, StatusBadge, type StatusTone } from '@/shared/ui'
import type { DocumentFilter } from '../api/analysis'
import { useDocuments } from '../model/useDocuments'
import type {
Expand Down Expand Up @@ -52,12 +52,10 @@ export function DocumentList({ filter = {}, sourceType }: Props) {
if (docs.length === 0) {
const subject = sourceType ? SOURCE_LABEL[sourceType] : '이력서·레포'
return (
<div className="rounded-2xl border border-dashed border-border-strong bg-surface-raised p-10 text-center">
<p className="text-body text-fg-strong">아직 분석된 문서가 없습니다.</p>
<p className="mt-1 text-caption text-fg-muted">
{subject} 분석이 완료되면 요약과 기술 스택이 여기에 표시됩니다.
</p>
</div>
<EmptyState
title="아직 분석된 문서가 없어요"
description={`${subject} 분석이 완료되면 요약과 기술 스택이 여기에 표시됩니다.`}
/>
)
}

Expand Down
15 changes: 14 additions & 1 deletion frontend/src/features/history/ui/SessionHistoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Link } from 'react-router-dom'
import { EmptyState } from '@/shared/ui'
import { useSessions } from '../model/useHistory'
import { SessionCard } from './SessionCard'

Expand Down Expand Up @@ -29,7 +31,18 @@ export function SessionHistoryList() {
const sessions = data?.pages.flatMap((p) => p.content ?? []) ?? []
if (sessions.length === 0) {
return (
<p className="py-8 text-center text-body text-fg-muted">아직 진행한 면접이 없어요.</p>
<EmptyState
title="아직 진행한 면접이 없어요"
description="첫 모의면접을 시작해 피드백을 받아보세요."
action={
<Link
to="/sessions/new"
className="inline-flex items-center rounded-md bg-primary px-4 py-2 text-button font-medium text-fg-on-primary transition-colors hover:bg-primary-hover"
>
면접 시작
</Link>
}
/>
)
}

Expand Down
14 changes: 5 additions & 9 deletions frontend/src/features/repo/ui/RepoList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { isApiError } from '@/shared/api'
import { useAnalysisProgress } from '@/shared/hooks'
import { ConfirmDialog, StatusBadge, type StatusTone } from '@/shared/ui'
import { ConfirmDialog, EmptyState, StatusBadge, type StatusTone } from '@/shared/ui'
import {
useDeleteRepository,
useRegisteredRepositories,
Expand Down Expand Up @@ -34,14 +34,10 @@ export function RepoList() {
}
if (data.length === 0) {
return (
<div className="rounded-2xl border border-dashed border-border-strong bg-surface-raised p-10 text-center">
<p className="text-body text-fg-strong">
아직 등록된 레포지토리가 없습니다.
</p>
<p className="mt-1 text-caption text-fg-muted">
위에서 GitHub 레포를 가져오면 분석이 자동으로 시작됩니다.
</p>
</div>
<EmptyState
title="아직 등록된 레포지토리가 없어요"
description="위에서 GitHub 레포를 가져오면 분석이 자동으로 시작됩니다."
/>
)
}

Expand Down
9 changes: 7 additions & 2 deletions frontend/src/features/resume/ui/ResumeList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { isApiError } from '@/shared/api'
import { useAnalysisProgress } from '@/shared/hooks'
import { ConfirmDialog, StatusBadge, type StatusTone } from '@/shared/ui'
import { ConfirmDialog, EmptyState, StatusBadge, type StatusTone } from '@/shared/ui'
import { useDeleteResume, useResumes } from '../model/useResumes'
import { formatFileSize } from '../lib/format'
import type { Resume, ResumeStatus } from '../model/types'
Expand All @@ -28,7 +28,12 @@ export function ResumeList() {
)
}
if (data.length === 0) {
return null
return (
<EmptyState
title="아직 업로드한 이력서가 없어요"
description="위에서 PDF 이력서를 올리면 분석이 자동으로 시작됩니다."
/>
)
}

return (
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/shared/ui/EmptyState/EmptyState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { EmptyState } from './EmptyState'

describe('EmptyState', () => {
it('제목과 설명을 보여준다', () => {
render(<EmptyState title="아직 없어요" description="추가해보세요" />)
expect(screen.getByText('아직 없어요')).toBeTruthy()
expect(screen.getByText('추가해보세요')).toBeTruthy()
})

it('description 없으면 제목만 렌더한다', () => {
render(<EmptyState title="비었음" />)
expect(screen.getByText('비었음')).toBeTruthy()
})

it('action 영역(CTA)을 렌더한다', () => {
render(<EmptyState title="비었음" action={<button type="button">시작</button>} />)
expect(screen.getByRole('button', { name: '시작' })).toBeTruthy()
})
})
36 changes: 36 additions & 0 deletions frontend/src/shared/ui/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ReactNode } from 'react'

export type EmptyStateProps = {
title: ReactNode
description?: ReactNode
icon?: ReactNode
// CTA 등 액션 영역. 라우팅/도메인 의존을 피하려 호출부에서 ReactNode 로 주입한다.
action?: ReactNode
className?: string
}

// 목록/결과가 비었을 때의 표준 안내 카드(점선 테두리). 4-state 패턴의 'empty' 담당.
export function EmptyState({
title,
description,
icon,
action,
className = '',
}: EmptyStateProps) {
return (
<div
className={`flex flex-col items-center rounded-2xl border border-dashed border-border-strong bg-surface-raised p-10 text-center ${className}`}
>
{icon ? (
<div className="mb-3 text-fg-subtle" aria-hidden>
{icon}
</div>
) : null}
<p className="text-body font-medium text-fg-strong">{title}</p>
{description ? (
<p className="mt-1 text-caption text-fg-muted">{description}</p>
) : null}
{action ? <div className="mt-4">{action}</div> : null}
</div>
)
}
2 changes: 2 additions & 0 deletions frontend/src/shared/ui/EmptyState/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EmptyState } from './EmptyState'
export type { EmptyStateProps } from './EmptyState'
2 changes: 2 additions & 0 deletions frontend/src/shared/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export { ConfirmDialog } from './ConfirmDialog'
export type { ConfirmDialogProps } from './ConfirmDialog'
export { ToastViewport, toast, dismissToast } from './Toast'
export type { ToastTone, ToastItem } from './Toast'
export { EmptyState } from './EmptyState'
export type { EmptyStateProps } from './EmptyState'