Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c228805
✨ feat: 몽고 의존성 추가
kfdsy0103 Jul 27, 2025
ae1fe12
✨ feat: 몽고 Config 생성
kfdsy0103 Jul 27, 2025
ae94e8d
✨ feat: 몽고 yml 설정
kfdsy0103 Jul 27, 2025
6e2ad39
✨ feat: PlaceCategoryLog 모델 생성
kfdsy0103 Jul 27, 2025
8b75156
✨ feat: 이번주 인기 키워드 조회 API 구현
kfdsy0103 Jul 27, 2025
35e3602
✨ feat: 이번주 인기 키워드 수집 어노테이션 생성
kfdsy0103 Jul 27, 2025
902e9d6
✨ feat: 리플랙션 기반 인기 키워드 수집 AOP 구현
kfdsy0103 Jul 27, 2025
3e13c50
✨ feat: 레디스 수집 정보 DB동기화 스케쥴러 생성
kfdsy0103 Jul 27, 2025
bfc227d
✨ feat: JVM 디폴트 TimeZone 설정
kfdsy0103 Jul 27, 2025
8a2c873
✨ feat: KST UTC 변환 컨버터 등록 및 _class 필드 제거
kfdsy0103 Jul 27, 2025
bbac077
✨ feat: 시큐리티 인증된 URL 추가
kfdsy0103 Jul 27, 2025
f9fc78f
🐛 fix: placeCategoryLog 패키지 분리
kfdsy0103 Jul 27, 2025
716c316
🐛 fix: 컨버터를 통해 다큐먼트를 생성하도록 수정
kfdsy0103 Jul 27, 2025
9c667d8
✨ feat: DatePlaceLog 모델 생성
kfdsy0103 Jul 27, 2025
f7f8e6f
✨ feat: 월별 데이트 장소 수 조회 API 구현
kfdsy0103 Jul 27, 2025
899b61f
✨ feat: 일별 데이트 장소 집계 스케쥴러 등록
kfdsy0103 Jul 27, 2025
fb15da8
✨ feat: 시큐리티 인증된 URL 추가
kfdsy0103 Jul 27, 2025
f5c6f57
✨ feat: yml에 설정 추가
kfdsy0103 Jul 27, 2025
a10dedd
🐛 fix: 스케쥴러 시간 오차 조정
kfdsy0103 Jul 29, 2025
89c7511
✨ feat: LogPlaceCategoryAOP 테스트 코드 작성
kfdsy0103 Jul 29, 2025
e64f8fa
🐛 fix: AOP 테스트 오류 수정
kfdsy0103 Jul 29, 2025
ea0de86
✨ feat: PlaceCategoryLogQueryService 테스트 코드 작성
kfdsy0103 Jul 29, 2025
72918a3
✨ feat: DatePlaceLogQueryService 테스트 코드 작성
kfdsy0103 Jul 29, 2025
7158843
🐛 fix: 누락 필드 추가
kfdsy0103 Jul 29, 2025
e608bfd
✨ feat: 최근 1개월간 평균 데이트 횟수 조회 API 구현
kfdsy0103 Jul 30, 2025
d439aa1
✨ feat: 평균 데이트 횟수 조회 테스트 코드 작성
kfdsy0103 Jul 30, 2025
10ff682
✨ feat: 다른 사람의 내 데이트 코스 저장 횟수 API 구현
kfdsy0103 Jul 30, 2025
b8f4ddf
✨ feat: 다른 사람의 내 데이트 코스 저장 횟수 테스트 코드 작성
kfdsy0103 Jul 30, 2025
a6913e8
Merge branch 'develop' into feat/#56-statistics-place-keyword
kfdsy0103 Jul 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'com.h2database:h2'

// MongoDB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

// firebase
implementation 'com.google.firebase:firebase-admin:9.1.0'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
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;
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;

import jakarta.annotation.PostConstruct;


