Skip to content

Develop/be - #1942

Merged
seongwon030 merged 15 commits into
mainfrom
develop/be
Aug 23, 2026
Merged

Develop/be#1942
seongwon030 merged 15 commits into
mainfrom
develop/be

Conversation

@seongwon030

@seongwon030 seongwon030 commented Aug 21, 2026

Copy link
Copy Markdown
Member

🚀 릴리즈 PR

📦 버전 정보

항목 내용
서비스 💾 BE
Bump 타입 ➕ MINOR
예상 버전 vX.Y.0

기존 API 스펙을 깨는 변경 없이 신규 엔드포인트(우체통)와 선택 필드(POST /api/student/token 본문)만 추가되어 MINOR 입니다.

⚠️ 반드시 라벨을 지정해주세요: 서비스 라벨(💾 BE, 💻 FE)과 버전 라벨(🚨 MAJOR, ➕ MINOR, 🔧 PATCH)이 없으면 태그가 생성되지 않습니다.

📖 버전 라벨 선택 가이드 (Semantic Versioning)
라벨 버전 변화 선택 기준 예시
🚨 MAJOR v1.0.0v2.0.0 기존 API/기능이 호환되지 않는 변경 API 엔드포인트 삭제/변경, 요청/응답 스펙 변경, DB 스키마 대규모 변경
➕ MINOR v1.0.0v1.1.0 기존 기능은 유지하면서 새 기능 추가 새 API 엔드포인트 추가, 새 기능 도입, 기존 API에 선택적 필드 추가
🔧 PATCH v1.0.0v1.0.1 기능 변경 없이 버그 수정/내부 개선 버그 수정, 성능 개선, 리팩토링, 문서 수정

📋 포함된 변경사항

이번 릴리즈의 핵심은 모아동 우체통(피드백/편지) 기능 신규 도입입니다.

