From 1d0d98c12693ecc4fc5f2b3a7615b4cf26a350be Mon Sep 17 00:00:00 2001 From: pywoo Date: Fri, 11 Jul 2025 18:10:40 +0900 Subject: [PATCH 01/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20VisitLog=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/log/entity/VisitLog.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java new file mode 100644 index 0000000..d30cfe9 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java @@ -0,0 +1,41 @@ +package org.withtime.be.withtimebe.domain.log.entity; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.withtime.be.withtimebe.global.common.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "visit_log") +public class VisitLog extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "visit_log_id") + private Long id; + + @Column(name = "date", nullable = false) + private LocalDate date; + + @Column(name = "hour", nullable = false) + private LocalTime hour; + + @Column(name = "count", nullable = false) + private Long count; +} From 90848be5a88124fe584ecf80e82ee1168202f3d1 Mon Sep 17 00:00:00 2001 From: pywoo Date: Fri, 11 Jul 2025 18:54:48 +0900 Subject: [PATCH 02/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=9D=BC=EA=B0=84?= =?UTF-8?q?=20=EB=B0=A9=EB=AC=B8=EC=9E=90=20=EC=88=98=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/VisitLogQueryController.java | 40 +++++++++++++++++++ .../log/converter/VisitLogConverter.java | 35 ++++++++++++++++ .../log/dto/response/VisitLogResponseDTO.java | 20 ++++++++++ .../log/repository/VisitLogRepository.java | 11 +++++ .../service/query/VisitLogQueryService.java | 9 +++++ .../query/VisitLogQueryServiceImpl.java | 30 ++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java new file mode 100644 index 0000000..be2f358 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java @@ -0,0 +1,40 @@ +package org.withtime.be.withtimebe.domain.log.controller.query; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +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.VisitLogConverter; +import org.withtime.be.withtimebe.domain.log.dto.response.VisitLogResponseDTO; +import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.service.query.VisitLogQueryService; +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/admin/visit-logs") +public class VisitLogQueryController { + + private final VisitLogQueryService visitLogQueryService; + + @Operation(summary = "최근 일주일 간 일별 방문자 수 조회 API Only Admin by 피우", description = "일주일 간 일별 방문자 수 조회 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @GetMapping("/daily") + public DefaultResponse findDailyVisitLogList() { + List result = visitLogQueryService.findDailyVisitLogList(); + VisitLogResponseDTO.DailyVisitLogList response = VisitLogConverter.toDailyVisitLogList(result); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java new file mode 100644 index 0000000..89ef60a --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java @@ -0,0 +1,35 @@ +package org.withtime.be.withtimebe.domain.log.converter; + +import java.time.LocalDate; +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.entity.VisitLog; + +public class VisitLogConverter { + + public static VisitLogResponseDTO.DailyVisitLogList toDailyVisitLogList(List visitLogList) { + + Map result = visitLogList.stream() + .collect(Collectors.groupingBy(VisitLog::getDate, Collectors.summingLong(VisitLog::getCount))); + + List 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(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java new file mode 100644 index 0000000..e01dd81 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java @@ -0,0 +1,20 @@ +package org.withtime.be.withtimebe.domain.log.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import lombok.Builder; + +public class VisitLogResponseDTO { + + @Builder + public record DailyVisitLogList( + List dailyVisitLogList + ) {} + + @Builder + public record DailyVisitLog( + LocalDate date, + Long count + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java new file mode 100644 index 0000000..b7c1b59 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java @@ -0,0 +1,11 @@ +package org.withtime.be.withtimebe.domain.log.repository; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.withtime.be.withtimebe.domain.log.entity.VisitLog; + +public interface VisitLogRepository extends JpaRepository { + List findByDateBetween(LocalDate dateAfter, LocalDate dateBefore); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java new file mode 100644 index 0000000..35193e1 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.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.entity.VisitLog; + +public interface VisitLogQueryService { + List findDailyVisitLogList(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java new file mode 100644 index 0000000..9097097 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java @@ -0,0 +1,30 @@ +package org.withtime.be.withtimebe.domain.log.service.query; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.repository.VisitLogRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class VisitLogQueryServiceImpl implements VisitLogQueryService { + + private final VisitLogRepository visitLogRepository; + + @Override + public List findDailyVisitLogList() { + + LocalDate dateBefore = LocalDate.now(); + LocalDate dateAfter = dateBefore.minusDays(6); + + return visitLogRepository.findByDateBetween(dateBefore, dateAfter); + } +} From 193826a3c34bc90ba12d9a61ac9efadee5184559 Mon Sep 17 00:00:00 2001 From: pywoo Date: Fri, 11 Jul 2025 22:19:21 +0900 Subject: [PATCH 03/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=ED=95=98=EB=A3=A8?= =?UTF-8?q?=EB=8F=99=EC=95=88=20=EB=B0=A9=EB=AC=B8=EC=9E=90=20=EC=88=98=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=8C=80=EB=B3=84=20=EC=A1=B0=ED=9A=8C=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 --- .../query/VisitLogQueryController.java | 20 ++++++++++++++--- .../log/converter/VisitLogConverter.java | 22 +++++++++++++++++++ .../log/dto/response/VisitLogResponseDTO.java | 13 +++++++++++ .../log/repository/VisitLogRepository.java | 1 + .../service/query/VisitLogQueryService.java | 2 ++ .../query/VisitLogQueryServiceImpl.java | 5 +++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java index be2f358..175b477 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java @@ -2,20 +2,19 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; 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.entity.VisitLog; import org.withtime.be.withtimebe.domain.log.service.query.VisitLogQueryService; -import org.withtime.be.withtimebe.global.annotation.SwaggerPageable; 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; @@ -37,4 +36,19 @@ public DefaultResponse findDailyVisitLogL VisitLogResponseDTO.DailyVisitLogList response = VisitLogConverter.toDailyVisitLogList(result); return DefaultResponse.ok(response); } + + @Operation(summary = "하루동안 시간대 별 방문자 수 추이 조회 API Only Admin by 피우", description = "하루동안 시간대 별 방문자 수 추이 조회 API 입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @Parameter(name = "date", description = "입력 예시) 2025-01-01") + @GetMapping("/hourly") + public DefaultResponse findHourlyVisitLog( + @RequestParam("date") LocalDate date + ) { + List result = visitLogQueryService.findHourlyVisitLogList(date); + VisitLogResponseDTO.HourlyVisitLogList response = VisitLogConverter.toHourlyVisitLogList(date, result); + return DefaultResponse.ok(response); + } + } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java index 89ef60a..14fafe2 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java @@ -1,6 +1,7 @@ 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; @@ -32,4 +33,25 @@ public static VisitLogResponseDTO.DailyVisitLog toDailyVisitLog(LocalDate localD .count(totalCount) .build(); } + + public static VisitLogResponseDTO.HourlyVisitLogList toHourlyVisitLogList(LocalDate date, List visitLogList) { + + List 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(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java index e01dd81..cb8a035 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/dto/response/VisitLogResponseDTO.java @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.log.dto.response; import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import lombok.Builder; @@ -17,4 +18,16 @@ public record DailyVisitLog( LocalDate date, Long count ) {} + + @Builder + public record HourlyVisitLogList( + LocalDate date, + List hourlyVisitLogList + ) {} + + @Builder + public record HourlyVisitLog( + LocalTime hour, + Long count + ) {} } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java index b7c1b59..9535566 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java @@ -8,4 +8,5 @@ public interface VisitLogRepository extends JpaRepository { List findByDateBetween(LocalDate dateAfter, LocalDate dateBefore); + List findByDate(LocalDate date); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java index 35193e1..b2b732b 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java @@ -1,9 +1,11 @@ package org.withtime.be.withtimebe.domain.log.service.query; +import java.time.LocalDate; import java.util.List; import org.withtime.be.withtimebe.domain.log.entity.VisitLog; public interface VisitLogQueryService { List findDailyVisitLogList(); + List findHourlyVisitLogList(LocalDate date); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java index 9097097..62d73e5 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java @@ -27,4 +27,9 @@ public List findDailyVisitLogList() { return visitLogRepository.findByDateBetween(dateBefore, dateAfter); } + + @Override + public List findHourlyVisitLogList(LocalDate date) { + return visitLogRepository.findByDate(date); + } } From ed0a65eadf0d79b7dcfbd182f183aee4dcf58ab0 Mon Sep 17 00:00:00 2001 From: pywoo Date: Fri, 11 Jul 2025 23:37:40 +0900 Subject: [PATCH 04/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=B0=A9=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20=EC=A7=91=EA=B3=84=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EB=B0=8F=20=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=9F=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/WithTimeBeApplication.java | 8 +++ .../interceptor/VisitCountInterceptor.java | 61 +++++++++++++++++++ .../log/scheduler/VisitCountScheduler.java | 60 ++++++++++++++++++ .../withtimebe/global/config/WebConfig.java | 9 +++ 4 files changed, 138 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.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..4ac8c4c 100644 --- a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java +++ b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java @@ -1,9 +1,15 @@ package org.withtime.be.withtimebe; +import java.util.TimeZone; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; + +import jakarta.annotation.PostConstruct; +@EnableScheduling @EnableJpaAuditing @SpringBootApplication public class WithTimeBeApplication { @@ -12,4 +18,6 @@ public static void main(String[] args) { SpringApplication.run(WithTimeBeApplication.class, args); } + @PostConstruct + public void init() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } // JVM 기본 TimeZone 설정 } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java b/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java new file mode 100644 index 0000000..5365a05 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java @@ -0,0 +1,61 @@ +package org.withtime.be.withtimebe.domain.log.interceptor; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; + +@Component +@AllArgsConstructor +public class VisitCountInterceptor implements HandlerInterceptor { + + private final RedisTemplate redisTemplate; + + // 리버스 프록시 확장 고려 + private static final String[] PROXY_HEADER_NAMES = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + String clientIp = getClientIp(request); + LocalDate today = LocalDate.now(); + Integer hour = LocalTime.now().getHour(); + + // Key + String redisKey = String.format("visitCount:%s:%02d", today.toString(), hour); + + // Set + redisTemplate.opsForSet().add(redisKey, clientIp); + + // TTL + Long expire = redisTemplate.getExpire(redisKey); + if (expire < 0) { + redisTemplate.expire(redisKey, Duration.ofDays(1)); + } + + return true; + } + + private String getClientIp(HttpServletRequest request) { + for (String header : PROXY_HEADER_NAMES) { + String ip = request.getHeader(header); + if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { + return ip.split(",")[0].trim(); + } + } + return request.getRemoteAddr(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java new file mode 100644 index 0000000..e5295d7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java @@ -0,0 +1,60 @@ +package org.withtime.be.withtimebe.domain.log.scheduler; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Set; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.repository.VisitLogRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class VisitCountScheduler { + + private final RedisTemplate redisTemplate; + private final VisitLogRepository visitLogRepository; + + // 매 정각마다 실행 + @Scheduled(cron = "0 0 * * * *") + @Transactional + public void saveHourlyVisitCounts() { + + LocalDate today = LocalDate.now(); + Integer hour = LocalTime.now().getHour(); + + if (hour == 0) { + today = today.minusDays(1); + hour = 23; + } else { + hour--; + } + + // Key + String redisKey = String.format("visitCount:%s:%02d", today, hour); + + // Set + Set ipSet = redisTemplate.opsForSet().members(redisKey); + + // Count + Long count = (ipSet != null) ? ipSet.size() : 0L; + + // Entity + VisitLog visitLog = VisitLog.builder() + .date(today) + .hour(LocalTime.of(hour, 0)) + .count(count) + .build(); + + // JPA Save + visitLogRepository.save(visitLog); + + // Delete from Redis + redisTemplate.delete(redisKey); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java b/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java index 05eb654..0d95997 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java +++ b/src/main/java/org/withtime/be/withtimebe/global/config/WebConfig.java @@ -3,7 +3,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.withtime.be.withtimebe.domain.log.interceptor.VisitCountInterceptor; import org.withtime.be.withtimebe.global.security.annotation.resolver.AuthenticatedMemberResolver; import java.util.List; @@ -13,9 +15,16 @@ public class WebConfig implements WebMvcConfigurer { private final AuthenticatedMemberResolver authenticatedMemberResolver; + private final VisitCountInterceptor visitCountInterceptor; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(authenticatedMemberResolver); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(visitCountInterceptor) + .addPathPatterns("/**"); + } } From 48fadbdf4c94dadc01be341cc2cfa5e11719aca9 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 20:56:45 +0900 Subject: [PATCH 05/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20Payments=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EC=8B=AD=20=EB=A7=8C=EB=A3=8C=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtime/be/withtimebe/domain/member/entity/Payments.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java index 6eb2b39..076ebcc 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java @@ -5,6 +5,7 @@ import org.withtime.be.withtimebe.domain.member.entity.enums.BillingStatus; import org.withtime.be.withtimebe.global.common.BaseEntity; +import java.time.LocalDate; import java.time.LocalDateTime; @Entity @@ -39,4 +40,7 @@ public class Payments extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; + + @Column(name = "membership_expire_date") + private LocalDateTime membershipExpireDate; } From ea15c4717ef55804b6457bc7880a31e1e90db312 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 20:57:39 +0900 Subject: [PATCH 06/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20Member=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=EC=97=90=20paymentList=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20@OneToMany=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/domain/member/entity/Member.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java index fb0aba4..78f6170 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java @@ -2,6 +2,8 @@ import jakarta.persistence.*; import lombok.*; + +import org.hibernate.annotations.BatchSize; import org.withtime.be.withtimebe.domain.member.entity.enums.Gender; import org.withtime.be.withtimebe.domain.member.entity.enums.ProviderType; import org.withtime.be.withtimebe.domain.member.entity.enums.Role; @@ -10,6 +12,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -59,4 +63,9 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) private Role role; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + @BatchSize(size = 10) + @Builder.Default + private List paymentList = new ArrayList<>(); } From bb7b272979c2787c064f8c35bd10c65b84a55ccf Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 20:58:03 +0900 Subject: [PATCH 07/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EC=8B=AD=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/MemberQueryController.java | 40 ++++++++++++ .../member/converter/MemberConverter.java | 61 +++++++++++++++++++ .../dto/response/MemberResponseDTO.java | 27 ++++++++ .../member/service/MemberQueryService.java | 4 ++ .../service/MemberQueryServiceImpl.java | 8 +++ 5 files changed, 140 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java new file mode 100644 index 0000000..af40d67 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java @@ -0,0 +1,40 @@ +package org.withtime.be.withtimebe.domain.member.controller.query; + +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.member.converter.MemberConverter; +import org.withtime.be.withtimebe.domain.member.dto.response.MemberResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.service.MemberQueryService; +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/members") +public class MemberQueryController { + + private final MemberQueryService memberQueryService; + + @Operation(summary = "멤버십 목록 전체 조회 API by 피우", description = "멤버십 목록 전체 조회 API입니다. (검색어 X)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다.") + }) + @SwaggerPageable + @GetMapping + public DefaultResponse findMemberList(@PageableDefault(page = 0, size = 10) Pageable pageable) { + Page result = memberQueryService.findMemberList(pageable); + MemberResponseDTO.MemberList response = MemberConverter.toMemberList(result); + return DefaultResponse.ok(response); + } + +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java new file mode 100644 index 0000000..0de64f5 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java @@ -0,0 +1,61 @@ +package org.withtime.be.withtimebe.domain.member.converter; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.withtime.be.withtimebe.domain.member.dto.response.MemberResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.entity.Payments; +import org.withtime.be.withtimebe.domain.member.entity.enums.BillingStatus; +import org.withtime.be.withtimebe.domain.member.entity.enums.UserRank; + +public class MemberConverter { + + // Response DTO : MemberResponseDTO.MemberList + public static MemberResponseDTO.MemberList toMemberList(Page memberPage) { + + List memberList = memberPage.stream() + .map(MemberConverter::toMember) + .toList(); + + return MemberResponseDTO.MemberList.builder() + .memberList(memberList) + .totalPages(memberPage.getTotalPages()) + .currentPage(memberPage.getNumber()) + .currentSize(memberPage.getNumberOfElements()) + .hasNextPage(memberPage.hasNext()) + .build(); + } + + // Response DTO : MemberResponseDTO.Member + public static MemberResponseDTO.Member toMember(Member member) { + + List paymentsList = member.getPaymentList(); + + // 멤버십 보유 여부 + boolean hasMembership = member.getUserRank().equals(UserRank.PREMIUM); + + // 멤버십 총 가입 기간 + LocalDate today = LocalDate.now(); + + Long totalDays = paymentsList.stream() + .filter(payment -> payment.getBillingStatus().equals(BillingStatus.COMPLETED)) + .mapToLong(payment -> { + LocalDate start = payment.getBillingDate().toLocalDate(); + LocalDate end = payment.getMembershipExpireDate().toLocalDate(); + if (end.isBefore(today)) return ChronoUnit.DAYS.between(start, end); // 만료 멤버십 + else return ChronoUnit.DAYS.between(start, today); // 아직 유효한 멤버십 + }) + .sum(); + + return MemberResponseDTO.Member.builder() + .memberId(member.getId()) + .name(member.getNickname()) + .hasMembership(hasMembership) + .membershipDuration(totalDays) + .createdAt(member.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java new file mode 100644 index 0000000..eead1b5 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java @@ -0,0 +1,27 @@ +package org.withtime.be.withtimebe.domain.member.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.Builder; + +public class MemberResponseDTO { + + @Builder + public record MemberList( + List memberList, + Integer totalPages, // 전체 페이지 개수 + Integer currentPage, // 현재 페이지 번호 + Integer currentSize, // 현재 페이지의 크기 + Boolean hasNextPage // 다음 페이지 존재 여부 + ) {} + + @Builder + public record Member( + Long memberId, // 게시글 식별자 값 + String name, // 사용자 닉네임 + Boolean hasMembership, // 멤버십 유무 + Long membershipDuration, // 멤버십 유지기간 + LocalDateTime createdAt // 회원가입 일자 + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java index d5b99a8..efaa297 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java @@ -1,7 +1,11 @@ package org.withtime.be.withtimebe.domain.member.service; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.withtime.be.withtimebe.domain.member.entity.Member; public interface MemberQueryService { Member findById(Long id); + + Page findMemberList(Pageable pageable); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java index b853a3d..60fba28 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java @@ -1,6 +1,9 @@ package org.withtime.be.withtimebe.domain.member.service; import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; @@ -17,4 +20,9 @@ public class MemberQueryServiceImpl implements MemberQueryService { public Member findById(Long id) { return memberRepository.findById(id).orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); } + + @Override + public Page findMemberList(Pageable pageable) { + return memberRepository.findAll(pageable); + } } From 2d34eba77d5667fa3177ed9618a2512a073ccef0 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 22:31:05 +0900 Subject: [PATCH 08/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EC=8B=AD=20=EC=97=B0=EC=9E=A5=20=EB=B0=8F=20=EC=B7=A8=EC=86=8C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/MemberCommandController.java | 44 +++++++++++++++++ .../query/MemberQueryController.java | 10 ++-- .../member/converter/MemberConverter.java | 18 +++---- .../member/dto/request/MemberRequestDTO.java | 19 ++++++++ .../dto/response/MemberResponseDTO.java | 8 ++-- .../domain/member/entity/Member.java | 5 +- .../domain/member/entity/Payments.java | 7 ++- .../service/command/MemberCommandService.java | 9 ++++ .../command/MemberCommandServiceImpl.java | 48 +++++++++++++++++++ .../{ => query}/MemberQueryService.java | 2 +- .../{ => query}/MemberQueryServiceImpl.java | 4 +- .../global/error/code/MemberErrorCode.java | 2 +- .../global/security/SecurityConfig.java | 16 ++++++- .../global/security/filter/JwtFilter.java | 2 +- 14 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java rename src/main/java/org/withtime/be/withtimebe/domain/member/service/{ => query}/MemberQueryService.java (81%) rename src/main/java/org/withtime/be/withtimebe/domain/member/service/{ => query}/MemberQueryServiceImpl.java (85%) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java new file mode 100644 index 0000000..c1f59d3 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java @@ -0,0 +1,44 @@ +package org.withtime.be.withtimebe.domain.member.controller.command; + +import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.withtime.be.withtimebe.domain.member.converter.MemberConverter; +import org.withtime.be.withtimebe.domain.member.dto.request.MemberRequestDTO; +import org.withtime.be.withtimebe.domain.member.dto.response.MemberResponseDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.service.command.MemberCommandService; +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 jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") +public class MemberCommandController { + + private final MemberCommandService memberCommandService; + + @Operation(summary = "멤버십 관리 API by 피우 [Only Admin]", description = "멤버십 관리 API입니다. 어드민만 사용 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공입니다."), + @ApiResponse(responseCode = "400", + description = """ + - MEMBER404_1 : 사용자를 찾지 못했습니다. + - MEMBER404_2 : 멤버십을 찾지 못했습니다. + """) + }) + @SwaggerPageable + @PutMapping("/{memberId}/membership") + public DefaultResponse updateMembership(@RequestBody @Valid MemberRequestDTO.UpdateMembership request) { + Member result = memberCommandService.updateMembership(request); + MemberResponseDTO.Membership response = MemberConverter.toMembership(result); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java index af40d67..782fc0a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/query/MemberQueryController.java @@ -10,7 +10,7 @@ import org.withtime.be.withtimebe.domain.member.converter.MemberConverter; import org.withtime.be.withtimebe.domain.member.dto.response.MemberResponseDTO; import org.withtime.be.withtimebe.domain.member.entity.Member; -import org.withtime.be.withtimebe.domain.member.service.MemberQueryService; +import org.withtime.be.withtimebe.domain.member.service.query.MemberQueryService; import org.withtime.be.withtimebe.global.annotation.SwaggerPageable; import io.swagger.v3.oas.annotations.Operation; @@ -25,15 +25,15 @@ public class MemberQueryController { private final MemberQueryService memberQueryService; - @Operation(summary = "멤버십 목록 전체 조회 API by 피우", description = "멤버십 목록 전체 조회 API입니다. (검색어 X)") + @Operation(summary = "멤버십 목록 전체 조회 API by 피우 [Only Admin]", description = "멤버십 목록 전체 조회 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다.") }) @SwaggerPageable - @GetMapping - public DefaultResponse findMemberList(@PageableDefault(page = 0, size = 10) Pageable pageable) { + @GetMapping("/membership") + public DefaultResponse findMembershipList(@PageableDefault(page = 0, size = 10) Pageable pageable) { Page result = memberQueryService.findMemberList(pageable); - MemberResponseDTO.MemberList response = MemberConverter.toMemberList(result); + MemberResponseDTO.MembershipList response = MemberConverter.toMembershipList(result); return DefaultResponse.ok(response); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java index 0de64f5..dcdb03c 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/converter/MemberConverter.java @@ -13,15 +13,15 @@ public class MemberConverter { - // Response DTO : MemberResponseDTO.MemberList - public static MemberResponseDTO.MemberList toMemberList(Page memberPage) { + // Response DTO : MemberResponseDTO.MembershipList + public static MemberResponseDTO.MembershipList toMembershipList(Page memberPage) { - List memberList = memberPage.stream() - .map(MemberConverter::toMember) + List memberList = memberPage.stream() + .map(MemberConverter::toMembership) .toList(); - return MemberResponseDTO.MemberList.builder() - .memberList(memberList) + return MemberResponseDTO.MembershipList.builder() + .membershipList(memberList) .totalPages(memberPage.getTotalPages()) .currentPage(memberPage.getNumber()) .currentSize(memberPage.getNumberOfElements()) @@ -29,8 +29,8 @@ public static MemberResponseDTO.MemberList toMemberList(Page memberPage) .build(); } - // Response DTO : MemberResponseDTO.Member - public static MemberResponseDTO.Member toMember(Member member) { + // Response DTO : MemberResponseDTO.Membership + public static MemberResponseDTO.Membership toMembership(Member member) { List paymentsList = member.getPaymentList(); @@ -50,7 +50,7 @@ public static MemberResponseDTO.Member toMember(Member member) { }) .sum(); - return MemberResponseDTO.Member.builder() + return MemberResponseDTO.Membership.builder() .memberId(member.getId()) .name(member.getNickname()) .hasMembership(hasMembership) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java new file mode 100644 index 0000000..681bc99 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java @@ -0,0 +1,19 @@ +package org.withtime.be.withtimebe.domain.member.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Builder; + +public class MemberRequestDTO { + + @Builder + public record UpdateMembership( + @NotNull(message = "회원의 식별자 값을 입력해주세요") + Long memberId, + @NotNull(message = "멤버십 연장 일수를 입력해주세요") + @PositiveOrZero(message = "0 이상의 수를 입력해주세요") + Long extendDays, + @NotNull(message = "멤비십 취소 여부를 입력해주세요") + Boolean cancelMembership + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java index eead1b5..b223cb9 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/response/MemberResponseDTO.java @@ -8,8 +8,8 @@ public class MemberResponseDTO { @Builder - public record MemberList( - List memberList, + public record MembershipList( + List membershipList, // 멤버십 목록 Integer totalPages, // 전체 페이지 개수 Integer currentPage, // 현재 페이지 번호 Integer currentSize, // 현재 페이지의 크기 @@ -17,8 +17,8 @@ public record MemberList( ) {} @Builder - public record Member( - Long memberId, // 게시글 식별자 값 + public record Membership( + Long memberId, // 회원 식별자 값 String name, // 사용자 닉네임 Boolean hasMembership, // 멤버십 유무 Long membershipDuration, // 멤버십 유지기간 diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java index 78f6170..bbd2f65 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Member.java @@ -5,7 +5,6 @@ import org.hibernate.annotations.BatchSize; import org.withtime.be.withtimebe.domain.member.entity.enums.Gender; -import org.withtime.be.withtimebe.domain.member.entity.enums.ProviderType; import org.withtime.be.withtimebe.domain.member.entity.enums.Role; import org.withtime.be.withtimebe.domain.member.entity.enums.UserRank; import org.withtime.be.withtimebe.global.common.BaseEntity; @@ -68,4 +67,8 @@ public class Member extends BaseEntity { @BatchSize(size = 10) @Builder.Default private List paymentList = new ArrayList<>(); + + public void updateUserRank(UserRank userRank) { + this.userRank = userRank; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java index 076ebcc..0df6403 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/entity/Payments.java @@ -1,11 +1,12 @@ package org.withtime.be.withtimebe.domain.member.entity; import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; import lombok.*; + import org.withtime.be.withtimebe.domain.member.entity.enums.BillingStatus; import org.withtime.be.withtimebe.global.common.BaseEntity; -import java.time.LocalDate; import java.time.LocalDateTime; @Entity @@ -43,4 +44,8 @@ public class Payments extends BaseEntity { @Column(name = "membership_expire_date") private LocalDateTime membershipExpireDate; + + public void updateExpireDate(LocalDateTime membershipExpireDate) { + this.membershipExpireDate = membershipExpireDate; + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java new file mode 100644 index 0000000..1c04bc7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java @@ -0,0 +1,9 @@ +package org.withtime.be.withtimebe.domain.member.service.command; + +import org.withtime.be.withtimebe.domain.member.dto.request.MemberRequestDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; + +public interface MemberCommandService { + + Member updateMembership(MemberRequestDTO.UpdateMembership request); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java new file mode 100644 index 0000000..a4f0cd3 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java @@ -0,0 +1,48 @@ +package org.withtime.be.withtimebe.domain.member.service.command; + +import java.time.LocalDateTime; +import java.util.Comparator; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.withtime.be.withtimebe.domain.member.dto.request.MemberRequestDTO; +import org.withtime.be.withtimebe.domain.member.entity.Member; +import org.withtime.be.withtimebe.domain.member.entity.Payments; +import org.withtime.be.withtimebe.domain.member.entity.enums.BillingStatus; +import org.withtime.be.withtimebe.domain.member.entity.enums.UserRank; +import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; +import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; +import org.withtime.be.withtimebe.global.error.exception.MemberException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = false) +public class MemberCommandServiceImpl implements MemberCommandService { + + private final MemberRepository memberRepository; + + @Override + public Member updateMembership(MemberRequestDTO.UpdateMembership request) { + + Member member = memberRepository.findById(request.memberId()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + // 가장 최근 멤버십 결제 정보 탐색 + Payments latestPayment = member.getPaymentList().stream() + .filter(payment -> payment.getBillingStatus().equals(BillingStatus.COMPLETED)) + .max(Comparator.comparing(Payments::getMembershipExpireDate)) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBERSHIP_NOT_FOUND)); + + if (request.extendDays() > 0) { + latestPayment.updateExpireDate(latestPayment.getMembershipExpireDate().plusDays(request.extendDays())); + } + if (request.cancelMembership() == true) { + latestPayment.updateExpireDate(LocalDateTime.now()); + member.updateUserRank(UserRank.COMMON); // 일반 등급으로 변경 + } + + return member; + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryService.java similarity index 81% rename from src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java rename to src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryService.java index efaa297..6d81102 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryService.java @@ -1,4 +1,4 @@ -package org.withtime.be.withtimebe.domain.member.service; +package org.withtime.be.withtimebe.domain.member.service.query; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryServiceImpl.java similarity index 85% rename from src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java rename to src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryServiceImpl.java index 60fba28..4569111 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/MemberQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/query/MemberQueryServiceImpl.java @@ -1,10 +1,11 @@ -package org.withtime.be.withtimebe.domain.member.service; +package org.withtime.be.withtimebe.domain.member.service.query; import lombok.RequiredArgsConstructor; 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.member.entity.Member; import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; @@ -12,6 +13,7 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MemberQueryServiceImpl implements MemberQueryService { private final MemberRepository memberRepository; diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java index 587e3be..1637cae 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/MemberErrorCode.java @@ -9,7 +9,7 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "사용자를 찾지 못했습니다."), - ; + MEMBERSHIP_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_2", "멤버십을 찾지 못했습니다."); private final HttpStatus httpStatus; private final String code; 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 e0f9195..dd9ef3b 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 @@ -6,6 +6,7 @@ import org.namul.api.payload.writer.FailureResponseWriter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -19,11 +20,13 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.withtime.be.withtimebe.domain.auth.service.query.TokenStorageQueryService; -import org.withtime.be.withtimebe.domain.member.service.MemberQueryService; +import org.withtime.be.withtimebe.domain.member.service.query.MemberQueryService; import org.withtime.be.withtimebe.global.security.filter.JsonLoginFilter; import org.withtime.be.withtimebe.global.security.filter.JwtFilter; import org.withtime.be.withtimebe.global.security.handler.CustomAccessDeniedHandler; @@ -50,12 +53,17 @@ public class SecurityConfig { "/v3/api-docs/**" }; + private RequestMatcher[] admin = { + requestMatcher(HttpMethod.GET, API_PREFIX + "/members/membership/**"), + requestMatcher(HttpMethod.PUT, API_PREFIX + "/members/**") + }; + @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request + .requestMatchers(admin).hasRole("ADMIN") .requestMatchers(allowUrl).permitAll() - .requestMatchers(API_PREFIX + "/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jsonLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class) @@ -120,4 +128,8 @@ private CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", configuration); return source; } + + private RequestMatcher requestMatcher(HttpMethod method, String url) { + return PathPatternRequestMatcher.withDefaults().matcher(method, url); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java b/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java index fd23f49..5792111 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java +++ b/src/main/java/org/withtime/be/withtimebe/global/security/filter/JwtFilter.java @@ -20,7 +20,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.withtime.be.withtimebe.domain.auth.service.query.TokenStorageQueryService; import org.withtime.be.withtimebe.domain.member.entity.Member; -import org.withtime.be.withtimebe.domain.member.service.MemberQueryService; +import org.withtime.be.withtimebe.domain.member.service.query.MemberQueryService; import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; import org.withtime.be.withtimebe.global.security.domain.CustomUserDetails; import org.withtime.be.withtimebe.global.util.CookieUtil; From 5db878db6c62478aa1fb4c082cec27942330a181 Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 23:45:06 +0900 Subject: [PATCH 09/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20SecurityConfig=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=B2=98=EB=A6=AC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/command/MemberCommandController.java | 10 +++++++--- .../domain/member/dto/request/MemberRequestDTO.java | 2 -- .../member/service/command/MemberCommandService.java | 2 +- .../service/command/MemberCommandServiceImpl.java | 4 ++-- .../be/withtimebe/global/security/SecurityConfig.java | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java index c1f59d3..ba6c4d4 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/controller/command/MemberCommandController.java @@ -1,6 +1,7 @@ package org.withtime.be.withtimebe.domain.member.controller.command; import org.namul.api.payload.response.DefaultResponse; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,7 +26,7 @@ public class MemberCommandController { private final MemberCommandService memberCommandService; - @Operation(summary = "멤버십 관리 API by 피우 [Only Admin]", description = "멤버십 관리 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "어드민 멤버십 관리하기 API by 피우 [Only Admin]", description = "어드민 창의 멤버십 수정하기 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다."), @ApiResponse(responseCode = "400", @@ -36,8 +37,11 @@ public class MemberCommandController { }) @SwaggerPageable @PutMapping("/{memberId}/membership") - public DefaultResponse updateMembership(@RequestBody @Valid MemberRequestDTO.UpdateMembership request) { - Member result = memberCommandService.updateMembership(request); + public DefaultResponse updateMembership( + @PathVariable Long memberId, + @RequestBody @Valid MemberRequestDTO.UpdateMembership request + ) { + Member result = memberCommandService.updateMembership(request, memberId); MemberResponseDTO.Membership response = MemberConverter.toMembership(result); return DefaultResponse.ok(response); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java index 681bc99..314b7c1 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/dto/request/MemberRequestDTO.java @@ -8,8 +8,6 @@ public class MemberRequestDTO { @Builder public record UpdateMembership( - @NotNull(message = "회원의 식별자 값을 입력해주세요") - Long memberId, @NotNull(message = "멤버십 연장 일수를 입력해주세요") @PositiveOrZero(message = "0 이상의 수를 입력해주세요") Long extendDays, diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java index 1c04bc7..859793e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandService.java @@ -5,5 +5,5 @@ public interface MemberCommandService { - Member updateMembership(MemberRequestDTO.UpdateMembership request); + Member updateMembership(MemberRequestDTO.UpdateMembership request, Long memberId); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java index a4f0cd3..07f241a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/member/service/command/MemberCommandServiceImpl.java @@ -24,9 +24,9 @@ public class MemberCommandServiceImpl implements MemberCommandService { private final MemberRepository memberRepository; @Override - public Member updateMembership(MemberRequestDTO.UpdateMembership request) { + public Member updateMembership(MemberRequestDTO.UpdateMembership request, Long memberId) { - Member member = memberRepository.findById(request.memberId()) + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); // 가장 최근 멤버십 결제 정보 탐색 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 dd9ef3b..9696a82 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,8 +54,8 @@ public class SecurityConfig { }; private RequestMatcher[] admin = { - requestMatcher(HttpMethod.GET, API_PREFIX + "/members/membership/**"), - requestMatcher(HttpMethod.PUT, API_PREFIX + "/members/**") + requestMatcher(HttpMethod.GET, API_PREFIX + "/members/membership"), + requestMatcher(HttpMethod.PUT, API_PREFIX + "/members/*/membership") }; @Bean From 1b5d3b11f2d8bd271fb21b6aa576d5da51021eff Mon Sep 17 00:00:00 2001 From: pywoo Date: Mon, 14 Jul 2025 23:56:18 +0900 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20VisitLog=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=9D=B8=EA=B0=80=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/controller/query/VisitLogQueryController.java | 8 ++++---- .../be/withtimebe/global/security/SecurityConfig.java | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java index 175b477..9b182e9 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java @@ -21,12 +21,12 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/admin/visit-logs") +@RequestMapping("/api/v1/visit-logs") public class VisitLogQueryController { private final VisitLogQueryService visitLogQueryService; - @Operation(summary = "최근 일주일 간 일별 방문자 수 조회 API Only Admin by 피우", description = "일주일 간 일별 방문자 수 조회 API입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "최근 일주일 간 일별 방문자 수 조회 API by 피우 [Only Admin]", description = "일주일 간 일별 방문자 수 조회 API입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다.") }) @@ -37,11 +37,11 @@ public DefaultResponse findDailyVisitLogL return DefaultResponse.ok(response); } - @Operation(summary = "하루동안 시간대 별 방문자 수 추이 조회 API Only Admin by 피우", description = "하루동안 시간대 별 방문자 수 추이 조회 API 입니다. 어드민만 사용 가능합니다.") + @Operation(summary = "하루동안 시간대 별 방문자 수 추이 조회 API by 피우 [Only Admin]", description = "하루동안 시간대 별 방문자 수 추이 조회 API 입니다. 어드민만 사용 가능합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공입니다.") }) - @Parameter(name = "date", description = "입력 예시) 2025-01-01") + @Parameter(name = "date", description = "예) 2025-01-01") @GetMapping("/hourly") public DefaultResponse findHourlyVisitLog( @RequestParam("date") LocalDate date 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 9696a82..b2b9ea9 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 @@ -55,7 +55,8 @@ public class SecurityConfig { private RequestMatcher[] admin = { requestMatcher(HttpMethod.GET, API_PREFIX + "/members/membership"), - requestMatcher(HttpMethod.PUT, API_PREFIX + "/members/*/membership") + requestMatcher(HttpMethod.PUT, API_PREFIX + "/members/*/membership"), + requestMatcher(HttpMethod.GET, API_PREFIX + "/visit-logs/**"), }; @Bean From f03bb5c8dec3dd63505f635c6faf5ec6c38b0165 Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 16 Jul 2025 19:34:53 +0900 Subject: [PATCH 11/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20DatePlace=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/domain/date/entity/BusinessTime.java | 1 + .../be/withtimebe/domain/date/entity/DatePlaceDateCourse.java | 2 ++ .../withtimebe/domain/date/entity/DatePlacePlaceCategory.java | 1 + .../org/withtime/be/withtimebe/domain/date/entity/Item.java | 2 ++ .../domain/{date => dateplace}/entity/DatePlace.java | 4 ++-- .../domain/{date => dateplace}/entity/enums/PlaceType.java | 2 +- 6 files changed, 9 insertions(+), 3 deletions(-) rename src/main/java/org/withtime/be/withtimebe/domain/{date => dateplace}/entity/DatePlace.java (89%) rename src/main/java/org/withtime/be/withtimebe/domain/{date => dateplace}/entity/enums/PlaceType.java (82%) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/BusinessTime.java b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/BusinessTime.java index 2c955d3..cca0cd7 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/BusinessTime.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/BusinessTime.java @@ -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; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlaceDateCourse.java b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlaceDateCourse.java index fbb5009..1fcbb73 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlaceDateCourse.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlaceDateCourse.java @@ -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 diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlacePlaceCategory.java b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlacePlaceCategory.java index 92a6303..34e8a88 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlacePlaceCategory.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlacePlaceCategory.java @@ -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 diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/Item.java b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/Item.java index adb9fc1..f34a0cf 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/Item.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/date/entity/Item.java @@ -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 diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlace.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java similarity index 89% rename from src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlace.java rename to src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java index 736aec3..f77ce19 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/DatePlace.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java @@ -1,9 +1,9 @@ -package org.withtime.be.withtimebe.domain.date.entity; +package org.withtime.be.withtimebe.domain.dateplace.entity; import jakarta.persistence.*; import lombok.*; -import org.withtime.be.withtimebe.domain.date.entity.enums.PlaceType; +import org.withtime.be.withtimebe.domain.dateplace.entity.enums.PlaceType; import org.withtime.be.withtimebe.global.common.BaseEntity; @Entity diff --git a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/enums/PlaceType.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/enums/PlaceType.java similarity index 82% rename from src/main/java/org/withtime/be/withtimebe/domain/date/entity/enums/PlaceType.java rename to src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/enums/PlaceType.java index c01432c..8856361 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/date/entity/enums/PlaceType.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/enums/PlaceType.java @@ -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; From bac03f52e090d3bbc6269d0664321ac0bec48df4 Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 16 Jul 2025 21:54:01 +0900 Subject: [PATCH 12/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=9D=98=20=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DatePlaceQueryController.java | 42 +++++++++++++++ .../converter/DatePlaceConverter.java | 54 +++++++++++++++++++ .../dto/response/DatePlaceResponseDTO.java | 36 +++++++++++++ .../domain/dateplace/entity/DatePlace.java | 12 +++++ .../repository/DatePlaceRepository.java | 7 +++ .../service/query/DatePlaceQueryService.java | 9 ++++ .../query/DatePlaceQueryServiceImpl.java | 24 +++++++++ .../global/security/SecurityConfig.java | 4 +- 8 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/dateplace/repository/DatePlaceRepository.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java new file mode 100644 index 0000000..017a978 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java @@ -0,0 +1,42 @@ +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.RequestParam; +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 findDatePlaceManagement( + @PageableDefault(page = 0, size = 10) Pageable pageable + ) { + Page result = datePlaceQueryService.findDatePlaces(pageable); + DatePlaceResponseDTO.DatePlaceManagementList response = DatePlaceConverter.toDatePlaceManagementList(result); + return DefaultResponse.ok(response); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java new file mode 100644 index 0000000..0e947d7 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java @@ -0,0 +1,54 @@ +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.DatePlacePlaceCategory; +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 datePlacePage) { + + List 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 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()) + .loadNameAddress(datePlace.getRoadNameAddress()) + .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(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java new file mode 100644 index 0000000..a7f8eee --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java @@ -0,0 +1,36 @@ +package org.withtime.be.withtimebe.domain.dateplace.dto.response; + +import java.util.List; + +import lombok.Builder; + +public class DatePlaceResponseDTO { + + @Builder + public record DatePlaceManagementList( + List datePlaceManagementList, + Integer totalPages, // 전체 페이지 개수 + Integer currentPage, // 현재 페이지 번호 + Integer currentSize, // 현재 페이지의 크기 + Boolean hasNextPage // 다음 페이지 존재 여부 + ) {} + + @Builder + public record DatePlaceManagement( + Long datePlaceId, // 데이트 장소 식별자 값 + String name, // 장소 이름 + String tel, // 전화 번호 + Integer averagePrice, // 평균 가격 + String loadNameAddress, // 도로명 주소 + String lotNumberAddress, // 지번 주소 + String placeType, // 장소 유형 + List placeCategoryList // 카테고리 목록 + ) {} + + @Builder + public record PlaceCategory( + Long placeCategoryId, + String placeCategoryType, + String label + ) {} +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java index f77ce19..fc61f1f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java @@ -1,11 +1,18 @@ package org.withtime.be.withtimebe.domain.dateplace.entity; +import java.util.ArrayList; +import java.util.List; + import jakarta.persistence.*; import lombok.*; +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; +@BatchSize(size = 100) @Entity @Getter @Builder @@ -49,4 +56,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 datePlacePlaceCategoryList = new ArrayList<>(); } \ No newline at end of file diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/repository/DatePlaceRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/repository/DatePlaceRepository.java new file mode 100644 index 0000000..5f78a8a --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/repository/DatePlaceRepository.java @@ -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 { +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryService.java new file mode 100644 index 0000000..ae02b0f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryService.java @@ -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 findDatePlaces(Pageable pageable); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryServiceImpl.java new file mode 100644 index 0000000..6dd7c4f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/service/query/DatePlaceQueryServiceImpl.java @@ -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 findDatePlaces(Pageable pageable) { + return datePlaceRepository.findAll(pageable); + } +} 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 b2b9ea9..1151596 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 @@ -50,13 +50,15 @@ public class SecurityConfig { API_PREFIX + "/notices/**", "/swagger-ui/**", "/swagger-resources/**", - "/v3/api-docs/**" + "/v3/api-docs/**", }; private RequestMatcher[] admin = { requestMatcher(HttpMethod.GET, API_PREFIX + "/members/membership"), requestMatcher(HttpMethod.PUT, API_PREFIX + "/members/*/membership"), requestMatcher(HttpMethod.GET, API_PREFIX + "/visit-logs/**"), + + requestMatcher(HttpMethod.GET, API_PREFIX + "/dateplaces/manage"), }; @Bean From 5f97c7d33fd5147cdf31c56c0e3e6eb754923087 Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 16 Jul 2025 22:22:46 +0900 Subject: [PATCH 13/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=A7=80=EB=B2=88?= =?UTF-8?q?=20=EC=A3=BC=EC=86=8C=EB=A7=8C=20=EB=B0=98=ED=99=98=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 --- .../domain/dateplace/controller/DatePlaceQueryController.java | 1 - .../domain/dateplace/converter/DatePlaceConverter.java | 2 -- .../domain/dateplace/dto/response/DatePlaceResponseDTO.java | 1 - 3 files changed, 4 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java index 017a978..50d7b1f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/controller/DatePlaceQueryController.java @@ -6,7 +6,6 @@ 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.RequestParam; 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; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java index 0e947d7..f148760 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/converter/DatePlaceConverter.java @@ -3,7 +3,6 @@ import java.util.List; import org.springframework.data.domain.Page; -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.dto.response.DatePlaceResponseDTO; import org.withtime.be.withtimebe.domain.dateplace.entity.DatePlace; @@ -36,7 +35,6 @@ public static DatePlaceResponseDTO.DatePlaceManagement toDatePlaceManagement(Dat .name(datePlace.getName()) .tel(datePlace.getTel()) .averagePrice(datePlace.getAveragePrice()) - .loadNameAddress(datePlace.getRoadNameAddress()) .lotNumberAddress(datePlace.getLotNumberAddress()) .placeType(datePlace.getPlaceType().getLabel()) .placeCategoryList(placeCategoryList) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java index a7f8eee..e8e09d5 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/dto/response/DatePlaceResponseDTO.java @@ -21,7 +21,6 @@ public record DatePlaceManagement( String name, // 장소 이름 String tel, // 전화 번호 Integer averagePrice, // 평균 가격 - String loadNameAddress, // 도로명 주소 String lotNumberAddress, // 지번 주소 String placeType, // 장소 유형 List placeCategoryList // 카테고리 목록 From 7314ab1c10964203151972d8f80be164ac85adc2 Mon Sep 17 00:00:00 2001 From: pywoo Date: Wed, 16 Jul 2025 22:35:45 +0900 Subject: [PATCH 14/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=B0=A9=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20=EB=A1=9C=EA=B7=B8=20=EC=9E=84=EC=8B=9C=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A5=B4=EB=9F=AC=20=EB=B0=8F=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/log/interceptor/VisitCountInterceptor.java | 1 + .../domain/log/scheduler/VisitCountScheduler.java | 10 ++++++---- src/main/resources/application-develop.yml | 7 ++++++- src/main/resources/application.yml | 7 ++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java b/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java index 5365a05..d2f19e5 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/interceptor/VisitCountInterceptor.java @@ -30,6 +30,7 @@ public class VisitCountInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // IP 기반 방문자 기록 String clientIp = getClientIp(request); LocalDate today = LocalDate.now(); Integer hour = LocalTime.now().getHour(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java index e5295d7..58ad28f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java @@ -4,6 +4,7 @@ import java.time.LocalTime; import java.util.Set; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -15,13 +16,14 @@ @Component @RequiredArgsConstructor +@ConditionalOnProperty(name = "scheduler.visit-logs.enabled", havingValue = "true") public class VisitCountScheduler { private final RedisTemplate redisTemplate; private final VisitLogRepository visitLogRepository; - // 매 정각마다 실행 - @Scheduled(cron = "0 0 * * * *") + // 매 정각마다 Redis 방문자 로그를 DB에 저장하는 스케쥴러 + @Scheduled(cron = "${scheduler.visit-logs.visit-logs-cron}") @Transactional public void saveHourlyVisitCounts() { @@ -51,10 +53,10 @@ public void saveHourlyVisitCounts() { .count(count) .build(); - // JPA Save + // JPA Save 우선 visitLogRepository.save(visitLog); - // Delete from Redis + // Redis Delete redisTemplate.delete(redisKey); } } diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index e9cfc7a..19d2f85 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -29,4 +29,9 @@ beginner: scope: - Exception web-hook-url: ${DISCORD_WEBHOOK_URL} - enable: true \ No newline at end of file + enable: true + +scheduler: + visit-logs: + enabled: true # 방문자 기록 스케쥴러 활성화 여부 + visit-logs-cron: "0 0 * * * *" # 매 정각 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8e9cfd..994c0a7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -29,4 +29,9 @@ beginner: scope: - Exception web-hook-url: ${DISCORD_WEBHOOK_URL} - enable: false \ No newline at end of file + enable: false + +scheduler: + visit-logs: + enabled: true # 방문자 기록 스케쥴러 활성화 여부 + visit-logs-cron: "0 0 * * * *" # 매 정각 \ No newline at end of file From 5ccfd42f1b342f45b884a6784935aaa875c3a6e8 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 17 Jul 2025 20:38:04 +0900 Subject: [PATCH 15/20] =?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 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build.gradle b/build.gradle index 494dbc6..99adafe 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,19 @@ dependencies { // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Email + 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' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'com.h2database:h2' } tasks.named('test') { From 03ca074c039967087109c6835ab12272eb8cc5bb Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 17 Jul 2025 20:39:42 +0900 Subject: [PATCH 16/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=9F=AC=20=EB=B0=8F=20yml=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/log/scheduler/VisitCountScheduler.java | 9 ++++----- src/main/resources/application-develop.yml | 13 +++++++++++++ src/main/resources/application.yml | 13 +++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java index 58ad28f..e43b042 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/scheduler/VisitCountScheduler.java @@ -9,7 +9,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.model.VisitLog; import org.withtime.be.withtimebe.domain.log.repository.VisitLogRepository; import lombok.RequiredArgsConstructor; @@ -22,9 +22,8 @@ public class VisitCountScheduler { private final RedisTemplate redisTemplate; private final VisitLogRepository visitLogRepository; - // 매 정각마다 Redis 방문자 로그를 DB에 저장하는 스케쥴러 + // 매 정각마다 Redis 방문자 로그를 DB에 저장하는 스케쥴러 (기획 완성되면 변경) @Scheduled(cron = "${scheduler.visit-logs.visit-logs-cron}") - @Transactional public void saveHourlyVisitCounts() { LocalDate today = LocalDate.now(); @@ -46,14 +45,14 @@ public void saveHourlyVisitCounts() { // Count Long count = (ipSet != null) ? ipSet.size() : 0L; - // Entity + // Model VisitLog visitLog = VisitLog.builder() .date(today) .hour(LocalTime.of(hour, 0)) .count(count) .build(); - // JPA Save 우선 + // JPA Save visitLogRepository.save(visitLog); // Redis Delete diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 19d2f85..b095816 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -15,6 +15,19 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mongodb: + uri: mongodb://localhost:27017/withtime + mail: + host: ${MAIL_SENDER_HOST} + port: 587 + username: ${MAIL_SENDER_USERNAME} + password: ${MAIL_SENDER_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true jwt: secret: ${JWT_SECRET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 994c0a7..5773286 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,6 +15,19 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mongodb: + uri: mongodb://localhost:27017/withtime + mail: + host: ${MAIL_SENDER_HOST} + port: 587 + username: ${MAIL_SENDER_USERNAME} + password: ${MAIL_SENDER_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true jwt: secret: ${JWT_SECRET} From b7fbf7386c87efafd9ea7f351734d746e4ffa234 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 17 Jul 2025 20:41:51 +0900 Subject: [PATCH 17/20] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=AA=BD=EA=B3=A0DB=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/WithTimeBeApplication.java | 8 ++++ .../query/VisitLogQueryController.java | 2 +- .../log/converter/VisitLogConverter.java | 2 +- .../domain/log/entity/VisitLog.java | 41 ------------------- .../withtimebe/domain/log/model/VisitLog.java | 31 ++++++++++++++ .../log/repository/VisitLogRepository.java | 6 +-- .../service/query/VisitLogQueryService.java | 2 +- .../query/VisitLogQueryServiceImpl.java | 4 +- 8 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/log/model/VisitLog.java diff --git a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java index 4ac8c4c..63bb2d7 100644 --- a/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java +++ b/src/main/java/org/withtime/be/withtimebe/WithTimeBeApplication.java @@ -4,13 +4,21 @@ 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 { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java index 9b182e9..ea97f6f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/controller/query/VisitLogQueryController.java @@ -10,7 +10,7 @@ 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.entity.VisitLog; +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; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java index 14fafe2..ffac28f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/converter/VisitLogConverter.java @@ -7,7 +7,7 @@ import java.util.stream.Collectors; import org.withtime.be.withtimebe.domain.log.dto.response.VisitLogResponseDTO; -import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.model.VisitLog; public class VisitLogConverter { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java deleted file mode 100644 index d30cfe9..0000000 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/entity/VisitLog.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.withtime.be.withtimebe.domain.log.entity; - -import java.time.LocalDate; -import java.time.LocalTime; - -import org.withtime.be.withtimebe.global.common.BaseEntity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@Table(name = "visit_log") -public class VisitLog extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "visit_log_id") - private Long id; - - @Column(name = "date", nullable = false) - private LocalDate date; - - @Column(name = "hour", nullable = false) - private LocalTime hour; - - @Column(name = "count", nullable = false) - private Long count; -} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/model/VisitLog.java b/src/main/java/org/withtime/be/withtimebe/domain/log/model/VisitLog.java new file mode 100644 index 0000000..69e9962 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/model/VisitLog.java @@ -0,0 +1,31 @@ +package org.withtime.be.withtimebe.domain.log.model; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.withtime.be.withtimebe.global.common.BaseEntity; + +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 = "visit_logs") +public class VisitLog extends BaseEntity { + + @Id + private String id; + + private LocalDate date; + + private LocalTime hour; + + private Long count; +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java index 9535566..50a1f81 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/repository/VisitLogRepository.java @@ -3,10 +3,10 @@ import java.time.LocalDate; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; -import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.withtime.be.withtimebe.domain.log.model.VisitLog; -public interface VisitLogRepository extends JpaRepository { +public interface VisitLogRepository extends MongoRepository { List findByDateBetween(LocalDate dateAfter, LocalDate dateBefore); List findByDate(LocalDate date); } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java index b2b732b..4aafa3f 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryService.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.List; -import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.model.VisitLog; public interface VisitLogQueryService { List findDailyVisitLogList(); diff --git a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java index 62d73e5..75ba1b8 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/log/service/query/VisitLogQueryServiceImpl.java @@ -2,12 +2,10 @@ import java.time.LocalDate; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.withtime.be.withtimebe.domain.log.entity.VisitLog; +import org.withtime.be.withtimebe.domain.log.model.VisitLog; import org.withtime.be.withtimebe.domain.log.repository.VisitLogRepository; import lombok.RequiredArgsConstructor; From 3d547a50498418686e0aa691bec5133986c222fc Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 17 Jul 2025 20:42:43 +0900 Subject: [PATCH 18/20] =?UTF-8?q?=E2=9C=A8=20=20feat:=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EB=8C=80=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=BB=A8=EB=B2=84?= =?UTF-8?q?=ED=84=B0=20MongoConfig=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withtimebe/global/config/MongoConfig.java | 47 +++++++++++++++ .../global/converter/MongoConverters.java | 59 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java 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 new file mode 100644 index 0000000..5abca13 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/config/MongoConfig.java @@ -0,0 +1,47 @@ +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.AbstractMongoClientConfiguration; +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.withtime.be.withtimebe.global.converter.MongoConverters; + +import lombok.AllArgsConstructor; + +@Configuration +@AllArgsConstructor +public class MongoConfig { + + // KST <-> UTC 커스텀 컨버터 빈 등록 + @Bean + public MongoCustomConversions mongoCustomConversions() { + return new MongoCustomConversions(List.of( + new MongoConverters.LocalDateToDateKstConverter(), + new MongoConverters.LocalTimeToDateKstConverter(), + new MongoConverters.DateToLocalDateKstConverter(), + new MongoConverters.DateToLocalTimeKstConverter() + )); + } + + // 커스텀 컨버터 등록 및 _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..43bf3aa --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java @@ -0,0 +1,59 @@ +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; +import org.springframework.stereotype.Component; + +@Component +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); + } + } + + @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(); + } + } +} From 5204e3a4518deddc6ce45725a2240da6b4a6952f Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 17 Jul 2025 20:45:38 +0900 Subject: [PATCH 19/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=AA=BD=EA=B3=A0?= =?UTF-8?q?=20URI=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-develop.yml | 2 +- src/main/resources/application.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index b095816..afb2217 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -16,7 +16,7 @@ spring: host: ${REDIS_HOST} port: 6379 mongodb: - uri: mongodb://localhost:27017/withtime + uri: ${MONGO_URI} mail: host: ${MAIL_SENDER_HOST} port: 587 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5773286..6717ebe 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,7 +16,7 @@ spring: host: ${REDIS_HOST} port: 6379 mongodb: - uri: mongodb://localhost:27017/withtime + uri: ${MONGO_URI} mail: host: ${MAIL_SENDER_HOST} port: 587 From b27ec43362162d60815c50a419c9a492172614e7 Mon Sep 17 00:00:00 2001 From: pywoo Date: Thu, 17 Jul 2025 20:56:32 +0900 Subject: [PATCH 20/20] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/withtimebe/domain/dateplace/entity/DatePlace.java | 1 - .../be/withtimebe/global/converter/MongoConverters.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java index fc61f1f..1dd74e4 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/dateplace/entity/DatePlace.java @@ -12,7 +12,6 @@ import org.withtime.be.withtimebe.domain.dateplace.entity.enums.PlaceType; import org.withtime.be.withtimebe.global.common.BaseEntity; -@BatchSize(size = 100) @Entity @Getter @Builder 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 index 43bf3aa..c69dd16 100644 --- a/src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java +++ b/src/main/java/org/withtime/be/withtimebe/global/converter/MongoConverters.java @@ -10,9 +10,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; -import org.springframework.stereotype.Component; -@Component public class MongoConverters { @WritingConverter