[FE-14] 지원 상태 변경 시트 — 허용 전이만 제시
작업 내용 (설계 의도)
근거 설계: 20260722-공고알림앱-design-fe-web.md — “S-08 상태 변경 시트”, “방안 3 상태 전이를 UI로 어떻게 드러낼 것인가”
근거 요구: FR-43(전이 표) · FR-44(OFFERED는 ACCEPTED·OFFER_DECLINED만) · FR-45(WITHDRAWN은 앞 3단계만) · FR-47(전이 이력)
변경 사항
“허용되지 않는 전이는 UI에서 아예 제시하지 않는다”가 이 티켓의 존재 이유입니다.
Kanban 드래그(Huntr 방식)를 미채택한 근거: 드래그는 모든 열을 물리적으로 제시하므로 요구와 충돌합니다. 전 상태를 나열하고 불가한 것을 disabled 처리하는 방식도 미채택입니다 — disabled 항목은 “왜 안 되는지” 설명을 요구하고, 종료 상태에서는 전체가 disabled인 무의미한 화면이 됩니다.
핵심 설계 의도:
- 현재 상태에서 갈 수 있는 상태만 라디오로 나열합니다.
OFFERED에서는ACCEPTED·OFFER_DECLINED2개만 보이고WITHDRAWN·REJECTED는 목록에 존재하지 않습니다(FR-44). - 허용 전이가 0건(종료 상태)이면 이 시트는 열리지 않습니다 — 호출부(FE-16)가 CTA 자체를 렌더하지 않습니다. 시트는 방어적으로 빈 목록을 렌더하지 않고 개발 시 조기 실패하도록 처리합니다.
REJECTED선택 시 확인 문구가 현재 상태에 따라 자동으로 바뀝니다 — “‘면접’ 단계에서 불합격으로 기록돼요”. 탈락 단계를 입력받지 않습니다: BE가 이전 상태로부터rejectedAtStage를 자동 기록하므로(BE TDD 상태 전이 표) 입력을 늘리지 않고 확인만 제공합니다(Minimum Input).- 종료 상태 선택 시 “변경하면 되돌릴 수 없어요” 경고를 CTA 위에 인라인 표시합니다. 별도 확인 다이얼로그를 겹치지 않습니다 — 시트 위 다이얼로그 중첩은 토스 패턴에 반합니다.
- 409를 최종 방어선으로 신뢰합니다. 클라이언트 전이 맵(FE-03)은 UX 최적화이지 권한 판정이 아닙니다. 409 수신 시 “상태가 이미 바뀌었어요” 안내 + 상세 자동 refetch → 선택지가 갱신됩니다(낙관적 잠금 충돌 포함).
- 전이 규칙 SSOT: 계약 요청 #7(
allowedNextStatuses응답 포함)이 수용되면 서버 값을 우선 사용하고, 미수용 시 FE-03의 상수 맵을 폴백으로 씁니다. 응답에 값이 있으면 항상 서버 값이 이깁니다. - 낙관적 업데이트를 적용하지 않습니다 — 전이 가능 여부 판정이 서버에 있고, 낙관적으로 상태를 바꿨다가 409로 되돌리면 사용자가 “됐다가 안 됐다”를 보게 됩니다.
범위: src/components/application/TransitStatusSheet.tsx, src/api/application/transition.ts, src/hooks/application/useTransitApplicationStatus.ts.
시트는 자기 mutation을 소유합니다(설계 “컨테이너/프레젠테이션 분리”의 명시된 예외) — 트리거 위치와 무관하게 재사용 가능해야 하기 때문입니다. 조립은 FE-16이 담당합니다.
의존
- FE-05 (ApplicationStatusChip), FE-03 (허용 전이 맵·한글 표기)
- BE 의존: BE-14 (상태 전이 API)
다이어그램
처리 흐름
sequenceDiagram participant U as 사용자 participant S as TransitStatusSheet participant A as allowedNextStatuses participant M as useTransitApplicationStatus participant Q as Query 캐시 participant Sv as 서버 S->>A: currentStatus (서버 값 우선 · 상수 폴백) A-->>S: 갈 수 있는 상태만 U->>S: "불합격" 선택 S-->>U: "'면접' 단계에서 불합격으로 기록돼요" + 되돌릴 수 없음 경고 U->>S: 변경하기 S->>M: mutate(nextStatus, memo?) M->>Sv: POST /api/applications/{id}/status-transitions alt 409 전이 불가 또는 잠금 충돌 Sv-->>M: 409 M->>Q: 상세 무효화 (refetch) S-->>U: "상태가 이미 바뀌었어요" + 선택지 갱신 else 성공 Sv-->>M: {status, rejectedAtStage?, historyId} M->>Q: 상세 무효화 S-->>U: 시트 닫힘 + 토스트 end
클래스 의존
flowchart LR subgraph Sheet["components/application"] T[TransitStatusSheet] end subgraph Hooks["hooks/application"] H[useTransitApplicationStatus] end subgraph Api["api/application"] A[transition.ts] end subgraph Constants["constants"] Map[ALLOWED_TRANSITIONS] Label[statusLabel] end subgraph Ui["components/ui · domain"] BS[BottomSheet] TF[TextField] Btn[Button] SC[ApplicationStatusChip] end T --> H T --> Map T --> Label T --> BS T --> TF T --> Btn T --> SC H --> A
테스트 케이스
- 현재 상태가
APPLIED이면DOCUMENT_SCREENING·REJECTED·WITHDRAWN3개만 렌더된다 - 현재 상태가
INTERVIEWING이면OFFERED·REJECTED·WITHDRAWN3개만 렌더된다 - 현재 상태가
OFFERED이면ACCEPTED·OFFER_DECLINED2개만 렌더되고WITHDRAWN이 목록에 없다 - 현재 상태가
OFFERED일 때REJECTED도 목록에 없다 - 현재 상태와 같은 상태로의 전이 선택지가 렌더되지 않는다
REJECTED선택 시 확인 문구에 현재 상태의 한글 표기가 포함된다 (“‘면접’ 단계에서”)- 현재 상태가
DOCUMENT_SCREENING일 때REJECTED선택 문구가 “‘서류전형’ 단계에서”로 바뀐다 - 탈락 시점 단계를 입력받는 필드가 존재하지 않는다
- 종료 상태를 선택하면 “되돌릴 수 없어요” 경고가 CTA 위에 렌더된다
- 종료 상태 선택 시 별도 확인 다이얼로그가 열리지 않는다
- 아무것도 선택하지 않으면 “변경하기” 버튼이 비활성이다
- 전이 성공 시 시트가 닫히고 상세 쿼리가 무효화된다
- 409 수신 시 시트가 유지되고 안내 문구가 표시되며 상세가 refetch된다
- 409 후 갱신된 상태에 맞춰 선택지가 다시 계산된다
- 서버 응답에
allowedNextStatuses가 있으면 상수 맵보다 우선 사용된다 - 500 수신 시 선택 상태가 보존된 채 에러 토스트가 표시된다
- 제출 중 라디오와 CTA가 비활성화된다
2차 갱신 반영 (2026-07-22) — 서버 allowedNextStatuses 우선 (계약 응답 #7)
- BE가 상세·전이 응답에
allowedNextStatuses[]를 포함하므로, 시트는 서버가 내려준 값을 그대로 선택지로 씁니다. FE-03의 전이 맵 상수는 서버 값이 없을 때만 쓰는 폴백으로 격하됩니다. - 전이 맵 동기화 주석·전수 검증 테스트를 삭제합니다 — SSOT가 서버로 이동했습니다.
- 409 방어선은 유지합니다 — 클라이언트가 서버 값으로 선택지를 그려도, 동시 전이·규칙 변경 시 서버가 409를 반환할 수 있으므로 409 → 안내 + 상세 refetch → 선택지 갱신 흐름은 그대로입니다.
수정·추가 테스트 케이스
- 서버 응답의
allowedNextStatuses가 시트 선택지로 그대로 렌더된다 - 서버 값이 없으면 폴백 상수로 선택지를 계산한다
- 전이 맵 전수 동기화 검증 테스트는 존재하지 않는다 (SSOT가 서버)
- 409 수신 시 상세 refetch로 서버의 최신
allowedNextStatuses가 다시 반영된다