Skip to content

fix: 프로젝트 카드 활동 시각 및 프로필 이미지 업로드 지원 - #125

Merged
chazy-d merged 7 commits into
developfrom
fix/project-card-profile-image
Aug 6, 2026
Merged

fix: 프로젝트 카드 활동 시각 및 프로필 이미지 업로드 지원#125
chazy-d merged 7 commits into
developfrom
fix/project-card-profile-image

Conversation

@chazy-d

@chazy-d chazy-d commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈 (Related Issue)

📝 작업 내용

홈 화면의 진행 중인 프로젝트 카드가 프로젝트 수정 시각이 아니라 실제 최근 활동의 최신 발생 시각을 받을 수 있도록 조회 로직을 보완했습니다. 또한 사용자가 프로필 이미지를 업로드하면 S3에 저장하고, 화면에서 바로 사용할 CDN 공개 URL을 반환하는 API를 추가했습니다. 최근활동 읽음 처리 관련 스키마 변경도 Flyway 기반으로 자동화했습니다.

주요 검토 파일

프로젝트 목록 최근활동 시각

  • src/main/java/com/slatto/domain/notification/repository/ActivityLogRepository.java - 프로젝트별 최신 활동 시각 집계 쿼리 추가
  • src/main/java/com/slatto/domain/project/service/ProjectService.java - 프로젝트 목록의 lastActivityAt을 활동 로그 기준으로 변경
  • src/test/java/com/slatto/domain/notification/repository/ActivityLogRepositoryIntegrationTest.java - 프로젝트별 최신 활동 시각 조회 통합 테스트 추가

프로필 이미지 업로드

  • src/main/java/com/slatto/domain/user/controller/UserController.java - 프로필 이미지 업로드 API 추가
  • src/main/java/com/slatto/domain/user/service/UserService.java - 이미지 검증, S3 저장, CDN URL 조합 및 기존 이미지 정리 처리
  • src/main/java/com/slatto/domain/user/entity/Users.java - 프로필 이미지 URL 변경 도메인 메서드 추가
  • src/test/java/com/slatto/domain/user/service/UserProfileImageServiceTest.java - 업로드 성공 및 허용하지 않는 형식 차단 테스트 추가

Flyway 마이그레이션 자동화

  • build.gradle / src/main/resources/application.yml - Flyway 활성화 및 기본 설정 추가
  • src/main/resources/db/migration/V009__activity_log_type.sql - activity_log.type 컬럼을 VARCHAR(50)로 전환
  • src/main/resources/db/migration/V010__project_activity_read.sql - 최근활동 개별 읽음 테이블 및 조회 인덱스 정리

1. 프로젝트 카드 최근활동 시각 정합성

프로젝트 목록 응답의 lastActivityAt을 프로젝트 updatedAt이 아닌 activity_log.created_at의 최신값으로 반환하도록 변경했습니다.

GET /api/v1/projects
  • 활동 로그가 있는 프로젝트는 가장 최신 활동 발생 시각을 반환합니다.
  • 활동 로그가 없는 프로젝트는 lastActivityAt: null을 반환합니다.
  • 프론트는 lastActivityAt과 현재 시각을 비교해 n분 전을 표시하고, 값이 null이면 문구를 숨깁니다.

2. 프로필 이미지 업로드 API

PUT /api/v1/users/me/profile-image
Content-Type: multipart/form-data
  • file 필드로 JPG, PNG, WebP 이미지를 최대 10MB까지 업로드할 수 있습니다.
  • 파일은 users/{userId}/profile-images/{uuid}.{extension} 경로로 저장합니다.
  • DB에는 조합된 공개 URL을 저장하고, 응답의 profileImageUrl로 CDN 공개 URL을 반환합니다.
  • 기존에 같은 방식으로 저장된 프로필 이미지가 있다면 새 이미지 저장과 DB 반영이 성공한 뒤 정리합니다.
  • DB 반영이 롤백되면 방금 업로드한 S3 객체를 정리합니다.

3. Flyway 기반 마이그레이션 자동화

자동 마이그레이션을 Flyway로 전환했고, 중복된 버전 번호도 정리했습니다.

  • ddl-auto=validate는 DB를 변경하지 않고 구조만 검증하도록 구성했습니다.
  • 기존 SQL은 자동 실행이 아니라 수동 반영된 상태였고, Flyway는 기존 RDS를 V8 기준으로 baseline 처리한 뒤 이후 V009, V010만 자동 적용하도록 정리했습니다.
  • activity_log.typeVARCHAR(50)으로 전환하는 V009를 추가했습니다.
  • 최근활동 개별 읽음 테이블 및 조회 인덱스는 V010으로 정리했습니다.
  • 중간 단계였던 last_activity_read_at 마이그레이션은 개별 읽음 테이블 방식으로 대체돼 제거했습니다.
  • 테스트 환경에서는 기존 H2 create-drop 방식을 유지하도록 Flyway를 비활성화했습니다.
  • 검증: ./gradlew compileJava, ./gradlew test 모두 통과했습니다.