1. 📮 모아동 우체통 백엔드 신규 (#1908 외)

학생이 피드백을 보내고, 운영자가 답장하거나 전체 편지를 발행하는 기능 전체가 추가되었습니다. (moadong.feedback 패키지 신설)

학생 API — /api/student/feedback

Method Path 설명
POST /api/student/feedback 피드백 보내기 (학생 토큰 sub 기준 저장)
POST /api/student/feedback/images/upload-url 첨부 사진 presigned 업로드 URL 발급 (최대 4장)
GET /api/student/feedback/received 받은 편지 목록 (category 필터 선택)
GET /api/student/feedback/received/{letterId} 받은 편지 상세 (REPLY면 원본 피드백 동봉)
PATCH /api/student/feedback/received/{letterId}/read 받은 편지 읽음 처리
GET /api/student/feedback/sent 보낸 편지 목록
GET /api/student/feedback/sent/{feedbackId} 보낸 편지 상세

운영자 API — /api/admin/feedback (ROLE_DEVELOPER)

Method Path 설명
GET /api/admin/feedback 받은 피드백 목록 + 미답변 개수
POST /api/admin/feedback/{feedbackId}/reply 답장 발행 (REPLY 편지 생성 + 상태 REPLIED)
PATCH /api/admin/feedback/{feedbackId}/status 상태 변경 (WAITING / IN_PROGRESS / REPLIED)
POST /api/admin/feedback/letters 전체 편지 발행 (UPDATE / STORY)
POST /api/admin/feedback/letters/images 편지 본문 이미지 업로드 (multipart)
POST GET PUT DELETE /api/admin/feedback/letters/drafts[/{draftId}] 편지 초안 저장 · 목록 · 이어쓰기 · 삭제

저장소 / 도메인

  • MongoDB 컬렉션 3종 신설: feedbacks, feedback_letters, feedback_letter_drafts
  • 목록 조회 경로에 복합 인덱스 부여 (student_created_at_idx, created_at_idx, recipient_created_at_idx, category_recipient_created_at_idx)
  • 에러코드 904-x 대역 신설 (피드백/편지 없음, 중복 답장, 발행 불가 카테고리, 상태 전이 불가, 업로드 레이트리밋)

안정성 · 보안 보완 (리뷰 반영분 포함)

  • 편지 발행 멱등성: requestId 유니크 인덱스로 중복 발행 차단, 빈 requestId 정규화 및 동시 발행 충돌 처리
  • 업로드 URL 발급 레이트리밋: studentId · IP 양쪽에 10분 20건 (Redis Lua 스크립트, 원자적 카운트)
  • 업로드 contentType서버에서 검증 (image/* 허용 패턴) — 서명된 헤더로 임의 파일이 올라가는 경로 차단
  • 피드백 상태 전이 제한 (REPLIED는 답장 발행으로만 진입, 되돌리기 불가), 공백 본문 차단
  • 푸시 발송을 트랜잭션 커밋 이후로 이동해 롤백 시 유령 알림 방지

2. 🔔 편지 푸시 웹뷰 라우팅 추가 (#1940)

답장 · 전체 발행 푸시 payload에 type / letterId만 있어 앱의 NAVIGATE_WEBVIEW 분기에 걸리지 않아, 알림을 탭해도 앱 홈만 열리던 문제를 수정했습니다.

  • payload에 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, LetterDraftServiceTest
  • LetterPreviewTest, FeedbackSerializationTest, StudentAuthControllerTest

🔍 리뷰 포인트

  • 신규 MongoDB 컬렉션 · 인덱스가 배포 시 자동 생성되는지 (운영 DB 인덱스 빌드 영향)
  • 업로드 레이트리밋 임계값(10분/20건)이 실사용 대비 적절한지
  • 푸시 path가 프론트 라우트(/feedback/letters/:letterId)와 계속 일치하는지

seongwon030 and others added 15 commits August 12, 2026 16:11
사용자 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] 편지 푸시에 웹뷰 라우팅 정보 추가
@seongwon030 seongwon030 self-assigned this Aug 21, 2026
@seongwon030 seongwon030 added 💾 BE Backend 📈 release 릴리즈 배포 ➕ MINOR Minor 릴리즈 labels Aug 21, 2026
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview Aug 21, 2026 12:16pm

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@seongwon030, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review or push new commits to the PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1558eb17-4e9c-4d5e-b64e-40268a5d7aa2

📥 Commits

Reviewing files that changed from the base of the PR and between 52218cb and d089495.

📒 Files selected for processing (48)
  • backend/src/main/java/moadong/feedback/controller/FeedbackAdminController.java
  • backend/src/main/java/moadong/feedback/controller/StudentFeedbackController.java
  • backend/src/main/java/moadong/feedback/entity/Feedback.java
  • backend/src/main/java/moadong/feedback/entity/Letter.java
  • backend/src/main/java/moadong/feedback/entity/LetterDraft.java
  • backend/src/main/java/moadong/feedback/enums/FeedbackStatus.java
  • backend/src/main/java/moadong/feedback/enums/FeedbackType.java
  • backend/src/main/java/moadong/feedback/enums/LetterCategory.java
  • backend/src/main/java/moadong/feedback/enums/SentFeedbackStatus.java
  • backend/src/main/java/moadong/feedback/payload/request/FeedbackCreateRequest.java
  • backend/src/main/java/moadong/feedback/payload/request/FeedbackReplyRequest.java
  • backend/src/main/java/moadong/feedback/payload/request/FeedbackStatusUpdateRequest.java
  • backend/src/main/java/moadong/feedback/payload/request/LetterCreateRequest.java
  • backend/src/main/java/moadong/feedback/payload/request/LetterDraftRequest.java
  • backend/src/main/java/moadong/feedback/payload/response/AdminFeedbackListResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/AdminFeedbackResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/FeedbackCreateResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/FeedbackReplyResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/LetterCreateResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/LetterDraftListResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/LetterDraftResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/LetterImageUploadResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/ReceivedLetterDetailResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/ReceivedLetterListResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/ReceivedLetterSummaryResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/SentFeedbackListResponse.java
  • backend/src/main/java/moadong/feedback/payload/response/SentFeedbackResponse.java
  • backend/src/main/java/moadong/feedback/repository/FeedbackRepository.java
  • backend/src/main/java/moadong/feedback/repository/LetterDraftRepository.java
  • backend/src/main/java/moadong/feedback/repository/LetterRepository.java
  • backend/src/main/java/moadong/feedback/service/FeedbackAdminService.java
  • backend/src/main/java/moadong/feedback/service/FeedbackImageService.java
  • backend/src/main/java/moadong/feedback/service/FeedbackService.java
  • backend/src/main/java/moadong/feedback/service/LetterDraftService.java
  • backend/src/main/java/moadong/feedback/service/LetterImageUploadService.java
  • backend/src/main/java/moadong/global/RegexConstants.java
  • backend/src/main/java/moadong/global/exception/ErrorCode.java
  • backend/src/main/java/moadong/user/controller/StudentAuthController.java
  • backend/src/main/java/moadong/user/payload/request/StudentIssueRequest.java
  • backend/src/main/java/moadong/user/service/UserCommandService.java
  • backend/src/main/resources/static/dev/index.html
  • backend/src/test/java/moadong/feedback/entity/LetterPreviewTest.java
  • backend/src/test/java/moadong/feedback/payload/FeedbackSerializationTest.java
  • backend/src/test/java/moadong/feedback/service/FeedbackAdminServiceTest.java
  • backend/src/test/java/moadong/feedback/service/FeedbackImageServiceTest.java
  • backend/src/test/java/moadong/feedback/service/FeedbackServiceTest.java
  • backend/src/test/java/moadong/feedback/service/LetterDraftServiceTest.java
  • backend/src/test/java/moadong/unit/user/StudentAuthControllerTest.java

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

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

Comment @coderabbitai help to get the list of available commands.

@seongwon030
seongwon030 merged commit 337c9b4 into main Aug 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💾 BE Backend ➕ MINOR Minor 릴리즈 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant