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 @@ -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;
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,11 +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);
Expand Down Expand Up @@ -118,7 +118,6 @@ public RecommendedCourseResult createDateCourse(DateRequestDTO.CreateDateCourse
}

// ───────────────────────── 내부 로직 ─────────────────────────

/** 주소 키워드 토큰으로 후보 조회 + ID 기준 중복 제거(순서 보존) */
private List<DatePlace> collectCandidatesByAddressTokens(List<String> tokens) {
if (tokens == null || tokens.isEmpty()) return List.of();
Expand Down Expand Up @@ -287,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
Expand All @@ -304,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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,30 @@
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
public class DateQueryServiceImpl implements DateQueryService {

private final DateCourseRepository dateCourseRepository;

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

@LogPlaceCategory
@GetPoint(action = PointAction.VIEW_DATE_COURSE)
public Page<DateCourse> findDateCourseBookmarks(DateRequestDTO.DateCourseSearchCond dateCourseSearchCond, Pageable pageable, Member member){
return dateCourseRepository.searchDateCourseBookmarkByMemberAndApplyPage(dateCourseSearchCond, member, pageable);
}

}
Original file line number Diff line number Diff line change
@@ -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;

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

private final RedisTemplate<String, Object> 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<Long> placeCategoryIds = IntStream.range(0, args.length)
.mapToObj(i -> extractIdsFromParam(paramNames[i], args[i]))
.flatMap(Collection::stream)
List<String> keywords = Arrays.stream(args)
.flatMap(arg -> extractKeywordsFromDTO(arg).stream())
.toList();

savePlaceCategoryIds(placeCategoryIds);
saveKeywords(keywords);
}

// 1. 파라미터로부터 placeCategoryId 추출
private List<Long> 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<Long> extractIdsFromDTO(Object dto) {
private List<String> extractKeywordsFromDTO(Object dto) {

if (dto == null) return Collections.emptyList();
List<Long> ids = new ArrayList<>();
List<String> 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<Long> placeCategoryIds) {
if (placeCategoryIds.isEmpty()) return;
private void saveKeywords(List<String> 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] 키워드 로그 임시 저장 완료");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,27 +14,34 @@ public class PlaceCategoryLogConverter {

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

List<PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog> weeklyPlaceCategoryLogList = placeCategoryLogList.stream()
.sorted(Comparator.comparing(PlaceCategoryLog::getCount).reversed()) // count 기준 내림차순
.map(PlaceCategoryLogConverter::toWeeklyPlaceCategoryLog)
// 1. 키워드 별 count 합산
Map<String, Integer> countPerKeyword = placeCategoryLogList.stream()
.collect(Collectors.groupingBy(
PlaceCategoryLog::getPlaceCategoryLabel,
Collectors.summingInt(PlaceCategoryLog::getCount)
));

// 2. DTO 변환
List<PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog> weeklyPlaceCategoryLogList = countPerKeyword.entrySet().stream()
.map((entry) -> toWeeklyPlaceCategoryLog(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog::count).reversed()) // count 기준 내림차순
.toList();

return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList.builder()
.placeCategoryLogList(weeklyPlaceCategoryLogList)
.build();
}

public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(PlaceCategoryLog placeCategoryLog) {
public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(String placeCategoryLabel, Integer count) {
return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog.builder()
.placeCategoryLabel(placeCategoryLog.getPlaceCategoryLabel())
.count(placeCategoryLog.getCount())
.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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@

public interface PlaceCategoryLogRepository extends MongoRepository<PlaceCategoryLog, String> {
List<PlaceCategoryLog> findByDateBetween(LocalDate startDate, LocalDate endDate);
List<PlaceCategoryLog> findByPlaceCategoryIdInAndDate(List<Long> placeCategoryIds, LocalDate date);
List<PlaceCategoryLog> findByDateAndPlaceCategoryLabelIn(LocalDate date, List<String> placeCategoryLabel);
}
Loading