diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 52dbd434..7aa58bcd 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -66,7 +66,7 @@ jobs: uses: jsdaniell/create-json@v1.2.2 with: name: "./src/main/resources/firebase/gifthub-b2dcb-firebase-adminsdk-yj7uq-912097b9ae.json" - json: ${{ secrets.FIREBASE_JSON }} + json: ${{ secrets.FIREBASE_DEVELOPMENT_JSON }} # Docker 이미지 build 및 push - name: docker build and push diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index edea652f..2ab256b2 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -64,7 +64,7 @@ jobs: uses: jsdaniell/create-json@v1.2.2 with: name: "./src/main/resources/firebase/gifthub-b2dcb-firebase-adminsdk-yj7uq-912097b9ae.json" - json: ${{ secrets.FIREBASE_JSON }} + json: ${{ secrets.FIREBASE_DEVELOPMENT_JSON }} # 5. 테스트를 위한 MySQL 설정 - name: Setup MySQL diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index a871d8fd..ec450df8 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -65,8 +65,8 @@ jobs: - name: Create Json uses: jsdaniell/create-json@v1.2.2 with: - name: "./src/main/resources/firebase/gifthub-b2dcb-firebase-adminsdk-yj7uq-912097b9ae.json" - json: ${{ secrets.FIREBASE_JSON }} + name: "./src/main/resources/firebase/gifthub-production-3d052-firebase-adminsdk-zkstv-377ec5747b.json" + json: ${{ secrets.FIREBASE_PRODUCTION_JSON }} # Docker 이미지 build 및 push - name: docker build and push diff --git a/.github/workflows/prod-ci.yml b/.github/workflows/prod-ci.yml index b2d3cdd7..92b7369b 100644 --- a/.github/workflows/prod-ci.yml +++ b/.github/workflows/prod-ci.yml @@ -64,7 +64,7 @@ jobs: uses: jsdaniell/create-json@v1.2.2 with: name: "./src/main/resources/firebase/gifthub-b2dcb-firebase-adminsdk-yj7uq-912097b9ae.json" - json: ${{ secrets.FIREBASE_JSON }} + json: ${{ secrets.FIREBASE_DEVELOPMENT_JSON }} # 5. 테스트를 위한 MySQL 설정 - name: Setup MySQL diff --git a/.gitignore b/.gitignore index 4e5aa9f6..621dfb27 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,7 @@ out/ src/.DS_Store # Firebase -/src/main/resources/firebase/gifthub-b2dcb-firebase-adminsdk-yj7uq-912097b9ae.json +/src/main/resources/firebase/ # Actions Secrets actions-secrets.yml diff --git a/src/main/java/org/swmaestro/repl/gifthub/config/FCMConfig.java b/src/main/java/org/swmaestro/repl/gifthub/config/FCMConfig.java index d5be1c0a..dd49f5c3 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/config/FCMConfig.java +++ b/src/main/java/org/swmaestro/repl/gifthub/config/FCMConfig.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -15,9 +16,12 @@ @Configuration public class FCMConfig { + @Value("${firebase.key-path}") + private String keyPath; + @Bean FirebaseMessaging firebaseMessaging() throws IOException { - ClassPathResource resource = new ClassPathResource("firebase/gifthub-b2dcb-firebase-adminsdk-yj7uq-912097b9ae.json"); + ClassPathResource resource = new ClassPathResource(keyPath); InputStream refreshToken = resource.getInputStream(); diff --git a/src/main/java/org/swmaestro/repl/gifthub/config/WebClientConfig.java b/src/main/java/org/swmaestro/repl/gifthub/config/WebClientConfig.java new file mode 100644 index 00000000..ef8dd52d --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/config/WebClientConfig.java @@ -0,0 +1,32 @@ +package org.swmaestro.repl.gifthub.config; + +import java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +@Configuration +public class WebClientConfig { + @Bean + public WebClient webClient() { + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + HttpClient httpClient = HttpClient.create( + ConnectionProvider.builder("gifthub-connections") + .maxConnections(100) + .maxIdleTime(Duration.ofSeconds(30)) + .pendingAcquireTimeout(Duration.ofSeconds(45)) + .pendingAcquireMaxCount(-1) + .evictInBackground(Duration.ofSeconds(30)) + .lifo() + .build() + ); +} diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java index deac3bea..148e680b 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherController.java @@ -27,6 +27,7 @@ import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareRequestDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateRequestDto; +import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUseRequestDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUseResponseDto; import org.swmaestro.repl.gifthub.vouchers.service.PendingVoucherService; @@ -132,7 +133,7 @@ public ResponseEntity listVoucher(HttpServletRequest request, @RequestP }) public ResponseEntity updateVoucher(HttpServletRequest request, @PathVariable Long voucherId, @RequestBody VoucherUpdateRequestDto voucherUpdateRequestDto) throws IOException { - VoucherSaveResponseDto updatedVoucher = voucherService.update(voucherId, voucherUpdateRequestDto); + VoucherUpdateResponseDto updatedVoucher = voucherService.update(voucherId, voucherUpdateRequestDto); return ResponseEntity.ok( SuccessMessage.builder() .path(request.getRequestURI()) diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUpdateResponseDto.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUpdateResponseDto.java new file mode 100644 index 00000000..d2f3aeae --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/dto/VoucherUpdateResponseDto.java @@ -0,0 +1,46 @@ +package org.swmaestro.repl.gifthub.vouchers.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +public class VoucherUpdateResponseDto { + private Long id; + private Long productId; + private String barcode; + private String expiresAt; + private Integer price; + private Integer balance; + private String imageUrl; + @JsonProperty("is_accessible") + private boolean accessible; + @JsonProperty("is_shared") + private boolean shared; + @JsonProperty("is_checked") + private boolean checked; + + @Builder + public VoucherUpdateResponseDto(Long id, Long productId, String barcode, String expiresAt, Integer price, + Integer balance, String imageUrl, boolean accessible, boolean shared, boolean checked) { + this.id = id; + this.productId = productId; + this.barcode = barcode; + this.expiresAt = expiresAt; + this.price = price; + this.balance = balance; + this.imageUrl = imageUrl; + this.accessible = accessible; + this.shared = shared; + this.checked = checked; + } +} diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/entity/Voucher.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/entity/Voucher.java index 3b5bb5b2..73c6f2ef 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/entity/Voucher.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/entity/Voucher.java @@ -37,7 +37,7 @@ public class Voucher extends BaseTimeEntity { @JoinColumn(name = "product_id", nullable = false) private Product product; - @Column(length = 12) + @Column(length = 16) private String barcode; @Column diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/GptService.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/GptService.java index 066ae0f0..bfcb3765 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/GptService.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/GptService.java @@ -5,10 +5,9 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; +import org.swmaestro.repl.gifthub.config.WebClientConfig; import org.swmaestro.repl.gifthub.vouchers.dto.GptResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherAutoSaveRequestDto; @@ -20,19 +19,19 @@ @Service public class GptService { - private final WebClient gptClient; @Value("${openai.api-url}") private String apiUrl; @Value("${openai.api-key}") private String apiKey; private String prompt; - @Autowired private ObjectMapper objectMapper; + private final WebClientConfig webClientConfig; - public GptService(WebClient.Builder webClientBuilder, @Value("/gpt/question.txt") String promptPath) throws IOException { - this.gptClient = webClientBuilder.build(); + public GptService(@Value("/gpt/question.txt") String promptPath, WebClientConfig webClientConfig, ObjectMapper objectMapper) throws IOException { this.prompt = loadQuestionFromFile(promptPath); + this.webClientConfig = webClientConfig; + this.objectMapper = objectMapper; } public Mono getGptResponse(VoucherAutoSaveRequestDto voucherAutoSaveRequestDto) { @@ -47,13 +46,14 @@ public Mono getGptResponse(VoucherAutoSaveRequestDto voucherAuto message.put("role", "assistant"); message.put("content", prompt); - return gptClient.post() + return webClientConfig.webClient().post() .uri(apiUrl) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .body(Mono.just(requestBody), ObjectNode.class) .retrieve() - .bodyToMono(GptResponseDto.class); + .bodyToMono(GptResponseDto.class) + .retry(2); } public String loadQuestionFromFile(String filePath) throws IOException { diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/SearchService.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/SearchService.java index 944eb930..dd85ef8f 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/SearchService.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/SearchService.java @@ -2,11 +2,9 @@ import java.util.Base64; -import javax.annotation.PostConstruct; - import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; +import org.swmaestro.repl.gifthub.config.WebClientConfig; import org.swmaestro.repl.gifthub.vouchers.dto.SearchResponseDto; import lombok.RequiredArgsConstructor; @@ -21,25 +19,16 @@ public class SearchService { @Value("${opensearch.password}") private String password; - private String auth; @Value("${opensearch.base-url}") private String baseUrl; - private WebClient openSearchClient; - - @PostConstruct - public void init() { - openSearchClient = WebClient.builder() - .baseUrl(baseUrl) - .build(); - auth = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); - } + private final WebClientConfig webClientConfig; public Mono search(String query) { - return openSearchClient.post() - .uri("/product/_search") - .header("Authorization", auth) + return webClientConfig.webClient().post() + .uri(baseUrl + "/product/_search") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes())) .header("Content-Type", "application/json") .bodyValue(query) .retrieve() diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/StorageService.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/StorageService.java index e96eddb0..e2db9307 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/StorageService.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/StorageService.java @@ -60,7 +60,7 @@ private String getUUidFileName(String fileName) { } public String getDefaultImagePath(String dirName) { - return "https://" + cloudFrontBucketName + "/" + dirName + "/" + defaultImageFile; + return "https://" + cloudFrontBucketName + "/" + defaultImageFile; } public String getPresignedUrlForSaveVoucher(String dirName, String extension) { diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherSaveService.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherSaveService.java index 47a1e0a2..5659643b 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherSaveService.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherSaveService.java @@ -72,8 +72,8 @@ public void execute(VoucherAutoSaveRequestDto voucherAutoSaveRequestDto, String fcmNotificationService.sendNotification("기프티콘 등록 실패", "이미 등록된 기프티콘 입니다.", username); notificationService.save(userService.read(username), null, NotificationType.REGISTERED, "이미 등록된 기프티콘 입니다."); } else { - fcmNotificationService.sendNotification("기프티콘 등록 실패", "자동 등록에 실패했습니다. 수동 등록을 이용해 주세요.", username); - notificationService.save(userService.read(username), null, NotificationType.REGISTERED, "자동 등록에 실패했습니다. 수동 등록을 이용해 주세요."); + fcmNotificationService.sendNotification("기프티콘 등록 실패", "자동 등록에 실패했습니다. 다시 시도해 주세요.", username); + notificationService.save(userService.read(username), null, NotificationType.REGISTERED, "자동 등록에 실패했습니다. 다시 시도해 주세요."); } }); } @@ -83,7 +83,7 @@ public Mono handleGptResponse(VoucherAutoSaveRequestDto v GptTimeoutException { return gptService.getGptResponse(voucherAutoSaveRequestDto) - .timeout(Duration.ofMinutes(15)) + .timeout(Duration.ofMinutes(5)) .onErrorResume(GptTimeoutException.class, throwable -> Mono.error(new GptTimeoutException())) .flatMap(response -> { VoucherSaveRequestDto voucherSaveRequestDto = null; @@ -102,12 +102,12 @@ public Mono handleGptResponse(VoucherAutoSaveRequestDto v public Mono handleSearchResponse(VoucherSaveRequestDto voucherSaveRequestDto, String username, String filename) { return searchService.search(createQuery(productNameProcessor.preprocessing(voucherSaveRequestDto))).flatMap(searchResponseDto -> { + voucherSaveRequestDto.setImageUrl(filename); try { String brandName = searchResponseDto.getHits().getHitsList().get(0).getSource().getBrandName(); String productName = searchResponseDto.getHits().getHitsList().get(0).getSource().getProductName(); voucherSaveRequestDto.setBrandName(brandName); voucherSaveRequestDto.setProductName(productName); - voucherSaveRequestDto.setImageUrl(filename); System.out.println("Search response"); System.out.println(brandName); System.out.println(productName); diff --git a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java index 0902770e..d2a0ae1a 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java +++ b/src/main/java/org/swmaestro/repl/gifthub/vouchers/service/VoucherService.java @@ -22,6 +22,7 @@ import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareRequestDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateRequestDto; +import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUseRequestDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUseResponseDto; import org.swmaestro.repl.gifthub.vouchers.entity.Brand; @@ -164,7 +165,7 @@ public List list(String username) { /* 기프티콘 정보 수정 메서드 */ - public VoucherSaveResponseDto update(Long voucherId, VoucherUpdateRequestDto voucherUpdateRequestDto) { + public VoucherUpdateResponseDto update(Long voucherId, VoucherUpdateRequestDto voucherUpdateRequestDto) { Voucher voucher = voucherRepository.findById(voucherId) .orElseThrow(() -> new BusinessException("존재하지 않는 상품권 입니다.", StatusEnum.NOT_FOUND)); // Balance 수정 @@ -211,8 +212,19 @@ public VoucherSaveResponseDto update(Long voucherId, VoucherUpdateRequestDto vou voucherRepository.save(voucher); - return VoucherSaveResponseDto.builder() + return VoucherUpdateResponseDto.builder() .id(voucherId) + .accessible(voucher.getDeletedAt() == null) + .id(voucher.getId()) + .productId(voucher.getProduct().getId()) + .barcode(voucher.getBarcode()) + .price(voucher.getProduct().getPrice()) + .balance(voucher.getBalance()) + .expiresAt(voucher.getExpiresAt().toString()) + .imageUrl(voucher.getImageUrl()) + .accessible(voucher.getDeletedAt() == null) + .shared(giftCardService.isExist(voucher.getId())) + .checked(voucher.isChecked()) .build(); } diff --git a/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java b/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java index 4d2dfa17..1d3cb1d0 100644 --- a/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java +++ b/src/test/java/org/swmaestro/repl/gifthub/vouchers/controller/VoucherControllerTest.java @@ -30,6 +30,7 @@ import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareRequestDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherShareResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateRequestDto; +import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUpdateResponseDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUseRequestDto; import org.swmaestro.repl.gifthub.vouchers.dto.VoucherUseResponseDto; import org.swmaestro.repl.gifthub.vouchers.service.GptService; @@ -176,20 +177,20 @@ void voucherUpdateTest() throws Exception { .expiresAt("2023-06-15") .build(); - VoucherSaveResponseDto voucherSaveResponseDto = VoucherSaveResponseDto.builder() + VoucherUpdateResponseDto voucherUpdateResponseDto = VoucherUpdateResponseDto.builder() .id(1L) .build(); // when when(jwtProvider.resolveToken(any())).thenReturn("my_awesome_access_token"); when(jwtProvider.getUsername(anyString())).thenReturn("이진우"); - when(voucherService.update(any(), any(VoucherUpdateRequestDto.class))).thenReturn(voucherSaveResponseDto); + when(voucherService.update(any(), any(VoucherUpdateRequestDto.class))).thenReturn(voucherUpdateResponseDto); // then mockMvc.perform(patch("/vouchers/1") .header("Authorization", "Bearer my_awesome_access_token") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(voucherSaveResponseDto))) + .content(objectMapper.writeValueAsString(voucherUpdateResponseDto))) .andExpect(status().isOk()); }