From f4c555909b98a157016767c7eaed32589c3502fe Mon Sep 17 00:00:00 2001 From: noeyoes <> Date: Wed, 28 May 2025 11:02:19 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/NotificationResponseDto.java | 2 + .../enums/NotificationTemplate.java | 1 + .../service/NotificationServiceImpl.java | 44 +++++++++++++++++-- .../service/SseNotificationService.java | 11 ++++- .../service/command/WalletCommandService.java | 4 ++ 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/notification/dto/response/NotificationResponseDto.java b/src/main/java/com/example/Tokkit_server/notification/dto/response/NotificationResponseDto.java index edee989..ed5b3c6 100644 --- a/src/main/java/com/example/Tokkit_server/notification/dto/response/NotificationResponseDto.java +++ b/src/main/java/com/example/Tokkit_server/notification/dto/response/NotificationResponseDto.java @@ -14,6 +14,7 @@ public class NotificationResponseDto { private String title; private String content; private NotificationCategory category; + private String deleted; private LocalDateTime createdAt; public static NotificationResponseDto from(Notification notification) { @@ -22,6 +23,7 @@ public static NotificationResponseDto from(Notification notification) { .title(notification.getTitle()) .content(notification.getContent()) .category(notification.getCategory()) + .deleted(notification.isDeleted() ? "deleted" : "active") .createdAt(notification.getCreatedAt()) .build(); } diff --git a/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java b/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java index 58674da..a614357 100644 --- a/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java +++ b/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java @@ -13,6 +13,7 @@ public enum NotificationTemplate { PAYMENT_REFUND(NotificationCategory.PAYMENT, "환불 완료", "%d원이 환불되었습니다."), // VOUCHER 알림 + VOUCHER_PURCHASED(NotificationCategory.VOUCHER, "바우처 구매 완료", "[%s] 바우처를 %d원에 구매하였습니다."), VOUCHER_EXPIRED(NotificationCategory.VOUCHER, "바우처 만료", "[%s] 바우처가 만료되었습니다."), // TOKEN 알림 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 91e8822..8533726 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 @@ -21,8 +21,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @@ -68,15 +72,47 @@ public void sendNotification(User user, NotificationTemplate template, Object... } } - @Transactional public SseEmitter subscribe(Long userId) { SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); sseEmitters.add(userId, emitter); + log.info("[SSE] 유저 {} 구독 등록됨", userId); + + try { + emitter.send(SseEmitter.event() + .name("connect") + .data("SSE 연결이 완료되었습니다.")); + } catch (IOException e) { + emitter.completeWithError(e); + sseEmitters.remove(userId); + } - emitter.onCompletion(() -> sseEmitters.remove(userId)); - emitter.onTimeout(() -> sseEmitters.remove(userId)); - emitter.onError((e) -> sseEmitters.remove(userId)); + // 연결 유지용 ping + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(() -> { + try { + emitter.send(SseEmitter.event() + .name("ping") + .data("keep-alive")); + } catch (Exception e) { + emitter.complete(); + } + }, 30, 30, TimeUnit.SECONDS); + + emitter.onCompletion(() -> { + sseEmitters.remove(userId); + scheduler.shutdown(); + }); + + emitter.onTimeout(() -> { + sseEmitters.remove(userId); + scheduler.shutdown(); + }); + + emitter.onError((e) -> { + sseEmitters.remove(userId); + scheduler.shutdown(); + }); return emitter; } diff --git a/src/main/java/com/example/Tokkit_server/notification/service/SseNotificationService.java b/src/main/java/com/example/Tokkit_server/notification/service/SseNotificationService.java index 89fe78a..0ef3f1a 100644 --- a/src/main/java/com/example/Tokkit_server/notification/service/SseNotificationService.java +++ b/src/main/java/com/example/Tokkit_server/notification/service/SseNotificationService.java @@ -9,6 +9,8 @@ import java.io.IOException; +import static org.apache.commons.lang3.StringEscapeUtils.escapeJson; + @Slf4j @Service @RequiredArgsConstructor @@ -20,13 +22,18 @@ public void sendSse(Long userId, String title, String content) { SseEmitter emitter = sseEmitters.get(userId); if (emitter != null) { try { + log.info("[SSE] 유저 {}에게 알림 전송 시도", userId); // 로그 추가 + String json = String.format("{\"title\": \"%s\", \"content\": \"%s\"}", title, content); emitter.send(SseEmitter.event() .name("notification") - .data(title + ": " + content, MediaType.valueOf("text/plain;charset=UTF-8"))); + .data(json, MediaType.APPLICATION_JSON)); + log.info("[SSE] 유저 {}에게 알림 전송 성공", userId); } catch (IOException e) { sseEmitters.remove(userId); - log.error("[SseNotificationService] SSE 전송 실패 - 연결 제거됨: {}", e.getMessage()); + log.error("[SSE] 전송 실패 - emitter 제거됨: {}", e.getMessage()); } + } else { + log.warn("[SSE] emitter 없음 → 전송 실패 (userId: {})", userId); // 로그 추가 } } } \ No newline at end of file diff --git a/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java b/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java index f1102ac..5cfc5ab 100644 --- a/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java +++ b/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java @@ -4,6 +4,8 @@ import com.example.Tokkit_server.global.apiPayload.exception.GeneralException; import com.example.Tokkit_server.merchant.entity.Merchant; import com.example.Tokkit_server.merchant.repository.MerchantRepository; +import com.example.Tokkit_server.notification.enums.NotificationTemplate; +import com.example.Tokkit_server.notification.service.NotificationService; import com.example.Tokkit_server.store.entity.Store; import com.example.Tokkit_server.store.repository.StoreRepository; import com.example.Tokkit_server.transaction.entity.Transaction; @@ -29,6 +31,7 @@ import com.example.contract.service.TokkitTokenService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -44,6 +47,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class WalletCommandService { private final WalletRepository walletRepository; From f4b58666034d22c549093c229567d4848439ef8f Mon Sep 17 00:00:00 2001 From: noeyoes <> Date: Wed, 28 May 2025 12:01:45 +0900 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/enums/NotificationCategory.java | 1 - .../notification/enums/NotificationTemplate.java | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/notification/enums/NotificationCategory.java b/src/main/java/com/example/Tokkit_server/notification/enums/NotificationCategory.java index 5fbc3e9..f2ce6c1 100644 --- a/src/main/java/com/example/Tokkit_server/notification/enums/NotificationCategory.java +++ b/src/main/java/com/example/Tokkit_server/notification/enums/NotificationCategory.java @@ -6,6 +6,5 @@ public enum NotificationCategory { SYSTEM, // 시스템 점검 등 PAYMENT, // 결제 - VOUCHER, // 바우처 TOKEN, // 지갑/토큰 } diff --git a/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java b/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java index a614357..2f92586 100644 --- a/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java +++ b/src/main/java/com/example/Tokkit_server/notification/enums/NotificationTemplate.java @@ -11,10 +11,8 @@ public enum NotificationTemplate { // PAYMENT 알림 PAYMENT_SUCCESS(NotificationCategory.PAYMENT, "결제 완료", "%d원이 결제되었습니다."), PAYMENT_REFUND(NotificationCategory.PAYMENT, "환불 완료", "%d원이 환불되었습니다."), - - // VOUCHER 알림 - VOUCHER_PURCHASED(NotificationCategory.VOUCHER, "바우처 구매 완료", "[%s] 바우처를 %d원에 구매하였습니다."), - VOUCHER_EXPIRED(NotificationCategory.VOUCHER, "바우처 만료", "[%s] 바우처가 만료되었습니다."), + VOUCHER_PURCHASED(NotificationCategory.PAYMENT, "바우처 구매 완료", "[%s] 바우처를 %d원에 구매하였습니다."), + VOUCHER_EXPIRED(NotificationCategory.PAYMENT, "바우처 만료", "[%s] 바우처가 만료되었습니다."), // TOKEN 알림 TOKEN_CONVERTED(NotificationCategory.TOKEN, "토큰 전환 완료", "토큰이 성공적으로 전환되었습니다."); From 4c3c6cef63243daed436fcbd98caa588cff6886d Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:45:12 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor=20:=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=EB=90=9C=20=EC=BB=A8=ED=8A=B8=EB=9E=99=ED=8A=B8=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=A9=94=EB=AA=A8=EB=A6=AC=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contract/service/ContractAddressService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/example/contract/service/ContractAddressService.java b/src/main/java/com/example/contract/service/ContractAddressService.java index 1ab1338..e72394a 100644 --- a/src/main/java/com/example/contract/service/ContractAddressService.java +++ b/src/main/java/com/example/contract/service/ContractAddressService.java @@ -2,13 +2,19 @@ import com.example.contract.entity.ContractAddress; import com.example.contract.repository.ContractAddressRepository; +import com.example.contract.storage.ContractAddressStorage; + import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor +@Slf4j public class ContractAddressService { private final ContractAddressRepository repository; + private final ContractAddressStorage contractAddressStorage; /** * 컨트랙트 주소 저장 */ @@ -19,8 +25,15 @@ public void save(String contractName, String address, String network) { .network(network) .build(); repository.save(entity); + + // 메모리에도 반영 + if ("TokkitToken".equals(contractName)) { + contractAddressStorage.setTokkitTokenAddress(address); + log.info("📦 ContractAddressStorage 업데이트 완료: {} = {}", contractName, address); + } } + /** * 저장된 컨트랙트 주소중 최신 주소를 가져옴 */ From 24418f5b2d1085b4858d486b2b797470124081db Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:45:25 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor=20:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/contract/controller/ContractController.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/example/contract/controller/ContractController.java b/src/main/java/com/example/contract/controller/ContractController.java index e4c8054..141b10e 100644 --- a/src/main/java/com/example/contract/controller/ContractController.java +++ b/src/main/java/com/example/contract/controller/ContractController.java @@ -26,10 +26,7 @@ public class ContractController { @Operation(summary = "스마트 컨트랙트 주소 등록", description = "Hardhat에서 배포된 스마트컨트랙트 주소를 전달받아 메모리 및 DB에 저장합니다.") public ResponseEntity receiveContractAddress(@RequestBody ContractAddressDto dto) { - // 1. 메모리에 저장 - contractAddressStorage.setTokkitTokenAddress(dto.getTokkitToken()); - // 2. DB에도 저장 contractAddressService.save("TokkitToken", dto.getTokkitToken(), dto.getNetwork()); System.out.println("📥 Stored contract address: " + dto.getTokkitToken()); From 4354d8dc102f03440fa2f7348b819e1638977279 Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:45:37 +0900 Subject: [PATCH 05/13] =?UTF-8?q?refactor=20:=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=97=90=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/apiPayload/code/status/ErrorStatus.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/example/Tokkit_server/global/apiPayload/code/status/ErrorStatus.java index 0523916..330aaa3 100644 --- a/src/main/java/com/example/Tokkit_server/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/example/Tokkit_server/global/apiPayload/code/status/ErrorStatus.java @@ -34,7 +34,7 @@ public enum ErrorStatus implements BaseErrorCode { // Wallet 관련 USER_WALLET_NOT_FOUND(HttpStatus.NOT_FOUND, "WALLET_001", "사용자 지갑이 존재하지 않습니다."), MERCHANT_WALLET_NOT_FOUND(HttpStatus.NOT_FOUND, "WALLET_002", "가맹점 지갑이 존재하지 않습니다."), - INSUFFICIENT_BALANCE(HttpStatus.BAD_REQUEST, "WALLET_003", "토큰 잔액이 부족합니다."), + INSUFFICIENT_BALANCE(HttpStatus.BAD_REQUEST, "WALLET_003", "토큰/바우처 잔액이 부족합니다."), INSUFFICIENT_TOKEN_BALANCE(HttpStatus.BAD_REQUEST,"WALLET_004", "토큰 잔액이 부족합니다."), // Transaction 관련 @@ -94,7 +94,10 @@ public enum ErrorStatus implements BaseErrorCode { TOKEN_TRANSFER_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"TOKEN_001" , "스마트컨트랙트 전송 중 오류가 발생했습니다."), TOKEN_MINT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"TOKEN_002" , "토큰 발행(mint) 처리 중 오류가 발생했습니다."), TOKEN_BALANCE_QUERY_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN_003", "스마트컨트랙트 잔액 조회 중 오류가 발생했습니다."), - TOKEN_BURN_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN_004", "토큰 소각(burn) 처리 중 오류가 발생했습니다."); + TOKEN_BURN_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN_004", "토큰 소각(burn) 처리 중 오류가 발생했습니다."), + BALANCE_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN_005","스마트컨트랙트와 DB의 토큰 잔액이 일치하지 않습니다."), + BALANCE_VERIFICATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN_005", "스마트컨트랙트 잔액 조회 중 오류가 발생했습니다."); + private final HttpStatus httpStatus; From 16ce1b9bf8649cdd6cd606cb621e388f5f80d439 Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:46:14 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat=20:=20@EnableScheduling=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 --- .../java/com/example/Tokkit_server/TokkitServerApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java b/src/main/java/com/example/Tokkit_server/TokkitServerApplication.java index b9d7cf5..f15a5c4 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.EnableScheduling; @SpringBootApplication(scanBasePackages = "com.example") @EnableJpaAuditing +@EnableScheduling @EnableJpaRepositories(basePackages = "com.example") @EntityScan(basePackages = "com.example") public class TokkitServerApplication { From 4e98d7d1206aeacfe9d258ea2ad473ff4fc1c2f7 Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:46:53 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/contract/service/TokkitTokenService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/example/contract/service/TokkitTokenService.java b/src/main/java/com/example/contract/service/TokkitTokenService.java index 137ad7a..7906dc2 100644 --- a/src/main/java/com/example/contract/service/TokkitTokenService.java +++ b/src/main/java/com/example/contract/service/TokkitTokenService.java @@ -58,6 +58,7 @@ public TransactionReceipt transfer(String toAddress, BigInteger amount) throws E * 잔액 조회 */ public BigInteger getBalanceOf(String address) throws Exception { + log.info("🧾 [getBalanceOf] address to query = {}", address); return loadContract().balanceOf(address).send(); } @@ -81,4 +82,12 @@ public TransactionReceipt payToMerchant(String merchantAddress, BigInteger amoun public String getName() throws Exception { return loadContract().name().send(); } + + /** + * Credentials 주소 조회 + */ + public String getOwnerAddress() { + return credentials.getAddress(); + } + } From 249c325f7ca2e7789f37cdd6d751bd0de0f50ed9 Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:47:31 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor=20:=20=EB=B0=94=EC=9A=B0?= =?UTF-8?q?=EC=B2=98=20=EC=9C=A0=ED=9A=A8=EA=B8=B0=EA=B0=84=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voucher_ownership/scheduler/VoucherExpirationScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/Tokkit_server/voucher_ownership/scheduler/VoucherExpirationScheduler.java b/src/main/java/com/example/Tokkit_server/voucher_ownership/scheduler/VoucherExpirationScheduler.java index e618700..37ba83d 100644 --- a/src/main/java/com/example/Tokkit_server/voucher_ownership/scheduler/VoucherExpirationScheduler.java +++ b/src/main/java/com/example/Tokkit_server/voucher_ownership/scheduler/VoucherExpirationScheduler.java @@ -23,7 +23,7 @@ public void expireVouchers() { log.info("바우처 유효기간 만료 체크 시작"); List expiredList = - voucherOwnershipRepository.findByStatusAndVoucher_ValidDateBefore( + voucherOwnershipRepository.findByStatusAndVoucherValidDateBeforeWithFetchJoin( VoucherOwnershipStatus.AVAILABLE, LocalDateTime.now()); expiredList.forEach(VoucherOwnership::expire); From 5866330b70ac2eb1b8a9434aab71f4597e76bf55 Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:48:22 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor=20:=20fetch=20join=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=95=9C=EB=B2=88=EC=97=90=20=EB=81=8C=EA=B3=A0=20?= =?UTF-8?q?=EC=98=A4=EB=8F=84=EB=A1=9D=20JPQL=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoucherOwnershipRepository.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 3c34193..2789b1c 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 @@ -5,6 +5,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; import java.util.List; @@ -14,4 +16,29 @@ public interface VoucherOwnershipRepository extends JpaRepository findByWalletUserId(Long userId, Pageable pageable); Optional findByIdAndWalletUserId(Long id, Long userId); List findByStatusAndVoucher_ValidDateBefore(VoucherOwnershipStatus status, LocalDateTime time); + + // 바우처 + 바우처 스토어 + 스토어를 모두 fetch join으로 한 번에 조회 + @Query(""" + SELECT vo + FROM VoucherOwnership vo + JOIN FETCH vo.voucher v + LEFT JOIN FETCH v.voucherStores vs + LEFT JOIN FETCH vs.store s + WHERE vo.wallet.user.id = :userId + """) + List findAllWithVoucherAndStoresByUserId(@Param("userId") Long userId); + + // 만료된 바우처만 fetch join으로 조회 (성능 개선용) + @Query(""" + SELECT vo + FROM VoucherOwnership vo + JOIN FETCH vo.voucher v + WHERE vo.status = :status + AND v.validDate < :now + """) + List findByStatusAndVoucherValidDateBeforeWithFetchJoin( + @Param("status") VoucherOwnershipStatus status, + @Param("now") LocalDateTime now + ); + } From aac16f7f37d0214cf7f89d5317dbc333a8b237da Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:48:37 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor=20:=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=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 --- .../repository/VoucherOwnershipRepositoryCustom.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryCustom.java b/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryCustom.java index fd7bee0..8958a37 100644 --- a/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryCustom.java +++ b/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryCustom.java @@ -4,11 +4,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.example.Tokkit_server.voucher_ownership.dto.request.VoucherOwnershipSearchRequest; import com.example.Tokkit_server.voucher_ownership.entity.VoucherOwnership; interface VoucherOwnershipRepositoryCustom { Page searchMyVoucher(VoucherOwnershipSearchRequest request, Long userId, Pageable pageable); - List findAllWithVoucherAndStoresByUserId(Long userId); } \ No newline at end of file From 4a2c6e6d2bba14c85b8661d192a9ef90f83cd8c5 Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:48:44 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor=20:=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=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 --- .../VoucherOwnershipRepositoryImpl.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryImpl.java b/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryImpl.java index 5eabe3c..62a8fb1 100644 --- a/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryImpl.java +++ b/src/main/java/com/example/Tokkit_server/voucher_ownership/repository/VoucherOwnershipRepositoryImpl.java @@ -69,21 +69,6 @@ public Page searchMyVoucher(VoucherOwnershipSearchRequest requ return new PageImpl<>(result, pageable, total); } - @Override - public List findAllWithVoucherAndStoresByUserId(Long userId) { - return em.createQuery(""" - SELECT vo FROM VoucherOwnership vo - JOIN FETCH vo.voucher v - LEFT JOIN FETCH v.voucherStores vs - LEFT JOIN FETCH vs.store s - JOIN vo.wallet w - JOIN w.user u - WHERE u.id = :userId - """, VoucherOwnership.class) - .setParameter("userId", userId) - .getResultList(); - } - } From 9f7af29b90f6a4bf7f173f5943b0b729055f1bae Mon Sep 17 00:00:00 2001 From: YoungjaeRo Date: Wed, 28 May 2025 15:49:24 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor=20:=20on-chain=20&=20off-chain?= =?UTF-8?q?=20=EC=9E=94=EC=95=A1=20=ED=86=B5=EC=9D=BC=20=ED=9B=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/command/WalletCommandService.java | 48 +++++++++++++++++-- .../service/query/WalletQueryService.java | 27 +++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java b/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java index f1102ac..0312eec 100644 --- a/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java +++ b/src/main/java/com/example/Tokkit_server/wallet/service/command/WalletCommandService.java @@ -29,6 +29,8 @@ import com.example.contract.service.TokkitTokenService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import org.slf4j.MDC; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -44,6 +46,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class WalletCommandService { private final WalletRepository walletRepository; @@ -235,6 +238,30 @@ public VoucherPurchaseResponse purchaseVoucher(Long userId,VoucherPurchaseReques // 7. 수량 차감 voucher.decreaseRemainingCount(); + // 7.1 온체인 잔액 검증 + try { + + BigInteger onChainBalance = tokkitTokenService.getBalanceOf(wallet.getWalletAddress()); + if (!onChainBalance.equals(BigInteger.valueOf(wallet.getTokenBalance()))) { + throw new GeneralException(ErrorStatus.BALANCE_MISMATCH); + } + } catch (Exception e) { + throw new GeneralException(ErrorStatus.BALANCE_VERIFICATION_FAILED); + } + + // 8. 토큰 소각 + TransactionReceipt receipt; + try { + receipt = tokkitTokenService.burn( + wallet.getWalletAddress(), + BigInteger.valueOf(amount) + ); + } catch (Exception e) { + throw new GeneralException(ErrorStatus.TOKEN_BURN_FAILED); + } + + String txHash = receipt.getTransactionHash(); + // 8. 토큰 차감 wallet.updateBalance(wallet.getDepositBalance(), wallet.getTokenBalance() - amount); @@ -254,7 +281,7 @@ public VoucherPurchaseResponse purchaseVoucher(Long userId,VoucherPurchaseReques String displayDescription = voucher.getName(); logAndSave(wallet, user.getId(), null, TransactionType.PURCHASE, TransactionStatus.SUCCESS, - (long) amount, logDescription, displayDescription); + (long) amount, logDescription, displayDescription,txHash); // 11. 응답 반환 @@ -326,6 +353,7 @@ public VoucherPaymentResponse payWithVoucher(Long userId,VoucherPaymentRequest r String userDisplayDescription = store.getStoreName(); + // 9. 사용자 거래 기록 생성 logAndSave(ownership.getWallet(), user.getId(), null, TransactionType.PURCHASE, TransactionStatus.SUCCESS, request.getAmount(), userLogDescription, userDisplayDescription); @@ -342,10 +370,10 @@ public VoucherPaymentResponse payWithVoucher(Long userId,VoucherPaymentRequest r // 스마트 컨트랙트 적용 TransactionReceipt receipt; try{ - receipt = tokkitTokenService.payToMerchant( + receipt = tokkitTokenService.mint( merchantWallet.getWalletAddress(), - BigInteger.valueOf(request.getAmount()), - "Voucher settlement from User ID : " + user.getId()); + BigInteger.valueOf(request.getAmount()) + ); } catch (Exception e) { throw new GeneralException(ErrorStatus.TOKEN_TRANSFER_FAILED); } @@ -404,6 +432,18 @@ public DirectPaymentResponse payDirectlyWithToken(Long userId,DirectPaymentReque throw new GeneralException(ErrorStatus.INVALID_SIMPLE_PASSWORD); } + + // 5.1 스마트컨트랙트 이전 상태 검증 (user) + try { + BigInteger userOnChainBalance = tokkitTokenService.getBalanceOf(userWallet.getWalletAddress()); + if (!userOnChainBalance.equals(BigInteger.valueOf(userWallet.getTokenBalance()))) { + throw new GeneralException(ErrorStatus.BALANCE_MISMATCH); + } + } catch (Exception e) { + throw new GeneralException(ErrorStatus.BALANCE_VERIFICATION_FAILED); + } + + // 스마트 컨트랙트 적용 TransactionReceipt receipt; try { diff --git a/src/main/java/com/example/Tokkit_server/wallet/service/query/WalletQueryService.java b/src/main/java/com/example/Tokkit_server/wallet/service/query/WalletQueryService.java index 536c559..3ff4bd7 100644 --- a/src/main/java/com/example/Tokkit_server/wallet/service/query/WalletQueryService.java +++ b/src/main/java/com/example/Tokkit_server/wallet/service/query/WalletQueryService.java @@ -111,6 +111,15 @@ public void convertDepositToToken(Long userId, DepositToTokenRequest request) { } String txHash = receipt.getTransactionHash(); + try { + BigInteger onChainBalance = tokkitTokenService.getBalanceOf(wallet.getWalletAddress()); + if (!onChainBalance.equals(BigInteger.valueOf(wallet.getTokenBalance()))) { + throw new GeneralException(ErrorStatus.BALANCE_MISMATCH); + } + } catch (Exception e) { + throw new GeneralException(ErrorStatus.BALANCE_VERIFICATION_FAILED); + } + logAndSave(wallet, user.getId(), null, TransactionType.CONVERT, TransactionStatus.SUCCESS, @@ -143,21 +152,31 @@ public void convertTokenToDeposit(Long userId ,TokenToDepositRequest request) { } - // 스마트컨트랙트에 burn 요청 - String txHash; + // 스마트컨트랙트 burn + TransactionReceipt receipt; try { - TransactionReceipt receipt = tokkitTokenService.burn(wallet.getWalletAddress(), BigInteger.valueOf(request.getAmount())); - txHash = receipt.getTransactionHash(); + receipt = tokkitTokenService.burn(wallet.getWalletAddress(), BigInteger.valueOf(request.getAmount())); } catch (Exception e) { throw new GeneralException(ErrorStatus.TOKEN_BURN_FAILED); } + String txHash = receipt.getTransactionHash(); + // 잔액 업데이트 wallet.updateBalance(wallet.getDepositBalance() + request.getAmount(), wallet.getTokenBalance() - request.getAmount()); + try { + BigInteger onChainBalance = tokkitTokenService.getBalanceOf(wallet.getWalletAddress()); + if (!onChainBalance.equals(BigInteger.valueOf(wallet.getTokenBalance()))) { + throw new GeneralException(ErrorStatus.BALANCE_MISMATCH); + } + } catch (Exception e) { + throw new GeneralException(ErrorStatus.BALANCE_VERIFICATION_FAILED); + } + logAndSave(wallet, user.getId(), null, TransactionType.CONVERT, From ceab1fe208de1ccd1e8759a43b0807f6306a15d8 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: Wed, 28 May 2025 19:55:58 +0900 Subject: [PATCH 13/13] :recycle: refactor: Update CorsConfig (#103) --- .../com/example/Tokkit_server/global/config/CorsConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/Tokkit_server/global/config/CorsConfig.java b/src/main/java/com/example/Tokkit_server/global/config/CorsConfig.java index 3d29ed8..389f86d 100644 --- a/src/main/java/com/example/Tokkit_server/global/config/CorsConfig.java +++ b/src/main/java/com/example/Tokkit_server/global/config/CorsConfig.java @@ -18,7 +18,9 @@ public CorsConfigurationSource apiConfigurationSource() { "http://localhost:3000", "http://localhost:8000", "http://localhost:8080", - "http://localhost:8800" + "http://localhost:8800", + "https://www.tokkit.site", + "https://admin.tokkit.site" )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*"));