Skip to content

Conversation

@yerinchun
Copy link
Contributor

@yerinchun yerinchun commented Dec 5, 2025

#️⃣ 연관된 이슈

관련된 이슈 번호를 적어주세요.
Close #87

✨ 작업 내용 (Summary)

이번 PR에서 작업한 내용을 간략히 설명해주세요. (이미지 첨부 가능)

비공개 계정을 위한 팔로우 요청/승인 시스템 구현

DB 스키마 변경

  • FollowStatus Enum 추가 (PENDING, APPROVED)
  • Follow 엔티티에 status 필드 추가
  • Flyway 마이그레이션 파일 작성 (V2.8__add_follow_status.sql)
  • 기존 팔로우 데이터 모두 APPROVED로 마이그레이션

Repository 수정

  • 팔로워/팔로잉 목록 조회 쿼리에 status='APPROVED' 필터 추가
  • PENDING 상태 팔로우 요청 조회 메서드 추가

✅ 변경 사항 체크리스트

다음 항목들을 확인하고 체크해주세요.

  • 코드에 영향이 있는 모든 부분에 대한 테스트를 작성하고 실행했나요?
  • 문서를 작성하거나 수정했나요? (필요한 경우)
  • 중요한 변경 사항이 팀에 공유되었나요?

🧪 테스트 결과

코드 변경에 대해 테스트를 수행한 결과를 요약해주세요.

  • 테스트 환경: 로컬
  • 테스트 방법: 스웨거
  • 결과 요약: 모든 테스트 통과

📸 스크린샷

관련된 스크린샷 또는 GIF가 있다면 여기에 첨부해주세요.

설명 사진
공개 계정 팔로우 image
비공개 계정 팔로우 image
받은 팔로우 목록 조회 image
팔로우 요청 승인 image
팔로우 요청 거절 image

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.


📎 참고 자료

관련 문서, 레퍼런스 링크 등이 있다면 여기에 첨부해주세요.

Summary by CodeRabbit

  • 새로운 기능
    • 받은 팔로우 요청을 한곳에서 페이지 단위로 조회할 수 있습니다.
    • 개별 요청을 승인하거나 거절할 수 있습니다.
    • 공개/비공개 대상에 따라 요청 상태(대기/승인)가 반영되어 표시됩니다.
    • 팔로우/언팔로우 응답에 현재 상태가 포함되어 실시간으로 확인 가능합니다.
  • 기타
    • 팔로우 상태 도입을 위한 데이터 마이그레이션이 적용되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@yerinchun yerinchun self-assigned this Dec 5, 2025
@yerinchun yerinchun added 🌟 feat 새로운 기능 개발 예린 labels Dec 5, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

📝 Walkthrough

Walkthrough

팔로우 요청/승인 기능을 도입합니다. Follow 엔티티에 FollowStatus(PENDING/APPROVED)을 추가하고, 비공개 계정은 요청을 PENDING으로 저장합니다. 요청 조회, 승인, 거절 API와 상태 기반 쿼리·DTO·마이그레이션이 추가되었습니다.

Changes

Cohort / File(s) Summary
엔티티 & DB 변경
src/main/java/com/hrr/backend/domain/follow/entity/Follow.java, src/main/java/com/hrr/backend/domain/follow/entity/enums/FollowStatus.java, src/main/resources/db/migration/V2.18__add_follow_status.sql
Follow.status: FollowStatus 필드 추가(기본 PENDING), approve() 메서드 추가, FollowStatus enum(PENDING, APPROVED) 추가, Flyway 마이그레이션으로 status 컬럼 조건부 추가
레포지토리 쿼리
src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java
상태 기반 조회/필터링 메서드 추가 및 기존 팔로워/팔로잉 조회 쿼리에 status = 'APPROVED' 조건 적용; pending 요청 페이징 조회 메서드 추가
서비스 로직
src/main/java/com/hrr/backend/domain/follow/service/FollowService.java, src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java
공개/비공개 분기(APPROVED/PENDING)로 follow 생성, 기존 존재 검사에 상태 고려, 승인/거절/대기열 조회 메서드 추가, isFollowing 계산 로직 개선
컨트롤러 & 엔드포인트
FollowController
src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java
대기 요청 조회(GET /me/requests), 승인(POST /requests/{requesterId}/approve), 거절(DELETE /requests/{requesterId}/reject) 엔드포인트 추가; 기존 엔드포인트는 CustomUserDetails로 currentUserId 추출
DTOs
src/main/java/com/hrr/backend/domain/follow/dto/FollowRequestDto.java, src/main/java/com/hrr/backend/domain/follow/dto/FollowResponseDto.java
FollowRequestDto 추가(요청 목록 표현), FollowResponseDtoidstatus 필드 추가·팩토리 시그니처 변경
에러 코드
src/main/java/com/hrr/backend/global/response/ErrorCode.java
FOLLOW_REQUEST_NOT_FOUND, UNAUTHORIZED_FOLLOW_ACTION, ALREADY_APPROVED_FOLLOW 오류 코드 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as FollowController
    participant Service as FollowService
    participant Repo as FollowRepository
    participant DB as Database

    rect rgb(245, 250, 255)
    Note over Client,Controller: 팔로우 요청 생성 (공개/비공개 분기)
    Client->>Controller: POST /api/.../follow/{followingId}
    Controller->>Service: followUser(currentUserId, followingId)
    Service->>Repo: findByFollowerIdAndFollowingId(...)
    Repo-->>Service: existing? (null or Follow)
    alt 신규
        Service->>Service: target.isPublic? → status = APPROVED : PENDING
        Service->>Repo: save(Follow(status))
        Repo->>DB: INSERT follow (status)
        DB-->>Repo: saved
        Repo-->>Service: Follow(entity)
    end
    Service-->>Controller: FollowResponseDto(message, id, status)
    Controller-->>Client: 200 OK
    end

    rect rgb(240, 255, 240)
    Note over Client,DB: 팔로우 요청 승인
    Client->>Controller: POST /me/requests/{requesterId}/approve
    Controller->>Service: approveFollowRequest(currentUserId, requesterId)
    Service->>Repo: findByFollowerIdAndFollowingIdAndStatus(requesterId, currentUserId, PENDING)
    Repo-->>Service: Follow(status=PENDING) / null
    alt found
        Service->>Service: follow.approve() (status -> APPROVED)
        Service->>Repo: save(Follow)
        Repo->>DB: UPDATE follow.status=APPROVED
        DB-->>Repo: updated
        Service-->>Controller: FollowResponseDto(..., APPROVED)
    else not found
        Service-->>Controller: throw FOLLOW_REQUEST_NOT_FOUND
    end
    Controller-->>Client: 200 OK / 404
    end

    rect rgb(255, 245, 250)
    Note over Client,DB: 팔로우 요청 거절
    Client->>Controller: DELETE /me/requests/{requesterId}/reject
    Controller->>Service: rejectFollowRequest(currentUserId, requesterId)
    Service->>Repo: findByFollowerIdAndFollowingIdAndStatus(requesterId, currentUserId, PENDING)
    Repo-->>Service: Follow(status=PENDING) / null
    alt found
        Service->>Repo: delete(Follow)
        Repo->>DB: DELETE from follow
        DB-->>Repo: deleted
        Service-->>Controller: FollowResponseDto(..., null)
    else not found
        Service-->>Controller: throw FOLLOW_REQUEST_NOT_FOUND
    end
    Controller-->>Client: 200 OK / 404
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • #84 — 동일한 Follow 도메인 파일들을 수정한 PR로, 상태 도입 전후의 연속성 및 충돌 가능성이 높습니다.
  • #86 — Follow 도메인(Controller/Service/Repository/DTO)을 확장해 승인 흐름을 추가한 PR로, 변경 범위가 유사합니다.
  • #61 — FollowRepository 관련 변경 이력이 있어 상태 기반 쿼리 추가와 직접적으로 연결될 가능성이 큽니다.

Poem

