Skip to content

feat: 기숙사 인증 사진 업로드 API#81

Open
vyfhfhd wants to merge 11 commits into
developfrom
feature/user-profile-modify
Open

feat: 기숙사 인증 사진 업로드 API#81
vyfhfhd wants to merge 11 commits into
developfrom
feature/user-profile-modify

Conversation

@vyfhfhd

@vyfhfhd vyfhfhd commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

작업 내용

  • [feat] 기숙사 인증 API #67
  • 기숙사 인증 사진 업로드/제출: presign(POST /me/dormitory-verification/presign) → 브라우저 S3 직접 PUT → confirm(POST /me/dormitory-verification/confirm) 3단계 흐름 구현
  • 프로필 사진 업로드도 동일 패턴으로 구현(POST /me/profile-image/presign, /confirm, 공개 버킷)
  • 기숙사 인증 학기 만료 처리: calcSemesterEnd 계산 로직 + 매일 00:00 배치(EXPIRED 전환 + S3 이미지 삭제)
  • 기숙사 인증 승인/반려는 MVP 방침대로 API 없이 관리자 DB 직접 UPDATE로 처리 - 실수 방지용 SQL 가이드 문서(docs/operations/dormitory-verification-admin-guide.md) + dorm_verified_until 계산용 보조 API(GET /api/v1/internal/dorm-semester-end) 추가
  • PATCH /me/profile의 profileImageUrl을 삭제(빈 값) 전용으로 제한 - 프로필 사진 설정은 업로드 API로만 가능하도록 변경

