Repository navigation
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
github-actions
Bot
requested review from
jjunh33,
leegain1,
sangrae2325 and
tnals0924
October 25, 2026 12:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#️⃣연관된 이슈
🎯 해결하려는 문제가 무엇인가요?
학생회비 납부 기획과 화면(학생 앱·관리자 콘솔)이 바뀌었는데, 지금 API는 예전 흐름에 맞춰져 있습니다.
GET /v1/app/fee/me가grade·semester를 받아 "(8학기 − 이수 학기) × 학기당 회비"로 계산합니다. 휴학·초과학기 등으로 실제 남은 학기와 어긋날 수 있고, 근거 자료도 받지 않습니다UNPAID로 되돌릴 뿐 안내 문구를 남길 곳이 없습니다FEE_REQUEST_ALREADY_REVIEWED로 막혀 있어, 잘못 처리하면 고칠 방법이 없습니다PUT /link,PUT /amount두 API로 갈려 있는데, 화면은 한 폼에서 같이 저장합니다FEE_TRANSFER_LINK_INVALID)이 납니다. 서버 오류가 아니라 설정 누락입니다❓ 왜 해결해야 하나요?
바뀐 납부 흐름은 남은 학기 수 입력 + 학점이수현황 캡처 → 토스 송금 → "송금 완료했어요" 입니다. 학생이 실제로 결정하는 순간은 마지막 한 번이라 그때 요청을 한 번에 받아야 합니다. 운영진은 캡처와 금액을 보고 납부를 확인·반려·정정할 수 있어야 합니다.
또 후속 작업에서 회원의 납부 여부(
members.is_fee_paid)를 member 도메인이 스스로 맞추게 할 예정이라, 납부 여부가 바뀌는 순간을 기록해 둬야 합니다.⭐ 어떻게 해결했나요?
납부 흐름
GET /v1/app/fee/me?remainingSemesters=6이 금액과 토스 송금 링크를 계산해 줍니다. 아무것도 저장하지 않습니다POST /v1/app/fee로 남은 학기 수와 캡처 키를 보내면, 요청 시점 금액과 함께 새 요청 행이 바로 납부확인중(PENDING)으로 저장됩니다PAID)나 확인필요(REJECTED, 안내 문구 필수)로 처리합니다. 처리한 건도 다시 바꿀 수 있습니다상태와 저장 (
student_transfer_statuses)PENDING(납부확인중) /PAID(납부완료) /REJECTED(화면 표기 "확인필요")입니다. 한 번도 요청하지 않은 회원은 행이 없고, 앱 응답에서만feeStatus: "UNPAID"로 내려갑니다remaining_semesters,proof_image_key,amount(요청 시점 금액 — 운영진이 학기당 회비를 바꿔도 그대로),admin_note,requested_atrequested_at은 초 단위라 겹칠 수 있어 PK로 고릅니다REJECTED일 때만 받습니다. 그 외에는 409FEE_REQUEST_ALREADY_REQUESTED입니다active_member_id = IF(status IN ('PENDING','PAID'), member_id, NULL)생성 컬럼에 UNIQUE를 겁니다. 진행 중·완료 요청은 회원당 하나만 들어가고, 반려 행은 NULL이라 몇 개든 남습니다. 두 번째 INSERT의 제약 위반은saveAndFlush+ConstraintViolationUtil로 409로 바꿉니다(사물함 신청과 같은 패턴)FEE_REQUEST_NOT_LATEST입니다. 지난 요청을 되살리면 진행 중 요청이 둘이 될 수 있어서입니다사용자 API (
/v1/app/fee)GET /megrade·semester로 계산한 송금 링크와 상태remainingSemesters(선택, 1~8). 계좌·상태를 주고, 납부 전·확인필요인 학생이 학기 수를 보내면 금액·링크를 계산합니다. 납부확인중·납부완료면 요청 때 남긴 금액만 주고 링크는 주지 않습니다("또 보내라"로 읽히지 않게)POST /grade·semester→ 납부확인중{remainingSemesters, proofImageKey}→ 새 요청을 납부확인중으로 저장. 응답은GET /me와 같은FeeResponseFeeResponse:studentTransferStatusId,feeStatus,bank,accountNo,remainingSemesters,transferAmount,transferLinkUrl,requestedAt,confirmedAt(납부완료일 때의 처리 시각),adminNote(확인필요일 때)FEE_TRANSCRIPT_IMAGE카테고리인지까지 봅니다. 다른 용도로 올린 파일 키를 넘기는 것을 막기 위해서입니다. 이를 위해FileCategory.FEE_TRANSCRIPT_IMAGE와FileService.findByFileKey(+files.file_keyprefix 인덱스)를 추가했습니다관리자 API (
/v1/admin/fee)GET /requestsisStreamMember,remainingSemesters,transferAmount(요청 시점 값),proofImageUrl,confirmedAt,adminNote를 추가하고,status→feeStatus로 바꿨습니다PATCH /requests/{studentTransferStatusId}{status, adminNote}를 받습니다. 처리한 건도 다시 바꿀 수 있고PAID도 종결 상태가 아닙니다.REJECTED는 안내 문구 1~255자가 필수입니다. 최신 요청만 바꿀 수 있습니다GET /accountnull, 회비는 기본값입니다PUT /accountPUT /link와PUT /amount를 대체합니다. 은행은 허용 목록 22개(FeeBanks), 계좌번호는 숫자 10~14자리, 회비는 1 이상이어야 합니다payers명부 반영은 지금처럼 한 트랜잭션입니다납부 여부 변경을 아웃박스에 기록
core:common에MemberFeePaid,MemberFeePaymentCanceled이벤트를 추가했습니다 (memberId,studentTransferStatusId,occurredAt)PENDING → PAID는MemberFeePaid,PAID → REJECTED·PENDING(정정)은MemberFeePaymentCanceled이고, 나머지 전이는 남기지 않습니다payers동기화가AdminFeeReviewUseCase한 트랜잭션에서 함께 커밋·롤백됩니다.OutboxWriter는 트랜잭션 밖에서 부르면 바로 실패합니다에러 코드 (
FeeErrorCode)INVALID_REMAINING_SEMESTERS,INVALID_PROOF_IMAGE,INVALID_BANK,INVALID_ACCOUNT_NO,INVALID_TRANSFER_AMOUNT,INVALID_ADMIN_NOTE(400),FEE_ACCOUNT_NOT_FOUND(404),FEE_REQUEST_ALREADY_REQUESTED,FEE_REQUEST_NOT_LATEST(409)FEE_REQUEST_ALREADY_REVIEWED,FEE_TRANSFER_LINK_INVALID(500 → 계좌 미설정은FEE_ACCOUNT_NOT_FOUND404)INVALID_INPUT이 됩니다)마이그레이션 (V20~V26)
스택에서 만든 파일을 그대로 옮겼습니다. 아직 배포되지 않은 번호입니다.
V20remaining_semesters,proof_image_key,admin_note,requested_at) 추가,files.file_keyprefix(255) 인덱스V21UNPAID행을 만듭니다 —V25에서 다시 지웁니다(중간 설계의 흔적)V23NEEDS_CHECK→REJECTED이름 변경 (main 데이터에는 해당 행이 없습니다)V24amount BIGINT추가V25UNPAID정리: 운영진이 처리한 적 있는 행(main에서 반려된 행)은REJECTED로 남기고, 나머지(V21이 만든 행)는 지웁니다V26(member_id, student_transfer_status_id)인덱스, 진행 중 요청 유니크(생성 컬럼)main 데이터 기준 순효과:
PENDING·PAID행은 그대로이고, 운영진이 반려해UNPAID가 된 행은REJECTED(안내 문구 없음)가 됩니다. 새 컬럼은 기존 행에서NULL입니다.커밋 구성
최종 상태 기준으로 작업 단위 6개로 다시 쌓았습니다. 3번에서 서비스 시그니처가 바뀌므로 3·4번만 따로 체크아웃하면 api 모듈이 컴파일되지 않고, 5번부터 전체가 맞습니다.
chore: .env를 git 추적에서 제외feat: 학점이수현황 캡처 파일 카테고리와 키 조회 추가feat: 학생회비 납부 요청을 학기 인증과 함께 이력으로 쌓도록 개편— 도메인·DB·마이그레이션feat: 학생회비 납부 사용자 API 개편feat: 학생회비 납부 관리자 API 개편feat: 운영진 처리로 납부 여부가 바뀌면 아웃박스에 기록🧩 이 PR의 한계 & 트레이드오프
members.is_fee_paid컬럼·리스너)에 회원별 최신 요청이PAID인지로 채우는 백필이 반드시 필요합니다amount·remaining_semesters가 비어 있어 금액이null로 보이고,V25로REJECTED가 된 행은 안내 문구가 없습니다FeeAccountrecord가app-api와admin-api에 각각 있습니다.common-api는 welfare·internal을 모르는 모듈이라 공유할 자리가 없습니다. 설정 키(FeeConfigKeys)는 하나를 같이 써서 값이 어긋나지는 않습니다isStreamMember)를 행마다memberService.isSignedUp으로 묻습니다. 목록이 커지면 member 쪽 벌크 조회가 필요합니다./gradlew check통과 —ModularityTests·DomainImplAccessTests, 서버 전체 컨텍스트를 띄우는RentalApplyConcurrencyTest(MySQL 컨테이너에 FlywayV26까지 적용,ddl-auto=validate통과. R2 환경변수는 연결하지 않는 더미 값) 포함. 회비 쪽 단위 테스트는 없고, 동시 요청의 409 변환과 이벤트가outbox_events에 쌓이는지는 테스트로 확인하지 않았습니다⛓️ 기존 기능에 미치는 영향
GET /me파라미터(grade·semester→remainingSemesters)와 응답,POST /요청·응답PUT /link·PUT /amount제거 →PUT /account,GET /account신규, 목록·처리 응답 필드 변경, 상태 문자열UNPAID는 400이고REJECTED가 새로 생깁니다student_transfer_statuses에 같은 회원의 행이 여러 개 생깁니다. 이 테이블을 회원 기준으로 읽는 곳은 모두 최신 요청 기준으로 바꿨습니다.payers는 그대로 회원당 1행입니다saveAndFlush라 INSERT·UPDATE가 즉시 나갑니다(같은 트랜잭션이라 원자성은 그대로입니다)outbox_eventsINSERT가 최대 1건 늘어납니다🔀 Edge Case & 실패 시나리오
GET /me·POST /FEE_ACCOUNT_NOT_FOUND(저장 전에 막음)INVALID_REMAINING_SEMESTERSINVALID_PROOF_IMAGEPENDING·PAID인데 다시POSTFEE_REQUEST_ALREADY_REQUESTEDREJECTED2행 +PENDING1행. 화면·목록은 최신 행PENDING·PAID인데GET /me에remainingSemestersPATCHFEE_REQUEST_NOT_LATESTREJECTED로 바꾸며 안내 문구가 없거나 255자 초과INVALID_ADMIN_NOTEPAID → PAID,REJECTED → REJECTED(문구 수정)MemberFeePaid→MemberFeePaymentCanceled→MemberFeePaid3건 (occurredAt으로 순서 판별)payers동기화 실패📋 검토한 대안과 선택 이유
is_latest플래그 컬럼: 새 행마다 이전 행 UPDATE가 따라와 쓰기가 둘이 되고 서로 어긋날 여지가 생깁니다members에 회비 상태 사본 두기: 주인이 아닌 member에 welfare 상태를 심고, 같은 트랜잭션에서 맞출 책임을 양쪽에 지웁니다. 대신 아웃박스 이벤트로 member가 스스로 맞추게 합니다boolean paid/ 상태가 바뀔 때마다 발행: member는 납부 여부만 알면 되고, "냈다 / 취소됐다"가 각각 하나의 사실로 읽히는 쪽을 골랐습니다V21생성 →V25삭제 같은 흔적은 사라지지만, 최종 상태에 없던 변경을 새로 검증해야 해서 그대로 옮겼습니다💬 리뷰 포인트
[r]앱·관리자 API 계약 변경 — 프론트 배포 순서[r]마이그레이션 번호 —V22는 [Feat/#139] 열린피드백 관리자 API 추가 #140(열린피드백)이 씁니다. FlywayoutOfOrder가 꺼져 있어 이 PR이 먼저 머지되면 #140의V22를 뒤 번호로 옮겨야 합니다(#140이 먼저면 그대로 됩니다)[c]생성 컬럼 유니크로 동시 요청을 막는 방식과, 최신 요청만 처리하게 한 것[c]이벤트를 둘로 나눈 것과occurredAt에LocalDateTime을 쓴 것[a]FeeAccount중복 —common-api로 올릴지, 지금처럼 둘지