문 앞에 쌓인 PENDING 한 줄,
클릭 한 번에 APPROVED로 반짝,
거절은 조용히 기록을 지우고,
작은 enum이 큰 흐름을 바꾸네 ✨
축하해요 — 팔로우에 새 계절이 왔습니다!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 타이틀이 PR의 주요 변경사항을 명확하게 요약합니다. 팔로우 요청/승인 API 구현이 이 PR의 핵심이며, 제공된 모든 변경사항이 이를 지원합니다.
Linked Issues check ✅ Passed PR이 연관 이슈 #87의 모든 주요 코딩 요구사항을 충족합니다. FollowStatus enum, Follow 엔티티 status 필드, 마이그레이션 파일, 승인/거절 API, Repository 쿼리, DTO, 에러코드가 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 팔로우 요청/승인 시스템 구현의 범위 내에 있습니다. 비공개 계정 팔로우 요청 처리, status 필드 추가, 마이그레이션, API 엔드포인트, DTO, 에러처리 등이 이슈 #87과 직접 관련됩니다.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/87-follow-request-approval-api

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00064b5 and c4a0f70.

📒 Files selected for processing (1)
  • src/main/resources/db/migration/V2.18__add_follow_status.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/resources/db/migration/V2.18__add_follow_status.sql

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (1)

97-100: 동일한 빈 Slice 처리 버그

getFollowers() 메서드와 동일한 문제입니다. 빈 Slice를 null로 매핑하지 말고, 위의 수정 사항과 동일하게 처리해주세요.

 // 팔로잉이 없으면 빈 Slice 반환
 if (followingsSlice.isEmpty()) {
-    return new SliceResponseDto<>(followingsSlice.map(user -> null));
+    return new SliceResponseDto<>(followingsSlice.map(user -> FollowListResponseDto.of(user, false)));
 }

또는 early return을 제거하여 더 간결하게 처리할 수 있습니다.

🧹 Nitpick comments (8)
src/main/resources/db/migration/V2.8__add_follow_status.sql (1)

5-6: 선택적: 중복된 UPDATE 구문

Line 3에서 이미 DEFAULT 'APPROVED'를 설정했으므로, 기존 레코드는 자동으로 해당 값을 가지게 됩니다. Line 6의 UPDATE 구문은 기술적으로 불필요합니다.

다만, 명시적인 데이터 마이그레이션 의도를 드러내고 특정 DB 버전의 동작 차이를 방지하는 방어적 코드로 볼 수 있어 유지해도 무방합니다.

필요시 다음과 같이 간소화할 수 있습니다:

 -- Follow 테이블에 status 컬럼 추가
 ALTER TABLE follow
 ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'APPROVED' AFTER following_id;
-
--- 기존 데이터는 모두 APPROVED로 처리
-UPDATE follow SET status = 'APPROVED';
src/main/java/com/hrr/backend/domain/follow/entity/Follow.java (1)

40-43: 선택적: approve() 메서드에 방어 로직 추가

현재는 상태 전환을 무조건 수행하지만, 이미 APPROVED 상태인 경우를 체크하면 더 안전합니다. 다만 서비스 레이어에서 ALREADY_APPROVED_FOLLOW 에러코드로 검증하고 있다면 현재 구조도 충분합니다.

필요시 다음과 같이 방어 로직을 추가할 수 있습니다:

 public void approve() {
+    if (this.status == FollowStatus.APPROVED) {
+        throw new IllegalStateException("이미 승인된 팔로우입니다.");
+    }
     this.status = FollowStatus.APPROVED;
 }

단, 이는 도메인 로직 위치에 대한 선택의 문제로, 서비스 레이어에서 검증하는 현재 방식도 유효합니다.

src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java (2)

38-39: JPQL에서 enum 문자열 리터럴 사용 주의

'APPROVED' 문자열 리터럴을 직접 사용하는 것은 동작하지만, enum 값 변경 시 컴파일 타임에 오류를 감지할 수 없습니다. 파라미터화된 쿼리로 변경하면 타입 안전성을 확보할 수 있습니다.

-    @Query("SELECT f.follower FROM Follow f WHERE f.following.id = :userId AND f.status = 'APPROVED' ORDER BY f.createdAt DESC")
-    Slice<User> findFollowersByUserId(@Param("userId") Long userId, Pageable pageable);
+    @Query("SELECT f.follower FROM Follow f WHERE f.following.id = :userId AND f.status = :status ORDER BY f.createdAt DESC")
+    Slice<User> findFollowersByUserId(@Param("userId") Long userId, @Param("status") FollowStatus status, Pageable pageable);

단, 이 변경 시 호출부에서도 FollowStatus.APPROVED를 명시적으로 전달해야 합니다.


59-69: 메서드명과 파라미터 설계 불일치

findPendingFollowRequests라는 이름은 PENDING 상태만 조회함을 암시하지만, status 파라미터를 받아 범용적으로 사용 가능합니다. 일관성을 위해 둘 중 하나를 선택하세요:

  1. 메서드명 변경: findFollowRequestsByStatus
  2. 파라미터 제거 후 PENDING 하드코딩

현재 구현이 PENDING 전용이라면:

-    @Query("SELECT f FROM Follow f " +
-            "WHERE f.following.id = :userId " +
-            "AND f.status = :status " +
-            "ORDER BY f.createdAt DESC")
-    List<Follow> findPendingFollowRequests(@Param("userId") Long userId, @Param("status") FollowStatus status);
+    @Query("SELECT f FROM Follow f " +
+            "WHERE f.following.id = :userId " +
+            "AND f.status = 'PENDING' " +
+            "ORDER BY f.createdAt DESC")
+    List<Follow> findPendingFollowRequests(@Param("userId") Long userId);
src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (1)

189-197: 대용량 팔로우 요청 시 페이지네이션 부재

getPendingFollowRequestsList<Follow>를 반환하며 페이지네이션이 없습니다. 인기 계정의 경우 수천 개의 pending 요청이 있을 수 있어 메모리 및 응답 시간 문제가 발생할 수 있습니다.

페이지네이션 적용을 권장합니다:

