Skip to content

Commit 10e92de

Browse files
authored
✨ [FEAT] 사용자 행동 별 포인트 적립 및 인기 키워드 집계 AOP 적용 (#103)
2 parents 0a3f8c2 + e7e1c8c commit 10e92de

File tree

9 files changed

+107
-123
lines changed

9 files changed

+107
-123
lines changed

src/main/java/org/withtime/be/withtimebe/domain/date/preference/service/command/DatePreferenceTestCommandServiceImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.withtime.be.withtimebe.domain.date.preference.repository.DatePreferenceQuestionRepository;
1515
import org.withtime.be.withtimebe.domain.date.preference.repository.DatePreferenceTestResultRepository;
1616
import org.withtime.be.withtimebe.domain.date.preference.util.DatePreferenceTestScoreCalculator;
17+
import org.withtime.be.withtimebe.domain.member.annotation.GetPoint;
18+
import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction;
1719
import org.withtime.be.withtimebe.domain.member.entity.Member;
1820
import org.withtime.be.withtimebe.global.error.code.DatePreferenceErrorCode;
1921
import org.withtime.be.withtimebe.global.error.exception.DatePreferenceException;
@@ -33,6 +35,7 @@ public class DatePreferenceTestCommandServiceImpl implements DatePreferenceTestC
3335
private final DatePreferenceTestScoreCalculator datePreferenceTestScoreCalculator;
3436

3537
@Override
38+
@GetPoint(action = PointAction.COMPLETE_TEST)
3639
public DatePreferenceResponseDTO.TestResult test(Member member, DatePreferenceRequestDTO.Test request) {
3740
// valid 판단
3841
if (!validateRequest(request)) {

src/main/java/org/withtime/be/withtimebe/domain/date/service/command/DateCommandServiceImpl.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository;
2020
import org.withtime.be.withtimebe.domain.date.repository.DatePlaceRepository;
2121
import org.withtime.be.withtimebe.domain.date.service.command.dto.RecommendedCourseResult;
22+
import org.withtime.be.withtimebe.domain.member.annotation.GetPoint;
23+
import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction;
2224
import org.withtime.be.withtimebe.domain.member.entity.Member;
2325
import org.withtime.be.withtimebe.global.error.code.DateCourseErrorCode;
2426
import org.withtime.be.withtimebe.global.error.exception.DateCourseException;
@@ -37,11 +39,9 @@ public class DateCommandServiceImpl implements DateCommandService{
3739
private final DateCourseRepository dateCourseRepository;
3840
private final DatePlaceRepository datePlaceRepository;
3941

40-
/** 컨트롤러로 전달할 단일 추천 결과 (코스 + 중복 방지 시그니처) */
41-
42-
43-
@Transactional(readOnly = true)
4442
/** 단일 코스 생성 (저장/북마크/attemptCount 없음, excludedCourseSignatures로 중복 제외) */
43+
@Transactional(readOnly = true)
44+
@GetPoint(action = PointAction.CREATE_DATE_COURSE)
4545
public RecommendedCourseResult createDateCourse(DateRequestDTO.CreateDateCourse request) {
4646
if (request == null || request.dateDurationTime() == null) {
4747
return new RecommendedCourseResult(List.of(), null);
@@ -118,7 +118,6 @@ public RecommendedCourseResult createDateCourse(DateRequestDTO.CreateDateCourse
118118
}
119119

120120
// ───────────────────────── 내부 로직 ─────────────────────────
121-
122121
/** 주소 키워드 토큰으로 후보 조회 + ID 기준 중복 제거(순서 보존) */
123122
private List<DatePlace> collectCandidatesByAddressTokens(List<String> tokens) {
124123
if (tokens == null || tokens.isEmpty()) return List.of();
@@ -287,6 +286,7 @@ public DateCourse deleteDateCourseBookmark(Long dateCourseId, Member member){
287286
}
288287

289288
// 데이트코스 북마크 생성 - AI 기반 데이트 코스 만들기
289+
@GetPoint(action = PointAction.SAVE_DATE_COURSE)
290290
public DateCourseBookmark createDateCourseBookmarkWithGeneratedCourse(
291291
DateRequestDTO.SaveDateCourse request,
292292
Member member
@@ -304,6 +304,7 @@ public DateCourseBookmark createDateCourseBookmarkWithGeneratedCourse(
304304
}
305305

306306
// 데이트코스 북마크 생성 - 직접 데이트 코스 찾아보기
307+
@GetPoint(action = PointAction.SAVE_DATE_COURSE)
307308
public DateCourseBookmark createDateCourseBookmark(Long dateCourseId, Member member) {
308309
DateCourse dateCourse = dateCourseRepository.findById(dateCourseId)
309310
.orElseThrow(() -> new DateCourseException(DateCourseErrorCode.DateCourse_NOT_FOUND));

src/main/java/org/withtime/be/withtimebe/domain/date/service/query/DateQueryServiceImpl.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,30 @@
55
import org.springframework.data.domain.Pageable;
66
import org.springframework.stereotype.Service;
77
import org.springframework.transaction.annotation.Transactional;
8-
import org.withtime.be.withtimebe.domain.date.converter.DateConverter;
98
import org.withtime.be.withtimebe.domain.date.dto.request.DateRequestDTO;
109
import org.withtime.be.withtimebe.domain.date.entity.DateCourse;
11-
import org.withtime.be.withtimebe.domain.date.entity.DateCourseBookmark;
12-
import org.withtime.be.withtimebe.domain.date.repository.DateCourseBookmarkRepository;
1310
import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository;
11+
import org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory;
12+
import org.withtime.be.withtimebe.domain.member.annotation.GetPoint;
13+
import org.withtime.be.withtimebe.domain.member.annotation.enums.PointAction;
1414
import org.withtime.be.withtimebe.domain.member.entity.Member;
1515

16-
import java.util.List;
17-
1816
@Service
1917
@Transactional(readOnly = true)
2018
@RequiredArgsConstructor
2119
public class DateQueryServiceImpl implements DateQueryService {
2220

2321
private final DateCourseRepository dateCourseRepository;
2422

23+
@LogPlaceCategory
24+
@GetPoint(action = PointAction.VIEW_DATE_COURSE)
2525
public Page<DateCourse> findDateCourses(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable){
2626
return dateCourseRepository.searchDateCourseByApplyPage(dateCourseSearchCond, pageable);
2727
}
2828

29+
@LogPlaceCategory
30+
@GetPoint(action = PointAction.VIEW_DATE_COURSE)
2931
public Page<DateCourse> findDateCourseBookmarks(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable, Member member){
3032
return dateCourseRepository.searchDateCourseBookmarkByMemberAndApplyPage(dateCourseSearchCond, member, pageable);
3133
}
32-
3334
}
Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
package org.withtime.be.withtimebe.domain.log.placecategorylog.aop;
22

33
import java.lang.reflect.Field;
4-
import java.time.DayOfWeek;
5-
import java.time.Duration;
64
import java.time.LocalDate;
7-
import java.time.LocalDateTime;
8-
import java.time.LocalTime;
95
import java.time.format.DateTimeFormatter;
106
import java.util.ArrayList;
11-
import java.util.Collection;
7+
import java.util.Arrays;
128
import java.util.Collections;
139
import java.util.List;
1410
import java.util.concurrent.TimeUnit;
15-
import java.util.stream.IntStream;
1611

1712
import org.aspectj.lang.JoinPoint;
13+
import org.aspectj.lang.annotation.AfterReturning;
1814
import org.aspectj.lang.annotation.Aspect;
19-
import org.aspectj.lang.annotation.Before;
20-
import org.aspectj.lang.reflect.MethodSignature;
2115
import org.springframework.data.redis.core.RedisTemplate;
16+
import org.springframework.scheduling.annotation.Async;
2217
import org.springframework.stereotype.Component;
2318

24-
2519
import lombok.RequiredArgsConstructor;
2620
import lombok.extern.slf4j.Slf4j;
2721

@@ -33,86 +27,61 @@ public class LogPlaceCategoryAspect {
3327

3428
private final RedisTemplate<String, Object> redisTemplate;
3529

36-
@Before("@annotation(org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory)")
30+
@Async
31+
@AfterReturning("@annotation(org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory)")
3732
public void logPlaceCategory(JoinPoint joinPoint) {
38-
39-
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
40-
String[] paramNames = signature.getParameterNames();
33+
4134
Object[] args = joinPoint.getArgs();
4235

43-
List<Long> placeCategoryIds = IntStream.range(0, args.length)
44-
.mapToObj(i -> extractIdsFromParam(paramNames[i], args[i]))
45-
.flatMap(Collection::stream)
36+
List<String> keywords = Arrays.stream(args)
37+
.flatMap(arg -> extractKeywordsFromDTO(arg).stream())
4638
.toList();
4739

48-
savePlaceCategoryIds(placeCategoryIds);
40+
saveKeywords(keywords);
4941
}
5042

51-
// 1. 파라미터로부터 placeCategoryId 추출
52-
private List<Long> extractIdsFromParam(String paramName, Object arg) {
53-
54-
if (paramName.contains("placeCategoryId") && arg instanceof Long id) {
55-
return List.of(id);
56-
}
57-
58-
if (paramName.contains("placeCategoryId") && arg instanceof List<?> list) {
59-
return list.stream()
60-
.filter(Long.class::isInstance)
61-
.map(Long.class::cast)
62-
.toList();
63-
}
64-
65-
// DTO로 간주하고 추출 시도
66-
return extractIdsFromDTO(arg);
67-
}
68-
69-
// 2. DTO로부터 placeCategoryId 추출
70-
private List<Long> extractIdsFromDTO(Object dto) {
43+
private List<String> extractKeywordsFromDTO(Object dto) {
7144

7245
if (dto == null) return Collections.emptyList();
73-
List<Long> ids = new ArrayList<>();
46+
List<String> keywords = new ArrayList<>();
7447

75-
// DTO에 정의된 필드에 접근
7648
for (Field field : dto.getClass().getDeclaredFields()) {
77-
if (!field.getName().contains("placeCategoryId")) continue;
49+
if (!field.getName().contains("userPreferredKeywords")) continue;
7850

79-
field.setAccessible(true); // private 필드 접근 설정
51+
field.setAccessible(true);
8052
try {
81-
Object value = field.get(dto); // 값 추출
82-
if (value instanceof Long placeCategoryId) {
83-
ids.add(placeCategoryId);
84-
}
85-
else if (value instanceof List<?> list) {
86-
for (Object id : list) {
87-
if (id instanceof Long placeCategoryId) {
88-
ids.add(placeCategoryId);
53+
Object value = field.get(dto);
54+
if (value instanceof List<?> list) {
55+
for (Object keyword : list) {
56+
if (keyword instanceof String str) {
57+
keywords.add(str);
8958
}
9059
}
9160
}
9261
} catch (Exception e) {
93-
log.warn("DTO에서 placeCategoryId 추출 실패");
62+
log.warn("[LogPlaceCategoryAspect] DTO에서 userPreferredKeywords 추출 실패", e);
9463
}
9564
}
96-
return ids;
65+
return keywords;
9766
}
9867

99-
private void savePlaceCategoryIds(List<Long> placeCategoryIds) {
100-
if (placeCategoryIds.isEmpty()) return;
68+
private void saveKeywords(List<String> keywords) {
69+
if (keywords.isEmpty()) return;
10170

102-
LocalDateTime now = LocalDateTime.now();
103-
String today = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
104-
String redisKey = "log:place-category:" + today;
71+
LocalDate now = LocalDate.now();
72+
String formattedDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
73+
String redisKey = "log:user-preferred-keywords:" + formattedDate;
10574

106-
// ZSET - 카테고리 별 검색 횟수 기록
107-
placeCategoryIds
108-
.forEach(id -> redisTemplate.opsForZSet().incrementScore(redisKey, id, 1));
75+
keywords.forEach(keyword ->
76+
redisTemplate.opsForZSet().incrementScore(redisKey, keyword, 1)
77+
);
10978

110-
// TTL - 이번 주까지로 설정
79+
// TTL 설정
11180
Long expire = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS);
11281
if (expire == null || expire <= 0) {
113-
LocalDateTime endOfWeek = now.with(DayOfWeek.SUNDAY).with(LocalTime.MAX);
114-
Duration duration = Duration.between(now, endOfWeek);
115-
redisTemplate.expire(redisKey, duration.getSeconds(), TimeUnit.SECONDS);
82+
redisTemplate.expire(redisKey, 1, TimeUnit.HOURS);
11683
}
84+
85+
log.info("[LogPlaceCategoryAspect] 키워드 로그 임시 저장 완료");
11786
}
11887
}

src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.time.LocalDate;
44
import java.util.Comparator;
55
import java.util.List;
6+
import java.util.Map;
7+
import java.util.stream.Collectors;
68

79
import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory;
810
import org.withtime.be.withtimebe.domain.log.placecategorylog.dto.PlaceCategoryLogResponseDTO;
@@ -12,27 +14,34 @@ public class PlaceCategoryLogConverter {
1214

1315
public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList toWeeklyPlaceCategoryLogList(List<PlaceCategoryLog> placeCategoryLogList) {
1416

15-
List<PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog> weeklyPlaceCategoryLogList = placeCategoryLogList.stream()
16-
.sorted(Comparator.comparing(PlaceCategoryLog::getCount).reversed()) // count 기준 내림차순
17-
.map(PlaceCategoryLogConverter::toWeeklyPlaceCategoryLog)
17+
// 1. 키워드 별 count 합산
18+
Map<String, Integer> countPerKeyword = placeCategoryLogList.stream()
19+
.collect(Collectors.groupingBy(
20+
PlaceCategoryLog::getPlaceCategoryLabel,
21+
Collectors.summingInt(PlaceCategoryLog::getCount)
22+
));
23+
24+
// 2. DTO 변환
25+
List<PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog> weeklyPlaceCategoryLogList = countPerKeyword.entrySet().stream()
26+
.map((entry) -> toWeeklyPlaceCategoryLog(entry.getKey(), entry.getValue()))
27+
.sorted(Comparator.comparing(PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog::count).reversed()) // count 기준 내림차순
1828
.toList();
1929

2030
return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList.builder()
2131
.placeCategoryLogList(weeklyPlaceCategoryLogList)
2232
.build();
2333
}
2434

25-
public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(PlaceCategoryLog placeCategoryLog) {
35+
public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(String placeCategoryLabel, Integer count) {
2636
return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog.builder()
27-
.placeCategoryLabel(placeCategoryLog.getPlaceCategoryLabel())
28-
.count(placeCategoryLog.getCount())
37+
.placeCategoryLabel(placeCategoryLabel)
38+
.count(count)
2939
.build();
3040
}
3141

32-
public static PlaceCategoryLog toPlaceCategoryLog(PlaceCategory placeCategory, Integer count, LocalDate date) {
42+
public static PlaceCategoryLog toPlaceCategoryLog(String placeCategoryLabel, Integer count, LocalDate date) {
3343
return PlaceCategoryLog.builder()
34-
.placeCategoryId(placeCategory.getId())
35-
.placeCategoryLabel(placeCategory.getLabel())
44+
.placeCategoryLabel(placeCategoryLabel)
3645
.count(count)
3746
.date(date)
3847
.build();

src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public class PlaceCategoryLog extends BaseEntity {
2222
@Id
2323
private String id;
2424

25-
private Long placeCategoryId;
2625
private String placeCategoryLabel;
2726
private LocalDate date;
2827
private Integer count;

src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88

99
public interface PlaceCategoryLogRepository extends MongoRepository<PlaceCategoryLog, String> {
1010
List<PlaceCategoryLog> findByDateBetween(LocalDate startDate, LocalDate endDate);
11-
List<PlaceCategoryLog> findByPlaceCategoryIdInAndDate(List<Long> placeCategoryIds, LocalDate date);
11+
List<PlaceCategoryLog> findByDateAndPlaceCategoryLabelIn(LocalDate date, List<String> placeCategoryLabel);
1212
}

0 commit comments

Comments
 (0)