Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1d0d98c
✨ feat: VisitLog 엔티티 생성
kfdsy0103 Jul 11, 2025
90848be
✨ feat: 일간 방문자 수 조회 기능 구현
kfdsy0103 Jul 11, 2025
193826a
✨ feat: 하루동안 방문자 수 시간대별 조회 구현
kfdsy0103 Jul 11, 2025
ed0a65e
✨ feat: 방문자 집계 인터셉터 및 스케쥴러 등록
kfdsy0103 Jul 11, 2025
48fadbd
✨ feat: Payments 엔티티에 멤버십 만료 필드 추가
kfdsy0103 Jul 14, 2025
ea15c47
✨ feat: Member 엔티티에 paymentList 필드 @OneToMany로 추가
kfdsy0103 Jul 14, 2025
bb7b272
✨ feat: 멤버십 전체 조회 구현
kfdsy0103 Jul 14, 2025
2d34eba
✨ feat: 멤버십 연장 및 취소 구현
kfdsy0103 Jul 14, 2025
5db878d
🐛 fix: SecurityConfig 인가 처리 통일
kfdsy0103 Jul 14, 2025
1b5d3b1
🐛 fix: VisitLog 관련 인가 처리 통일
kfdsy0103 Jul 14, 2025
f03bb5c
🐛 fix: DatePlace 엔티티 패키지 위치 변경
kfdsy0103 Jul 16, 2025
bac03f5
✨ feat: 어드민 관리 페이지의 데이트 장소 조회 구현
kfdsy0103 Jul 16, 2025
5f97c7d
🐛 fix: 지번 주소만 반환하도록 수정
kfdsy0103 Jul 16, 2025
7314ab1
🐛 fix: 방문자 로그 임시 스케쥴러 및 인터셉터 수정
kfdsy0103 Jul 16, 2025
5ccfd42
✨ feat: 몽고 의존성 추가
kfdsy0103 Jul 17, 2025
03ca074
🐛 fix: 로그 스케쥴러 및 yml 변경
kfdsy0103 Jul 17, 2025
b7fbf73
♻️ refactor: 몽고DB로 변경
kfdsy0103 Jul 17, 2025
3d547a5
✨ feat: 시간대 설정 및 컨버터 MongoConfig에 등록
kfdsy0103 Jul 17, 2025
5204e3a
🐛 fix: 몽고 URI 환경변수 수정
kfdsy0103 Jul 17, 2025
b27ec43
🐛 fix: 불필요 코드 삭제
kfdsy0103 Jul 17, 2025
0d7897a
Merge branch 'develop' into feat/#31-admin-page-api
kfdsy0103 Jul 17, 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

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

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
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.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.withtime.be.withtimebe.domain.log.repository.VisitLogRepository;

import jakarta.annotation.PostConstruct;

@EnableScheduling
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = {"org.withtime.be"}, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = VisitLogRepository.class)})
@EnableMongoRepositories(basePackageClasses = VisitLogRepository.class)
@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
Expand Up @@ -3,6 +3,7 @@
import jakarta.persistence.*;
import lombok.*;
import org.withtime.be.withtimebe.domain.date.entity.enums.Day;
import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;
import org.withtime.be.withtimebe.global.common.BaseEntity;

import java.time.LocalTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import jakarta.persistence.*;
import lombok.*;

import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;
import org.withtime.be.withtimebe.global.common.BaseEntity;

@Entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.persistence.*;
import lombok.*;

import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;
import org.withtime.be.withtimebe.global.common.BaseEntity;

@Entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import jakarta.persistence.*;
import lombok.*;

import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;
import org.withtime.be.withtimebe.global.common.BaseEntity;

@Entity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.withtime.be.withtimebe.domain.dateplace.controller;