-    public List<FollowRequestDto> getPendingFollowRequests(Long currentUserId) {
+    public SliceResponseDto<FollowRequestDto> getPendingFollowRequests(Long currentUserId, Pageable pageable) {
         log.info("받은 팔로우 요청 목록 조회 - userId: {}", currentUserId);
 
-        List<Follow> pendingFollows = followRepository.findPendingFollowRequests(currentUserId, FollowStatus.PENDING);
+        Slice<Follow> pendingFollows = followRepository.findPendingFollowRequests(currentUserId, FollowStatus.PENDING, pageable);
 
-        return pendingFollows.stream()
+        List<FollowRequestDto> content = pendingFollows.stream()
                 .map(FollowRequestDto::from)
                 .collect(Collectors.toList());
+        return SliceResponseDto.of(content, pendingFollows.hasNext());
     }

Repository 메서드도 Slice<Follow> 반환으로 변경이 필요합니다.

src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (3)

42-43: authentication.getName() 파싱 예외 처리 고려

Long.parseLong()NumberFormatException을 발생시킬 수 있습니다. 인증 설정이 올바르다면 문제없지만, 방어적 코딩 관점에서 공통 유틸리티 메서드로 추출하는 것을 권장합니다.

여러 컨트롤러에서 반복되는 패턴이므로, 공통 유틸리티 또는 @AuthenticationPrincipal 커스텀 resolver 활용을 고려하세요:

// 예: SecurityUtils.getCurrentUserId(Authentication auth)
private Long getCurrentUserId(Authentication authentication) {
    try {
        return Long.parseLong(authentication.getName());
    } catch (NumberFormatException e) {
        throw new GlobalException(ErrorCode.INVALID_TOKEN);
    }
}

69-71: 페이지 크기(size) 상한 제한 권장

size 파라미터에 상한이 없어 악의적인 요청(예: size=10000)으로 서버 부하를 유발할 수 있습니다.

최대 크기 제한 추가:

+    private static final int MAX_PAGE_SIZE = 100;
+
     @GetMapping("/me/followings")
     public ApiResponse<SliceResponseDto<FollowListResponseDto>> getMyFollowings(
             @RequestParam(defaultValue = "0") int page,
             @RequestParam(defaultValue = "20") int size,
             Authentication authentication
     ) {
         Long currentUserId = Long.parseLong(authentication.getName());
+        int validatedSize = Math.min(size, MAX_PAGE_SIZE);
-        Pageable pageable = PageRequest.of(page, size);
+        Pageable pageable = PageRequest.of(Math.max(0, page), validatedSize);

또는 @Max 어노테이션으로 Bean Validation을 활용할 수 있습니다.


33-61: 컨트롤러-서비스 간 중복 로깅

Controller와 Service 모두 동일한 정보를 로깅하고 있습니다. 한 레이어에서만 로깅하면 로그 가독성이 향상됩니다. 일반적으로 서비스 레이어에서 비즈니스 로직 로깅을, 컨트롤러에서는 요청/응답 로깅만 수행합니다.

Controller의 상세 로깅을 간소화하거나, AOP 기반 로깅으로 통합하는 것을 고려하세요.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between feecb86 and bf24c21.

📒 Files selected for processing (12)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/dto/FollowListResponseDto.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/dto/FollowRequestDto.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/dto/FollowResponseDto.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/entity/Follow.java (2 hunks)
  • src/main/java/com/hrr/backend/domain/follow/entity/enums/FollowStatus.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (1 hunks)
  • src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (1 hunks)
  • src/main/java/com/hrr/backend/global/response/ErrorCode.java (1 hunks)
  • src/main/java/com/hrr/backend/global/response/SuccessCode.java (1 hunks)
  • src/main/resources/db/migration/V2.8__add_follow_status.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1)
src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (1)
  • Slf4j (20-198)
🔇 Additional comments (10)
src/main/resources/db/migration/V2.8__add_follow_status.sql (1)

2-3: 컬럼 추가 전략이 안전합니다!

기본값을 'APPROVED'로 설정하여 기존 데이터 마이그레이션을 안전하게 처리했습니다. 비공개 계정이 도입되기 전의 팔로우 관계를 모두 승인된 상태로 보는 것이 논리적으로 합당합니다.

src/main/java/com/hrr/backend/domain/follow/entity/enums/FollowStatus.java (1)

8-13: 깔끔한 enum 설계입니다!

PENDING/APPROVED 두 가지 상태로 팔로우 요청 플로우를 명확하게 표현했습니다. description 필드는 향후 UI 노출이나 로깅에 유용하게 활용될 수 있습니다.

src/main/java/com/hrr/backend/domain/follow/entity/Follow.java (1)

34-38: EnumType.STRING 사용이 올바릅니다!

EnumType.STRING을 사용하여 DB에서 가독성을 높이고, enum 순서 변경에 안전하게 대응했습니다. 기본값 APPROVED도 마이그레이션 전략과 일치합니다.

src/main/java/com/hrr/backend/global/response/ErrorCode.java (1)

72-79: 팔로우 기능의 에러 케이스를 잘 정의했습니다!

자기 자신 팔로우 방지, 중복 팔로우, 권한 검증 등 주요 엣지 케이스를 모두 커버했습니다. HTTP 상태 코드도 적절하게 선택되었습니다:

  • 400 (BAD_REQUEST): 잘못된 요청
  • 403 (FORBIDDEN): 권한 없음
  • 404 (NOT_FOUND): 리소스 없음
  • 409 (CONFLICT): 중복 상태
src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (1)

38-73: N+1 쿼리 방지 전략이 훌륭합니다!

followerIds를 미리 수집한 후 한 번의 쿼리로 팔로우 관계를 조회하는 방식으로 N+1 문제를 효과적으로 해결했습니다. 이는 성능 최적화의 모범 사례입니다.

src/main/java/com/hrr/backend/domain/follow/dto/FollowListResponseDto.java (1)

28-36: 깔끔한 팩토리 메서드 설계입니다!

User 엔티티를 DTO로 변환하는 로직이 명확하고, getUserLevel().name().toLowerCase()로 enum을 소문자 문자열로 변환하는 것도 API 응답 일관성을 위해 좋은 선택입니다. isFollowing 플래그는 서비스에서 계산되어 주입되므로 관심사 분리도 잘 되어 있습니다.

src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (2)

29-85: 깔끔한 팔로우 로직 구현! 👍

공개/비공개 계정에 따른 상태 분기, 중복 체크, 자기 자신 팔로우 방지 등 요구사항이 잘 반영되어 있습니다.


160-182: 거절 시 APPROVED 상태 팔로우도 삭제 가능 - 의도된 동작인지 확인 필요

rejectFollowRequest는 PENDING 상태 체크 없이 삭제를 수행합니다. 이는 "팔로워 삭제" 기능으로도 사용될 수 있는데, API 이름이 "reject"이므로 PENDING 상태만 처리하는 것이 명확할 수 있습니다.

의도된 동작이라면 무시해도 되지만, PENDING 전용이라면:

+        // PENDING 상태 확인
+        if (follow.getStatus() != FollowStatus.PENDING) {
+            log.warn("PENDING 상태가 아닌 팔로우 요청입니다 - followId: {}, status: {}", followId, follow.getStatus());
+            throw new GlobalException(ErrorCode.FOLLOW_REQUEST_NOT_FOUND);
+        }
+
         // 요청 삭제
         followRepository.delete(follow);

참고: 팔로워 삭제 기능이 별도로 필요하다면 removeFollower API를 분리하는 것을 권장합니다.

src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1)

155-181: 팔로우 요청 승인/거절 API 구조 적절! 👌

RESTful 설계 원칙에 따라 approve는 POST, reject는 DELETE로 구현되어 있고, Swagger 문서화도 잘 되어 있습니다.

src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java (1)

56-57: Empty list handling is already in place

The calling methods in FollowListService already include defensive checks (isEmpty() at lines 54 and 98) that return early before calling findFollowingIdsByFollowerIdAndFollowingIds(). This prevents empty lists from being passed to the method, so the IN clause concern is already addressed.

