feat(feedback): 모아동 우체통 백엔드와 개발자 포털 화면 추가 - #1908
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Layer / File(s) | Summary |
|---|---|
도메인 모델과 저장소 계약 backend/src/main/java/moadong/feedback/entity/*, backend/src/main/java/moadong/feedback/enums/*, backend/src/main/java/moadong/feedback/repository/*, backend/src/main/java/moadong/global/exception/ErrorCode.java |
피드백과 편지 엔티티, 상태·분류 열거형, MongoDB 저장소 계약과 관련 오류 코드를 추가했습니다. 편지 미리보기와 읽음 상태 판별을 구현했습니다. |
학생 피드백 흐름 backend/src/main/java/moadong/feedback/controller/StudentFeedbackController.java, backend/src/main/java/moadong/feedback/service/FeedbackService.java, backend/src/main/java/moadong/feedback/service/FeedbackImageService.java, backend/src/main/java/moadong/feedback/payload/..., backend/src/test/java/moadong/feedback/service/*Test.java |
학생 JWT를 사용해 피드백 생성, 이미지 URL 발급과 검증, 받은 편지 조회·읽음 처리, 보낸 피드백 조회를 제공합니다. 관련 응답 매핑과 서비스 테스트를 추가했습니다. |
관리자 답장과 편지 발행 backend/src/main/java/moadong/feedback/controller/FeedbackAdminController.java, backend/src/main/java/moadong/feedback/service/FeedbackAdminService.java, backend/src/main/java/moadong/feedback/service/LetterImageUploadService.java, backend/src/test/java/moadong/feedback/service/FeedbackAdminServiceTest.java |
관리자 피드백 조회, 답장 발행, 상태 변경, 전체 편지 발행과 이미지 업로드를 추가했습니다. 답장 중복, 발행 가능 분류, FCM 성공·실패 처리를 검증합니다. |
편지 초안과 개발자 포털 backend/src/main/java/moadong/feedback/entity/LetterDraft.java, backend/src/main/java/moadong/feedback/service/LetterDraftService.java, backend/src/main/resources/static/dev/index.html, backend/src/test/java/moadong/feedback/service/LetterDraftServiceTest.java |
편지 초안의 생성·조회·수정·삭제를 추가했습니다. 개발자 포털에서 피드백 선택, 답장 작성, 초안 관리, 이미지 삽입과 편지 발행을 지원합니다. |
Estimated code review effort: 4 (Complex) | ~60 minutes
Suggested labels: ✨ Feature, 💾 BE, 📬 API
Sequence Diagram(s)
sequenceDiagram
participant DeveloperPortal
participant FeedbackAdminController
participant FeedbackAdminService
participant LetterRepository
participant FCM
DeveloperPortal->>FeedbackAdminController: 피드백 목록 또는 편지 발행 요청
FeedbackAdminController->>FeedbackAdminService: 요청 검증 및 서비스 호출
FeedbackAdminService->>LetterRepository: 답장 또는 전체 편지 저장
FeedbackAdminService->>FCM: 선택된 푸시 발송
FeedbackAdminService-->>DeveloperPortal: 편지 ID와 발송 결과 표시
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | 제목이 신규 feedback 도메인의 백엔드 API와 개발자 포털 화면 추가라는 주요 변경 사항을 정확하고 간결하게 요약합니다. |
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
feedback-image-upload-api
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.
Comment @coderabbitai help to get the list of available commands.
Test Results328 tests 328 ✅ 33s ⏱️ Results for commit 75744f8. ♻️ This comment has been updated with latest results. |
유형 · 받은 날짜 · 상태 컬럼이 짧은 한국어라 우측 답장 패널(370px)에 폭을 내주면 "문제 신/고", "답장 대/기" 처럼 글자 단위로 쪼개졌다. 해당 컬럼만 nowrap으로 고정한다. 푸시 체크박스는 정렬을 강제하는 규칙을 찾지 못했으나 화면에서 우측 두 줄로 깨져 있어, flex 시작 정렬과 text-align을 명시하고 체크박스가 줄지 않도록 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (1)
backend/src/test/java/moadong/feedback/service/FeedbackAdminServiceTest.java (1)
68-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
updateStatus테스트를 추가하십시오.이 테스트 클래스는
reply와createBroadcastLetter만 호출합니다.updateStatus의 정상 상태 변경과FEEDBACK_NOT_FOUND예외 경로를 테스트하십시오. 관리자 상태 변경 API의 회귀를 방지할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/test/java/moadong/feedback/service/FeedbackAdminServiceTest.java` around lines 68 - 229, FeedbackAdminServiceTest에 updateStatus 테스트가 없어 정상적인 상태 변경과 피드백 미존재 예외 경로가 검증되지 않습니다. 기존 테스트 픽스처와 feedbackRepository 목을 활용해 updateStatus 호출 후 상태가 요청값으로 변경되고 저장되는 정상 테스트를 추가하고, 조회 결과가 없는 경우 FEEDBACK_NOT_FOUND를 반환하며 저장하지 않는 예외 테스트를 추가하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/src/main/java/moadong/feedback/entity/Feedback.java`:
- Around line 57-65: Feedback의 changeStatus 메서드에서 REPLIED 상태로 직접 변경하지 못하도록 상태 전이
규칙을 제한하세요. REPLIED 전이는 replyLetterId와 repliedAt을 함께 설정하는 markReplied 메서드만 수행하게
하고, 답장 완료 후 WAITING 또는 IN_PROGRESS로 변경하는 경우에도 기존 답장 연결 정보가 남지 않도록 처리하세요.
In `@backend/src/main/java/moadong/feedback/entity/Letter.java`:
- Around line 95-100: Update Letter.broadcast to reject the REPLY category and
allow only UPDATE and STORY categories, preserving the existing letter
construction for valid inputs. Add a unit test covering the rejected REPLY case
and ensure the factory cannot create a broadcast letter without a recipient.
In
`@backend/src/main/java/moadong/feedback/payload/request/FeedbackCreateRequest.java`:
- Around line 13-15: Update the content validation in FeedbackCreateRequest by
replacing `@NotNull` with `@NotBlank`, preserving the existing size constraint and
messages so whitespace-only feedback is rejected.
In `@backend/src/main/java/moadong/feedback/repository/LetterRepository.java`:
- Around line 18-24: Update LetterRepository.findInboxByStudentId and
findInboxByStudentIdAndCategory to use pageable or cursor-based results and
expose next-page information in the response; apply the same bounded pagination
to student/admin queries in
backend/src/main/java/moadong/feedback/repository/FeedbackRepository.java lines
13-15 and draft queries in
backend/src/main/java/moadong/feedback/repository/LetterDraftRepository.java
line 12. Add studentId,createdAt and createdAt indexes to Feedback in
backend/src/main/java/moadong/feedback/entity/Feedback.java lines 37-38, and
recipientStudentId,createdAt plus category,recipientStudentId,createdAt indexes
to Letter in backend/src/main/java/moadong/feedback/entity/Letter.java lines
32-33, preserving descending createdAt ordering.
In `@backend/src/main/java/moadong/feedback/service/FeedbackAdminService.java`:
- Around line 74-75: Update the reply flow in FeedbackAdminService.reply so
sendReplyPush is deferred until the surrounding transaction commits, using a
transaction-after-commit event or transactional outbox. Preserve the sendPush
condition and push payload, and do not send the notification when the
transaction rolls back.
In `@backend/src/main/java/moadong/feedback/service/FeedbackImageService.java`:
- Around line 99-110: Separate the validated upload object from the persisted
object in the FeedbackImageService flow: issue the presigned PUT for a temporary
key, validate that object with validateImage, then have the server copy it to a
final key that has no client PUT permission before saving or returning the final
URL. Alternatively, update the presignPutObject request and upload response so
the presigned request signs and sends If-None-Match: * to prevent overwrites
after the first upload.
In `@backend/src/main/resources/static/dev/index.html`:
- Around line 3410-3418: Update enterFeedbackLetterMode() to also reset the
letter settings when starting a new letter: set feedbackLetterCategory to
'UPDATE' and feedbackSendPush to true, alongside the existing draft field
resets.
- Around line 3633-3650: Update the letter publishing flow around the POST
request in the feedback editor so retries are idempotent: generate and persist a
stable request identifier for the same draft, send it with POST
/api/admin/feedback/letters, and have the backend reuse the existing publication
result instead of creating another Letter or resending all pushes. Keep the
existing reply flow unchanged, and clear the identifier only after successful
letter publication and draft discard.
---
Nitpick comments:
In
`@backend/src/test/java/moadong/feedback/service/FeedbackAdminServiceTest.java`:
- Around line 68-229: FeedbackAdminServiceTest에 updateStatus 테스트가 없어 정상적인 상태 변경과
피드백 미존재 예외 경로가 검증되지 않습니다. 기존 테스트 픽스처와 feedbackRepository 목을 활용해 updateStatus 호출
후 상태가 요청값으로 변경되고 저장되는 정상 테스트를 추가하고, 조회 결과가 없는 경우 FEEDBACK_NOT_FOUND를 반환하며 저장하지
않는 예외 테스트를 추가하세요.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 98b53a78-288b-4042-89b9-ba3b60b2c345
📒 Files selected for processing (43)
backend/src/main/java/moadong/feedback/controller/FeedbackAdminController.javabackend/src/main/java/moadong/feedback/controller/StudentFeedbackController.javabackend/src/main/java/moadong/feedback/entity/Feedback.javabackend/src/main/java/moadong/feedback/entity/Letter.javabackend/src/main/java/moadong/feedback/entity/LetterDraft.javabackend/src/main/java/moadong/feedback/enums/FeedbackStatus.javabackend/src/main/java/moadong/feedback/enums/FeedbackType.javabackend/src/main/java/moadong/feedback/enums/LetterCategory.javabackend/src/main/java/moadong/feedback/enums/SentFeedbackStatus.javabackend/src/main/java/moadong/feedback/payload/request/FeedbackCreateRequest.javabackend/src/main/java/moadong/feedback/payload/request/FeedbackReplyRequest.javabackend/src/main/java/moadong/feedback/payload/request/FeedbackStatusUpdateRequest.javabackend/src/main/java/moadong/feedback/payload/request/LetterCreateRequest.javabackend/src/main/java/moadong/feedback/payload/request/LetterDraftRequest.javabackend/src/main/java/moadong/feedback/payload/response/AdminFeedbackListResponse.javabackend/src/main/java/moadong/feedback/payload/response/AdminFeedbackResponse.javabackend/src/main/java/moadong/feedback/payload/response/FeedbackCreateResponse.javabackend/src/main/java/moadong/feedback/payload/response/FeedbackReplyResponse.javabackend/src/main/java/moadong/feedback/payload/response/LetterCreateResponse.javabackend/src/main/java/moadong/feedback/payload/response/LetterDraftListResponse.javabackend/src/main/java/moadong/feedback/payload/response/LetterDraftResponse.javabackend/src/main/java/moadong/feedback/payload/response/LetterImageUploadResponse.javabackend/src/main/java/moadong/feedback/payload/response/ReceivedLetterDetailResponse.javabackend/src/main/java/moadong/feedback/payload/response/ReceivedLetterListResponse.javabackend/src/main/java/moadong/feedback/payload/response/ReceivedLetterSummaryResponse.javabackend/src/main/java/moadong/feedback/payload/response/SentFeedbackListResponse.javabackend/src/main/java/moadong/feedback/payload/response/SentFeedbackResponse.javabackend/src/main/java/moadong/feedback/repository/FeedbackRepository.javabackend/src/main/java/moadong/feedback/repository/LetterDraftRepository.javabackend/src/main/java/moadong/feedback/repository/LetterRepository.javabackend/src/main/java/moadong/feedback/service/FeedbackAdminService.javabackend/src/main/java/moadong/feedback/service/FeedbackImageService.javabackend/src/main/java/moadong/feedback/service/FeedbackService.javabackend/src/main/java/moadong/feedback/service/LetterDraftService.javabackend/src/main/java/moadong/feedback/service/LetterImageUploadService.javabackend/src/main/java/moadong/global/exception/ErrorCode.javabackend/src/main/resources/static/dev/index.htmlbackend/src/test/java/moadong/feedback/entity/LetterPreviewTest.javabackend/src/test/java/moadong/feedback/payload/FeedbackSerializationTest.javabackend/src/test/java/moadong/feedback/service/FeedbackAdminServiceTest.javabackend/src/test/java/moadong/feedback/service/FeedbackImageServiceTest.javabackend/src/test/java/moadong/feedback/service/FeedbackServiceTest.javabackend/src/test/java/moadong/feedback/service/LetterDraftServiceTest.java
| public void markReplied(String replyLetterId) { | ||
| this.status = FeedbackStatus.REPLIED; | ||
| this.replyLetterId = replyLetterId; | ||
| this.repliedAt = Instant.now(); | ||
| } | ||
|
|
||
| public void changeStatus(FeedbackStatus status) { | ||
| this.status = status; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
REPLIED 상태 전이를 markReplied로 제한하세요.
Line 63의 changeStatus(REPLIED)는 replyLetterId와 repliedAt 없이 REPLIED 상태를 저장합니다. 반대로 답장 후 WAITING 또는 IN_PROGRESS로 변경하면 답장 연결 정보가 남습니다. 상태 전이 규칙을 제한하고, 답장 완료는 markReplied만 수행하게 하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/moadong/feedback/entity/Feedback.java` around lines 57
- 65, Feedback의 changeStatus 메서드에서 REPLIED 상태로 직접 변경하지 못하도록 상태 전이 규칙을 제한하세요.
REPLIED 전이는 replyLetterId와 repliedAt을 함께 설정하는 markReplied 메서드만 수행하게 하고, 답장 완료 후
WAITING 또는 IN_PROGRESS로 변경하는 경우에도 기존 답장 연결 정보가 남지 않도록 처리하세요.
| public static Letter broadcast(LetterCategory category, String title, String body) { | ||
| return Letter.builder() | ||
| .category(category) | ||
| .title(title) | ||
| .body(body) | ||
| .build(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
REPLY 카테고리를 broadcast 팩토리에서 거부하세요.
Letter.broadcast(LetterCategory.REPLY, ...)는 수신자가 없는 REPLY 편지를 생성합니다. Line 50의 isBroadcast()는 이 편지를 전체 발행 편지로 처리합니다. 현재 관리자 서비스가 차단해도 공개 팩토리가 도메인 규칙을 우회할 수 있습니다. UPDATE와 STORY만 허용하고 이 경우의 단위 테스트를 추가하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/moadong/feedback/entity/Letter.java` around lines 95 -
100, Update Letter.broadcast to reject the REPLY category and allow only UPDATE
and STORY categories, preserving the existing letter construction for valid
inputs. Add a unit test covering the rejected REPLY case and ensure the factory
cannot create a broadcast letter without a recipient.
| @NotNull(message = "내용은 필수입니다.") | ||
| @Size(min = 10, max = 300, message = "내용은 10자 이상 300자 이하로 입력해주세요.") | ||
| String content, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
file="backend/src/main/java/moadong/feedback/payload/request/FeedbackCreateRequest.java"
printf '%s\n' '--- target file ---'
cat -n "$file"
printf '%s\n' '--- validation annotations in request payloads ---'
rg -n --glob 'backend/src/main/java/moadong/**/*.java' \
'`@NotBlank`|`@NotNull`|`@Size`' backend/src/main/java/moadong | head -200
printf '%s\n' '--- validation dependencies and project validation style ---'
rg -n 'spring-boot-starter-validation|jakarta.validation|javax.validation|MethodArgumentNotValidException|NotBlank' \
backend/build.gradle* backend/src/main/java backend/src/test 2>/dev/null | head -250
printf '%s\n' '--- standalone constraint behavior model ---'
python3 - <<'PY'
inputs = [None, "", " ", " a", "abcdefghij"]
for value in inputs:
not_null = value is not None
size = value is not None and 10 <= len(value) <= 300
not_blank = value is not None and bool(value.strip()) and len(value.strip()) >= 1
print(repr(value), {"NotNull": not_null, "Size(10,300)": size, "NotBlank": not_blank})
PYRepository: Moadong/moadong
Length of output: 35234
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- exact boundary inputs ---'
python3 - <<'PY'
inputs = [
" " * 10,
"\t" * 10,
"\n" * 10,
" " * 9,
" " * 10 + "a",
]
for value in inputs:
size_passes = 10 <= len(value) <= 300
not_blank_passes = bool(value.strip())
print({
"repr": repr(value),
"length": len(value),
"Size(10,300)": size_passes,
"NotBlank": not_blank_passes,
})
PY
printf '%s\n' '--- feedback validation call sites and persistence path ---'
rg -n -A8 -B4 'FeedbackCreateRequest|createFeedback|`@Valid`' \
backend/src/main/java/moadong/feedback | head -220
printf '%s\n' '--- neighboring feedback request conventions ---'
cat -n backend/src/main/java/moadong/feedback/payload/request/FeedbackReplyRequest.java
cat -n backend/src/main/java/moadong/feedback/payload/request/LetterCreateRequest.javaRepository: Moadong/moadong
Length of output: 16337
공백만 있는 content를 거부하세요.
길이가 10자인 공백 문자열은 현재 제약을 통과한 후 저장됩니다. @NotBlank로 교체해 내용 없는 피드백을 차단하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@backend/src/main/java/moadong/feedback/payload/request/FeedbackCreateRequest.java`
around lines 13 - 15, Update the content validation in FeedbackCreateRequest by
replacing `@NotNull` with `@NotBlank`, preserving the existing size constraint and
messages so whitespace-only feedback is rejected.
| @Query(value = "{ $or: [ { 'recipientStudentId': ?0 }, { 'recipientStudentId': null } ] }", | ||
| sort = "{ 'createdAt': -1 }") | ||
| List<Letter> findInboxByStudentId(String studentId); | ||
|
|
||
| @Query(value = "{ 'category': ?1, $or: [ { 'recipientStudentId': ?0 }, { 'recipientStudentId': null } ] }", | ||
| sort = "{ 'createdAt': -1 }") | ||
| List<Letter> findInboxByStudentIdAndCategory(String studentId, LetterCategory category); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
목록 조회에 페이지네이션과 복합 인덱스를 추가하세요.
현재 모든 목록 조회가 List를 반환합니다. 학생 받은 편지함은 전체 발행 편지를 포함하므로 데이터가 증가하면 요청마다 모든 과거 편지를 읽고 정렬합니다. 단일 필드 인덱스는 createdAt DESC 정렬 조건을 함께 처리하지 못합니다.
backend/src/main/java/moadong/feedback/repository/LetterRepository.java#L18-L24:Pageable또는 cursor 기반 조회로 변경하고 응답에 다음 페이지 정보를 포함하세요.backend/src/main/java/moadong/feedback/repository/FeedbackRepository.java#L13-L15: 학생별 및 관리자 목록 조회에 동일한 제한을 적용하세요.backend/src/main/java/moadong/feedback/repository/LetterDraftRepository.java#L12-L12: 초안 목록에도 제한을 적용하세요.backend/src/main/java/moadong/feedback/entity/Feedback.java#L37-L38:studentId, createdAt복합 인덱스와 관리자 정렬용createdAt인덱스를 추가하세요.backend/src/main/java/moadong/feedback/entity/Letter.java#L32-L33: 받은 편지함 쿼리용recipientStudentId, createdAt및category, recipientStudentId, createdAt복합 인덱스를 추가하세요.
📍 Affects 5 files
backend/src/main/java/moadong/feedback/repository/LetterRepository.java#L18-L24(this comment)backend/src/main/java/moadong/feedback/repository/FeedbackRepository.java#L13-L15backend/src/main/java/moadong/feedback/repository/LetterDraftRepository.java#L12-L12backend/src/main/java/moadong/feedback/entity/Feedback.java#L37-L38backend/src/main/java/moadong/feedback/entity/Letter.java#L32-L33
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/moadong/feedback/repository/LetterRepository.java`
around lines 18 - 24, Update LetterRepository.findInboxByStudentId and
findInboxByStudentIdAndCategory to use pageable or cursor-based results and
expose next-page information in the response; apply the same bounded pagination
to student/admin queries in
backend/src/main/java/moadong/feedback/repository/FeedbackRepository.java lines
13-15 and draft queries in
backend/src/main/java/moadong/feedback/repository/LetterDraftRepository.java
line 12. Add studentId,createdAt and createdAt indexes to Feedback in
backend/src/main/java/moadong/feedback/entity/Feedback.java lines 37-38, and
recipientStudentId,createdAt plus category,recipientStudentId,createdAt indexes
to Letter in backend/src/main/java/moadong/feedback/entity/Letter.java lines
32-33, preserving descending createdAt ordering.
| boolean pushSent = request.sendPush() | ||
| && sendReplyPush(feedback.getStudentId(), letter); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
트랜잭션 커밋 후에 답장 푸시를 발송하십시오.
reply는 @Transactional 메서드입니다. Line 75의 sendReplyPush는 Letter와 Feedback 변경이 커밋되기 전에 실행됩니다. 커밋이 실패하면 사용자는 존재하지 않는 답장에 대한 푸시를 받을 수 있습니다. 커밋 후 이벤트 또는 트랜잭셔널 아웃박스로 푸시를 발행하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/moadong/feedback/service/FeedbackAdminService.java`
around lines 74 - 75, Update the reply flow in FeedbackAdminService.reply so
sendReplyPush is deferred until the surrounding transaction commits, using a
transaction-after-commit event or transactional outbox. Preserve the sendPush
condition and push payload, and do not send the notification when the
transaction rolls back.
| PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() | ||
| .signatureDuration(Duration.ofMinutes(serverProperties.fileUrl().expirationTime())) | ||
| .putObjectRequest(putObjectRequest) | ||
| .build(); | ||
|
|
||
| String presignedUrl = s3Presigner.presignPutObject(presignRequest).url().toString(); | ||
| return new PresignedUploadResponse( | ||
| presignedUrl, | ||
| normalizedViewEndpoint + "/" + key, | ||
| Map.of("Content-Type", request.contentType()), | ||
| true, | ||
| null); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Find whether feedback uploads have an immutable finalization path.
rg -n -C 4 --glob '*.java' \
'presignPutObject|PutObjectPresignRequest|copyObject|CopyObject|versionId|ObjectLock|feedback/' \
backend
# Inspect repository configuration for R2/S3 versioning, object-lock, or lifecycle rules.
rg -n -C 3 --hidden -g '!**/.git/**' \
'versioning|object.?lock|lifecycle|copyObject|feedback/' .Repository: Moadong/moadong
Length of output: 36016
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo '--- FeedbackImageService structure ---'
ast-grep outline backend/src/main/java/moadong/feedback/service/FeedbackImageService.java
echo '--- FeedbackImageService implementation ---'
sed -n '1,230p' backend/src/main/java/moadong/feedback/service/FeedbackImageService.java
echo '--- Feedback image call sites ---'
rg -n -C 5 --glob '*.java' \
'createUploadUrls|validateImages|PresignedUploadResponse|FeedbackImageService' \
backend/src/main/java backend/src/test/java
echo '--- Storage configuration and infrastructure ---'
rg -n -C 4 --hidden -g '!**/.git/**' \
'S3Client|S3Presigner|AwsProperties|serverProperties|endpoint|bucket|R2|Cloudflare|versioning|object.?lock|lifecycle|copyObject|CopyObject' \
backend .github docker-compose.yml compose.yml 2>/dev/null || trueRepository: Moadong/moadong
Length of output: 50372
🌐 Web query:
Cloudflare R2 presigned PUT URL overwrite existing object same key behavior
💡 Result:
When using a Cloudflare R2 presigned PUT URL, performing an upload to a key that already exists will overwrite the existing object by default [1][2][3]. There is no requirement to delete the existing object before uploading a new one with the same key [4]. If you wish to prevent accidental overwrites or implement a write-once guarantee, you can utilize R2's support for conditional headers [5][6]. By including specific headers in the request—such as If-None-Match: *—you can instruct R2 to reject the upload if an object already exists at that key [5][2]. When using conditional headers with presigned URLs, the client must include the exact same header in their PUT request that was used during the generation of the presigned URL [2][3]. Because the signature parameters (including headers) are cryptographically bound to the URL, the request will fail with a 403/SignatureDoesNotMatch error if the client attempts to use a different header or omits it entirely [1][2]. In summary: - Default behavior: Overwrites existing objects [1][3]. - Preventing overwrites: Use conditional headers (e.g., If-None-Match: *) during both the signature generation and the final PUT request [5][2]. - Security: Any headers specified in the presigned URL must be present and identical in the subsequent upload request [1][2].
Citations:
- 1: https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- 2: https://9df0cba6.preview.developers.cloudflare.com/r2/api/s3/presigned-urls/
- 3: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js/
- 4: https://www.answeroverflow.com/m/1251247084808441957
- 5: https://9df0cba6.preview.developers.cloudflare.com/r2/api/s3/extensions/
- 6: https://developers.cloudflare.com/r2/api/s3/api/
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo '--- Storage-related configuration ---'
rg -n -C 5 --hidden -g '!**/.git/**' \
'cloud\.aws|s3:|bucket|endpoint|view-endpoint|versioning|object.?lock|lifecycle|R2|Cloudflare' \
backend/src/main/resources backend/build.gradle* infra .github 2>/dev/null || true
echo '--- Feedback endpoints and persistence flow ---'
sed -n '45,80p' backend/src/main/java/moadong/feedback/controller/StudentFeedbackController.java
sed -n '20,45p' backend/src/main/java/moadong/feedback/service/FeedbackService.java
echo '--- Presigned response contract ---'
cat -n backend/src/main/java/moadong/media/dto/PresignedUploadResponse.javaRepository: Moadong/moadong
Length of output: 4039
검증한 객체와 저장한 객체를 분리하십시오.
FeedbackImageService.java:99-110은 피드백에 저장할 key에 재사용 가능한 PUT URL을 발급합니다. R2는 같은 key에 대한 PUT을 기본적으로 덮어쓰기로 처리합니다. validateImage는 HeadObject로 한 번만 검사하므로, 피드백 저장 후에도 학생이 만료 전 URL로 객체를 교체할 수 있습니다.
임시 key에 업로드한 뒤 서버가 검증된 객체를 PUT 권한이 없는 최종 key로 복사하고 최종 URL을 저장하십시오. 또는 presigned PUT에 If-None-Match: *를 서명하고 업로드 요청에도 포함하여 최초 업로드 후 덮어쓰기를 차단하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/java/moadong/feedback/service/FeedbackImageService.java`
around lines 99 - 110, Separate the validated upload object from the persisted
object in the FeedbackImageService flow: issue the presigned PUT for a temporary
key, validate that object with validateImage, then have the server copy it to a
final key that has no client PUT permission before saving or returning the final
URL. Alternatively, update the presignPutObject request and upload response so
the presigned request signs and sends If-None-Match: * to prevent overwrites
after the first upload.
| function enterFeedbackLetterMode() { | ||
| feedbackMode = 'letter'; | ||
| feedbackSelectedId = ''; | ||
| feedbackCurrentDraftId = ''; | ||
| document.getElementById('feedbackReplyTitle').value = ''; | ||
| document.getElementById('feedbackReplyBody').value = ''; | ||
| document.getElementById('feedbackSaveResult').classList.add('hidden'); | ||
| renderFeedbackList(); | ||
| reloadFeedbackDrafts(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
새 편지 모드에서 편지 설정도 초기화하세요.
enterFeedbackLetterMode()는 제목과 본문만 초기화합니다. 이전 초안의 feedbackLetterCategory와 feedbackSendPush 값은 유지됩니다.
그 후 새 편지를 발행하면 이전 초안의 분류 또는 전체 푸시 설정을 의도하지 않게 사용할 수 있습니다. 새 편지 모드 진입 시 분류를 UPDATE로, 푸시를 기본값인 true로 초기화하세요.
수정 예시
function enterFeedbackLetterMode() {
feedbackMode = 'letter';
feedbackSelectedId = '';
feedbackCurrentDraftId = '';
+ document.getElementById('feedbackLetterCategory').value = 'UPDATE';
+ document.getElementById('feedbackSendPush').checked = true;
document.getElementById('feedbackReplyTitle').value = '';
document.getElementById('feedbackReplyBody').value = '';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function enterFeedbackLetterMode() { | |
| feedbackMode = 'letter'; | |
| feedbackSelectedId = ''; | |
| feedbackCurrentDraftId = ''; | |
| document.getElementById('feedbackReplyTitle').value = ''; | |
| document.getElementById('feedbackReplyBody').value = ''; | |
| document.getElementById('feedbackSaveResult').classList.add('hidden'); | |
| renderFeedbackList(); | |
| reloadFeedbackDrafts(); | |
| function enterFeedbackLetterMode() { | |
| feedbackMode = 'letter'; | |
| feedbackSelectedId = ''; | |
| feedbackCurrentDraftId = ''; | |
| document.getElementById('feedbackLetterCategory').value = 'UPDATE'; | |
| document.getElementById('feedbackSendPush').checked = true; | |
| document.getElementById('feedbackReplyTitle').value = ''; | |
| document.getElementById('feedbackReplyBody').value = ''; | |
| document.getElementById('feedbackSaveResult').classList.add('hidden'); | |
| renderFeedbackList(); | |
| reloadFeedbackDrafts(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/resources/static/dev/index.html` around lines 3410 - 3418,
Update enterFeedbackLetterMode() to also reset the letter settings when starting
a new letter: set feedbackLetterCategory to 'UPDATE' and feedbackSendPush to
true, alongside the existing draft field resets.
| feedbackIsPublishing = true; | ||
| updateFeedbackEditorState(); | ||
| try { | ||
| const res = await fetch(url, { method: 'POST', headers: headers(), body: JSON.stringify(payload) }); | ||
| const data = await readJsonOrEmpty(res); | ||
| if (!res.ok) { | ||
| setMessageBox('feedbackSaveResult', false, data.message || '발행 실패 (HTTP ' + res.status + ')'); | ||
| return; | ||
| } | ||
| setMessageBox('feedbackSaveResult', true, getApiSuccessMessage(data, isLetterMode ? '편지가 발행되었습니다.' : '답장이 발행되었습니다.')); | ||
| if (sendPush && data.data && data.data.pushSent === false) { | ||
| showToast((isLetterMode ? '편지는' : '답장은') + ' 발행됐지만 푸시는 전송되지 않았습니다.', 'error'); | ||
| } else if (isLetterMode && sendPush && data.data) { | ||
| showToast('푸시 ' + (data.data.pushSuccessCount || 0) + '건 발송됨'); | ||
| } | ||
| if (isLetterMode && feedbackCurrentDraftId) { | ||
| await discardFeedbackDraftAfterPublish(feedbackCurrentDraftId); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a 'FeedbackAdminController.java|FeedbackAdminService.java' backend/src/main/java
rg -n -C 8 \
'`@PostMapping`|reply|letters|Idempotency|idempot|LetterCreateRequest|FeedbackReplyRequest' \
backend/src/main/java/moadong/feedback/controller/FeedbackAdminController.java \
backend/src/main/java/moadong/feedback/service/FeedbackAdminService.javaRepository: Moadong/moadong
Length of output: 17873
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- service implementation ---'
cat -n backend/src/main/java/moadong/feedback/service/FeedbackAdminService.java | sed -n '55,145p'
printf '%s\n' '--- request/response/entity/repository candidates ---'
fd -a -e java backend/src/main/java | rg '/(FeedbackReplyRequest|LetterCreateRequest|FeedbackReplyResponse|LetterCreateResponse|Letter|Feedback)(\\.java)?$|/(LetterRepository|FeedbackRepository)(\\.java)?$' | sort
printf '%s\n' '--- idempotency and uniqueness search ---'
rg -n -i -C 3 'idempot|request.?id|client.?token|uniqueconstraint|uniqueConstraints|`@Column`\\s*\\([^\\n]*unique|findBy.*(feedback|title|body)|FEEDBACK_ALREADY_REPLIED|markReplied' backend/src/main/java || true
printf '%s\n' '--- client publish functions and catch paths ---'
rg -n -C 18 'feedbackIsPublishing|discardFeedbackDraftAfterPublish|fetch\\(url|발행 실패|catch \\(' backend/src/main/resources/static/dev/index.html | sed -n '1,360p'Repository: Moadong/moadong
Length of output: 4960
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- request/response/entity/repository files ---'
fd --full-path -a -e java 'backend/src/main/java' | rg '/(FeedbackReplyRequest|LetterCreateRequest|FeedbackReplyResponse|LetterCreateResponse|Letter|Feedback|LetterRepository|FeedbackRepository)\.java$' | sort
printf '%s\n' '--- relevant source excerpts ---'
for f in $(fd --full-path -a -e java 'backend/src/main/java' | rg '/(FeedbackReplyRequest|LetterCreateRequest|Letter|Feedback|LetterRepository|FeedbackRepository)\.java$' | sort); do
echo "--- $f"
cat -n "$f" | sed -n '1,240p'
done
printf '%s\n' '--- idempotency and duplicate-prevention search ---'
rg -n -i -C 3 'idempot|request.?id|client.?token|uniqueconstraint|uniqueConstraints|`@Column`[[:space:]]*\([^)]*unique|findBy.*(feedback|title|body)|FEEDBACK_ALREADY_REPLIED|markReplied' backend/src/main/java || true
printf '%s\n' '--- client publish functions and catch paths ---'
rg -n -C 18 'feedbackIsPublishing|discardFeedbackDraftAfterPublish|fetch\(url|발행 실패|catch \(' backend/src/main/resources/static/dev/index.html | sed -n '1,420p'
printf '%s\n' '--- publication result/recovery endpoints ---'
rg -n -i -C 5 'reply|letters|발행|published|publication|result' backend/src/main/java/moadong/feedback/controller backend/src/main/resources/static/dev/index.html | sed -n '1,500p'Repository: Moadong/moadong
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- client feedback state and publication ---'
cat -n backend/src/main/resources/static/dev/index.html | sed -n '3280,3725p'
printf '%s\n' '--- feedback-related client URLs and handlers ---'
rg -n -C 8 'api/admin/feedback|feedback/(reply|letters)|/letters|btnPublishFeedbackReply|btnNewFeedbackLetter|enterFeedbackLetterMode|clearFeedback' backend/src/main/resources/static/dev/index.html
printf '%s\n' '--- feedback controller complete route surface ---'
cat -n backend/src/main/java/moadong/feedback/controller/FeedbackAdminController.java | sed -n '35,130p'
printf '%s\n' '--- letter read/list routes for result recovery ---'
rg -n -i -C 6 '@(Get|Post|Put|Patch|Delete)Mapping|find.*Letter|letterRepository|feedbackId' backend/src/main/java/moadong/feedback/controller backend/src/main/java/moadong/feedback/service backend/src/main/java/moadong/feedback/repository | rg -i 'letter|feedback|Mapping|find.*letter|letterRepository|feedbackId' | sed -n '1,360p'Repository: Moadong/moadong
Length of output: 50371
전체 편지 발행 API에 멱등성 처리를 추가하세요.
POST /api/admin/feedback/letters는 요청 식별자를 받지 않고 매번 새 Letter를 저장합니다. 응답이 유실되면 클라이언트가 편집 내용과 초안을 유지하므로 재발행 시 같은 편지와 전체 푸시가 중복됩니다. 요청 식별자 기반 멱등 처리 또는 재시도 전 발행 결과 조회를 구현하세요. 답장 API는 REPLIED 상태 검사로 순차 재시도 중복을 차단하므로 이 문제의 대상이 아닙니다.
🧰 Tools
🪛 ast-grep (0.45.1)
[error] 3638-3638: React's useState should not be directly called
Context: setMessageBox('feedbackSaveResult', false, data.message || '발행 실패 (HTTP ' + res.status + ')')
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(usestate-direct-usage)
[error] 3641-3641: React's useState should not be directly called
Context: setMessageBox('feedbackSaveResult', true, getApiSuccessMessage(data, isLetterMode ? '편지가 발행되었습니다.' : '답장이 발행되었습니다.'))
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(usestate-direct-usage)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/src/main/resources/static/dev/index.html` around lines 3633 - 3650,
Update the letter publishing flow around the POST request in the feedback editor
so retries are idempotent: generate and persist a stable request identifier for
the same draft, send it with POST /api/admin/feedback/letters, and have the
backend reuse the existing publication result instead of creating another Letter
or resending all pushes. Keep the existing reply flow unchanged, and clear the
identifier only after successful letter publication and draft discard.
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>
#️⃣연관된 이슈
📝작업 내용
프론트가 MSW 목으로 먼저 구현한 모아동 우체통의 백엔드입니다.
feedback도메인이 없던 상태에서 전부 신규로 만들었습니다.사용자 API — 경로가⚠️
/api/student/feedback입니다전달 문서는
/api/feedback을 가정했지만 그대로 두면 매 요청 500입니다.JwtAuthenticationFilter:33이/api/student,/auth/student,/api/v2/fcm만 건너뜁니다./api/feedback에 학생 토큰을 붙이면 필터가 동작하고 →CustomUserDetailService가 UUID를 사용자로 조회하다USER_NOT_EXIST를 던지는데 → 필터는extractUsername구간만 try/catch라 이 예외가 안 잡히고 → 필터 체인이라GlobalExceptionHandler도 못 탑니다.그래서
StudentFcmController와 같은 방식(필터 제외 경로 +StudentJwtService로 직접 파싱)으로 맞췄습니다./api/student/feedback/api/student/feedback/images/upload-url/api/student/feedback/received?category=/api/student/feedback/received/{letterId}/api/student/feedback/received/{letterId}/read/api/student/feedback/sent/api/student/feedback/sent/{feedbackId}운영 API — 전부
/api/admin아래피드백은 개인적인 의견이라
SecurityConfig의hasRole('DEVELOPER')안쪽에 두는 게 필수입니다. 다른 경로면anyRequest().permitAll()에 걸려 인증 없이 뚫립니다.GET /api/admin/feedback·POST .../{id}/reply·PATCH .../{id}/status·POST .../letters·POST .../letters/images· 초안 CRUD 4개첨부 사진
동아리 활동사진(
generateFeedUploadUrls)과 같은 presigned 방식이며 요청/응답 배열 형태도 동일해서 프론트가uploadToStorage를 그대로 재사용합니다. 저장 시점에 장수·소유 경로·실제 업로드 여부·용량을 R2에서 다시 검증합니다. key가feedback/{studentId}/라 경로 자체가 권한 경계입니다.규격은 기존 규약대로 6종(jpeg·jpg·png·gif·bmp·webp) · 10MB입니다. 시안의 "PNG·JPG·최대 5MB" 문구는 오기이며
application.yml과 프론트MAX_FILE_SIZE모두 10MB로 이미 일치합니다.개발자 포털
사이드바에
받은 피드백추가. 좌측 테이블 · 우측 답장 패널(원문 인용 + 첨부 썸네일), 새 편지 발행 시 이미지 업로드 · 임시저장 · 전체 푸시. 로컬 구동으로 목록·답장·첨부 썸네일 렌더까지 확인했습니다.중점적으로 리뷰받고 싶은 부분
/api/student/feedback경로 선택 — 위 500 이슈 때문인데,shouldNotFilter에/api/feedback을 추가하는 쪽이 나을지 의견 주세요. 프론트 계약이 바뀌는 지점입니다.feedback_letter_drafts)에 둔 것 —Letter에 상태 플래그로 두면 받은 편지함 쿼리에 조건 하나만 빠져도 미발행 글이 전체 사용자에게 노출됩니다. 물리 분리로 그 사고를 구조적으로 막았습니다.readStudentIds— 편지 문서 안에 읽은 사람 UUID를 모읍니다. 지금 규모(수천)에선 문제없지만 1만 명이면 조회 1회에 편지당 360KB를 끌어옵니다. 커지면 projection으로 분리해야 합니다.논의하고 싶은 부분
공개 버킷 — 첨부 사진이 활동사진·로고와 같은 공개 버킷에 올라갑니다. key가 랜덤이라 URL을 모르면 못 열지만 알면 누구나 열립니다. 정책적으로 괜찮은지만 확인 부탁드립니다.
🫡 참고사항
확정된 결정 (논의 완료)
user_+ 학생 UUID 앞 8자리 (user_a3f9c2d1). 시안의 4자리는 1만 버킷이라 제보자 200명에서 충돌 확률 86%, 500명이면 사실상 100%입니다. 서로 다른 학생이 같은 ID로 보이면 운영자가 동일인의 반복 제보로 오해합니다. 학번은 서버가 알지도 못하고 개인정보라 노출하지 않습니다.WAITING/IN_PROGRESS/REPLIED, 사용자 응답은PENDING/REPLIED로 축약.sendToAll필드 제외 — 대상 선택 UI가 없어 의미가 미정이라 넣지 않았습니다.답장 푸시는 웹뷰 토큰 주입 전까지 동작하지 않습니다
우체통 출시를 앱 릴리즈에 묶지 않기로 하면서, 앱 웹뷰 안에서도 웹이 자체 UUID를 발급하게 됩니다. 앱
@access_token의 UUID(= FCM 등록 주체)와 달라져StudentUser를 찾을 수 없습니다.그래서 포털 푸시 체크박스를 기본 해제로 두었습니다(
75744f8a). 켜둔 채 발행하면 실패가 아닌데 매번 "푸시는 전송되지 않았습니다" 경고가 뜹니다. 발송 코드는 그대로 두었고, 주입이 들어오면 체크만 켜면 동작합니다.전체 편지 발행(
UPDATE/STORY) 푸시는 지금도 정상 동작합니다. 전체 토큰 멀티캐스트라 studentId 매칭이 없습니다. 답장만 안 됩니다.후속 작업 — 신원 이관 API 검토 필요
위 순서(우체통 먼저 출시 → 나중에 앱에 주입 추가) 때문에, 주입이 들어오는 시점에 studentId가 바뀌어 그 사이에 쌓인 편지함이 유실됩니다. 유실 규모는
출시 ~ 주입 릴리즈기간 × 사용량이라 미룰수록 커집니다.필요해지면 범위는 피드백 도메인 3곳입니다 —
Feedback.studentId,Letter.recipientStudentId,Letter.readStudentIds. FCM 쪽(StudentUser,StudentFcmToken)은 주입 토큰으로 이미 등록돼 있으므로 건드리면 안 됩니다. 두 토큰의 서명을 모두 검증해야 남의 편지함 흡수를 막을 수 있습니다.이번 PR 범위 밖입니다. 주입 릴리즈 시점이 정해지면 별도로 올리겠습니다.
테스트
신규 43개 전부 통과. 전체 328개 / 20실패인데, 실패 20개는 작업 전과 동일한
@SpringBootTest컨텍스트 로딩 실패입니다(워크스페이스에application.yml미생성). 피드백 관련 클래스는 없습니다.FeedbackServiceTest11 ·FeedbackImageServiceTest12 ·FeedbackAdminServiceTest10 ·LetterPreviewTest5 ·LetterDraftServiceTest4 ·FeedbackSerializationTest1배포 후 첫 확인
학생 토큰을 붙인
GET /api/student/feedback/sent한 번./api/student가 필터에서 제외된다는 전제는 코드로만 확인했고 실행 검증을 못 했습니다. 이게 틀리면 사용자 API 전체가 500입니다.알려진 제약 (이번 범위 밖)
listObjectsV2호출이 코드에 아예 없어 정리 수단 자체가 없습니다. 새로 생긴 문제가 아닙니다.🤖 Generated with Claude Code