참고: 빈 DB를 처음부터 구성하는 완전한 초기 스키마 마이그레이션은 아직 없고, 현재 구성은 이미 스키마가 있는 dev/prod RDS를 기준점으로 삼아 이후 변경을 안전하게 자동 적용하는 방식입니다.


3. 테스트 및 검증

  • 프로젝트별 활동 로그가 여러 건일 때 가장 최신 createdAt만 조회되는지 통합 테스트를 추가했습니다.
  • 활동 로그가 없는 프로젝트가 집계 결과에서 제외되어 서비스 응답에서 null로 처리되는지 확인했습니다.
  • PNG 업로드 시 사용자 전용 storage key와 CDN URL이 생성되는지 서비스 테스트를 추가했습니다.
  • GIF 업로드가 S3 저장 전에 차단되는지 검증했습니다.
  • 기존 포트폴리오 수정 시각 테스트 컨텍스트에 StorageService mock을 추가해 전체 테스트 환경을 보완했습니다.

✅ PR 체크리스트

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • 변경 사항에 대한 테스트를 진행했습니다.

Summary by CodeRabbit

  • 새로운 기능
    • 프로필 이미지를 업로드하고 공개 이미지 URL로 확인할 수 있습니다.
    • 이미지 형식, 크기 및 누락 파일을 검증해 부적절한 업로드를 안내합니다.
    • 프로젝트 목록에서 실제 활동 로그를 기준으로 최신 활동 시각을 제공합니다.
  • 개선 사항
    • 데이터베이스 마이그레이션 관리와 스키마 변경 검증이 강화되었습니다.
    • 알림, 채용 지원, 프로젝트 고정 및 공지 읽음 상태 등 관련 데이터 처리가 안정화되었습니다.
  • 테스트
    • 프로필 이미지 업로드와 프로젝트별 최신 활동 시각 조회 검증이 추가되었습니다.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a3860b55-0cf9-4440-9b95-bae997bcb04c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

프로젝트 목록이 활동 로그의 최신 시각을 사용하도록 변경되었습니다. 프로필 이미지 멀티파트 업로드와 스토리지 정리 처리가 추가되었습니다. Flyway 설정과 V001~V010 데이터베이스 마이그레이션이 추가되었습니다.

Changes

프로젝트 최신 활동 시각

Layer / File(s) Summary
최신 활동 조회 계약
src/main/java/com/slatto/domain/notification/repository/...
프로젝트별 최대 createdAt을 반환하는 projection과 JPQL repository 메서드를 추가했습니다.
프로젝트 목록 연동
src/main/java/com/slatto/domain/project/service/ProjectService.java
프로젝트 목록의 활동 시각을 활동 로그 조회 결과로 설정했습니다. 기존 updatedAtcreatedAt fallback 로직을 제거했습니다.
최신 활동 조회 검증
src/test/java/com/slatto/domain/notification/repository/ActivityLogRepositoryIntegrationTest.java
프로젝트별 최신 시각과 활동이 없는 프로젝트의 결과 제외를 검증합니다.

Flyway 스키마 마이그레이션