Comment on lines +34 to +42
public static FollowRequestDto from(Follow follow) {
return FollowRequestDto.builder()
.followId(follow.getId())
.requesterId(follow.getFollower().getId())
.requesterNickname(follow.getFollower().getNickname())
.requesterProfileImage(follow.getFollower().getProfileImage())
.requestedAt(follow.getCreatedAt())
.build();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for FollowRepository file first
fd -t f "FollowRepository" src/main/java --type java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 249


🏁 Script executed:

#!/bin/bash
# Find FollowRepository file
find src -name "*FollowRepository*" -type f

# Search for pending/status queries in follow-related repository files
rg -A 10 "findBy.*[Pp]ending|findBy.*[Ss]tatus|@Query" src/main/java/com/hrr/backend/domain/follow --type java

# Search for JOIN FETCH patterns in follow repositories
rg "JOIN FETCH" src/main/java/com/hrr/backend/domain/follow --type java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 5087


🏁 Script executed:

cat -n src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 3332


findPendingFollowRequests() 쿼리에 JOIN FETCH f.follower 추가 필요

findPendingFollowRequests() 메서드의 쿼리(lines 65-69)에서 follower를 명시적으로 로드하지 않으므로, FollowRequestDto.from()이 follow.getFollower()의 여러 필드에 접근할 때 N+1 쿼리가 발생합니다. 쿼리를 다음과 같이 수정하세요:

@Query("SELECT f FROM Follow f " +
        "JOIN FETCH f.follower " +
        "WHERE f.following.id = :userId " +
        "AND f.status = :status " +
        "ORDER BY f.createdAt DESC")
🤖 Prompt for AI Agents
In src/main/java/com/hrr/backend/domain/follow/dto/FollowRequestDto.java lines
34 to 42, FollowRequestDto.from() accesses multiple follower fields causing N+1
queries because the repository's findPendingFollowRequests() query does not
eagerly load follower; update the repository query (around lines 65–69) to
include "JOIN FETCH f.follower" so the follower is fetched in the same query
(keep the WHERE f.following.id = :userId AND f.status = :status and the ORDER BY
f.createdAt DESC clauses unchanged).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (1)

52-78: 팔로워 목록 조회 로직은 OK, empty Slice 분기는 더 단순화할 수 있습니다

팔로워 조회 흐름(존재 여부 체크 → Slice 조회 → ID 배치 조회 → DTO 매핑)과 N+1 방지 패턴 자체는 잘 잡혀 있습니다. 도메인 서비스에서 ID를 한 번에 모아서 followRepository.findFollowingIdsByFollowerIdAndFollowingIds로 조회하는 구조가 깔끔합니다.

다만 Line 56–61의 followersSlice.isEmpty() 분기는 논리상 필수는 아닙니다. 아래 ID 수집 + map 로직은 빈 Slice도 안전하게 처리하므로, 빈 Slice일 때도 같은 경로를 타게 하면 중복을 줄이고 읽기가 더 쉬워집니다. 쿼리 최적화를 유지하면서도 early-return을 없애려면, ID 리스트 기준으로 한 번 더 분기하는 방식이 유용합니다. 예시는 다음과 같습니다:

-        // 팔로워가 없으면 빈 Slice 반환
-        if (followersSlice.isEmpty()) {
-            return new SliceResponseDto<>(followersSlice.map(user ->
-                    FollowListResponseDto.of(user, false)
-            ));
-        }
-
-        // N+1 방지: 한 번의 쿼리로 현재 사용자가 팔로우 중인 ID 목록 조회
-        List<Long> followerIds = followersSlice.getContent().stream()
-                .map(User::getId)
-                .toList();
-
-        Set<Long> followingIds = new HashSet<>(
-                followRepository.findFollowingIdsByFollowerIdAndFollowingIds(currentUserId, followerIds)
-        );
+        // N+1 방지: 한 번의 쿼리로 현재 사용자가 팔로우 중인 ID 목록 조회
+        List<Long> followerIds = followersSlice.getContent().stream()
+                .map(User::getId)
+                .toList();
+
+        Set<Long> followingIds = followerIds.isEmpty()
+                ? Set.of()
+                : new HashSet<>(followRepository.findFollowingIdsByFollowerIdAndFollowingIds(currentUserId, followerIds));

이렇게 하면 빈 Slice에서도 추가 쿼리 없이 동일한 결과(빈 DTO Slice)를 만들 수 있고, 전체 흐름도 “Slice → IDs → Set → map”으로 일관되게 보입니다. Spring Data JPA의 Slice.map(...)가 빈 Slice에 대해서도 안전하게 동작하는 건 공식 레퍼런스(Slice/Page 섹션)에서도 보장하니, 해당 패턴을 적극 활용하셔도 좋습니다.

🧹 Nitpick comments (1)
src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (1)

102-128: 팔로잉 목록 조회도 패턴 일관성이 좋아서 유지보수에 유리합니다 (공통화 여지)

팔로워 조회와 거의 대칭 구조로 팔로잉 목록도 Slice 기반 페이징 + ID 배치 조회 + FollowListResponseDto.of 매핑까지 일관되게 구현된 점이 좋습니다. 호출자가 두 메서드를 읽을 때 흐름이 동일해서, 이후 도메인 정책이 바뀌어도 추론하기 쉽겠습니다.

다만 Line 102–127의 로직은 getFollowers와 매우 유사하므로,
Slice<User>를 받아 현재 사용자의 isFollowing 플래그를 계산해 SliceResponseDto<FollowListResponseDto>로 바꿔주는” private 헬퍼 메서드로 공통화하는 것도 고려해볼 만합니다. 예를 들어:

  • extractTargetIds(Slice<User> users)
  • resolveFollowingIds(Long currentUserId, List<Long> targetIds)
  • mapToFollowListDtoSlice(Slice<User> users, Set<Long> followingIds)

와 같이 역할을 나누거나, 하나의 convertToSliceResponse(Slice<User> slice, Long currentUserId, Function<User, Long> idExtractor) 형태로 추상화하면, 추후 FollowStatus 조건이 바뀌거나 FollowListResponseDto 필드가 늘어났을 때 수정 포인트를 줄일 수 있습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf24c21 and c5b8435.

📒 Files selected for processing (1)
  • src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (1 hunks)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1)

114-152: 비공개 계정 접근 제어 - 별도 이슈로 처리 예정 확인

이전 리뷰에서 언급된 비공개 계정(isPublic=false)의 팔로우 목록 접근 제어가 아직 미구현 상태입니다. 작성자께서 별도 이슈로 구현 예정이라고 답변하셨으므로, 해당 이슈가 생성되었는지 확인 부탁드립니다.

🧹 Nitpick comments (2)
src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (2)

68-72: 페이지네이션 파라미터에 상한 검증 추가 고려

size 파라미터에 상한 제한이 없어, 매우 큰 값이 전달될 경우 메모리 이슈가 발생할 수 있습니다. @Max 어노테이션을 활용한 검증을 권장드립니다.

