From 490016a968b525e39876096c53f15d9307a658b9 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 14 Aug 2025 18:37:01 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9D=B8=EA=B8=B0?= =?UTF-8?q?=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A7=91=EA=B3=84=20=EC=96=B4?= =?UTF-8?q?=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/query/DateQueryServiceImpl.java | 3 + .../aop/LogPlaceCategoryAspect.java | 95 +++++++------------ .../PlaceCategoryLogRepository.java | 2 +- .../scheduler/PlaceCategoryLogScheduler.java | 75 ++++++++------- 4 files changed, 73 insertions(+), 102 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java index 00e4be5..4522700 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java @@ -11,6 +11,7 @@ import org.withtime.be.withtimebe.domain.date.entity.DateCourseBookmark; import org.withtime.be.withtimebe.domain.date.repository.DateCourseBookmarkRepository; import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; +import org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory; import org.withtime.be.withtimebe.domain.member.entity.Member; import java.util.List; @@ -22,10 +23,12 @@ public class DateQueryServiceImpl implements DateQueryService { private final DateCourseRepository dateCourseRepository; + @LogPlaceCategory public Page findDateCourses(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable){ return dateCourseRepository.searchDateCourseByApplyPage(dateCourseSearchCond, pageable); } + @LogPlaceCategory public Page findDateCourseBookmarks(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable, Member member){ return dateCourseRepository.searchDateCourseBookmarkByMemberAndApplyPage(dateCourseSearchCond, member, pageable); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java index 4937c5b..2bede1e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java @@ -1,27 +1,21 @@ package org.withtime.be.withtimebe.domain.log.placecategorylog.aop; import java.lang.reflect.Field; -import java.time.DayOfWeek; -import java.time.Duration; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Collection; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.stream.IntStream; import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.aspectj.lang.reflect.MethodSignature; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,86 +27,61 @@ public class LogPlaceCategoryAspect { private final RedisTemplate redisTemplate; - @Before("@annotation(org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory)") + @Async + @AfterReturning("@annotation(org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory)") public void logPlaceCategory(JoinPoint joinPoint) { - - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - String[] paramNames = signature.getParameterNames(); + Object[] args = joinPoint.getArgs(); - List placeCategoryIds = IntStream.range(0, args.length) - .mapToObj(i -> extractIdsFromParam(paramNames[i], args[i])) - .flatMap(Collection::stream) + List keywords = Arrays.stream(args) + .flatMap(arg -> extractKeywordsFromDTO(arg).stream()) .toList(); - savePlaceCategoryIds(placeCategoryIds); + saveKeywords(keywords); } - // 1. 파라미터로부터 placeCategoryId 추출 - private List extractIdsFromParam(String paramName, Object arg) { - - if (paramName.contains("placeCategoryId") && arg instanceof Long id) { - return List.of(id); - } - - if (paramName.contains("placeCategoryId") && arg instanceof List list) { - return list.stream() - .filter(Long.class::isInstance) - .map(Long.class::cast) - .toList(); - } - - // DTO로 간주하고 추출 시도 - return extractIdsFromDTO(arg); - } - - // 2. DTO로부터 placeCategoryId 추출 - private List extractIdsFromDTO(Object dto) { + private List extractKeywordsFromDTO(Object dto) { if (dto == null) return Collections.emptyList(); - List ids = new ArrayList<>(); + List keywords = new ArrayList<>(); - // DTO에 정의된 필드에 접근 for (Field field : dto.getClass().getDeclaredFields()) { - if (!field.getName().contains("placeCategoryId")) continue; + if (!field.getName().contains("userPreferredKeywords")) continue; - field.setAccessible(true); // private 필드 접근 설정 + field.setAccessible(true); try { - Object value = field.get(dto); // 값 추출 - if (value instanceof Long placeCategoryId) { - ids.add(placeCategoryId); - } - else if (value instanceof List list) { - for (Object id : list) { - if (id instanceof Long placeCategoryId) { - ids.add(placeCategoryId); + Object value = field.get(dto); + if (value instanceof List list) { + for (Object keyword : list) { + if (keyword instanceof String str) { + keywords.add(str); } } } } catch (Exception e) { - log.warn("DTO에서 placeCategoryId 추출 실패"); + log.warn("[LogPlaceCategoryAspect] DTO에서 userPreferredKeywords 추출 실패", e); } } - return ids; + return keywords; } - private void savePlaceCategoryIds(List placeCategoryIds) { - if (placeCategoryIds.isEmpty()) return; + private void saveKeywords(List keywords) { + if (keywords.isEmpty()) return; - LocalDateTime now = LocalDateTime.now(); - String today = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); - String redisKey = "log:place-category:" + today; + LocalDate now = LocalDate.now(); + String formattedDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String redisKey = "log:user-preferred-keywords:" + formattedDate; - // ZSET - 카테고리 별 검색 횟수 기록 - placeCategoryIds - .forEach(id -> redisTemplate.opsForZSet().incrementScore(redisKey, id, 1)); + keywords.forEach(keyword -> + redisTemplate.opsForZSet().incrementScore(redisKey, keyword, 1) + ); - // TTL - 이번 주까지로 설정 + // TTL 설정 Long expire = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS); if (expire == null || expire <= 0) { - LocalDateTime endOfWeek = now.with(DayOfWeek.SUNDAY).with(LocalTime.MAX); - Duration duration = Duration.between(now, endOfWeek); - redisTemplate.expire(redisKey, duration.getSeconds(), TimeUnit.SECONDS); + redisTemplate.expire(redisKey, 1, TimeUnit.HOURS); } + + log.info("[LogPlaceCategoryAspect] 키워드 로그 임시 저장 완료"); } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java index e7d48ec..cba6ae0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java @@ -8,5 +8,5 @@ public interface PlaceCategoryLogRepository extends MongoRepository { List findByDateBetween(LocalDate startDate, LocalDate endDate); - List findByPlaceCategoryIdInAndDate(List placeCategoryIds, LocalDate date); + List findByDateAndPlaceCategoryLabelIn(LocalDate date, List placeCategoryLabel); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java index 641cd7c..b92201b 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java @@ -34,7 +34,6 @@ public class PlaceCategoryLogScheduler { private final RedisTemplate redisTemplate; private final PlaceCategoryLogRepository placeCategoryLogRepository; - private final PlaceCategoryRepository placeCategoryRepository; @Scheduled(cron = "${scheduler.logs.place-category.sync-cron}") // 매 5분마다 @CacheEvict( @@ -43,64 +42,64 @@ public class PlaceCategoryLogScheduler { cacheManager = "redisCacheManager", beforeInvocation = false ) - public void syncPlaceCategoryLogsToDB() { + @Scheduled(cron = "${scheduler.logs.place-category.sync-cron}") // 매 5분마다 + public void syncUserPreferredKeywordsToDB() { log.info("[PlaceCategoryLogScheduler] 동기화 스케쥴러 동작"); // 현재 날짜 및 레디스 키 생성 - LocalDate now = LocalDate.from(LocalDateTime.now().minusMinutes(1)); + LocalDate now = LocalDate.now(); String formattedDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); - String redisKey = "log:place-category:" + formattedDate; + String redisKey = "log:user-preferred-keywords:" + formattedDate; // ZSET 추출 Set> zSet = redisTemplate.opsForZSet().rangeWithScores(redisKey, 0, -1); - // 없는 경우 Skip - if (zSet == null || zSet.isEmpty()) + if (zSet == null || zSet.isEmpty()) { + log.info("[PlaceCategoryLogScheduler] 저장된 로그가 없어 동기화 작업을 종료합니다."); return; + } - // ZSET 데이터 가공 - Map placeCategoryScoreMap = zSet.stream() + // 키워드별 점수 추출 + Map keywordScoreMap = zSet.stream() .collect(Collectors.toMap( - tuple -> Long.valueOf(String.valueOf(tuple.getValue())), + tuple -> String.valueOf(tuple.getValue()), tuple -> tuple.getScore().intValue() )); - // IN 쿼리로 이미 존재하는 PlaceCategoryLog 조회 - List placeCategoryIds = new ArrayList<>(placeCategoryScoreMap.keySet()); - List placeCategoryLogList = placeCategoryLogRepository.findByPlaceCategoryIdInAndDate(placeCategoryIds, now); - - // placeCategoryId - placeCategoryLog 매핑 - Map placeCategoryLogMap = placeCategoryLogList.stream() - .collect(Collectors.toMap(PlaceCategoryLog::getPlaceCategoryId, Function.identity())); - - // 레디스 데이터를 다큐먼트에 반영 - for (Map.Entry entry : placeCategoryScoreMap.entrySet()) { - Long placeCategoryId = entry.getKey(); + // 키워드를 통해 이미 저장되어 있던 로그 조회 + List keywords = new ArrayList<>(keywordScoreMap.keySet()); + List existingLogs = placeCategoryLogRepository.findByDateAndPlaceCategoryLabelIn(now, keywords); + + // for문에서 + Map logMap = existingLogs.stream() + .collect(Collectors.toMap(PlaceCategoryLog::getPlaceCategoryLabel, Function.identity())); + + // saveAll로 저장될 로그 리스트 + List logsToSave = new ArrayList<>(); + for (Map.Entry entry : keywordScoreMap.entrySet()) { + String keyword = entry.getKey(); Integer score = entry.getValue(); - // 기존 로그가 존재하는 경우 count만 증가 - if (placeCategoryLogMap.containsKey(placeCategoryId)) { - placeCategoryLogMap.get(placeCategoryId).incrementCount(score); - } - // 그렇지 않으면 새로 로그 생성 - else { - Optional placeCategoryOptional = placeCategoryRepository.findById(placeCategoryId); - if(placeCategoryOptional.isPresent()) { - PlaceCategory placeCategory = placeCategoryOptional.get(); - PlaceCategoryLog newPlaceCategoryLog = PlaceCategoryLogConverter - .toPlaceCategoryLog(placeCategory, score, now); - placeCategoryLogList.add(newPlaceCategoryLog); - } + if (logMap.containsKey(keyword)) { + logMap.get(keyword).incrementCount(score); + } else { + PlaceCategoryLog log = PlaceCategoryLog.builder() + .placeCategoryLabel(keyword) + .count(score) + .date(now) + .build(); + logsToSave.add(log); } } - - // 다큐먼트를 DB에 반영 - if (!placeCategoryLogList.isEmpty()) { - placeCategoryLogRepository.saveAll(placeCategoryLogList); + + // saveAll로 한번에 저장되도록 + if (!logsToSave.isEmpty() || !existingLogs.isEmpty()) { + placeCategoryLogRepository.saveAll(existingLogs); // 기존 로그 count 증가 반영 + placeCategoryLogRepository.saveAll(logsToSave); // 신규 로그 저장 redisTemplate.delete(redisKey); - log.info("[PlaceCategoryLogScheduler] 카테고리 로그 저장 완료, 개수 : {}", placeCategoryLogList.size()); + log.info("[PlaceCategoryLogScheduler] 키워드 로그 저장 완료, 개수 : {}", logsToSave.size() + existingLogs.size()); } } } From c2237436e581b57c2ac8059bbc37cde6155c29d2 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 14 Aug 2025 19:04:41 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=A3=BC=EA=B0=84=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B3=BC=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlaceCategoryLogQueryController.java | 4 +-- .../converter/PlaceCategoryLogConverter.java | 33 ++++++++++++------- .../dto/PlaceCategoryLogResponseDTO.java | 6 ++-- .../model/PlaceCategoryLog.java | 1 - .../scheduler/PlaceCategoryLogScheduler.java | 8 ++--- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java index 53356ed..992e1a4 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java @@ -30,9 +30,9 @@ public class PlaceCategoryLogQueryController { @ApiResponse(responseCode = "200", description = "성공입니다.") }) @GetMapping("/weekly") - public DefaultResponse findWeeklyPlaceCategoryLogList() { + public DefaultResponse findWeeklyPlaceCategoryLogList() { List result = placeCategoryLogQueryService.findWeeklyPlaceCategoryLogList(); - PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList response = PlaceCategoryLogConverter.toWeeklyPlaceCategoryLogList(result); + PlaceCategoryLogResponseDTO.PlaceCategoryLogList response = PlaceCategoryLogConverter.toWeeklyPlaceCategoryLogList(result); return DefaultResponse.ok(response); } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java index e4dc3b0..a6af9ec 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java @@ -3,6 +3,8 @@ import java.time.LocalDate; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory; import org.withtime.be.withtimebe.domain.log.placecategorylog.dto.PlaceCategoryLogResponseDTO; @@ -10,29 +12,36 @@ public class PlaceCategoryLogConverter { - public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList toWeeklyPlaceCategoryLogList(List placeCategoryLogList) { + public static PlaceCategoryLogResponseDTO.PlaceCategoryLogList toWeeklyPlaceCategoryLogList(List placeCategoryLogList) { - List weeklyPlaceCategoryLogList = placeCategoryLogList.stream() - .sorted(Comparator.comparing(PlaceCategoryLog::getCount).reversed()) // count 기준 내림차순 - .map(PlaceCategoryLogConverter::toWeeklyPlaceCategoryLog) + // 1. 키워드 별 count 합산 + Map countPerKeyword = placeCategoryLogList.stream() + .collect(Collectors.groupingBy( + PlaceCategoryLog::getPlaceCategoryLabel, + Collectors.summingInt(PlaceCategoryLog::getCount) + )); + + // 2. DTO 변환 + List weeklyPlaceCategoryLogList = countPerKeyword.entrySet().stream() + .map((entry) -> toPlaceCategoryLogDTO(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(PlaceCategoryLogResponseDTO.PlaceCategoryLog::count).reversed()) // count 기준 내림차순 .toList(); - return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList.builder() + return PlaceCategoryLogResponseDTO.PlaceCategoryLogList.builder() .placeCategoryLogList(weeklyPlaceCategoryLogList) .build(); } - public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(PlaceCategoryLog placeCategoryLog) { - return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog.builder() - .placeCategoryLabel(placeCategoryLog.getPlaceCategoryLabel()) - .count(placeCategoryLog.getCount()) + public static PlaceCategoryLogResponseDTO.PlaceCategoryLog toPlaceCategoryLogDTO(String placeCategoryLabel, Integer count) { + return PlaceCategoryLogResponseDTO.PlaceCategoryLog.builder() + .placeCategoryLabel(placeCategoryLabel) + .count(count) .build(); } - public static PlaceCategoryLog toPlaceCategoryLog(PlaceCategory placeCategory, Integer count, LocalDate date) { + public static PlaceCategoryLog toPlaceCategoryLog(String placeCategoryLabel, Integer count, LocalDate date) { return PlaceCategoryLog.builder() - .placeCategoryId(placeCategory.getId()) - .placeCategoryLabel(placeCategory.getLabel()) + .placeCategoryLabel(placeCategoryLabel) .count(count) .date(date) .build(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java index 30194e2..be87c68 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java @@ -7,12 +7,12 @@ public class PlaceCategoryLogResponseDTO { @Builder - public record WeeklyPlaceCategoryLogList( - List placeCategoryLogList + public record PlaceCategoryLogList( + List placeCategoryLogList ) {} @Builder - public record WeeklyPlaceCategoryLog( + public record PlaceCategoryLog( String placeCategoryLabel, Integer count ) {} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java index ddb999a..e78c18d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java @@ -22,7 +22,6 @@ public class PlaceCategoryLog extends BaseEntity { @Id private String id; - private Long placeCategoryId; private String placeCategoryLabel; private LocalDate date; private Integer count; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java index b92201b..19203a0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java @@ -72,7 +72,7 @@ public void syncUserPreferredKeywordsToDB() { List keywords = new ArrayList<>(keywordScoreMap.keySet()); List existingLogs = placeCategoryLogRepository.findByDateAndPlaceCategoryLabelIn(now, keywords); - // for문에서 + // for문에서 빠른 분기 처리를 위한 Map 생성 Map logMap = existingLogs.stream() .collect(Collectors.toMap(PlaceCategoryLog::getPlaceCategoryLabel, Function.identity())); @@ -85,11 +85,7 @@ public void syncUserPreferredKeywordsToDB() { if (logMap.containsKey(keyword)) { logMap.get(keyword).incrementCount(score); } else { - PlaceCategoryLog log = PlaceCategoryLog.builder() - .placeCategoryLabel(keyword) - .count(score) - .date(now) - .build(); + PlaceCategoryLog log = PlaceCategoryLogConverter.toPlaceCategoryLog(keyword, score, now); logsToSave.add(log); } } From e6a43bbe916fa420b6740d1ebb4eac0d4a915a7e Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 14 Aug 2025 19:53:09 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=80=EA=B2=BD=20=EA=B0=90=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20=EC=A0=81=EB=A6=BD=20AOP=EC=9D=98=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/domain/member/aop/GetPointAspect.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java b/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java index 99071d9..a7f9a0a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/aop/GetPointAspect.java @@ -3,6 +3,8 @@ import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -13,10 +15,13 @@ import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Aspect @Component @RequiredArgsConstructor +@Order(Ordered.LOWEST_PRECEDENCE - 1) // 읽기 전용 트랜잭션보다 먼저 실행되도록 하여 포인트 변경 감지되도록 public class GetPointAspect { private final MemberCommandService memberCommandService; @@ -31,6 +36,7 @@ public void addPoint(GetPoint getPoint) { // 인증 객체 추출 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication == null || !authentication.isAuthenticated()) { + log.info("[GetPointAspect] 유저의 인증 정보가 존재하지 않아 포인트가 적립되지 않았습니다."); return; } @@ -39,6 +45,7 @@ public void addPoint(GetPoint getPoint) { if(userDetails instanceof CustomUserDetails customUserDetails) { Long memberId = customUserDetails.getMember().getId(); memberCommandService.addPoint(memberId, point); + log.info("[GetPointAspect] {} 님의 포인트 적립에 성공하였습니다.", customUserDetails.getMember().getUsername()); } } } From 833d62bf302d36b7005430c989c24642bac2a0c0 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 14 Aug 2025 20:02:06 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=81=EB=A6=BD=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/DatePreferenceTestCommandServiceImpl.java | 3 +++ .../date/service/command/DateCommandServiceImpl.java | 5 +++++ .../date/service/query/DateQueryServiceImpl.java | 10 ++++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/preference/service/command/DatePreferenceTestCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/date/preference/service/command/DatePreferenceTestCommandServiceImpl.java index 15609e0..7f6be6a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/preference/service/command/DatePreferenceTestCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/preference/service/command/DatePreferenceTestCommandServiceImpl.java @@ -14,6 +14,8 @@ import org.withtime.be.withtimebe.domain.date.preference.repository.DatePreferenceQuestionRepository; import org.withtime.be.withtimebe.domain.date.preference.repository.DatePreferenceTestResultRepository; import org.withtime.be.withtimebe.domain.date.preference.util.DatePreferenceTestScoreCalculator; +import org.withtime.be.withtimebe.domain.member.annotation.GetPoint; +import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.global.error.code.DatePreferenceErrorCode; import org.withtime.be.withtimebe.global.error.exception.DatePreferenceException; @@ -33,6 +35,7 @@ public class DatePreferenceTestCommandServiceImpl implements DatePreferenceTestC private final DatePreferenceTestScoreCalculator datePreferenceTestScoreCalculator; @Override + @GetPoint(action = PointAction.COMPLETE_TEST) public DatePreferenceResponseDTO.TestResult test(Member member, DatePreferenceRequestDTO.Test request) { // valid 판단 if (!validateRequest(request)) { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java index c8c72a1..c1cf30c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java @@ -18,6 +18,8 @@ import org.withtime.be.withtimebe.domain.date.repository.DateCourseBookmarkRepository; import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; import org.withtime.be.withtimebe.domain.date.repository.DatePlaceRepository; +import org.withtime.be.withtimebe.domain.member.annotation.GetPoint; +import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.global.error.code.DateCourseErrorCode; import org.withtime.be.withtimebe.global.error.exception.DateCourseException; @@ -40,6 +42,7 @@ public class DateCommandServiceImpl implements DateCommandService{ private final DatePlaceRepository datePlaceRepository; // 사용자 맞춤형 데이트 코스 생성 + @GetPoint(action = PointAction.CREATE_DATE_COURSE) public List createDateCourse( DateRequestDTO.CreateDateCourse request ) { @@ -119,6 +122,7 @@ private List getScheduledDateCourses( } // 데이트코스 북마크 생성 - 직접 데이트 코스 찾아보기 + @GetPoint(action = PointAction.SAVE_DATE_COURSE) public DateCourseBookmark createDateCourseBookmark(Long dateCourseId, Member member) { DateCourse dateCourse = dateCourseRepository.findById(dateCourseId) .orElseThrow(() -> new DateCourseException(DateCourseErrorCode.DateCourse_NOT_FOUND)); @@ -137,6 +141,7 @@ public DateCourse deleteDateCourseBookmark(Long dateCourseId, Member member){ } // 데이트코스 북마크 생성 - AI 기반 데이트 코스 만들기 + @GetPoint(action = PointAction.CREATE_DATE_COURSE) public DateCourseBookmark createDateCourseBookmarkWithGeneratedCourse( DateRequestDTO.SaveDateCourse request, Member member diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java index 4522700..b10f019 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java @@ -5,17 +5,14 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.withtime.be.withtimebe.domain.date.converter.DateConverter; import org.withtime.be.withtimebe.domain.date.dto.request.DateRequestDTO; import org.withtime.be.withtimebe.domain.date.entity.DateCourse; -import org.withtime.be.withtimebe.domain.date.entity.DateCourseBookmark; -import org.withtime.be.withtimebe.domain.date.repository.DateCourseBookmarkRepository; import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; import org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory; +import org.withtime.be.withtimebe.domain.member.annotation.GetPoint; +import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction; import org.withtime.be.withtimebe.domain.member.entity.Member; -import java.util.List; - @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -24,13 +21,14 @@ public class DateQueryServiceImpl implements DateQueryService { private final DateCourseRepository dateCourseRepository; @LogPlaceCategory + @GetPoint(action = PointAction.VIEW_DATE_COURSE) public Page findDateCourses(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable){ return dateCourseRepository.searchDateCourseByApplyPage(dateCourseSearchCond, pageable); } @LogPlaceCategory + @GetPoint(action = PointAction.VIEW_DATE_COURSE) public Page findDateCourseBookmarks(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable, Member member){ return dateCourseRepository.searchDateCourseBookmarkByMemberAndApplyPage(dateCourseSearchCond, member, pageable); } - } From d424df7f7c166098f8c43baeacb498372f19dd73 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 14 Aug 2025 20:31:27 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=B3=91=ED=95=A9?= =?UTF-8?q?=20=ED=9B=84=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A0=81=EB=A6=BD?= =?UTF-8?q?=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=9E=AC?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../date/service/command/DateCommandServiceImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java index ff06fe5..b1e1ff7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java @@ -19,6 +19,8 @@ import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; import org.withtime.be.withtimebe.domain.date.repository.DatePlaceRepository; import org.withtime.be.withtimebe.domain.date.service.command.dto.RecommendedCourseResult; +import org.withtime.be.withtimebe.domain.member.annotation.GetPoint; +import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.global.error.code.DateCourseErrorCode; import org.withtime.be.withtimebe.global.error.exception.DateCourseException; @@ -37,8 +39,9 @@ public class DateCommandServiceImpl implements DateCommandService{ private final DateCourseRepository dateCourseRepository; private final DatePlaceRepository datePlaceRepository; - @Transactional(readOnly = true) /** 단일 코스 생성 (저장/북마크/attemptCount 없음, excludedCourseSignatures로 중복 제외) */ + @Transactional(readOnly = true) + @GetPoint(action = PointAction.CREATE_DATE_COURSE) public RecommendedCourseResult createDateCourse(DateRequestDTO.CreateDateCourse request) { if (request == null || request.dateDurationTime() == null) { return new RecommendedCourseResult(List.of(), null); @@ -283,6 +286,7 @@ public DateCourse deleteDateCourseBookmark(Long dateCourseId, Member member){ } // 데이트코스 북마크 생성 - AI 기반 데이트 코스 만들기 + @GetPoint(action = PointAction.SAVE_DATE_COURSE) public DateCourseBookmark createDateCourseBookmarkWithGeneratedCourse( DateRequestDTO.SaveDateCourse request, Member member @@ -300,6 +304,7 @@ public DateCourseBookmark createDateCourseBookmarkWithGeneratedCourse( } // 데이트코스 북마크 생성 - 직접 데이트 코스 찾아보기 + @GetPoint(action = PointAction.SAVE_DATE_COURSE) public DateCourseBookmark createDateCourseBookmark(Long dateCourseId, Member member) { DateCourse dateCourse = dateCourseRepository.findById(dateCourseId) .orElseThrow(() -> new DateCourseException(DateCourseErrorCode.DateCourse_NOT_FOUND)); From e7e1c8c542b1cfd2261f0ba9239583effc375679 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 14 Aug 2025 20:48:56 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=90=9B=20fix:=20DTO=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlaceCategoryLogQueryController.java | 4 ++-- .../converter/PlaceCategoryLogConverter.java | 14 +++++++------- .../dto/PlaceCategoryLogResponseDTO.java | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java index 992e1a4..53356ed 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java @@ -30,9 +30,9 @@ public class PlaceCategoryLogQueryController { @ApiResponse(responseCode = "200", description = "성공입니다.") }) @GetMapping("/weekly") - public DefaultResponse findWeeklyPlaceCategoryLogList() { + public DefaultResponse findWeeklyPlaceCategoryLogList() { List result = placeCategoryLogQueryService.findWeeklyPlaceCategoryLogList(); - PlaceCategoryLogResponseDTO.PlaceCategoryLogList response = PlaceCategoryLogConverter.toWeeklyPlaceCategoryLogList(result); + PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList response = PlaceCategoryLogConverter.toWeeklyPlaceCategoryLogList(result); return DefaultResponse.ok(response); } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java index a6af9ec..e904fd0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java @@ -12,7 +12,7 @@ public class PlaceCategoryLogConverter { - public static PlaceCategoryLogResponseDTO.PlaceCategoryLogList toWeeklyPlaceCategoryLogList(List placeCategoryLogList) { + public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList toWeeklyPlaceCategoryLogList(List placeCategoryLogList) { // 1. 키워드 별 count 합산 Map countPerKeyword = placeCategoryLogList.stream() @@ -22,18 +22,18 @@ public static PlaceCategoryLogResponseDTO.PlaceCategoryLogList toWeeklyPlaceCate )); // 2. DTO 변환 - List weeklyPlaceCategoryLogList = countPerKeyword.entrySet().stream() - .map((entry) -> toPlaceCategoryLogDTO(entry.getKey(), entry.getValue())) - .sorted(Comparator.comparing(PlaceCategoryLogResponseDTO.PlaceCategoryLog::count).reversed()) // count 기준 내림차순 + List weeklyPlaceCategoryLogList = countPerKeyword.entrySet().stream() + .map((entry) -> toWeeklyPlaceCategoryLog(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog::count).reversed()) // count 기준 내림차순 .toList(); - return PlaceCategoryLogResponseDTO.PlaceCategoryLogList.builder() + return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList.builder() .placeCategoryLogList(weeklyPlaceCategoryLogList) .build(); } - public static PlaceCategoryLogResponseDTO.PlaceCategoryLog toPlaceCategoryLogDTO(String placeCategoryLabel, Integer count) { - return PlaceCategoryLogResponseDTO.PlaceCategoryLog.builder() + public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(String placeCategoryLabel, Integer count) { + return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog.builder() .placeCategoryLabel(placeCategoryLabel) .count(count) .build(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java index be87c68..30194e2 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java @@ -7,12 +7,12 @@ public class PlaceCategoryLogResponseDTO { @Builder - public record PlaceCategoryLogList( - List placeCategoryLogList + public record WeeklyPlaceCategoryLogList( + List placeCategoryLogList ) {} @Builder - public record PlaceCategoryLog( + public record WeeklyPlaceCategoryLog( String placeCategoryLabel, Integer count ) {}