Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
import project.flipnote.common.model.response.PagingResponse;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.like.controller.docs.LikeControllerDocs;
import project.flipnote.like.model.LikeResponse;
import project.flipnote.like.model.LikeSearchRequest;
import project.flipnote.like.model.LikeTargetResponse;
import project.flipnote.like.model.LikeTargetTypeRequest;
import project.flipnote.like.model.response.LikeResponse;
import project.flipnote.like.model.request.LikeSearchRequest;
import project.flipnote.like.model.response.LikeTargetResponse;
import project.flipnote.like.model.request.LikeTargetTypeRequest;
import project.flipnote.like.service.LikeService;

@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import project.flipnote.common.model.response.PagingResponse;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.like.model.LikeResponse;
import project.flipnote.like.model.LikeSearchRequest;
import project.flipnote.like.model.LikeTargetResponse;
import project.flipnote.like.model.LikeTargetTypeRequest;
import project.flipnote.like.model.response.LikeResponse;
import project.flipnote.like.model.request.LikeSearchRequest;
import project.flipnote.like.model.response.LikeTargetResponse;
import project.flipnote.like.model.request.LikeTargetTypeRequest;

@Tag(name = "Like", description = "Like API")
public interface LikeControllerDocs {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package project.flipnote.like.model;
package project.flipnote.like.model.request;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package project.flipnote.like.model;
package project.flipnote.like.model.request;

import project.flipnote.like.entity.LikeTargetType;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package project.flipnote.like.model;
package project.flipnote.like.model.response;

import lombok.AllArgsConstructor;
import lombok.Data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package project.flipnote.like.model;
package project.flipnote.like.model.response;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package project.flipnote.like.model;
package project.flipnote.like.model.response;

public abstract class LikeTargetResponse {
public abstract Long getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,21 @@
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import project.flipnote.cardset.service.CardSetService;
import project.flipnote.common.exception.BizException;
import project.flipnote.like.entity.LikeTargetType;
import project.flipnote.like.exception.LikeErrorCode;
import project.flipnote.like.model.response.LikeTargetResponse;
import project.flipnote.like.repository.LikeRepository;

@RequiredArgsConstructor
@Service
public class LikePolicyService {

private final CardSetService cardSetService;
private final LikeRepository likeRepository;
private final LikeTargetFetchService<LikeTargetResponse> likeTargetFetchService;

public void validateTargetExists(LikeTargetType targetType, Long targetId) {
boolean targetExists = false;
switch (targetType) {
case CARD_SET -> targetExists = cardSetService.existsById(targetId);
}

if (!targetExists) {
public void validateTargetExists(LikeTargetType targetType, Long targetId, Long userId) {
if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
throw new BizException(LikeErrorCode.LIKE_TARGET_NOT_FOUND);
}
Comment on lines +19 to 22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Like 도메인의 오류 코드를 사용하세요 (북마크 도메인으로 던짐).

가시성 검증 실패 시 BOOKMARK_TARGET_NOT_FOUND를 던지는 것은 도메인 불일치입니다. Like 흐름에서는 LikeErrorCode 쪽의 대상-부재 코드를 사용해야 API/문서 일관성이 유지됩니다. 또한 현재 메서드명은 “존재”를 암시하나, 실제로는 “가시성(뷰어블)”을 검증하므로 네이밍/주석 정비가 필요합니다.

다음과 같이 수정을 제안합니다(상수명은 기존 코드베이스에 맞춰 확인 필요합니다. 기존에 LIKE_TARGET_NOT_FOUND가 있었다면 그걸 사용하세요).

-    if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
-        throw new BizException(BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND);
-    }
+    if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
+        throw new BizException(LikeErrorCode.LIKE_TARGET_NOT_FOUND);
+    }

추가로, 실질 검증이 “존재”가 아니라 “가시성”이므로 아래 중 하나를 권장합니다.