+ import jakarta.validation.constraints.Max;
+ import jakarta.validation.constraints.Min;
 public ApiResponse<SliceResponseDto<FollowListResponseDto>> getMyFollowings(
         @Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
-        @RequestParam(defaultValue = "0") int page,
+        @RequestParam(defaultValue = "0") @Min(0) int page,
         @Parameter(description = "페이지 크기", example = "20")
-        @RequestParam(defaultValue = "20") int size,
+        @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,

동일한 패턴을 다른 페이지네이션 엔드포인트에도 적용하시면 됩니다.


100-110: 대기 중인 팔로우 요청 목록에 페이지네이션 추가 권장

현재 getPendingFollowRequestsList를 반환하여 모든 대기 요청을 한 번에 조회합니다. 대기 요청이 많아질 경우 성능 저하가 발생할 수 있으므로, 다른 목록 조회 API와의 일관성 및 확장성을 위해 페이지네이션 적용을 권장드립니다.

-    @GetMapping("/me/requests")
-    public ApiResponse<List<FollowRequestDto>> getPendingFollowRequests(
-            @AuthenticationPrincipal CustomUserDetails customUserDetails
+    @GetMapping("/me/requests")
+    public ApiResponse<SliceResponseDto<FollowRequestDto>> getPendingFollowRequests(
+            @Parameter(description = "페이지 번호 (0부터 시작)", example = "0")
+            @RequestParam(defaultValue = "0") int page,
+            @Parameter(description = "페이지 크기", example = "20")
+            @RequestParam(defaultValue = "20") int size,
+            @AuthenticationPrincipal CustomUserDetails customUserDetails
     ) {
         Long currentUserId = customUserDetails.getUser().getId();
-        log.info("팔로우 요청 목록 조회 - currentUserId: {}", currentUserId);
+        log.info("팔로우 요청 목록 조회 - currentUserId: {}, page: {}, size: {}", currentUserId, page, size);
 
-        List<FollowRequestDto> response = followService.getPendingFollowRequests(currentUserId);
+        Pageable pageable = PageRequest.of(page, size);
+        SliceResponseDto<FollowRequestDto> response = followService.getPendingFollowRequests(currentUserId, pageable);
         return ApiResponse.onSuccess(SuccessCode.OK, response);
     }

FollowService.getPendingFollowRequests 메서드도 함께 수정이 필요합니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5b8435 and bc5f70d.

📒 Files selected for processing (1)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1 hunks)
🔇 Additional comments (3)
src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (3)

24-32: 깔끔한 컨트롤러 설정! 👍

Lombok과 OpenAPI 어노테이션 사용이 적절하며, 생성자 주입 방식으로 의존성을 관리하고 있습니다.


36-62: RESTful 설계 원칙 준수! 👌

팔로우(POST)/언팔로우(DELETE) 엔드포인트가 적절한 HTTP 메서드를 사용하고 있으며, 서비스 레이어에 비즈니스 로직(자기 자신 팔로우 방지, 중복 체크 등)을 위임하고 있습니다.


156-182: 승인/거절 엔드포인트 구현 적절! ✨

  • POST /approve: 상태 변경에 적합한 메서드
  • DELETE /reject: 요청 삭제(거절)에 적합한 메서드
  • followId를 사용하여 직접 엔티티를 참조하는 방식이 효율적
  • 권한 검증(요청 수신자 확인)은 서비스 레이어에서 처리

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/main/java/com/hrr/backend/global/response/ErrorCode.java (1)

117-120: 팔로우 요청용 ErrorCode 추가 방향은 적절합니다

  • FOLLOW_REQUEST_NOT_FOUND / UNAUTHORIZED_FOLLOW_ACTION 둘 다 HTTP status, 코드 prefix, 한글 메시지까지 기존 FOLLOW_* 패턴과 잘 맞습니다. 🙆‍♂️
  • ALREADY_APPROVED_FOLLOW 추가도 서비스 레벨에서 상태 기반 검증을 명확하게 표현해 주는 좋은 선택입니다.

다만, HTTP status 선택에 대해 한 가지 개선 포인트를 제안드립니다.

  1. ALREADY_APPROVED_FOLLOW 의 HTTP status 재검토 제안

    현재는:

    ALREADY_APPROVED_FOLLOW(HttpStatus.BAD_REQUEST, "FOLLOW4006", "이미 승인된 팔로우입니다."),

    으로 되어 있는데, 클라이언트 입장에서 보면 “요청 형식이 잘못됐다(BAD_REQUEST)”라기보다는 리소스의 현재 상태와 충돌(CONFLICT, 409) 에 더 가깝습니다.
    이미 승인된 팔로우에 대해 다시 승인 요청을 보내는 상황은 다음 성격을 가집니다.

    • 요청 자체는 형식적으로 유효함 (followId 등 정상)
    • 하지만 서버에 저장된 리소스의 상태(이미 APPROVED)와 비즈니스 규칙 상 충돌 발생

    그래서 다음과 같이 409로 맞추는 것도 고려해볼 수 있습니다:

    -ALREADY_APPROVED_FOLLOW(HttpStatus.BAD_REQUEST, "FOLLOW4006", "이미 승인된 팔로우입니다."),
    +ALREADY_APPROVED_FOLLOW(HttpStatus.CONFLICT, "FOLLOW4092", "이미 승인된 팔로우입니다."),

    이렇게 하면:

    • ALREADY_FOLLOWING(이미 팔로우 관계 존재)와 마찬가지로 상태 충돌은 409 계열로 일관되게 가져갈 수 있고
    • 후속으로 클라이언트에서 멱등 처리/리트라이 로직을 짤 때도 의미 해석이 더 직관적입니다.

    물론, 팀 내에서 4xx 세부 코드를 어떻게 쓰기로 했는지 컨벤션이 이미 있다면 그 컨벤션을 우선하면 됩니다. 다만 지금 시점에 follow 쪽 에러 코드가 새로 늘어나는 상황이어서, HTTP status 및 코드 체계를 한 번 정리해 두면 이후 기능 확장 시에도 도움이 될 것 같습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc5f70d and 7bf5a59.

📒 Files selected for processing (3)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (3 hunks)
  • src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java (2 hunks)
  • src/main/java/com/hrr/backend/global/response/ErrorCode.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java
  • src/main/java/com/hrr/backend/domain/follow/service/FollowListService.java

Copy link
Contributor

@yc3697 yc3697 left a comment

Choose a reason for hiding this comment

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

리뷰 내용들 수정해주세요~

log.info("사용자 팔로우 완료 - followerId: {}, followingId: {}, status: {}",
currentUserId, followedUserId, status);

return FollowResponseDto.of(message, followedUserId);
Copy link
Contributor

Choose a reason for hiding this comment

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

팔로우 완료/팔로우 요청 이 두 가지 결과에 대해 별도의 메시지로 반환을 하고 있는데 이 응답으로 프론트가 분기 처리를 해야하기 때문에 일관되게 유지할 수 있도록 status를 담아 반환하는 게 좋을 것 같아요!

Comment on lines 121 to 133
* @param followId 팔로우 ID
* @return FollowResponseDto
*/
@Transactional
public FollowResponseDto approveFollowRequest(Long currentUserId, Long followId) {
log.info("팔로우 요청 승인 - userId: {}, followId: {}", currentUserId, followId);

// 팔로우 요청 조회
Follow follow = followRepository.findById(followId)
.orElseThrow(() -> {
log.warn("팔로우 요청을 찾을 수 없습니다 - followId: {}", followId);
return new GlobalException(ErrorCode.FOLLOW_REQUEST_NOT_FOUND);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

여기서 followId에 대해 정리가 필요할 것 같아요!

지금은 인자로 넘겨받는 걸로 봐서 팔로우되는 사람의 userId인 것 같은데, 그러면 followRepository의 findById는 follow 엔티티의 id를 조회하기 때문에 의도와 다르게 실행될 가능성이 커보입니다.

Comment on lines 157 to 165
* @param followId 팔로우 ID
* @return FollowResponseDto
*/
@Transactional
public FollowResponseDto rejectFollowRequest(Long currentUserId, Long followId) {
log.info("팔로우 요청 거절 - userId: {}, followId: {}", currentUserId, followId);

// 팔로우 요청 조회
Follow follow = followRepository.findById(followId)
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 마찬가지입니다!

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
@Builder.Default
private FollowStatus status = FollowStatus.APPROVED;
Copy link
Contributor

Choose a reason for hiding this comment

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

기본값은 PENDING으로 해야할 것 같아요

Copy link
Contributor

Choose a reason for hiding this comment

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

스크립트 파일의 최신 버전이 2.12였나? 그래서 2.13 등으로 변경 부탁드립니다

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/com/hrr/backend/domain/follow/dto/FollowResponseDto.java (1)

20-21: id 필드 의미가 여전히 모호합니다.

이전 리뷰에서 지적된 대로 "관련 ID (userId 또는 followId)" 설명이 API 소비자에게 혼란을 줄 수 있습니다. 각 엔드포인트별로 반환하는 ID가 무엇인지 명확히 문서화하거나, 용도별로 DTO를 분리하는 것을 고려해주세요.

🧹 Nitpick comments (4)
src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java (2)

30-58: verifiedAt의 시간대 표현(LocalDateTime + 'Z' 포맷) 일관성 한번 점검해 주세요

지금은 LocalDateTime@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")를 사용하고 있어서, 실제로는 시간대 정보가 없는 로컬 시간인데 응답 문자열은 ...Z(보통 UTC를 의미)로 내려가게 됩니다.

  • 실제로 서버/DB에 UTC로 저장하고 있다면, OffsetDateTime 또는 ZonedDateTime으로 타입을 올리고 패턴을 ...XXX 형태로 주는 것도 고려할 수 있습니다.
  • 반대로 KST 기준 로컬 시간을 표현하려는 의도라면, 'Z'를 제거하거나 timezone 속성을 명시해 주는 편이 API 소비자 입장에서 덜 헷갈립니다.
  • 팀에서 정한 “시간/타임존 표준”이 있다면, 이 필드가 그 규칙을 따르고 있는지 한 번만 맞춰봐 주세요.

Jackson @JsonFormat 관련 공식 문서(예: @JsonFormat + LocalDateTime/OffsetDateTime 사용법)를 참고하면 팀 컨벤션 정리에도 도움이 됩니다.


60-87: 페이지네이션 DTO를 공용 타입으로 추출하는 것 고려

PaginationDto 구조(totalCount, currentPage, totalPages, pageSize)가 매우 범용적인 형태라, 팔로우 목록·챌린지 목록 등 다른 API에서도 동일/유사한 DTO가 생길 가능성이 큽니다.

  • 현재는 UserVerificationResponseDto 안에 중첩 클래스로 정의되어 있어 이 도메인에만 한정되어 재사용하기가 어렵습니다.
  • 향후 다른 응답에서도 동일한 형태의 페이지네이션을 쓴다면, com.hrr.backend.global.dto.PaginationResponseDto 같은 공용 DTO로 추출해 두면 중복 정의와 Swagger 스키마 난립을 줄일 수 있습니다.

지금 당장 필수는 아니지만, 리스트 API가 늘어날수록 이 refactor의 효과가 커질 것이므로 중·장기적으로 한 번 검토해 보시면 좋겠습니다.

src/main/resources/db/migration/V2.13__add_follow_status.sql (1)

1-2: status 컬럼에 인덱스 추가를 권장합니다.

FollowRepository.findPendingFollowRequests에서 status 컬럼으로 필터링하는 쿼리가 사용됩니다. 팔로우 데이터가 증가하면 인덱스 없이 성능 저하가 발생할 수 있습니다.

 -- Follow 테이블에 status 컬럼 추가
 ALTER TABLE follow ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'APPROVED';
+
+-- status 컬럼 인덱스 추가 (팔로우 요청 조회 성능 최적화)
+CREATE INDEX idx_follow_status ON follow(status);
src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1)

99-109: 페이지네이션 적용을 권장합니다.

다른 목록 조회 API들(/me/followings, /me/followers)은 SliceResponseDto로 페이지네이션을 지원하지만, 이 엔드포인트는 List로 전체를 반환합니다. 팔로우 요청이 많아지면 응답 크기와 성능에 문제가 될 수 있습니다.

일관성과 확장성을 위해 페이지네이션 적용을 고려해주세요. 참고: Spring Data Slice 문서

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bf5a59 and 1d20d7e.

📒 Files selected for processing (6)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (3 hunks)
  • src/main/java/com/hrr/backend/domain/follow/dto/FollowResponseDto.java (2 hunks)
  • src/main/java/com/hrr/backend/domain/follow/entity/Follow.java (2 hunks)
  • src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (6 hunks)
  • src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java (1 hunks)
  • src/main/resources/db/migration/V2.13__add_follow_status.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java (1)
src/main/java/com/hrr/backend/domain/follow/dto/FollowResponseDto.java (1)
  • Getter (10-33)
🔇 Additional comments (5)
src/main/java/com/hrr/backend/domain/user/dto/UserVerificationResponseDto.java (1)

14-28: 컨테이너형 DTO 설계 깔끔합니다 👍

VerificationHistoryDtoverificationList + pagination으로 잘 묶여 있어서, 응답 스키마를 한 곳에서 관리하기 좋습니다. 이런 패턴은 이후 다른 도메인(팔로우 목록, 챌린지 목록 등)에도 재사용하기 좋아 유지보수성이 높습니다.

src/main/java/com/hrr/backend/domain/follow/entity/Follow.java (1)

33-43: LGTM! 깔끔한 구현입니다 👍

@Builder.Default로 새 팔로우의 기본값을 PENDING으로 설정하고, approve() 메서드로 상태 변경을 캡슐화한 점이 좋습니다. DB 마이그레이션에서 기존 레코드는 APPROVED로, 엔티티에서 신규 레코드는 PENDING으로 설정하는 분리된 설계가 적절합니다.

src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (1)

155-181: 승인/거절 API 구조가 잘 설계되었습니다 ✨

requesterId를 path variable로 받아 현재 사용자와의 관계를 명확히 하고, @AuthenticationPrincipal로 현재 사용자를 안전하게 추출하는 패턴이 적절합니다. 로깅도 충실하게 포함되어 있어 디버깅에 유용할 것입니다.

src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (2)

65-84: 공개/비공개 계정 분기 로직이 명확합니다 👏

followedUser.getIsPublic()으로 상태를 결정하고, 응답 메시지와 status를 함께 반환하여 프론트엔드 분기 처리가 용이하도록 구현되었습니다. 이전 리뷰 피드백이 잘 반영되었네요!


124-148: 승인 로직이 정확합니다.

PENDING 상태 확인 후 follow.approve() 호출로 상태를 변경하는 흐름이 명확합니다. @Transactional이 적용되어 있어 dirty checking으로 자동 저장됩니다.

Comment on lines 187 to 195
public List<FollowRequestDto> getPendingFollowRequests(Long currentUserId) {
log.info("받은 팔로우 요청 목록 조회 - userId: {}", currentUserId);

List<Follow> pendingFollows = followRepository.findPendingFollowRequests(currentUserId, FollowStatus.PENDING);

return pendingFollows.stream()
.map(FollowRequestDto::from)
.collect(Collectors.toList());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find FollowRepository and check the findPendingFollowRequests implementation
find . -type f -name "FollowRepository.java" | head -5

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 156


🏁 Script executed:

# Find FollowService to understand the full context
find . -type f -name "FollowService.java" | head -5

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 150


🏁 Script executed:

# Check for project configuration files
find . -type f \( -name "pom.xml" -o -name "build.gradle" \) | head -10

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 92


🏁 Script executed:

cat -n ./src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 3332


🏁 Script executed:

cat -n ./build.gradle

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 3057


🏁 Script executed:

# Find FollowRequestDto
find . -type f -name "FollowRequestDto.java" | head -5

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 149


🏁 Script executed:

# Find Follow entity
find . -type f -name "Follow.java" | grep -E "domain|entity" | head -5

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 142


🏁 Script executed:

cat -n ./src/main/java/com/hrr/backend/domain/follow/dto/FollowRequestDto.java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 1734


🏁 Script executed:

cat -n ./src/main/java/com/hrr/backend/domain/follow/entity/Follow.java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 1541


🏁 Script executed:

# Verify if there are other usages of findPendingFollowRequests in the codebase
rg -n "findPendingFollowRequests" --type java

Repository: Hrr-HabbitRoutineResult/hrr-server

Length of output: 463


Fix N+1 query issue and modernize stream collection for Java 17.

The findPendingFollowRequests query does not eagerly load the follower relationship. Since Follow.follower uses FetchType.LAZY and FollowRequestDto::from() accesses follow.getFollower().getId(), getNickname(), and getProfileImage(), this triggers an N+1 query problem. Add JOIN FETCH f.follower to the repository query to load the follower in a single query.

Additionally, with Java 17, replace Collectors.toList() with .toList():

 return pendingFollows.stream()
         .map(FollowRequestDto::from)
-        .collect(Collectors.toList());
+        .toList();

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/hrr/backend/domain/follow/service/FollowService.java around
lines 187 to 195, the service maps pending Follow entities to DTOs while the
repository query does not eagerly load the follower, causing an N+1 when
FollowRequestDto::from accesses follower fields; update the repository query
used by findPendingFollowRequests to include JOIN FETCH f.follower so the
follower is loaded in the same query, and in this service method modernize the
stream terminal operation by replacing .collect(Collectors.toList()) with the
Java 17 .toList() call.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java (1)

59-69: 받은 요청 조회 쿼리에 JOIN FETCH f.follower를 추가해 N+1을 방지하는 것이 좋습니다.

  • 현재 쿼리:
@Query("SELECT f FROM Follow f " +
       "WHERE f.following.id = :userId " +
       "AND f.status = :status " +
       "ORDER BY f.createdAt DESC")
Slice<Follow> findPendingFollowRequests(...);
  • FollowService.getPendingFollowRequests에서 이 결과를 받아 Slice.map(FollowRequestDto::from)으로 매핑하는데,
    FollowRequestDto.from()follow.getFollower().getId()/getNickname()/getProfileImage() 등을 접근하고,
    Follow.followerLAZY라면 팔로우 요청마다 추가 조회가 발생하는 전형적인 N+1 패턴입니다.
    (이 이슈는 이전 리뷰 코멘트에서도 이미 지적된 바 있습니다.)

개선 예시:

@Query("""
       SELECT f FROM Follow f
       JOIN FETCH f.follower
       WHERE f.following.id = :userId
       AND f.status = :status
       ORDER BY f.createdAt DESC
       """)
Slice<Follow> findPendingFollowRequests(@Param("userId") Long userId,
                                        @Param("status") FollowStatus status,
                                        Pageable pageable);
  • 이렇게 하면 한 번의 쿼리로 Follow + follower를 모두 가져와, 서비스 레이어에서 DTO로 변환할 때 추가 쿼리 없이 처리할 수 있습니다.
  • 페이지네이션은 Slice 기반이라 count가 필요 없고, JOIN FETCH도 큰 문제 없이 동작하는 패턴입니다. 다만 데이터량이 매우 커지는 경우에는 인덱스(following_id, status, created_at)도 함께 고려해 보시면 좋겠습니다.
src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (1)

185-203: 받은 요청 목록 Javadoc 및 N+1 쿼리 가능성 정리 제안 (이전 리뷰와 동일 이슈).

  1. Javadoc 반환 타입 불일치

    • 주석에는 @return List<FollowRequestDto>라고 되어 있지만 실제 시그니처는 SliceResponseDto<FollowRequestDto>입니다.
    • IDE/Javadoc만 보고 사용할 때 혼동을 줄이기 위해, 주석을 실제 반환 타입과 맞춰 수정하는 것이 좋습니다.

    예시:

    /**
     * 받은 팔로우 요청 목록 조회
     * @param currentUserId 현재 로그인한 사용자 ID
     * @param pageable 페이징 정보
     * @return 받은 팔로우 요청 Slice 응답 DTO
     */
  2. FollowRequestDto.from 매핑 시 N+1 쿼리 가능성 (이전 코멘트와 동일 이슈입니다.)

    • Slice<Follow> pendingFollowsSlice = followRepository.findPendingFollowRequests(...) 후,
      pendingFollowsSlice.map(FollowRequestDto::from)으로 DTO를 만들고 있습니다.
    • Follow 엔티티의 followerLAZY이고, FollowRequestDto.from()에서 follow.getFollower().getId()/getNickname()/getProfileImage() 등을 접근한다면,
      각 팔로우 요청마다 follower를 개별 조회하는 N+1 쿼리가 발생할 수 있습니다.

    개선 방향:

    • FollowRepository.findPendingFollowRequests 쿼리 쪽에서 JOIN FETCH f.follower를 추가해 한 번에 로딩하는 것을 추천드립니다. (아래 리포지토리 코멘트에 예시 포함)
    • 서비스단 로직은 현재 Slice.map(...) 패턴이 깔끔하므로 그대로 두고, 페치 전략만 리포지토리에서 조정하는 편이 좋습니다.
🧹 Nitpick comments (3)
src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (2)

62-88: 공개/비공개 분기 & 응답 구조는 요구사항과 잘 맞습니다.

  • 공개 계정 → APPROVED, 비공개 계정 → PENDING 분기가 명확하고, FollowResponseDto.of(message, followedUserId, status)status까지 내려주는 구조라 프론트 분기에도 적합해 보입니다.
  • 다만 현재는 영어 메시지 문자열에 강하게 의존하고 있어 i18n/문구 변경 시 영향 범위가 커질 수 있습니다. 장기적으로는 메시지는 프론트/문구 리소스로 위임하고, 백엔드는 status + 코드(enum 등) 위주로 내려주는 쪽을 고려해보셔도 좋겠습니다.

96-119: 언팔로우 시 status = null 계약을 명시적으로 문서화하는 것을 추천합니다.

  • 언팔로우 시 상태에 상관없이(PENDING/APPROVED) 관계를 삭제하고, FollowResponseDto.of("User unfollowed successfully", unfollowedUserId, null)statusnull로 내려주고 있습니다.
  • 이는 “현재 팔로우 관계가 없음”을 표현하는 일관된 신호로 유용하지만, 다른 곳에서 status를 enum으로만 가정하면 NPE/분기 누락 위험이 있습니다.

제안:

  • FollowResponseDto의 Javadoc 혹은 클래스-level 주석에 “언팔로우/거절 시 status는 null”이라는 계약을 명시해 두면, 이후 사용하는 쪽에서 명시적으로 null 처리를 하게 되어 안전성이 올라갑니다.
src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java (1)

32-57: APPROVED 문자열 리터럴 대신 enum 기반 필터로 통일하는 것을 추천합니다.

  • findFollowersByUserId, findFollowingsByUserId, findFollowingIdsByFollowerIdAndFollowingIds에서 모두
    f.status = 'APPROVED' 문자열을 직접 사용하고 있습니다.
  • 이제 FollowStatus enum이 도입되어 있고, 이 파일에서도 이미 임포트하고 있으므로 문자열 리터럴보다는 enum을 사용하는 편이 타입 안정성과 유지보수성 측면에서 더 좋습니다.
    • 예를 들어 enum 이름이 리팩터링되거나 매핑 전략이 바뀌면, 문자열 기반 JPQL은 컴파일 타임에 깨지지 않고 런타임에만 터질 수 있습니다.

개선 예시 (둘 중 택1):

  1. 파라미터 방식
@Query("SELECT f.follower FROM Follow f " +
       "WHERE f.following.id = :userId " +
       "AND f.status = :status " +
       "ORDER BY f.createdAt DESC")
Slice<User> findFollowersByUserId(@Param("userId") Long userId,
                                  @Param("status") FollowStatus status,
                                  Pageable pageable);
  1. enum 상수 직접 사용
@Query("SELECT f.follower FROM Follow f " +
       "WHERE f.following.id = :userId " +
       "AND f.status = com.hrr.backend.domain.follow.entity.enums.FollowStatus.APPROVED " +
       "ORDER BY f.createdAt DESC")

현재 PENDING 쿼리(findPendingFollowRequests)는 이미 :status 파라미터를 사용하고 있으니, 같은 패턴으로 통일하면 읽기도 더 쉬울 것 같습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d18032 and a405f16.

📒 Files selected for processing (3)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java (3 hunks)
  • src/main/java/com/hrr/backend/domain/follow/repository/FollowRepository.java (2 hunks)
  • src/main/java/com/hrr/backend/domain/follow/service/FollowService.java (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/hrr/backend/domain/follow/controller/FollowController.java

Comment on lines +121 to +183
/**
* 팔로우 요청 승인
* @param currentUserId 현재 로그인한 사용자 ID (요청 받은 사람)
* @param requesterId 팔로우 요청한 사용자 ID
* @return FollowResponseDto
*/
@Transactional
public FollowResponseDto approveFollowRequest(Long currentUserId, Long requesterId) {
log.info("팔로우 요청 승인 - currentUserId: {}, requesterId: {}", currentUserId, requesterId);

// 요청한 사용자 존재 여부 확인
userRepository.findById(requesterId)
.orElseThrow(() -> {
log.warn("요청한 사용자를 찾을 수 없습니다 - userId: {}", requesterId);
return new GlobalException(ErrorCode.USER_NOT_FOUND);
});

// PENDING 상태의 팔로우 요청 조회
Follow follow = followRepository
.findByFollowerIdAndFollowingIdAndStatus(requesterId, currentUserId, FollowStatus.PENDING)
.orElseThrow(() -> {
log.warn("팔로우 요청을 찾을 수 없습니다 - requesterId: {}, currentUserId: {}", requesterId, currentUserId);
return new GlobalException(ErrorCode.FOLLOW_REQUEST_NOT_FOUND);
});

// 승인 처리
follow.approve();
log.info("팔로우 요청 승인 완료 - requesterId: {}, currentUserId: {}", requesterId, currentUserId);

return FollowResponseDto.of("Follow request approved successfully", requesterId, FollowStatus.APPROVED);
}

/**
* 팔로우 요청 거절/삭제
* @param currentUserId 현재 로그인한 사용자 ID (요청 받은 사람)
* @param requesterId 팔로우 요청한 사용자 ID
* @return FollowResponseDto
*/
@Transactional
public FollowResponseDto rejectFollowRequest(Long currentUserId, Long requesterId) {
log.info("팔로우 요청 거절 - currentUserId: {}, requesterId: {}", currentUserId, requesterId);

// 요청한 사용자 존재 여부 확인
userRepository.findById(requesterId)
.orElseThrow(() -> {
log.warn("요청한 사용자를 찾을 수 없습니다 - userId: {}", requesterId);
return new GlobalException(ErrorCode.USER_NOT_FOUND);
});

// PENDING 상태의 팔로우 요청 조회
Follow follow = followRepository
.findByFollowerIdAndFollowingIdAndStatus(requesterId, currentUserId, FollowStatus.PENDING)
.orElseThrow(() -> {
log.warn("팔로우 요청을 찾을 수 없습니다 - requesterId: {}, currentUserId: {}", requesterId, currentUserId);
return new GlobalException(ErrorCode.FOLLOW_REQUEST_NOT_FOUND);
});

// 요청 삭제
followRepository.delete(follow);
log.info("팔로우 요청 거절 완료 - requesterId: {}, currentUserId: {}", requesterId, currentUserId);

return FollowResponseDto.of("Follow request rejected successfully", requesterId, null);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify authorization handling and validation consistency in follow approval/rejection

The code does show an inconsistency worth addressing:

  1. Authorization error code usage: The current implementation uses only FOLLOW_REQUEST_NOT_FOUND when a follow request cannot be found or the user lacks authorization. If the requirement specifies that UNAUTHORIZED_FOLLOW_ACTION should distinguish cases where the request exists but the user lacks permission to approve/reject it, this pattern should be implemented explicitly rather than hidden behind a 404 response. This affects API contract clarity and client-side error handling. Consider clarifying with your team whether:

    • Unauthorized access should return 403 with UNAUTHORIZED_FOLLOW_ACTION, or
    • Security-through-obscurity (404 for both missing and unauthorized) is the intended pattern, in which case document this design choice.
  2. Inconsistent user validation: Both approveFollowRequest and rejectFollowRequest validate that requesterId exists but do not validate currentUserId. If other methods like followUser and unfollowUser validate both parties' existence, consider adding the same check for consistency:

    userRepository.findById(currentUserId)
            .orElseThrow(() -> {
                log.warn("사용자를 찾을 수 없습니다 - userId: {}", currentUserId);
                return new GlobalException(ErrorCode.USER_NOT_FOUND);
            });

    This ensures uniform validation across the service.

@yerinchun yerinchun requested a review from yc3697 December 7, 2025 15:56
import java.time.LocalDateTime;
import java.util.List;

public class UserVerificationResponseDto {
Copy link
Contributor

Choose a reason for hiding this comment

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

혹시 이건 왜 들어갔을까요..?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/main/resources/db/migration/V2.18__add_follow_status.sql (1)

1-2: 마이그레이션 로직은 정확합니다! DEFAULT 'APPROVED' 설정으로 기존 팔로우 데이터가 자동 마이그레이션되는 부분이 잘 처리되었네요.

다만 데이터 무결성과 성능을 위해 다음 개선을 고려해주세요:

1. DB 레벨 상태값 검증 (CHECK 제약조건)

현재 상태값 검증이 애플리케이션 레이어에만 의존하고 있습니다. 데이터베이스 차원의 제약을 추가하면 직접 SQL 실행 시에도 잘못된 상태값이 삽입되는 것을 방지할 수 있습니다.

ALTER TABLE follow ADD CONSTRAINT follow_status_check CHECK (status IN ('PENDING', 'APPROVED'));

관련 문서: MySQL CHECK Constraint

2. 쿼리 성능 최적화를 위한 인덱싱

status 컬럼은 팔로워/팔로잉 목록 조회(APPROVED), 팔로우 요청 조회(PENDING) 등에서 자주 필터링됩니다. 대규모 데이터셋에서는 다음 인덱스 추가를 권장합니다:

-- 기본 인덱스
CREATE INDEX idx_follow_status ON follow(status);

-- 또는 더 최적화된 복합 인덱스 (자주 함께 조회되는 컬럼 포함)
CREATE INDEX idx_follow_status_followee ON follow(followee_id, status);
CREATE INDEX idx_follow_status_follower ON follow(follower_id, status);

관련 문서: MySQL Index Design

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a405f16 and 57f9bbd.

📒 Files selected for processing (1)
  • src/main/resources/db/migration/V2.18__add_follow_status.sql

@yerinchun yerinchun requested a review from yc3697 December 27, 2025 06:51
Copy link
Contributor

Choose a reason for hiding this comment

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

이미 해당 칼럼이 추가되었는데 이 스크립트가 다시 실행될 경우 충돌이 일어날 수 있기 때문에 멱등성을 만족할 수 있도록 변경이 필요합니다

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57f9bbd and 00064b5.

📒 Files selected for processing (1)
  • src/main/resources/db/migration/V2.18__add_follow_status.sql

Comment on lines 1 to 3
-- Follow 테이블에 status 컬럼 추가 (없을 경우에만)
ALTER TABLE follow
ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PENDING'; No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

기존 팔로우 레코드가 PENDING으로 설정되는 치명적 문제

멱등성 수정(IF NOT EXISTS)은 잘 적용되었습니다! 👍 하지만 두 가지 중대한 문제가 있습니다:

  1. DEFAULT 값 문제: 현재 DEFAULT 'PENDING'으로 설정되어 있어, 마이그레이션 실행 시 기존의 모든 팔로우 관계가 'PENDING' 상태로 설정됩니다. PR 목표에 따르면 기존 레코드는 'APPROVED'여야 합니다.

  2. UPDATE 구문 누락: 기존 데이터를 'APPROVED'로 마이그레이션하는 구문이 없습니다.

이 상태로 배포하면 기존 사용자들의 팔로우 관계가 모두 대기 상태가 되어 데이터 무결성이 깨집니다.

🔎 제안하는 수정안
 -- Follow 테이블에 status 컬럼 추가 (없을 경우에만)
 ALTER TABLE follow
-ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PENDING';
+ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'APPROVED';
+
+-- 기존 팔로우 레코드를 APPROVED 상태로 마이그레이션
+UPDATE follow
+SET status = 'APPROVED'
+WHERE status = 'APPROVED';

참고:

  • 새로운 팔로우 요청의 초기 상태는 애플리케이션 레이어(FollowService)에서 계정 공개 여부에 따라 결정되므로, DEFAULT 값은 기존 데이터 보호를 위해 'APPROVED'가 적절합니다.
  • UPDATE 구문은 멱등성을 위해 WHERE 조건을 포함했습니다.
💡 선택사항: 타입 안정성 개선

더 나은 타입 안정성을 위해 데이터베이스의 ENUM 타입 사용을 고려해보세요:

-- MySQL의 경우
ALTER TABLE follow
ADD COLUMN IF NOT EXISTS status ENUM('PENDING', 'APPROVED') NOT NULL DEFAULT 'APPROVED';

-- PostgreSQL의 경우 (먼저 타입 생성 필요)
DO $$ 
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'follow_status') THEN
        CREATE TYPE follow_status AS ENUM ('PENDING', 'APPROVED');
    END IF;
END $$;

ALTER TABLE follow
ADD COLUMN IF NOT EXISTS status follow_status NOT NULL DEFAULT 'APPROVED';

장점: 잘못된 값 입력 방지, 성능 향상, 명시적 타입 선언
참고: Flyway Best Practices

🤖 Prompt for AI Agents
In src/main/resources/db/migration/V2.18__add_follow_status.sql lines 1-3,
change the migration so existing follow rows are not set to PENDING: set the
column DEFAULT to 'APPROVED' instead of 'PENDING' and add an idempotent UPDATE
statement after adding the column to explicitly set any preexisting rows to
'APPROVED' (use a WHERE that only affects non-APPROVED or NULL statuses so the
migration remains safe to re-run). Optionally consider using a DB enum type for
the status column for stronger typing as suggested.

Copy link
Contributor

Choose a reason for hiding this comment

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

아마 IF NOT EXISTS가 안 될 거라 프로시저 기반으로 해주셔야 할 것 같아요. 그리고 기본값 변경 부탁드립니다

Copy link
Contributor

@yc3697 yc3697 left a comment

Choose a reason for hiding this comment

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

확인했습니다 수고하셨어요~

@yerinchun yerinchun merged commit f4b434a into develop Dec 30, 2025
2 checks passed
@yerinchun yerinchun deleted the feat/87-follow-request-approval-api branch December 30, 2025 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

예린 🌟 feat 새로운 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 팔로우 요청 및 수락 API 구현

3 participants