중앙 해커톤 리팩토링-4

EauLune01·2026년 2월 13일

ggolist-refactor

목록 보기
3/3

Event/Popup/Store 즐겨찾기 설정, 해제(총 6개)와 로그인한 유저의 즐겨찾기 목록을 Slice 객체로 반환하는 API 총 7개가 남았다.
즐겨찾기 설정 및 해제는 로직이 동일하므로 Store만 정리해놓고 바로 즐겨찾기 목록 반환 API로 넘어갈 예정이다.

Store 즐겨찾기 등록

Controller

@PostMapping("/stores/{storeId}/favorites")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<ApiResponse<FavoriteResponse>> addStoreFavorite(
            @AuthenticationPrincipal User loginUser,
            @PathVariable Long storeId) {
        favoriteService.addStoreFavorite(loginUser.getId(), storeId);
        FavoriteResult result = favoriteQueryService.getStoreFavoriteStatus(loginUser.getId(), storeId);
        return ResponseEntity.status(HttpStatus.CREATED).body(new ApiResponse<>(true, 201, "가게 즐겨찾기 등록이 완료되었습니다.", FavoriteResponse.from(result)));
    }
  • security = @SecurityRequirement(name = "bearerAuth") 추가 필수
  • CQRS 원칙 적용

Service

public void addStoreFavorite(Long userId, Long storeId) {
        validateDuplicateFavorite(userId, storeId);

        User user = findUser(userId);
        Store store = findStore(storeId);

        favoriteStoreRepository.save(FavoriteStore.create(user, store));
        store.increaseLikeCount();
    }
  • 즐겨찾기 등록 가능 여부 검증-> 즐겨찾기 등록->가게의 좋아요 1개 추가
 public FavoriteResult getStoreFavoriteStatus(Long userId, Long storeId) {
        Store store = storeRepository.findById(storeId)
                .orElseThrow(() -> new PlaceNotFoundException("가게를 찾을 수 없습니다."));

        boolean isLiked = favoriteStoreRepository.existsByUserIdAndStoreId(userId, storeId);

        return FavoriteResult.of(storeId, isLiked, store.getLikeCount());
    }
  • store의 id와 유저가 좋아요 눌렀는지의 여부, store의 총 좋아요 개수를 result로 생성해 반환

Repository


    boolean existsByUserIdAndStoreId(Long userId, Long storeId);

Store 즐겨찾기 해제

Controller

@DeleteMapping("/stores/{storeId}/favorites")
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<ApiResponse<FavoriteResponse>> deleteStoreFavorite(
            @AuthenticationPrincipal User loginUser,
            @PathVariable Long storeId) {
        favoriteService.deleteStoreFavorite(loginUser.getId(), storeId);
        FavoriteResult result = favoriteQueryService.getStoreFavoriteStatus(loginUser.getId(), storeId);
        return ResponseEntity.ok(new ApiResponse<>(true, 200, "가게 즐겨찾기가 해제되었습니다.", FavoriteResponse.from(result)));
    }
  • security = @SecurityRequirement(name = "bearerAuth") 필수
  • CQRS 원칙 적용

Service

public void deleteStoreFavorite(Long userId, Long storeId) {
        FavoriteStore favorite = findFavoriteStore(userId, storeId);
        favoriteStoreRepository.delete(favorite);
        favorite.getStore().decreaseLikeCount();
    }
  • 이미 즐겨찾기 등록된 객체가 있는지 확인->즐겨찾기 삭제->가게의 좋아요 1 감소

쿼리서비스의 메소드는 즐겨찾기 등록과 같다.

Repository

@Query("select f from FavoriteStore f join fetch f.store where f.user.id = :userId and f.store.id = :storeId")
    Optional<FavoriteStore> findByUserIdAndStoreId(@Param("userId") Long userId, @Param("storeId") Long storeId);
  • @Query 메소드로 n+1 방지

전체 즐겨찾기 목록 조회

로그인한 유저가 찜한 가게, 이벤트, 팝업 목록을 통합하여 최신순으로 조회하는 API

Controller

@PreAuthorize("hasRole('USER')")
    @GetMapping("/favorites")
    public ResponseEntity<ApiResponse<SliceResponse<FavoriteItemResponse>>> getAllFavorites(
            @AuthenticationPrincipal User loginUser,
            @PageableDefault(size = 10) Pageable pageable) {
        Slice<FavoriteItemResult> results = favoriteQueryService.getAllFavorites(loginUser.getId(), pageable);
        SliceResponse<FavoriteItemResponse> response = SliceResponse.from(results.map(FavoriteItemResponse::from));
        return ResponseEntity.ok(new ApiResponse<>(true, 200, "전체 즐겨찾기 목록 조회 성공", response));
    }

Service

 public Slice<FavoriteItemResult> getAllFavorites(Long userId, Pageable pageable) {
        Slice<FavoriteItem> slice = favoriteQueryRepository.findAllFavorites(userId, pageable);
        return slice.map(FavoriteItemResult::from);
    }

Repository

public Slice<FavoriteItem> findAllFavorites(Long userId, Pageable pageable) {
        List<FavoriteItem> stores = queryFactory
                .select(new QFavoriteItem(store.id, ConstantImpl.create("store"), store.name, store.likeCount, favoriteStore.createdAt))
                .from(favoriteStore)
                .join(favoriteStore.store, store)
                .where(favoriteStore.user.id.eq(userId))
                .fetch();

        List<FavoriteItem> events = queryFactory
                .select(new QFavoriteItem(event.id, ConstantImpl.create("event"), event.name, event.likeCount, favoriteEvent.createdAt))
                .from(favoriteEvent)
                .join(favoriteEvent.event, event)
                .where(favoriteEvent.user.id.eq(userId))
                .fetch();

        List<FavoriteItem> popups = queryFactory
                .select(new QFavoriteItem(popup.id, ConstantImpl.create("popup"), popup.name, popup.likeCount, favoritePopup.createdAt))
                .from(favoritePopup)
                .join(favoritePopup.popup, popup)
                .where(favoritePopup.user.id.eq(userId))
                .fetch();

        List<FavoriteItem> total = new ArrayList<>();
        total.addAll(stores);
        total.addAll(events);
        total.addAll(popups);

        total.sort(Comparator.comparing(FavoriteItem::getCreatedAt).reversed());

        int start = (int) pageable.getOffset();
        int end = Math.min(start + pageable.getPageSize() + 1, total.size());

        List<FavoriteItem> content = (start >= total.size()) ? new ArrayList<>() : new ArrayList<>(total.subList(start, end));

        return SliceUtils.checkLastPage(pageable, content);
    }
  • 명세서 요구대로 QueryDSL을 작성하였다.

DTO

@Getter
@NoArgsConstructor
public class FavoriteItem {
    private String type;
    private Long id;
    private String name;
    private int likeCount;
    private boolean liked;
    private LocalDateTime createdAt;

    @QueryProjection
    public FavoriteItem(String type, Long id, String name, int likeCount, LocalDateTime createdAt) {
        this.type = type;
        this.id = id;
        this.name = name;
        this.likeCount = likeCount;
        this.liked = true;
        this.createdAt = createdAt;
    }
}
  • QueryDSL용 DTO를 하나 만들어 사용하였다.

지금까지 ggolist 중 내 담당 파트 리팩토링을 수행하였다. 개인적으로 필터와 조건이 많아서, querydsl을 공부하고 리팩터링한 것이 querydsl과 친해지는 데에도 많은 도움이 된 것 같고 entity도 잘 리팩터링한 것 같아 성취감을 느꼈던 리팩토링이었다.

profile
Séoul

0개의 댓글