Layer / File(s) Summary
Flyway 실행 기반
build.gradle, src/main/resources/application.yml, src/test/resources/application.yml
Flyway 의존성, 기준 버전 8, 마이그레이션 검증 및 테스트 비활성화 설정을 추가했습니다.
스키마 마이그레이션 정의
src/main/resources/db/migration/*
프로젝트 고정, 공지 읽음, 알림, 모집, 지원서, 활동 로그 관련 V001~V010 마이그레이션을 추가하거나 수정했습니다.

프로필 이미지 업로드

Layer / File(s) Summary
프로필 이미지 API 계약
src/main/java/com/slatto/domain/user/controller/UserController.java, src/main/java/com/slatto/domain/user/dto/UserProfileImageResponse.java, src/main/java/com/slatto/domain/user/entity/Users.java, src/main/java/com/slatto/domain/user/exception/UserErrorCode.java
PUT /api/v1/users/me/profile-image 엔드포인트와 응답 DTO를 추가했습니다. 사용자 이미지 갱신 메서드와 관련 오류 코드를 추가했습니다.
프로필 이미지 저장 흐름
src/main/java/com/slatto/domain/user/service/UserService.java, .env.example, src/main/resources/application.yml
파일 형식과 크기를 검증하고, 공개 URL을 생성하며, 업로드와 트랜잭션 후처리를 수행합니다.
프로필 이미지 동작 검증
src/test/java/com/slatto/domain/user/service/UserProfileImageServiceTest.java, src/test/java/com/slatto/domain/user/service/PortfolioUpdatedAtTest.java
PNG 업로드, CDN URL 반영, GIF 차단 및 테스트 의존성 구성을 검증합니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant UserController
  participant UserService
  participant StorageService
  participant Users
  Client->>UserController: 멀티파트 프로필 이미지 업로드
  UserController->>UserService: uploadProfileImage(userId, file)
  UserService->>StorageService: 이미지 객체 업로드
  UserService->>Users: 프로필 이미지 URL 갱신
  UserService-->>UserController: UserProfileImageResponse
  UserController-->>Client: 성공 응답
Loading

Possibly related PRs

  • SLAT-TO/SLATE-TO-BE#16: ProjectService.getProjects의 기존 활동 시각 계산을 이번 활동 로그 projection 조회로 확장합니다.

Suggested labels: feature

Suggested reviewers: sangwon02

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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.
Title check ✅ Passed 제목은 프로젝트 카드의 활동 시각 변경과 프로필 이미지 업로드라는 주요 변경 사항을 명확하게 요약합니다.
Description check ✅ Passed 관련 이슈, 작업 내용, 주요 변경 사항, 테스트 및 검증 결과를 포함해 템플릿 요구 사항을 대부분 충족합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@chazy-d chazy-d self-assigned this Aug 5, 2026
@chazy-d chazy-d added feature 새로운 기능 추가 fix 버그 수정 labels Aug 5, 2026

@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: 5

🧹 Nitpick comments (2)
src/test/resources/application.yml (1)

19-20: 🗄️ Data Integrity & Integration | 🔵 Trivial

MySQL Flyway 마이그레이션 검증 단계를 별도로 추가하세요.

Line 19의 spring.flyway.enabled: false와 H2 ddl-auto: create-drop 설정은 V001~V010을 실행하지 않습니다. 따라서 V008의 중복 데이터 처리, V009의 레거시 enum 값 처리, V010의 MySQL SQL과 데이터 이전 문제를 현재 테스트가 검증하지 못합니다.

H2 테스트는 유지하되, CI 또는 배포 단계에서 Testcontainers 같은 임시 MySQL 환경으로 Flyway 마이그레이션을 실행하세요.

🤖 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/resources/application.yml` around lines 19 - 20, 추가 테스트 구성을 마련해
MySQL Testcontainers 환경에서 Flyway 마이그레이션을 활성화한 채 V001~V010을 실행하고 검증하세요. 기존 H2
테스트와 spring.flyway.enabled: false 설정은 유지하되, CI 또는 배포 단계에서 V008 중복 데이터 처리, V009
레거시 enum 값 처리, V010 MySQL SQL 및 데이터 이전을 확인하도록 구성하세요.
src/main/resources/db/migration/V002__project_pin.sql (1)

12-13: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

복합 유니크 키의 선두 컬럼 인덱스를 중복 생성하지 마세요.

두 복합 유니크 키는 각각 첫 번째 컬럼의 단독 조건을 지원합니다. 전체 코드에서 단독 컬럼 조회가 없는지 확인한 뒤 별도 인덱스를 제거하세요.

  • src/main/resources/db/migration/V002__project_pin.sql#L12-L13: idx_project_pin_user_id를 제거하세요.
  • src/main/resources/db/migration/V003__project_notice_read.sql#L12-L14: idx_project_notice_read_notice_id를 제거하세요.

중복 인덱스는 쓰기 비용과 저장 공간을 증가시킵니다.

🤖 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/db/migration/V002__project_pin.sql` around lines 12 - 13,
Remove the redundant standalone indexes supporting the leading columns of the
composite unique keys after confirming no standalone-column queries require
them: remove idx_project_pin_user_id in
src/main/resources/db/migration/V002__project_pin.sql (lines 12-13) and
idx_project_notice_read_notice_id in
src/main/resources/db/migration/V003__project_notice_read.sql (lines 12-14).
🤖 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/main/java/com/slatto/domain/user/service/UserService.java`:
- Around line 47-53: 프로필 이미지의 multipart 허용 크기를 MAX_PROFILE_IMAGE_SIZE인 10 MiB와
일치시키십시오. spring.servlet.multipart의 max-file-size와 max-request-size를 10 MiB 기준으로
조정하거나, 해당 설정을 유지해야 한다면 업로드 요청에서 validateProfileImage 이전에 파일 크기를 직접 거절하도록 변경하십시오.
- Around line 222-223: Update the upload flow in UserService so
storageService.upload is wrapped in try/catch; if it throws after creating the
object, call deleteStorageObjectQuietly(storageKey, ...) before rethrowing the
original exception, while preserving
registerUploadedFileCleanupOnRollback(storageKey) for successful uploads.

In
`@src/main/resources/db/migration/V008__recruitment_application_active_unique.sql`:
- Around line 6-11: Before creating uq_recruitment_application_active in V008,
detect existing duplicate active applications using recruitment_id and user_id,
then resolve or backfill them according to the business rules so each active
pair is unique. Run the unique-key creation only after the data cleanup has
completed.

In `@src/main/resources/db/migration/V009__activity_log_type.sql`:
- Around line 4-5: Update V009__activity_log_type.sql to migrate existing
activity_log.type values before altering the column: inspect distinct values,
map legacy RoleName entries such as DIRECTOR, PM, and EDITOR to valid
ActivityLogType strings, and explicitly handle any unmappable rows so every
persisted value is compatible with ActivityLog.type’s ActivityLogType enum
before applying the VARCHAR(50) NOT NULL change.

In `@src/main/resources/db/migration/V010__project_activity_read.sql`:
- Around line 2-5: Update migration V010 so project_activity_read is created
before removing project_member.last_activity_read_at, then backfill one read
record per member from the legacy timestamp for all project activity at or
before that time. Execute the backfill before the DROP COLUMN statement,
preserving existing read status, and retain the current cleanup of the legacy
column as the final step.

---

Nitpick comments:
In `@src/main/resources/db/migration/V002__project_pin.sql`:
- Around line 12-13: Remove the redundant standalone indexes supporting the
leading columns of the composite unique keys after confirming no
standalone-column queries require them: remove idx_project_pin_user_id in
src/main/resources/db/migration/V002__project_pin.sql (lines 12-13) and
idx_project_notice_read_notice_id in
src/main/resources/db/migration/V003__project_notice_read.sql (lines 12-14).

In `@src/test/resources/application.yml`:
- Around line 19-20: 추가 테스트 구성을 마련해 MySQL Testcontainers 환경에서 Flyway 마이그레이션을
활성화한 채 V001~V010을 실행하고 검증하세요. 기존 H2 테스트와 spring.flyway.enabled: false 설정은 유지하되,
CI 또는 배포 단계에서 V008 중복 데이터 처리, V009 레거시 enum 값 처리, V010 MySQL SQL 및 데이터 이전을 확인하도록
구성하세요.
🪄 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: fc3b0c39-4fa3-4980-87d9-a8652de17a87

📥 Commits

Reviewing files that changed from the base of the PR and between c1e1a47 and 75cd92a.

📒 Files selected for processing (26)
  • .env.example
  • build.gradle
  • src/main/java/com/slatto/domain/notification/repository/ActivityLogRepository.java
  • src/main/java/com/slatto/domain/notification/repository/ProjectLatestActivityProjection.java
  • src/main/java/com/slatto/domain/project/service/ProjectService.java
  • src/main/java/com/slatto/domain/user/controller/UserController.java
  • src/main/java/com/slatto/domain/user/dto/UserProfileImageResponse.java
  • src/main/java/com/slatto/domain/user/entity/Users.java
  • src/main/java/com/slatto/domain/user/exception/UserErrorCode.java
  • src/main/java/com/slatto/domain/user/service/UserService.java
  • src/main/resources/application.yml
  • src/main/resources/db/migration/005-project-member-activity-read.sql
  • src/main/resources/db/migration/V001__auth_google_login.sql
  • src/main/resources/db/migration/V002__project_pin.sql
  • src/main/resources/db/migration/V003__project_notice_read.sql
  • src/main/resources/db/migration/V004__notification_group_count.sql
  • src/main/resources/db/migration/V005__recruitment_crud.sql
  • src/main/resources/db/migration/V006__recruitment_application_status.sql
  • src/main/resources/db/migration/V007__notification_project_nullable.sql
  • src/main/resources/db/migration/V008__recruitment_application_active_unique.sql
  • src/main/resources/db/migration/V009__activity_log_type.sql
  • src/main/resources/db/migration/V010__project_activity_read.sql
  • src/test/java/com/slatto/domain/notification/repository/ActivityLogRepositoryIntegrationTest.java
  • src/test/java/com/slatto/domain/user/service/PortfolioUpdatedAtTest.java
  • src/test/java/com/slatto/domain/user/service/UserProfileImageServiceTest.java
  • src/test/resources/application.yml
💤 Files with no reviewable changes (1)
  • src/main/resources/db/migration/005-project-member-activity-read.sql

Comment on lines +47 to +53
private static final long MAX_PROFILE_IMAGE_SIZE = 10L * 1024 * 1024;
private static final String PROFILE_IMAGE_STORAGE_KEY_FORMAT = "users/%d/profile-images/%s.%s";
private static final Map<String, Set<String>> ALLOWED_EXTENSIONS_BY_CONTENT_TYPE = Map.of(
"image/jpeg", Set.of("jpg", "jpeg"),
"image/png", Set.of("png"),
"image/webp", Set.of("webp")
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'multipart|max-file-size|max-request-size' \
  src/main/resources/application.yml src/test/resources/application.yml

rg -n -C 3 'PROFILE_IMAGE_SIZE_EXCEEDED|10 \* 1024 \* 1024|uploadProfileImage' \
  src/test/java

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 2886


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Service outline =="
ast-grep outline src/main/java/com/slatto/domain/user/service/UserService.java --match UserProfileImageService --view expanded || true

echo "== Relevant service lines =="
sed -n '1,120p' src/main/java/com/slatto/domain/user/service/UserService.java | cat -n

echo "== Relevant test file outline and lines =="
ast-grep outline src/test/java/com/slatto/domain/user/service/UserProfileImageServiceTest.java --view expanded || true
sed -n '1,140p' src/test/java/com/slatto/domain/user/service/UserProfileImageServiceTest.java | cat -n

echo "== Search size boundary tests =="
rg -n -C 3 'size|Size|contentLength|1024|10 .* 1024|10MB|10MiB|PROFILE_IMAGE_SIZE_EXPIRED|PROFILE_IMAGE_SIZE_EXCEEDED|uploadProfileImage' src/test-java src/test/java || true

echo "== Search multipart config files =="
git ls-files | rg '(^|/)(application.*\.ya?ml|application.*\.properties|web.*\.ya?ml|web.*\.properties)$' | while read -r f; do
  echo "--- $f"
  rg -n -C 2 'multipart|max-file-size|max-request-size' "$f" || true
done

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 19985


프로필 이미지 10 MiB 한도와 multipart 허용 범위를 일치시키십시오.

spring.servlet.multipart.max-file-size: 100MB, max-request-size: 105MBMAX_PROFILE_IMAGE_SIZE = 10L * 1024 * 1024보다 큽니다. validateProfileImage가 Spring multipart 파싱 이후에 실행되므로, 10 MiB 초과 파일이 이 체크까지 도달합니다. 허용 범위를 10 MiB 경계에 맞추거나, multipart 설정은 그대로 두고 업로드 요청에서 파일 크기를 직접 거절하도록 조정하십시오.

🤖 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/slatto/domain/user/service/UserService.java` around lines
47 - 53, 프로필 이미지의 multipart 허용 크기를 MAX_PROFILE_IMAGE_SIZE인 10 MiB와 일치시키십시오.
spring.servlet.multipart의 max-file-size와 max-request-size를 10 MiB 기준으로 조정하거나, 해당
설정을 유지해야 한다면 업로드 요청에서 validateProfileImage 이전에 파일 크기를 직접 거절하도록 변경하십시오.

Comment on lines +222 to +223
storageService.upload(file, storageKey);
registerUploadedFileCleanupOnRollback(storageKey);

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 UserService"
fd -a 'UserService.java$' . || true

echo "== outline UserService"
FILE="$(fd 'UserService.java$' . | head -n1 || true)"
if [ -n "${FILE:-}" ]; then
  echo "FILE=$FILE"
  wc -l "$FILE"
  ast-grep outline "$FILE" --view expanded | sed -n '1,220p' || true
  echo "== relevant snippets"
  sed -n '180,250p' "$FILE" | nl -ba -v180
  echo "== storage upload usages"
  rg -n "storageService\.upload|registerUploadedFileCleanupOnRollback|upload\(" "$FILE"
fi

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 1486


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="./src/main/java/com/slatto/domain/user/service/UserService.java"

echo "== lines 200-235"
awk 'NR>=200 && NR<=235 { printf "%6d  %s\n", NR, $0 }' "$FILE"

echo "== lines 303-350"
awk 'NR>=303 && NR<=350 { printf "%6d  %s\n", NR, $0 }' "$FILE"

echo "== storageService definition/usages"
rg -n "storageService|registerUploadedFileCleanupOnRollback|registerPreviousFileDeletionAfterCommit|deleteStorageObjectQuietly|`@Transactional`" "$FILE"

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 5043


업로드 실패 후에도 새 객체를 삭제하십시오.

storageService.upload(file, storageKey)가 S3 저장 후 예외를 던지면 registerUploadedFileCleanupOnRollback(storageKey)가 호출되지 않아 롤백에서 새 이미지가 삭제되지 않습니다. 해당 호출을 try/catch로 감싸 예외 상황에서 deleteStorageObjectQuietly(storageKey, ...)를 먼저 수행하고 다시 예외를 던지십시오.

🤖 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/slatto/domain/user/service/UserService.java` around lines
222 - 223, Update the upload flow in UserService so storageService.upload is
wrapped in try/catch; if it throws after creating the object, call
deleteStorageObjectQuietly(storageKey, ...) before rethrowing the original
exception, while preserving registerUploadedFileCleanupOnRollback(storageKey)
for successful uploads.

Comment on lines +4 to +5
ALTER TABLE activity_log
MODIFY COLUMN type VARCHAR(50) NOT NULL;

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 "== files =="
git ls-files | rg '(^|/)V009__activity_log_type\.sql$|ActivityLog|ActivityLogType|activity_log'

echo
echo "== V009 content =="
if [ -f src/main/resources/db/migration/V009__activity_log_type.sql ]; then
  cat -n src/main/resources/db/migration/V009__activity_log_type.sql
fi

echo
echo "== ActivityLog references =="
rg -n "class ActivityLog|ActivityLog|`@Enumerated`|EnumType\.STRING|activity_log|class ActivityLogType|enum ActivityLogType|enum RoleName|RoleName" -S .

echo
echo "== migration metadata around activity_log =="
fd '.*activity.*\.sql$|.*migration.*\.sql$' src/main/resources/db/migration 2>/dev/null | sort | xargs -r -n1 sh -c 'echo "--- $0 ---"; sed -n "1,160p" "$0"'

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ActivityLog entity =="
cat -n src/main/java/com/slatto/domain/notification/entity/ActivityLog.java

echo
echo "== ActivityLogType enum =="
cat -n src/main/java/com/slatto/domain/notification/enums/ActivityLogType.java

echo
echo "== RoleName enum =="
cat -n src/main/java/com/slatto/domain/user/enums/RoleName.java

echo
echo "== earlier migrations containing activity_log/type =="
rg -n "activity_log|CREATE TABLE|ALTER TABLE.*type|type" src/main/resources/db/migration -g '*.sql' --max-count 200

echo
echo "== ActivityLogRepository methods =="
cat -n src/main/java/com/slatto/domain/notification/repository/ActivityLogRepository.java

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 8861


레거시 activity_log.type 값을 ActivityLogType 값으로 변환하세요.

V009__activity_log_type.sql은 기존 컬럼 값을 VARCHAR(50) 문자열로 바꾸지만 내용 변환을 하지 않습니다. ActivityLog.type@Enumerated(EnumType.STRING) ActivityLogType으로 매핑되고, RoleName 값(DIRECTOR, PM, EDITOR 등)은 ActivityLogType에 없습니다. 이 컬럼 데이터를 읽을 때 해당 enum 값이 없어서 엔티티 로드/마이그레이션 실행 실패가 발생할 수 있습니다.

ALTER 전에 SELECT type, COUNT(*) FROM activity_log GROUP BY type로 기존 값을 확인한 뒤, ActivityLogType 값으로 매핑하거나 호환 불가 행을 별도 처리하는 마이그레이션 SQL을 추가하세요.

🤖 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/db/migration/V009__activity_log_type.sql` around lines 4 -
5, Update V009__activity_log_type.sql to migrate existing activity_log.type
values before altering the column: inspect distinct values, map legacy RoleName
entries such as DIRECTOR, PM, and EDITOR to valid ActivityLogType strings, and
explicitly handle any unmappable rows so every persisted value is compatible
with ActivityLog.type’s ActivityLogType enum before applying the VARCHAR(50) NOT
NULL change.

Comment on lines +2 to 5
-- 이전 확인 시각 방식이 일부 DB에 반영된 경우를 함께 정리한다.

ALTER TABLE project_member
DROP COLUMN IF EXISTS last_activity_read_at;

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 "== migration file =="
sed -n '1,120p' src/main/resources/db/migration/V010__project_activity_read.sql 2>/dev/null || true

echo
echo "== related project_member/last_activity_read_at/project_activity_read definitions =="
rg -n "last_activity_read_at|project_activity_read|activity_log|project_member" src/main/resources/db/migration . --glob '!**/.git/**' --glob '!**/build/**' --glob '!**/target/**' | head -200

echo
echo "== git diff stat/summary =="
git diff --stat || true
git diff --name-only || true

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 11362


기존 last_activity_read_at을 백필한 뒤 컬럼을 삭제하세요.

project_member.last_activity_read_at을 드롭 후 project_activity_read를 생성하는 이 마이그레이션은 기존 읽음 시각을 새 읽음 로그로 이전하지 않습니다. 해당 값이 있으면 읽음 상태가 영구히 사라지고, 기존 로그가 ‘읽지 않은’ 활동처럼 처리될 수 있습니다. 먼저 project_activity_read를 생성해 기존 시각 이하의 활동 로그를 멤버별로 백필한 후, 레거시 컬럼을 마지막에 삭제하세요.

🤖 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/db/migration/V010__project_activity_read.sql` around lines
2 - 5, Update migration V010 so project_activity_read is created before removing
project_member.last_activity_read_at, then backfill one read record per member
from the legacy timestamp for all project activity at or before that time.
Execute the backfill before the DROP COLUMN statement, preserving existing read
status, and retain the current cleanup of the legacy column as the final step.

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/test/resources/application.yml (1)

19-20: 🗄️ Data Integrity & Integration | 🔵 Trivial

MySQL Flyway 마이그레이션 검증 단계를 별도로 추가하세요.

Line 19의 spring.flyway.enabled: false와 H2 ddl-auto: create-drop 설정은 V001~V010을 실행하지 않습니다. 따라서 V008의 중복 데이터 처리, V009의 레거시 enum 값 처리, V010의 MySQL SQL과 데이터 이전 문제를 현재 테스트가 검증하지 못합니다.

H2 테스트는 유지하되, CI 또는 배포 단계에서 Testcontainers 같은 임시 MySQL 환경으로 Flyway 마이그레이션을 실행하세요.

🤖 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/resources/application.yml` around lines 19 - 20, 추가 테스트 구성을 마련해
MySQL Testcontainers 환경에서 Flyway 마이그레이션을 활성화한 채 V001~V010을 실행하고 검증하세요. 기존 H2
테스트와 spring.flyway.enabled: false 설정은 유지하되, CI 또는 배포 단계에서 V008 중복 데이터 처리, V009
레거시 enum 값 처리, V010 MySQL SQL 및 데이터 이전을 확인하도록 구성하세요.
src/main/resources/db/migration/V002__project_pin.sql (1)

12-13: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

복합 유니크 키의 선두 컬럼 인덱스를 중복 생성하지 마세요.

두 복합 유니크 키는 각각 첫 번째 컬럼의 단독 조건을 지원합니다. 전체 코드에서 단독 컬럼 조회가 없는지 확인한 뒤 별도 인덱스를 제거하세요.

  • src/main/resources/db/migration/V002__project_pin.sql#L12-L13: idx_project_pin_user_id를 제거하세요.
  • src/main/resources/db/migration/V003__project_notice_read.sql#L12-L14: idx_project_notice_read_notice_id를 제거하세요.

중복 인덱스는 쓰기 비용과 저장 공간을 증가시킵니다.

🤖 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/db/migration/V002__project_pin.sql` around lines 12 - 13,
Remove the redundant standalone indexes supporting the leading columns of the
composite unique keys after confirming no standalone-column queries require
them: remove idx_project_pin_user_id in
src/main/resources/db/migration/V002__project_pin.sql (lines 12-13) and
idx_project_notice_read_notice_id in
src/main/resources/db/migration/V003__project_notice_read.sql (lines 12-14).
🤖 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/main/java/com/slatto/domain/user/service/UserService.java`:
- Around line 47-53: 프로필 이미지의 multipart 허용 크기를 MAX_PROFILE_IMAGE_SIZE인 10 MiB와
일치시키십시오. spring.servlet.multipart의 max-file-size와 max-request-size를 10 MiB 기준으로
조정하거나, 해당 설정을 유지해야 한다면 업로드 요청에서 validateProfileImage 이전에 파일 크기를 직접 거절하도록 변경하십시오.
- Around line 222-223: Update the upload flow in UserService so
storageService.upload is wrapped in try/catch; if it throws after creating the
object, call deleteStorageObjectQuietly(storageKey, ...) before rethrowing the
original exception, while preserving
registerUploadedFileCleanupOnRollback(storageKey) for successful uploads.

In
`@src/main/resources/db/migration/V008__recruitment_application_active_unique.sql`:
- Around line 6-11: Before creating uq_recruitment_application_active in V008,
detect existing duplicate active applications using recruitment_id and user_id,
then resolve or backfill them according to the business rules so each active
pair is unique. Run the unique-key creation only after the data cleanup has
completed.

In `@src/main/resources/db/migration/V009__activity_log_type.sql`:
- Around line 4-5: Update V009__activity_log_type.sql to migrate existing
activity_log.type values before altering the column: inspect distinct values,
map legacy RoleName entries such as DIRECTOR, PM, and EDITOR to valid
ActivityLogType strings, and explicitly handle any unmappable rows so every
persisted value is compatible with ActivityLog.type’s ActivityLogType enum
before applying the VARCHAR(50) NOT NULL change.

In `@src/main/resources/db/migration/V010__project_activity_read.sql`:
- Around line 2-5: Update migration V010 so project_activity_read is created
before removing project_member.last_activity_read_at, then backfill one read
record per member from the legacy timestamp for all project activity at or
before that time. Execute the backfill before the DROP COLUMN statement,
preserving existing read status, and retain the current cleanup of the legacy
column as the final step.

---

Nitpick comments:
In `@src/main/resources/db/migration/V002__project_pin.sql`:
- Around line 12-13: Remove the redundant standalone indexes supporting the
leading columns of the composite unique keys after confirming no
standalone-column queries require them: remove idx_project_pin_user_id in
src/main/resources/db/migration/V002__project_pin.sql (lines 12-13) and
idx_project_notice_read_notice_id in
src/main/resources/db/migration/V003__project_notice_read.sql (lines 12-14).

In `@src/test/resources/application.yml`:
- Around line 19-20: 추가 테스트 구성을 마련해 MySQL Testcontainers 환경에서 Flyway 마이그레이션을
활성화한 채 V001~V010을 실행하고 검증하세요. 기존 H2 테스트와 spring.flyway.enabled: false 설정은 유지하되,
CI 또는 배포 단계에서 V008 중복 데이터 처리, V009 레거시 enum 값 처리, V010 MySQL SQL 및 데이터 이전을 확인하도록
구성하세요.
🪄 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: fc3b0c39-4fa3-4980-87d9-a8652de17a87

📥 Commits

Reviewing files that changed from the base of the PR and between c1e1a47 and 75cd92a.

📒 Files selected for processing (26)
  • .env.example
  • build.gradle
  • src/main/java/com/slatto/domain/notification/repository/ActivityLogRepository.java
  • src/main/java/com/slatto/domain/notification/repository/ProjectLatestActivityProjection.java
  • src/main/java/com/slatto/domain/project/service/ProjectService.java
  • src/main/java/com/slatto/domain/user/controller/UserController.java
  • src/main/java/com/slatto/domain/user/dto/UserProfileImageResponse.java
  • src/main/java/com/slatto/domain/user/entity/Users.java
  • src/main/java/com/slatto/domain/user/exception/UserErrorCode.java
  • src/main/java/com/slatto/domain/user/service/UserService.java
  • src/main/resources/application.yml
  • src/main/resources/db/migration/005-project-member-activity-read.sql
  • src/main/resources/db/migration/V001__auth_google_login.sql
  • src/main/resources/db/migration/V002__project_pin.sql
  • src/main/resources/db/migration/V003__project_notice_read.sql
  • src/main/resources/db/migration/V004__notification_group_count.sql
  • src/main/resources/db/migration/V005__recruitment_crud.sql
  • src/main/resources/db/migration/V006__recruitment_application_status.sql
  • src/main/resources/db/migration/V007__notification_project_nullable.sql
  • src/main/resources/db/migration/V008__recruitment_application_active_unique.sql
  • src/main/resources/db/migration/V009__activity_log_type.sql
  • src/main/resources/db/migration/V010__project_activity_read.sql
  • src/test/java/com/slatto/domain/notification/repository/ActivityLogRepositoryIntegrationTest.java
  • src/test/java/com/slatto/domain/user/service/PortfolioUpdatedAtTest.java
  • src/test/java/com/slatto/domain/user/service/UserProfileImageServiceTest.java
  • src/test/resources/application.yml
💤 Files with no reviewable changes (1)
  • src/main/resources/db/migration/005-project-member-activity-read.sql
🛑 Comments failed to post (1)
src/main/resources/db/migration/V008__recruitment_application_active_unique.sql (1)

6-11: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'src/main/resources/db/migration|V008|recruitment_application|recruitment_application_active_unique|recruitment.*migration|migration'

echo "== target file =="
if [ -f src/main/resources/db/migration/V008__recruitment_application_active_unique.sql ]; then
  cat -n src/main/resources/db/migration/V008__recruitment_application_active_unique.sql
fi

echo "== related migration files =="
fd -p 'src/main/resources/db/migration' -t f | sort | sed -n '1,120p'

echo "== search recruitment_application references =="
rg -n "recruitment_application|deleted_at|active_user_id|uq_recruitment_application_active|INSERT INTO.*recruitment_application|exists|SELECT .*recruitment_application" -S .

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 13033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("src/main/resources/db/migration/V008__recruitment_application_active_unique.sql")
if p.exists():
    text = p.read_text()
    print("contains generated virtual column:", "GENERATED ALWAYS AS" in text and "VIRTUAL" in text)
    print("contains unique key add:", "ADD UNIQUE KEY uq_recruitment_application_active" in text)
    print("contains pre-existing duplicate cleanup select/update/delete:", any(s in text for s in [
        "DELETE",
        "UPDATE",
        "SELECT recruitment_id, user_id, COUNT(*)",
        "active_count",
        "GROUP BY recruitment_id, user_id",
        "HAVING COUNT(*) > 1"
    ]))
PY

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 295


기존 활성 중복 데이터를 정리한 후 유니크 키를 생성하세요.

V008은 deleted_at IS NULL인 활성 지원의 recruitment_iduser_id로 유니크 키를 추가합니다. 기존 중복 활성 지원이 있으면 마이그레이션 실행이 실패할 수 있으므로 배포 전에 중복을 확인하고, 발견된 경우 비즈니스 규칙에 맞게 정리하거나 백필한 뒤 이 마이그레이션을 수행하세요.

SELECT recruitment_id, user_id, COUNT(*) AS active_count
FROM recruitment_application
WHERE deleted_at IS NULL
GROUP BY recruitment_id, user_id
HAVING COUNT(*) > 1;
🤖 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/db/migration/V008__recruitment_application_active_unique.sql`
around lines 6 - 11, Before creating uq_recruitment_application_active in V008,
detect existing duplicate active applications using recruitment_id and user_id,
then resolve or backfill them according to the business rules so each active
pair is unique. Run the unique-key creation only after the data cleanup has
completed.

chazy-d and others added 3 commits August 6, 2026 01:50
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@chazy-d
chazy-d merged commit 31b8cc9 into develop Aug 6, 2026
2 checks passed
sangwon02 added a commit that referenced this pull request Aug 6, 2026
UserService 충돌 해결. 양쪽이 getUserOrThrow 앞에 서로 다른 private 메서드를
추가해 발생한 위치 충돌이라 둘 다 유지했다.

- HEAD: getUserRegions (활동 지역 다중 조회)
- develop: 프로필 이미지 업로드 관련 헬퍼 (#125)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 추가 fix 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: 마이그레이션 버그 픽스 및 fe 피드백 반영

3 participants