@EnableScheduling
@EnableJpaAuditing
@EnableJpaRepositories(
basePackages = {"org.withtime.be"},
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "org\\.withtime\\.be\\.withtimebe\\.domain\\.log\\..*"
))
@SpringBootApplication
public class WithTimeBeApplication {

public static void main(String[] args) {
SpringApplication.run(WithTimeBeApplication.class, args);
}

@PostConstruct
public void init() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } // JVM 기본 TimeZone 설정
}
Original file line number Diff line number Diff line change
@@ -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<DateCourse, Long> {
Long countByCreatedAtBetween(LocalDate startTime, LocalDate endTime);
Long countByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -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<DatePlaceDateCourse, Long> {
@Query("""
SELECT COUNT(dpc)
FROM DatePlaceDateCourse dpc
JOIN dpc.dateCourse dc
WHERE dc.member.id = :memberId
""")
Long countByCreatorMemberId(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
@@ -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<DatePlace, Long> {
}
Original file line number Diff line number Diff line change
@@ -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<PlaceCategory, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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<DateCourseLogResponseDTO.FindAverageDateCourseCount> findAverageDateCourseCount(
@AuthenticatedMember Member member
) {
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<DateCourseLogResponseDTO.FindSavedDateCourseCount> findSavedDateCourseCount(
@AuthenticatedMember Member member
) {
DateCourseLogResponseDTO.FindSavedDateCourseCount response = dateCourseLogQueryService.findSavedDateCourseCount(member);
return DefaultResponse.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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();
}

public static DateCourseLogResponseDTO.FindSavedDateCourseCount toFindSavedDateCourseCount(Long count) {
return DateCourseLogResponseDTO.FindSavedDateCourseCount.builder()
.count(count)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.withtime.be.withtimebe.domain.log.datecourselog.dto;

import lombok.Builder;

public class DateCourseLogResponseDTO {

@Builder
public record FindAverageDateCourseCount(
Double averageDateCount,
Long myDateCount
) {}

@Builder
public record FindSavedDateCourseCount(
Long count
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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);
DateCourseLogResponseDTO.FindSavedDateCourseCount findSavedDateCourseCount(Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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.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;
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;
private final DatePlaceDateCourseRepository datePlaceDateCourseRepository;

@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);
}

@Override
public DateCourseLogResponseDTO.FindSavedDateCourseCount findSavedDateCourseCount(Member member) {
Long savedCount = (member == null) ? 0L : datePlaceDateCourseRepository.countByCreatorMemberId(member.getId());
return DateCourseLogConverter.toFindSavedDateCourseCount(savedCount);
}
}
Original file line number Diff line number Diff line change
@@ -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<DatePlaceLogResponseDTO.MonthlyDatePlaceLogList> findMonthlyDatePlaceLogList() {
List<DatePlaceLog> result = datePlaceLogQueryService.findMonthlyDatePlaceLogList();
DatePlaceLogResponseDTO.MonthlyDatePlaceLogList response = DatePlaceLogConverter.toMonthlyDatePlaceLogList(result);
return DefaultResponse.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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<DatePlaceLog> datePlaceLogs) {

List<DatePlaceLogResponseDTO.MonthlyDatePlaceLog> datePlaceLogList = datePlaceLogs.stream()
.map(DatePlaceLogConverter::toMonthlyDatePlaceLog)
.toList();

return DatePlaceLogResponseDTO.MonthlyDatePlaceLogList.builder()
.datePlaceLogList(datePlaceLogList)
.build();
}

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();
}

public static DatePlaceLog toDatePlaceLog(LocalDate date, Long count) {
return DatePlaceLog.builder()
.date(date)
.count(count)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.withtime.be.withtimebe.domain.log.dateplacelog.dto;

import java.util.List;

import lombok.Builder;

public class DatePlaceLogResponseDTO {

@Builder
public record MonthlyDatePlaceLogList(
List<MonthlyDatePlaceLog> datePlaceLogList
) {}

@Builder
public record MonthlyDatePlaceLog(
Long year,
Long month,
Long count
) {}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<DatePlaceLog, String> {
}
Loading