From 1e9df07245975ade4211ae6b67fbda48a0baff3b Mon Sep 17 00:00:00 2001 From: sengjun0624 Date: Thu, 29 May 2025 16:25:34 +0900 Subject: [PATCH 1/7] :sparkles: feat: Add StoreDetail Api --- .../store/controller/StoreController.java | 21 +++++++++ .../response/StoreBasicInfoResponseDto.java | 21 +++++++++ .../service/query/StoreQueryService.java | 9 ++++ .../service/query/StoreQueryServiceImpl.java | 47 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 src/main/java/com/example/Tokkit_server/store/dto/response/StoreBasicInfoResponseDto.java create mode 100644 src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryService.java create mode 100644 src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java diff --git a/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java b/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java index 83e0cd6..7d3ccbc 100644 --- a/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java +++ b/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java @@ -2,16 +2,23 @@ import com.example.Tokkit_server.global.apiPayload.ApiResponse; import com.example.Tokkit_server.store.dto.response.KakaoMapSearchResponse; +import com.example.Tokkit_server.store.dto.response.StoreBasicInfoResponseDto; import com.example.Tokkit_server.store.dto.response.StoreInfoResponse; import com.example.Tokkit_server.store.dto.response.StoreSimpleResponse; +import com.example.Tokkit_server.store.dto.response.VoucherPageResponseDto; import com.example.Tokkit_server.store.service.StoreService; import com.example.Tokkit_server.store.service.command.StoreCommandService; +import com.example.Tokkit_server.store.service.query.StoreQueryService; import com.example.Tokkit_server.user.auth.CustomUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -27,6 +34,7 @@ public class StoreController { private final StoreCommandService storeCommandService; private final StoreService storeService; + private final StoreQueryService storeQueryService; @GetMapping("/nearby") @Operation( @@ -69,4 +77,17 @@ public ApiResponse getStoreSimpleInfo(@RequestParam Long st StoreSimpleResponse response = storeService.getSimpleStoreInfo(storeId); return ApiResponse.onSuccess(response); } + @GetMapping("/{storeId}") + public ApiResponse getStoreInfo(@PathVariable Long storeId) { + return ApiResponse.onSuccess(storeQueryService.getStoreInfo(storeId)); + } + + @GetMapping("/{storeId}/vouchers") + public ApiResponse getAvailableVouchers( + @PathVariable Long storeId, + @RequestParam Long userId, + @PageableDefault(size = 5) Pageable pageable + ) { + return ApiResponse.onSuccess(storeQueryService.getAvailableVouchers(storeId, userId, pageable)); + } } diff --git a/src/main/java/com/example/Tokkit_server/store/dto/response/StoreBasicInfoResponseDto.java b/src/main/java/com/example/Tokkit_server/store/dto/response/StoreBasicInfoResponseDto.java new file mode 100644 index 0000000..ca9ca22 --- /dev/null +++ b/src/main/java/com/example/Tokkit_server/store/dto/response/StoreBasicInfoResponseDto.java @@ -0,0 +1,21 @@ +package com.example.Tokkit_server.store.dto.response; + +import com.example.Tokkit_server.store.entity.Store; + +public record StoreBasicInfoResponseDto( + Long storeId, + String storeName, + String category, + String address, + String postalCode +) { + public static StoreBasicInfoResponseDto from(Store store) { + return new StoreBasicInfoResponseDto( + store.getId(), + store.getStoreName(), + store.getStoreCategory().name(), + store.getRoadAddress(), + store.getNewZipcode() + ); + } +} diff --git a/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryService.java b/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryService.java new file mode 100644 index 0000000..86fb71c --- /dev/null +++ b/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryService.java @@ -0,0 +1,9 @@ +package com.example.Tokkit_server.store.service.query; +import com.example.Tokkit_server.store.dto.response.StoreBasicInfoResponseDto; +import com.example.Tokkit_server.store.dto.response.VoucherPageResponseDto; + +import org.springframework.data.domain.Pageable; +public interface StoreQueryService { + StoreBasicInfoResponseDto getStoreInfo(Long storeId); + VoucherPageResponseDto getAvailableVouchers(Long storeId, Long userId, Pageable pageable); +} diff --git a/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java b/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java new file mode 100644 index 0000000..8fece6a --- /dev/null +++ b/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java @@ -0,0 +1,47 @@ +package com.example.Tokkit_server.store.service.query; + +import com.example.Tokkit_server.global.apiPayload.code.status.ErrorStatus; +import com.example.Tokkit_server.global.apiPayload.exception.GeneralException; +import com.example.Tokkit_server.store.dto.response.StoreBasicInfoResponseDto; +import com.example.Tokkit_server.store.dto.response.VoucherPageResponseDto; +import com.example.Tokkit_server.store.entity.Store; +import com.example.Tokkit_server.store.repository.StoreRepository; +import com.example.Tokkit_server.user.repository.UserRepository; +import com.example.Tokkit_server.voucher_ownership.entity.VoucherOwnership; +import com.example.Tokkit_server.voucher_ownership.repository.VoucherOwnershipRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StoreQueryServiceImpl implements StoreQueryService { + + private final StoreRepository storeRepository; + private final VoucherOwnershipRepository voucherOwnershipRepository; + private final UserRepository userRepository; + + @Override + public StoreBasicInfoResponseDto getStoreInfo(Long storeId) { + Store store = storeRepository.findById(storeId) + .orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); + return StoreBasicInfoResponseDto.from(store); + } + + @Override + public VoucherPageResponseDto getAvailableVouchers(Long storeId, Long userId, Pageable pageable) { + + storeRepository.findById(storeId).orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); + + userRepository.findById(userId).orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); + + Page page = voucherOwnershipRepository.findAvailableVouchersByUserAndStore( + userId, storeId, pageable + ); + + return VoucherPageResponseDto.from(page); + } +} From b43c30b9188e9e0b2803cc7b919303e79ff11094 Mon Sep 17 00:00:00 2001 From: sengjun0624 Date: Thu, 29 May 2025 16:25:43 +0900 Subject: [PATCH 2/7] :sparkles: feat: Add StoreDetail Voucher Api --- .../dto/response/VoucherPageResponseDto.java | 50 +++++++++++++++++++ .../VoucherOwnershipRepository.java | 17 +++++++ 2 files changed, 67 insertions(+) create mode 100644 src/main/java/com/example/Tokkit_server/store/dto/response/VoucherPageResponseDto.java diff --git a/src/main/java/com/example/Tokkit_server/store/dto/response/VoucherPageResponseDto.java b/src/main/java/com/example/Tokkit_server/store/dto/response/VoucherPageResponseDto.java new file mode 100644 index 0000000..6eae849 --- /dev/null +++ b/src/main/java/com/example/Tokkit_server/store/dto/response/VoucherPageResponseDto.java @@ -0,0 +1,50 @@ +package com.example.Tokkit_server.store.dto.response; + +import com.example.Tokkit_server.voucher.entity.Voucher; +import com.example.Tokkit_server.voucher_ownership.entity.VoucherOwnership; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.domain.Page; + +public record VoucherPageResponseDto( + List content, + int currentPage, + int pageSize, + int totalPages, + long totalElements, + boolean hasNext +) { + public static VoucherPageResponseDto from(Page page) { + List content = page.getContent().stream() + .map(VoucherInfoDto::from) + .toList(); + + return new VoucherPageResponseDto( + content, + page.getNumber(), + page.getSize(), + page.getTotalPages(), + page.getTotalElements(), + page.hasNext() + ); + } + + public record VoucherInfoDto( + Long voucherId, + String name, + LocalDate validUntil, + Long balance + ) { + public static VoucherInfoDto from(VoucherOwnership ownership) { + Voucher v = ownership.getVoucher(); + return new VoucherInfoDto( + v.getId(), + v.getName(), + v.getValidDate().toLocalDate(), + ownership.getRemainingAmount() + ); + } + } +} diff --git a/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepository.java b/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepository.java index 2789b1c..87848cb 100644 --- a/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepository.java +++ b/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepository.java @@ -41,4 +41,21 @@ List findByStatusAndVoucherValidDateBeforeWithFetchJoin( @Param("now") LocalDateTime now ); + @Query(""" + SELECT vo FROM VoucherOwnership vo + JOIN vo.voucher v + JOIN v.voucherStores vs + WHERE vo.wallet.user.id = :userId + AND vs.store.id = :storeId + AND vo.status = 'AVAILABLE' + AND vo.remainingAmount > 0 + AND v.validDate >= CURRENT_TIMESTAMP + ORDER BY vo.remainingAmount DESC,v.validDate ASC + """) + Page findAvailableVouchersByUserAndStore( + @Param("userId") Long userId, + + @Param("storeId") Long storeId, + Pageable pageable + ); } From a86c5402ba93cf7aa426872599fb61d54d722923 Mon Sep 17 00:00:00 2001 From: sengjun0624 Date: Thu, 29 May 2025 16:29:56 +0900 Subject: [PATCH 3/7] :sparkles: feat: Set TimeZone KR On Boot --- .../com/example/Tokkit_server/TokkitServerApplication.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java b/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java index f15a5c4..8ada9be 100644 --- a/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java +++ b/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java @@ -1,5 +1,7 @@ package com.example.Tokkit_server; +import java.util.TimeZone; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; @@ -15,7 +17,7 @@ public class TokkitServerApplication { public static void main(String[] args) { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); SpringApplication.run(TokkitServerApplication.class, args); } - } From 3382886fe3a2814060982e346a695d90997e9c3a Mon Sep 17 00:00:00 2001 From: sengjun0624 Date: Thu, 29 May 2025 17:55:10 +0900 Subject: [PATCH 4/7] =?UTF-8?q?:sparkles:=20feat:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=EB=A7=B5=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../store/controller/StoreController.java | 2 +- .../store/repository/StoreRepository.java | 34 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java b/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java index 7d3ccbc..f28ecf8 100644 --- a/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java +++ b/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java @@ -83,7 +83,7 @@ public ApiResponse getStoreInfo(@PathVariable Long st } @GetMapping("/{storeId}/vouchers") - public ApiResponse getAvailableVouchers( + public ApiResponse getAvailabㄴleVouchers( @PathVariable Long storeId, @RequestParam Long userId, @PageableDefault(size = 5) Pageable pageable diff --git a/src/main/java/com/example/Tokkit_server/store/repository/StoreRepository.java b/src/main/java/com/example/Tokkit_server/store/repository/StoreRepository.java index 6e2c683..55c95ad 100644 --- a/src/main/java/com/example/Tokkit_server/store/repository/StoreRepository.java +++ b/src/main/java/com/example/Tokkit_server/store/repository/StoreRepository.java @@ -19,7 +19,7 @@ public interface StoreRepository extends JpaRepository { "FROM VoucherStore vs JOIN vs.store s " + "WHERE vs.voucher.id = :voucherId") Page findByVoucherId(@Param("voucherId") Long voucherId, Pageable pageable); - +/* @Query(value = """ SELECT @@ -48,7 +48,37 @@ List findNearbyStoresRaw( @Param("radius") double radius, @Param("category") String category, @Param("keyword") String keyword - ); + );*/ +@Query(value = """ + SELECT + s.id AS id, + s.store_name AS storeName, + s.road_address AS roadAddress, + s.new_zipcode AS newZipcode, + s.latitude AS latitude, + s.longitude AS longitude, + s.store_category AS storeCategory, + ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(:lng, :lat)) AS distance + FROM wallet w + JOIN voucher_ownership vo ON w.id = vo.wallet_id + JOIN voucher_store vs ON vo.voucher_id = vs.voucher_id + JOIN store s ON vs.store_id = s.id + WHERE w.user_id = :userId + AND s.latitude BETWEEN :lat - (:radius / 111320) AND :lat + (:radius / 111320) + AND s.longitude BETWEEN :lng - (:radius / (111320 * COS(RADIANS(:lat)))) AND :lng + (:radius / (111320 * COS(RADIANS(:lat)))) + AND (:category IS NULL OR s.store_category = :category) + AND (:keyword IS NULL OR s.store_name LIKE CONCAT('%', :keyword, '%') OR s.road_address LIKE CONCAT('%', :keyword, '%')) + HAVING distance <= :radius + ORDER BY distance + """, nativeQuery = true) +List findNearbyStoresRaw( + @Param("userId") Long userId, + @Param("lat") double lat, + @Param("lng") double lng, + @Param("radius") double radius, + @Param("category") String category, + @Param("keyword") String keyword +); Optional findByIdAndMerchantId(Long storeId, Long merchantId); From 5b7a308c6d5878981859f32aaea64f1aaf3c8d52 Mon Sep 17 00:00:00 2001 From: noeyoes <> Date: Thu, 29 May 2025 20:09:10 +0900 Subject: [PATCH 5/7] fix: HikariCP Pool size error --- .../Tokkit_server/TokkitServerApplication.java | 2 ++ .../service/NotificationServiceImpl.java | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java b/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java index f15a5c4..e0e42ab 100644 --- a/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java +++ b/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java @@ -5,9 +5,11 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(scanBasePackages = "com.example") +@EnableAsync @EnableJpaAuditing @EnableScheduling @EnableJpaRepositories(basePackages = "com.example") diff --git a/src/main/java/com/example/Tokkit_server/notification/service/NotificationServiceImpl.java b/src/main/java/com/example/Tokkit_server/notification/service/NotificationServiceImpl.java index 14a9891..38032b8 100644 --- a/src/main/java/com/example/Tokkit_server/notification/service/NotificationServiceImpl.java +++ b/src/main/java/com/example/Tokkit_server/notification/service/NotificationServiceImpl.java @@ -17,7 +17,9 @@ import com.example.Tokkit_server.user.utils.SseEmitters; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -76,7 +78,6 @@ public void sendNotification(User user, NotificationTemplate template, Object... notificationRepository.save(notification); } - @Transactional public SseEmitter subscribe(Long userId) { SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); sseEmitters.add(userId, emitter); @@ -91,9 +92,10 @@ public SseEmitter subscribe(Long userId) { sseEmitters.remove(userId); } - userRepository.findById(userId).ifPresent(this::sendUnsentNotifications); + // 트랜잭션 점유 방지를 위해 비동기로 분리된 메서드 호출 + userRepository.findById(userId).ifPresent(this::sendUnsentNotificationsAsync); - // 연결 유지용 ping + // Ping 유지 ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { try { @@ -123,6 +125,12 @@ public SseEmitter subscribe(Long userId) { return emitter; } + @Async + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void sendUnsentNotificationsAsync(User user) { + sendUnsentNotifications(user); + } + @Transactional public void deleteNotification(Long notificationId, User user) { Notification notification = notificationRepository.findByIdAndUser(notificationId, user) From 9a5ab9492d07a28aec446b32f2c90957d034619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=A4=80?= <105282117+sengjun0624@users.noreply.github.com> Date: Fri, 30 May 2025 09:16:00 +0900 Subject: [PATCH 6/7] Update src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../store/service/query/StoreQueryServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java b/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java index 8fece6a..dca6bee 100644 --- a/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java +++ b/src/main/java/com/example/Tokkit_server/store/service/query/StoreQueryServiceImpl.java @@ -34,7 +34,7 @@ public StoreBasicInfoResponseDto getStoreInfo(Long storeId) { @Override public VoucherPageResponseDto getAvailableVouchers(Long storeId, Long userId, Pageable pageable) { - storeRepository.findById(storeId).orElseThrow(() -> new GeneralException(ErrorStatus.STORE_NOT_FOUND)); + getStoreInfo(storeId); userRepository.findById(userId).orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND)); From 037c37e515f046e860b01952a47e8a47108a4fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=A4=80?= <105282117+sengjun0624@users.noreply.github.com> Date: Fri, 30 May 2025 09:17:39 +0900 Subject: [PATCH 7/7] Update src/main/java/com/example/Tokkit_server/store/controller/StoreController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Tokkit_server/store/controller/StoreController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java b/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java index f28ecf8..59eeff3 100644 --- a/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java +++ b/src/main/java/com/example/Tokkit_server/store/controller/StoreController.java @@ -85,9 +85,9 @@ public ApiResponse getStoreInfo(@PathVariable Long st @GetMapping("/{storeId}/vouchers") public ApiResponse getAvailabㄴleVouchers( @PathVariable Long storeId, - @RequestParam Long userId, + @AuthenticationPrincipal CustomUserDetails userDetails, @PageableDefault(size = 5) Pageable pageable ) { - return ApiResponse.onSuccess(storeQueryService.getAvailableVouchers(storeId, userId, pageable)); + return ApiResponse.onSuccess(storeQueryService.getAvailableVouchers(storeId, userDetails.getId(), pageable)); } }