Skip to content

[Feat/#149] 학생회비 납부 API 연결 - #151

Merged
xeoxxn merged 24 commits into
mainfrom
feat/#149-fee-api-integration
Nov 4, 2026
Merged

xeoxxn merged 24 commits into
mainfrom
feat/#149-fee-api-integration

Conversation

@xeoxxn

@xeoxxn xeoxxn commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

학생회비 화면(납부내역 /my/fee, 계좌 송금 퍼널 /fee/transfer)과 행사 상세의 "학생회비 납부자 전용" 판단이 목데이터(feeMock.ts)로 돈다. 서버(billilge/stream-server#167)가 납부 흐름을 "토스로 송금한 뒤 남은 학기 수 + 학점이수현황 캡처를 한 번에 요청"으로 개편해서 앱 API가 확정됐다. 이걸 실제로 연결한다. 파일 업로드(presigned URL) 방식도 이번에 정했다.

❓ 왜 해결해야 하나요?

목데이터로는 납부 요청이 운영진에게 가지 않고, 송금 링크도 비어 있어 토스로 넘어갈 수 없다(지금은 항상 "토스를 열지 못했어요"). 납부 흐름 전체가 서버 연결을 기다리고 있다.

⭐ 어떻게 해결했나요?

쓰는 API — 사용자 앱 API만 (관리자 API는 건드리지 않음)

용도 요청 부르는 곳
내 납부 상태 GET /v1/app/fee/me 납부내역, 행사 상세, 송금 퍼널 진입
송금 링크 GET /v1/app/fee/me?remainingSemesters= 캡처 단계 "다음"
캡처 업로드 POST /v1/files/presigned-url → 스토리지 PUT 캡처 단계 "다음"
납부 요청 POST /v1/app/fee { remainingSemesters, proofImageKey } 송금 확인 "네, 송금했어요"

1. 공용 — 서버 일시 표시 (fec1a3d) — 서버 LocalDateTime은 "11:49:00.123456"까지 내려와서 formatDateTime이 시:분까지만 남긴다

2. 상태 이름을 서버 문자열로 (5e6a8cd) — checking·needs-check·paid → PENDING·REJECTED·PAID(+ 미납 UNPAID). 매핑 계층 없이 서버와 1:1이다. 배지 key와 홈 목데이터도 같이 바뀐다

3. 납부 상태 조회 (4c8cfd2) — feeQueries.me()를 실 API로, feeMock.ts 삭제

  • 캐시에는 응답(FeeResponse) 그대로 두고 화면이 select로 바꾼다 — 납부내역은 미납이면 null(빈 화면), 행사 상세는 PAID만 납부로 본다
  • 서버는 최신 요청 한 건만 줘서 납부내역도 카드 한 장이다. 카드 제목은 서버에 없어서 FEE_TITLE 상수

4. 송금 링크 (9ebba1b) — feeQueries.transferGuide(semester). 캡처 단계 "다음"에서 받아 토스 단계 context에 싣는다 — 토스 단계가 데이터를 기다리지 않는다. 링크가 null이면 기존 "토스를 열지 못했어요" 화면

5. 캡처 업로드 (267a47f) — shared/entities/files(useUploadFileMutation) 신설

  • "다음" = 업로드 + 송금 링크를 같이 기다린 뒤 토스 단계로. 송금하러 가기 전에 업로드 실패를 알린다
  • 스토리지 PUT은 apiClient를 거치지 않는다(baseURL·인증 헤더·봉투 해제가 붙으면 안 됨)
  • 서버는 형식을 MIME이 아니라 파일 이름 확장자로 판단하고 10MB로 막아서, 고를 때 먼저 거른다(isAcceptableTranscript)

6. 납부 요청 (6277c1b) — useSubmitFeeMutation. 성공하면 ["fee"] 무효화 후 완료 단계로 replace

7. 진입 차단 (665fb62) — 이미 요청한 학생(PENDING·PAID)이 퍼널로 들어오면 납부내역으로 보낸다(안 막으면 송금까지 다 하고 409를 받는다). 들어올 때의 상태만 본다 — 요청을 보내면 PENDING이 되는데 그때 완료 단계에서 쫓겨나면 안 되기 때문이다. 퍼널은 Suspense + FeeTransferSkeleton

8. 다시 납부하기 (0585f8d) — 확인필요(REJECTED) 카드 아래쪽에 "문의하기"와 같은 모양으로. 서버는 확인필요면 학기 수부터 다시 내게 한다

9. 뒤로가기 버그 수정 (af2504e) — 확인 중 발견. 캡처 File을 퍼널 context(history.state)에 담고 있었는데, Chromium은 File이 든 history.state를 뒤로가기로 되살리지 못하고 null로 돌려준다(문자열만 든 state는 그대로 돌아오는 것으로 따로 확인). 그래서 토스·송금 확인에서 뒤로 가면 퍼널이 첫 단계로 초기화됐다(#107부터 있던 구조). 파일은 퍼널 화면 state로 옮기고 context에는 proofImageKey·transferLinkUrl만 싣는다

10. 문서 (b403683, b31f162) — coding-style.md 데이터 로딩(파일 업로드), wds-component-usage.md 송금 퍼널·버튼 표

11. #154 구조에 맞춤 (9922fe0, b1ad775) — #154 브랜치를 합치고 회비·파일 코드를 새 구조로 옮겼다

  • entities/fee/feeApi.ts → shared/entities/fee/feeKeys.ts(root·me·transferGuide) + feeQueries.ts(feeQueries·FEE_ERROR_CODES·useSubmitFeeMutation), types.ts → feeTypes.ts, #154가 옮긴 feeMock.ts는 삭제
  • entities/files/filesApi.ts·types.ts → shared/entities/files/filesQueries.ts·filesTypes.ts(key 없음 — auth·search와 같은 예외)
  • 요청 코드 import는 @/shared/api(index)로만

12. main 병합 (923695c, e443885) — #154가 squash merge되고 빌릴게 용어 통일(#163)·로그인 연동(#172)·이메일 인증(#152)이 main에 들어와서 base를 main으로 바꾸고 합쳤다. 회비 변경분과 겹친 곳은 homeMock.ts의 아이콘 경로(billilge-items) 하나다. 로그인이 먼저 붙어서 coding-style.md의 "회비가 처음 붙인 도메인" 문구는 지웠다

13. zod 응답 검사 (d504e08, 5badfb2, d79306e) — #156(사물함)과 같은 방식이다. zod도 같은 버전(^4.6.7)이라 lockfile 추가분이 #156과 똑같다

  • feeQueries.ts에 FEE_STATUSES·feeResponseSchema, filesQueries.ts에 fileUploadUrlResponseSchema(z.looseObject — 서버가 필드를 더해도 통과). 요청은 api.get<unknown>(…)을 스키마의 parse로 감싼다
  • 타입은 feeTypes.ts·filesTypes.ts에서 z.infer로 만든다(FeeResponse·FeeStatus는 이름 그대로라 화면 코드는 그대로, FileUploadUrl → FileUploadUrlResponse)
  • 납부 요청(POST /v1/app/fee) 응답은 검사하지 않는다 — 응답을 쓰지 않고 무효화로 me를 다시 받아서(그쪽이 검사한다), 여기서 검사하면 이미 접수된 요청을 응답 모양 때문에 실패로 안내하게 된다
  • zod 사용 규칙 문서는 #156이 coding-style.md에 넣어서 여기선 건드리지 않았다(같은 줄을 고치면 충돌)

🧩 이 PR의 한계 & 트레이드오프

  • 실서버 확인은 다시 납부하기 흐름 하나 — dev 서버에 실제 토큰으로 확인필요 → 다시 납부하기 흐름을 확인했다(테스트 뒤 DB 원복). 나머지 상태·실패 경로는 서버 코드에 맞춘 응답을 브라우저에서 돌려줘서 확인했다(아래)
  • dev 서버에 학생회비 계좌가 없으면 GET /me·POST /fee가 404(FEE_ACCOUNT_NOT_FOUND)다. 관리자 페이지가 생기기 전까지는 dev DB(admin_config_values)나 관리자 API로 넣어야 한다
  • 에러 경계가 없다 — 조회가 실패하면(응답이 스키마와 달라도) React Router 기본 에러 페이지가 뜬다. 앱 공통 문제라 따로 다룬다. 스키마 실패(ZodError)는 ApiError가 아니라서 queryClient 재시도 규칙에 따라 한 번 더 요청한 뒤 실패한다
  • 토스 딥링크는 window.open 그대로 — 모바일 브라우저·앱 WebView 동작은 앱에서 따로 작업한다. 토스 앱이 실제로 송금 화면을 여는지는 기기에서 확인해야 한다
  • 송금 확인 단계에서 실패 토스트가 "네, 송금했어요" 버튼을 가린다(공용 ScreenToast 위치, 버튼이 두 줄인 화면) — 이번엔 손대지 않았다
  • Figma에 없는 화면 두 곳 — 미납 납부내역 빈 화면(사물함 빈 화면과 같은 그림), "다시 납부하기" 버튼("문의하기"와 같은 모양)
  • 캡처를 올린 뒤 "다음에 할게요"로 나가면 파일이 서버에 남는다(서버에 정리 배치가 없다)
  • 퍼널에서 새로고침하면 캡처 파일만 다시 골라야 한다(토스·송금 확인 단계는 파일 키로 계속 동작)

⛓️ 기존 기능에 미치는 영향

  • 행사 상세 — 회비 상태를 실서버에서 받는다(로그인 #172로 받은 토큰). 토큰이 만료돼 401이면 #172의 AuthEntryGate가 로그인으로 보낸다
  • 홈 — 목데이터의 상태 이름만 바뀌고 화면은 같다. 서버 홈 API(stream-server PR 홈 신청 카드 신청완료 숨김·사물함 제목 문구 #200)도 미납을 UNPAID로 맞추기로 했다
  • formatDateTime — 모든 도메인이 쓴다. 기존 목데이터("13:59")는 결과가 같다
  • 송금 퍼널 — 캡처 파일을 context에서 화면 state로 옮겼다(9번). 단계·주소(?fee.step=)는 그대로다

🔀 Edge Case & 실패 시나리오

실패 처리
캡처 형식·용량이 서버 정책 밖 고를 때 토스트, 파일을 받지 않음
캡처 업로드·송금 링크 조회 실패 캡처 단계에 머물러 토스트, 다시 "다음"
서버가 링크를 주지 않음(null) 토스 단계가 "토스를 열지 못했어요"
납부 요청 409 FEE_REQUEST_ALREADY_REQUESTED 응답을 못 받고 다시 누른 경우라 요청된 것으로 보고 완료 단계로(회비 캐시 무효화)
납부 요청 400 INVALID_PROOF_IMAGE 토스트 + 캡처 단계로 되돌리고 다시 올린다
그 밖의 실패(네트워크·5xx 등) 토스트 "납부 확인 요청에 실패했어요" — 이미 송금한 뒤라 "다시 송금하라"로 읽히지 않게
PENDING·PAID인데 퍼널 진입 납부내역으로

브라우저 확인 (dev 서버, 375×812)

  • 실서버(실제 토큰): 확인필요 → 다시 납부하기 흐름
  • 서버 코드에 맞춘 응답으로
    • 납부내역 4가지 상태(납부확인중·납부완료·확인필요·미납 빈 화면), 일시 2026.11.01 10:20
    • 확인필요 → 다시 납부하기 → 학기 수 9는 "다음" 비활성, 3은 활성
    • 확장자 없는 캡처 → 토스트
    • "다음" → presigned-url 본문 { category: "FEE_TRANSCRIPT_IMAGE", contentType, fileSize, originalName } → 스토리지 PUT(Content-Type: image/png, 인증 헤더 없음) → GET /me?remainingSemesters=3
    • 토스 이동 → 링크 supertoss://send?bank=…&accountNo=…&amount=30000으로 열고 송금 확인으로
    • 납부 요청 본문 { proofImageKey, remainingSemesters: 3 } — 성공·500·409·INVALID_PROOF_IMAGE 각각 위 표대로
    • 송금 확인 → 뒤로 → 토스(context 유지) → 뒤로 → 캡처(파일 유지) → 같은 파일로 "다음" 시 다시 올리지 않음
    • 완료 → "홈으로 가기"는 들어온 곳(납부내역)으로, 납부확인중으로 다시 조회됨
    • 납부확인중일 때 /fee/transfer → 납부내역
    • 행사 상세(납부자 전용): 납부확인중이면 "학생회비 납부자만 신청할 수 있어요"(비활성), 납부완료면 "신청하기"
  • zod 반영 후(main 병합 상태)
    • 확인필요 → 다시 납부하기 → 학기 수 → 캡처 "다음"(GET /me?remainingSemesters=3 · presigned-url · 스토리지 PUT) → 토스 링크 → 납부 요청 → 완료까지 그대로 동작
    • 스키마 검사: 서버가 필드를 더하면 통과, 모르는 feeStatus·숫자 자리에 문자열·필드 누락은 거절
    • presigned-url 응답에 fileKey가 없으면 스토리지 PUT을 보내지 않고 캡처 단계에 머물러 토스트
    • 납부 상태 응답이 깨지면 한 번 더 요청한 뒤 React Router 기본 에러 페이지(위 한계)

📋 검토한 대안과 선택 이유

  • 송금 링크 받는 시점 — 토스 단계에서 조회 vs 캡처 "다음"에서 업로드와 같이: 후자. 토스 단계가 로딩 없이 바로 뜨고, 실패 처리가 한 곳에 모인다
  • 캡처 업로드 시점 — 고르는 즉시 / "다음" / 송금 확인 제출 때: "다음". 송금 전에 실패를 알리면서, 고를 때마다 올리지 않아 남는 파일이 적다
  • 캡처 파일 보관 — history.state(기존) vs 퍼널 화면 state: 후자. 전자는 Chromium에서 뒤로가기로 state가 통째로 사라진다
  • 409 처리 — 납부내역으로 보내기 vs 완료 단계: 완료. 대부분 응답을 못 받고 다시 누른 경우라 요청은 이미 된 상태다
  • dev 계좌가 없을 때 클라에 목 송금 링크 — 링크만 대신할 뿐 GET /me·POST /fee의 404는 그대로라 화면이 깨진다. dev 쪽에 계좌를 넣는 게 맞다

💬 리뷰 포인트

  • [r] 진입 차단을 "들어올 때 상태"(useState 초기값)로만 판단하는 부분 — FeeTransferScreen.tsx
  • [c] 캐시엔 FeeResponse 그대로 두고 화면에서 select로 바꾸는 방식 — FeePaymentList(FeeResponse & { feeStatus: FeePaymentStatus }), EventsDetailContent
  • [c] 스토리지 PUT을 apiClient 밖에서 보내는 부분 — shared/entities/files/filesQueries.ts. #154의 "요청은 @/shared/api로" 규칙에 맞춰 shared/api 도우미로 옮길지
  • [c] 납부 요청 응답을 스키마로 검사하지 않는 부분 — feeQueries.ts의 useSubmitFeeMutation(13번)
  • [a] 토스트 문구 — FeeTransferScreen.tsx의 TOAST_MESSAGES

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: eb3a689f-1f15-49d9-830c-4b7fcdd42cc7
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@sangrae2325 sangrae2325 mentioned this pull request Nov 3, 2026
7 tasks
충돌은 회비 쪽 내용을 새 경로로 옮겨 풀었다(feeKeys·feeQueries 분리, feeMock 삭제, import 경로, coding-style 데이터 로딩 절).
@xeoxxn
xeoxxn changed the base branch from refactor/#153-shared-folder-structure to main November 4, 2026 12:14
@xeoxxn
xeoxxn marked this pull request as ready for review November 4, 2026 12:28
@xeoxxn
xeoxxn merged commit 71412b1 into main Nov 4, 2026
2 checks passed
@xeoxxn
xeoxxn deleted the feat/#149-fee-api-integration branch November 4, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

학생회비 납부 API 연결

3 participants