-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] User 정책 전면 리팩토링 + 스케쥴러 설정 #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
[CHORE] 카카오 탈퇴 로그 추가
Merge branch 'develop' into 'main'
Merge branch develop into main
…d 로직 반영
- BlockRepository에 만료 블록 조회(findExpiredBlocks), purge 로그 기록(markFeedPurgedAt) 추가
- FeedRepository에 단방향/양방향 삭제 메서드 추가
- BlockPersistenceAdapter/FeedPersistenceAdapter 확장, Port와 연결
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough유저 관계/차단 상태 관리를 Block 도메인 중심으로 재설계하고, Follow/Unfollow/Block/Unblock/Report 이벤트 시 Feed/Follow/NewFollow 간 정합성 처리와 재팔로우 백필 로직을 추가했다. 만료/정리 스케줄러와 페이징 처리, 만료 기준/퍼지 시각 컬럼을 도입하고, 관련 포트/어댑터/리포지토리를 확장·정비했다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant C as UserController
participant S as BlockService
participant BP as BlockPort
participant UP as UserPort
participant FP as FeedPort
participant PP as PostPort
U->>C: POST /users/follow
C->>S: follow(userId, targetUserId)
S->>BP: findByBlockerAndBlocked
alt no existing
S->>BP: saveBlock(FOLLOW)
S->>UP: saveFollow / saveNewFollow
else existing == UNFOLLOWED
S->>BP: saveBlock(FOLLOW with updated statusChangedAt)
alt feedPurgedAt != null
S->>UP: findNewFollowingIds / etc.
S->>FP: addFeedsIfNotExists (full backfill via NewFollow)
else statusChangedAt != null
S->>PP: findByAuthorIdAndCreatedAtAfter
S->>FP: backfillIncremental
end
S->>UP: saveFollow / deleteNewFollow
else existing in (BLOCKED, REPORT)
S-->>C: throw USER_NOT_FOUND
else existing == FOLLOW
S-->>C: throw ALREADY_FOLLOW
end
C-->>U: 200 OK
sequenceDiagram
autonumber
participant SCH as FeedCleanupScheduler
participant BP as BlockPort
participant FP as FeedPort
SCH->>SCH: cron 0 0 3 * * *
loop pages until empty
SCH->>BP: findExpiredBlocks([UNFOLLOWED,BLOCKED], now, pageable)
alt UNFOLLOWED
SCH->>FP: deleteOneWay(blocker, blocked)
else BLOCKED
SCH->>FP: deleteBidirectional(blocker, blocked)
else other
SCH-->>SCH: IllegalStateException
end
SCH->>BP: markFeedPurgedAt(blocker, blocked, now)
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
✨ Finishing touches🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 28
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/main/java/com/spoony/spoony_server/application/auth/service/KakaoService.java (1)
29-36: 다음 두 가지 수정이 필요합니다:
unlink메소드에서platformId를Long.parseLong으로 한 번만 파싱하고,NumberFormatException을 적절히 처리하세요.System.out.println으로 민감 정보(adminKey·토큰 등)를 노출하지 말고 SLF4J Logger로 대체하세요.src/main/java/com/spoony/spoony_server/application/service/report/ReportService.java (1)
68-73: getReportDetail() null 시 NPE 위험trim() 호출 전에 null 가능성 처리 필요합니다. createUserReport와 createReport 모두 동일 리스크가 있습니다.
- if (command.getReportDetail().trim().isEmpty()){ + String detail = Optional.ofNullable(command.getReportDetail()) + .map(String::trim) + .orElse(""); + if (detail.isEmpty()){ throw new BusinessException(ReportErrorMessage.BAD_REQUEST_CONTENT_MISSING); } - if (command.getReportDetail().length()>300){ + if (detail.length() > 300){ throw new BusinessException(ReportErrorMessage.BAD_REQUEST_CONTENT_TOO_LONG); } @@ - UserReport userReport = new UserReport(userReportType, command.getReportDetail(),user,targetUser); + UserReport userReport = new UserReport(userReportType, detail, user, targetUser);createReport(…)에도 동일 적용 권장:
- if (command.getReportDetail().trim().isEmpty()) { + String detail = Optional.ofNullable(command.getReportDetail()).map(String::trim).orElse(""); + if (detail.isEmpty()) { throw new BusinessException(ReportErrorMessage.BAD_REQUEST_CONTENT_MISSING); } - if (command.getReportDetail().length() > 300) { + if (detail.length() > 300) { throw new BusinessException(ReportErrorMessage.BAD_REQUEST_CONTENT_TOO_LONG); } @@ - Report report = new Report(reportType,command.getReportDetail(),post,user); + Report report = new Report(reportType, detail, post, user);src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/event/PostCreatedEventListener.java (1)
44-56: findAllById 사용해 N+1 제거 및 중복 처리 추가
- followerIds 배치를 한 번의 IN 쿼리(userRepository.findAllById)로 조회해 Map으로 캐시 → DB 부하·RTT 감소
- (user_id, post_id) 유니크 제약으로 saveAll 중복 예외 발생 가능 → 배치별 try-catch로 예외 처리 후 중복 스킵/로깅 고려
src/main/java/com/spoony/spoony_server/adapter/in/web/user/UserController.java (1)
207-219: 차단도 BlockUseCase로 일원화(컨트롤러 역할 단순화)PR 목적에 맞춰 차단 역시 BlockUseCase로 위임해 일관성을 높이는 것이 좋습니다.
- BlockUserCommand command = new BlockUserCommand( - userId, - requestDTO.targetUserId() - ); - blockUserCreateUseCase.createUserBlock(command); + blockUseCase.block(userId, requestDTO.targetUserId());
🧹 Nitpick comments (56)
src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java (4)
63-77: null Place를 조용히 스킵하기보다 관찰 가능성을 확보하세요place == null을 단순 continue하면 데이터 정합성 문제를 놓칠 수 있습니다. 최소 WARN 로그 및 카운터(메트릭) 기록을 권장합니다.
134-140: 차단/신고 제외 집합 생성 시 3회 조회 → 단일 쿼리로 축약 고려getBlockedUserIds, getBlockerUserIds, getRelatedUserIdsByReportStatus를 한 번에 반환하는 Port(예: getExcludedUserIdsForUser)를 추가하면 DB 왕복/병합 비용을 줄일 수 있습니다. 캐시(요청 스코프)도 고려하세요.
231-263: Focus DTO 매핑도 카테고리 null 계약 점검 및 대응postCategory 또는 category가 null일 수 있는지 계약을 확인하고, null 가능 시 로그+스킵/기본값/예외 등 정책을 명시하세요. 사진 리스트가 비어도 안전하지만 카테고리 체인은 NPE 위험이 있습니다.
265-275: 지구 반지름 타입 변경(미세 개선) 및 상수화정밀도 관점에서 double 사용이 낫습니다. 필요 시 클래스 상단에 상수로 승격하세요.
- final int EARTH_RADIUS = 6371; // km + final double EARTH_RADIUS = 6371.0; // km또는
private static final double EARTH_RADIUS_KM = 6371.0;src/main/java/com/spoony/spoony_server/application/auth/service/AuthService.java (2)
52-55: System.out.println 대신 로거 사용 권장표준 출력은 운영환경 로그 수집/레벨 제어가 어렵습니다. 경고 레벨 로깅으로 교체하세요.
@@ - if (user == null) { - System.out.println("User is null"); - return LoginResponseDTO.of(false, null, null); - } + if (user == null) { + log.warn("Login failed: user not found. platform={}", platform); + return LoginResponseDTO.of(false, null, null); + }추가 필요:
@@ -import lombok.RequiredArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @@ -@Service +@Service +@Slf4j @RequiredArgsConstructor @Transactional public class AuthService implements
100-120: Enum 비교 시 문자열 비교 지양하고 직접 비교 사용toString().equals("KAKAO") 패턴은 오타/리팩토링에 취약합니다. Enum 직접 비교로 단순화하세요.
- if (userSignupDto.platform().toString().equals("KAKAO")){ + if (userSignupDto.platform() == Platform.KAKAO){ return kakaoService.getPlatformUserInfo(platformToken); - } else if (userSignupDto.platform().toString().equals("APPLE")){ + } else if (userSignupDto.platform() == Platform.APPLE){ return appleService.getPlatformUserInfo(platformToken); } else { throw new AuthException(AuthErrorMessage.PLATFORM_NOT_FOUND); } @@ - if (platform.toString().equals("KAKAO")){ + if (platform == Platform.KAKAO){ return kakaoService.getPlatformUserInfo(platformToken); - } else if (platform.toString().equals("APPLE")){ + } else if (platform == Platform.APPLE){ return appleService.getPlatformUserInfo(platformToken); } else { throw new AuthException(AuthErrorMessage.PLATFORM_NOT_FOUND); }src/main/java/com/spoony/spoony_server/application/port/out/user/UserPort.java (1)
32-33: 신규 메서드 시그니처 적절 — 배치 삭제 API도 고려해주세요단건 삭제(deleteNewFollowRelation) 반복 호출은 채팅(왕복) 비용이 큽니다. 동일 사용자에 대한 다건 삭제 배치 API 추가를 제안합니다.
제안:
+ void deleteNewFollowRelations(Long userId, List<Long> targetUserIds);원하시면 Adapter/Repository까지 포함한 배치 구현안도 드리겠습니다.
src/main/java/com/spoony/spoony_server/application/service/report/ReportService.java (1)
1-8: 불필요한 import 정리BlockStatus가 이 클래스에서 사용되지 않습니다. import 제거해 주세요.
-import com.spoony.spoony_server.adapter.out.persistence.block.db.BlockStatus;src/main/java/com/spoony/spoony_server/application/port/in/user/UserGetUseCase.java (1)
13-17: 조회 API 확장 적절 — 페이징/차단 필터 명세 확인 필요팔로워/팔로잉/차단 목록은 대량 데이터가 될 수 있습니다. DTO가 페이지네이션/커서 정보를 포함하는지, 그리고 차단/신고 사용자가 결과에서 일관되게 제외되는지 확인 부탁드립니다.
필요 시:
- FollowListResponseDTO/BlockListResponseDTO에 cursor/hasNext 등 추가
- 정렬 기준(createdAt/statusChangedAt 등) 명시
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/event/PostCreatedEventListener.java (3)
40-43: 매직 넘버(10_000) 외부화배치 크기 10,000은 운영 환경/DB 커넥션 상황에 따라 조정 필요합니다. 설정값(@value, application.yml)으로 외부화해 동적으로 튜닝 가능하게 해주세요.
30-31: System.out 대신 로거 사용운영 로그는 Logger(예: lombok @slf4j 또는 LoggerFactory)를 사용해 주세요. 비동기 이벤트 리스너에서는 특히 중요합니다.
1-1: 코딩 컨벤션 일회성 안내현재 들여쓰기에 스페이스가 사용됩니다. 사내 규칙(탭 들여쓰기, indent_size=4, 최대 120자, K&R 스타일)에 맞는지 한 번 점검 부탁드립니다.
src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java (1)
49-50: 메서드 명명 일관성(Author/User/Posts 접두사 정렬)기존 포트는
findPostsByUserId처럼Posts를 명시합니다. 새 메서드도 가독성/일관성을 위해 다음과 같이 변경을 제안합니다.- List<Post> findByAuthorIdAndCreatedAtAfter(Long authorId, LocalDateTime since); + List<Post> findPostsByAuthorIdAndCreatedAtAfter(Long authorId, LocalDateTime since);또한 레포지토리는
UserId용어를 쓰므로, 포트/어댑터/레포지토리 전반의 용어(Author vs User) 통일도 검토해 주세요.src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.java (1)
10-14: 보조 인덱스 제안: (user_id, created_at, post_id)위 쿼리를 고빈도로 사용할 예정이라면 다음 복합 인덱스를 추가해 Range Scan 성능을 확보해 주세요.
예시 DDL
CREATE INDEX idx_post_user_created_at_post_id ON post(user_id, created_at, post_id);src/main/java/com/spoony/spoony_server/application/port/out/feed/FeedPort.java (1)
15-19: 중복/혼동 가능 API 정리(Deprecate + 시그니처 정돈)
deleteByUserIdAndAuthorId와deleteOneWay/ deleteBidirectional의 역할이 겹칩니다. 혼선을 줄이려면 deprecated 처리 또는 주석으로 명확히 분리해 주세요. 또한targetUserId/authorId용어 통일을 권장합니다.- void deleteByUserIdAndAuthorId(Long userId, Long authorId); + @Deprecated // 사용처는 deleteOneWay(언팔) 또는 deleteBidirectional(차단/신고)로 전환 권장 + void deleteByUserIdAndAuthorId(Long userId, Long authorId); - void backfillIncremental(Long userId, Long targetUserId, List<Post> newPosts); + void backfillIncremental(Long userId, Long authorId, List<Post> newPosts);src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/PostSpecification.java (6)
97-103: 불필요한 리스트 초기화 및 조기 반환으로 단순화 가능
ageGroups가 null/empty면 바로cb.conjunction()반환하면 리스트 생성 불필요합니다. 성능 영향은 경미하나 가독성 향상됩니다.- return (root, query, cb) -> { - List<Predicate> predicates = new ArrayList<>(); - logger.debug("연령대 필터링 시작: ageGroups = {}", ageGroups); - - if (ageGroups != null && !ageGroups.isEmpty()) { + return (root, query, cb) -> { + logger.debug("연령대 필터링 시작: ageGroups = {}", ageGroups); + if (ageGroups == null || ageGroups.isEmpty()) { + return cb.conjunction(); + } + List<Predicate> predicates = new ArrayList<>(); // UserEntity의 ageGroup과 비교하여 필터링 Join<PostEntity, UserEntity> userJoin = root.join("user"); predicates.add(userJoin.get("ageGroup").in(ageGroups)); logger.debug("ageGroup in {} 필터링 추가", ageGroups); - - } logger.debug("연령대 필터링 최종 Predicate: {}", predicates); return cb.and(predicates.toArray(new Predicate[0])); };
115-117: 파라미터 의미를 주석으로 더 명확히 하거나 DTO로 묶기
blockedUserIds(내가 차단/신고한),blockerUserIds(나를 차단/신고한) 혼동 여지가 있습니다. 명확한 명명 또는 DTO로 그룹화하면 오용 위험이 줄어듭니다.
153-155: 커서 조건 중복 정의zzimCount/createdAt 커서 조건이 withCursor와 buildFilterSpec 하단에 중복 구현되어 있습니다. 한쪽으로 일원화하면 유지보수성 향상됩니다.
- return (root, query, cb) -> { - Predicate zzimLess = cb.lessThan(root.get("zzimCount"), cursor.zzimCount()); - Predicate zzimEqualCreatedAtLess = cb.and( - cb.equal(root.get("zzimCount"), cursor.zzimCount()), - cb.lessThan(root.get("createdAt"), cursor.createdAt()) - ); - return cb.or(zzimLess, zzimEqualCreatedAtLess); - }; + return (root, query, cb) -> PostSpecification.buildZzimCursorPredicate(root, cb, cursor); ... - return (root, query, cb) -> - cb.lessThan(root.get("createdAt"), cursor.createdAt()); + return (root, query, cb) -> cb.lessThan(root.get("createdAt"), cursor.createdAt());(별도 정적 헬퍼
buildZzimCursorPredicate추가 제안)Also applies to: 162-162
166-175: 빌더 메서드 시그니처가 비대함필터 파라미터가 많아 가독성이 떨어집니다. 요청 DTO(예: PostFilterCriteria)를 도입하면 확장성과 테스트 용이성이 좋아집니다.
181-198: Specification 결합 시 null 처리 패턴 단순화 가능
(spec != null ? spec : Specification.where(null))반복 대신 유틸 메서드로 흡수하면 깔끔합니다.- Specification<PostEntity> baseSpec = Specification.where(localReviewSpec) - .and(regionSpec) - .and(ageGroupSpec != null ? ageGroupSpec : Specification.where(null)) - .and(categorySpec != null ? categorySpec : Specification.where(null)) - .and(exclusionSpec != null ? exclusionSpec : Specification.where(null)) - .and(cursorSpec != null ? cursorSpec : Specification.where(null)); + Specification<PostEntity> baseSpec = Specification.where(localReviewSpec) + .and(regionSpec) + .and(opt(ageGroupSpec)) + .and(opt(categorySpec)) + .and(opt(exclusionSpec)) + .and(opt(cursorSpec));(정적 메서드
opt(Specification<T> s)가 null일 때Specification.where(null)반환)
229-229: 코드 컨벤션(탭 인덴트) 경고Java 파일은 hackday-conventions-java 기준 탭 인덴트, K&R 스타일 권장입니다. 현재 스페이스 인덴트가 혼재합니다. 일회성 체크만 남깁니다.
src/main/java/com/spoony/spoony_server/adapter/out/persistence/user/UserPersistenceAdapter.java (2)
261-263: 성능/인덱스 고려: findFollowedUserIdsByUserId대량 유저에서 빈번히 호출되면 인덱스(
(new_follower_id)또는(user_id, following_id))가 필요합니다. 페이지네이션이 없다면 호출부에서 배치 처리도 고려하세요.
255-258: deleteNewFollowRelation 호출 대상인 deleteFollowRelation이 NewFollowRepository에 @Modifying 및 @query로 올바르게 정의되어 있음. 영향 행 수 확인·로깅이 필요할 경우 반환 타입을 int로 변경하고 로직을 추가하세요.src/main/java/com/spoony/spoony_server/adapter/out/persistence/user/mapper/BlockMapper.java (1)
24-35: toEntity에서 User 매핑 전략 최적화 제안
UserMapper.toEntity()가 전체 사용자 엔티티를 구성/로드한다면 불필요한 로딩/병합이 발생할 수 있습니다. 식별자 참조만 설정하는 경량 매퍼를 권장합니다.- return BlockEntity.builder() - .blockId(block.getBlockId()) - .blocker(UserMapper.toEntity(block.getBlocker())) - .blocked(UserMapper.toEntity(block.getBlocked())) + return BlockEntity.builder() + .blockId(block.getBlockId()) + // 성능 최적화를 위해 ID-only 엔티티/프록시 사용 권장 + .blocker(UserMapper.toEntityIdOnly(block.getBlocker())) + .blocked(UserMapper.toEntityIdOnly(block.getBlocked())) .status(block.getStatus()) .statusChangedAt(block.getStatusChangedAt()) .expireAt(block.getExpireAt()) .feedPurgedAt(block.getFeedPurgedAt()) .build();
UserMapper.toEntityIdOnly(User)와 같이 식별자만 매핑하는 헬퍼 추가를 제안합니다.src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedRepository.java (4)
30-35: 양방향 벌크 삭제에도 동일한 flush/clear 적용 권장동일 사유로 clear/flush 자동화를 권장합니다.
- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" delete from FeedEntity f where (f.user.userId = :u and f.author.userId = :t) or (f.user.userId = :t and f.author.userId = :u) """) int deleteBidirectional(@Param("u") Long u, @Param("t") Long t);
14-15: 기능 중복 제거: 메서드 하나로 통일
deleteByUser_UserIdAndAuthor_UserId(...)와deleteOneWay(...)가 동일 목적(단방향 삭제)입니다. 유지보수성을 위해 하나로 통일하거나 한쪽을 @deprecated 처리하세요.예) 파생 메서드만 사용:
- @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query(""" - delete from FeedEntity f - where f.user.userId =:u and f.author.userId = :t - """) - int deleteOneWay(@Param("u") Long u, @Param("t") Long t); + void deleteByUser_UserIdAndAuthor_UserId(Long userId, Long authorId);또는 JPQL 메서드만 유지 시, 파생 메서드 제거.
Also applies to: 21-26
19-19: 주석 오탈자
UNFOLLOE→UNFOLLOW로 수정 바랍니다.- //Status == UNFOLLOE인 경우 -> 단방향 삭제 + // Status == UNFOLLOW 인 경우 -> 단방향 삭제
21-26: JPQL 벌크 삭제 후 1차 캐시 정합성 확보를 위해 @Modifying 옵션 추가 권장
FeedRepository의 deleteOneWay/deleteBidirectional 메서드는 현재 @Modifying만 적용되어 있어, 영속성 컨텍스트에 남아 있는 스테일 엔티티가 후속 로직에 노출될 수 있습니다. FeedService·FeedCleanupScheduler에는 @transactional이 적용되어 있지만, BlockService에는 별도 트랜잭션이 없어 리포지토리 단에서 안전장치로 flush/clear 옵션을 지정하는 것이 좋습니다. 아래와 같이 수정하세요.- @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true)src/main/java/com/spoony/spoony_server/adapter/in/web/user/UserController.java (4)
191-198: unfollow 대칭성 확인 필요언팔로우는
blockUseCase.unfollow(...)만 호출합니다. 팔로우 관계 삭제가 BlockUseCase 내부에서 수행되는지 확인이 필요합니다. follow에서 createFollow를 별도로 호출하는 현재 구조와 비대칭입니다.
- BlockUseCase가 언팔로우 영속 삭제까지 포함한다면 현 구조 유지.
- 아니라면 userFollowUseCase의 언팔로우 삭제를 추가하세요.
Also applies to: 195-197
186-194: DELETE 메서드의 RequestBody 사용 재고(호환성/캐시 관점)일부 클라이언트/프록시는 DELETE 본문을 지원하지 않습니다. URI 경로 파라미터로 식별자를 받도록 변경을 고려해 주세요.
- @DeleteMapping("/follow") - public ResponseEntity<ResponseDTO<Void>> unfollowUser(@UserId Long userId, @RequestBody UserFollowRequestDTO requestDTO) { - blockUseCase.unfollow(userId, requestDTO.targetUserId()); + @DeleteMapping("/follow/{targetUserId}") + public ResponseEntity<ResponseDTO<Void>> unfollowUser(@UserId Long userId, @PathVariable Long targetUserId) { + blockUseCase.unfollow(userId, targetUserId); return ResponseEntity.ok(ResponseDTO.success(null)); }- @DeleteMapping("/block") - public ResponseEntity<ResponseDTO<Void>> unBlockUser(@UserId Long userId, @RequestBody UserBlockRequestDTO requestDTO) { - blockUseCase.unblock(userId, requestDTO.targetUserId()); + @DeleteMapping("/block/{targetUserId}") + public ResponseEntity<ResponseDTO<Void>> unBlockUser(@UserId Long userId, @PathVariable Long targetUserId) { + blockUseCase.unblock(userId, targetUserId); return ResponseEntity.ok(ResponseDTO.success(null)); }Also applies to: 226-231
236-244: 메서드명과 기능 불일치
searchLocations는 유저 검색을 수행합니다. 혼동 방지를 위해 메서드명을searchUsers등으로 정정하세요.- public ResponseEntity<ResponseDTO<UserSearchResponseListDTO>> searchLocations( + public ResponseEntity<ResponseDTO<UserSearchResponseListDTO>> searchUsers(
24-27: 리소스 경로 네이밍(참고)베이스 경로가 단수(
/api/v1/user)입니다. 신규 API 확장성을 고려하면 관례적으로 복수형(/api/v1/users)을 권장합니다. 현 변경 범위 밖이므로 참고만 남깁니다.src/main/java/com/spoony/spoony_server/application/port/in/user/BlockUseCase.java (1)
5-12: 파라미터 네이밍 일관성
changeStatus(blockerId, blockedId)vs 나머지userId, targetUserId가 혼재합니다. API 사용성 향상을 위해 일관된 명명으로 정렬하세요.예) 모두
userId, targetUserId사용.src/main/java/com/spoony/spoony_server/domain/user/User.java (3)
3-4: JPA 관련 import 제거 권고도메인 객체(User)는 @entity가 아니므로 jakarta.persistence.EnumType/Enumerated import는 혼란만 유발합니다. 제거해 주세요.
-import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated;
14-15: 무분별한 public 무인자 생성자 지양 — 불변성/유효성 깨질 수 있음프레임워크 요구(예: Jackson) 때문에 필요하다면 접근 제어를 제한하세요. PROTECTED로 두고, 팩토리/빌더 사용을 권장합니다.
+import lombok.AccessLevel; -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED)확인: 이 클래스의 public no-args가 실제로 필요한 사용처가 있나요?
Also applies to: 7-7
1-33: 스타일 일회성 안내: 들여쓰기현재 공백 4칸 사용. 가이드라인은 탭(indent_style=tab, size=4)을 권장합니다. 추후 포맷터 설정으로 일관화 부탁드립니다.
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/FeedPersistenceAdapter.java (3)
55-60: 중복 매핑 및 불필요 객체 생성PostMapper.toEntity(post)를 두 번 호출합니다. 한 번만 생성해 재사용하세요. 또한 author는 postEntity.getUser()로 일관되게 가져오면 매핑 일치성이 좋아집니다.
- .map(post -> FeedEntity.builder() - .user(UserMapper.toEntity(user)) - .post(PostMapper.toEntity(post)) - .author(PostMapper.toEntity(post).getUser()) // 또는 Post에서 작성자 유저 꺼내기 - .build()) + .map(post -> { + var postEntity = PostMapper.toEntity(post); + return FeedEntity.builder() + .user(UserMapper.toEntity(user)) + .author(postEntity.getUser()) + .post(postEntity) + .build(); + })
85-85: 백필 중 user 미존재 시 예외 대신 무시 고려스케줄/비동기 플로우에서 USER_NOT_FOUND로 전체 작업이 중단될 수 있습니다. 로깅 후 스킵하는 것이 운영 안정성에 유리합니다.
71-79: 대량 삭제 경로에 인덱스 필요deleteOneWay/deleteBidirectional는 (user_id, author_id) 조건 삭제일 가능성이 큽니다. 다음 인덱스 권장: feed(user_id, author_id), 또한 기존 유니크(user_id, post_id)는 유지. 레포지토리 @Modifying, 배치 옵션(clearAutomatically)도 확인 바랍니다.
src/main/java/com/spoony/spoony_server/application/service/feed/FeedCleanupScheduler.java (2)
50-73: 개별 실패 격리 및 관측성 보강한 건 실패로 전체 배치가 중단되지 않도록 try/catch로 격리하고, 삭제 건수 로깅/메트릭을 추가하세요. 또한 중복 실행 방지를 위해 ShedLock 등 분산 락을 고려하세요.
- for (Block block : blocks) { - switch (block.getStatus()) { + for (Block block : blocks) { + try { + switch (block.getStatus()) { case UNFOLLOWED -> { // UNFOLLOWED → 단방향 삭제 feedPort.deleteOneWay(block.getBlocker().getUserId(), block.getBlocked().getUserId()); } case BLOCKED -> { // BLOCKED → 양방향 삭제 feedPort.deleteBidirectional(block.getBlocker().getUserId(), block.getBlocked().getUserId()); } default -> throw new IllegalStateException( "스케줄러 대상이 아닌 상태 발견: " + block.getStatus() ); - } + } // feed 삭제 시각 기록 → feed_purged_at 업데이트 blockPort.markFeedPurgedAt( block.getBlocker().getUserId(), block.getBlocked().getUserId(), now ); + } catch (Exception e) { + // TODO: 로깅/메트릭 (blocker, blocked, status, 에러메시지) + } }
31-33: 설정 키/타임존 명시@value("${batch.size:2000}")는 범용 키로 충돌 우려가 있습니다. feed 관련 네임스페이스로 변경을 권장합니다. 스케줄 타임존도 명시하세요.
- @Value("${batch.size:2000}") int pageSize; - @Scheduled(cron = "0 0 3 * * *") + @Value("${feed.cleanup.page-size:2000}") int pageSize; + @Scheduled(cron = "0 0 3 * * *", zone = "${app.timezone:Asia/Seoul}")src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedEntity.java (2)
23-33: 연관관계 null 방지 명시JPA 측면에서 optional=false를 추가해 제약을 명확히 하세요. (DDL은 이미 nullable=false)
- @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", nullable = false) ... - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "author_id", nullable = false) ... - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "post_id", nullable = false)
16-18: 유니크 제약 추가는 적절함(user_id, post_id) 유니크로 중복 피드 방지됩니다. 대량 삭제 최적화를 위해 (user_id, author_id) 보조 인덱스도 고려 바랍니다.
src/main/java/com/spoony/spoony_server/application/port/out/user/BlockPort.java (1)
16-24: 레거시 API 혼재 — 인터페이스 표면 축소 필요deleteUserBlockRelation/existsBlockUserRelation 등 관계 중심 메서드는 새 도메인 API와 중복됩니다. 일단 @deprecated로 표시하고, 사용처 제거 후 인터페이스에서 제거를 제안합니다.
- void deleteUserBlockRelation(Long fromUserId, Long toUserId, BlockStatus status); - boolean existsBlockUserRelation(Long fromUserId, Long toUserId); + @Deprecated void deleteUserBlockRelation(Long fromUserId, Long toUserId, BlockStatus status); + @Deprecated boolean existsBlockUserRelation(Long fromUserId, Long toUserId);또한 findExpiredBlocks의 필터 조건(예: feedPurgedAt IS NULL, expireAt <= now)을 자바독로 명시해 오해를 줄이세요.
src/main/java/com/spoony/spoony_server/application/service/feed/FeedService.java (1)
66-70: 필터 성능(Set 사용) + ‘신고(Report)’ 상태 필터링 누락
- 현재 List.contains로 매 피드마다 O(n) 탐색 → Set으로 전환 권장.
- PR 요약엔 “Block/Report/Unfollow 상태 필터링”이라 했으나 본 메서드는 Report 필터가 없습니다. 비즈 규칙상 신고 사용자/게시물 배제 필요하면 동일 단계에서 반영해야 합니다.
성능 개선 diff(Set 적용):
- List<Long> blockedUserIds = blockPort.getBlockedUserIds(command.getUserId()); + java.util.Set<Long> blockedUserIds = new java.util.HashSet<>(blockPort.getBlockedUserIds(command.getUserId())); - List<Long> blockerUserIds = blockPort.getBlockerUserIds(command.getUserId()); + java.util.Set<Long> blockerUserIds = new java.util.HashSet<>(blockPort.getBlockerUserIds(command.getUserId()));추가로, 필요 시 신고 관계도 미리 조회해 동일 필터 체인에서 제외하세요.
Also applies to: 75-78
src/main/java/com/spoony/spoony_server/domain/user/Block.java (2)
41-49: id 전용 User 생성 방식 확인 필요
new User(blockerId)/new User(blockedId)생성자가 실제로 존재/유지되는지 확인해 주세요. 명시적 팩토리(예:User.idOnly(id)또는User.of(id))를 두면 의도 전달이 더 명확하고 생성자 변경에 덜 취약합니다. 필요 시 해당 정적 팩토리 추가를 제안합니다.
66-69: feedPurgedAt는 단조 증가(idempotent) 보장 권장재실행/중복 호출 시 이전 기록을 덮지 않도록 가드가 있으면 안전합니다.
- public void markFeedPurged(LocalDateTime now) { - this.feedPurgedAt = now; - } + public void markFeedPurged(LocalDateTime now) { + if (this.feedPurgedAt == null || now.isAfter(this.feedPurgedAt)) { + this.feedPurgedAt = now; + } + }src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockEntity.java (1)
38-42: 만료 스케줄러 쿼리 성능을 위한 인덱스 추가 제안
findExpiredBlocks(status, expireAt <= now)류의 쿼리를 고려하면 (status, expire_at) 복합 인덱스가 효과적입니다. blocker_id/blocked_id 단독 인덱스도 조회 최적화에 도움.제안 diff(@table에 인덱스 추가):
-@Table(name = "block", - uniqueConstraints = @UniqueConstraint(columnNames = {"blocker_id", "blocked_id"})) +@Table( + name = "block", + uniqueConstraints = @UniqueConstraint(columnNames = {"blocker_id", "blocked_id"}), + indexes = { + @Index(name = "idx_block_status_expire_at", columnList = "status, expire_at"), + @Index(name = "idx_block_blocker_id", columnList = "blocker_id"), + @Index(name = "idx_block_blocked_id", columnList = "blocked_id") + } +)src/main/java/com/spoony/spoony_server/application/service/Block/BlockService.java (3)
1-1: 패키지명은 소문자 권장 (hackday-conventions-java).
package ...service.Block;처럼 대문자를 포함한 패키지명은 지양하고,...service.block처럼 소문자 패키지를 권장합니다.
129-140: report 처리: 삭제 호출의 멱등성/에러 핸들링.상대 방향까지 일괄 삭제하는 부분이 재시도에 안전한지(존재하지 않아도 성공)와 예외 전파 정책을 확인해주세요. 포트 레벨에서 “있으면 삭제” 형태로 멱등하게 구현 권장.
143-150: 상태 변경의 경쟁 조건과 재시도 안전성.
findByBlockerAndBlocked -> updateStatus -> save흐름은 경합 시 덮어쓰기 위험.BlockEntity에 version(낙관적 락) 추가 및 저장 실패 시 재시도 전략을 권장합니다.src/main/java/com/spoony/spoony_server/application/service/user/UserService.java (2)
134-151: Followers 조회: 블록 필터 contains 비용과 팔로우 여부 N+1.위와 동일하게 Set 변환 및 팔로우 여부 일괄 조회로 교체 권장. 결과 집계(count)는 컬렉션 크기 의존이므로 OK.
- List<Long> blockedUserIds = blockPort.getBlockedUserIds(currentUserId); - List<Long> blockerUserIds = blockPort.getBlockerUserIds(currentUserId); + Set<Long> blockedUserIds = new HashSet<>(blockPort.getBlockedUserIds(currentUserId)); + Set<Long> blockerUserIds = new HashSet<>(blockPort.getBlockerUserIds(currentUserId));
159-176: Followings 조회: 동일한 최적화 적용.Set 변환 + 팔로우 관계 일괄 조회 포트 도입 권장.
- List<Long> blockedUserIds = blockPort.getBlockedUserIds(currentUserId); - List<Long> blockerUserIds = blockPort.getBlockerUserIds(currentUserId); + Set<Long> blockedUserIds = new HashSet<>(blockPort.getBlockedUserIds(currentUserId)); + Set<Long> blockerUserIds = new HashSet<>(blockPort.getBlockerUserIds(currentUserId));src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/BlockPersistenceAdapter.java (3)
42-45: 만료 블록 조회 read 전용 힌트.대량 조회일 가능성이 높아 메서드 레벨
@Transactional(readOnly = true)부여를 고려하세요(클래스 레벨 트랜잭션이 쓰기 기본인 경우).
87-181: 대규모 주석 처리(dead code) 정리 필요.과거 구현 전체가 주석으로 남아 있어 가독성/유지보수에 악영향. 히스토리는 Git에 있으니 제거 권장.
- // private final UserRepository userRepository; - // private final BlockRepository blockRepository; - // ... - // public void markFeedPurgedAt(Long blockerId, Long blockedId, LocalDateTime now) { - // blockRepository.markFeedPurgedAt(blockerId,blockedId,now); - // } + // (주석 제거)
53-60: 상태별 삭제 메서드의 호출부 일치 여부 확인.
deleteUserBlockRelation(..., status)는 현재 서비스 계층에서 사용되지 않는 것으로 보입니다. 불필요 시 포트/어댑터에서 제거하여 API 표면 축소를 검토하세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (30)
src/main/java/com/spoony/spoony_server/adapter/in/web/user/UserController.java(5 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/BlockPersistenceAdapter.java(1 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockEntity.java(1 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockRepository.java(2 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockStatus.java(1 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/FeedPersistenceAdapter.java(3 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/PostSpecification.java(8 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedEntity.java(2 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedRepository.java(2 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/event/PostCreatedEventListener.java(1 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.java(2 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.java(2 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/user/UserPersistenceAdapter.java(1 hunks)src/main/java/com/spoony/spoony_server/adapter/out/persistence/user/mapper/BlockMapper.java(1 hunks)src/main/java/com/spoony/spoony_server/application/auth/service/AuthService.java(1 hunks)src/main/java/com/spoony/spoony_server/application/auth/service/KakaoService.java(1 hunks)src/main/java/com/spoony/spoony_server/application/port/in/user/BlockUseCase.java(1 hunks)src/main/java/com/spoony/spoony_server/application/port/in/user/UserGetUseCase.java(1 hunks)src/main/java/com/spoony/spoony_server/application/port/out/feed/FeedPort.java(1 hunks)src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java(2 hunks)src/main/java/com/spoony/spoony_server/application/port/out/user/BlockPort.java(1 hunks)src/main/java/com/spoony/spoony_server/application/port/out/user/UserPort.java(1 hunks)src/main/java/com/spoony/spoony_server/application/service/Block/BlockService.java(1 hunks)src/main/java/com/spoony/spoony_server/application/service/feed/FeedCleanupScheduler.java(1 hunks)src/main/java/com/spoony/spoony_server/application/service/feed/FeedService.java(3 hunks)src/main/java/com/spoony/spoony_server/application/service/report/ReportService.java(2 hunks)src/main/java/com/spoony/spoony_server/application/service/user/UserService.java(1 hunks)src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java(5 hunks)src/main/java/com/spoony/spoony_server/domain/user/Block.java(1 hunks)src/main/java/com/spoony/spoony_server/domain/user/User.java(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.java
⚙️ CodeRabbit configuration file
**/*.java: 다음 규칙은 '일회성 체크'로 위반 패턴만 지적(지속적 강제 X). hackday-conventions-java 기준: - 탭 들여쓰기(indent_style=tab), indent_size=4 - 한 줄 최대 120자 (max_line_length=120) - 파일 끝 개행 (newline-eof) - 주석 전후 공백 (space-around-comment) - 중괄호 K&R 스타일 참고: https://naver.github.io/hackday-conventions-java/
Files:
src/main/java/com/spoony/spoony_server/application/port/out/user/UserPort.javasrc/main/java/com/spoony/spoony_server/application/auth/service/AuthService.javasrc/main/java/com/spoony/spoony_server/application/port/in/user/BlockUseCase.javasrc/main/java/com/spoony/spoony_server/application/port/out/feed/FeedPort.javasrc/main/java/com/spoony/spoony_server/application/service/report/ReportService.javasrc/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/event/PostCreatedEventListener.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockStatus.javasrc/main/java/com/spoony/spoony_server/application/port/in/user/UserGetUseCase.javasrc/main/java/com/spoony/spoony_server/application/port/out/user/BlockPort.javasrc/main/java/com/spoony/spoony_server/adapter/in/web/user/UserController.javasrc/main/java/com/spoony/spoony_server/application/service/feed/FeedCleanupScheduler.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/user/UserPersistenceAdapter.javasrc/main/java/com/spoony/spoony_server/application/auth/service/KakaoService.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedRepository.javasrc/main/java/com/spoony/spoony_server/domain/user/Block.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockEntity.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/user/mapper/BlockMapper.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockRepository.javasrc/main/java/com/spoony/spoony_server/application/service/Block/BlockService.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/FeedPersistenceAdapter.javasrc/main/java/com/spoony/spoony_server/application/service/feed/FeedService.javasrc/main/java/com/spoony/spoony_server/domain/user/User.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedEntity.javasrc/main/java/com/spoony/spoony_server/application/service/user/UserService.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/PostSpecification.javasrc/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.javasrc/main/java/com/spoony/spoony_server/adapter/out/persistence/block/BlockPersistenceAdapter.java
**/*Controller*.java
⚙️ CodeRabbit configuration file
**/*Controller*.java: REST 컨트롤러 체크: - @RestController 명시 - 리소스 중심 URI(/api/v1/users 등) - 동작은 서브리소스/액션으로 표현(/orders/{id}/cancel) - 하나의 URI에 하나의 식별자 - HTTP 메서드 의미에 맞게 사용(GET/POST/PUT/DELETE)
Files:
src/main/java/com/spoony/spoony_server/adapter/in/web/user/UserController.java
🔇 Additional comments (20)
src/main/java/com/spoony/spoony_server/application/service/report/ReportService.java (1)
75-77: Optional 기반 기본값 처리 깔끔합니다null일 때 PROMOTIONAL_CONTENT 기본값 부여 방식 명확합니다. 기존 createReport의 기본값 처리와 일관성 있습니다.
src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockStatus.java (1)
3-8: BlockEntity.status에 STRING 매핑 적용으로 ORDINAL 위험 없음
BlockEntity.status 필드에 이미@Enumerated(EnumType.STRING)이 적용되어 있어 enum 순서 변경 시에도 기존 데이터가 올바르게 해석됩니다. 별도 마이그레이션이나 추가 조치 불필요합니다.src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java (1)
11-11: 시간 파라미터의 기준/타임존 명시 필요
LocalDateTime since는 타임존 정보가 없어 서버/DB 타임존 차이로 경계값 오차가 날 수 있습니다. 저장 측이 UTC라면Instant/OffsetDateTime또는 명시적 TZ 합의를 문서화해 주세요.src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.java (1)
330-336: 경계 포함 여부 명세
CreatedAtAfter는 “초과(>)”입니다. 동일 타임스탬프 경계에서 누락/중복 방지를 위해 커서 기준(시각, postId)을 함께 관리하거나, 포함(≥)일 경우 명세/테스트를 맞춰 주세요.src/main/java/com/spoony/spoony_server/application/port/out/feed/FeedPort.java (1)
18-18: 증분 백필 계약 정의(Idempotent/정렬/용량)
newPosts에 대해 (1) 정렬 기준(asc by createdAt, postId) (2) 중복 방지(idempotent) (3) 대용량 시 배치/스트리밍 계약을 포트 주석으로 명시해 주세요. 구현은 유니크키와 존재검사로 보완하세요.src/main/java/com/spoony/spoony_server/adapter/out/persistence/user/mapper/BlockMapper.java (1)
11-21: toDomain 필드 매핑은 타당해 보임엔티티의 신규 타임스탬프 필드를 도메인으로 모두 반영하고 있어 일관성이 있습니다.
src/main/java/com/spoony/spoony_server/adapter/in/web/user/UserController.java (2)
226-231: 차단 해제 흐름 일원화는 좋습니다unblock을 BlockUseCase로 위임하여 도메인 규칙을 한 곳에서 관리하는 방향이 적절합니다.
169-184: BlockUseCase.follow과 UserFollowUseCase.createFollow은 중복이 아닙니다
BlockUseCase.follow은 blockPort를 통한 상태 전이(팔로우/언팔로우)만 처리하며, UserFollowUseCase.createFollow은 별도의 팔로우 엔티티를 생성합니다. 두 호출 모두 유지하세요.Likely an incorrect or invalid review comment.
src/main/java/com/spoony/spoony_server/domain/user/User.java (1)
29-32: ID-전용 생성자 추가는 적절함연관 도메인에서 참조용 프록시로 쓰기 좋습니다. 다만 equals/hashCode를 userId 기반으로 오버라이드할지 팀 컨벤션 확인 바랍니다.
src/main/java/com/spoony/spoony_server/application/port/out/user/BlockPort.java (2)
14-16: 도메인 중심 메서드 추가 적절findByBlockerAndBlocked/saveBlock 도입 좋습니다. 구현체/트랜잭션 경계 일관성만 확인해 주세요.
25-26: 스케줄러 연동 메서드 시그니처 적합만료 블록 조회/퍼지 시각 마킹은 목적에 부합합니다. Pageable을 받지만 List를 반환하므로, 페이징 일관성(정렬 키 고정 등)만 확인 바랍니다.
src/main/java/com/spoony/spoony_server/application/service/feed/FeedService.java (2)
34-38: UserPort DI 추가는 적절합니다새 백필/프리필 흐름을 위해 UserPort 주입한 선택 합리적입니다.
44-60: 검증 완료: FeedEntity에user_id, post_id복합 유니크 제약이 선언되어 있으며,addFeedsIfNotExists에서existsByUserIdAndPostId체크로 중복 삽입을 방지하고 있어 멱등성이 보장됩니다.src/main/java/com/spoony/spoony_server/domain/user/Block.java (1)
22-37: 빌더 기반 생성자 추가는 도메인 표현력 향상에 도움상태 전이 관련 타임스탬프를 함께 보존하는 점 좋습니다.
src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockEntity.java (2)
15-17: (blocker_id, blocked_id) 유니크 제약 추가 LGTM중복 관계 방지에 필수 제약입니다. 잘 반영되었습니다.
24-29: FK null 금지 설정 적절관계 무결성 확보 측면에서 적합합니다.
src/main/java/com/spoony/spoony_server/application/service/Block/BlockService.java (2)
69-75: 증분 백필 기준시각 확인 요청.
changeStatus()호출 후에도 이전block인스턴스의statusChangedAt을 기준으로 증분 백필을 수행합니다. 의도대로 “언팔 시각 이후”가 맞는지 확인 부탁드립니다. 도메인에서updateStatus가 이전 값을 덮지 않는 전제라면 OK입니다.
120-127: unblock 시 상태 UNFOLLOWED로 전환하는 설계 재확인.차단 해제 후 자동 재팔로우가 아닌 UNFOLLOWED로 두는 정책이 요구사항과 일치하는지 확인 바랍니다(클라이언트 UX에도 영향).
src/main/java/com/spoony/spoony_server/application/service/user/UserService.java (1)
187-203: BLOCKED만 노출하는 정책 확인.신고(REPORT) 상태를 차단 목록에서 제외하는 정책이 기획과 일치하는지 재확인 바랍니다(사용자 혼선 방지).
src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/BlockPersistenceAdapter.java (1)
31-39: 도메인 매퍼 의존 경로 확인.
BlockMapper가adapter.out.persistence.user.mapper패키지에 있으며, 다른 레이어에 동명이존이 없는지 확인 바랍니다. 중복 매퍼가 있다면 단일화 권장.
| // 스케줄러가 만료된 (UNFOLLOWED, BLOCKED) 관계 조회 | ||
| @Query(""" | ||
| SELECT b | ||
| FROM BlockEntity b | ||
| WHERE b.status IN :statuses | ||
| AND b.expireAt IS NOT NULL | ||
| AND b.expireAt <= :now | ||
| """) | ||
| List<BlockEntity> findExpiredBlocks(@Param("statuses") List<BlockStatus> statuses, | ||
| @Param("now") LocalDateTime now, | ||
| Pageable pageable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
만료 블록 페이지 조회에 ORDER BY 부재: 페이지 누락/중복 위험
Pageable과 함께 사용시 결정적 정렬이 없으면 만료 스캔 중 삽입/갱신으로 인해 누락/중복 가능성이 큽니다. 안정 정렬 키를 추가하세요.
@Query("""
SELECT b
FROM BlockEntity b
WHERE b.status IN :statuses
AND b.expireAt IS NOT NULL
AND b.expireAt <= :now
+ ORDER BY b.expireAt ASC, b.blockId ASC
""")
List<BlockEntity> findExpiredBlocks(@Param("statuses") List<BlockStatus> statuses,
@Param("now") LocalDateTime now,
Pageable pageable);추가로 운영 관점에서 b.status, b.expireAt에 합성 인덱스를 두는 것을 권장합니다.
📝 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.
| // 스케줄러가 만료된 (UNFOLLOWED, BLOCKED) 관계 조회 | |
| @Query(""" | |
| SELECT b | |
| FROM BlockEntity b | |
| WHERE b.status IN :statuses | |
| AND b.expireAt IS NOT NULL | |
| AND b.expireAt <= :now | |
| """) | |
| List<BlockEntity> findExpiredBlocks(@Param("statuses") List<BlockStatus> statuses, | |
| @Param("now") LocalDateTime now, | |
| Pageable pageable); | |
| // 스케줄러가 만료된 (UNFOLLOWED, BLOCKED) 관계 조회 | |
| @Query(""" | |
| SELECT b | |
| FROM BlockEntity b | |
| WHERE b.status IN :statuses | |
| AND b.expireAt IS NOT NULL | |
| AND b.expireAt <= :now | |
| ORDER BY b.expireAt ASC, b.blockId ASC | |
| """) | |
| List<BlockEntity> findExpiredBlocks(@Param("statuses") List<BlockStatus> statuses, | |
| @Param("now") LocalDateTime now, | |
| Pageable pageable); |
🤖 Prompt for AI Agents
In
src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockRepository.java
around lines 66 to 76, the JPQL query for findExpiredBlocks lacks a
deterministic ORDER BY when used with Pageable which can cause
missing/duplicated results during concurrent inserts/updates; update the query
to include a stable sort (for example ORDER BY b.expireAt ASC, b.id ASC) so
pagination is deterministic, and add a composite DB index on (status, expireAt)
(or status + expireAt + id if supported) to improve scan performance and ensure
the ORDER BY can use the index.
| // 스케줄러가 Feed 삭제 후 라이트로그 기록(JPQL 직접 업데이트) | ||
| @Modifying | ||
| @Query(""" | ||
| update BlockEntity b | ||
| set b.feedPurgedAt = :now | ||
| where b.blocker.userId = :u | ||
| and b.blocked.userId = :t | ||
| """) | ||
| int markFeedPurgedAt(@Param("u") Long u, // 언팔 or 차단한 사람 | ||
| @Param("t") Long t, // 당한 사람 | ||
| @Param("now") LocalDateTime now); // 언제 삭제했는지 기록 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
feedPurgedAt 업데이트의 멱등성 및 과도한 갱신 가능성
- 기존 값을 덮어쓰면 재시도 시각이 변질됩니다. null일 때만 기록하도록 변경하세요.
- @Modifying 옵션으로 flush/clear를 켜면 동시성 이슈/캐시 오염을 줄일 수 있습니다.
- @Modifying
+ @Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update BlockEntity b
- set b.feedPurgedAt = :now
+ set b.feedPurgedAt = COALESCE(b.feedPurgedAt, :now)
where b.blocker.userId = :u
and b.blocked.userId = :t
+ and b.feedPurgedAt IS NULL
""")
int markFeedPurgedAt(@Param("u") Long u, // 언팔 or 차단한 사람
@Param("t") Long t, // 당한 사람
@Param("now") LocalDateTime now); // 언제 삭제했는지 기록🤖 Prompt for AI Agents
In
src/main/java/com/spoony/spoony_server/adapter/out/persistence/block/db/BlockRepository.java
around lines 78 to 89, the JPQL update currently overwrites feedPurgedAt
unconditionally which breaks idempotency on retries and causes unnecessary
updates; change the query to set b.feedPurgedAt = :now only when b.feedPurgedAt
IS NULL (e.g., add "and b.feedPurgedAt IS NULL" to the WHERE clause) and add
@Modifying(clearAutomatically = true, flushAutomatically = true) to reduce
concurrency/cache issues so the update only writes once and repository cache is
refreshed.
| @Column(name = "created_at", nullable = false) | ||
| private LocalDateTime createdAt = LocalDateTime.now(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
생성 시각은 ORM/DB가 책임지게
필드 초기화(LocalDateTime.now())는 테스트/타임존 불일치 유발. @CreationTimestamp와 updatable=false 사용을 권장합니다.
- @Column(name = "created_at", nullable = false)
- private LocalDateTime createdAt = LocalDateTime.now();
+ @org.hibernate.annotations.CreationTimestamp
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private LocalDateTime createdAt;📝 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.
| @Column(name = "created_at", nullable = false) | |
| private LocalDateTime createdAt = LocalDateTime.now(); | |
| @org.hibernate.annotations.CreationTimestamp | |
| @Column(name = "created_at", nullable = false, updatable = false) | |
| private LocalDateTime createdAt; |
🤖 Prompt for AI Agents
In
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedEntity.java
around lines 35-37, remove the field initializer LocalDateTime.now() from
createdAt and let the database/ORM set the value: annotate the field with
Hibernate's @CreationTimestamp and set @Column(updatable = false, nullable =
false) so the timestamp is populated on insert and not updated; ensure the
proper import for org.hibernate.annotations.CreationTimestamp is added and
remove any direct initialization to avoid test/timezone inconsistencies.
| delete from FeedEntity f | ||
| where f.user.userId =:u and f.author.userId = :t | ||
| """) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
삭제 쿼리 성능 보강(인덱스 확인 요청)
where 절이 (user_id, author_id) 조합을 사용합니다. 해당 컬럼 복합 인덱스가 없으면 대량 삭제 시 성능 저하가 큽니다. 스키마에 (user_id, author_id) 복합 인덱스 존재 여부 확인/추가를 권장합니다.
검증 스크립트(DDL/엔티티 인덱스 어노테이션 탐색):
Also applies to: 32-34
🏁 Script executed:
#!/bin/bash
rg -nP --type=java -C2 '@Table\('
rg -nP --type=java -C2 '@Index\(|indexes\s*='
rg -nP -C2 --type=sql 'create\s+index|create\s+unique\s+index'Length of output: 15137
FeedEntity에 (user_id, author_id) 복합 인덱스 추가 필요
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedEntity.java:16
@table 어노테이션에 indexes = @Index(name = "idx_feed_user_author", columnList = "user_id, author_id") 를 추가하여 대량 삭제 시 성능 저하 방지
🤖 Prompt for AI Agents
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedRepository.java
lines 22-24 and
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedEntity.java
around line 16: add a composite index to the FeedEntity @Table annotation by
setting indexes = @Index(name = "idx_feed_user_author", columnList = "user_id,
author_id"); ensure the javax.persistence.Index (and Table) import is present
and update any DB migration/schema files so the new index is created in
production.
| """) | ||
| int deleteBidirectional(@Param("u") Long u, @Param("t") Long t); | ||
|
|
||
| boolean existsByUserIdAndPostId(Long userId, Long postId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
파생 쿼리 메서드 경로 오탈자: 런타임 실패 가능
엔티티가 연관 필드(user, post)를 가지는 구조이므로 파생 쿼리는 중첩 경로를 사용해야 합니다. 현재 시그니처는 파싱 실패 위험이 큽니다.
다음으로 수정 제안:
- boolean existsByUserIdAndPostId(Long userId, Long postId);
+ boolean existsByUser_UserIdAndPost_PostId(Long userId, Long postId);📝 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.
| boolean existsByUserIdAndPostId(Long userId, Long postId); | |
| boolean existsByUser_UserIdAndPost_PostId(Long userId, Long postId); |
🤖 Prompt for AI Agents
In
src/main/java/com/spoony/spoony_server/adapter/out/persistence/feed/db/FeedRepository.java
around line 37, the derived query method uses flat property names but the entity
has associations (user, post), so change the method signature to use nested path
properties (e.g. existsByUser_IdAndPost_Id) to match Spring Data JPA parsing;
rename the method accordingly, keep the parameter types as Long, and run/compile
tests to ensure the repository bean loads correctly.
| // 차단 + 신고 관계 유저 ID | ||
| Set<Long> excludedUserIds = collectExcludedUserIds(user.getUserId()); | ||
|
|
||
| List<ZzimPost> zzimPostList = | ||
| zzimPostPort.findZzimPostsByUserIdSortedByCreatedAtDesc(user.getUserId()); | ||
|
|
||
| List<ZzimFocusResponseDTO> responseList = zzimPostList.stream() | ||
| .filter(zzimPost -> { | ||
| Post post = zzimPost.getPost(); | ||
| Place place = post.getPlace(); | ||
| if (place == null) return false; | ||
|
|
||
| Long authorId = post.getUser().getUserId(); | ||
| return place.getPlaceId().equals(command.getPlaceId()) | ||
| && !excludedUserIds.contains(authorId); | ||
| }) | ||
| .map(this::toZzimFocusResponse) | ||
| .toList(); | ||
|
|
||
| return ZzimFocusListResponseDTO.of(responseList); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Focus 리스트도 N+1 위험 있으며 동일한 배치 전략 적용 필요
toZzimFocusResponse에서 사진 리스트/카테고리를 포스트별 조회합니다. getZzimCardList와 동일하게 배치 조회 포트를 도입해 왕복 수를 줄이세요.
🤖 Prompt for AI Agents
In
src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java
around lines 90 to 110, the stream mapping to toZzimFocusResponse causes an N+1
by loading photos/categories per post; instead introduce a batch fetch port
(like the one used for getZzimCardList) to load all photos and category data for
the collected postIds in one call, build maps keyed by postId, then replace the
direct toZzimFocusResponse calls with a mapping that supplies the pre-fetched
maps (or overload toZzimFocusResponse to accept those maps) so each post
assembles its DTO from the cached map entries and you preserve the existing
filtering/sorting logic.
| private ZzimCardListResponseDTO getZzimByAreaDong(Long userId, Double lon, Double lat) { | ||
| Map<Long, ZzimPost> uniqueZzimPosts = getFilteredUniqueZzimPosts(userId); | ||
|
|
||
| List<ZzimCardResponseDTO> responseList = uniqueZzimPosts.values().stream() | ||
| .filter(zzimPost -> { | ||
| Place place = zzimPost.getPost().getPlace(); | ||
| return place.getLatitude() != null && place.getLongitude() != null && | ||
| calculateDistance(latitude, longitude, place.getLatitude(), place.getLongitude()) <= 2.0; | ||
| }) | ||
| .map(this::toZzimCardResponse) | ||
| .toList(); | ||
| .filter(zzimPost -> { | ||
| Place place = zzimPost.getPost().getPlace(); | ||
| return place.getLatitude() != null && place.getLongitude() != null && | ||
| calculateDistance(lat, lon, place.getLatitude(), place.getLongitude()) <= 2.0; | ||
| }) | ||
| .map(this::toZzimCardResponse) | ||
| .toList(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
lat/lon 파라미터 순서 혼동 가능 — 시그니처 정비 및 상수화 권장
메서드 서명은 (lon, lat)지만 calculateDistance에는 (lat, lon) 순서로 전달됩니다. 현재 동작은 맞지만 오탈자 유발 지점입니다. 일반 관례인 (lat, lon) 순서로 서명을 바꾸고 반경은 상수로 추출하세요.
적용 예:
-private ZzimCardListResponseDTO getZzimByAreaDong(Long userId, Double lon, Double lat) {
+private ZzimCardListResponseDTO getZzimByAreaDong(Long userId, Double lat, Double lon) {
Map<Long, ZzimPost> uniqueZzimPosts = getFilteredUniqueZzimPosts(userId);
List<ZzimCardResponseDTO> responseList = uniqueZzimPosts.values().stream()
.filter(zzimPost -> {
Place place = zzimPost.getPost().getPlace();
return place.getLatitude() != null && place.getLongitude() != null &&
- calculateDistance(lat, lon, place.getLatitude(), place.getLongitude()) <= 2.0;
+ calculateDistance(lat, lon, place.getLatitude(), place.getLongitude()) <= AREA_DONG_RADIUS_KM;
})
.map(this::toZzimCardResponse)
.toList();클래스 상단(필드 영역)에 아래 상수 추가:
private static final double AREA_DONG_RADIUS_KM = 2.0;getZzimByLocation에서의 호출 인자도 (lat, lon) 순으로 교체하세요.
| private ZzimCardListResponseDTO getZzimByAreaStation(Long userId, Double lon, Double lat) { | ||
| Map<Long, ZzimPost> uniqueZzimPosts = getFilteredUniqueZzimPosts(userId); | ||
|
|
||
| List<ZzimCardResponseDTO> responseList = uniqueZzimPosts.values().stream() | ||
| .filter(zzimPost -> { | ||
| Place place = zzimPost.getPost().getPlace(); | ||
| return place.getLatitude() != null && place.getLongitude() != null && | ||
| calculateDistance(latitude, longitude, place.getLatitude(), place.getLongitude()) <= 1.0; | ||
| }) | ||
| .map(this::toZzimCardResponse) | ||
| .toList(); | ||
| .filter(zzimPost -> { | ||
| Place place = zzimPost.getPost().getPlace(); | ||
| return place.getLatitude() != null && place.getLongitude() != null && | ||
| calculateDistance(lat, lon, place.getLatitude(), place.getLongitude()) <= 1.0; | ||
| }) | ||
| .map(this::toZzimCardResponse) | ||
| .toList(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
역/스테이션 범위도 동일하게 서명/상수 정비
AreaStation도 (lat, lon) 시그니처와 상수 사용으로 통일하세요.
-private ZzimCardListResponseDTO getZzimByAreaStation(Long userId, Double lon, Double lat) {
+private ZzimCardListResponseDTO getZzimByAreaStation(Long userId, Double lat, Double lon) {
Map<Long, ZzimPost> uniqueZzimPosts = getFilteredUniqueZzimPosts(userId);
List<ZzimCardResponseDTO> responseList = uniqueZzimPosts.values().stream()
.filter(zzimPost -> {
Place place = zzimPost.getPost().getPlace();
return place.getLatitude() != null && place.getLongitude() != null &&
- calculateDistance(lat, lon, place.getLatitude(), place.getLongitude()) <= 1.0;
+ calculateDistance(lat, lon, place.getLatitude(), place.getLongitude()) <= AREA_STATION_RADIUS_KM;
})
.map(this::toZzimCardResponse)
.toList();클래스 상단에 추가:
private static final double AREA_STATION_RADIUS_KM = 1.0;| postCategory.getCategory().getCategoryId(), | ||
| postCategory.getCategory().getCategoryName(), | ||
| postCategory.getCategory().getIconUrlColor(), | ||
| postCategory.getCategory().getTextColor(), | ||
| postCategory.getCategory().getBackgroundColor() | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
사진/카테고리 null 안전성 보강 (NPE 위험 제거)
사진이 없는 게시글일 경우 photo.getPhotoUrl()에서 NPE가 발생할 수 있습니다. null-safe 처리하세요.
-Photo photo = zzimPostPort.findFistPhotoById(post.getPostId());
+Photo photo = zzimPostPort.findFistPhotoById(post.getPostId());
+String photoUrl = (photo != null) ? photo.getPhotoUrl() : null;
...
return ZzimCardResponseDTO.of(
place.getPlaceId(),
place.getPlaceName(),
post.getDescription(),
place.getPlaceAddress(),
- photo.getPhotoUrl(),
+ photoUrl,
place.getLatitude(),
place.getLongitude(),
categoryColor
);또한 postCategory 또는 그 하위 category가 null일 가능성이 계약상 배제되지 않았다면 사전 검증 또는 안전한 기본값을 적용해 주세요.
Also applies to: 220-227
🏁 Script executed:
#!/bin/bash
set -euxo pipefail
# Locate ZzimPostService.java
service_file=$(fd ZzimPostService.java)
echo "Service file: $service_file"
# Show lines around the snippet (200–250)
sed -n '200,250p' "$service_file"
# Search for the photo lookup and usage
rg -n "findFistPhotoById" -C2 "$service_file"
# Locate ZzimPostPort.java to inspect the method signature
port_file=$(fd ZzimPostPort.java)
echo "Port file: $port_file"
# Show the signature of findFistPhotoById
grep -R "findFistPhotoById" -n "$port_file"Length of output: 3367
null 안전성 보강
- toZzimCardResponse:
zzimPostPort.findFistPhotoById()가 null을 반환할 수 있으므로와 같이 NPE 방지 후 DTO에 전달Photo photo = zzimPostPort.findFistPhotoById(post.getPostId()); String photoUrl = (photo != null) ? photo.getPhotoUrl() : null;
- toZzimFocusResponse:
findPhotoListById()가 null을 반환하지 않는지 계약을 확인하거나, null일 경우Collections.emptyList()로 변환해.stream().map(...)전 null 체크 적용 - postCategory 또는
postCategory.getCategory()가 null일 가능성을 점검하고, 필요 시 기본값 할당 또는 예외 처리
🤖 Prompt for AI Agents
In
src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java
around lines 212-217, guard against possible null returns and NPEs: when calling
zzimPostPort.findFistPhotoById(post.getPostId()) check for null and derive
photoUrl = photo != null ? photo.getPhotoUrl() : null before passing to the DTO;
when calling findPhotoListById(...) ensure the returned list is not null by
using Collections.emptyList() as a fallback before streaming and mapping; and
validate that postCategory and postCategory.getCategory() are non-null (or
provide sensible default values or throw a clear exception) before accessing
categoryId/categoryName/iconUrlColor/textColor.
| public void updateStatus(BlockStatus newStatus, LocalDateTime now, int unfollowedDays, int blockedDays) { | ||
| if (this.status == newStatus) return; | ||
| this.status = newStatus; | ||
| this.statusChangedAt = now; | ||
|
|
||
| switch (newStatus) { | ||
| case UNFOLLOWED -> this.expireAt = now.plusDays(unfollowedDays); | ||
| case BLOCKED -> this.expireAt = now.plusDays(blockedDays); | ||
| case FOLLOW -> { this.expireAt = null; this.feedPurgedAt = null; } | ||
| case REPORT -> this.expireAt = null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
도메인 레이어가 adapter 패키지의 BlockStatus에 의존 → 레이어링 위반
- Domain이 adapter(out.persistence)를 import하면 의존 방향이 뒤집힙니다. BlockStatus를 domain 패키지로 승격하고, persistence는 이를 매핑만 하도록 정리하세요.
- 향후 새 상태 추가 시 안전망을 위해 switch default 처리도 권장합니다.
default 가드 추가 diff:
switch (newStatus) {
case UNFOLLOWED -> this.expireAt = now.plusDays(unfollowedDays);
case BLOCKED -> this.expireAt = now.plusDays(blockedDays);
case FOLLOW -> { this.expireAt = null; this.feedPurgedAt = null; }
case REPORT -> this.expireAt = null;
+ default -> throw new IllegalArgumentException("Unsupported status: " + newStatus);
}📝 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.
| public void updateStatus(BlockStatus newStatus, LocalDateTime now, int unfollowedDays, int blockedDays) { | |
| if (this.status == newStatus) return; | |
| this.status = newStatus; | |
| this.statusChangedAt = now; | |
| switch (newStatus) { | |
| case UNFOLLOWED -> this.expireAt = now.plusDays(unfollowedDays); | |
| case BLOCKED -> this.expireAt = now.plusDays(blockedDays); | |
| case FOLLOW -> { this.expireAt = null; this.feedPurgedAt = null; } | |
| case REPORT -> this.expireAt = null; | |
| } | |
| } | |
| public void updateStatus(BlockStatus newStatus, LocalDateTime now, int unfollowedDays, int blockedDays) { | |
| if (this.status == newStatus) return; | |
| this.status = newStatus; | |
| this.statusChangedAt = now; | |
| switch (newStatus) { | |
| case UNFOLLOWED -> this.expireAt = now.plusDays(unfollowedDays); | |
| case BLOCKED -> this.expireAt = now.plusDays(blockedDays); | |
| case FOLLOW -> { this.expireAt = null; this.feedPurgedAt = null; } | |
| case REPORT -> this.expireAt = null; | |
| default -> throw new IllegalArgumentException("Unsupported status: " + newStatus); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/spoony/spoony_server/domain/user/Block.java around lines 52
to 63, the domain class currently depends on BlockStatus from the
adapter/persistence package which inverts layering: move/define BlockStatus into
the domain package (e.g., com.spoony.spoony_server.domain.user) and update
persistence adapters to map their own enums to the domain enum; then update
imports in this file to use the domain BlockStatus. Also add a default branch to
the switch in updateStatus to handle unknown/future statuses (e.g., throw
IllegalStateException or log and no-op) so new enum values don’t silently bypass
expected handling.
| @Enumerated(EnumType.STRING) | ||
| @Column(length = 50) | ||
| private BlockStatus status; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
status도 nullable=true가 들어갔음 싶네요
| } | ||
|
|
||
| //스케쥴러 작동 여부에 대해 라이트로그 찍는 매서드 | ||
| // 스케줄러가 실제 Feed 삭제했을 때 라이트로그 기록 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
라이트로그와 백필전략에 관해서 다음번에 구두로도 설명 부탁드립니다!!!
| """) | ||
| int deleteBidirectional(@Param("u") Long u, @Param("t") Long t); | ||
|
|
||
| boolean existsByUserIdAndPostId(Long userId, Long postId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분 실행이 잘 되나요??
|
file changed 30개 ㄷㄷㄷ.... 고생하셨습니다 |
|
UserController API는 내부 로직만 바뀌는거라 크게 상관이 없을듯 하네요 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
회의날 발표해주신 자료를 통해 이해 완료 하였습니다!
변경량이 많아 파악하는데 시간이 오래걸린 것 같아요! 다음 작업부터는 양이 많아지면 PR을 분리하는 방안도 고려해보면 좋을 것 같습니다!
고생하셨습니다!!
| } | ||
|
|
||
| @Override | ||
| public FollowListResponseDTO getFollowers(FollowGetCommand command) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수민님 작업 공간에서 삭제되어 있었던 것 같아요! 다음엔 브랜치 파기 전에 pull 한번 하고 작업 진행하는게 좋을 것 같습니다!!
cf032d9 to
6b88dd6
Compare


🔍️ 이 PR을 통해 해결하려는 문제가 무엇인가요?
closed [FEAT] Feed 테이블 정합성 관리 및 스케쥴러 처리 #214
팔로우/언팔/차단/신고 흐름에서 Feed 테이블 관리 책임과 백필 정책이 혼재되어 있던 문제를 정리했습니다.
특히, 차단/언팔 시 Feed 삭제 시점과, 재팔로우 시 전체 백필과, 증분 백필의 분기 로직을 명확히 하는 것이 목적입니다.
상세 배경은 Notion 문서 링크 에 정리했습니다.
✨ 이 PR에서 핵심적으로 변경된 사항은 무엇일까요?
Follow/Unfollow/Block/Report 전체 관계를 관리하는 공통 도메인 으로 확장
향후에는 별도의 엔티티로 분리하는 것도 검토할 예정
new_follow기준으로 전체 백필 처리feed_purged_at라이트 로그 기록으로, 재팔로우 시 전체/증분 백필 판단 가능🔖 핵심 변경 사항 외에 추가적으로 변경된 부분이 있나요?
ReportService에서 처리하던 feed/zzim/follow 정리 책임을BlockService.report()로 위임deleteByUserIdAndAuthorId등 중복 책임 제거Scheduler & BlockEntity 설계 배경은 Notion 에 따로 정🙏 Reviewer 분들이 이런 부분을 신경써서 봐 주시면 좋겠어요
🩺 이 PR에서 테스트 혹은 검증이 필요한 부분이 있을까요?
📌 PR 진행 시 이러한 점들을 참고해 주세요
📝 Assignee를 위한 CheckList
Summary by CodeRabbit
신규 기능
개선 사항