-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: [FN-145][FN-150][FN-151] 좋아요 기능 #39
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
Conversation
Walkthrough카드셋 좋아요 기능이 신규 추가됨: Like/Unlike 도메인·이벤트·엔티티·리포지토리·서비스·컨트롤러·DTO·페처 및 카드셋 메타데이터 엔티티/리포지토리와 CardSetService 연동, 비동기·재시도 기반 이벤트 핸들러와 테스트용 S3 모킹/프로파일 분기 추가. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant API as LikeController
participant SVC as LikeService
participant POL as LikePolicyService
participant REP as LikeRepository
participant PUB as ApplicationEventPublisher
participant LST_L as CardSetLikeEventHandler
participant LST_U as CardSetUnlikeEventHandler
participant CSV as CardSetService
participant CMR as CardSetMetadataRepository
participant FCH as CardSetFetcher
rect rgb(235,245,255)
note over User,API: 좋아요 추가
User->>API: POST /v1/likes/{type}/{targetId}
API->>SVC: addLike(userId,type,targetId)
SVC->>POL: validateTargetExists(...)
POL->>CSV: existsById(targetId)
POL-->>SVC: exists
SVC->>POL: validateNotAlreadyLiked(...)
POL->>REP: existsByTypeAndTargetIdAndUserId(...)
POL-->>SVC: not exists
SVC->>REP: save(Like)
SVC->>PUB: publish(LikeEvent)
end
rect rgb(240,255,240)
note over PUB,LST_L: 비동기/재시도 (after commit)
PUB-->>LST_L: LikeEvent(CARD_SET)
LST_L->>CSV: incrementLikeCount(cardSetId)
CSV->>CMR: incrementLikeCount(cardSetId)
CMR-->>CSV: updated
end
rect rgb(255,245,240)
note over User,API: 좋아요 취소
User->>API: DELETE /v1/likes/{type}/{targetId}
API->>SVC: removeLike(userId,type,targetId)
SVC->>REP: findByTypeAndTargetIdAndUserId(...)
SVC->>REP: delete(Like)
SVC->>PUB: publish(UnlikeEvent)
end
rect rgb(255,250,230)
note over PUB,LST_U: 비동기/재시도 (after commit)
PUB-->>LST_U: UnlikeEvent(CARD_SET)
LST_U->>CSV: decrementLikeCount(cardSetId)
CSV->>CMR: decrementLikeCount(cardSetId)
CMR-->>CSV: updated
end
rect rgb(245,245,255)
note over User,API: 좋아요 목록 조회
User->>API: GET /v1/likes/{type}
API->>SVC: getLikes(userId,type,req)
SVC->>REP: findByTypeAndUserId(...)
REP-->>SVC: Page<Like>
SVC->>FCH: fetchByIds(targetIds)
FCH->>CSV: getCardSetsByIds(ids)
CSV-->>FCH: summaries
FCH-->>SVC: targets
SVC-->>API: PagingResponse<LikeResponse<T>>
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
64-66: 메서드 네이밍 및 불필요한 괄호 제거
CardSetService.java의 existGroupMember 메서드에서 잘못된 메서드명을 호출해 컴파일/런타임 오류가 발생합니다. 불필요한 괄호도 제거해 아래와 같이 수정하세요:- return groupMemberRepository.existsByGroup_idAndUser_id((group.getId()), user.getId()); + return groupMemberRepository.existsByGroup_IdAndUser_Id(group.getId(), user.getId());
🧹 Nitpick comments (20)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
3-5: 인터페이스 전환 시 CardSetLikeResponse 수정 필요
- src/main/java/project/flipnote/like/model/CardSetLikeResponse.java:
extends LikeTargetResponse→implements LikeTargetResponse, 필드id타입을Long→long으로 변경src/main/java/project/flipnote/like/model/LikeSearchRequest.java (1)
14-17: 고정 정렬은 클라이언트 정렬 의도를 무시합니다 — order는 반영하고 필드는 id로 제한하세요현재 항상 id DESC로만 정렬합니다. order 파라미터만이라도 반영하고, 필드는 보안을 위해 id로 고정하는 타협안을 제안합니다. (CardSetSearchRequest와의 일관성 고려)
@Override - public PageRequest getPageRequest() { - return PageRequest.of(getPage() - 1, getSize() + 1, Sort.by(Sort.Direction.DESC, "id")); - } + public final PageRequest getPageRequest() { + Sort.Direction direction; + try { + direction = Sort.Direction.fromString(this.getOrder()); + } catch (IllegalArgumentException e) { + direction = Sort.Direction.DESC; + } + return PageRequest.of(getPage() - 1, getSize() + 1, Sort.by(direction, "id")); + }src/main/java/project/flipnote/common/entity/LikeType.java (1)
3-5: 패키지 위치 재고 권장
LikeType은 JPA 엔티티가 아니므로common.entity보다는common.model/common.type등으로 이동하는 편이 의미상 명확합니다. (선택 사항)src/main/java/project/flipnote/common/model/event/LikeEvent.java (1)
5-9: 관측 가능성 강화(선택) — occurredAt/correlationId 고려재시도·장애 분석을 위해 이벤트 발생 시각과 상관관계 ID를 포함하면 추적성이 좋아집니다. 도입 시 퍼블리셔에서 채우고, 리스너 로그/메트릭에 포함하는 것을 권장합니다.
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (1)
5-14: Enum 네이밍/직렬화 일관성 — 대문자 상수 + JSON 매핑 유지로 리팩터링 제안자바 컨벤션과 직렬화 계약을 동시에 만족하도록 상수는 대문자(
CARD_SET)로, JSON 값은"card_set"으로 유지하는 패턴을 권장합니다.package project.flipnote.like.model; import project.flipnote.common.entity.LikeType; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; public enum LikeTypeRequest { - card_set; + CARD_SET("card_set"); + + private final String value; + LikeTypeRequest(String value) { this.value = value; } + + @JsonValue + public String value() { return value; } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static LikeTypeRequest from(String v) { + if (v == null) return null; + return switch (v.toLowerCase()) { + case "card_set" -> CARD_SET; + default -> throw new IllegalArgumentException("Invalid LikeTypeRequest: " + v); + }; + } public LikeType toDomain() { - switch (this) { - case card_set: return LikeType.CARD_SET; - default: throw new IllegalArgumentException("Invalid LikeTypeRequest"); - } + return switch (this) { + case CARD_SET -> LikeType.CARD_SET; + }; } }API가 소문자
card_set을 이미 공개 계약으로 사용 중인지도 함께 확인해 주세요.src/main/java/project/flipnote/like/model/LikeResponse.java (1)
10-17: 불변 DTO로 단순화 — record로 전환 제안다른 응답들이 record를 사용하는 추세와 맞춰 불변성을 보장하면 안전합니다.
-@AllArgsConstructor -@Data -public class LikeResponse<T extends LikeTargetResponse> { - private T target; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime likedAt; -} +public record LikeResponse<T extends LikeTargetResponse>( + T target, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime likedAt +) {}src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (2)
8-13: DTO 가변성 제거 및 불필요한 callSuper 제거상위 추상 클래스가 상태를 갖지 않는다면
callSuper=true는 불필요합니다. 또한 DTO는 불변이 안전하므로 필드를final로 하고 세터 생성을 막는 편이 좋습니다.-@EqualsAndHashCode(callSuper = true) -@AllArgsConstructor -@Data +@EqualsAndHashCode(callSuper = false) +@AllArgsConstructor +@lombok.Getter public class CardSetLikeResponse extends LikeTargetResponse { - private Long id; - private String name; + private final Long id; + private final String name; }
15-17: 매핑 확장 여지향후 목록/피드 화면에 이미지/그룹 정보가 필요할 수 있습니다.
CardSetSummaryResponse에 이미 존재하는imageUrl,groupId등을 포함한 확장 DTO가 필요해지면 대응하기 쉽도록 메서드 오버로드(from(res, includeMeta)) 여지를 남겨두는 것을 검토해 주세요.src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (1)
23-27: 타깃 일부 미존재 시 LikeResponse에 null 주입 가능성현재 LikeService는
targetMap.get(id)결과를 그대로 DTO에 담습니다. 카드셋이 이후 삭제되었거나 권한상 제외되어 이 fetcher가 일부 ID를 반환하지 않으면 null이 섞일 수 있습니다. 목록에서 누락을 필터링하거나 경고 로그를 남기는 방식을 고려해 주세요. (예: fetcher에서 요청 ID 대비 응답 ID를 비교해 미존재 항목 로깅)다음과 같이 경고 로깅을 추가할 수 있습니다:
+import lombok.extern.slf4j.Slf4j; ... -@RequiredArgsConstructor +@RequiredArgsConstructor +@Slf4j @Component public class CardSetFetcher implements LikeTargetFetcher<CardSetLikeResponse> { ... public List<CardSetLikeResponse> fetchByIds(List<Long> ids) { - return cardSetService.getCardSetsByIds(ids).stream() - .map(CardSetLikeResponse::from) - .toList(); + var responses = cardSetService.getCardSetsByIds(ids).stream() + .map(CardSetLikeResponse::from) + .toList(); + if (responses.size() != ids.size()) { + log.debug("일부 카드셋이 조회되지 않음. requested={}, resolved={}", ids.size(), responses.size()); + } + return responses; }src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java (1)
12-19: 재시도(@retryable)와 조합 시 멱등성 확보 필요Like/Unlike 핸들러가 재시도되면 동일 이벤트가 중복 반영될 수 있습니다. 단순 가/감산은 멱등하지 않아 실제 좋아요 수와 어긋날 수 있습니다. 복구(Recover) 시점 혹은 백오프 재시도 실패 후에 실제 Like 테이블 카운트로 재동기화하는 API를 두는 것을 권장합니다.
추가 변경(다른 파일)에 대한 참고 코드:
// CardSetService 내 (기존 파일) @Transactional public void incrementLikeCount(Long cardSetId) { int updated = cardSetMetadataRepository.incrementLikeCount(cardSetId); if (updated == 0) { log.warn("CardSetMetadata 미존재: increment skip cardSetId={}", cardSetId); } } @Transactional public void decrementLikeCount(Long cardSetId) { int updated = cardSetMetadataRepository.decrementLikeCount(cardSetId); if (updated == 0) { log.warn("CardSetMetadata 미존재 또는 이미 0: decrement skip cardSetId={}", cardSetId); } } /** 멱등 보정용: 실제 Like 테이블 기준으로 likeCount 동기화 */ @Transactional public void syncLikeCountFromLikes(Long cardSetId) { long cnt = likeRepository.countByTypeAndTargetId(LikeType.CARD_SET, cardSetId); cardSetMetadataRepository.updateLikeCount(cardSetId, cnt); }// CardSetMetadataRepository 보강 @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE CardSetMetadata m SET m.likeCount = :likeCount WHERE m.id = :cardSetId") int updateLikeCount(@Param("cardSetId") Long cardSetId, @Param("likeCount") long likeCount);// LikeRepository 보강 long countByTypeAndTargetId(LikeType likeType, Long targetId);src/main/java/project/flipnote/like/service/LikePolicyService.java (1)
30-34: 중복 좋아요 경쟁 조건 대비
existsBy...→save순서는 경쟁 상태에서 중복 레코드가 생성될 수 있습니다. (두 요청이 동시에 존재 확인을 통과하는 경우) 테이블에(type, target_id, user_id)유니크 제약을 두고,DataIntegrityViolationException을ALREADY_LIKED로 매핑하는 방식을 권장합니다.추가 적용 예시(다른 파일):
// Like 엔티티 @Table(name = "likes", uniqueConstraints = { @UniqueConstraint(name = "uk_like_type_target_user", columnNames = {"type", "target_id", "user_id"}) })-- 마이그레이션 예시 ALTER TABLE likes ADD CONSTRAINT uk_like_type_target_user UNIQUE (type, target_id, user_id);// LikeService.addLike 내부 try-catch 예시 try { likeRepository.save(like); } catch (DataIntegrityViolationException e) { throw new BizException(LikeErrorCode.ALREADY_LIKED); }src/main/java/project/flipnote/like/repository/LikeRepository.java (1)
12-18: 조회/보정 시 필요 메서드 및 인덱스 제안
- 멱등 보정을 위해
countByTypeAndTargetId가 있으면 좋습니다.- 성능을 위해
(type, user_id)와(type, target_id, user_id)인덱스를 권장합니다. 후자는 유니크로 설정하십시오.public interface LikeRepository extends JpaRepository<Like, Long> { boolean existsByTypeAndTargetIdAndUserId(LikeType likeType, Long targetId, Long userId); Optional<Like> findByTypeAndTargetIdAndUserId(LikeType likeType, Long targetId, Long userId); Page<Like> findByTypeAndUserId(LikeType likeType, Long userId, Pageable pageable); + long countByTypeAndTargetId(LikeType likeType, Long targetId); }추가 DDL(마이그레이션):
CREATE INDEX idx_like_type_user ON likes(type, user_id); -- 유니크는 앞선 코멘트 참조src/main/java/project/flipnote/like/controller/docs/LikeControllerDocs.java (1)
24-26: 중첩 제네릭 응답의 스키마 전개 이슈 가능성
PagingResponse<LikeResponse<LikeTargetResponse>>는 문서화 시 스키마 전개가 불완전할 수 있습니다. 구체 DTO 래퍼(예:LikeListResponse) 도입 또는@Schema(implementation=...)로 스키마를 명시하는 방안을 검토해 주세요.src/main/java/project/flipnote/like/service/LikeService.java (2)
43-47: EnumMap + 불변 맵으로 초기화 시 명시성과 오버헤드 개선 가능
현재toMap은 해시맵이며 수정 가능입니다.EnumMap+unmodifiableMap으로 교체하면 타입 키에 최적화되고 런타임 수정을 방지할 수 있습니다.- public void init() { - this.fetcherMap = this.fetchers.stream() - .collect(Collectors.toMap(LikeTargetFetcher::getLikeType, Function.identity())); - } + public void init() { + java.util.EnumMap<LikeType, LikeTargetFetcher<?>> map = new java.util.EnumMap<>(LikeType.class); + this.fetchers.forEach(f -> map.put(f.getLikeType(), f)); + this.fetcherMap = java.util.Collections.unmodifiableMap(map); + }
112-116: 안전하지 않은 제네릭 캐스팅 — 경고 억제 또는 타입 시그니처 개선
캐스팅은 불가피해 보입니다. 최소한 캐스트 부근에 억제 어노테이션을 추가해 경고를 제거하세요.- LikeTargetFetcher<T> fetcher = (LikeTargetFetcher<T>)fetcherMap.get(likeType); + @SuppressWarnings("unchecked") + LikeTargetFetcher<T> fetcher = (LikeTargetFetcher<T>) fetcherMap.get(likeType);src/main/java/project/flipnote/cardset/service/CardSetService.java (2)
215-219: 입력 ID 순서 보존 필요
findAllById는 반환 순서를 보장하지 않습니다. 호출 측(예: Like 리스트)이 입력 순서/페이지 정렬을 기대한다면 순서를 복원하세요.다음과 같이 입력 순서를 기준으로 재정렬하세요:
- return cardSetRepository.findAllById(targetIds).stream() - .map(CardSetSummaryResponse::from) - .toList(); + var byId = cardSetRepository.findAllById(targetIds).stream() + .collect(java.util.stream.Collectors.toMap(CardSet::getId, java.util.function.Function.identity())); + return targetIds.stream() + .map(byId::get) + .filter(java.util.Objects::nonNull) + .map(CardSetSummaryResponse::from) + .toList();추가 import가 필요하면 다음을 추가하세요:
import java.util.function.Function; import java.util.stream.Collectors; import java.util.Objects;
68-112: 생성 트랜잭션 내 일관성 및 에러 처리트랜잭션 경계는 적절합니다. 다만 메타데이터/매니저 저장 중 제약조건 위반 등 예외 발생 시 전체 롤백이 되는지(체크 예외 변환 등) 확인해 주세요. 해시태그 조인 시 공백 트리밍도 고려하면 데이터 일관성이 좋아집니다.
src/main/java/project/flipnote/like/controller/LikeController.java (3)
31-40: POST 응답 코드 정합성(201 또는 204 고려)리소스 생성/토글 성격의 액션이라면
200 OK대신201 Created(생성) 또는204 No Content를 고려하세요. API 일관성을 위해 팀 컨벤션에 맞춰 조정 바랍니다.예시(201 사용 시):
- return ResponseEntity.ok().build(); + return ResponseEntity.status(org.springframework.http.HttpStatus.CREATED).build();
31-36: PathVariable 검증 추가 제안
targetId에 대한 음수/0 입력 방지용 제약을 추가하면 좋습니다.예시:
- @PathVariable("targetId") Long targetId, + @PathVariable("targetId") @jakarta.validation.constraints.Positive Long targetId,
42-51: DELETE 응답 코드 204 권장삭제(또는 좋아요 해제) 시 본문이 없으므로
204 No Content가 더 적절합니다.- return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (22)
src/main/java/project/flipnote/cardset/entity/CardSetMetadata.java(1 hunks)src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java(1 hunks)src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java(1 hunks)src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java(1 hunks)src/main/java/project/flipnote/cardset/service/CardSetService.java(7 hunks)src/main/java/project/flipnote/common/entity/LikeType.java(1 hunks)src/main/java/project/flipnote/common/model/event/LikeEvent.java(1 hunks)src/main/java/project/flipnote/common/model/event/UnlikeEvent.java(1 hunks)src/main/java/project/flipnote/like/controller/LikeController.java(1 hunks)src/main/java/project/flipnote/like/controller/docs/LikeControllerDocs.java(1 hunks)src/main/java/project/flipnote/like/entity/Like.java(1 hunks)src/main/java/project/flipnote/like/exception/LikeErrorCode.java(1 hunks)src/main/java/project/flipnote/like/model/CardSetLikeResponse.java(1 hunks)src/main/java/project/flipnote/like/model/LikeResponse.java(1 hunks)src/main/java/project/flipnote/like/model/LikeSearchRequest.java(1 hunks)src/main/java/project/flipnote/like/model/LikeTargetResponse.java(1 hunks)src/main/java/project/flipnote/like/model/LikeTypeRequest.java(1 hunks)src/main/java/project/flipnote/like/repository/LikeRepository.java(1 hunks)src/main/java/project/flipnote/like/service/LikePolicyService.java(1 hunks)src/main/java/project/flipnote/like/service/LikeService.java(1 hunks)src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java(1 hunks)src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
src/main/java/project/flipnote/like/controller/docs/LikeControllerDocs.java (3)
src/main/java/project/flipnote/auth/controller/docs/AuthControllerDocs.java (3)
AuthControllerDocs(19-53)Operation(36-37)Operation(51-52)src/main/java/project/flipnote/user/controller/docs/UserControllerDocs.java (2)
Tag(14-28)Operation(20-21)src/main/java/project/flipnote/notification/controller/docs/NotificationControllerDocs.java (1)
Tag(13-30)
src/main/java/project/flipnote/like/repository/LikeRepository.java (2)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
Repository(12-19)src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
Repository(14-29)
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
LikeTargetResponse(3-5)
src/main/java/project/flipnote/like/controller/LikeController.java (3)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (1)
RequiredArgsConstructor(12-29)src/main/java/project/flipnote/like/service/LikePolicyService.java (1)
RequiredArgsConstructor(12-35)src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
LikeTargetResponse(3-5)
src/main/java/project/flipnote/like/model/LikeSearchRequest.java (3)
src/main/java/project/flipnote/common/model/request/PagingRequest.java (2)
Getter(12-42)Schema(27-41)src/main/java/project/flipnote/group/model/GroupInvitationListRequest.java (2)
Override(10-13)GroupInvitationListRequest(8-14)src/main/java/project/flipnote/cardset/model/CardSetSearchRequest.java (2)
Override(21-34)Getter(12-35)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
src/main/java/project/flipnote/user/model/UserIdNickname.java (2)
getId(4-4)UserIdNickname(3-7)
src/main/java/project/flipnote/cardset/service/CardSetService.java (6)
src/main/java/project/flipnote/cardset/entity/CardSet.java (1)
Getter(22-69)src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
Repository(15-32)src/main/java/project/flipnote/cardset/repository/CardSetManagerRepository.java (1)
Repository(8-12)src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java (1)
RequiredArgsConstructor(13-60)src/main/java/project/flipnote/cardset/entity/CardSetManager.java (1)
Getter(18-45)src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (1)
CardSetSummaryResponse(5-24)
src/main/java/project/flipnote/common/model/event/LikeEvent.java (4)
src/main/java/project/flipnote/common/model/event/GroupInvitationCreatedEvent.java (1)
GroupInvitationCreatedEvent(3-7)src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java (1)
UserRegisteredEvent(3-7)src/main/java/project/flipnote/common/model/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java (1)
GroupJoinRequestedEvent(5-10)
src/main/java/project/flipnote/cardset/entity/CardSetMetadata.java (3)
src/main/java/project/flipnote/like/entity/Like.java (1)
Getter(19-50)src/main/java/project/flipnote/cardset/entity/CardSet.java (2)
Getter(22-69)Builder(51-60)src/main/java/project/flipnote/cardset/repository/CardSetRepository.java (1)
Repository(15-32)
src/main/java/project/flipnote/like/model/LikeResponse.java (2)
src/main/java/project/flipnote/notification/model/NotificationResponse.java (1)
NotificationResponse(10-35)src/main/java/project/flipnote/user/model/SocialLinkResponse.java (1)
SocialLinkResponse(9-26)
src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java (3)
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
Slf4j(38-221)src/main/java/project/flipnote/like/service/LikeService.java (1)
Slf4j(30-127)src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java (1)
Slf4j(17-45)
src/main/java/project/flipnote/like/exception/LikeErrorCode.java (2)
src/main/java/project/flipnote/like/service/LikePolicyService.java (1)
RequiredArgsConstructor(12-35)src/main/java/project/flipnote/common/exception/ErrorCode.java (1)
ErrorCode(3-10)
src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java (3)
src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
Slf4j(38-221)src/main/java/project/flipnote/like/service/LikeService.java (1)
Slf4j(30-127)src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java (1)
Slf4j(17-45)
src/main/java/project/flipnote/like/service/LikeService.java (7)
src/main/java/project/flipnote/like/controller/LikeController.java (1)
RequiredArgsConstructor(24-64)src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (1)
RequiredArgsConstructor(12-29)src/main/java/project/flipnote/like/service/LikePolicyService.java (1)
RequiredArgsConstructor(12-35)src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
Slf4j(38-221)src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java (1)
Slf4j(17-45)src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java (1)
Slf4j(17-45)src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
LikeTargetResponse(3-5)
src/main/java/project/flipnote/like/entity/Like.java (1)
src/main/java/project/flipnote/common/entity/BaseEntity.java (1)
BaseEntity(14-26)
src/main/java/project/flipnote/common/model/event/UnlikeEvent.java (4)
src/main/java/project/flipnote/common/model/event/UserRegisteredEvent.java (1)
UserRegisteredEvent(3-7)src/main/java/project/flipnote/common/model/event/UserWithdrawnEvent.java (1)
UserWithdrawnEvent(3-6)src/main/java/project/flipnote/common/model/event/GroupJoinRequestedEvent.java (1)
GroupJoinRequestedEvent(5-10)src/main/java/project/flipnote/auth/event/EmailVerificationSendEvent.java (1)
EmailVerificationSendEvent(3-7)
src/main/java/project/flipnote/like/model/CardSetLikeResponse.java (2)
src/main/java/project/flipnote/like/model/LikeResponse.java (1)
AllArgsConstructor(10-17)src/main/java/project/flipnote/cardset/model/CardSetSummaryResponse.java (2)
CardSetSummaryResponse(5-24)from(14-23)
🔇 Additional comments (8)
src/main/java/project/flipnote/common/model/event/UnlikeEvent.java (1)
5-10: 간결하고 명확 — LGTM레코드 모델 적절합니다. 이벤트 핸들러에서 중복 처리/재시도 시 멱등성만 확보하면 됩니다.
src/main/java/project/flipnote/like/model/LikeTypeRequest.java (1)
5-6: 현재 형태 유지 시 주의사항소문자 enum 이름은 컨벤션 위반이므로, 정합성 검증(정적 분석 규칙/가이드)에서 예외로 허용되는지 확인 바랍니다.
src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java (1)
38-44: 로그 메세지 적절 — 스택트레이스 포함 OK
현재 placeholder/인자 매핑과 예외 전달이 적절합니다.src/main/java/project/flipnote/like/exception/LikeErrorCode.java (1)
11-26: 에러 코드 설계 적절 — HTTP 매핑, 코드, 메시지 일관성 양호표준화된 응답 구성이며 도메인 예외에 적절히 활용 가능합니다.
src/main/java/project/flipnote/like/entity/Like.java (1)
44-49: 엔티티 빌더 구성 적절
필수 필드 설정만 허용하는 빌더 패턴 적용이 적절합니다.src/main/java/project/flipnote/cardset/service/CardSetService.java (2)
174-184: 타깃 존재 여부 API 추가 — 적절합니다
existsById는 Like 타깃 검증에 충분하며 트랜잭션 읽기전용 컨텍스트와도 부합합니다.
98-102: CardSetMetadata 엔티티의 likeCount는 @builder 생성자에서 이미 0으로 초기화되며 @column(nullable = false) 설정도 있어 NULL 위험이 없습니다.Likely an incorrect or invalid review comment.
src/main/java/project/flipnote/like/controller/LikeController.java (1)
53-63: 페이징 조회 엔드포인트 — 구성 적절모델 속성 바인딩과 제네릭 응답 타입 구성이 깔끔합니다.
src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java
Show resolved
Hide resolved
src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java
Show resolved
Hide resolved
src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java
Show resolved
Hide resolved
src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java
Show resolved
Hide resolved
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java
Outdated
Show resolved
Hide resolved
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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/project/flipnote/common/config/S3Config.java (1)
14-16: @Profile를 클래스 수준으로 이동하여 테스트에서 @value Placeholder 문제 예방현재 메서드 수준 @Profile("!test")는 @bean 실행만 막고, 클래스의 @value 필드 주입은 여전히 시도될 수 있어(test에 해당 프로퍼티가 없으면) 컨텍스트 로딩이 실패할 수 있습니다. 클래스 전체에 @Profile("!test")를 부여하고 메서드의 @Profile는 제거하세요.
@@ -@Configuration -public class S3Config { +@Configuration +@Profile("!test") +public class S3Config { @@ - @Profile("!test") @Bean public S3Client s3Client() { @@ - @Profile("!test") @Bean public S3Presigner s3Presigner() {Also applies to: 28-31, 41-44
🧹 Nitpick comments (2)
src/test/java/project/flipnote/common/config/S3TestConfig.java (1)
3-6: 테스트 빈에 @primary 부여 권장(모호성 예방)다른 테스트 구성에서 동일 타입 빈이 추가될 경우 주입 모호성을 피하기 위해 @primary를 부여하세요.
@@ -import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; @@ @Bean + @Primary public S3Client s3Client() { return Mockito.mock(S3Client.class); } @@ @Bean + @Primary public S3Presigner s3Presigner() { return Mockito.mock(S3Presigner.class); }Also applies to: 13-16, 18-21
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/main/java/project/flipnote/common/config/S3Config.java(3 hunks)src/test/java/project/flipnote/FlipnoteApplicationTests.java(1 hunks)src/test/java/project/flipnote/cardset/service/CardSetServiceTest.java(4 hunks)src/test/java/project/flipnote/common/config/S3TestConfig.java(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/test/java/project/flipnote/cardset/service/CardSetServiceTest.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/project/flipnote/common/config/S3Config.java (2)
src/test/java/project/flipnote/image/service/ImageUploadServiceTest.java (1)
ExtendWith(28-77)src/main/java/project/flipnote/infra/firebase/FirebaseService.java (1)
PostConstruct(33-47)
src/test/java/project/flipnote/common/config/S3TestConfig.java (1)
src/test/java/project/flipnote/image/service/ImageUploadServiceTest.java (1)
ExtendWith(28-77)
🔇 Additional comments (2)
src/test/java/project/flipnote/FlipnoteApplicationTests.java (1)
12-14: 프로필 분리 + 테스트 구성 임포트 적절합니다test 프로필로 실 S3 빈을 배제하고, S3TestConfig 모킹 빈 주입으로 컨텍스트 로딩을 안정화한 점 좋습니다.
src/main/java/project/flipnote/common/config/S3Config.java (1)
16-23: 테스트 프로필에서 S3Config 빈 로딩 시 cloud.aws. 플래스홀더 확인*
Test 프로필(application-test.yml/properties)에 아래 키들이 정의되어 있는지 확인하거나, 테스트 시 해당 빈이 로드되지 않도록 S3Config 클래스에@Profile("!test")등을 적용해주세요.
- cloud.aws.credentials.access-key
- cloud.aws.credentials.secret-key
- cloud.aws.region
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: 0
♻️ Duplicate comments (3)
src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java (1)
25-38: 재시도 시 중복 디크리먼트 가능성 — 멱등/보정 경로 필요DB 갱신은 성공했으나 예외가 던져져 재시도되는 경우, 실제 좋아요 수가 두 번 감소할 수 있습니다. 업데이트 영향 행 수 체크 또는 보정 API 호출로 멱등성을 확보해 주세요.
예시(보정 경로; 서비스에 보정 메서드가 필요합니다):
@Recover public void recover(Exception ex, UnlikeEvent event) { log.error( "좋아요 취소 수 반영 처리 중 예외 발생 : likeType={}, targetId={}, userId={}", event.likeType(), event.targetId(), event.userId(), ex ); + if (event.likeType() == LikeType.CARD_SET) { + // 권장: 실제 Like 테이블 기준으로 재동기화 + cardSetService.reconcileLikeCountFromLikes(event.targetId()); + } }대안:
CardSetService.decrementLikeCount가 리포지토리의 반환값(영향 행 수)을 확인하여 0일 경우 경고/보정 호출.src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java (1)
12-15: 영향 행 수를 상위 계층에서 활용해 누락 검출/보정현재 서비스가 반환값을 무시하고 있어, 메타데이터 행 부재 등 이상 상황을 감지하기 어렵습니다. 서비스에서 반환값 체크 후 로깅/보정을 해 주세요.
서비스 측 예시(다른 파일 변경):
--- a/src/main/java/project/flipnote/cardset/service/CardSetService.java +++ b/src/main/java/project/flipnote/cardset/service/CardSetService.java @@ - @Transactional - public void incrementLikeCount(Long cardSetId) { - cardSetMetadataRepository.incrementLikeCount(cardSetId); - } + @Transactional + public void incrementLikeCount(Long cardSetId) { + int updated = cardSetMetadataRepository.incrementLikeCount(cardSetId); + if (updated == 0) { + log.warn("CardSetMetadata 미갱신 감지: incrementLikeCount cardSetId={}", cardSetId); + // 선택: 보정 시도 + // reconcileLikeCountFromLikes(cardSetId); + } + } @@ - @Transactional - public void decrementLikeCount(Long cardSetId) { - cardSetMetadataRepository.decrementLikeCount(cardSetId); - } + @Transactional + public void decrementLikeCount(Long cardSetId) { + int updated = cardSetMetadataRepository.decrementLikeCount(cardSetId); + if (updated == 0) { + log.warn("CardSetMetadata 미갱신 감지: decrementLikeCount cardSetId={}", cardSetId); + // 선택: 보정 시도 + // reconcileLikeCountFromLikes(cardSetId); + } + }src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1)
12-12: 입력 파라미터 범용화(Collection) 및 계약(Javadoc) 명시호출 측 유연성을 위해
List→Collection으로 완화하고, 누락/중복/널 처리 계약을 인터페이스에 명시해 주세요.-public interface LikeTargetFetcher<T extends LikeTargetResponse> { +public interface LikeTargetFetcher<T extends LikeTargetResponse> { LikeType getLikeType(); - Map<Long, T> fetchByIds(List<Long> ids); + /** + * 주어진 ID 컬렉션에 해당하는 대상들을 조회하여 ID→대상 맵으로 반환합니다. + * 계약: + * - ids는 null이 아니어야 합니다. null이면 IllegalArgumentException을 던집니다. + * - 반환 맵에는 존재하는 ID만 포함되며, 누락된 ID는 키 부재로 표현합니다. + * - 입력에 중복 ID가 있어도 맵 특성상 하나의 엔트리만 유지됩니다(호출 측이 입력을 순회하며 map.get(id)로 조인). + * - ids가 비어 있으면 빈 맵을 반환합니다. + */ + Map<Long, T> fetchByIds(java.util.Collection<Long> ids); }
🧹 Nitpick comments (4)
src/main/java/project/flipnote/cardset/entity/CardSetMetadata.java (3)
21-22: likeCount 기본값 명시로 의도를 코드에 고정JPA 상
int는 0으로 초기화되지만, 의도를 명확히 하려면 필드에 기본값을 부여해 두는 편이 안전합니다.- @Column(nullable = false) - private int likeCount; + @Column(nullable = false) + private int likeCount = 0;
14-16: DB 레벨 하한 보장: 음수 방지 CHECK 제약 추가 제안리포지토리에서 CASE로 하한을 막고 있지만, 직접 쿼리나 미래 변경으로부터 보호하려면 스키마 제약을 함께 두는 게 좋습니다.
@Table(name = "card_set_metadata") @Entity +@org.hibernate.annotations.Check(constraints = "like_count >= 0") public class CardSetMetadata {
18-19: CardSet와의 강결합 필요 시 1:1 FK + @mapsid 고려현재는 동일 ID를 수동 주입하고 있어 참조 무결성 보장이 없습니다. 도메인이 강결합이라면
@OneToOne @MapsId로 FK를 두는 설계를 검토해 주세요. 구현 난이도가 있으므로 추후 단계로도 충분합니다.src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (1)
27-33: 중복 키 안전성 및 빈 입력 최적화
Collectors.toMap은 중복 키가 발생하면 예외가 납니다. 현실적으로 중복은 드물지만, 방어적으로 머지 함수를 지정해 두는 게 안전합니다. 또한 빈 입력 시 빠른 반환으로 불필요한 호출을 줄일 수 있습니다.- @Override - public Map<Long, CardSetLikeResponse> fetchByIds(List<Long> ids) { - return cardSetService.getCardSetsByIds(ids).stream() - .map(CardSetLikeResponse::from) - .collect(Collectors.toMap(LikeTargetResponse::getId, Function.identity())); - } + @Override + public Map<Long, CardSetLikeResponse> fetchByIds(java.util.Collection<Long> ids) { + if (ids == null || ids.isEmpty()) { + return java.util.Collections.emptyMap(); + } + return cardSetService.getCardSetsByIds(ids.stream().toList()).stream() + .map(CardSetLikeResponse::from) + .collect(Collectors.toMap( + LikeTargetResponse::getId, + Function.identity(), + (existing, ignored) -> existing, // 중복 키 시 기존 값 유지 + java.util.LinkedHashMap::new // 선택: 입력 순서 유지 + )); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
build.gradle(1 hunks)src/main/java/project/flipnote/cardset/entity/CardSetMetadata.java(1 hunks)src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java(1 hunks)src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java(1 hunks)src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java(1 hunks)src/main/java/project/flipnote/like/controller/LikeController.java(1 hunks)src/main/java/project/flipnote/like/entity/Like.java(1 hunks)src/main/java/project/flipnote/like/service/LikeService.java(1 hunks)src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java(1 hunks)src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java(1 hunks)src/test/java/project/flipnote/common/config/S3TestConfig.java(1 hunks)src/test/resources/application-test.yml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/test/java/project/flipnote/common/config/S3TestConfig.java
- src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java
- src/main/java/project/flipnote/like/controller/LikeController.java
- src/main/java/project/flipnote/like/service/LikeService.java
- src/main/java/project/flipnote/like/entity/Like.java
🧰 Additional context used
🧬 Code graph analysis (5)
build.gradle (1)
src/test/java/project/flipnote/groupjoin/service/GroupJoinServiceTest.java (1)
ExtendWith(35-283)
src/test/resources/application-test.yml (1)
src/main/java/project/flipnote/common/config/S3Config.java (2)
Configuration(13-51)Bean(27-37)
src/main/java/project/flipnote/cardset/listener/CardSetUnlikeEventHandler.java (3)
src/main/java/project/flipnote/cardset/listener/CardSetLikeEventHandler.java (1)
Slf4j(18-47)src/main/java/project/flipnote/like/service/LikeService.java (1)
Slf4j(31-130)src/main/java/project/flipnote/cardset/service/CardSetService.java (2)
Slf4j(38-221)Slf4j(36-166)
src/main/java/project/flipnote/like/service/fetcher/CardSetFetcher.java (2)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
LikeTargetResponse(3-5)src/main/java/project/flipnote/cardset/service/CardSetService.java (1)
Slf4j(36-166)
src/main/java/project/flipnote/like/service/fetcher/LikeTargetFetcher.java (1)
src/main/java/project/flipnote/like/model/LikeTargetResponse.java (1)
LikeTargetResponse(3-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
src/main/java/project/flipnote/cardset/repository/CardSetMetadataRepository.java (1)
16-22: 음수 방지 및 영향 행 수 반환 LGTMCASE 기반 디크리먼트와
int반환으로 하한과 업데이트 누락 감지가 가능해졌습니다. 좋습니다.build.gradle (1)
50-50: 환경 변수 JAVA_HOME이 올바르지 않아 의존성 분석이 실패했습니다.
올바른 JDK 경로로 JAVA_HOME을 설정한 뒤 아래 명령을 재실행해 mockito-core·mockito-inline 버전 일치 여부와 byte-buddy 호환성을 확인해 주세요.export JAVA_HOME=/path/to/your/jdk ./gradlew -q dependencyInsight --dependency mockito --configuration testRuntimeClasspath ./gradlew -q dependencyInsight --dependency byte-buddy --configuration testRuntimeClasspathsrc/test/resources/application-test.yml (2)
33-40: 참조 검색 결과 없음 – 키 경로 변경 전 수동 검증 필요
cloud.s3.bucket또는cloud.aws.s3.bucket참조가 코드·리소스에서 검색되지 않았으므로, 설정 키 경로를 변경하기 전 모든 참조가 올바르게 업데이트되었는지 수동으로 확인하세요.
33-40: 리뷰 코멘트 무시: S3Config에 이미@Profile("!test")가 적용되어 테스트 실행 시 실제S3Client/S3Presigner빈이 등록되지 않아S3TestConfig와 충돌이 없습니다.Likely an incorrect or invalid review comment.
📝 변경 내용
✅ 체크리스트
💬 기타 참고 사항
Summary by CodeRabbit
New Features
Improvements
Tests
Chores