import org.namul.api.payload.response.DefaultResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
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.dateplace.converter.DatePlaceConverter;
import org.withtime.be.withtimebe.domain.dateplace.dto.response.DatePlaceResponseDTO;
import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;
import org.withtime.be.withtimebe.domain.dateplace.service.query.DatePlaceQueryService;
import org.withtime.be.withtimebe.global.annotation.SwaggerPageable;

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/dateplaces")
public class DatePlaceQueryController {

private final DatePlaceQueryService datePlaceQueryService;

@Operation(summary = "어드민 페이지 데이트 장소 조회 API by 피우 [Only Admin]", description = "어드민 페이지에서 데이트 장소를 조회하는 API입니다. 어드민만 사용 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공입니다.")
})
@SwaggerPageable
@GetMapping("/management")
public DefaultResponse<DatePlaceResponseDTO.DatePlaceManagementList> findDatePlaceManagement(
@PageableDefault(page = 0, size = 10) Pageable pageable
) {
Page<DatePlace> result = datePlaceQueryService.findDatePlaces(pageable);
DatePlaceResponseDTO.DatePlaceManagementList response = DatePlaceConverter.toDatePlaceManagementList(result);
return DefaultResponse.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.withtime.be.withtimebe.domain.dateplace.converter;

import java.util.List;

import org.springframework.data.domain.Page;
import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory;
import org.withtime.be.withtimebe.domain.dateplace.dto.response.DatePlaceResponseDTO;
import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;

public class DatePlaceConverter {

public static DatePlaceResponseDTO.DatePlaceManagementList toDatePlaceManagementList(Page<DatePlace> datePlacePage) {

List<DatePlaceResponseDTO.DatePlaceManagement> datePlaceManagementList = datePlacePage.stream()
.map(DatePlaceConverter::toDatePlaceManagement)
.toList();

return DatePlaceResponseDTO.DatePlaceManagementList.builder()
.datePlaceManagementList(datePlaceManagementList)
.totalPages(datePlacePage.getTotalPages())
.currentPage(datePlacePage.getNumber())
.currentSize(datePlacePage.getNumberOfElements())
.hasNextPage(datePlacePage.hasNext())
.build();
}

public static DatePlaceResponseDTO.DatePlaceManagement toDatePlaceManagement(DatePlace datePlace) {

List<DatePlaceResponseDTO.PlaceCategory> placeCategoryList = datePlace.getDatePlacePlaceCategoryList().stream()
.map(datePlacePlaceCategory -> toPlaceCategory(datePlacePlaceCategory.getPlaceCategory()))
.toList();

return DatePlaceResponseDTO.DatePlaceManagement.builder()
.datePlaceId(datePlace.getId())
.name(datePlace.getName())
.tel(datePlace.getTel())
.averagePrice(datePlace.getAveragePrice())
.lotNumberAddress(datePlace.getLotNumberAddress())
.placeType(datePlace.getPlaceType().getLabel())
.placeCategoryList(placeCategoryList)
.build();
}

public static DatePlaceResponseDTO.PlaceCategory toPlaceCategory(PlaceCategory placeCategory) {

return DatePlaceResponseDTO.PlaceCategory.builder()
.placeCategoryId(placeCategory.getId())
.placeCategoryType(placeCategory.getCategoryType().name())
.label(placeCategory.getLabel())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.withtime.be.withtimebe.domain.dateplace.dto.response;

import java.util.List;

import lombok.Builder;

public class DatePlaceResponseDTO {

@Builder
public record DatePlaceManagementList(
List<DatePlaceManagement> datePlaceManagementList,
Integer totalPages, // 전체 페이지 개수
Integer currentPage, // 현재 페이지 번호
Integer currentSize, // 현재 페이지의 크기
Boolean hasNextPage // 다음 페이지 존재 여부
) {}

@Builder
public record DatePlaceManagement(
Long datePlaceId, // 데이트 장소 식별자 값
String name, // 장소 이름
String tel, // 전화 번호
Integer averagePrice, // 평균 가격
String lotNumberAddress, // 지번 주소
String placeType, // 장소 유형
List<PlaceCategory> placeCategoryList // 카테고리 목록
) {}

@Builder
public record PlaceCategory(
Long placeCategoryId,
String placeCategoryType,
String label
) {}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package org.withtime.be.withtimebe.domain.date.entity;
package org.withtime.be.withtimebe.domain.dateplace.entity;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.*;
import lombok.*;

import org.withtime.be.withtimebe.domain.date.entity.enums.PlaceType;
import org.hibernate.annotations.BatchSize;
import org.withtime.be.withtimebe.domain.date.entity.DatePlacePlaceCategory;
import org.withtime.be.withtimebe.domain.date.entity.PlaceCategory;
import org.withtime.be.withtimebe.domain.dateplace.entity.enums.PlaceType;
import org.withtime.be.withtimebe.global.common.BaseEntity;

@Entity
Expand Down Expand Up @@ -49,4 +55,9 @@ public class DatePlace extends BaseEntity {
@Enumerated(EnumType.STRING)
@Column(name = "place_type")
private PlaceType placeType;

@OneToMany(mappedBy = "datePlace")
@BatchSize(size = 10)
@Builder.Default
private List<DatePlacePlaceCategory> datePlacePlaceCategoryList = new ArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.withtime.be.withtimebe.domain.date.entity.enums;
package org.withtime.be.withtimebe.domain.dateplace.entity.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.withtime.be.withtimebe.domain.dateplace.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;

public interface DatePlaceRepository extends JpaRepository<DatePlace, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.withtime.be.withtimebe.domain.dateplace.service.query;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;

public interface DatePlaceQueryService {
Page<DatePlace> findDatePlaces(Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.withtime.be.withtimebe.domain.dateplace.service.query;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace;
import org.withtime.be.withtimebe.domain.dateplace.repository.DatePlaceRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DatePlaceQueryServiceImpl implements DatePlaceQueryService {

private final DatePlaceRepository datePlaceRepository;

@Override
public Page<DatePlace> findDatePlaces(Pageable pageable) {
return datePlaceRepository.findAll(pageable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.withtime.be.withtimebe.domain.log.controller.query;

import java.time.LocalDate;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.withtime.be.withtimebe.domain.log.converter.VisitLogConverter;
import org.withtime.be.withtimebe.domain.log.dto.response.VisitLogResponseDTO;
import org.withtime.be.withtimebe.domain.log.model.VisitLog;
import org.withtime.be.withtimebe.domain.log.service.query.VisitLogQueryService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/visit-logs")
public class VisitLogQueryController {

private final VisitLogQueryService visitLogQueryService;

@Operation(summary = "최근 일주일 간 일별 방문자 수 조회 API by 피우 [Only Admin]", description = "일주일 간 일별 방문자 수 조회 API입니다. 어드민만 사용 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공입니다.")
})
@GetMapping("/daily")
public DefaultResponse<VisitLogResponseDTO.DailyVisitLogList> findDailyVisitLogList() {
List<VisitLog> result = visitLogQueryService.findDailyVisitLogList();
VisitLogResponseDTO.DailyVisitLogList response = VisitLogConverter.toDailyVisitLogList(result);
return DefaultResponse.ok(response);
}

@Operation(summary = "하루동안 시간대 별 방문자 수 추이 조회 API by 피우 [Only Admin]", description = "하루동안 시간대 별 방문자 수 추이 조회 API 입니다. 어드민만 사용 가능합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "성공입니다.")
})
@Parameter(name = "date", description = "예) 2025-01-01")
@GetMapping("/hourly")
public DefaultResponse<VisitLogResponseDTO.HourlyVisitLogList> findHourlyVisitLog(
@RequestParam("date") LocalDate date
) {
List<VisitLog> result = visitLogQueryService.findHourlyVisitLogList(date);
VisitLogResponseDTO.HourlyVisitLogList response = VisitLogConverter.toHourlyVisitLogList(date, result);
return DefaultResponse.ok(response);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.withtime.be.withtimebe.domain.log.converter;

import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.withtime.be.withtimebe.domain.log.dto.response.VisitLogResponseDTO;
import org.withtime.be.withtimebe.domain.log.model.VisitLog;

public class VisitLogConverter {

public static VisitLogResponseDTO.DailyVisitLogList toDailyVisitLogList(List<VisitLog> visitLogList) {

Map<LocalDate, Long> result = visitLogList.stream()
.collect(Collectors.groupingBy(VisitLog::getDate, Collectors.summingLong(VisitLog::getCount)));

List<VisitLogResponseDTO.DailyVisitLog> dailyVisitLogList = result.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> toDailyVisitLog(entry.getKey(), entry.getValue()))
.toList();

return VisitLogResponseDTO.DailyVisitLogList.builder()
.dailyVisitLogList(dailyVisitLogList)
.build();
}

public static VisitLogResponseDTO.DailyVisitLog toDailyVisitLog(LocalDate localDate, Long totalCount) {

return VisitLogResponseDTO.DailyVisitLog.builder()
.date(localDate)
.count(totalCount)
.build();
}

public static VisitLogResponseDTO.HourlyVisitLogList toHourlyVisitLogList(LocalDate date, List<VisitLog> visitLogList) {

List<VisitLogResponseDTO.HourlyVisitLog> hourlyVisitLogList = visitLogList.stream()
.sorted(Comparator.comparing(VisitLog::getHour))
.map(VisitLogConverter::toHourlyVisitLog)
.toList();

return VisitLogResponseDTO.HourlyVisitLogList.builder()
.date(date)
.hourlyVisitLogList(hourlyVisitLogList)
.build();
}

public static VisitLogResponseDTO.HourlyVisitLog toHourlyVisitLog(VisitLog visitLog) {

return VisitLogResponseDTO.HourlyVisitLog.builder()
.hour(visitLog.getHour())
.count(visitLog.getCount())
.build();
}
}
Loading
Loading