[FE-27] 담당자 관리 섹션 · 시트

작업 내용 (설계 의도)

근거: 지원 관리 확장 FE 웹 설계 S-19 담당자 관리 — 지원 상세 내 섹션 · 컴포넌트 트리/applications/:id 서브트리 · 신규 공용 도메인 컴포넌트(ContactSheet) · API 연동 > 1단계의 S-19 행 · Single Writer per File 검증 > 단계 1 wave 2, 지원 관리 확장 TDD 1단계 — 담당자 (FR-80).

변경 사항

  • 지원 건에 붙는 담당자 정보를 등록·편집·삭제하는 섹션을 기존 지원 상세(S-07) 안에 추가합니다. 별도 화면을 만들지 않는 이유는 담당자가 지원 건에 종속된 부가 정보이기 때문입니다.
  • api/application/contacts.ts(POST·GET·PUT·DELETE)와 hooks/application/의 담당자 훅을 신규로 둡니다. hooks/application/의 기존 파일은 수정하지 않고 신규 파일만 추가합니다 — 같은 wave의 다른 티켓과 파일 교집합을 0으로 유지하기 위해서입니다.
  • 섹션(pages/application/ContactSection.tsx)과 시트(components/application/ContactSheet.tsx)를 나눕니다. 섹션은 화면 전용이라 props만 받고, 시트는 추가·편집 양쪽에서 열리므로 자기 mutation을 소유합니다(컴포넌트 트리의 시트 예외 규칙).
  • 시트 하나가 추가와 편집을 겸합니다. 폼 구성이 동일한데 화면을 둘로 나누면 검증 로직이 복제됩니다. 삭제는 편집 모드에서만 시트 하단의 danger 텍스트 버튼으로 노출합니다.
  • 삭제는 확인 단계 없이 즉시 실행하되 토스트에 [실행 취소]를 두지 않습니다 — 계약에 복원 API가 없어서 되돌릴 수 없기 때문입니다. 대신 danger 텍스트 버튼이라 오탭 가능성이 낮습니다.
  • 이메일을 FE에서 소문자화하지 않습니다. 서버가 소문자 정규화해 저장하므로 FE는 입력값을 그대로 보내고 응답 값을 표시합니다. FE가 미리 변형하면 화면의 값과 저장된 값이 어긋난 것처럼 보이는 순간이 생깁니다.
  • 빈 상태 문구에 FR-91(3단계 담당자 일치 20점 축)의 전제임을 안내합니다 — “이메일을 등록하면 연락 이벤트를 이 지원 건과 연결할 수 있어요”. 지금 당장 쓸모가 없어 보이는 입력에 등록 동기를 만들어야 3단계에서 매칭할 데이터가 있습니다.
  • 오류는 층위를 나눕니다. 조회 실패는 섹션 내 축소 ErrorState(지원 상세의 다른 섹션은 살립니다), 저장 400 VALIDATION_FAILED는 해당 필드 인라인, 404 APPLICATION_NOT_FOUND는 토스트 + 지원 목록으로 이동.
  • CRUD 성공 시 ['applications', applicationId, 'contacts']를 무효화합니다.
  • pages/application/ApplicationDetailPage.tsx에 섹션을 추가합니다. 이 파일은 단계 1에서 이 티켓만 수정합니다(2단계 제출 서류 섹션은 FE-37, 다른 단계).
  • 메모 입력은 FE-20이 만든 TextArea 프리미티브를 씁니다.

의존

  • FE-20 — ApplicationContactResponse·SaveApplicationContactRequest 타입, ['applications', id, 'contacts'] queryKey, 담당자 CRUD MSW 목, TextArea 프리미티브.
  • FE-22 — 지원 상세가 인증 게이트 뒤에 배치된 라우트 골격.
  • BE-49 — 담당자 API. 통합 검증은 BE-49 완료 후 수행합니다.

다이어그램

처리 흐름

sequenceDiagram
    participant User as 사용자
    participant Section as ContactSection
    participant Sheet as ContactSheet
    participant Mutation as useSaveApplicationContact
    participant Api as contacts API
    User->>Section: [+ 추가] 탭
    Section->>Sheet: 빈 폼으로 열기
    User->>Sheet: 이름·이메일 입력 후 저장
    Sheet->>Mutation: POST 요청
    Mutation->>Api: 입력값 그대로 전송
    Api-->>Sheet: 201 정규화된 이메일

컴포넌트 의존

flowchart LR
    Detail[ApplicationDetailPage] --> Section[ContactSection]
    Section --> List[useApplicationContacts]
    Section --> Sheet[ContactSheet]
    Sheet --> Save[useSaveApplicationContact]
    Sheet --> Delete[useDeleteApplicationContact]
    Sheet --> Area[TextArea FE-20]
    List --> Api[api/application/contacts]
    Save --> Api
    Delete --> Api

테스트 케이스

  • 담당자가 있으면 이름·소속·이메일·전화번호가 목록으로 렌더된다.
  • [+ 추가]로 시트를 열어 이름만 입력해도 저장되고 목록이 갱신된다.
  • 담당자 0건이면 빈 상태 문구와 함께 3단계 FR-91 안내 문구가 보인다.
  • 목록 항목을 탭하면 시트가 기존 값이 채워진 편집 모드로 열린다.
  • 편집 모드에서만 담당자 삭제 버튼이 보이고, 추가 모드에서는 보이지 않는다.
  • 삭제하면 확인 단계 없이 즉시 실행되고 목록에서 사라진다.
  • 대문자가 섞인 이메일을 입력하면 요청 본문이 입력값 그대로 전송되고, 화면에는 응답의 정규화된 값이 표시된다.
  • 400 VALIDATION_FAILED 응답 시 해당 필드 아래 인라인 에러가 보이고 시트가 닫히지 않는다.
  • 404 APPLICATION_NOT_FOUND 응답 시 토스트가 뜨고 지원 목록으로 이동한다.
  • 담당자 조회가 실패해도 지원 상세의 다른 섹션은 정상 렌더되고 담당자 섹션만 축소 에러가 보인다.
  • 저장 성공 시 담당자 쿼리가 무효화된다.
  • 담당자 섹션과 시트를 다크 모드로 렌더하면 시맨틱 토큰 class만 사용하고 하드코딩 색이 0건이다.