  • 메서드명 변경: validateTargetViewable(...)
  • 또는 Javadoc으로 “가시성(접근 가능성) 검증”임을 명시

🏁 Script executed:

#!/bin/bash
set -e
echo "Searching for BookmarkErrorCode occurrences..."
rg -n --hidden -S 'BookmarkErrorCode' || true

echo -e "\nSearching for LikeErrorCode occurrences..."
rg -n --hidden -S 'LikeErrorCode' || true

echo -e "\nSearching for LIKE_TARGET_NOT_FOUND or BOOKMARK_TARGET_NOT_FOUND..."
rg -n --hidden -S 'LIKE_TARGET_NOT_FOUND|BOOKMARK_TARGET_NOT_FOUND' || true

echo -e "\nSearching for validateTargetExists / validateTargetViewable..."
rg -n --hidden -S 'validateTargetExists|validateTargetViewable' || true

echo -e "\nLocating LikePolicyService.java..."
rg -n --hidden -S 'class\s+LikePolicyService' || true

if [ -f src/main/java/project/flipnote/like/service/LikePolicyService.java ]; then
  echo -e "\n--- src/main/java/project/flipnote/like/service/LikePolicyService.java ---"
  sed -n '1,240p' src/main/java/project/flipnote/like/service/LikePolicyService.java || true
fi

Length of output: 5522


Like 도메인 오류 코드 사용 및 메서드명 정비

src/main/java/project/flipnote/like/service/LikePolicyService.java(라인 20-23): 가시성 검증 실패에 BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND를 던지고 있음 — LikeErrorCode.LIKE_TARGET_NOT_FOUND로 교체하세요.

-    if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
-        throw new BizException(BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND);
-    }
+    if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
+        throw new BizException(LikeErrorCode.LIKE_TARGET_NOT_FOUND);
+    }

메서드명은 '존재'가 아니라 '가시성(viewable)'을 검증하므로 validateTargetViewable(...)로 변경하거나 Javadoc으로 가시성 검증임을 명시하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void validateTargetExists(LikeTargetType targetType, Long targetId, Long userId) {
if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
throw new BizException(BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND);
}
public void validateTargetExists(LikeTargetType targetType, Long targetId, Long userId) {
if (!likeTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
throw new BizException(LikeErrorCode.LIKE_TARGET_NOT_FOUND);
}
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/like/service/LikePolicyService.java around
lines 20-23, the method validateTargetExists throws
BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND on visibility check failure; change
the thrown error to LikeErrorCode.LIKE_TARGET_NOT_FOUND and rename the method to
validateTargetViewable (or, if you prefer not to rename, add a Javadoc
clarifying it validates "viewable" not mere existence). Update the method
signature, its usages across the codebase (calls/tests), and any imports so they
compile, and run unit tests to ensure all references are adjusted.

}
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/project/flipnote/like/service/LikeReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package project.flipnote.like.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import project.flipnote.common.exception.BizException;
import project.flipnote.like.entity.Like;
import project.flipnote.like.entity.LikeTargetType;
import project.flipnote.like.exception.LikeErrorCode;
import project.flipnote.like.repository.LikeRepository;

@RequiredArgsConstructor
@Service
public class LikeReader {

private final LikeRepository likeRepository;

public Like findByTargetAndUserId(LikeTargetType targetType, Long targetId, Long userId) {
return likeRepository.findByTargetTypeAndTargetIdAndUserId(targetType, targetId, userId)
.orElseThrow(() -> new BizException(LikeErrorCode.LIKE_NOT_FOUND));
}
}
40 changes: 11 additions & 29 deletions src/main/java/project/flipnote/like/service/LikeService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package project.flipnote.like.service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.context.ApplicationEventPublisher;
Expand All @@ -13,7 +11,6 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import project.flipnote.common.exception.BizException;
Expand All @@ -23,11 +20,10 @@
import project.flipnote.like.entity.Like;
import project.flipnote.like.entity.LikeTargetType;
import project.flipnote.like.exception.LikeErrorCode;
import project.flipnote.like.model.LikeResponse;
import project.flipnote.like.model.LikeSearchRequest;
import project.flipnote.like.model.LikeTargetResponse;
import project.flipnote.like.model.response.LikeResponse;
import project.flipnote.like.model.request.LikeSearchRequest;
import project.flipnote.like.model.response.LikeTargetResponse;
import project.flipnote.like.repository.LikeRepository;
import project.flipnote.like.service.fetcher.LikeTargetFetcher;

@Slf4j
@RequiredArgsConstructor
Expand All @@ -38,15 +34,8 @@ public class LikeService {
private final LikeRepository likeRepository;
private final ApplicationEventPublisher eventPublisher;
private final LikePolicyService likePolicyService;
private final List<LikeTargetFetcher<?>> fetchers;

private Map<LikeTargetType, LikeTargetFetcher<?>> fetcherMap;

@PostConstruct
public void init() {
this.fetcherMap = this.fetchers.stream()
.collect(Collectors.toMap(LikeTargetFetcher::getTargetType, Function.identity()));
}
private final LikeTargetFetchService<LikeTargetResponse> likeTargetFetchService;
private final LikeReader likeReader;

Comment thread
dungbik marked this conversation as resolved.
/**
* 좋아요 추가
Expand All @@ -58,7 +47,7 @@ public void init() {
*/
@Transactional
public void addLike(Long userId, LikeTargetType targetType, Long targetId) {
likePolicyService.validateTargetExists(targetType, targetId);
likePolicyService.validateTargetExists(targetType, targetId, userId);
likePolicyService.validateNotAlreadyLiked(targetType, targetId, userId);

Like like = Like.builder()
Expand Down Expand Up @@ -86,8 +75,7 @@ public void addLike(Long userId, LikeTargetType targetType, Long targetId) {
*/
@Transactional
public void removeLike(Long userId, LikeTargetType targetType, Long targetId) {
Like like = likeRepository.findByTargetTypeAndTargetIdAndUserId(targetType, targetId, userId)
.orElseThrow(() -> new BizException(LikeErrorCode.LIKE_NOT_FOUND));
Like like = likeReader.findByTargetAndUserId(targetType, targetId, userId);

likeRepository.delete(like);

Expand All @@ -100,11 +88,10 @@ public void removeLike(Long userId, LikeTargetType targetType, Long targetId) {
* @param userId 좋아요 누른 목록을 조회하는 회원의 ID
* @param targetType 조회할 좋아요 대상 타입
* @param req 페이징 및 검색 조건이 포함된 요청 정보
* @param <T> 좋아요 대상의 상세 정보를 담은 DTO 타입 (LikeTargetResponse 하위 타입)
* @return 페이징된 좋아요 누른 목록
* @author 윤정환
*/
public <T extends LikeTargetResponse> PagingResponse<LikeResponse<T>> getLikes(
public PagingResponse<LikeResponse<LikeTargetResponse>> getLikes(
Long userId,
LikeTargetType targetType,
LikeSearchRequest req
Expand All @@ -114,14 +101,9 @@ public <T extends LikeTargetResponse> PagingResponse<LikeResponse<T>> getLikes(
.collect(Collectors.toMap(Like::getTargetId, Like::getCreatedAt));
Set<Long> targetIds = likedAtMap.keySet();

// TODO: 제네릭이 아닌 타입 별로 엔드포인트를 따로 만드는게 좋으려나 고민중, 현재 방법을 유지하면서 더 나은 구조 알고싶음...
LikeTargetFetcher<T> fetcher = (LikeTargetFetcher<T>)fetcherMap.get(targetType);
if (fetcher == null) {
throw new BizException(LikeErrorCode.INVALID_LIKE_TYPE);
}

Map<Long, T> targetMap = fetcher.fetchByIds(targetIds);
Page<LikeResponse<T>> content = likePage
Map<Long, LikeTargetResponse> targetMap
= likeTargetFetchService.fetchByTypeAndIds(targetType, targetIds, userId);
Page<LikeResponse<LikeTargetResponse>> content = likePage
.map(like -> new LikeResponse<>(targetMap.get(like.getTargetId()), likedAtMap.get(like.getTargetId())));

return PagingResponse.from(content);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package project.flipnote.like.service;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import project.flipnote.common.exception.BizException;
import project.flipnote.like.entity.LikeTargetType;
import project.flipnote.like.exception.LikeErrorCode;
import project.flipnote.like.model.response.LikeTargetResponse;
import project.flipnote.like.service.fetcher.LikeTargetFetcher;

@RequiredArgsConstructor
@Service
public class LikeTargetFetchService<T extends LikeTargetResponse> {

private final List<LikeTargetFetcher<T>> fetchers;

Comment on lines +21 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

제네릭 불변성으로 인한 Spring 주입 실패 가능성 (치명적).

List<LikeTargetFetcher>는 제네릭 불변(invariant)이라 LikeTargetFetcher가 LikeTargetFetcher에 주입되지 않습니다. 현재 LikeService/LikePolicyService에서 LikeTargetFetchService를 주입하고 있어, 실제로는 컨텍스트 로딩 시점에 주입 실패가 발생할 확률이 높습니다.

아래처럼 서비스의 제네릭을 제거하고, 내부에서 안전한(경고 억제) 캐스팅을 사용하면 주입 문제가 해소됩니다.

-@RequiredArgsConstructor
-@Service
-public class LikeTargetFetchService<T extends LikeTargetResponse> {
+@RequiredArgsConstructor
+@Service
+public class LikeTargetFetchService {

-    private final List<LikeTargetFetcher<T>> fetchers;
+    private final List<LikeTargetFetcher<?>> fetchers;

-    private Map<LikeTargetType, LikeTargetFetcher<T>> fetcherMap;
+    private Map<LikeTargetType, LikeTargetFetcher<?>> fetcherMap;

     @PostConstruct
     public void init() {
-        this.fetcherMap = this.fetchers.stream()
-            .collect(Collectors.toMap(LikeTargetFetcher::getTargetType, Function.identity()));
+        this.fetcherMap = this.fetchers.stream()
+            .collect(Collectors.toMap(LikeTargetFetcher::getTargetType, Function.identity()));
     }

     public boolean isTargetViewable(LikeTargetType targetType, Long targetId, Long userId) {
-        LikeTargetFetcher<T> targetFetcher = getFetcher(targetType);
+        LikeTargetFetcher<?> targetFetcher = getFetcher(targetType);
         return targetFetcher.isTargetViewable(targetId, userId);
     }

-    public Map<Long, T> fetchByTypeAndIds(
+    public <T extends LikeTargetResponse> Map<Long, T> fetchByTypeAndIds(
         LikeTargetType targetType,
         Set<Long> targetIds,
         Long userId
     ) {
-        LikeTargetFetcher<T> targetFetcher = getFetcher(targetType);
-        return targetFetcher.fetchByIds(targetIds, userId);
+        @SuppressWarnings("unchecked")
+        LikeTargetFetcher<T> targetFetcher = (LikeTargetFetcher<T>) getFetcher(targetType);
+        return targetFetcher.fetchByIds(targetIds, userId);
     }

-    private LikeTargetFetcher<T> getFetcher(LikeTargetType targetType) {
-        LikeTargetFetcher<T> fetcher = fetcherMap.get(targetType);
+    private LikeTargetFetcher<?> getFetcher(LikeTargetType targetType) {
+        LikeTargetFetcher<?> fetcher = fetcherMap.get(targetType);
         if (fetcher == null) {
             throw new BizException(LikeErrorCode.INVALID_LIKE_TYPE);
         }
         return fetcher;
     }
 }

이 변경에 맞춰 LikeService/LikePolicyService의 필드 타입도 LikeTargetFetchService (비제네릭)로 조정해야 합니다.

Also applies to: 39-47, 49-56

🤖 Prompt for AI Agents
In src/main/java/project/flipnote/like/service/LikeTargetFetchService.java
around lines 21-24 (and similarly at 39-47, 49-56), the class is declared with a
generic parameter T which makes the injected List<LikeTargetFetcher<T>>
invariant and prevents Spring from injecting implementations like
LikeTargetFetcher<CardSetLikeResponse>; remove the generic from the service
class declaration, change the internal fetchers field to use a wildcard or raw
type (e.g. List<LikeTargetFetcher<?>> or List) and perform any necessary
unchecked casts inside methods with @SuppressWarnings("unchecked") where you
return or operate on a specific LikeTargetResponse subtype, and then update
callers (LikeService and LikePolicyService) to inject LikeTargetFetchService
(non-generic) instead of LikeTargetFetchService<LikeTargetResponse>.

private Map<LikeTargetType, LikeTargetFetcher<T>> fetcherMap;

@PostConstruct
public void init() {
this.fetcherMap = this.fetchers.stream()
.collect(Collectors.toMap(LikeTargetFetcher::getTargetType, Function.identity()));
}

public boolean isTargetViewable(LikeTargetType targetType, Long targetId, Long userId) {
LikeTargetFetcher<T> targetFetcher = getFetcher(targetType);

return targetFetcher.isTargetViewable(targetId, userId);
}

public Map<Long, T> fetchByTypeAndIds(
LikeTargetType targetType,
Set<Long> targetIds,
Long userId
) {
LikeTargetFetcher<T> targetFetcher = getFetcher(targetType);

return targetFetcher.fetchByIds(targetIds, userId);
}

private LikeTargetFetcher<T> getFetcher(LikeTargetType targetType) {
LikeTargetFetcher<T> fetcher = fetcherMap.get(targetType);
if (fetcher == null) {
throw new BizException(LikeErrorCode.INVALID_LIKE_TYPE);
}

return fetcher;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import lombok.RequiredArgsConstructor;
import project.flipnote.cardset.service.CardSetService;
import project.flipnote.like.entity.LikeTargetType;
import project.flipnote.like.model.CardSetLikeResponse;
import project.flipnote.like.model.LikeTargetResponse;
import project.flipnote.like.model.response.CardSetLikeResponse;
import project.flipnote.like.model.response.LikeTargetResponse;

@RequiredArgsConstructor
@Component
Expand All @@ -25,7 +25,12 @@ public LikeTargetType getTargetType() {
}

@Override
public Map<Long, CardSetLikeResponse> fetchByIds(Set<Long> ids) {
public boolean isTargetViewable(Long targetId, Long userId) {
return cardSetService.isCardSetViewable(targetId, userId);
}

@Override
public Map<Long, CardSetLikeResponse> fetchByIds(Set<Long> ids, Long userId) {
return cardSetService.getCardSetsByIds(ids).stream()
.map(CardSetLikeResponse::from)
.collect(Collectors.toMap(LikeTargetResponse::getId, Function.identity()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import java.util.Set;

import project.flipnote.like.entity.LikeTargetType;
import project.flipnote.like.model.LikeTargetResponse;
import project.flipnote.like.model.response.LikeTargetResponse;

public interface LikeTargetFetcher<T extends LikeTargetResponse> {
LikeTargetType getTargetType();

Map<Long, T> fetchByIds(Set<Long> ids);
boolean isTargetViewable(Long targetId, Long userId);

Map<Long, T> fetchByIds(Set<Long> targetIds, Long userId);
}
Loading