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
18 changes: 17 additions & 1 deletion frontend/src/features/interview/ui/live/InterviewStage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { StatusBadge } from '@/shared/ui/StatusBadge'
import { Button } from '@/shared/ui/Button'
import { ConfirmDialog } from '@/shared/ui/ConfirmDialog'
import { isQuestion, isTranscribing, sessionProgress } from '@/domain/session'
import type { Session } from '@/domain/session'
import type { ConnectionStatus, ThreadItem } from '../../model/useLiveInterview'
Expand Down Expand Up @@ -75,6 +76,7 @@ export function InterviewStage({
onDeliveryModeChange: (mode: DeliveryMode) => void
}) {
const [transcriptOpen, setTranscriptOpen] = useState(false)
const [endConfirmOpen, setEndConfirmOpen] = useState(false)
const progress = sessionProgress(session)
const currentQuestion = [...items].reverse().find(isQuestion)
const lastItem = items[items.length - 1]
Expand Down Expand Up @@ -116,7 +118,7 @@ export function InterviewStage({
<Button variant="ghost" size="sm" onClick={() => setTranscriptOpen(true)}>
기록
</Button>
<Button variant="danger" size="sm" onClick={onEnd}>
<Button variant="danger" size="sm" onClick={() => setEndConfirmOpen(true)}>
종료
</Button>
</div>
Expand Down Expand Up @@ -169,6 +171,20 @@ export function InterviewStage({
onClose={() => setTranscriptOpen(false)}
/>
)}

<ConfirmDialog
open={endConfirmOpen}
title="면접을 종료하시겠습니까?"
description="진행 중인 면접이 끝나고 피드백 단계로 넘어갑니다. 이 작업은 되돌릴 수 없습니다."
confirmLabel="종료"
cancelLabel="계속 진행"
danger
onConfirm={() => {
setEndConfirmOpen(false)
onEnd()
}}
onCancel={() => setEndConfirmOpen(false)}
/>
</section>
)
}
17 changes: 15 additions & 2 deletions frontend/src/features/repo/ui/RepoList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { isApiError } from '@/shared/api'
import { useAnalysisProgress } from '@/shared/hooks'
import { StatusBadge, type StatusTone } from '@/shared/ui'
import { ConfirmDialog, StatusBadge, type StatusTone } from '@/shared/ui'
import {
useDeleteRepository,
useRegisteredRepositories,
Expand Down Expand Up @@ -68,6 +69,7 @@ function RepoCard({
onDelete: () => void
}) {
const meta = STATUS_META[repo.status]
const [confirmOpen, setConfirmOpen] = useState(false)
const progress = useAnalysisProgress('REPOSITORY', repo.id)
// 진행 문구는 분석 진행 중일 때만 의미 있다. 완료/실패 시 store 가 clear 되지만 방어적으로 가드.
const showProgress =
Expand Down Expand Up @@ -95,14 +97,25 @@ function RepoCard({
<button
type="button"
disabled={deleting}
onClick={onDelete}
onClick={() => setConfirmOpen(true)}
aria-label="레포지토리 삭제"
className="shrink-0 rounded-md p-1 text-fg-subtle opacity-0 transition-colors duration-fast hover:bg-surface hover:text-danger-700 focus-visible:opacity-100 group-hover:opacity-100 disabled:opacity-40"
>
<TrashIcon />
</button>
</div>

<ConfirmDialog
open={confirmOpen}
title="레포지토리를 삭제하시겠습니까?"
description={`'${repo.repoFullName}'을(를) 목록에서 삭제합니다. 이 작업은 되돌릴 수 없습니다.`}
confirmLabel="삭제"
danger
loading={deleting}
onConfirm={onDelete}
onCancel={() => setConfirmOpen(false)}
/>

<div className="mt-2 flex items-center gap-2">
<StatusBadge tone={meta.tone}>{meta.label}</StatusBadge>
<span className="truncate text-caption text-fg-muted">
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/features/resume/ui/ResumeList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { isApiError } from '@/shared/api'
import { useAnalysisProgress } from '@/shared/hooks'
import { StatusBadge, type StatusTone } from '@/shared/ui'
import { ConfirmDialog, 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 Down Expand Up @@ -54,6 +55,7 @@ function ResumeCard({
onDelete: () => void
}) {
const meta = STATUS_META[resume.status]
const [confirmOpen, setConfirmOpen] = useState(false)
const progress = useAnalysisProgress('RESUME', resume.id)
const showProgress =
!!progress &&
Expand All @@ -76,14 +78,25 @@ function ResumeCard({
<button
type="button"
disabled={deleting}
onClick={onDelete}
onClick={() => setConfirmOpen(true)}
aria-label="이력서 삭제"
className="shrink-0 rounded-md p-1 text-fg-subtle opacity-0 transition-colors duration-fast hover:bg-surface hover:text-danger-700 focus-visible:opacity-100 group-hover:opacity-100 disabled:opacity-40"
>
<TrashIcon />
</button>
</div>

<ConfirmDialog
open={confirmOpen}
title="이력서를 삭제하시겠습니까?"
description={`'${resume.originalFilename}'을(를) 삭제합니다. 이 작업은 되돌릴 수 없습니다.`}
confirmLabel="삭제"
danger
loading={deleting}
onConfirm={onDelete}
onCancel={() => setConfirmOpen(false)}
/>

<div className="mt-2 flex items-center gap-2">
<StatusBadge tone={meta.tone}>{meta.label}</StatusBadge>
<span className="truncate text-caption text-fg-muted">
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/shared/ui/ConfirmDialog/ConfirmDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ConfirmDialog } from './ConfirmDialog'

const base = {
title: '삭제하시겠습니까?',
description: '되돌릴 수 없습니다.',
onConfirm: () => {},
onCancel: () => {},
}

describe('ConfirmDialog', () => {
it('open=false면 렌더되지 않는다', () => {
render(<ConfirmDialog {...base} open={false} />)
expect(screen.queryByText('삭제하시겠습니까?')).toBeNull()
})

it('open이면 제목·설명을 보여준다', () => {
render(<ConfirmDialog {...base} open />)
expect(screen.getByText('삭제하시겠습니까?')).toBeTruthy()
expect(screen.getByText('되돌릴 수 없습니다.')).toBeTruthy()
})

it('확인/취소 버튼이 각각 콜백을 호출한다', async () => {
const onConfirm = vi.fn()
const onCancel = vi.fn()
render(
<ConfirmDialog
{...base}
open
confirmLabel="삭제"
cancelLabel="취소"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
)
await userEvent.click(screen.getByRole('button', { name: '삭제' }))
expect(onConfirm).toHaveBeenCalledOnce()
await userEvent.click(screen.getByRole('button', { name: '취소' }))
expect(onCancel).toHaveBeenCalledOnce()
})

it('loading이면 취소가 비활성화되고 확인은 막힌다', async () => {
const onConfirm = vi.fn()
render(
<ConfirmDialog
{...base}
open
loading
confirmLabel="삭제"
cancelLabel="취소"
onConfirm={onConfirm}
/>,
)
expect(screen.getByRole('button', { name: '취소' })).toBeDisabled()
// loading 시 Spinner가 접근성 이름에 더해질 수 있어 부분 일치로 찾는다.
const confirm = screen.getByRole('button', { name: /삭제/ })
expect(confirm).toBeDisabled()
await userEvent.click(confirm)
expect(onConfirm).not.toHaveBeenCalled()
})
})
53 changes: 53 additions & 0 deletions frontend/src/shared/ui/ConfirmDialog/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ReactNode } from 'react'
import { Modal } from '../Modal'
import { Button } from '../Button'

export type ConfirmDialogProps = {
open: boolean
title: ReactNode
description?: ReactNode
confirmLabel?: string
cancelLabel?: string
danger?: boolean
loading?: boolean
onConfirm: () => void
onCancel: () => void
}

// 파괴적/되돌릴 수 없는 액션 전 확인을 받는 공용 다이얼로그.
// Modal(ESC·포커스 복원·스크롤 락 내장) 위에 표준 취소/확인 푸터를 얹는다.
export function ConfirmDialog({
open,
title,
description,
confirmLabel = '확인',
cancelLabel = '취소',
danger = false,
loading = false,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
return (
<Modal
open={open}
onClose={loading ? () => {} : onCancel}
title={title}
footer={
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel} disabled={loading}>
{cancelLabel}
</Button>
<Button
variant={danger ? 'danger' : 'primary'}
onClick={onConfirm}
loading={loading}
>
{confirmLabel}
</Button>
</div>
}
>
<p className="text-body text-fg-muted">{description}</p>
</Modal>
)
}
2 changes: 2 additions & 0 deletions frontend/src/shared/ui/ConfirmDialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ConfirmDialog } from './ConfirmDialog'
export type { ConfirmDialogProps } from './ConfirmDialog'
2 changes: 2 additions & 0 deletions frontend/src/shared/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { StatusBadge } from './StatusBadge'
export type { StatusBadgeProps, StatusTone } from './StatusBadge'
export { Modal } from './Modal'
export type { ModalProps } from './Modal'
export { ConfirmDialog } from './ConfirmDialog'
export type { ConfirmDialogProps } from './ConfirmDialog'