변경 사항

  • UserController: dormitory-verification/profile-image presign·confirm 엔드포인트 추가
  • UserService: 공용 ImageStorageService(ImageCategory.DORMITORY_VERIFICATION/PROFILE) 사용, 소유권(key가 본인 몫인지) 검증은 공용 모듈에 없어서 서비스단에 유지
  • Dormitory/DormStatus: EXPIRED 상태 처리, DormVerificationExpiryScheduler 추가
  • DormSemesterCalculator: 학기 만료일 계산 유틸(+단위 테스트)
  • InternalToolController: dorm-semester-end 계산기(인증 불필요, 조회/변경 없음)
  • SecurityConfig: /api/v1/internal/** permitAll 추가
  • 마이그레이션 변경 없음(기존 dormitory 테이블 컬럼으로 충분)

리뷰 포인트

  • 기숙사 인증/프로필 이미지 confirm 시 key 소유권 검증(belongsToUser)은 공용 confirmUpload가 안 해줘서 UserService에 직접 구현 - 이 부분 로직 검토 부탁
  • 관리자 승인 관련: 정식 API 없이 수동 SQL로 처리하는 방식이 맞는지, 물량 늘어나면 API화할지 논의 필요

체크리스트

  • 로컬에서 실행 확인
  • Swagger 문서 갱신

Summary by CodeRabbit

  • New Features
    • Added profile image presign/confirm upload flows (JPEG/PNG/WEBP) plus clear-on-empty behavior and size limits.
    • Added dormitory verification image presign/confirm upload and resubmission flows with duplicate/in-progress protection.
    • Added an unauthenticated internal endpoint to calculate dormitory verification semester end timestamps.
  • Documentation
    • Added an administrator guide for manually approving/rejecting dormitory verification requests.
  • Bug Fixes
    • Improved reliability of daily expiration processing for overdue dormitory verifications.

@vyfhfhd vyfhfhd self-assigned this Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds presigned profile and dormitory verification image uploads, restricts profile updates to deletion-only image changes, introduces semester-end calculation and expiry processing, exposes an internal calculation endpoint, and documents manual dormitory approval and rejection procedures.

Changes

Dormitory verification workflow

Layer / File(s) Summary
Upload contracts and controller wiring
src/main/java/com/leets/tdd/user/controller/UserController.java, src/main/java/com/leets/tdd/user/dto/*, src/main/java/com/leets/tdd/user/exception/UserErrorCode.java, src/test/java/com/leets/tdd/user/controller/UserControllerTest.java
Adds validated presign/confirm request and response DTOs, four authenticated upload endpoints, deletion-only profile image validation, new error codes, and controller coverage for authorization and validation cases.
Storage-backed upload processing
src/main/java/com/leets/tdd/user/service/UserService.java, src/main/java/com/leets/tdd/user/domain/{User,Dormitory}.java, src/main/resources/application.yaml, src/test/java/com/leets/tdd/user/service/UserServiceTest.java
Adds profile and dormitory verification presign/confirm flows, key ownership checks, storage confirmation and deletion handling, dormitory state updates, and multipart limits with service tests.
Semester calculation and verification expiry
src/main/java/com/leets/tdd/user/domain/DormSemesterCalculator.java, src/main/java/com/leets/tdd/user/controller/InternalToolController.java, src/main/java/com/leets/tdd/user/scheduler/*, src/main/java/com/leets/tdd/user/service/DormVerificationExpiryService.java, src/main/java/com/leets/tdd/global/config/SecurityConfig.java, src/test/java/com/leets/tdd/user/{domain,controller,scheduler}/*
Calculates semester end timestamps, exposes an unauthenticated internal endpoint, and expires overdue approved verifications with associated image deletion.
Operational procedures and regression coverage
docs/operations/dormitory-verification-admin-guide.md, src/test/java/com/leets/tdd/user/*
Documents manual approval and rejection SQL procedures and adds coverage for semester boundaries, leap years, expiry handling, and upload scenarios.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Leets-Official/TDD-BE#73 — The PR uses ImageStorageService for profile and dormitory verification image presign, confirmation, and deletion flows.
  • Leets-Official/TDD-BE#67 — The PR implements dormitory verification APIs, semester-end calculation, and expiry scheduling.

Possibly related PRs

Suggested reviewers: daekyochung, thesnackoverflow, handoa01

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches a major part of the PR, but it omits the profile photo upload and supporting admin/internal changes.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/user-profile-modify

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
src/test/java/com/leets/tdd/user/controller/InternalToolControllerTest.java (1)

49-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the no-date calculation, not only field presence.

The test passes for any non-null timestamp, so regressions in default-date calculation or serialization would go unnoticed. Inject a fixed Clock/business date and assert the expected semester end.

🤖 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 `@src/test/java/com/leets/tdd/user/controller/InternalToolControllerTest.java`
around lines 49 - 55, Strengthen calcDormSemesterEnd_withoutDateParam_returns200
by injecting a fixed Clock or business date into the controller’s
date-calculation path, then assert that data.dorm_verified_until equals the
expected semester-end value for that date instead of only checking field
existence. Preserve the existing successful-response assertion.
src/main/java/com/leets/tdd/global/config/SecurityConfig.java (1)

29-31: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Narrow the anonymous matcher to the calculator route.

"/api/v1/internal/**" permits every current and future endpoint below this prefix, including any future data-changing tool. Permit only GET /api/v1/internal/dorm-semester-end, or verify that all endpoints under this prefix are intentionally public.

Proposed narrowing
-            "/api/v1/internal/**"
+            "/api/v1/internal/dorm-semester-end"
🤖 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 `@src/main/java/com/leets/tdd/global/config/SecurityConfig.java` around lines
29 - 31, Update the anonymous authorization matcher in SecurityConfig to permit
only GET requests for /api/v1/internal/dorm-semester-end instead of the entire
/api/v1/internal/** prefix. Preserve the existing public WebSocket route and
require authentication for other internal endpoints.
src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java (1)

35-46: 🚀 Performance & Scalability | 🔵 Trivial

Process overdue records in bounded batches.

The repository returns every overdue row as one List, while the transaction remains open during network calls to S3. A large backlog can cause memory pressure, long locks, or transaction timeouts. Use pagination/chunking and independent cleanup processing.

🤖 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
`@src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java`
around lines 35 - 46, Update the overdue-processing flow in
DormVerificationExpiryScheduler to fetch and process records in bounded pages or
chunks instead of loading the full result from
findByDormStatusAndDormVerifiedUntilBefore. Ensure each batch is processed
independently, including dormitory expiration and dormVerificationImageKey
deletion, with transactions scoped per batch so S3 calls do not keep one
transaction open across the entire backlog.
src/test/java/com/leets/tdd/user/controller/UserControllerTest.java (1)

249-333: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover invalid request validation at the controller boundary.

Add MockMvc cases for an unsupported contentType and blank key, asserting 400 and no UserService invocation. The current tests only exercise valid payloads and service-thrown errors.

🤖 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 `@src/test/java/com/leets/tdd/user/controller/UserControllerTest.java` around
lines 249 - 333, Extend the controller tests around
presignDormVerificationUpload_success_returns200 and
confirmDormVerificationUpload_success_returns200 with MockMvc cases for an
unsupported contentType and a blank key. Assert each request returns 400 and
verify the corresponding UserService method is never invoked, covering
validation before service execution.
src/main/resources/application.yaml (1)

27-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused multipart limits.

Uploads flow through presigned S3 PUTs, and no multipart upload handlers remain in the application; move size validation to the presigned-url/confirm flow if limits should still be enforced.

🤖 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 `@src/main/resources/application.yaml` around lines 27 - 30, Remove the
servlet.multipart max-file-size and max-request-size properties from the
application configuration. Do not add replacement multipart handling; retain any
upload size validation only in the existing presigned URL or confirmation flow
if enforcement is required.
🤖 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 `@docs/operations/dormitory-verification-admin-guide.md`:
- Around line 21-24: Add an appropriate fenced-code language identifier, such as
http or text, to the code block containing the dorm-semester-end API example,
while preserving its request and response content.
- Around line 29-35: Update the dormitory approval SQL so the named parameter in
dorm_verified_until is unquoted, using :dormVerifiedUntil directly as the bind
value; preserve the existing update behavior and other assignments.

In `@src/main/java/com/leets/tdd/user/controller/InternalToolController.java`:
- Around line 39-40: The business-time handling is inconsistent because semester
calculation and expiry checks use the JVM default timezone. In
InternalToolController, replace the omitted-date fallback with an explicit
Asia/Seoul clock/time source; in DormVerificationExpiryScheduler, use the same
Asia/Seoul time source for dormVerifiedUntil comparisons and configure the
scheduled job with zone = "Asia/Seoul" so its midnight execution uses Seoul
wall-clock time.

In `@src/main/java/com/leets/tdd/user/dto/ProfileUpdateRequest.java`:
- Around line 27-28: Update UserService.updateProfile and the
ProfileUpdateRequest flow so an omitted profileImageUrl (null) preserves the
existing image, while an explicit empty string clears it before calling
User.updateProfile. Add a regression test covering nickname/dormitory-only
updates with an existing profile image and asserting the image remains
unchanged.

In
`@src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java`:
- Around line 33-46: Separate the database update flow in
expireOverdueVerifications from S3 cleanup: persist the EXPIRED state within the
transaction, then enqueue each non-null dorm verification image key for an
after-commit/outbox worker. Perform imageStorageService.delete through that
worker with idempotent retry handling, so rollbacks or later failures cannot
leave S3 deletion ahead of the database state.

In `@src/main/java/com/leets/tdd/user/service/UserService.java`:
- Around line 161-179: Update UserService.confirmProfileImageUpload at
src/main/java/com/leets/tdd/user/service/UserService.java:161-179 to capture
user.getProfileImageUrl() before user.updateProfileImageKey(key), then call
imageStorageService.delete(oldKey) after updating when the old key is non-null
and differs from key. Apply the same cleanup in the dormitory resubmit branch at
src/main/java/com/leets/tdd/user/service/UserService.java:208-240 by capturing
dormitory.getDormVerificationImageKey() before dormitory.resubmit(...) and
deleting it afterward under the same conditions.

---

Nitpick comments:
In `@src/main/java/com/leets/tdd/global/config/SecurityConfig.java`:
- Around line 29-31: Update the anonymous authorization matcher in
SecurityConfig to permit only GET requests for
/api/v1/internal/dorm-semester-end instead of the entire /api/v1/internal/**
prefix. Preserve the existing public WebSocket route and require authentication
for other internal endpoints.

In
`@src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java`:
- Around line 35-46: Update the overdue-processing flow in
DormVerificationExpiryScheduler to fetch and process records in bounded pages or
chunks instead of loading the full result from
findByDormStatusAndDormVerifiedUntilBefore. Ensure each batch is processed
independently, including dormitory expiration and dormVerificationImageKey
deletion, with transactions scoped per batch so S3 calls do not keep one
transaction open across the entire backlog.

In `@src/main/resources/application.yaml`:
- Around line 27-30: Remove the servlet.multipart max-file-size and
max-request-size properties from the application configuration. Do not add
replacement multipart handling; retain any upload size validation only in the
existing presigned URL or confirmation flow if enforcement is required.

In `@src/test/java/com/leets/tdd/user/controller/InternalToolControllerTest.java`:
- Around line 49-55: Strengthen calcDormSemesterEnd_withoutDateParam_returns200
by injecting a fixed Clock or business date into the controller’s
date-calculation path, then assert that data.dorm_verified_until equals the
expected semester-end value for that date instead of only checking field
existence. Preserve the existing successful-response assertion.

In `@src/test/java/com/leets/tdd/user/controller/UserControllerTest.java`:
- Around line 249-333: Extend the controller tests around
presignDormVerificationUpload_success_returns200 and
confirmDormVerificationUpload_success_returns200 with MockMvc cases for an
unsupported contentType and a blank key. Assert each request returns 400 and
verify the corresponding UserService method is never invoked, covering
validation before service execution.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8424c86-878c-400e-aaf0-41ddd2087834

📥 Commits

Reviewing files that changed from the base of the PR and between d0a68b4 and 243ff4e.

📒 Files selected for processing (27)
  • docs/operations/dormitory-verification-admin-guide.md
  • src/main/java/com/leets/tdd/global/config/SecurityConfig.java
  • src/main/java/com/leets/tdd/user/controller/InternalToolController.java
  • src/main/java/com/leets/tdd/user/controller/UserController.java
  • src/main/java/com/leets/tdd/user/domain/DormSemesterCalculator.java
  • src/main/java/com/leets/tdd/user/domain/Dormitory.java
  • src/main/java/com/leets/tdd/user/domain/User.java
  • src/main/java/com/leets/tdd/user/dto/DormSemesterEndResponse.java
  • src/main/java/com/leets/tdd/user/dto/DormVerificationConfirmRequest.java
  • src/main/java/com/leets/tdd/user/dto/DormVerificationPresignRequest.java
  • src/main/java/com/leets/tdd/user/dto/DormVerificationPresignResponse.java
  • src/main/java/com/leets/tdd/user/dto/DormVerificationUploadResponse.java
  • src/main/java/com/leets/tdd/user/dto/ProfileImageConfirmRequest.java
  • src/main/java/com/leets/tdd/user/dto/ProfileImagePresignRequest.java
  • src/main/java/com/leets/tdd/user/dto/ProfileImagePresignResponse.java
  • src/main/java/com/leets/tdd/user/dto/ProfileImageUploadResponse.java
  • src/main/java/com/leets/tdd/user/dto/ProfileUpdateRequest.java
  • src/main/java/com/leets/tdd/user/exception/UserErrorCode.java
  • src/main/java/com/leets/tdd/user/repository/DormitoryRepository.java
  • src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java
  • src/main/java/com/leets/tdd/user/service/UserService.java
  • src/main/resources/application.yaml
  • src/test/java/com/leets/tdd/user/controller/InternalToolControllerTest.java
  • src/test/java/com/leets/tdd/user/controller/UserControllerTest.java
  • src/test/java/com/leets/tdd/user/domain/DormSemesterCalculatorTest.java
  • src/test/java/com/leets/tdd/user/scheduler/DormVerificationExpirySchedulerTest.java
  • src/test/java/com/leets/tdd/user/service/UserServiceTest.java

Comment thread docs/operations/dormitory-verification-admin-guide.md
Comment on lines +29 to +35
UPDATE dormitory
SET dorm_status = 'APPROVED',
dorm_verified_at = NOW(),
dorm_verified_until = ':dormVerifiedUntil', -- 1번에서 받은 값 그대로
reject_reason = NULL
WHERE user_id = :userId
AND dorm_status = 'PENDING';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remove the quotes around the named SQL parameter.

':dormVerifiedUntil' is interpreted as a literal string by named-parameter SQL clients, not as a bind parameter. Use dorm_verified_until = :dormVerifiedUntil; if literal substitution is intended, document replacing it with a properly quoted timestamp value.

Proposed correction
-       dorm_verified_until = ':dormVerifiedUntil',  -- 1번에서 받은 값 그대로
+       dorm_verified_until = :dormVerifiedUntil,    -- 1번에서 받은 값 그대로
📝 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.

Suggested change
UPDATE dormitory
SET dorm_status = 'APPROVED',
dorm_verified_at = NOW(),
dorm_verified_until = ':dormVerifiedUntil', -- 1번에서 받은 값 그대로
reject_reason = NULL
WHERE user_id = :userId
AND dorm_status = 'PENDING';
UPDATE dormitory
SET dorm_status = 'APPROVED',
dorm_verified_at = NOW(),
dorm_verified_until = :dormVerifiedUntil, -- 1번에서 받은 값 그대로
reject_reason = NULL
WHERE user_id = :userId
AND dorm_status = 'PENDING';
🤖 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 `@docs/operations/dormitory-verification-admin-guide.md` around lines 29 - 35,
Update the dormitory approval SQL so the named parameter in dorm_verified_until
is unquoted, using :dormVerifiedUntil directly as the bind value; preserve the
existing update behavior and other assignments.

Comment on lines +39 to +40
LocalDateTime verifiedAt = (date != null ? date : LocalDate.now()).atStartOfDay();
LocalDateTime semesterEnd = DormSemesterCalculator.calcSemesterEnd(verifiedAt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n 'spring\.jpa.*timezone|user\.timezone|TimeZone|ZoneId|TZ|`@Scheduled`' \
  src/main/resources/application.yaml src/main/java

Repository: Leets-Official/TDD-BE

Length of output: 392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant files =="
git ls-files 'src/main/java/com/leets/tdd/user/controller/InternalToolController.java' \
  'src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java' \
  'src/main/java/com/leets/tdd/user/**' \
  'src/main/java/com/leets/tdd/global/**' \
  'src/main/resources/**' \
  'build.gradle' 'build.gradle.kts' 'pom.xml'

echo
echo "== application configs =="
for f in $(git ls-files 'src/main/resources/**'); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo
echo "== controller excerpt =="
sed -n '1,90p' src/main/java/com/leets/tdd/user/controller/InternalToolController.java | cat -n

echo
echo "== scheduler excerpts =="
sed -n '1,90p' src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java | cat -n
sed -n '1,90p' src/main/java/com/leets/tdd/global/jwt/RefreshTokenCleanupScheduler.java | cat -n

echo
echo "== dorm semester calculator excerpt =="
rg -n "class DormSemesterCalculator|calcSemesterEnd|verifiedAt" src/main/java
sed -n '1,160p' src/main/java/com/leets/tdd/user/calculator/DormSemesterCalculator.java | cat -n

echo
echo "== repository query =="
rg -n "findByDormStatusAndDormVerifiedUntilBefore|verifyUntil|DormStatus" src/main/java/com/leets/tdd/user src/main/java/com/leets/tdd/global -g '*.java'

Repository: Leets-Official/TDD-BE

Length of output: 28830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DormSemesterCalculator =="
cat -n src/main/java/com/leets/tdd/user/domain/DormSemesterCalculator.java

echo
echo "== scheduler configuration search =="
rg -n "EnableScheduling|spring\.task\s|\btask\s" src/main/java src/test/java build.gradle pom.xml || true

echo
echo "== repository query implementation =="
sed -n '1,180p' src/main/java/com/leets/tdd/user/repository/DormitoryRepository.java | cat -n

echo
echo "== Dormitory entity date fields =="
rg -n "dormVerifiedUntil|verified_at|dorm_verified_until|LocalDateTime|LocalDate" src/main/java/com/leets/tdd/user/domain/Dormitory.java src/main/java/com/leets/tdd/user/repository/DormitoryRepository.java src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java -C 2

Repository: Leets-Official/TDD-BE

Length of output: 8885


Use a consistent business “now” for semester/expiry checks.

InternalToolController defaults omitted dates with LocalDate.now() (JVM default timezone) before calculating semester end, and DormVerificationExpiryScheduler compares dormVerifiedUntil with LocalDateTime.now(). Since the app sets jackson.time-zone: Asia/Seoul and the cron is wall-clock midnight, use an explicit Asia/Seoul clock/@Scheduled(zone = "Asia/Seoul") so semester calculation and daily expiry don’t drift with the JVM default timezone.

📍 Affects 2 files
  • src/main/java/com/leets/tdd/user/controller/InternalToolController.java#L39-L40 (this comment)
  • src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java#L32-L36
🤖 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 `@src/main/java/com/leets/tdd/user/controller/InternalToolController.java`
around lines 39 - 40, The business-time handling is inconsistent because
semester calculation and expiry checks use the JVM default timezone. In
InternalToolController, replace the omitted-date fallback with an explicit
Asia/Seoul clock/time source; in DormVerificationExpiryScheduler, use the same
Asia/Seoul time source for dormVerifiedUntil comparisons and configure the
scheduled job with zone = "Asia/Seoul" so its midnight execution uses Seoul
wall-clock time.

Comment on lines +27 to +28
// null이거나 빈 문자열이어야만 통과한다(프로필 사진 해제 전용) - 값이 있으면 무조건 거부.
@Pattern(regexp = "^$", message = "프로필 사진은 업로드 API로만 설정할 수 있습니다. 여기서는 삭제(빈 값)만 가능합니다.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Every profile edit now silently wipes the user's photo — no way to preserve it.

Since this constraint only ever lets profileImageUrl be null or "", and UserService.updateProfile maps both to null unconditionally before calling User.updateProfile(nickname, null), calling PATCH /me/profile to change just the nickname or dormitory will always clear the existing profile photo. Before this PR a client could resend its current URL to no-op preserve the image; that workaround is now blocked, so there is no remaining way to edit nickname/dormitory without losing the photo.

Consider distinguishing "field omitted" (null → leave unchanged) from an explicit clear signal ("" → delete), e.g. in UserService.updateProfile:

💡 Proposed fix
-        String newProfileImageUrl = request.profileImageUrl();
-        user.updateProfile(newNickname, newProfileImageUrl == null || newProfileImageUrl.isBlank()
-                ? null : newProfileImageUrl);
+        String newProfileImageUrl = request.profileImageUrl();
+        if (newProfileImageUrl == null) {
+            user.updateProfile(newNickname, user.getProfileImageUrl());
+        } else {
+            user.updateProfile(newNickname, newProfileImageUrl.isBlank() ? null : newProfileImageUrl);
+        }

Please also add a regression test asserting that an existing profileImageUrl survives a nickname/dormitory-only update.

🤖 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 `@src/main/java/com/leets/tdd/user/dto/ProfileUpdateRequest.java` around lines
27 - 28, Update UserService.updateProfile and the ProfileUpdateRequest flow so
an omitted profileImageUrl (null) preserves the existing image, while an
explicit empty string clears it before calling User.updateProfile. Add a
regression test covering nickname/dormitory-only updates with an existing
profile image and asserting the image remains unchanged.

Comment on lines +33 to +46
@Transactional
public void expireOverdueVerifications() {
List<Dormitory> overdue = dormitoryRepository
.findByDormStatusAndDormVerifiedUntilBefore(DormStatus.APPROVED, LocalDateTime.now());
if (overdue.isEmpty()) {
return;
}

for (Dormitory dormitory : overdue) {
String imageKey = dormitory.getDormVerificationImageKey();
dormitory.expire();
if (imageKey != null) {
imageStorageService.delete(imageKey);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not perform S3 deletion inside the database transaction.

@Transactional can roll back the EXPIRED updates, but it cannot roll back imageStorageService.delete(...). If a later deletion or database commit fails, S3 and the database can disagree. Move cleanup to an outbox/after-commit worker with idempotent retries, or otherwise separate expiry persistence from storage deletion.

🤖 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
`@src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java`
around lines 33 - 46, Separate the database update flow in
expireOverdueVerifications from S3 cleanup: persist the EXPIRED state within the
transaction, then enqueue each non-null dorm verification image key for an
after-commit/outbox worker. Perform imageStorageService.delete through that
worker with idempotent retry handling, so rollbacks or later failures cannot
leave S3 deletion ahead of the database state.

Comment thread src/main/java/com/leets/tdd/user/service/UserService.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@src/test/java/com/leets/tdd/user/service/UserServiceTest.java`:
- Around line 478-491: Update confirmProfileImageUpload so deletion of the
previous profile image key occurs only after the transaction commits, using the
existing transactional event or synchronization mechanism rather than calling
imageStorageService.delete synchronously. Revise
confirmProfileImageUpload_replacesExistingPhoto_deletesOldKey to verify deletion
is scheduled/deferred and executed after commit.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60c597fc-2155-44ac-b39b-ca5faa5aee53

📥 Commits

Reviewing files that changed from the base of the PR and between 243ff4e and 263dc48.

📒 Files selected for processing (7)
  • docs/operations/dormitory-verification-admin-guide.md
  • src/main/java/com/leets/tdd/user/scheduler/DormVerificationExpiryScheduler.java
  • src/main/java/com/leets/tdd/user/service/DormVerificationExpiryService.java
  • src/main/java/com/leets/tdd/user/service/UserService.java
  • src/test/java/com/leets/tdd/user/scheduler/DormVerificationExpirySchedulerTest.java
  • src/test/java/com/leets/tdd/user/service/DormVerificationExpiryServiceTest.java
  • src/test/java/com/leets/tdd/user/service/UserServiceTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/operations/dormitory-verification-admin-guide.md
  • src/main/java/com/leets/tdd/user/service/UserService.java

Comment on lines +478 to +491
@Test
@DisplayName("기존 프로필 사진이 있었으면 새 사진 반영 후 예전 사진을 S3에서 지운다")
void confirmProfileImageUpload_replacesExistingPhoto_deletesOldKey() {
User user = newUser();
user.updateProfileImageKey("profiles/1/old.jpg");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
when(imageStorageService.resolveViewUrl(PROFILE_KEY))
.thenReturn("https://public.example.com/" + PROFILE_KEY);

userService.confirmProfileImageUpload(1L, new ProfileImageConfirmRequest(PROFILE_KEY));

assertThat(user.getProfileImageUrl()).isEqualTo(PROFILE_KEY);
verify(imageStorageService).delete("profiles/1/old.jpg");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate test and service files =="
git ls-files | rg 'src/test/java/com/leets/tdd/user/service/UserServiceTest\.java|com/leets/tdd/user/service/UserService\.java|imageStorageService|Repository' | sed -n '1,200p'

echo
echo "== test lines =="
sed -n '440,510p' src/test/java/com/leets/tdd/user/service/UserServiceTest.java

echo
echo "== service references =="
rg -n "confirmProfileImageUpload|Transactional|`@Tx`|ProfileImage|userRepository|imageStorageService|save|delete" src/main src/test -g '*.java' | sed -n '1,260p'

Repository: Leets-Official/TDD-BE

Length of output: 38531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== UserService confirm method =="
sed -n '150,195p' src/main/java/com/leets/tdd/user/service/UserService.java

echo
echo "== User profile image methods =="
sed -n '220,245p' src/main/java/com/leets/tdd/user/domain/User.java

echo
echo "== Test setup and imports around UserServiceTest =="
sed -n '1,110p' src/test/java/com/leets/tdd/user/service/UserServiceTest.java

echo
echo "== imports for Spring Transaction in test/service =="
rg -n "import org\.springframework\.transaction|TestableTransactionManager|AbstractTransactionTest|TransactionalTestExecutionListener|Transactional|`@TestableTransaction`\b" src/test src/main -g '*.java' | sed -n '1,240p'

Repository: Leets-Official/TDD-BE

Length of output: 13523


🌐 Web query:

Spring Framework TransactionAwareDataSourcePlatformTransactionManager after commit callback TransactionSynchronizationManager registerSynchronization

💡 Result:

In the Spring Framework, you can register an after-commit callback by implementing the TransactionSynchronization interface and registering it with the TransactionSynchronizationManager [1][2]. The TransactionSynchronization interface provides the afterCommit method, which is invoked immediately after a transaction has successfully committed [3][4]. This is the appropriate place to perform post-commit actions, such as sending confirmation emails or triggering follow-up tasks that should only occur if the main transaction is successful [2][4]. To register your synchronization, use the following approach: TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization { @Override public void afterCommit { // Perform post-commit actions here } }); Important Considerations: 1. Lifecycle: While the main transaction has already committed when afterCommit is called, the transactional resources (like database connections) are often still active and accessible [2][5]. 2. Participating in Transactions: Because resources may still be active, any data access code executed within afterCommit will "participate" in the original transaction, meaning no further commits will occur for it [3][2]. If you need to perform additional transactional work that requires its own transaction, you must explicitly use PROPAGATION_REQUIRES_NEW [2][4]. 3. Registration: You must ensure that transaction synchronization is active before calling registerSynchronization by checking TransactionSynchronizationManager.isSynchronizationActive [1][6]. If it is not active, the registration will throw an IllegalStateException [1][7]. 4. Callback Order: If you have multiple synchronizations registered, they are executed in the order of their assigned priority if they implement the Ordered interface [1][6]. The TransactionAwareDataSourcePlatformTransactionManager (and other Spring transaction managers) handles these callbacks automatically through their internal commit process, which triggers the registered synchronizations once the actual transaction commit has finished [8][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Repository implementation/transaction config candidates =="
git ls-files | rg 'src/(main|test)/java|src/(main|test)/resources' | rg 'Transaction\.(java|properties$|xml$)|config|jpa|database|Testcontainers|Mockito|`@Enable`' | sed -n '1,200p'

echo
echo "== build dependency for testcontainers/spring test =="
if [ -f build.gradle ]; then cat build.gradle | sed -n '1,220p'; fi
if [ -f build.gradle.kts ]; then cat build.gradle.kts | sed -n '1,220p'; fi

echo
echo "== any existing after-commit / event / transaction sync usages =="
rg -n "TransactionSynchronization|afterCommit|ApplicationEventPublisher|EventTransaction|TransactionalEventListener|afterCommit|TestableTransactionManager|MockExternalResource|TransactionTemplate|MockitoExtension|TestTemplate" src/main src/test -g '*.java' | sed -n '1,200p'

Repository: Leets-Official/TDD-BE

Length of output: 6084


Defer old profile-image deletion to an after-commit cleanup.

confirmProfileImageUpload is transactional, so userRepository.save(user) is only committed after the method returns. Calling imageStorageService.delete(previousKey) in the transaction puts the new profile at risk of pointing to a deleted S3 object if the transaction rolls back afterward. Move the delete out of the transaction, e.g. a transactional event/synchronization handler, and make this test assert that deletion is scheduled/deferred instead of emitted synchronously.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 484-484: Avoid building a URL host from untrusted input
Context: "https://public.example.com/" + PROFILE_KEY
Note: [CWE-20] Improper Input Validation.

(tainted-url-host)

🤖 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 `@src/test/java/com/leets/tdd/user/service/UserServiceTest.java` around lines
478 - 491, Update confirmProfileImageUpload so deletion of the previous profile
image key occurs only after the transaction commits, using the existing
transactional event or synchronization mechanism rather than calling
imageStorageService.delete synchronously. Revise
confirmProfileImageUpload_replacesExistingPhoto_deletesOldKey to verify deletion
is scheduled/deferred and executed after commit.

@HandoA01 HandoA01 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다!

Comment on lines +390 to +401
private boolean belongsToUser(ImageCategory category, Long userId, String key) {
if (key == null) {
return false;
}
String expectedPrefix = "%s/%d/".formatted(category.getPrefix(), userId);
if (!key.startsWith(expectedPrefix)) {
return false;
}
String remainder = key.substring(expectedPrefix.length());
return !remainder.isEmpty() && !remainder.contains("/") && !remainder.contains("..");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 소유권 검증 방식 참고하겠습니다. 채팅 이미지는 여기가 "이 유저가 이 팟 참여자인지"(party_participants 조회) 검증으로 바뀔 것 같은데, 참여 API 나오면 비슷한 위치에 넣으려고 합니다. 수고하셨어요!

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.

2 participants