반복되는 UX 패턴과 상태 처리 규약. 컴포넌트가 아닌 상호작용 시나리오 단위.
모든 비동기 데이터 표시 영역은 다음 4가지 상태를 명시적으로 핸들링한다.
| 상태 | 컴포넌트 | 사용자에게 보여줄 것 |
|---|---|---|
loading |
Skeleton |
실제 컴포넌트 골격을 흉내낸 placeholder |
empty |
EmptyState |
아이콘 + 안내 + CTA ("이력서를 업로드하세요") |
error |
Alert 또는 EmptyState (variant=error) |
사용자 행동 가능한 메시지 + 재시도 버튼 |
success |
실제 데이터 |
구현 패턴: frontend/src/shared/lib/AsyncBoundary 사용.
<AsyncBoundary
pendingFallback={<ResumeListSkeleton />}
rejectedFallback={({ error, reset }) => <ResumeListError error={error} onRetry={reset} />}
>
<ResumeList />
</AsyncBoundary>empty는 데이터 fetch 후 컴포넌트 내부에서 분기 (boundary 책임 아님).
이력서·레포 분석 진행 상태는 다음 단계로 표현:
QUEUED → ANALYZING → ANALYZED
(대기) (진행 중) (완료)
↘
FAILED
UI 표현:
- List 화면:
StatusBadge(4색 매핑) - Detail 화면: 상단에
AnalysisStateIndicator(4단계 progress bar) + 마지막 메시지 표시 - 푸시 알림: SSE → Toast (
ANALYZED시 "분석이 완료되었습니다" + "면접 시작" CTA)
FAILED 시 재시도 버튼 + errorMessage 노출.
- 입력 시점에는 검증 안 함 (사용자 방해)
onBlur시점에 검증 + 에러 표시- 에러는 input 하단
--color-danger텍스트
- 제출 시 모든 필드 재검증
- 첫 번째 에러 필드로 자동 focus + scroll
- 서버 검증 실패 (422) → 해당 필드에
details.field매핑
- 라벨 옆
*(빨강) — 모든 필수 필드 - 안내문구는 placeholder가 아닌 helper text로
다음은 반드시 ConfirmDialog로 재확인:
- 회원 탈퇴
- 이력서 / 레포 삭제 (활성 세션에서 사용 중이면 추가 경고)
- 진행 중인 면접 세션 취소
- 분석 결과 재분석 (기존 결과 교체)
ConfirmDialog 메시지 톤:
"정말 삭제하시겠습니까?" "이 작업은 되돌릴 수 없습니다." (필요 시) 확인 버튼은
dangervariant + 동작 명시 ("삭제", "탈퇴", "취소")
| 상황 | Toast 사용 여부 | 대안 |
|---|---|---|
| API 성공 (CRUD) | ✓ success | 페이지 전환 시는 생략 가능 |
| API 실패 | ✓ error | inline 에러로 충분하면 생략 |
| 백그라운드 작업 완료 (분석) | ✓ info | |
| 폼 검증 실패 | ✗ | inline 에러 |
| 네트워크 오류 (offline) | ✓ warning + 재시도 |
위치: 우상단, 동시 최대 3개, 4초 자동 사라짐.
- 면접관 영역(좌측)에 large typography
- TTS 재생 시 자동으로 음성 출력 + 자막 동시 표시
- 사용자가 끝까지 듣지 않아도 답변 입력 가능
- 음성 모드: 마이크 버튼 → 녹음 시작, 실시간 STT 자막
- 텍스트 모드: textarea, Enter+Cmd 제출 (Enter 단독은 줄바꿈)
- 답변 제출 후 입력 영역 즉시 disable, "AI가 답변을 분석 중입니다…" 로딩
- 직전 답변 위에 새 질문 카드 추가 (역순 X — 자연스러운 대화 흐름)
- 부드러운 fade-in (200ms)
- 자동 scroll to 새 질문
- 사용자 명시 종료: 우상단 종료 버튼 → ConfirmDialog
- 자동 종료: 최대 질문/시간 도달 → "면접이 종료되었습니다" 모달 → 피드백 생성 페이지로 이동
| 위치 | 메시지 | CTA |
|---|---|---|
| 이력서 목록 | "아직 업로드한 이력서가 없습니다" | "이력서 업로드" |
| 레포 목록 | "면접에 사용할 레포지토리를 등록해주세요" | "GitHub에서 가져오기" |
| 세션 히스토리 | "첫 모의면접을 시작해보세요" | "면접 시작" |
| 검색 결과 없음 | "'{query}'에 대한 결과가 없습니다" | (없음) |
| 통계 부족 | "최소 2회 이상 면접을 완료하면 추이를 확인할 수 있습니다" | "면접 시작" |
| 단축키 | 동작 | 컨텍스트 |
|---|---|---|
Esc |
모달 / 드로어 / 팝오버 닫기 | 전역 |
Enter |
기본 액션 | form, dialog |
Cmd/Ctrl + Enter |
답변 제출 | 면접 textarea |
Cmd/Ctrl + K |
빠른 검색 (옵션) | 워크스페이스 |
Space |
마이크 toggle (push-to-talk 옵션) | 면접 진행 중 |
? |
단축키 목록 | 전역 |
- 면접 세션은 데스크탑 우선. 모바일 진입 시 "데스크탑 환경 권장" 배너 (dismissable)
- 외 페이지: 768px 이하에서 SideNav → Drawer로 전환, TopNav 햄버거 메뉴
- 404: "페이지를 찾을 수 없습니다" + 홈 이동 CTA
- 403: "접근 권한이 없습니다" + 로그인/홈 이동
- 500: "일시적인 오류가 발생했습니다" + 새로고침 + 문의 (traceId 표시)
- 오프라인: 전역 배너 + 자동 재연결 시도
- Phase 1: 한국어 only
- 모든 UI 문자열은
frontend/src/shared/i18n/ko.json에 키 기반으로 분리 (Phase 2 영어 확장 대비) - 형식: 평면 키 (
session.list.empty.title)