From c228805b66328f2ffc35718b21b4ab460322a4d1 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:14:27 +0900 Subject: [PATCH 01/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=AA=BD=EA=B3=A0?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 6555ead..7de79a9 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,9 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'com.h2database:h2' + + // MongoDB + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' } tasks.named('test') { From ae1fe12d3c2e84cde45bc225120964bcb5538b61 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:15:01 +0900 Subject: [PATCH 02/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=AA=BD=EA=B3=A0?= =?UTF-8?q?=20Config=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/WithTimeBeApplication.java | 11 +++++++++++ .../be/withtimebe/global/config/MongoConfig.java | 14 ++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java diff --git a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java index 11234d3..2dff454 100644 --- a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java +++ b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java @@ -2,9 +2,20 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @EnableJpaAuditing +@EnableJpaRepositories( + basePackages = {"org.withtime.be"}, + excludeFilters = @ComponentScan.Filter( + type = FilterType.REGEX, + pattern = "org\\.withtime\\.be\\.withtimebe\\.domain\\.log\\..*" + )) @SpringBootApplication public class WithTimeBeApplication { diff --git a/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java b/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java new file mode 100644 index 0000000..4cf3257 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java @@ -0,0 +1,14 @@ +package org.withtime.be.withtimebe.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + +import lombok.AllArgsConstructor; + +@Configuration +@AllArgsConstructor +@EnableMongoAuditing +@EnableMongoRepositories(basePackages = "org.withtime.be.withtimebe.domain.log") +public class MongoConfig { +} From ae94e8d4cae83219cea83c19ae1cd6ebe65dc73f Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:22:26 +0900 Subject: [PATCH 03/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=AA=BD=EA=B3=A0?= =?UTF-8?q?=20yml=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-develop.yml | 9 ++++++++- src/main/resources/application.yml | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 34e0df7..2f989f7 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -15,6 +15,8 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mongodb: + uri: ${MONGO_URI} mail: host: ${MAIL_SENDER_HOST} port: 587 @@ -121,4 +123,9 @@ scheduler: retention: short-term-days: 7 # 단기 예보 보관 기간 medium-term-days: 7 # 중기 예보 보관 기간 - recommendation-days: 30 # 추천 정보 보관 기간 \ No newline at end of file + recommendation-days: 30 # 추천 정보 보관 기간 + + logs: + place-category: + enabled: true + sync-cron: "0 */5 * * * *" # 5분마다 Redis -> Mongo 동기화 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ebb1f92..5ea55cc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,6 +15,8 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mongodb: + uri: ${MONGO_URI} mail: host: ${MAIL_SENDER_HOST} port: 587 @@ -121,4 +123,9 @@ scheduler: retention: short-term-days: 7 # 단기 예보 보관 기간 medium-term-days: 7 # 중기 예보 보관 기간 - recommendation-days: 30 # 추천 정보 보관 기간 \ No newline at end of file + recommendation-days: 30 # 추천 정보 보관 기간 + + logs: + place-category: + enabled: true + sync-cron: "0 */5 * * * *" # 5분마다 Redis -> Mongo 동기화 From 6e2ad39937b05adea878832d1483d0e7c56c1131 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:23:53 +0900 Subject: [PATCH 04/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20PlaceCategoryLog?= =?UTF-8?q?=20=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/log/model/PlaceCategoryLog.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java new file mode 100644 index 0000000..2c4aba9 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java @@ -0,0 +1,33 @@ +package org.withtime.be.withtimebe.domain.log.model; + +import java.time.LocalDate; + +import org.springframework.data.mongodb.core.mapping.Document; +import org.withtime.be.withtimebe.global.common.BaseEntity; + +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Document(collection = "place_category_logs") +public class PlaceCategoryLog extends BaseEntity { + + @Id + private String id; + + private Long placeCategoryId; + private String placeCategoryLabel; + private LocalDate date; + private Integer count; + + public void incrementCount(Integer count) { + this.count += count; + } +} From 8b75156f009bc5bddbb96bc9227a687be70e5187 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:26:29 +0900 Subject: [PATCH 05/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9D=B4=EB=B2=88?= =?UTF-8?q?=EC=A3=BC=20=EC=9D=B8=EA=B8=B0=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/PlaceCategoryRepository.java | 7 ++++ .../PlaceCategoryLogQueryController.java | 36 +++++++++++++++++++ .../converter/PlaceCategoryLogConverter.java | 29 +++++++++++++++ .../log/dto/PlaceCategoryLogResponseDTO.java | 19 ++++++++++ .../PlaceCategoryLogRepository.java | 14 ++++++++ .../query/PlaceCategoryLogQueryService.java | 9 +++++ .../PlaceCategoryLogQueryServiceImpl.java | 33 +++++++++++++++++ 7 files changed, 147 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/date/repository/PlaceCategoryRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/repository/PlaceCategoryRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/PlaceCategoryRepository.java new file mode 100644 index 0000000..135ae95 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/PlaceCategoryRepository.java @@ -0,0 +1,7 @@ +package org.withtime.be.withtimebe.domain.date.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory; + +public interface PlaceCategoryRepository extends JpaRepository { +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java new file mode 100644 index 0000000..1562807 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java @@ -0,0 +1,36 @@ +package org.withtime.be.withtimebe.domain.log.controller; + +import java.util.List; + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.log.converter.PlaceCategoryLogConverter; +import org.withtime.be.withtimebe.domain.log.dto.PlaceCategoryLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.service.query.PlaceCategoryLogQueryService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/logs/keyword") +public class PlaceCategoryLogQueryController { + + private final PlaceCategoryLogQueryService placeCategoryLogQueryService; + + @Operation(summary = "이번 주 인기 키워드 조회 API by 피우 [Only Admin]", description = "이번 주 많이 찾은 키워드를 조회하는 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @GetMapping("/weekly") + public DefaultResponse findWeeklyPlaceCategoryLogList() { + List result = placeCategoryLogQueryService.findWeeklyPlaceCategoryLogList(); + PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList response = PlaceCategoryLogConverter.toWeeklyPlaceCategoryLogList(result); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java new file mode 100644 index 0000000..12c7aef --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java @@ -0,0 +1,29 @@ +package org.withtime.be.withtimebe.domain.log.converter; + +import java.util.Comparator; +import java.util.List; + +import org.withtime.be.withtimebe.domain.log.dto.PlaceCategoryLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; + +public class PlaceCategoryLogConverter { + + public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList toWeeklyPlaceCategoryLogList(List placeCategoryLogList) { + + List weeklyPlaceCategoryLogList = placeCategoryLogList.stream() + .sorted(Comparator.comparing(PlaceCategoryLog::getCount).reversed()) // count 기준 내림차순 + .map(PlaceCategoryLogConverter::toWeeklyPlaceCategoryLog) + .toList(); + + return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLogList.builder() + .placeCategoryLogList(weeklyPlaceCategoryLogList) + .build(); + } + + public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCategoryLog(PlaceCategoryLog placeCategoryLog) { + return PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog.builder() + .placeCategoryLabel(placeCategoryLog.getPlaceCategoryLabel()) + .count(placeCategoryLog.getCount()) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java new file mode 100644 index 0000000..f4b3d04 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java @@ -0,0 +1,19 @@ +package org.withtime.be.withtimebe.domain.log.dto; + +import java.util.List; + +import lombok.Builder; + +public class PlaceCategoryLogResponseDTO { + + @Builder + public record WeeklyPlaceCategoryLogList( + List placeCategoryLogList + ) {} + + @Builder + public record WeeklyPlaceCategoryLog( + String placeCategoryLabel, + Integer count + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java new file mode 100644 index 0000000..0e45cd1 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java @@ -0,0 +1,14 @@ +package org.withtime.be.withtimebe.domain.log.repository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; + +public interface PlaceCategoryLogRepository extends MongoRepository { + List findByDateBetween(LocalDate startDate, LocalDate endDate); + List findByPlaceCategoryIdInAndDate(List placeCategoryIds, LocalDate date); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java new file mode 100644 index 0000000..2ae2b54 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.domain.log.service.query; + +import java.util.List; + +import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; + +public interface PlaceCategoryLogQueryService { + List findWeeklyPlaceCategoryLogList(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java new file mode 100644 index 0000000..77eab52 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java @@ -0,0 +1,33 @@ +package org.withtime.be.withtimebe.domain.log.service.query; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.repository.PlaceCategoryLogRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PlaceCategoryLogQueryServiceImpl implements PlaceCategoryLogQueryService { + + private final PlaceCategoryLogRepository placeCategoryLogRepository; + + @Override + public List findWeeklyPlaceCategoryLogList() { + + LocalDate now = LocalDate.now(); + + LocalDate startOfWeek = now.with(DayOfWeek.MONDAY); + LocalDate endOfWeek = now.with(DayOfWeek.SUNDAY); + + return placeCategoryLogRepository.findByDateBetween(startOfWeek, endOfWeek); + } +} From 35e360258485e3de25986292dc28e77901bba1cb Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:26:54 +0900 Subject: [PATCH 06/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9D=B4=EB=B2=88?= =?UTF-8?q?=EC=A3=BC=20=EC=9D=B8=EA=B8=B0=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A7=91=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/log/annotation/LogPlaceCategory.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java b/src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java new file mode 100644 index 0000000..338622b --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java @@ -0,0 +1,13 @@ +package org.withtime.be.withtimebe.domain.log.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LogPlaceCategory { +} From 902e9d6011f025a816ec713b809b017bae194782 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:27:48 +0900 Subject: [PATCH 07/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=A6=AC=ED=94=8C?= =?UTF-8?q?=EB=9E=99=EC=85=98=20=EA=B8=B0=EB=B0=98=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=88=98=EC=A7=91=20AOP=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/aop/LogPlaceCategoryAspect.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java b/src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java new file mode 100644 index 0000000..9f24542 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java @@ -0,0 +1,118 @@ +package org.withtime.be.withtimebe.domain.log.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.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.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class LogPlaceCategoryAspect { + + private final RedisTemplate redisTemplate; + + @Before("@annotation(org.withtime.be.withtimebe.domain.log.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) + .toList(); + + savePlaceCategoryIds(placeCategoryIds); + } + + // 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) { + + if (dto == null) return Collections.emptyList(); + List ids = new ArrayList<>(); + + // DTO에 정의된 필드에 접근 + for (Field field : dto.getClass().getDeclaredFields()) { + if (!field.getName().contains("placeCategoryId")) continue; + + field.setAccessible(true); // private 필드 접근 설정 + 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); + } + } + } + } catch (Exception e) { + log.warn("DTO에서 placeCategoryId 추출 실패"); + } + } + return ids; + } + + private void savePlaceCategoryIds(List placeCategoryIds) { + if (placeCategoryIds.isEmpty()) return; + + LocalDateTime now = LocalDateTime.now(); + String today = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String redisKey = "log:place-category:" + today; + + // ZSET - 카테고리 별 검색 횟수 기록 + placeCategoryIds + .forEach(id -> redisTemplate.opsForZSet().incrementScore(redisKey, id, 1)); + + // 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); + } + } +} From 3e13c50cc6c33cae1fee2832d6e19e5c7279b1f7 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:28:37 +0900 Subject: [PATCH 08/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EC=A7=91=20=EC=A0=95=EB=B3=B4=20DB?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/PlaceCategoryLogScheduler.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java new file mode 100644 index 0000000..b66b34c --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java @@ -0,0 +1,99 @@ +package org.withtime.be.withtimebe.domain.log.scheduler; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory; +import org.withtime.be.withtimebe.domain.date.repository.PlaceCategoryRepository; +import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.repository.PlaceCategoryLogRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(name = "scheduler.logs.place-category.enabled", havingValue = "true") +public class PlaceCategoryLogScheduler { + + private final RedisTemplate redisTemplate; + private final PlaceCategoryLogRepository placeCategoryLogRepository; + private final PlaceCategoryRepository placeCategoryRepository; + + @Scheduled(cron = "${scheduler.logs.place-category.sync-cron}") // 매 5분마다 + public void syncPlaceCategoryLogsToDB() { + + // 현재 날짜 및 레디스 키 생성 + LocalDate now = LocalDate.now(); + String formattedDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String redisKey = "log:place-category:" + formattedDate; + + // ZSET 추출 + Set> zSet = + redisTemplate.opsForZSet().rangeWithScores(redisKey, 0, -1); + + // 없는 경우 Skip + if (zSet == null || zSet.isEmpty()) + return; + + // ZSET 데이터 가공 + Map placeCategoryScoreMap = zSet.stream() + .collect(Collectors.toMap( + tuple -> Long.valueOf(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(); + 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 = PlaceCategoryLog.builder() + .placeCategoryId(placeCategory.getId()) + .placeCategoryLabel(placeCategory.getLabel()) + .count(score) + .date(now) + .build(); + placeCategoryLogList.add(newPlaceCategoryLog); + } + } + } + + // 다큐먼트를 DB에 반영 + if (!placeCategoryLogList.isEmpty()) { + placeCategoryLogRepository.saveAll(placeCategoryLogList); + redisTemplate.delete(redisKey); + log.info("[PlaceCategoryLogScheduler] 카테고리 로그 저장 완료, 개수 : {}", placeCategoryLogList.size()); + } + } +} From bfc227d2983de202b850ca6879fa564e5dad9b51 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:32:45 +0900 Subject: [PATCH 09/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20JVM=20=EB=94=94?= =?UTF-8?q?=ED=8F=B4=ED=8A=B8=20TimeZone=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/withtime/be/withtimebe/WithTimeBeApplication.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java index 2dff454..847d480 100644 --- a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java +++ b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java @@ -1,5 +1,7 @@ package org.withtime.be.withtimebe; +import java.util.TimeZone; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; @@ -8,6 +10,8 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.scheduling.annotation.EnableScheduling; +import jakarta.annotation.PostConstruct; + @EnableScheduling @EnableJpaAuditing @EnableJpaRepositories( @@ -23,4 +27,6 @@ public static void main(String[] args) { SpringApplication.run(WithTimeBeApplication.class, args); } + @PostConstruct + public void init() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } // JVM 기본 TimeZone 설정 } From 8a2c873886f8c0fee0681861f3fa61cdf88212c4 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:36:33 +0900 Subject: [PATCH 10/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20KST=20UTC=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=BB=A8=EB=B2=84=ED=84=B0=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=8F=20=5Fclass=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtimebe/global/config/MongoConfig.java | 38 +++++++++ .../global/converter/MongoConverters.java | 78 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java diff --git a/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java b/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java index 4cf3257..a0d7b08 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java @@ -1,8 +1,19 @@ package org.withtime.be.withtimebe.global.config; +import java.util.List; + +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.config.EnableMongoAuditing; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.withtime.be.withtimebe.global.converter.MongoConverters; import lombok.AllArgsConstructor; @@ -11,4 +22,31 @@ @EnableMongoAuditing @EnableMongoRepositories(basePackages = "org.withtime.be.withtimebe.domain.log") public class MongoConfig { + + // KST <-> UTC 커스텀 컨버터 빈 등록 + @Bean + public MongoCustomConversions mongoCustomConversions() { + return new MongoCustomConversions(List.of( + new MongoConverters.LocalDateToDateKstConverter(), + new MongoConverters.LocalTimeToDateKstConverter(), + new MongoConverters.LocalDateTimeToDateKstConverter(), + new MongoConverters.DateToLocalDateKstConverter(), + new MongoConverters.DateToLocalTimeKstConverter(), + new MongoConverters.DateToLocalDateTimeKstConverter() + )); + } + + // 커스텀 컨버터 등록 및 _class 필드 제거 + @Bean + public MappingMongoConverter mappingMongoConverter( + MongoDatabaseFactory mongoDatabaseFactory, + MongoMappingContext mongoMappingContext, + MongoCustomConversions conversions + ) { + DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory); + MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext); + converter.setTypeMapper(new DefaultMongoTypeMapper(null)); + converter.setCustomConversions(conversions); + return converter; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java b/src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java new file mode 100644 index 0000000..8ea6e32 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java @@ -0,0 +1,78 @@ +package org.withtime.be.withtimebe.global.converter; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.Date; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; + +public class MongoConverters { + + @WritingConverter + public static class LocalDateToDateKstConverter implements Converter { + @Override + public Date convert(LocalDate source) { + return Timestamp.valueOf(source.atStartOfDay().plusHours(9)); + } + } + + @WritingConverter + public static class LocalTimeToDateKstConverter implements Converter { + @Override + public Date convert(LocalTime source) { + LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(), source).plusHours(9); + return Timestamp.valueOf(localDateTime); + } + } + + @WritingConverter + public static class LocalDateTimeToDateKstConverter implements Converter { + @Override + public Date convert(LocalDateTime source) { + return Timestamp.valueOf(source.plusHours(9)); + } + } + + @ReadingConverter + public static class DateToLocalDateKstConverter implements Converter { + @Override + public LocalDate convert(Date source) { + return source.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .minusHours(9) + .toLocalDate(); + } + } + + @ReadingConverter + public static class DateToLocalTimeKstConverter implements Converter { + + @Override + public LocalTime convert(Date source) { + return source.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .minusHours(9) + .toLocalTime(); + } + } + + @ReadingConverter + public static class DateToLocalDateTimeKstConverter implements Converter { + @Override + public LocalDateTime convert(Date source) { + return source.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .minusHours(9); + } + } +} + + From bbac0773cc7b813315744957a65b7737382a708f Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:38:34 +0900 Subject: [PATCH 11/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=8B=9C=ED=81=90?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20URL=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtime/be/withtimebe/global/security/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java index fec718b..f95bb53 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java @@ -53,6 +53,7 @@ public class SecurityConfig { API_PREFIX + "/notices/**", API_PREFIX + "/oauth2/**", API_PREFIX + "/faqs/**", + API_PREFIX + "/logs/keyword/**", "/oauth2/authorization/**", "/swagger-ui/**", "/swagger-resources/**", From f9fc78fa127841472a8d390d17b96ebe60a6ddae Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 22:42:37 +0900 Subject: [PATCH 12/28] =?UTF-8?q?=F0=9F=90=9B=20fix:=20placeCategoryLog=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../annotation/LogPlaceCategory.java | 2 +- .../aop/LogPlaceCategoryAspect.java | 4 ++-- .../controller/PlaceCategoryLogQueryController.java | 10 +++++----- .../converter/PlaceCategoryLogConverter.java | 6 +++--- .../dto/PlaceCategoryLogResponseDTO.java | 2 +- .../{ => placecategorylog}/model/PlaceCategoryLog.java | 2 +- .../repository/PlaceCategoryLogRepository.java | 6 ++---- .../scheduler/PlaceCategoryLogScheduler.java | 6 +++--- .../service/query/PlaceCategoryLogQueryService.java | 9 +++++++++ .../query/PlaceCategoryLogQueryServiceImpl.java | 8 +++----- .../service/query/PlaceCategoryLogQueryService.java | 9 --------- 11 files changed, 30 insertions(+), 34 deletions(-) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/annotation/LogPlaceCategory.java (80%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/aop/LogPlaceCategoryAspect.java (95%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/controller/PlaceCategoryLogQueryController.java (75%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/converter/PlaceCategoryLogConverter.java (79%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/dto/PlaceCategoryLogResponseDTO.java (81%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/model/PlaceCategoryLog.java (91%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/repository/PlaceCategoryLogRepository.java (68%) rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/scheduler/PlaceCategoryLogScheduler.java (93%) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryService.java rename src/main/java/org/withtime/be/withtimebe/domain/log/{ => placecategorylog}/service/query/PlaceCategoryLogQueryServiceImpl.java (73%) delete mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/annotation/LogPlaceCategory.java similarity index 80% rename from src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/annotation/LogPlaceCategory.java index 338622b..a9b461d 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/annotation/LogPlaceCategory.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/annotation/LogPlaceCategory.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.log.annotation; +package org.withtime.be.withtimebe.domain.log.placecategorylog.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java similarity index 95% rename from src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java index 9f24542..78585c0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/aop/LogPlaceCategoryAspect.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.log.aop; +package org.withtime.be.withtimebe.domain.log.placecategorylog.aop; import java.lang.reflect.Field; import java.time.DayOfWeek; @@ -33,7 +33,7 @@ public class LogPlaceCategoryAspect { private final RedisTemplate redisTemplate; - @Before("@annotation(org.withtime.be.withtimebe.domain.log.annotation.LogPlaceCategory)") + @Before("@annotation(org.withtime.be.withtimebe.domain.log.placecategorylog.annotation.LogPlaceCategory)") public void logPlaceCategory(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java similarity index 75% rename from src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java index 1562807..00509a1 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/PlaceCategoryLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/controller/PlaceCategoryLogQueryController.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.log.controller; +package org.withtime.be.withtimebe.domain.log.placecategorylog.controller; import java.util.List; @@ -6,10 +6,10 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.withtime.be.withtimebe.domain.log.converter.PlaceCategoryLogConverter; -import org.withtime.be.withtimebe.domain.log.dto.PlaceCategoryLogResponseDTO; -import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; -import org.withtime.be.withtimebe.domain.log.service.query.PlaceCategoryLogQueryService; +import org.withtime.be.withtimebe.domain.log.placecategorylog.converter.PlaceCategoryLogConverter; +import org.withtime.be.withtimebe.domain.log.placecategorylog.dto.PlaceCategoryLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.placecategorylog.service.query.PlaceCategoryLogQueryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java similarity index 79% rename from src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java index 12c7aef..b973742 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/PlaceCategoryLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/converter/PlaceCategoryLogConverter.java @@ -1,10 +1,10 @@ -package org.withtime.be.withtimebe.domain.log.converter; +package org.withtime.be.withtimebe.domain.log.placecategorylog.converter; import java.util.Comparator; import java.util.List; -import org.withtime.be.withtimebe.domain.log.dto.PlaceCategoryLogResponseDTO; -import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.placecategorylog.dto.PlaceCategoryLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; public class PlaceCategoryLogConverter { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java similarity index 81% rename from src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java index f4b3d04..30194e2 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/dto/PlaceCategoryLogResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/dto/PlaceCategoryLogResponseDTO.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.log.dto; +package org.withtime.be.withtimebe.domain.log.placecategorylog.dto; import java.util.List; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java similarity index 91% rename from src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java index 2c4aba9..ddb999a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/model/PlaceCategoryLog.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/model/PlaceCategoryLog.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.log.model; +package org.withtime.be.withtimebe.domain.log.placecategorylog.model; import java.time.LocalDate; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java similarity index 68% rename from src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java index 0e45cd1..e7d48ec 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/PlaceCategoryLogRepository.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/repository/PlaceCategoryLogRepository.java @@ -1,12 +1,10 @@ -package org.withtime.be.withtimebe.domain.log.repository; +package org.withtime.be.withtimebe.domain.log.placecategorylog.repository; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; import org.springframework.data.mongodb.repository.MongoRepository; -import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; public interface PlaceCategoryLogRepository extends MongoRepository { List findByDateBetween(LocalDate startDate, LocalDate endDate); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java similarity index 93% rename from src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java index b66b34c..617658b 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/PlaceCategoryLogScheduler.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/scheduler/PlaceCategoryLogScheduler.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.log.scheduler; +package org.withtime.be.withtimebe.domain.log.placecategorylog.scheduler; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -17,8 +17,8 @@ import org.springframework.stereotype.Component; import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory; import org.withtime.be.withtimebe.domain.date.repository.PlaceCategoryRepository; -import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; -import org.withtime.be.withtimebe.domain.log.repository.PlaceCategoryLogRepository; +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.placecategorylog.repository.PlaceCategoryLogRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryService.java new file mode 100644 index 0000000..cb5c910 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryService.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.domain.log.placecategorylog.service.query; + +import java.util.List; + +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; + +public interface PlaceCategoryLogQueryService { + List findWeeklyPlaceCategoryLogList(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImpl.java similarity index 73% rename from src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java rename to src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImpl.java index 77eab52..88a4f4b 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImpl.java @@ -1,15 +1,13 @@ -package org.withtime.be.withtimebe.domain.log.service.query; +package org.withtime.be.withtimebe.domain.log.placecategorylog.service.query; import java.time.DayOfWeek; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; -import org.withtime.be.withtimebe.domain.log.repository.PlaceCategoryLogRepository; +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.placecategorylog.repository.PlaceCategoryLogRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java deleted file mode 100644 index 2ae2b54..0000000 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/PlaceCategoryLogQueryService.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.withtime.be.withtimebe.domain.log.service.query; - -import java.util.List; - -import org.withtime.be.withtimebe.domain.log.model.PlaceCategoryLog; - -public interface PlaceCategoryLogQueryService { - List findWeeklyPlaceCategoryLogList(); -} From 716c3169bf3dda7e43c16c9d1bf25e874e2cca1c Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 23:45:51 +0900 Subject: [PATCH 13/28] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=BB=A8=EB=B2=84?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EB=8B=A4=ED=81=90?= =?UTF-8?q?=EB=A8=BC=ED=8A=B8=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PlaceCategoryLogQueryController.java | 2 +- .../converter/PlaceCategoryLogConverter.java | 11 +++++++++++ .../scheduler/PlaceCategoryLogScheduler.java | 9 +++------ 3 files changed, 15 insertions(+), 7 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 00509a1..f3435a5 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 @@ -23,7 +23,7 @@ public class PlaceCategoryLogQueryController { private final PlaceCategoryLogQueryService placeCategoryLogQueryService; - @Operation(summary = "이번 주 인기 키워드 조회 API by 피우 [Only Admin]", description = "이번 주 많이 찾은 키워드를 조회하는 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "이번 주 인기 키워드 조회 API by 피우", description = "이번 주 많이 찾은 키워드를 조회하는 API입니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다.") }) 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 b973742..e4dc3b0 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 @@ -1,8 +1,10 @@ package org.withtime.be.withtimebe.domain.log.placecategorylog.converter; +import java.time.LocalDate; import java.util.Comparator; import java.util.List; +import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory; import org.withtime.be.withtimebe.domain.log.placecategorylog.dto.PlaceCategoryLogResponseDTO; import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; @@ -26,4 +28,13 @@ public static PlaceCategoryLogResponseDTO.WeeklyPlaceCategoryLog toWeeklyPlaceCa .count(placeCategoryLog.getCount()) .build(); } + + public static PlaceCategoryLog toPlaceCategoryLog(PlaceCategory placeCategory, Integer count, LocalDate date) { + return PlaceCategoryLog.builder() + .placeCategoryId(placeCategory.getId()) + .placeCategoryLabel(placeCategory.getLabel()) + .count(count) + .date(date) + .build(); + } } 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 617658b..698522a 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 @@ -17,6 +17,7 @@ import org.springframework.stereotype.Component; import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory; import org.withtime.be.withtimebe.domain.date.repository.PlaceCategoryRepository; +import org.withtime.be.withtimebe.domain.log.placecategorylog.converter.PlaceCategoryLogConverter; import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; import org.withtime.be.withtimebe.domain.log.placecategorylog.repository.PlaceCategoryLogRepository; @@ -78,12 +79,8 @@ public void syncPlaceCategoryLogsToDB() { Optional placeCategoryOptional = placeCategoryRepository.findById(placeCategoryId); if(placeCategoryOptional.isPresent()) { PlaceCategory placeCategory = placeCategoryOptional.get(); - PlaceCategoryLog newPlaceCategoryLog = PlaceCategoryLog.builder() - .placeCategoryId(placeCategory.getId()) - .placeCategoryLabel(placeCategory.getLabel()) - .count(score) - .date(now) - .build(); + PlaceCategoryLog newPlaceCategoryLog = PlaceCategoryLogConverter + .toPlaceCategoryLog(placeCategory, score, now); placeCategoryLogList.add(newPlaceCategoryLog); } } From 9c667d8806fb7c3214088e4fa8f7ca6c410a757b Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 23:59:01 +0900 Subject: [PATCH 14/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20DatePlaceLog=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/dateplacelog/entity/DatePlaceLog.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/entity/DatePlaceLog.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/entity/DatePlaceLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/entity/DatePlaceLog.java new file mode 100644 index 0000000..c18530a --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/entity/DatePlaceLog.java @@ -0,0 +1,27 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.entity; + +import java.time.LocalDate; + +import org.springframework.data.mongodb.core.mapping.Document; +import org.withtime.be.withtimebe.global.common.BaseEntity; + +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Document(collection = "date_place_logs") +public class DatePlaceLog extends BaseEntity { + + @Id + private String id; + + private LocalDate date; + private Long count; +} From f7f8e6f22298458a61f5d389ee18690ebd552996 Mon Sep 17 00:00:00 2001 From: pywoo Date: Sun, 27 Jul 2025 23:59:55 +0900 Subject: [PATCH 15/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9B=94=EB=B3=84?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=9E=A5=EC=86=8C=20=EC=88=98?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../date/repository/DatePlaceRepository.java | 7 +++ .../DatePlaceLogQueryController.java | 36 +++++++++++ .../converter/DatePlaceLogConverter.java | 39 ++++++++++++ .../dto/DatePlaceLogResponseDTO.java | 19 ++++++ .../repository/DatePlaceLogRepository.java | 7 +++ .../query/DatePlaceLogQueryService.java | 9 +++ .../query/DatePlaceLogQueryServiceImpl.java | 63 +++++++++++++++++++ 7 files changed, 180 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/controller/DatePlaceLogQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/repository/DatePlaceLogRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceRepository.java new file mode 100644 index 0000000..d6d9a9b --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceRepository.java @@ -0,0 +1,7 @@ +package org.withtime.be.withtimebe.domain.date.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.withtime.be.withtimebe.domain.date.entity.DatePlace; + +public interface DatePlaceRepository extends JpaRepository { +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/controller/DatePlaceLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/controller/DatePlaceLogQueryController.java new file mode 100644 index 0000000..c3a1c1d --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/controller/DatePlaceLogQueryController.java @@ -0,0 +1,36 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.controller; + +import java.util.List; + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.log.dateplacelog.converter.DatePlaceLogConverter; +import org.withtime.be.withtimebe.domain.log.dateplacelog.dto.DatePlaceLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; +import org.withtime.be.withtimebe.domain.log.dateplacelog.service.query.DatePlaceLogQueryService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/logs/dateplaces") +public class DatePlaceLogQueryController { + + private final DatePlaceLogQueryService datePlaceLogQueryService; + + @Operation(summary = "WithTime에 등록된 월별 데이트 장소 수 조회 API by 피우", description = "WithTime에 등록된 월별 데이트 장소를 조회하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @GetMapping("/monthly") + public DefaultResponse findMonthlyDatePlaceLogList() { + List result = datePlaceLogQueryService.findMonthlyDatePlaceLogList(); + DatePlaceLogResponseDTO.MonthlyDatePlaceLogList response = DatePlaceLogConverter.toMonthlyDatePlaceLogList(result); + return DefaultResponse.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java new file mode 100644 index 0000000..e2c65d5 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java @@ -0,0 +1,39 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.converter; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import org.withtime.be.withtimebe.domain.log.dateplacelog.dto.DatePlaceLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; + +public class DatePlaceLogConverter { + + public static DatePlaceLogResponseDTO.MonthlyDatePlaceLogList toMonthlyDatePlaceLogList(List datePlaceLogs) { + + List datePlaceLogList = datePlaceLogs.stream() + .map(DatePlaceLogConverter::toMonthlyDatePlaceLog) + .toList(); + + return DatePlaceLogResponseDTO.MonthlyDatePlaceLogList.builder() + .datePlaceLogList(datePlaceLogList) + .build(); + } + + public static DatePlaceLogResponseDTO.MonthlyDatePlaceLog toMonthlyDatePlaceLog(DatePlaceLog datePlaceLog) { + + Long month = (long)datePlaceLog.getDate().getMonthValue(); + + return DatePlaceLogResponseDTO.MonthlyDatePlaceLog.builder() + .month(month) + .count(datePlaceLog.getCount()) + .build(); + } + + public static DatePlaceLog toDatePlaceLog(LocalDate date, Long count) { + return DatePlaceLog.builder() + .date(date) + .count(count) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java new file mode 100644 index 0000000..0f9a669 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java @@ -0,0 +1,19 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.dto; + +import java.util.List; + +import lombok.Builder; + +public class DatePlaceLogResponseDTO { + + @Builder + public record MonthlyDatePlaceLogList( + List datePlaceLogList + ) {} + + @Builder + public record MonthlyDatePlaceLog( + Long month, + Long count + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/repository/DatePlaceLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/repository/DatePlaceLogRepository.java new file mode 100644 index 0000000..ca92174 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/repository/DatePlaceLogRepository.java @@ -0,0 +1,7 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.repository; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; + +public interface DatePlaceLogRepository extends MongoRepository { +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryService.java new file mode 100644 index 0000000..80d1f8e --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryService.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.service.query; + +import java.util.List; + +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; + +public interface DatePlaceLogQueryService { + List findMonthlyDatePlaceLogList(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImpl.java new file mode 100644 index 0000000..372fa40 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImpl.java @@ -0,0 +1,63 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.service.query; + +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.GroupOperation; +import org.springframework.data.mongodb.core.aggregation.ProjectionOperation; +import org.springframework.data.mongodb.core.aggregation.SortOperation; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DatePlaceLogQueryServiceImpl implements DatePlaceLogQueryService { + + private final MongoTemplate mongoTemplate; + + @Override + public List findMonthlyDatePlaceLogList() { + + // 1. 추출할 필드 정의 + ProjectionOperation projectToMonth = Aggregation.project() + .andExpression("date").as("date") + .andExpression("count").as("count") + .andExpression("year(date)").as("year") + .andExpression("month(date)").as("month"); + + // 2. date 기준 내림차순 정렬 + SortOperation sortByDateDesc = Aggregation.sort(Sort.Direction.DESC, "date"); + + // 3. 월별 groupBy, 이후 최신 다큐먼트 하나 선택 + GroupOperation groupByYearMonth = Aggregation.group("year", "month") + .first("date").as("date") + .first("count").as("count"); + + // 4. 결과 월별 오름차순 정렬 + SortOperation sortByYearMonthAsc = Aggregation.sort(Sort.by(Sort.Direction.ASC, "_id.year", "_id.month")); + + // 5. 파이프라인 생성 + Aggregation aggregation = Aggregation.newAggregation( + projectToMonth, + sortByDateDesc, + groupByYearMonth, + sortByYearMonthAsc + ); + + AggregationResults results = + mongoTemplate.aggregate( + aggregation, + "date_place_logs", + DatePlaceLog.class + ); + + return results.getMappedResults(); + } +} From 899b61f724110d976f3da3b64579e80b5b172b93 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 28 Jul 2025 00:00:34 +0900 Subject: [PATCH 16/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9D=BC=EB=B3=84?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=9E=A5=EC=86=8C=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=9F=AC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/DatePlaceLogScheduler.java | 38 +++++++++++++++++++ src/main/resources/application.yml | 3 ++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java new file mode 100644 index 0000000..449011c --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java @@ -0,0 +1,38 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.scheduler; + +import java.time.LocalDate; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.date.repository.DatePlaceRepository; +import org.withtime.be.withtimebe.domain.log.dateplacelog.converter.DatePlaceLogConverter; +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; +import org.withtime.be.withtimebe.domain.log.dateplacelog.repository.DatePlaceLogRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(name = "scheduler.logs.date-place.enabled", havingValue = "true") +public class DatePlaceLogScheduler { + + private final DatePlaceRepository datePlaceRepository; + private final DatePlaceLogRepository datePlaceLogRepository; + + @Scheduled(cron = "${scheduler.logs.date-place.sync-cron}") + @Transactional(readOnly = true) + public void syncPlaceCategoryLogsToDB() { + + LocalDate now = LocalDate.now(); + Long count = datePlaceRepository.count(); + + DatePlaceLog datePlaceLog = DatePlaceLogConverter.toDatePlaceLog(now, count); + datePlaceLogRepository.save(datePlaceLog); + + log.info("[DatePlaceLogScheduler] {} - 누적 데이트 장소 {}건 저장 완료", now, count); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5ea55cc..172d7a2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -129,3 +129,6 @@ scheduler: place-category: enabled: true sync-cron: "0 */5 * * * *" # 5분마다 Redis -> Mongo 동기화 + date-place: + enabled: true + sync-cron: "0 0 0 * * *" # 매일 자정마다 동기화 From fb15da8a88a9cbfd7053fdd1dd4d233c9c527f8d Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 28 Jul 2025 00:01:08 +0900 Subject: [PATCH 17/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=8B=9C=ED=81=90?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20URL=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtime/be/withtimebe/global/security/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java index f95bb53..cba321f 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/SecurityConfig.java @@ -54,6 +54,7 @@ public class SecurityConfig { API_PREFIX + "/oauth2/**", API_PREFIX + "/faqs/**", API_PREFIX + "/logs/keyword/**", + API_PREFIX + "/logs/dateplaces/**", "/oauth2/authorization/**", "/swagger-ui/**", "/swagger-resources/**", From f5c6f57baea7264e8b7d68d0324d01a8bc850249 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 28 Jul 2025 00:02:20 +0900 Subject: [PATCH 18/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20yml=EC=97=90=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-develop.yml | 5 ++++- src/main/resources/application.yml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 2f989f7..fb652af 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -128,4 +128,7 @@ scheduler: logs: place-category: enabled: true - sync-cron: "0 */5 * * * *" # 5분마다 Redis -> Mongo 동기화 \ No newline at end of file + sync-cron: "0 */5 * * * *" # 5분마다 Redis -> Mongo 동기화 + date-place: + enabled: true + sync-cron: "0 0 0 * * *" # 매일 자정마다 동기화 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 172d7a2..4cc8be9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -131,4 +131,4 @@ scheduler: sync-cron: "0 */5 * * * *" # 5분마다 Redis -> Mongo 동기화 date-place: enabled: true - sync-cron: "0 0 0 * * *" # 매일 자정마다 동기화 + sync-cron: "0 0 0 * * *" # 매일 자정마다 동기화 \ No newline at end of file From a10dedd6505de84532cb6c2b17386e1f8761f99c Mon Sep 17 00:00:00 2001 From: pywoo Date: Tue, 29 Jul 2025 16:06:43 +0900 Subject: [PATCH 19/28] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A5=B4=EB=9F=AC=20=EC=8B=9C=EA=B0=84=20=EC=98=A4=EC=B0=A8=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/dateplacelog/scheduler/DatePlaceLogScheduler.java | 3 ++- .../placecategorylog/scheduler/PlaceCategoryLogScheduler.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java index 449011c..75911f1 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/scheduler/DatePlaceLogScheduler.java @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.log.dateplacelog.scheduler; import java.time.LocalDate; +import java.time.LocalDateTime; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; @@ -27,7 +28,7 @@ public class DatePlaceLogScheduler { @Transactional(readOnly = true) public void syncPlaceCategoryLogsToDB() { - LocalDate now = LocalDate.now(); + LocalDate now = LocalDate.from(LocalDateTime.now().minusMinutes(1)); Long count = datePlaceRepository.count(); DatePlaceLog datePlaceLog = DatePlaceLogConverter.toDatePlaceLog(now, 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 698522a..2c63abe 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 @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.log.placecategorylog.scheduler; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @@ -38,7 +39,7 @@ public class PlaceCategoryLogScheduler { public void syncPlaceCategoryLogsToDB() { // 현재 날짜 및 레디스 키 생성 - LocalDate now = LocalDate.now(); + LocalDate now = LocalDate.from(LocalDateTime.now().minusMinutes(1)); String formattedDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd")); String redisKey = "log:place-category:" + formattedDate; From 89c75110f59746f1ee0f50c92f0f98af0d6fe1b8 Mon Sep 17 00:00:00 2001 From: pywoo Date: Tue, 29 Jul 2025 18:03:52 +0900 Subject: [PATCH 20/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20LogPlaceCategoryAO?= =?UTF-8?q?P=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aop/LogPlaceCategoryAspectTest.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspectTest.java diff --git a/src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspectTest.java b/src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspectTest.java new file mode 100644 index 0000000..b8c87e6 --- /dev/null +++ b/src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/aop/LogPlaceCategoryAspectTest.java @@ -0,0 +1,161 @@ +package org.withtime.be.withtimebe.domain.log.placecategorylog.aop; + +import static org.mockito.BDDMockito.*; + +import java.util.List; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[AOP] LogPlaceCategoryAspect 단위 테스트") +class LogPlaceCategoryAspectTest { + + @InjectMocks + private LogPlaceCategoryAspect aspect; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ZSetOperations zSetOperations; + + // JoinPoint 생성 메서드 + private JoinPoint createJoinPoint(String[] paramNames, Object[] args) { + JoinPoint joinPoint = mock(JoinPoint.class); + MethodSignature methodSignature = mock(MethodSignature.class); + + given(joinPoint.getSignature()).willReturn(methodSignature); + given(joinPoint.getArgs()).willReturn(args); + given(methodSignature.getParameterNames()).willReturn(paramNames); + + return joinPoint; + } + + @Nested + @DisplayName("logPlaceCategory()") + class LogPlaceCategoryTest { + + @Test + @DisplayName("'placeCategoryId'가 단일 인자로 들어오면, Redis에 ZSET으로 기록된다.") + void single_placeCategoryId_param_test() { + // given + Long expectedId = 1L; + Integer expectedMethodCall = 1; + + JoinPoint joinPoint = createJoinPoint( + new String[] {"placeCategoryId", "anotherId", "anotherDTO"}, + new Object[] {expectedId, 1L, new Object()} + ); + + given(redisTemplate.opsForZSet()).willReturn(zSetOperations); + given(redisTemplate.getExpire(anyString(), any())).willReturn(-1L); + + // when + aspect.logPlaceCategory(joinPoint); + + // then + verify(zSetOperations, times(expectedMethodCall)).incrementScore(anyString(), eq(expectedId), eq(1.0)); + verify(redisTemplate, times(expectedMethodCall)).expire(anyString(), anyLong(), any()); + } + + @Test + @DisplayName("'placeCategoryId'가 List로 들어오면, Redis에 ZSET으로 기록된다.") + void list_placeCategoryId_param_test() { + // given + List expectedIdList = List.of(1L, 2L, 3L); + Integer expectedMethodCall = 1; + + JoinPoint joinPoint = createJoinPoint( + new String[] {"placeCategoryIdList", "anotherDTO", "anotherIdList"}, + new Object[] {expectedIdList, new Object(), List.of(1L, 2L)} + ); + + given(redisTemplate.opsForZSet()).willReturn(zSetOperations); + given(redisTemplate.getExpire(anyString(), any())).willReturn(-1L); + + // when + aspect.logPlaceCategory(joinPoint); + + // then + expectedIdList + .forEach(id -> verify(zSetOperations, times(expectedMethodCall)).incrementScore(anyString(), eq(id), eq(1.0))); + verify(redisTemplate, times(expectedMethodCall)).expire(anyString(), anyLong(), any()); + } + + @Test + @DisplayName("'placeCategoryId'가 DTO 내부 필드에 있으면 Redis에 기록된다.") + void dto_placeCategoryId_field_test() { + // given + Long expectedId = 1L; + Integer expectedMethodCall = 1; + + class RequestDTO { + Long placeCategoryId = expectedId; + } + class AnotherDTO { + Long anotherId = 1L; + } + Object requestDTO = new RequestDTO(); + Object anotherDTO = new AnotherDTO(); + + JoinPoint joinPoint = createJoinPoint( + new String[] {"requestDTO", "anotherDTO"}, + new Object[] {requestDTO, anotherDTO} + ); + + given(redisTemplate.opsForZSet()).willReturn(zSetOperations); + given(redisTemplate.getExpire(anyString(), any())).willReturn(-1L); + + // when + aspect.logPlaceCategory(joinPoint); + + // then + verify(zSetOperations, times(expectedMethodCall)).incrementScore(anyString(), eq(expectedId), eq(1.0)); + verify(redisTemplate, times(expectedMethodCall)).expire(anyString(), anyLong(), any()); + } + + @Test + @DisplayName("'placeCategoryIdList'가 DTO 내부 List 필드로 있으면, Redis에 각각 기록된다.") + void dto_placeCategoryId_list_test() { + // given + List expectedIdList = List.of(1L, 2L, 3L); + Integer expectedMethodCall = 1; + + class RequestDTO { + List placeCategoryIdList = expectedIdList; + } + class AnotherDTO { + List anotherIdList = expectedIdList; + } + Object requestDTO = new RequestDTO(); + Object anotherDTO = new AnotherDTO(); + + JoinPoint joinPoint = createJoinPoint( + new String[] {"requestDTO", "anotherDTO", "anotherId"}, + new Object[] {requestDTO, anotherDTO, 1L} + ); + + given(redisTemplate.opsForZSet()).willReturn(zSetOperations); + given(redisTemplate.getExpire(anyString(), any())).willReturn(-1L); + + // when + aspect.logPlaceCategory(joinPoint); + + // then + expectedIdList.forEach(id -> + verify(zSetOperations, times(expectedMethodCall)).incrementScore(anyString(), eq(id), eq(1.0)) + ); + verify(redisTemplate, times(expectedMethodCall)).expire(anyString(), anyLong(), any()); + } + } +} \ No newline at end of file From e64f8fa33d727ee8718b1b628b4574b643777e2d Mon Sep 17 00:00:00 2001 From: pywoo Date: Tue, 29 Jul 2025 18:04:15 +0900 Subject: [PATCH 21/28] =?UTF-8?q?=F0=9F=90=9B=20fix:=20AOP=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/log/placecategorylog/aop/LogPlaceCategoryAspect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 78585c0..4937c5b 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 @@ -109,7 +109,7 @@ private void savePlaceCategoryIds(List placeCategoryIds) { // TTL - 이번 주까지로 설정 Long expire = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS); - if (expire == null || expire == 0) { + 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); From ea0de86e4e6f45626edf170ac39a462e622f26af Mon Sep 17 00:00:00 2001 From: pywoo Date: Tue, 29 Jul 2025 18:24:55 +0900 Subject: [PATCH 22/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20PlaceCategoryLogQu?= =?UTF-8?q?eryService=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlaceCategoryLogQueryServiceImplTest.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImplTest.java diff --git a/src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImplTest.java b/src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImplTest.java new file mode 100644 index 0000000..bb7ebea --- /dev/null +++ b/src/test/java/org/withtime/be/withtimebe/domain/log/placecategorylog/service/query/PlaceCategoryLogQueryServiceImplTest.java @@ -0,0 +1,67 @@ +package org.withtime.be.withtimebe.domain.log.placecategorylog.service.query; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.withtime.be.withtimebe.domain.log.placecategorylog.model.PlaceCategoryLog; +import org.withtime.be.withtimebe.domain.log.placecategorylog.repository.PlaceCategoryLogRepository; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[PlaceCategoryLogQueryService] 단위 테스트") +class PlaceCategoryLogQueryServiceImplTest { + + @InjectMocks + private PlaceCategoryLogQueryServiceImpl placeCategoryLogQueryService; + + @Mock + private PlaceCategoryLogRepository placeCategoryLogRepository; + + @Nested + @DisplayName("findWeeklyPlaceCategoryLogList()") + class FindWeeklyPlaceCategoryLogListTest { + + @Test + @DisplayName("이번 주 월요일부터 일요일까지, 조회된 placeCategoryLog를 반환한다.") + void find_weekly_placeCategoryLogList_test() { + // given + LocalDate now = LocalDate.now(); + LocalDate monday = now.with(DayOfWeek.MONDAY); + LocalDate sunday = now.with(DayOfWeek.SUNDAY); + + PlaceCategoryLog expectedLog1 = PlaceCategoryLog.builder() + .placeCategoryId(1L) + .placeCategoryLabel("감성적인") + .date(monday) + .count(30) + .build(); + PlaceCategoryLog expectedLog2 = PlaceCategoryLog.builder() + .placeCategoryId(2L) + .placeCategoryLabel("잔잔한") + .date(monday.plusDays(1)) + .count(50) + .build(); + + given(placeCategoryLogRepository.findByDateBetween(monday, sunday)) + .willReturn(List.of(expectedLog1, expectedLog2)); + + // when + List result = placeCategoryLogQueryService.findWeeklyPlaceCategoryLogList(); + + // then + assertThat(result).hasSize(2); + assertThat(result).containsExactlyInAnyOrder(expectedLog1, expectedLog2); + } + } +} \ No newline at end of file From 72918a3e35562c11aeac172b4c109ebf6a65df05 Mon Sep 17 00:00:00 2001 From: pywoo Date: Tue, 29 Jul 2025 18:44:07 +0900 Subject: [PATCH 23/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20DatePlaceLogQueryS?= =?UTF-8?q?ervice=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatePlaceLogQueryServiceImplTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/test/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImplTest.java diff --git a/src/test/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImplTest.java b/src/test/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImplTest.java new file mode 100644 index 0000000..42bdadd --- /dev/null +++ b/src/test/java/org/withtime/be/withtimebe/domain/log/dateplacelog/service/query/DatePlaceLogQueryServiceImplTest.java @@ -0,0 +1,66 @@ +package org.withtime.be.withtimebe.domain.log.dateplacelog.service.query; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; +import java.util.List; + +import org.bson.Document; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.withtime.be.withtimebe.domain.log.dateplacelog.entity.DatePlaceLog; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[DatePlaceLogQueryService] 단위 테스트") +class DatePlaceLogQueryServiceImplTest { + + @InjectMocks + private DatePlaceLogQueryServiceImpl datePlaceLogQueryService; + + @Mock + private MongoTemplate mongoTemplate; + + @Nested + @DisplayName("findMonthlyDatePlaceLogList()") + class FindMonthlyDatePlaceLogList { + + @Test + @DisplayName("Aggregation을 사용하여 월별 DatePlace 개수를 반환한다.") + void returnsMonthlyDatePlaceLogList() { + // given + DatePlaceLog log1 = DatePlaceLog.builder() + .date(LocalDate.of(2025, 6, 1)) + .count(300L) + .build(); + DatePlaceLog log2 = DatePlaceLog.builder() + .date(LocalDate.of(2025, 7, 1)) + .count(350L) + .build(); + List expectedLogs = List.of(log1, log2); + + AggregationResults aggregationResult = new AggregationResults<>(expectedLogs, new Document()); + + given(mongoTemplate.aggregate(any(Aggregation.class), eq("date_place_logs"), eq(DatePlaceLog.class))).willReturn(aggregationResult); + + // when + List result = datePlaceLogQueryService.findMonthlyDatePlaceLogList(); + + // then + assertThat(result).hasSize(2); + assertThat(result).containsExactly(log1, log2); + verify(mongoTemplate, times(1)).aggregate(any(Aggregation.class), eq("date_place_logs"), eq(DatePlaceLog.class)); + } + } + +} \ No newline at end of file From 715884314c8426d386a001de6c96d26ec21a5b88 Mon Sep 17 00:00:00 2001 From: pywoo Date: Tue, 29 Jul 2025 18:44:26 +0900 Subject: [PATCH 24/28] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/dateplacelog/converter/DatePlaceLogConverter.java | 2 ++ .../domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java index e2c65d5..046ada7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/converter/DatePlaceLogConverter.java @@ -22,9 +22,11 @@ public static DatePlaceLogResponseDTO.MonthlyDatePlaceLogList toMonthlyDatePlace public static DatePlaceLogResponseDTO.MonthlyDatePlaceLog toMonthlyDatePlaceLog(DatePlaceLog datePlaceLog) { + Long year = (long)datePlaceLog.getDate().getYear(); Long month = (long)datePlaceLog.getDate().getMonthValue(); return DatePlaceLogResponseDTO.MonthlyDatePlaceLog.builder() + .year(year) .month(month) .count(datePlaceLog.getCount()) .build(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java index 0f9a669..43cd32e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dateplacelog/dto/DatePlaceLogResponseDTO.java @@ -13,6 +13,7 @@ public record MonthlyDatePlaceLogList( @Builder public record MonthlyDatePlaceLog( + Long year, Long month, Long count ) {} From e608bfd96523f71776e669b30c87b141b3bddc7e Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 30 Jul 2025 15:47:51 +0900 Subject: [PATCH 25/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=B5=9C=EA=B7=BC?= =?UTF-8?q?=201=EA=B0=9C=EC=9B=94=EA=B0=84=20=ED=8F=89=EA=B7=A0=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=9A=9F=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../date/repository/DateCourseRepository.java | 11 +++++ .../DateCourseLogQueryController.java | 36 +++++++++++++++ .../converter/DateCourseLogConverter.java | 13 ++++++ .../dto/DateCourseLogResponseDTO.java | 12 +++++ .../query/DateCourseLogQueryService.java | 8 ++++ .../query/DateCourseLogQueryServiceImpl.java | 44 +++++++++++++++++++ 6 files changed, 124 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/date/repository/DateCourseRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DateCourseRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DateCourseRepository.java new file mode 100644 index 0000000..adeb353 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DateCourseRepository.java @@ -0,0 +1,11 @@ +package org.withtime.be.withtimebe.domain.date.repository; + +import java.time.LocalDate; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.withtime.be.withtimebe.domain.date.entity.DateCourse; + +public interface DateCourseRepository extends JpaRepository { + Long countByCreatedAtBetween(LocalDate startTime, LocalDate endTime); + Long countByMemberId(Long memberId); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java new file mode 100644 index 0000000..d76e850 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java @@ -0,0 +1,36 @@ +package org.withtime.be.withtimebe.domain.log.datecourselog.controller; + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; +import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.datecourselog.service.query.DateCourseLogQueryService; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/logs/datecourses") +public class DateCourseLogQueryController { + + private final DateCourseLogQueryService dateCourseLogQueryService; + + @Operation(summary = "최근 1개월 WithTime 사용자의 데이트 평균 횟수와 나의 데이트 횟수 조회 API by 피우", description = "메인 페이지의 데이트 나침반에 해당하는 API입니다. 최근 1개월 WithTime 사용자의 데이트 평균 횟수와 나의 데이트 횟수 조회하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @GetMapping("/average") + public DefaultResponse findAverageDateCourseCount( + @AuthenticatedMember Member member + ) { + DateCourseLogResponseDTO.FindAverageDateCourseCount response = dateCourseLogQueryService.findAverageDateCourseCount(member); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java new file mode 100644 index 0000000..b40f601 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java @@ -0,0 +1,13 @@ +package org.withtime.be.withtimebe.domain.log.datecourselog.converter; + +import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; + +public class DateCourseLogConverter { + + public static DateCourseLogResponseDTO.FindAverageDateCourseCount toFindAverageDateCourseCount(Double averageDateCount, Long myDateCount) { + return DateCourseLogResponseDTO.FindAverageDateCourseCount.builder() + .averageDateCount(averageDateCount) + .myDateCount(myDateCount) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java new file mode 100644 index 0000000..c873325 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java @@ -0,0 +1,12 @@ +package org.withtime.be.withtimebe.domain.log.datecourselog.dto; + +import lombok.Builder; + +public class DateCourseLogResponseDTO { + + @Builder + public record FindAverageDateCourseCount( + Double averageDateCount, + Long myDateCount + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java new file mode 100644 index 0000000..ffd6451 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java @@ -0,0 +1,8 @@ +package org.withtime.be.withtimebe.domain.log.datecourselog.service.query; + +import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public interface DateCourseLogQueryService { + DateCourseLogResponseDTO.FindAverageDateCourseCount findAverageDateCourseCount(Member member); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java new file mode 100644 index 0000000..30561b1 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java @@ -0,0 +1,44 @@ +package org.withtime.be.withtimebe.domain.log.datecourselog.service.query; + +import java.time.LocalDate; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; +import org.withtime.be.withtimebe.domain.log.datecourselog.converter.DateCourseLogConverter; +import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DateCourseLogQueryServiceImpl implements DateCourseLogQueryService { + + private final DateCourseRepository dateCourseRepository; + private final MemberRepository memberRepository; + + @Override + public DateCourseLogResponseDTO.FindAverageDateCourseCount findAverageDateCourseCount(Member member) { + + LocalDate now = LocalDate.now(); + LocalDate oneMonthAgo = now.minusDays(30); + + // 최근 1개월동안 생성된 데이트 코스 + Long dateCourseCount = dateCourseRepository.countByCreatedAtBetween(oneMonthAgo, now); + + // 전체 멤버 수 + Long memberCount = memberRepository.count(); + + // 평균 데이트 횟수 + Double averageDateCount = (double) dateCourseCount / memberCount; + averageDateCount = Math.round(averageDateCount * 10.0) / 10.0; // 첫째 자리까지 + + // 나의 데이트 횟수 + Long myDateCount = (member == null) ? 0L : dateCourseRepository.countByMemberId(member.getId()); + + return DateCourseLogConverter.toFindAverageDateCourseCount(averageDateCount, myDateCount); + } +} From d439aa1547a12fa29e7f0b616cb31832fb2eb8d1 Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 30 Jul 2025 15:48:13 +0900 Subject: [PATCH 26/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=ED=8F=89=EA=B7=A0?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=9A=9F=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DateCourseLogQueryServiceImplTest.java | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java diff --git a/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java b/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java new file mode 100644 index 0000000..3a4a9fd --- /dev/null +++ b/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java @@ -0,0 +1,83 @@ +package org.withtime.be.withtimebe.domain.log.datecourselog.service.query; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; +import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; + +@ExtendWith(MockitoExtension.class) +@DisplayName("[DateCourseLogQueryService] 단위 테스트") +class DateCourseLogQueryServiceImplTest { + + @InjectMocks + private DateCourseLogQueryServiceImpl dateCourseLogQueryService; + + @Mock + private DateCourseRepository dateCourseRepository; + + @Mock + private MemberRepository memberRepository; + + @Nested + @DisplayName("findAverageDateCourseCount()") + class FindAverageDateCourseCount { + + @Test + @DisplayName("member가 성공적으로 주어지면 평균 데이트 횟수와 나의 데이트 횟수가 계산된다.") + void returnsAverageWithMember() { + // given + Long memberId = 1L; + + Member member = Member.builder() + .id(memberId) + .build(); + + LocalDate now = LocalDate.now(); + LocalDate oneMonthAgo = now.minusDays(30); + + given(dateCourseRepository.countByCreatedAtBetween(oneMonthAgo, now)).willReturn(300L); + given(memberRepository.count()).willReturn(15L); + given(dateCourseRepository.countByMemberId(memberId)).willReturn(10L); // 횟수 : 10번 + + // when + DateCourseLogResponseDTO.FindAverageDateCourseCount result = + dateCourseLogQueryService.findAverageDateCourseCount(member); + + // then + assertThat(result.averageDateCount()).isEqualTo(20.0); + assertThat(result.myDateCount()).isEqualTo(10L); + } + + @Test + @DisplayName("비회원인 경우 (member == null) 나의 데이트 횟수는 0으로 계산된다.") + void returnsAverageWithNullMember() { + // given + LocalDate now = LocalDate.now(); + LocalDate oneMonthAgo = now.minusDays(30); + + given(dateCourseRepository.countByCreatedAtBetween(oneMonthAgo, now)).willReturn(300L); + given(memberRepository.count()).willReturn(15L); + + // when + DateCourseLogResponseDTO.FindAverageDateCourseCount result = + dateCourseLogQueryService.findAverageDateCourseCount(null); + + // then + assertThat(result.averageDateCount()).isEqualTo(20.0); + assertThat(result.myDateCount()).isEqualTo(0L); + } + } +} \ No newline at end of file From 10ff682eac25b8aee43de3c2a4f014b6b5cdbe62 Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 30 Jul 2025 16:15:36 +0900 Subject: [PATCH 27/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=8B=A4=EB=A5=B8?= =?UTF-8?q?=20=EC=82=AC=EB=9E=8C=EC=9D=98=20=EB=82=B4=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EC=8A=A4=20=EC=A0=80=EC=9E=A5=20=ED=9A=9F?= =?UTF-8?q?=EC=88=98=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatePlaceDateCourseRepository.java | 16 ++++++++++++++++ .../controller/DateCourseLogQueryController.java | 12 ++++++++++++ .../converter/DateCourseLogConverter.java | 6 ++++++ .../dto/DateCourseLogResponseDTO.java | 7 ++++++- .../service/query/DateCourseLogQueryService.java | 1 + .../query/DateCourseLogQueryServiceImpl.java | 8 ++++++++ 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceDateCourseRepository.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceDateCourseRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceDateCourseRepository.java new file mode 100644 index 0000000..c6dd9b8 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/repository/DatePlaceDateCourseRepository.java @@ -0,0 +1,16 @@ +package org.withtime.be.withtimebe.domain.date.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.withtime.be.withtimebe.domain.date.entity.DatePlaceDateCourse; + +public interface DatePlaceDateCourseRepository extends JpaRepository { + @Query(""" + SELECT COUNT(dpc) + FROM DatePlaceDateCourse dpc + JOIN dpc.dateCourse dc + WHERE dc.member.id = :memberId + """) + Long countByCreatorMemberId(@Param("memberId") Long memberId); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java index d76e850..ca83135 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java @@ -33,4 +33,16 @@ public DefaultResponse find DateCourseLogResponseDTO.FindAverageDateCourseCount response = dateCourseLogQueryService.findAverageDateCourseCount(member); return DefaultResponse.ok(response); } + + @Operation(summary = "다른 사람의 내 데이트 코스 저장 횟수 조회 API by 피우", description = "나의 데이트 코스를 다른 사람이 얼마나 저장했는지 조회하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @GetMapping("/saved-count") + public DefaultResponse findSavedDateCourseCount( + @AuthenticatedMember Member member + ) { + DateCourseLogResponseDTO.FindSavedDateCourseCount response = dateCourseLogQueryService.findSavedDateCourseCount(member); + return DefaultResponse.ok(response); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java index b40f601..4fc4ba7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/converter/DateCourseLogConverter.java @@ -10,4 +10,10 @@ public static DateCourseLogResponseDTO.FindAverageDateCourseCount toFindAverageD .myDateCount(myDateCount) .build(); } + + public static DateCourseLogResponseDTO.FindSavedDateCourseCount toFindSavedDateCourseCount(Long count) { + return DateCourseLogResponseDTO.FindSavedDateCourseCount.builder() + .count(count) + .build(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java index c873325..ec8c9cb 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/dto/DateCourseLogResponseDTO.java @@ -9,4 +9,9 @@ public record FindAverageDateCourseCount( Double averageDateCount, Long myDateCount ) {} -} + + @Builder + public record FindSavedDateCourseCount( + Long count + ) {} +} \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java index ffd6451..64c0f38 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryService.java @@ -5,4 +5,5 @@ public interface DateCourseLogQueryService { DateCourseLogResponseDTO.FindAverageDateCourseCount findAverageDateCourseCount(Member member); + DateCourseLogResponseDTO.FindSavedDateCourseCount findSavedDateCourseCount(Member member); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java index 30561b1..a721d4f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImpl.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; +import org.withtime.be.withtimebe.domain.date.repository.DatePlaceDateCourseRepository; import org.withtime.be.withtimebe.domain.log.datecourselog.converter.DateCourseLogConverter; import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.Member; @@ -19,6 +20,7 @@ public class DateCourseLogQueryServiceImpl implements DateCourseLogQueryService private final DateCourseRepository dateCourseRepository; private final MemberRepository memberRepository; + private final DatePlaceDateCourseRepository datePlaceDateCourseRepository; @Override public DateCourseLogResponseDTO.FindAverageDateCourseCount findAverageDateCourseCount(Member member) { @@ -41,4 +43,10 @@ public DateCourseLogResponseDTO.FindAverageDateCourseCount findAverageDateCourse return DateCourseLogConverter.toFindAverageDateCourseCount(averageDateCount, myDateCount); } + + @Override + public DateCourseLogResponseDTO.FindSavedDateCourseCount findSavedDateCourseCount(Member member) { + Long savedCount = (member == null) ? 0L : datePlaceDateCourseRepository.countByCreatorMemberId(member.getId()); + return DateCourseLogConverter.toFindSavedDateCourseCount(savedCount); + } } From b8f4ddf26073fdc81c5048cebd5c695ec13575ad Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 30 Jul 2025 16:28:58 +0900 Subject: [PATCH 28/28] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=8B=A4=EB=A5=B8?= =?UTF-8?q?=20=EC=82=AC=EB=9E=8C=EC=9D=98=20=EB=82=B4=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EC=8A=A4=20=EC=A0=80=EC=9E=A5=20=ED=9A=9F?= =?UTF-8?q?=EC=88=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DateCourseLogQueryController.java | 2 +- .../DateCourseLogQueryServiceImplTest.java | 53 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java index ca83135..9656523 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/datecourselog/controller/DateCourseLogQueryController.java @@ -39,7 +39,7 @@ public DefaultResponse find @ApiResponse(responseCode = "200", description = "성공입니다.") }) @GetMapping("/saved-count") - public DefaultResponse findSavedDateCourseCount( + public DefaultResponse findSavedDateCourseCount( @AuthenticatedMember Member member ) { DateCourseLogResponseDTO.FindSavedDateCourseCount response = dateCourseLogQueryService.findSavedDateCourseCount(member); diff --git a/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java b/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java index 3a4a9fd..9fd44ff 100644 --- a/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java +++ b/src/test/java/org/withtime/be/withtimebe/domain/log/datecourselog/service/query/DateCourseLogQueryServiceImplTest.java @@ -1,7 +1,6 @@ package org.withtime.be.withtimebe.domain.log.datecourselog.service.query; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.time.LocalDate; @@ -14,6 +13,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.withtime.be.withtimebe.domain.date.repository.DateCourseRepository; +import org.withtime.be.withtimebe.domain.date.repository.DatePlaceDateCourseRepository; import org.withtime.be.withtimebe.domain.log.datecourselog.dto.DateCourseLogResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; @@ -31,13 +31,16 @@ class DateCourseLogQueryServiceImplTest { @Mock private MemberRepository memberRepository; + @Mock + private DatePlaceDateCourseRepository datePlaceDateCourseRepository; + @Nested @DisplayName("findAverageDateCourseCount()") class FindAverageDateCourseCount { @Test @DisplayName("member가 성공적으로 주어지면 평균 데이트 횟수와 나의 데이트 횟수가 계산된다.") - void returnsAverageWithMember() { + void averageCountWithMember() { // given Long memberId = 1L; @@ -63,7 +66,7 @@ void returnsAverageWithMember() { @Test @DisplayName("비회원인 경우 (member == null) 나의 데이트 횟수는 0으로 계산된다.") - void returnsAverageWithNullMember() { + void averageCountWithNullMember() { // given LocalDate now = LocalDate.now(); LocalDate oneMonthAgo = now.minusDays(30); @@ -80,4 +83,48 @@ void returnsAverageWithNullMember() { assertThat(result.myDateCount()).isEqualTo(0L); } } + + @Nested + @DisplayName("findSavedDateCourseCount()") + class FindSavedDateCourseCount { + + @Test + @DisplayName("member가 성공적으로 주어지면 count 쿼리가 실행되고, 다른 사람이 저장한 내 데이트 코스 횟수가 계산된다.") + void savedCountWithMember() { + // given + Long memberId = 1L; + Long expectedSavedCount = 5L; + Integer expectedMethodCall = 1; + + Member member = Member.builder() + .id(memberId) + .build(); + + given(datePlaceDateCourseRepository.countByCreatorMemberId(memberId)).willReturn(expectedSavedCount); + + // when + DateCourseLogResponseDTO.FindSavedDateCourseCount result = + dateCourseLogQueryService.findSavedDateCourseCount(member); + + // then + assertThat(result.count()).isEqualTo(expectedSavedCount); + verify(datePlaceDateCourseRepository, times(expectedMethodCall)).countByCreatorMemberId(anyLong()); + } + + @Test + @DisplayName("비회원인 경우 (member == null) count 쿼리는 실행되지 않고, 저장 수는 0으로 계산된다.") + void savedCountWithNullMember() { + // given + Member member = null; + Integer expectedMethodCall = 0; + + // when + DateCourseLogResponseDTO.FindSavedDateCourseCount result = + dateCourseLogQueryService.findSavedDateCourseCount(member); + + // then + assertThat(result.count()).isEqualTo(0L); + verify(datePlaceDateCourseRepository, times(expectedMethodCall)).countByCreatorMemberId(anyLong()); + } + } } \ No newline at end of file