Develop/be - #1942
Conversation
사용자 API를 /api/feedback이 아니라 /api/student/feedback 아래에 둔다. JwtAuthenticationFilter가 /api/student, /auth/student, /api/v2/fcm만 건너뛰므로, /api/feedback에 학생 토큰을 붙이면 필터가 동작해 CustomUserDetailService가 UUID를 사용자로 조회하다 실패한다. 이 예외는 필터 안에서 잡히지 않아 GlobalExceptionHandler도 타지 못하고 매 요청 500이 된다. - 도메인: Feedback(보낸 편지) / Letter(받은 편지) - 사용자 API: 피드백 전송, 받은/보낸 편지 목록·상세, 읽음 처리 - 첨부 사진: 활동사진과 같은 presigned 방식. 저장 시점에 장수·소유 경로· 실제 업로드 여부·용량을 R2에서 다시 검증한다 - 운영 API: 피드백 목록, 답장 발행(FCM), 전체 편지 발행, 초안 임시저장 - 개발자 포털: 받은 피드백 섹션 (홍보 게시판 구조 참고) 초안은 발행된 편지와 다른 컬렉션에 둔다. 같은 컬렉션에 상태 플래그로 섞으면 받은 편지함 쿼리에 조건 하나만 빠져도 쓰다 만 글이 전체 사용자에게 노출되기 때문이다. 보낸 사람 식별자는 학생 UUID 앞 8자리를 쓴다. 시안의 4자리 형식은 제보자 200명에서 충돌 확률이 86%라 서로 다른 학생이 같은 ID로 보인다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
유형 · 받은 날짜 · 상태 컬럼이 짧은 한국어라 우측 답장 패널(370px)에 폭을 내주면 "문제 신/고", "답장 대/기" 처럼 글자 단위로 쪼개졌다. 해당 컬럼만 nowrap으로 고정한다. 푸시 체크박스는 정렬을 강제하는 규칙을 찾지 못했으나 화면에서 우측 두 줄로 깨져 있어, flex 시작 정렬과 text-align을 명시하고 체크박스가 줄지 않도록 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
flex로 두면 폭이 좁아졌을 때 라벨이 글자 단위로 세로로 쪼개졌다. 포털의 다른 폼과 같은 일반 인라인 흐름으로 바꾼다. 내용 컬럼이 320px을 차지해 상태 컬럼이 잘리고 가로 스크롤이 생기던 것도 220px로 줄인다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
index.html:61의 `input, select, textarea { width:100%; max-width:400px;
display:block; }`가 체크박스에도 적용돼, 체크박스가 400px 블록으로
한 줄을 통째로 차지하고 라벨이 다음 줄로 밀렸다. macOS에서는 그 블록의
가운데에 체크 글리프가 그려져 위치까지 어긋나 보였다.
포털의 유일한 체크박스라 이 클래스 안에서만 width/display/padding을
되돌린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
웹뷰 토큰 주입 전에는 웹이 자체 발급한 studentId로 피드백이 저장되어 StudentUser를 찾을 수 없다. 답장 푸시 대상이 없으므로 켜둔 채로 발행하면 매번 "푸시는 전송되지 않았습니다" 경고가 뜬다. 실패가 아닌데 실패로 보인다. 전체 편지 발행 푸시는 전체 토큰 멀티캐스트라 studentId 매칭이 없고 지금도 정상 동작하므로, 그때 운영자가 켜서 쓰면 된다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- content를 @NotNull에서 @notblank로 변경. 공백 10자가 검증을 통과해 저장됐다. - 상태 변경 API가 REPLIED를 직접 지정하거나 이미 답장한 피드백을 되돌리지 못하게 막는다. replyLetterId · repliedAt 없이 답장 완료가 되거나, 답장 후 되돌렸을 때 답장 연결 정보가 남는 문제를 차단한다. - 목록 조회가 전부 createdAt 내림차순이라 단일 필드 인덱스로는 정렬을 함께 처리하지 못한다. Feedback · Letter에 복합 인덱스를 추가하고 접두사가 겹치는 단일 @indexed는 제거한다. - 포털에서 새 편지 모드로 들어가거나 선택을 해제할 때 이전 초안의 분류·푸시 설정이 남던 것을 초기화한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
발행 요청이 타임아웃된 뒤 재시도하면 편지가 두 번 만들어지고 전체 푸시도 두 번 나갔다. 답장은 FEEDBACK_ALREADY_REPLIED로 막히지만 전체 편지에는 방어가 없었다. requestId를 받아 같은 값이면 처음 결과를 그대로 돌려주고, 조회만으로는 동시 요청이 둘 다 통과하므로 유니크 인덱스로 최종 방어한다. 재시도 응답이 거짓말하지 않도록 푸시 성공 수를 편지에 남긴다. 학생 토큰은 /auth/student로 누구나 무제한 발급받을 수 있어, 제한이 없으면 피드백을 만들지 않고도 presigned URL만 받아 공개 버킷에 임의 파일을 올릴 수 있었다. studentId와 IP 양쪽에 10분 40건 창을 건다. studentId는 새로 발급받아 우회할 수 있으므로 IP 창을 함께 둔다. 기존 통계 수집의 Redis 스크립트 방식을 그대로 따랐다. 답장 푸시가 트랜잭션 안에서 나가 커밋이 실패하면 존재하지 않는 답장의 알림이 갈 수 있었다. 저장 구간만 TransactionTemplate으로 감싸고 푸시를 밖으로 뺀다. 응답의 pushSent를 동기로 유지하기 위해 afterCommit 이벤트 대신 이 방식을 썼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Valid는 @RequestBody List<T>의 요소까지 내려가지 않는다. 확인 결과 단일 객체는 위반 1건, 같은 값을 담은 리스트는 0건이었다. 따라서 UploadUrlRequest의 @pattern이 동작하지 않고 contentType이 검증 없이 presigned 서명에 들어갔다. 서명된 contentType은 R2가 응답 헤더로 되돌려주므로 text/html을 통과시키면 업로드한 파일이 CDN 도메인에서 그대로 실행된다. 키가 .png로 끝나도 Content-Type이 우선한다. 파일명 확장자만 보던 검사에 contentType 허용 목록을 추가한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
편지 한 건이 사진 4장이므로 10분에 5번 작성할 수 있는 여유가 남는다. IP당 최악 상한이 400MB/10분에서 200MB/10분으로 줄어든다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
requestId가 빈 문자열이면 hasText 검사로 조회는 건너뛰면서 값은 그대로 저장돼, sparse 유니크 인덱스가 걸러주지 못했다. 필드가 있는 문서는 인덱싱되므로 두 번째 발행이 중복 키로 500이 됐다. 공백은 null로 정규화하고 조회와 저장에 같은 값을 쓴다. 조회 후 저장 사이에 동시 요청이 끼면 둘 다 조회를 통과한다. 유니크 인덱스가 중복 저장은 막지만 진 쪽이 오류 응답을 받았다. DuplicateKeyException을 잡아 먼저 저장된 편지 결과를 돌려주고 푸시는 보내지 않는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[feature] 우체통 백엔드와 개발자 포털 화면 추가
POST /auth/student에 @RequestBody가 없어 호출할 때마다 새 UUID로 studentId가 발급됐다. 우체통은 로그인이 없어 신원이 JWT의 sub에만 있으므로, 서명 키를 교체해 전 사용자 토큰이 무효가 되는 순간 Feedback.studentId와의 연결이 끊겨 편지함이 모두 사라진다. 선택 본문 { "sub": "..." }를 받아 같은 신원으로 재발급한다. - @RequestBody(required = false)로 본문 없이 POST하는 기존 웹과 호환 - sub는 UUIDv4만 허용(형식 위반 400). JwtAuthenticationFilter가 sub로 UserDetails를 조회하므로 임의 문자열을 허용하면 관리자 계정 sub로 서명된 토큰을 받아낼 수 있다 - sub는 소문자로 정규화. Feedback.studentId는 문자열 그대로 비교하므로 대소문자만 다르면 편지함이 둘로 갈린다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[fix] 학생 토큰 재발급 시 기존 sub를 받아 신원 유지
답장·전체 발행 푸시 payload에 type과 letterId만 있어 앱의 NAVIGATE_WEBVIEW 분기에 걸리지 않았다. 알림을 탭해도 편지 상세로 가지 못하고 앱 홈만 열린다. action과 path를 추가한다. path는 프론트 라우트 (/feedback/letters/:letterId)와 정확히 같아야 웹뷰가 매칭한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[fix] 편지 푸시에 웹뷰 라우팅 정보 추가
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 59 minutes Limit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (48)
Warning
|
🚀 릴리즈 PR
📦 버전 정보
💾 BE➕ MINORvX.Y.0📖 버전 라벨 선택 가이드 (Semantic Versioning)
🚨 MAJORv1.0.0→v2.0.0➕ MINORv1.0.0→v1.1.0🔧 PATCHv1.0.0→v1.0.1📋 포함된 변경사항
1. 📮 모아동 우체통 백엔드 신규 (#1908 외)
학생이 피드백을 보내고, 운영자가 답장하거나 전체 편지를 발행하는 기능 전체가 추가되었습니다. (
moadong.feedback패키지 신설)학생 API —
/api/student/feedbackPOST/api/student/feedbacksub기준 저장)POST/api/student/feedback/images/upload-urlGET/api/student/feedback/receivedcategory필터 선택)GET/api/student/feedback/received/{letterId}PATCH/api/student/feedback/received/{letterId}/readGET/api/student/feedback/sentGET/api/student/feedback/sent/{feedbackId}운영자 API —
/api/admin/feedback(ROLE_DEVELOPER)GET/api/admin/feedbackPOST/api/admin/feedback/{feedbackId}/replyPATCH/api/admin/feedback/{feedbackId}/statusPOST/api/admin/feedback/lettersPOST/api/admin/feedback/letters/imagesPOSTGETPUTDELETE/api/admin/feedback/letters/drafts[/{draftId}]저장소 / 도메인
feedbacks,feedback_letters,feedback_letter_draftsstudent_created_at_idx,created_at_idx,recipient_created_at_idx,category_recipient_created_at_idx)904-x대역 신설 (피드백/편지 없음, 중복 답장, 발행 불가 카테고리, 상태 전이 불가, 업로드 레이트리밋)안정성 · 보안 보완 (리뷰 반영분 포함)
requestId유니크 인덱스로 중복 발행 차단, 빈requestId정규화 및 동시 발행 충돌 처리contentType을 서버에서 검증 (image/*허용 패턴) — 서명된 헤더로 임의 파일이 올라가는 경로 차단REPLIED는 답장 발행으로만 진입, 되돌리기 불가), 공백 본문 차단2. 🔔 편지 푸시 웹뷰 라우팅 추가 (#1940)
답장 · 전체 발행 푸시 payload에
type/letterId만 있어 앱의NAVIGATE_WEBVIEW분기에 걸리지 않아, 알림을 탭해도 앱 홈만 열리던 문제를 수정했습니다.action=NAVIGATE_WEBVIEW,path=/feedback/letters/{letterId}추가 (프론트 라우트와 정확히 일치)3. 🔐 학생 토큰 재발급 시 신원 유지 (#1926)
POST /api/student/token이 매번 랜덤 UUID를sub로 발급해, 토큰을 재발급받으면 편지함이 초기화되던 문제를 수정했습니다.sub(UUIDv4)를 담으면 같은 신원으로 재발급sub가 없으면 기존과 동일하게 새 신원 생성 → 이미 배포된 웹과 하위 호환sub는 소문자로 정규화해 대소문자 차이로 편지함이 갈라지는 것을 방지4. 🛠️ 개발자 포털 화면 (
/dev)input스타일 규칙 충돌로 깨지던 체크박스 정렬 · 테이블 줄바꿈 수정5. ✅ 테스트
신규 테스트 7종 추가 (총 1,239줄)
FeedbackServiceTest,FeedbackAdminServiceTest,FeedbackImageServiceTest,LetterDraftServiceTestLetterPreviewTest,FeedbackSerializationTest,StudentAuthControllerTest🔍 리뷰 포인트
path가 프론트 라우트(/feedback/letters/:letterId)와 계속 일치하는지