From 0ae8325c1e08a971045564dbe53cde9179f49057 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:26:20 +0900 Subject: [PATCH 01/47] =?UTF-8?q?=F0=9F=93=A6=EF=B8=8F=20=20Chore:=20build?= =?UTF-8?q?=20gradle=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++++ .../domain/model/port/out/EmbeddingPort.java | 2 ++ .../embedding/OpenaiEmbeddingAdapter.java | 2 ++ .../infrastructure/config/OpenaiConfig.java | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java diff --git a/build.gradle b/build.gradle index 172f63c..3df4e2c 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,8 @@ configurations { repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } } dependencies { @@ -46,6 +48,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.17.10' + // OpenAi + implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M5' + // build.gradle if (project.hasProperty('env') && project.env == 'test') { dependencies { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java new file mode 100644 index 0000000..4073e5d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java @@ -0,0 +1,2 @@ +package com.likelion.backendplus4.yakplus.drug.domain.model.port.out;public interface EmbeddingPort { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java new file mode 100644 index 0000000..eaa5b03 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java @@ -0,0 +1,2 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding;public class OpenaiEmbeddingAdapter { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java new file mode 100644 index 0000000..a1ae50a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.config; + +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenaiConfig { + @Value("${spring.ai.openai.api-key}") + private String apiKey; + + @Bean + public OpenAiApi openaiApi() { + OpenAiApi openaiApiKey = new OpenAiApi(apiKey); + return openaiApiKey; + } +} From a6d02eff3e508c5d53812a0daf71507ab5ef8c34 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:30:05 +0900 Subject: [PATCH 02/47] =?UTF-8?q?=E2=9C=A8=20=20Feat:=20Openai=20Embedding?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/port/out/EmbeddingPort.java | 5 ++- .../embedding/OpenaiEmbeddingAdapter.java | 40 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java index 4073e5d..665f7f3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java @@ -1,2 +1,5 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model.port.out;public interface EmbeddingPort { +package com.likelion.backendplus4.yakplus.drug.domain.model.port.out; + +public interface EmbeddingPort { + float[] getEmbedding(String text); //문자 embedding 하여 float 배열로 반환 } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java index eaa5b03..3703b0a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java @@ -1,2 +1,40 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding;public class OpenaiEmbeddingAdapter { +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding; + +import java.util.List; + +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.Embedding; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; + +@Service +public class OpenaiEmbeddingAdapter implements EmbeddingPort { + + private final OpenAiApi openAiApi; + + public OpenaiEmbeddingAdapter(OpenAiApi openAiApi) { + this.openAiApi = openAiApi; + } + @Override + public float[] getEmbedding(String text) { + OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( + this.openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model("text-embedding-ada-002") + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE); + + EmbeddingResponse embeddingResponse = openAiEmbeddingModel + .embedForResponse(List.of(text)); + + Embedding embedding = embeddingResponse.getResults().getFirst(); + return embedding.getOutput(); + } } From 8b48c6d35b162f7f2f36a14aa4477d7bb75ba2ea Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:22:48 +0900 Subject: [PATCH 03/47] =?UTF-8?q?=F0=9F=90=9B=20=20Bug:=20application.yml?= =?UTF-8?q?=20openai=20api=20key=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6b4b91a..ec3d3b7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,7 @@ spring: + ai: + openai: + api-key: ${OPENAI_API_KEY} application: name: yakplus elasticsearch: From a33906a9f3a93c57f8b79691bd5b0acbf62e5a68 Mon Sep 17 00:00:00 2001 From: thelightway Date: Thu, 24 Apr 2025 15:03:25 +0900 Subject: [PATCH 04/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=A3=BC=EC=9D=98=EC=82=AC=ED=95=AD=20=EC=9C=A0=ED=98=95=20enu?= =?UTF-8?q?m=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yakplus/drug/domain/model/GovDrug.java | 10 ++--- .../drug/domain/model/vo/WarningType.java | 45 ------------------- src/main/resources/application.yml | 2 +- 3 files changed, 5 insertions(+), 52 deletions(-) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/WarningType.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java index aa20b2d..dc93cff 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.WarningType; import lombok.Builder; import lombok.Getter; @@ -72,24 +71,23 @@ public List getEfficacy() { return efficacys; } - public Map> getPrecaution() { + public Map> getPrecaution() { ObjectMapper objectMapper = new ObjectMapper(); - Map> result = new LinkedHashMap<>(); + Map> result = new LinkedHashMap<>(); try { JsonNode json = objectMapper.readTree(this.precaution); JsonNode articles = json.get("sections").get(0).get("articles"); for (JsonNode article : articles) { - String rawTitle = article.get("title").asText(); - WarningType type = WarningType.fromLabel(rawTitle); + String title = article.get("title").asText(); List texts = new ArrayList<>(); for (JsonNode paragraph : article.get("paragraphs")) { texts.add(paragraph.get("text").asText()); } - result.put(type, texts); + result.put(title, texts); } } catch (JsonProcessingException e) { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/WarningType.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/WarningType.java deleted file mode 100644 index 38640bf..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/WarningType.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model.vo; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum WarningType { - WARNING("경고"), - DO_NOT_ADMINISTER("다음 환자에는 투여하지 말 것."), - CAUTION_ADMINISTER("다음 환자에는 신중히 투여할 것."), - ADVERSE_REACTIONS("이상반응"), - GENERAL_CAUTION("일반적 주의"), - PREGNANCY("임부에 대한 투여"), - PREGNANCY2("임부, 수유부, 가임여성, 신생아, 유아, 소아, 고령자에 대한 투여"), - PEDIATRIC("소아에 대한 투여"), - ELDERLY("고령자에 대한 투여"), - OVERDOSE("과량투여시의 처치"), - USAGE_NOTES("적용상의 주의"), - STORE_NOTES("보관 및 취급상의 주의사항"); - - private final String label; - - WarningType(String label) { - this.label = label; - } - - @JsonValue - public String getLabel() { - return label; - } - - @JsonCreator - public static WarningType fromLabel(String title) { - String cleaned = removeLeadingNumber(title); - for (WarningType type : values()) { - if (type.label.equals(cleaned)) { - return type; - } - } - throw new IllegalArgumentException("Unknown title: " + title); - } - - private static String removeLeadingNumber(String title) { - return title.replaceFirst("^\\d+\\.\\s*", ""); // 숫자 + 점 + 공백 제거 - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ec3d3b7..178849b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: jpa: database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: create + ddl-auto: none # ddl-auto: create # show-sql: true properties: From d5681ba2d215364a6010b6acf6a1ee8fbefd3a63 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:45:23 +0900 Subject: [PATCH 05/47] =?UTF-8?q?=E2=9C=A8=20=20Feat:=20Embedding=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/port/out/EmbeddingPort.java | 4 ++- .../adapter/embedding/EmbeddingAdapter.java | 36 +++++++++++++++++++ .../adapter/embedding/EmbeddingModelType.java | 7 ++++ .../embedding/client/EmbeddingClient.java | 8 +++++ .../OpenaiEmbeddingClient.java} | 19 ++++++---- .../controller/DrugDataTestController.java | 22 ++++++++++-- 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/{OpenaiEmbeddingAdapter.java => client/OpenaiEmbeddingClient.java} (72%) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java index 665f7f3..2a2ff9a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java @@ -1,5 +1,7 @@ package com.likelion.backendplus4.yakplus.drug.domain.model.port.out; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; + public interface EmbeddingPort { - float[] getEmbedding(String text); //문자 embedding 하여 float 배열로 반환 + float[] getEmbedding(String text, EmbeddingModelType modelType); //문자 embedding 하여 float 배열로 반환 } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java new file mode 100644 index 0000000..65cf5f6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java @@ -0,0 +1,36 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.Embedding; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client.EmbeddingClient; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client.OpenaiEmbeddingClient; + +import lombok.RequiredArgsConstructor; + +@Service +public class EmbeddingAdapter implements EmbeddingPort { + private final Map embeddingClientMap; + + public EmbeddingAdapter(List clients) { + this.embeddingClientMap = clients.stream() + .collect(Collectors.toMap(EmbeddingClient::getModelType, client -> client)); + } + @Override + public float[] getEmbedding(String text, EmbeddingModelType embeddingModelType) { + EmbeddingClient client = embeddingClientMap.get(embeddingModelType); + + return client.getEmbedding(text); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java new file mode 100644 index 0000000..ffd7633 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding; + +public enum EmbeddingModelType { + OPENAI, + SBERT, + KM_BERT +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java new file mode 100644 index 0000000..9e96d82 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; + +public interface EmbeddingClient { + EmbeddingModelType getModelType(); + float[] getEmbedding(String text); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/OpenaiEmbeddingClient.java similarity index 72% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/OpenaiEmbeddingClient.java index 3703b0a..5a7cf39 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/OpenaiEmbeddingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/OpenaiEmbeddingClient.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding; +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; import java.util.List; @@ -9,18 +9,23 @@ import org.springframework.ai.openai.OpenAiEmbeddingOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.retry.RetryUtils; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; -import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; - -@Service -public class OpenaiEmbeddingAdapter implements EmbeddingPort { +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; +@Component +public class OpenaiEmbeddingClient implements EmbeddingClient { private final OpenAiApi openAiApi; - public OpenaiEmbeddingAdapter(OpenAiApi openAiApi) { + public OpenaiEmbeddingClient(OpenAiApi openAiApi) { this.openAiApi = openAiApi; } + + @Override + public EmbeddingModelType getModelType() { + return EmbeddingModelType.OPENAI; + } + @Override public float[] getEmbedding(String text) { OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index c36be75..1798bc5 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -1,25 +1,41 @@ package com.likelion.backendplus4.yakplus.drug.presentation.controller; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + import java.util.List; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.likelion.backendplus4.yakplus.drug.application.service.DrugDataService; import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; +import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; + import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.response.ApiResponse; @RestController -@Slf4j @RequiredArgsConstructor public class DrugDataTestController { private final DrugDataService dragDataService; + private final EmbeddingPort embeddingPort; @GetMapping("/data/all") public List getAllData(){ - log.info("getAllData"); + // log.info("getAllData"); return dragDataService.findAllRawDrug(); } + @GetMapping("/test/embed") + public ResponseEntity> getEmbedData(){ + log("getEmbedData"); + float[] embedding = embeddingPort.getEmbedding("test", EmbeddingModelType.OPENAI); + return ApiResponse.success(embedding); + + } + } From 4b461a85d6aa62d957616f42680a577fc5042eb4 Mon Sep 17 00:00:00 2001 From: thelightway Date: Thu, 24 Apr 2025 17:03:35 +0900 Subject: [PATCH 06/47] =?UTF-8?q?=E2=9C=A8=20Feat:=20API=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=95=EC=A0=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/DrugDataService.java | 4 +- .../service/DrugDataServiceImpl.java | 5 +- .../yakplus/drug/domain/model/GovDrug.java | 82 ++--------- .../support/api/ApiResponseMapper.java | 21 ++- .../support/mapper/DrugDataMapper.java | 131 +++++++++++++++++- .../controller/DrugDataTestController.java | 5 +- 6 files changed, 155 insertions(+), 93 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java index eabec2b..7db9215 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java @@ -2,8 +2,10 @@ import java.util.List; +import org.springframework.data.domain.Pageable; + import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; public interface DrugDataService { - List findAllRawDrug(); + List findAllRawDrug(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java index 9ce3101..4b071ff 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java @@ -4,6 +4,7 @@ import java.util.List; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; @@ -20,9 +21,9 @@ public class DrugDataServiceImpl implements DrugDataService { private final GovDrugJpaRepository govDrugJpaRepository; @Override - public List findAllRawDrug() { + public List findAllRawDrug(Pageable pageable) { log.info("findAllRawDrug called"); - return govDrugJpaRepository.findAll().stream() + return govDrugJpaRepository.findAll(pageable).stream() .map(DrugDataMapper::toDomainFromEntity) .collect(toList()); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java index dc93cff..2d39c3e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java @@ -6,95 +6,33 @@ import java.util.List; import java.util.Map; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; import lombok.Builder; import lombok.Getter; +import lombok.ToString; @Builder @Getter +@ToString public class GovDrug { private Long drugId; private String drugName; private String company; private LocalDate permitDate; private boolean isGeneral; - private String materialInfo; + private List materialInfo; private String storeMethod; private String validTerm; - private String efficacy; - private String usage; - private String precaution; + private List efficacy; + private List usage; + private Map> precaution; private String imageUrl; - - public List getMaterialInfo() { - List matrerials = new ArrayList<>(); - - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode json = objectMapper.readTree(materialInfo); - - if (json.isArray()) { - for (JsonNode node : json) { - Material ingredient = objectMapper.treeToValue(node, Material.class); - matrerials.add(ingredient); - } - } - } - catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - - - return matrerials; - } - - public List getEfficacy() { - List efficacys = new ArrayList<>(); - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode json = objectMapper.readTree(this.efficacy); - for (JsonNode section : json.get("sections")) { - for (JsonNode article : section.get("articles")) { - for (JsonNode paragraph : article.get("paragraphs")) { - efficacys.add(paragraph.get("text").asText()); - } - } - } - } catch (JsonProcessingException e) { - //TODO: 예외처리 - throw new RuntimeException(e); - } - return efficacys; - } - - public Map> getPrecaution() { - ObjectMapper objectMapper = new ObjectMapper(); - Map> result = new LinkedHashMap<>(); - - try { - JsonNode json = objectMapper.readTree(this.precaution); - JsonNode articles = json.get("sections").get(0).get("articles"); - - for (JsonNode article : articles) { - String title = article.get("title").asText(); - - List texts = new ArrayList<>(); - for (JsonNode paragraph : article.get("paragraphs")) { - texts.add(paragraph.get("text").asText()); - } - - result.put(title, texts); - } - - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - - - return result; - } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java index c48cf28..566ff5f 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java @@ -26,17 +26,16 @@ public static JsonNode getItemsFromResponse(String response) { public static int getTotalCountFromResponse(String response) { log.info("응답에서 데이터 사이즈 추출"); - return 10_000; - // try { - // return new ObjectMapper().readTree(response) - // .path("body") - // .path("totalCount") - // .asInt(); - // } catch (JsonProcessingException e) { - // log.error("totalCount 추출 실패"); - // //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 - // throw new RuntimeException(e); - // } + try { + return new ObjectMapper().readTree(response) + .path("body") + .path("totalCount") + .asInt(); + } catch (JsonProcessingException e) { + log.error("totalCount 추출 실패"); + //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 + throw new RuntimeException(e); + } } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java index c5e8b83..d8e00da 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java @@ -1,23 +1,144 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; public class DrugDataMapper { public static GovDrug toDomainFromEntity(GovDrugEntity e){ - return GovDrug.builder() + GovDrug domain = GovDrug.builder() .drugId(e.getId()) .drugName(e.getDrugName()) .company(e.getCompany()) .permitDate(e.getPermitDate()) .isGeneral(e.isGeneral()) - .materialInfo(e.getMaterialInfo()) + .materialInfo(convertMaterialInfo(e.getMaterialInfo())) .storeMethod(e.getStoreMethod()) .validTerm(e.getValidTerm()) - .efficacy(e.getEfficacy()) - .usage(e.getUsage()) - .precaution(e.getPrecaution()) + .efficacy(convertEfficacy(e.getEfficacy())) + .usage(getUsage(e.getUsage())) + .precaution(getPrecaution(e.getPrecaution())) .imageUrl(e.getImageUrl()) .build(); + log(domain.toString()); + return domain; + } + + private static List getUsage(String usage){ + List usages = new ArrayList<>(); + JsonNode json = toJsonNodeFromString(usage); + if(!json.isNull() && json.has("sections")){ + for (JsonNode section : json.get("sections")) { + for (JsonNode article : section.get("articles")) { + for (JsonNode paragraph : article.get("paragraphs")) { + usages.add(paragraph.get("text").asText()); + } + } + } + } + return usages; + } + + private static List convertMaterialInfo(String material) { + JsonNode json = toJsonNodeFromString(material); + if (json.isArray()) { + return mapFromMaterialJson(json); + } + return null; + } + + private static List mapFromMaterialJson(JsonNode json) { + List matrerials = new ArrayList<>(); + + ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + for (JsonNode node : json) { + Material ingredient = objectMapper.treeToValue(node, Material.class); + matrerials.add(ingredient); + } + return matrerials; + } catch (Exception e){ + log(LogLevel.ERROR, "객체 맵핑 실패", e); + return null; + } + } + + private static JsonNode toJsonNodeFromString(String json) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readTree(json); + } catch (Exception e) { + log(LogLevel.ERROR, "json 객체 생성 에러", e); + return null; + } + } + + private static List convertEfficacy(String efficacy) { + JsonNode json = toJsonNodeFromString(efficacy); + List efficacys = new ArrayList<>(); + tryParseParagraphs(json, efficacys); + + if(efficacys.size() == 0){ + tryParseTitle(json, efficacys); + } + return efficacys; + } + + private static List tryParseTitle(JsonNode json, List efficacys) { + if(json.has("sections")){ + for (JsonNode section : json.get("sections")) { + for (JsonNode article : section.get("articles")) { + efficacys.add(article.get("title").asText()); + } + } + } + + return efficacys; + } + + private static List tryParseParagraphs(JsonNode json, List efficacys) { + if(json.has("sections")){ + for (JsonNode section : json.get("sections")) { + for (JsonNode article : section.get("articles")) { + for (JsonNode paragraph : article.get("paragraphs")) { + efficacys.add(paragraph.get("text").asText()); + } + } + } + } + + return efficacys; + } + + private static Map> getPrecaution(String precaution) { + Map> result = new LinkedHashMap<>(); + + JsonNode json = toJsonNodeFromString(precaution); + if(json.has("sections")){ + JsonNode articles = json.get("sections").get(0).get("articles"); + for (JsonNode article : articles) { + String title = article.get("title").asText(); + List texts = new ArrayList<>(); + for (JsonNode paragraph : article.get("paragraphs")) { + texts.add(paragraph.get("text").asText()); + } + result.put(title, texts); + } + } + + return result; } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index c36be75..0dd71d5 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,9 +18,9 @@ public class DrugDataTestController { private final DrugDataService dragDataService; @GetMapping("/data/all") - public List getAllData(){ + public List getAllData(Pageable pageable){ log.info("getAllData"); - return dragDataService.findAllRawDrug(); + return dragDataService.findAllRawDrug(pageable); } } From 385b0fe1cd60eb32281c58b05be63f0ea538fc56 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:23:25 +0900 Subject: [PATCH 07/47] =?UTF-8?q?=E2=9C=A8=20=20Feat:=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=EC=88=98=20=EA=B3=84=EC=82=B0=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/api/ApiPageCounter.java | 47 +++++++++++++++++++ .../support/api/ApiUriCompBuilder.java | 28 +++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java new file mode 100644 index 0000000..6ebf7fb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.support.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; + +@Component +public class ApiPageCounter { + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ApiUriCompBuilder uriBuilder; + private final int numOfRows; + + public ApiPageCounter(ApiUriCompBuilder uriBuilder, + @Value("${gov.numOfRows}") int numOfRows) { + this.uriBuilder = uriBuilder; + this.numOfRows = numOfRows; + } + + public int getDetailApiTotalPageCount() { + URI uri = uriBuilder.getUriForDetailApiShort(); + return getPageCountFromUri(uri); + } + + public int getImgApiTotalPageCount() { + URI uri = uriBuilder.getUriForImgApiShort(); + return getPageCountFromUri(uri); + } + + private int getPageCountFromUri(URI uri) { + try { + String response = restTemplate.getForObject(uri, String.class); + int totalRows = objectMapper.readTree(response).path("body").path("totalCount").asInt(); + int pageCount = totalRows / numOfRows; + if (totalRows % numOfRows > 0) { + pageCount += 1; + } + return pageCount; + } catch (Exception e) { + throw new RuntimeException("페이지 수 계산 중 오류 발생", e); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java index f3312f6..eba7127 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java @@ -79,4 +79,32 @@ public URI getUriForDetailApi(int pageNo) { public URI getUriForImgApi(int pageNo) { return getUri(API_IMG_PATH, pageNo); } + + public URI getUriForDetailApiShort() { + return UriComponentsBuilder.newInstance() + .scheme("https") + .host(HOST) + .port(443) + .path(API_DETAIL_PATH) + .queryParam("serviceKey", SERVICE_KEY) + .queryParam("type", "json") + .queryParam("pageNo", 1) + .queryParam("numOfRows", 1) + .build(true) + .toUri(); + } + + public URI getUriForImgApiShort() { + return UriComponentsBuilder.newInstance() + .scheme("https") + .host(HOST) + .port(443) + .path(API_IMG_PATH) + .queryParam("serviceKey", SERVICE_KEY) + .queryParam("type", "json") + .queryParam("pageNo", 1) + .queryParam("numOfRows", 1) + .build(true) + .toUri(); + } } From 5f5b94ab3ebc500665eaab8257887c1ba4bf02a5 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Thu, 24 Apr 2025 21:14:33 +0900 Subject: [PATCH 08/47] =?UTF-8?q?=20=F0=9F=93=A6=20Chore:=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=84=9C=EB=B2=84=20=EB=B0=B0=ED=8F=AC=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 * 📦 Chore: 포트 번호 변경 * 📦 Chore: git-action 테스트 서버 상태 URL 변경 * 📦 Chore: 오타 수정 --- .github/workflows/dev-deploy.yml | 4 ++-- src/main/resources/application.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index e7b999b..577a40b 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -38,7 +38,7 @@ jobs: - name: Wait for test server to be healthy run: | - echo "🔍 Checking https://yakplus-test.techlog.dev/actuator/health ..." + echo "🔍 Checking https://yakplus-batch.techlog.dev/actuator/health ..." curl --silent --fail \ --retry 5 --retry-connrefused --retry-delay 5 \ - https://yakplus-test.techlog.dev/actuator/health \ No newline at end of file + https://yakplus-batch.techlog.dev/actuator/health \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 178849b..7a49aa4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,7 +28,7 @@ gov: serviceKey: ${GOV_SERVICE_KEY} numOfRows: 100 path: - detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnDtlInq05 + detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrbmsnDtlInq05 img: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnInq06 server: - port: 8084 + port: 8077 From 9eee333e120bb523ae5da95f7c6f9986367f300d Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Thu, 24 Apr 2025 21:23:52 +0900 Subject: [PATCH 09/47] =?UTF-8?q?=20=F0=9F=90=9B=20Fix:=20=EC=BB=B4?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drug/presentation/controller/DrugDataTestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index 679249b..f729f59 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -5,6 +5,7 @@ import java.util.List; import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -26,7 +27,6 @@ public class DrugDataTestController { @GetMapping("/data/all") public List getAllData(Pageable pageable){ - log.info("getAllData"); return dragDataService.findAllRawDrug(pageable); } From d5ca2ff90064c82a3c643d43c7823db706d3ecd9 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Thu, 24 Apr 2025 22:10:19 +0900 Subject: [PATCH 10/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20API=20URI=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7a49aa4..fd9bfab 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,7 +28,7 @@ gov: serviceKey: ${GOV_SERVICE_KEY} numOfRows: 100 path: - detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrbmsnDtlInq05 + detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnDtlInq05 img: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnInq06 server: port: 8077 From 6426d9f9a4eb860d2cb1f429fbf09a2315162d0b Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Fri, 25 Apr 2025 05:25:06 +0900 Subject: [PATCH 11/47] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A0=84=EC=B2=B4=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DrugImageGovScraper.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java index 78a4359..9aabca5 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiPageCounter; import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiResponseMapper; import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.ApiDataDrugImgRepo; @@ -27,6 +28,7 @@ public class DrugImageGovScraper { private final RestTemplate restTemplate; private final ApiDataDrugImgRepo imgRepo; private final ObjectMapper objectMapper; + private final ApiPageCounter apiPageCounter; @Transactional public void getApiData(){ @@ -45,4 +47,23 @@ public void getApiData(){ } imgRepo.saveAllAndFlush(imgDatas); } + + public void getAllApiData(){ + log.info("의약품 개요 정보 API 호출 시작"); + int totalPageCount = apiPageCounter.getImgApiTotalPageCount(); + for(int i=1;i<=totalPageCount;i++){ + URI uriForImgApi = uriCompBuilder.getUriForImgApi(i); + String response = restTemplate.getForObject(uriForImgApi, String.class); + JsonNode items = ApiResponseMapper.getItemsFromResponse(response); + List imgDatas = null; + try { + imgDatas = objectMapper.readValue(items.toString(), + new TypeReference>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + imgRepo.saveAllAndFlush(imgDatas); + } + } + } From c7b08b005771498451d553c3403817b37c3d13ea Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Fri, 25 Apr 2025 16:17:55 +0900 Subject: [PATCH 12/47] =?UTF-8?q?=E2=9C=A8=20Feat:=20Embed=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=202=EA=B0=9C(KM-BERT,KR-SBERT)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: Embed 모델 2개(KM-BERT,KR-SBERT) 추가 * ✨ Feat: 임베딩 처리 클래스 생성 * ✨ Feat: Api Raw 데이터 테이블 병합 * ✨ Feat: 테스트 컨트롤러 추가 및 사소한 오류 수정 * ♻️ Refactor: 통합 테이블 생성 구조 변경 * 🐛 Fix: 테이블 병합 코드 주석처리 --- .../service/DrugDataServiceImpl.java | 10 +- .../service/DrugEmbedProcessor.java | 116 ++++++++++++++++++ .../service/scraper/DrugScraper.java | 28 +++++ .../yakplus/drug/domain/model/GovDrug.java | 3 + .../drug/domain/model/GovDrugDetail.java | 11 +- .../client/EmbeddingRequestText.java | 14 +++ .../client/KmBertEmbeddingClient.java | 47 +++++++ .../client/KrSbertEmbeddingClient.java | 46 +++++++ .../entity/ApiDataDrugImgEntity.java | 4 + .../repository/entity/GovDrugEmbedEntity.java | 29 +++++ .../repository/entity/GovDrugEntity.java | 20 ++- .../jdbc/GovDrugJdbcRepository.java | 29 +++++ .../repository/jdbc/MergeBatchSetter.java | 53 ++++++++ .../jpa/GovDrugEmbedJpaRepository.java | 10 ++ .../repository/jpa/GovDrugJpaRepository.java | 64 ++++++++++ .../support/api/ApiPageCounter.java | 5 +- .../support/api/ApiResponseMapper.java | 23 ++-- .../support/api/ApiUriCompBuilder.java | 28 ++++- ...gDataMapper.java => DrugDetailMapper.java} | 43 +++++-- .../controller/DrugDataTestController.java | 9 ++ src/main/resources/application.yml | 15 ++- 21 files changed, 568 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/{DrugDataMapper.java => DrugDetailMapper.java} (77%) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java index 4b071ff..a04402c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDataMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailMapper; import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; import lombok.RequiredArgsConstructor; @@ -23,8 +23,10 @@ public class DrugDataServiceImpl implements DrugDataService { @Override public List findAllRawDrug(Pageable pageable) { log.info("findAllRawDrug called"); - return govDrugJpaRepository.findAll(pageable).stream() - .map(DrugDataMapper::toDomainFromEntity) - .collect(toList()); + //TODO: Detail을 전처리에서 미리 텍스트를 plain으로 만들고 통합 테이블 가져오는 것 다시 제작 필요 + return null; + // return govDrugJpaRepository.findAll(pageable).stream() + // .map(DrugDetailMapper::toDomainFromEntity) + // .collect(toList()); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java new file mode 100644 index 0000000..ea8dba7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java @@ -0,0 +1,116 @@ +package com.likelion.backendplus4.yakplus.drug.application.service; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrugDetail; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingAdapter; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc.GovDrugJdbcRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.ApiDataDrugImgRepo; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugDetailJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailMapper; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DrugEmbedProcessor { + private final EmbeddingAdapter adapter; + private final GovDrugDetailJpaRepository govDrugDetailJpaRepository; + private final EmbeddingAdapter embeddingAdapter; + private final ApiDataDrugImgRepo apiDataDrugImgRepo; + private final GovDrugJpaRepository govDrugJpaRepository; + private final GovDrugJdbcRepository govDrugJdbcRepository; + + public void startEmbedding(){ + List allItem = getAllItem(); + List drugEntitys = new ArrayList<>(); + for (GovDrugDetailEntity detailEntity : allItem) { + GovDrugDetail govDrugDetail = DrugDetailMapper.toDomainFromEntity(detailEntity); + + String text = convertSingleStringForEfficacy(govDrugDetail.getEfficacy()); + + float[] sbertVector = embeddingAdapter.getEmbedding( + text, + EmbeddingModelType.SBERT); + + float[] kmbertVector = embeddingAdapter.getEmbedding( + text, + EmbeddingModelType.KM_BERT); + + float[] openAIVector = embeddingAdapter.getEmbedding( + text, + EmbeddingModelType.OPENAI); + + GovDrugEntity govDrugEntity = bulidGovDrugEntity( + govDrugDetail , openAIVector, kmbertVector, sbertVector + ); + drugEntitys.add(govDrugEntity); + } + saveEntitys(drugEntitys); + + + } + private void saveEntitys(List entitys){ + govDrugJpaRepository.saveAll(entitys); + govDrugJpaRepository.flush(); + entitys.clear(); + } + + private GovDrugEntity bulidGovDrugEntity(GovDrugDetail govDrugDetail, float[] openAIVector, float[] kmbertVector, + float[] sbertVector) { + return GovDrugEntity.builder() + .drugId(govDrugDetail.getDrugId()) + .drugName(govDrugDetail.getDrugName()) + .company(govDrugDetail.getCompany()) + .permitDate(govDrugDetail.getPermitDate()) + .isGeneral(govDrugDetail.isGeneral()) + .materialInfo(govDrugDetail.getMaterialInfo().toString()) + .storeMethod(govDrugDetail.getStoreMethod()) + .validTerm(govDrugDetail.getValidTerm()) + .efficacy(govDrugDetail.getEfficacy().toString()) + .usage(govDrugDetail.getUsage().toString()) + .precaution(govDrugDetail.getPrecaution().toString()) + .imageUrl(getImageUrl(govDrugDetail.getDrugId())) + .gptVector(toStringFromFloatArray(openAIVector)) + .kmBertVector(toStringFromFloatArray(kmbertVector)) + .sbertVector(toStringFromFloatArray(sbertVector)) + .build(); + } + + private String getImageUrl(Long drugId) { + ApiDataDrugImgEntity drugImgEntity = apiDataDrugImgRepo.findById(drugId) + .orElseGet(() -> new ApiDataDrugImgEntity()); + return drugImgEntity.getImgUrl(); + } + private String toStringFromFloatArray(float[] openAIVector) { + try { + return new ObjectMapper().writeValueAsString(openAIVector); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return null; + } + } + + private String convertSingleStringForEfficacy(List stringList) { + StringBuilder stringBuilder = new StringBuilder(); + for (String s : stringList) { + stringBuilder.append(s); + stringBuilder.append(" "); + } + return stringBuilder.toString(); + } + + private List getAllItem() { + return govDrugDetailJpaRepository.findAll(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java new file mode 100644 index 0000000..8d7ff20 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper; + +import org.springframework.stereotype.Service; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.drug.application.service.DrugApprovalDetailScraper; +import com.likelion.backendplus4.yakplus.drug.application.service.DrugEmbedProcessor; +import com.likelion.backendplus4.yakplus.drug.application.service.DrugImageGovScraper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugDetailJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DrugScraper { + private final DrugImageGovScraper drugImageGovScraper; + private final DrugEmbedProcessor embedProcessor; + private final DrugApprovalDetailScraper drugApprovalDetailScraper; + private final DrugEmbedProcessor drugEmbedProcessor; + private final GovDrugJpaRepository govDrugJpaRepository; + + public void scraperStart(){ + drugApprovalDetailScraper.requestUpdateAllRawDataByJdbc(); + drugImageGovScraper.getAllApiData(); + drugEmbedProcessor.startEmbedding(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java index 2d39c3e..9e828e3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java @@ -35,4 +35,7 @@ public class GovDrug { private List usage; private Map> precaution; private String imageUrl; + private float[] gptVector; + private float[] sbertVector; + private float[] kmBertVector; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java index bb7ceb0..420fc06 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java @@ -1,10 +1,13 @@ package com.likelion.backendplus4.yakplus.drug.domain.model; import java.time.LocalDate; +import java.util.List; +import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; import lombok.Builder; import lombok.Getter; @@ -17,12 +20,12 @@ public class GovDrugDetail { private String company; private LocalDate permitDate; private boolean isGeneral; - private String materialInfo; + private List materialInfo; private String storeMethod; private String validTerm; - private String efficacy; - private String usage; - private String precaution; + private List efficacy; + private List usage; + private Map> precaution; public JsonNode toJson(String json) { try { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java new file mode 100644 index 0000000..af90ff1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java @@ -0,0 +1,14 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class EmbeddingRequestText { + private String text; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java new file mode 100644 index 0000000..898e2f6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; + +import java.net.URI; + +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class KmBertEmbeddingClient implements EmbeddingClient { + private final ApiUriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + @Override + public EmbeddingModelType getModelType() { + return EmbeddingModelType.KM_BERT; + } + + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVetor(embeddingURI, text); + } + + private float[] getEmbeddingVetor(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKmbertEmbeding(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java new file mode 100644 index 0000000..702fa1b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; + +import java.net.URI; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class KrSbertEmbeddingClient implements EmbeddingClient { + private final ApiUriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + @Override + public EmbeddingModelType getModelType() { + return EmbeddingModelType.SBERT; + } + + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVetor(embeddingURI, text); + } + + private float[] getEmbeddingVetor(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKrSbertEmbeding(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java index 3b4cbdf..6cde395 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java @@ -5,9 +5,11 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.Getter; import lombok.ToString; @Entity +@Getter @ToString @Table(name="API_DATA_DRUG_IMG") public class ApiDataDrugImgEntity { @@ -17,4 +19,6 @@ public class ApiDataDrugImgEntity { @JsonProperty("BIG_PRDT_IMG_URL") private String imgUrl; + + } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java new file mode 100644 index 0000000..38f58bd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "GOV_EMBED_DATA") +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GovDrugEmbedEntity { + @Id + @Column( name= "ITEM_SEQ") + private Long drugId; + + @Column( name= "GPT_VECTOR", columnDefinition = "JSON") + private String gptVector; + + @Column( name= "KR_SBERT_VECTOR", columnDefinition = "JSON") + private String krSbertVector; + + @Column( name= "KM_BERT_VECTOR", columnDefinition = "JSON") + private String kmBertVector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java index c879ccf..5f6aae8 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java @@ -1,6 +1,7 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; import java.time.LocalDate; +import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -22,7 +23,7 @@ public class GovDrugEntity { @Id @Column(name="ITEM_SEQ") - private Long id; + private Long drugId; @Column( name= "ITEM_NAME", columnDefinition = "TEXT") private String drugName; @@ -36,7 +37,7 @@ public class GovDrugEntity { @Column(name = "ETC_OTC_CODE") private boolean isGeneral; - @Column(name = "MATERIAL_NAME", columnDefinition = "JSON") + @Column(name = "MATERIAL_NAME", columnDefinition = "TEXT") private String materialInfo; @JsonProperty("STORAGE_METHOD") @@ -46,15 +47,24 @@ public class GovDrugEntity { @Column(name = "VALID_TERM") private String validTerm; - @Column(name = "EE_DOC_DATA", columnDefinition = "JSON") + @Column(name = "EE_DOC_DATA", columnDefinition = "TEXT") private String efficacy; - @Column(name = "UD_DOC_DATA", columnDefinition = "JSON") + @Column(name = "UD_DOC_DATA", columnDefinition = "TEXT") private String usage; - @Column(name = "NB_DOC_DATA", columnDefinition = "JSON") + @Column(name = "NB_DOC_DATA", columnDefinition = "TEXT") private String precaution; @Column(name= "IMG_URL") private String imageUrl; + + @Column(name= "gpt_vector", columnDefinition = "JSON") + private String gptVector; + + @Column(name= "KR_SBERT_VECTOR",columnDefinition = "JSON") + private String sbertVector; + + @Column(name= "KM_BERT_VECTOR",columnDefinition = "JSON") + private String kmBertVector; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java index 6b008bc..59e16f6 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -37,4 +38,32 @@ public void saveAll(List entities) { + " NB_DOC_DATA = VALUES(NB_DOC_DATA)"; jdbc.batchUpdate(sql, new JdbcBatchSetter(entities)); } + + @Transactional + public void mergeAll(List entities) { + String sql = "" + + "INSERT INTO gov_drug_raw_data " + + " (ITEM_SEQ, ITEM_NAME, ENTP_NAME, " + + " ITEM_PERMIT_DATE, ETC_OTC_CODE, MATERIAL_NAME, " + + " STORAGE_METHOD, VALID_TERM, EE_DOC_DATA, " + + " UD_DOC_DATA, NB_DOC_DATA, IMG_URL, " + + " GPT_VECTOR, KR_SBERT_VECTOR, KM_BERT_VECTOR) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE " + + " ITEM_NAME = VALUES(ITEM_NAME), " + + " ENTP_NAME = VALUES(ENTP_NAME), " + + " ITEM_PERMIT_DATE = VALUES(ITEM_PERMIT_DATE), " + + " ETC_OTC_CODE = VALUES(ETC_OTC_CODE), " + + " MATERIAL_NAME = VALUES(MATERIAL_NAME), " + + " STORAGE_METHOD = VALUES(STORAGE_METHOD), " + + " VALID_TERM = VALUES(VALID_TERM), " + + " EE_DOC_DATA = VALUES(EE_DOC_DATA), " + + " UD_DOC_DATA = VALUES(UD_DOC_DATA), " + + " NB_DOC_DATA = VALUES(NB_DOC_DATA), " + + " IMG_URL = VALUES(IMG_URL), " + + " GPT_VECTOR = VALUES(GPT_VECTOR), " + + " KR_SBERT_VECTOR = VALUES(KR_SBERT_VECTOR), " + + " KM_BERT_VECTOR = VALUES(KM_BERT_VECTOR) "; + jdbc.batchUpdate(sql, new MergeBatchSetter(entities)); + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java new file mode 100644 index 0000000..4dfaa17 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java @@ -0,0 +1,53 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.time.LocalDate; +import java.util.List; + +import org.springframework.jdbc.core.BatchPreparedStatementSetter; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MergeBatchSetter implements BatchPreparedStatementSetter { + + private final List entities; + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + GovDrugEntity e = entities.get(i); + ps.setLong (1, e.getDrugId()); + ps.setString (2, e.getDrugName()); + ps.setString (3, e.getCompany()); + + LocalDate permit = e.getPermitDate(); + if (permit != null) { + ps.setDate(4, Date.valueOf(permit)); + } else { + ps.setNull(4, Types.DATE); + } + + ps.setBoolean(5, e.isGeneral()); + ps.setString (6, e.getMaterialInfo()); + ps.setString (7, e.getStoreMethod()); + ps.setString (8, e.getValidTerm()); + ps.setString (9, e.getEfficacy()); + ps.setString (10, e.getUsage()); + ps.setString (11, e.getPrecaution()); + ps.setString(12, e.getImageUrl()); + ps.setString(13, e.getGptVector()); + ps.setString(14, e.getSbertVector()); + ps.setString(15, e.getKmBertVector()); + } + + @Override + public int getBatchSize() { + return entities.size(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java new file mode 100644 index 0000000..f63ceca --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEmbedEntity; + +@Repository +public interface GovDrugEmbedJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java index b6ab711..39ed73b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java @@ -1,8 +1,72 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; +import jakarta.transaction.Transactional; + public interface GovDrugJpaRepository extends JpaRepository { + @Modifying + @Transactional + @Query(value = """ + INSERT INTO gov_drug_raw_data + ( + etc_otc_code, + item_permit_date, + item_seq, + ee_doc_data, + entp_name, + item_name, + material_name, + nb_doc_data, + storage_method, + ud_doc_data, + valid_term, + img_url, + gpt_vector, + kr_sbert_vector, + km_bert_vector + ) + SELECT + d.etc_otc_code, + d.item_permit_date, + d.item_seq, + d.ee_doc_data, + d.entp_name, + d.item_name, + d.material_name, + d.nb_doc_data, + d.storage_method, + d.ud_doc_data, + d.valid_term, + i.img_url, + e.gpt_vector, + e.kr_sbert_vector, + e.km_bert_vector + FROM + gov_drug_detail AS d + LEFT JOIN + api_data_drug_img AS i ON d.item_seq = i.seq + LEFT JOIN + gov_embed_data AS e ON d.item_seq = e.item_seq + ON DUPLICATE KEY UPDATE + etc_otc_code = VALUES(etc_otc_code), + item_permit_date = VALUES(item_permit_date), + ee_doc_data = VALUES(ee_doc_data), + entp_name = VALUES(entp_name), + item_name = VALUES(item_name), + material_name = VALUES(material_name), + nb_doc_data = VALUES(nb_doc_data), + storage_method = VALUES(storage_method), + ud_doc_data = VALUES(ud_doc_data), + valid_term = VALUES(valid_term), + img_url = VALUES(img_url), + gpt_vector = VALUES(gpt_vector), + kr_sbert_vector = VALUES(kr_sbert_vector), + km_bert_vector = VALUES(km_bert_vector) + """, nativeQuery = true) + void createRawDataByApiTable(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java index 6ebf7fb..df70c1a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java @@ -27,8 +27,9 @@ public int getDetailApiTotalPageCount() { } public int getImgApiTotalPageCount() { - URI uri = uriBuilder.getUriForImgApiShort(); - return getPageCountFromUri(uri); + return 4; + // URI uri = uriBuilder.getUriForImgApiShort(); + // return getPageCountFromUri(uri); } private int getPageCountFromUri(URI uri) { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java index 566ff5f..f0c0ada 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java @@ -25,17 +25,18 @@ public static JsonNode getItemsFromResponse(String response) { } public static int getTotalCountFromResponse(String response) { - log.info("응답에서 데이터 사이즈 추출"); - try { - return new ObjectMapper().readTree(response) - .path("body") - .path("totalCount") - .asInt(); - } catch (JsonProcessingException e) { - log.error("totalCount 추출 실패"); - //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 - throw new RuntimeException(e); - } + return 20; + // log.info("응답에서 데이터 사이즈 추출"); + // try { + // return new ObjectMapper().readTree(response) + // .path("body") + // .path("totalCount") + // .asInt(); + // } catch (JsonProcessingException e) { + // log.error("totalCount 추출 실패"); + // //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 + // throw new RuntimeException(e); + // } } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java index eba7127..5f3d3b7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java @@ -22,17 +22,23 @@ public class ApiUriCompBuilder { private final String API_DETAIL_PATH; private final String API_IMG_PATH ; private final int NUM_OF_ROWS; + private final String API_KM_BERT; + private final String API_KR_SBERT; public ApiUriCompBuilder(@Value("${gov.host}") String host, @Value("${gov.serviceKey}") String serviceKey, @Value("${gov.path.detail}") String pathDetail, @Value("${gov.path.img}") String pathImg, - @Value("${gov.numOfRows}") int numOfRows) { + @Value("${gov.numOfRows}") int numOfRows, + @Value("${embed.kmbert}") String API_KM_BERT, + @Value("${embed.krsbert}") String API_KR_SBERT) { this.HOST = host; this.SERVICE_KEY = serviceKey; this.API_DETAIL_PATH = pathDetail; this.API_IMG_PATH = pathImg; this.NUM_OF_ROWS = numOfRows; + this.API_KM_BERT = API_KM_BERT; + this.API_KR_SBERT = API_KR_SBERT; } /*** @@ -107,4 +113,24 @@ public URI getUriForImgApiShort() { .build(true) .toUri(); } + + public URI getUriForKmbertEmbeding() { + return UriComponentsBuilder.newInstance() + .scheme("https") + .host(API_KM_BERT) + .port(443) + .path("/predict") + .build(true) + .toUri(); + } + + public URI getUriForKrSbertEmbeding() { + return UriComponentsBuilder.newInstance() + .scheme("https") + .host(API_KR_SBERT) + .port(443) + .path("/predict") + .build(true) + .toUri(); + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java similarity index 77% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java index d8e00da..effb2c3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java @@ -7,19 +7,20 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrugDetail; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; -public class DrugDataMapper { - public static GovDrug toDomainFromEntity(GovDrugEntity e){ - GovDrug domain = GovDrug.builder() - .drugId(e.getId()) +public class DrugDetailMapper { + public static GovDrugDetail toDomainFromEntity(GovDrugDetailEntity e){ + GovDrugDetail domain = GovDrugDetail.builder() + .drugId(e.getDrugId()) .drugName(e.getDrugName()) .company(e.getCompany()) .permitDate(e.getPermitDate()) @@ -30,12 +31,37 @@ public static GovDrug toDomainFromEntity(GovDrugEntity e){ .efficacy(convertEfficacy(e.getEfficacy())) .usage(getUsage(e.getUsage())) .precaution(getPrecaution(e.getPrecaution())) - .imageUrl(e.getImageUrl()) .build(); - log(domain.toString()); return domain; } + public static GovDrug toDomainFromEntity(GovDrugEntity e){ + return GovDrug.builder() + .drugId(e.getDrugId()) + .drugName(e.getDrugName()) + .company(e.getCompany()) + .permitDate(e.getPermitDate()) + .isGeneral(e.isGeneral()) + .materialInfo(convertMaterialInfo(e.getMaterialInfo())) + .storeMethod(e.getStoreMethod()) + .validTerm(e.getValidTerm()) + .efficacy(convertEfficacy(e.getEfficacy())) + .usage(getUsage(e.getUsage())) + .precaution(getPrecaution(e.getPrecaution())) + .gptVector(toArraysFromFloatString(e.getGptVector())) + .sbertVector(toArraysFromFloatString(e.getSbertVector())) + .kmBertVector(toArraysFromFloatString(e.getKmBertVector())) + .build(); + + } + + private static float[] toArraysFromFloatString(String floatString){ + try { + return new ObjectMapper().readValue(floatString, float[].class); + } catch (Exception e) { + throw new RuntimeException("float 배열로 변환 실패"); + } + } private static List getUsage(String usage){ List usages = new ArrayList<>(); JsonNode json = toJsonNodeFromString(usage); @@ -141,4 +167,7 @@ private static Map> getPrecaution(String precaution) { return result; } + + + } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index f729f59..6773b0b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -9,7 +9,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import com.likelion.backendplus4.yakplus.drug.application.service.DrugApprovalDetailScraper; import com.likelion.backendplus4.yakplus.drug.application.service.DrugDataService; +import com.likelion.backendplus4.yakplus.drug.application.service.scraper.DrugScraper; import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; @@ -24,6 +26,7 @@ public class DrugDataTestController { private final DrugDataService dragDataService; private final EmbeddingPort embeddingPort; + private final DrugScraper scraperUseCase; @GetMapping("/data/all") public List getAllData(Pageable pageable){ @@ -38,4 +41,10 @@ public ResponseEntity> getEmbedData(){ } + @GetMapping("/test/parse") + public ResponseEntity saveAPIData(){ + scraperUseCase.scraperStart(); + return ResponseEntity.ok().build(); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fd9bfab..3cf3445 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,21 +14,26 @@ spring: jpa: database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: none + ddl-auto: create # ddl-auto: create # show-sql: true properties: hibernate: format_sql: true -#logging: -# level: -# root: DEBUG +logging: + level: + org.springframework.jdbc: DEBUG + org.springframework.data.jpa: DEBUG gov: host: apis.data.go.kr serviceKey: ${GOV_SERVICE_KEY} - numOfRows: 100 + numOfRows: 5 path: detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnDtlInq05 img: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnInq06 + +embed: + kmbert: embed.techlog.dev + krsbert: embedb.techlog.dev server: port: 8077 From 469913d1661e2f6c98180ae518d91e7139e0f37b Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Fri, 25 Apr 2025 16:19:56 +0900 Subject: [PATCH 13/47] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9E=84=EB=B2=A0?= =?UTF-8?q?=EB=94=A9=20=EB=AA=A8=EB=8D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: Embed 모델 2개(KM-BERT,KR-SBERT) 추가 * ✨ Feat: 임베딩 처리 클래스 생성 * ✨ Feat: Api Raw 데이터 테이블 병합 * ✨ Feat: 테스트 컨트롤러 추가 및 사소한 오류 수정 * ♻️ Refactor: 통합 테이블 생성 구조 변경 * 🐛 Fix: 테이블 병합 코드 주석처리 From 8da3a974a9f943cdb5e7fe5e81ee478a23f99712 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Mon, 28 Apr 2025 11:19:06 +0900 Subject: [PATCH 14/47] =?UTF-8?q?=E2=9C=A8=20Feature/#21=20=EC=97=98?= =?UTF-8?q?=EB=9D=BC=EC=8A=A4=ED=8B=B1=20=EC=84=9C=EC=B9=98=20=EC=83=89?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 Chore: index 패키지 병합 * ♻️ Refactor: 임베딩 Entity 객체 분할 * 📦 Chore: 파일명 오류 수정 * 📦 Chore: entity 테이블 수정 * ♻️ Refactor: 패키지 구조 변경 및 임베딩 처리 * ♻️ Refactor: 패키지 구조 변경 및 임베딩 처리 * 🐛 Fix: 똑같은 페이지만 가져오는 오류 수정 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ♻️ Refactor: 이미지 데이터 저장 로직 개선 * ♻️ Refactor: 이미지 데이터 저장 로직 개선 * ♻️ Refactor: 메서드명 변경 * ✨ Feature/#16 테이블 병합 (#25) * ✨ Feat: 테이블 병합 추가 * 🐛 Fix: image 저장 과정 중 Object Mapper 오류 수정 * 🐛 Fix: 저장 방법이 안가져와지는 문제 수정 * ✨ Feat: 임베딩 포트 어댑터 추가 (#28) * ✨ Feat: 임베딩 포트 어뎁터 추가 * ♻️ Refactor: 맵핑 공통함수 DrugMapper로 분리 * ✨ Feature/#29 색인 코드 통합 (#30) * ♻️ Refactor: EsIndexName을 GovDrugRawDataPort에서 받아오도록 수정하였습니다. * 📝 Docs: DrugIndexer와 ElasticsearchDrugAdapter 클래스의 주석을 수정하였습니다. * ♻️ Refactor: Elasticsearch에서 개별저장하는 로직을 Bulk로 저장하도록 변경하였습니다., 로그를 추가하였습니다. * ♻️ Refactor: 에러 로그를 추가하였습니다. * 📦 Chore; 주석 추가 (#32) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Co-authored-by: JUNG ANSIK --- .../service/DrugApprovalDetailScraper.java | 9 - .../DrugApprovalDetailScraperImpl.java | 179 ---------------- .../application/service/DrugDataService.java | 11 - .../service/DrugDataServiceImpl.java | 32 --- .../service/DrugEmbedProcessor.java | 116 ----------- .../service/DrugImageGovScraper.java | 69 ------- .../service/port/in/DrugCombineUsecase.java | 15 ++ .../port/in/DrugDetailScraperUsecase.java | 22 ++ .../port/in/DrugEmbedProcessorUseCase.java | 16 ++ .../port/in/DrugImageScraperUsecase.java | 23 +++ .../service/port/in/DrugScraperUsecase.java | 13 ++ .../service/port/out/ApiRequestPort.java | 48 +++++ .../port/out/DrugDetailRepositoryPort.java | 41 ++++ .../port/out/DrugEmbedRepositoryPort.java | 40 ++++ .../port/out/DrugImageRepositoryPort.java | 40 ++++ .../port/out/DrugRawDataRepositoryPort.java | 30 +++ .../service/port/out/EmbeddingPort.java | 24 +++ .../service/scraper/DrugScraper.java | 40 ++-- .../scraper/combiner/DrugCombiner.java | 88 ++++++++ .../detail/DrugDetailScraperService.java | 192 ++++++++++++++++++ .../detail/support/MaterialParser.java | 112 ++++++++++ .../scraper/detail/support}/XMLParser.java | 97 +++++++-- .../scraper/embed/DrugEmbedProcessor.java | 121 +++++++++++ .../scraper/image/DrugImageGovScraper.java | 72 +++++++ .../exception/ScraperException.java | 2 +- .../exception/error/ScraperErrorCode.java | 5 +- .../yakplus/drug/domain/model/Drug.java | 61 ++++++ .../{GovDrugDetail.java => DrugDetail.java} | 16 +- .../yakplus/drug/domain/model/DrugImage.java | 25 +++ .../drug/domain/model/DrugRawData.java | 57 ++++++ .../yakplus/drug/domain/model/GovDrug.java | 41 ---- .../domain/model/port/out/EmbeddingPort.java | 7 - .../drug/domain/model/vo/Material.java | 11 + .../drug/domain/model/vo/MaterialInfo.java | 14 ++ .../adapter/embedding/EmbeddingAdapter.java | 36 ---- .../adapter/embedding/EmbeddingModelType.java | 7 - .../embedding/client/EmbeddingClient.java | 8 - .../repository/jpa/ApiDataDrugImgRepo.java | 10 - .../jpa/GovDrugDetailJpaRepository.java | 18 -- .../jpa/GovDrugEmbedJpaRepository.java | 10 - .../repository/jpa/GovDrugJpaRepository.java | 72 ------- .../api/adapter/ApiRequestAdapter.java | 61 ++++++ .../config/ApiRestTemplateConfig.java | 2 +- .../api => api/support}/ApiPageCounter.java | 8 +- .../support}/ApiResponseMapper.java | 4 +- .../support}/ApiUriCompBuilder.java | 2 +- .../embedding/adapter/EmbeddingAdapter.java | 27 +++ .../embedding/client/EmbeddingClient.java | 8 + .../client/KmBertEmbeddingClient.java | 8 +- .../client/KrSbertEmbeddingClient.java | 7 +- .../client/OpenaiEmbeddingClient.java | 4 +- .../{ => embedding}/config/OpenaiConfig.java | 2 +- .../embedding/model/EmbeddingModelType.java | 7 + .../model}/EmbeddingRequestText.java | 4 +- .../out/DrugDetailRepositoryAdapter.java | 41 ++++ .../out/DrugEmbedRepositoryAdapter.java | 112 ++++++++++ .../out/DrugImageRepositoryAdapter.java | 44 ++++ .../out/DrugRawDataRepositoryAdapter.java | 35 ++++ .../persistence/dto/DrugDetailRequest.java | 63 ++++++ .../entity/ApiDataDrugImgEntity.java | 12 +- .../repository/entity/DrugDetailEntity.java} | 6 +- .../repository/entity/DrugGptEmbedEntity.java | 25 +++ .../entity/DrugKmBertEmbedEntity.java | 25 +++ .../entity/DrugKrSbertEmbedEntity.java} | 14 +- .../repository/entity/DrugRawDataEntity.java} | 14 +- .../jdbc/GovDrugJdbcRepository.java | 10 +- .../repository/jdbc/JdbcBatchSetter.java | 8 +- .../repository/jdbc/MergeBatchSetter.java | 12 +- .../repository/jpa/ApiDataDrugImgRepo.java | 10 + .../jpa/GovDrugDetailJpaRepository.java | 21 ++ .../jpa/GovDrugGptEmbedJpaRepository.java | 10 + .../repository/jpa/GovDrugJpaRepository.java | 8 + .../jpa/GovDrugKmBertEmbedJpaRepository.java | 10 + .../jpa/GovDrugKrSbertEmbedJpaRepository.java | 10 + .../support/mapper/DrugDetailMapper.java | 30 +-- .../mapper/DrugDetailRequestMapper.java | 23 +++ .../support/mapper/DrugImageMapper.java | 30 +++ .../support/mapper/DrugRawDataMapper.java | 37 ++++ .../support/parser/MaterialParser.java | 59 ------ .../controller/DrugDataTestController.java | 19 +- .../controller/DrugDetailController.java | 19 +- .../controller/DrugImageController.java | 4 +- .../application/port/in/IndexUseCase.java | 7 + .../port/out/DrugIndexRepositoryPort.java | 9 + .../port/out/EmbeddingLoadingPort.java | 9 + .../application/port/out/EmbeddingPort.java | 5 + .../port/out/GovDrugRawDataPort.java | 11 + .../application/service/DrugIndexer.java | 107 ++++++++++ .../index/config/ElasticsearchConfig.java | 8 + .../yakplus/index/config/OpenAiConfig.java | 19 ++ .../yakplus/index/domain/model/Drug.java | 30 +++ .../index/exception/IndexException.java | 18 ++ .../index/exception/error/IndexErrorCode.java | 31 +++ .../adapter/persistence/DrugMapper.java | 46 +++++ .../persistence/ElasticsearchDrugAdapter.java | 115 +++++++++++ .../persistence/GovDrugRawDataAdapter.java | 122 +++++++++++ .../GptEmbeddingLoadingAdapter.java | 67 ++++++ .../KmBertEmbeddingLoadingAdapter.java | 65 ++++++ .../KrSBertEmbeddingLoadingAdapter.java | 65 ++++++ .../persistence/OpenAIEmbeddingAdapter.java | 75 +++++++ .../controller/DrugController.java | 38 ++++ .../controller/dto/request/IndexRequest.java | 13 ++ 102 files changed, 2728 insertions(+), 837 deletions(-) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraperImpl.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/support/parser => application/service/scraper/detail/support}/XMLParser.java (68%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => domain}/exception/ScraperException.java (87%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => domain}/exception/error/ScraperErrorCode.java (70%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/{GovDrugDetail.java => DrugDetail.java} (58%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{ => api}/config/ApiRestTemplateConfig.java (84%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{support/api => api/support}/ApiPageCounter.java (87%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{support/api => api/support}/ApiResponseMapper.java (89%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{support/api => api/support}/ApiUriCompBuilder.java (98%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/embedding/client/KmBertEmbeddingClient.java (77%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/embedding/client/KrSbertEmbeddingClient.java (77%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/embedding/client/OpenaiEmbeddingClient.java (87%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{ => embedding}/config/OpenaiConfig.java (84%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter/embedding/client => embedding/model}/EmbeddingRequestText.java (50%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/persistence/repository/entity/ApiDataDrugImgEntity.java (56%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter/persistence/repository/entity/GovDrugDetailEntity.java => persistence/repository/entity/DrugDetailEntity.java} (90%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter/persistence/repository/entity/GovDrugEmbedEntity.java => persistence/repository/entity/DrugKrSbertEmbedEntity.java} (55%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter/persistence/repository/entity/GovDrugEntity.java => persistence/repository/entity/DrugRawDataEntity.java} (76%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/persistence/repository/jdbc/GovDrugJdbcRepository.java (84%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/persistence/repository/jdbc/JdbcBatchSetter.java (77%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{adapter => }/persistence/repository/jdbc/MergeBatchSetter.java (67%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraper.java deleted file mode 100644 index 9ad4592..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service; - -public interface DrugApprovalDetailScraper { - void requestUpdateRawData(); - - void requestUpdateAllRawData(); - - void requestUpdateAllRawDataByJdbc(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraperImpl.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraperImpl.java deleted file mode 100644 index 63eb994..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugApprovalDetailScraperImpl.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc.GovDrugJdbcRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugDetailJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiResponseMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.parser.MaterialParser; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.parser.XMLParser; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; - - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -@Slf4j -@Component -@RequiredArgsConstructor -public class DrugApprovalDetailScraperImpl implements DrugApprovalDetailScraper { - private final ObjectMapper objectMapper; - private final RestTemplate restTemplate; - private final ApiUriCompBuilder apiUriCompBuilder; - private final GovDrugDetailJpaRepository govDrugDetailJpaRepository; - private final GovDrugJdbcRepository govDrugJdbcRepository; - - @Override - public void requestUpdateRawData() { - log.info("API 데이터 요청"); - String response = restTemplate.getForObject(apiUriCompBuilder.getUriForDetailApi(1), String.class); - log.debug("API Response: {}", response); - - JsonNode items = ApiResponseMapper.getItemsFromResponse(response); - List drugs = toListFromJson(items); - govDrugDetailJpaRepository.saveAllAndFlush(drugs); - } - - - @Override - public void requestUpdateAllRawData() { - int pageNo = 1; - int receivedCount = 0; - int savedCountWithoutDuplicates = 0; - - String response = fetchPage(pageNo); - int totalCount = ApiResponseMapper.getTotalCountFromResponse(response); - - while (hasMoreData(receivedCount, totalCount)) { - JsonNode items = ApiResponseMapper.getItemsFromResponse(response); - List drugs = toListFromJson(items); - receivedCount += drugs.size(); - - // item_seq 기준 중복 제거된 약품 개수 유지 (실제 db에 저장된 데이터와 같은 지 비교용) - int uniqueItems = deduplicateByItemSeq(drugs); - savedCountWithoutDuplicates += uniqueItems; - - govDrugDetailJpaRepository.saveAllAndFlush(drugs); - - log.info("Page {}, received: {}, saved (unique): {}, totalReceived: {}, totalUniqueSaved: {}", - pageNo, drugs.size(), uniqueItems, receivedCount, savedCountWithoutDuplicates); - - response = fetchPage(++pageNo); - } - - } - - @Override - public void requestUpdateAllRawDataByJdbc() { - int pageNo = 1; - int receivedCount = 0; - int savedCountWithoutDuplicates = 0; - - String response = fetchPage(pageNo); - int totalCount = ApiResponseMapper.getTotalCountFromResponse(response); - - while (hasMoreData(receivedCount, totalCount)) { - JsonNode items = ApiResponseMapper.getItemsFromResponse(response); - List drugs = toListFromJson(items); - receivedCount += drugs.size(); - - // item_seq 기준 중복 제거된 약품 개수 유지 (실제 db에 저장된 데이터와 같은 지 비교용) - int uniqueItems = deduplicateByItemSeq(drugs); - savedCountWithoutDuplicates += uniqueItems; - - govDrugJdbcRepository.saveAll(drugs); - - log.info("Page {}, received: {}, saved (unique): {}, totalReceived: {}, totalUniqueSaved: {}", - pageNo, drugs.size(), uniqueItems, receivedCount, savedCountWithoutDuplicates); - - response = fetchPage(++pageNo); - } - } - - private List toListFromJson(JsonNode items) { - - log.info("items 약품 객체로 맵핑"); - try { - List apiDataDrugDetails = toApiDetails(items); - for (int i = 0; i < apiDataDrugDetails.size(); i++) { - GovDrugDetailEntity drugDetail = apiDataDrugDetails.get(i); - JsonNode item = items.get(i); - log.debug("item seq: " + item.get("ITEM_SEQ").asText()); - - String materialRawData = item.get("MATERIAL_NAME").asText(); - String materialInfo = MaterialParser.parseMaterial(materialRawData); - drugDetail.changeMaterialInfo(materialInfo); - - String efficacyXmlText = item.get("EE_DOC_DATA").asText(); - String efficacy = XMLParser.toJson(efficacyXmlText); - drugDetail.changeEfficacy(efficacy); - - String usageXmlText = items.get(i).get("UD_DOC_DATA").asText(); - String usages = XMLParser.toJson(usageXmlText); - drugDetail.changeUsage(usages); - - String precautionxmlText = items.get(i).get("NB_DOC_DATA").asText(); - String precautions = XMLParser.toJson(precautionxmlText); - drugDetail.changePrecaution(precautions); - } - return apiDataDrugDetails; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private List toApiDetails(JsonNode items) { - try { - return objectMapper.readValue(items.toString(), - new TypeReference>() { - }); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - // private JsonNode toJsonFromXml(String usageXmlText) throws JsonProcessingException { - // XmlMapper xmlMapper = new XmlMapper(); - // - // JsonNode jsonNode = xmlMapper.readTree(usageXmlText) - // .path("SECTION") - // .path("ARTICLE"); - // return jsonNode; - // } - - // TODO: 추후 삭제 예정 - // private String replaceText(String text){ - // return text.replace("ᆞ ", "&") - // .replace("• ","") - // .replace("〜 ", "~"); - // } - - private int deduplicateByItemSeq(List drugs) { - // itemseq 기준으로 set에 저장 --> set은 중복 허용하지 않으므로 item seq 다 넣으면 알아서 중복 없이 저장됨 - Set uniqueItems = new HashSet<>(); - - for (GovDrugDetailEntity drug : drugs) { - uniqueItems.add(drug.getDrugId()); - } - return uniqueItems.size(); - } - - private String fetchPage(int pageNo) { - return restTemplate.getForObject(apiUriCompBuilder.getUriForDetailApi(pageNo), String.class); - } - - private boolean hasMoreData(int receivedCount, int totalCount) { - return receivedCount < totalCount; - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java deleted file mode 100644 index 7db9215..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service; - -import java.util.List; - -import org.springframework.data.domain.Pageable; - -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; - -public interface DrugDataService { - List findAllRawDrug(Pageable pageable); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java deleted file mode 100644 index a04402c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service; - -import static java.util.stream.Collectors.*; - -import java.util.List; - -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailMapper; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RequiredArgsConstructor -@Slf4j -@Service -public class DrugDataServiceImpl implements DrugDataService { - private final GovDrugJpaRepository govDrugJpaRepository; - - @Override - public List findAllRawDrug(Pageable pageable) { - log.info("findAllRawDrug called"); - //TODO: Detail을 전처리에서 미리 텍스트를 plain으로 만들고 통합 테이블 가져오는 것 다시 제작 필요 - return null; - // return govDrugJpaRepository.findAll(pageable).stream() - // .map(DrugDetailMapper::toDomainFromEntity) - // .collect(toList()); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java deleted file mode 100644 index ea8dba7..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugEmbedProcessor.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrugDetail; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingAdapter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc.GovDrugJdbcRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.ApiDataDrugImgRepo; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugDetailJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailMapper; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class DrugEmbedProcessor { - private final EmbeddingAdapter adapter; - private final GovDrugDetailJpaRepository govDrugDetailJpaRepository; - private final EmbeddingAdapter embeddingAdapter; - private final ApiDataDrugImgRepo apiDataDrugImgRepo; - private final GovDrugJpaRepository govDrugJpaRepository; - private final GovDrugJdbcRepository govDrugJdbcRepository; - - public void startEmbedding(){ - List allItem = getAllItem(); - List drugEntitys = new ArrayList<>(); - for (GovDrugDetailEntity detailEntity : allItem) { - GovDrugDetail govDrugDetail = DrugDetailMapper.toDomainFromEntity(detailEntity); - - String text = convertSingleStringForEfficacy(govDrugDetail.getEfficacy()); - - float[] sbertVector = embeddingAdapter.getEmbedding( - text, - EmbeddingModelType.SBERT); - - float[] kmbertVector = embeddingAdapter.getEmbedding( - text, - EmbeddingModelType.KM_BERT); - - float[] openAIVector = embeddingAdapter.getEmbedding( - text, - EmbeddingModelType.OPENAI); - - GovDrugEntity govDrugEntity = bulidGovDrugEntity( - govDrugDetail , openAIVector, kmbertVector, sbertVector - ); - drugEntitys.add(govDrugEntity); - } - saveEntitys(drugEntitys); - - - } - private void saveEntitys(List entitys){ - govDrugJpaRepository.saveAll(entitys); - govDrugJpaRepository.flush(); - entitys.clear(); - } - - private GovDrugEntity bulidGovDrugEntity(GovDrugDetail govDrugDetail, float[] openAIVector, float[] kmbertVector, - float[] sbertVector) { - return GovDrugEntity.builder() - .drugId(govDrugDetail.getDrugId()) - .drugName(govDrugDetail.getDrugName()) - .company(govDrugDetail.getCompany()) - .permitDate(govDrugDetail.getPermitDate()) - .isGeneral(govDrugDetail.isGeneral()) - .materialInfo(govDrugDetail.getMaterialInfo().toString()) - .storeMethod(govDrugDetail.getStoreMethod()) - .validTerm(govDrugDetail.getValidTerm()) - .efficacy(govDrugDetail.getEfficacy().toString()) - .usage(govDrugDetail.getUsage().toString()) - .precaution(govDrugDetail.getPrecaution().toString()) - .imageUrl(getImageUrl(govDrugDetail.getDrugId())) - .gptVector(toStringFromFloatArray(openAIVector)) - .kmBertVector(toStringFromFloatArray(kmbertVector)) - .sbertVector(toStringFromFloatArray(sbertVector)) - .build(); - } - - private String getImageUrl(Long drugId) { - ApiDataDrugImgEntity drugImgEntity = apiDataDrugImgRepo.findById(drugId) - .orElseGet(() -> new ApiDataDrugImgEntity()); - return drugImgEntity.getImgUrl(); - } - private String toStringFromFloatArray(float[] openAIVector) { - try { - return new ObjectMapper().writeValueAsString(openAIVector); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return null; - } - } - - private String convertSingleStringForEfficacy(List stringList) { - StringBuilder stringBuilder = new StringBuilder(); - for (String s : stringList) { - stringBuilder.append(s); - stringBuilder.append(" "); - } - return stringBuilder.toString(); - } - - private List getAllItem() { - return govDrugDetailJpaRepository.findAll(); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java deleted file mode 100644 index 9aabca5..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service; - -import java.net.URI; -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiPageCounter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiResponseMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.ApiDataDrugImgRepo; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@Slf4j -@RequiredArgsConstructor -public class DrugImageGovScraper { - private final ApiUriCompBuilder uriCompBuilder; - private final RestTemplate restTemplate; - private final ApiDataDrugImgRepo imgRepo; - private final ObjectMapper objectMapper; - private final ApiPageCounter apiPageCounter; - - @Transactional - public void getApiData(){ - log.info("의약품 개요 정보 API 호출 시작"); - - URI uriForImgApi = uriCompBuilder.getUriForImgApi(1); - - String response = restTemplate.getForObject(uriForImgApi, String.class); - JsonNode items = ApiResponseMapper.getItemsFromResponse(response); - List imgDatas = null; - try { - imgDatas = objectMapper.readValue(items.toString(), - new TypeReference>() {}); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - imgRepo.saveAllAndFlush(imgDatas); - } - - public void getAllApiData(){ - log.info("의약품 개요 정보 API 호출 시작"); - int totalPageCount = apiPageCounter.getImgApiTotalPageCount(); - for(int i=1;i<=totalPageCount;i++){ - URI uriForImgApi = uriCompBuilder.getUriForImgApi(i); - String response = restTemplate.getForObject(uriForImgApi, String.class); - JsonNode items = ApiResponseMapper.getItemsFromResponse(response); - List imgDatas = null; - try { - imgDatas = objectMapper.readValue(items.toString(), - new TypeReference>() {}); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - imgRepo.saveAllAndFlush(imgDatas); - } - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java new file mode 100644 index 0000000..fbb3dca --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java @@ -0,0 +1,15 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in; + +import jakarta.transaction.Transactional; + +public interface DrugCombineUsecase { + + /** + * API 요청으로 받아온 RAW 데이터 테이블 2개를 병합해 + * 1개의 테이블로 만드는 기능을 수행합니다. + * + * @since 2025-04-21 + */ + @Transactional + void mergeTable(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java new file mode 100644 index 0000000..a012c48 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in; +/** + * 의약품 상세 정보를 수집하는 유스케이스 인터페이스입니다. + * 1페이지 요청 및 전체 데이터 요청을 정의합니다. + * + * @since 2025-04-21 + */ +public interface DrugDetailScraperUsecase { + /** + * 특정 페이지의 의약품 상세 정보를 요청하여 수집합니다. + * + * @since 2025-04-21 + */ + void requestSingleData(int pageNumber); + + /** + * 모든 의약품의 상세 정보를 일괄적으로 요청하여 수집합니다. + * + * @since 2025-04-21 + */ + void requestAllData(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java new file mode 100644 index 0000000..53ac43a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java @@ -0,0 +1,16 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in; + +/** + * 의약품 효능 정보를 임베딩 처리를 위한 유스케이스 인터페이스입니다. + * + * @since 2025-04-25 + */ +public interface DrugEmbedProcessorUseCase { + + /** + * 의약품 데이터를 기반으로 임베딩 프로세스를 시작합니다. + * + * @since 2025-04-25 + */ + void startEmbedding(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java new file mode 100644 index 0000000..2d33d8f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java @@ -0,0 +1,23 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in; + +/** + * 의약품 이미지 정보를 수집하는 유스케이스 인터페이스입니다. + * API를 통해 단일 페이지 또는 전체 이미지 데이터를 수집하는 기능을 정의합니다. + */ +public interface DrugImageScraperUsecase { + + /** + * 주어진 페이지 번호에 해당하는 의약품 이미지 데이터를 외부 API로부터 수집하여 저장합니다. + * + * @param pageNumber 수집할 페이지 번호 + * @since 2025-04-21 + */ + void getApiData(int pageNumber); + + /** + * 모든 의약품 이미지 정보를 외부 API를 통해 일괄 수집합니다. + * + * @since 2025-04-21 + */ + void getAllApiData(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java new file mode 100644 index 0000000..17fa370 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java @@ -0,0 +1,13 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in; + +public interface DrugScraperUsecase { + + /** + * 의약품 데이터 수집 및 임베딩 프로세스를 순차적으로 실행합니다. + * 1. 상세 정보 수집 후 RDB 테이블 저장 + * 2. 이미지 정보 수집 후 RDB 테이블 저장 + * 3. 상세 정보와 이미지 병합 후 통합 테이블 저장 + * 4. 임베딩 벡터 생성 및 각각 벡터 테이블 저장 + */ + void scraperStart(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java new file mode 100644 index 0000000..b1f978d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java @@ -0,0 +1,48 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * 외부 API로부터 의약품 상세 정보 및 이미지 데이터를 요청하는 포트 인터페이스입니다. + * 페이징 처리된 데이터를 수집하고, 전체 페이지 수를 조회하는 기능을 제공합니다. + */ +public interface ApiRequestPort { + + /** + * 주어진 페이지 번호에 해당하는 의약품 상세 정보를 외부 API로부터 조회합니다. + * + * @param pageNo 조회할 페이지 번호 + * @return 해당 페이지의 의약품 상세 정보를 포함하는 JsonNode + * + * @since 2025-04-21 + */ + JsonNode getAllDetailData(int pageNo); + + /** + * 주어진 페이지 번호에 해당하는 의약품 이미지 정보를 외부 API로부터 조회합니다. + * + * @param pageNo 조회할 페이지 번호 + * @return 해당 페이지의 의약품 이미지 정보를 포함하는 JsonNode + * + * @since 2025-04-21 + */ + JsonNode getAllImageData(int pageNo); + + /** + * 의약품 상세 정보의 전체 페이지 수를 외부 API로부터 조회합니다. + * + * @return 상세 정보의 총 페이지 수 + * + * @since 2025-04-21 + */ + int getDetailTotalPageCount(); + + /** + * 의약품 이미지 정보의 전체 페이지 수를 외부 API로부터 조회합니다. + * + * @return 이미지 정보의 총 페이지 수 + * + * @since 2025-04-21 + */ + int getImageTotalPageCount(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java new file mode 100644 index 0000000..438dc8e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; + +/** + * 의약품 상세 정보를 저장 및 조회하는 리포지토리 포트 인터페이스입니다. + * 개별 및 대량 저장 기능과 저장된 전체 데이터를 조회하는 기능을 제공합니다. + * + * @since 2025-04-21 + */ +public interface DrugDetailRepositoryPort { + /** + * 단일 의약품 상세 정보를 저장합니다. + * + * @param e 저장할 의약품 상세 정보 저장 요청 객체 + * + * @since 2025-04-21 + */ + void saveDrugDetail(DrugDetailRequest e); + + /** + * 의약품 상세 정보를 일괄 저장합니다. + * + * @param list 저장할 의약품 상세 정보 저장 요청 객체 리스트 + * + * @since 2025-04-21 + */ + void saveDrugDetailBulk(List list); + + /** + * 저장된 모든 의약품 상세 정보를 조회합니다. + * + * @return 의약품 상세 정보 리스트 + * + * @since 2025-04-21 + */ + List getAllGovDrugDetail(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java new file mode 100644 index 0000000..9c0be8d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java @@ -0,0 +1,40 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +/** + * 의약품 임베딩 벡터를 저장 및 조회하는 리포지토리 포트 인터페이스입니다. + * 다양한 임베딩 모델(GPT, KmBERT, KorSBERT)의 벡터를 저장하고 조회하는 기능을 제공합니다. + * + * @since 2025-04-21 + */ +public interface DrugEmbedRepositoryPort { + + /** + * GPT 기반 임베딩 벡터를 저장합니다. + * + * @param drugId 임베딩할 의약품 ID + * @param vector GPT 임베딩 벡터 + * + * @since 2025-04-21 + */ + void saveGptEmbed(Long drugId, float[] vector); + + /** + * KmBERT 기반 임베딩 벡터를 저장합니다. + * + * @param drugId 임베딩할 의약품 ID + * @param gptVector KmBERT 임베딩 벡터 + * + * @since 2025-04-21 + */ + void saveKmBertEmbed(Long drugId, float[] gptVector); + + /** + * KrSBERT 기반 임베딩 벡터를 저장합니다. + * + * @param drugId 임베딩할 의약품 ID + * @param krSbertVector KorSBERT 임베딩 벡터 + * + * @since 2025-04-21 + */ + void saveKrSbertEmbed(Long drugId, float[] krSbertVector); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java new file mode 100644 index 0000000..33e121c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java @@ -0,0 +1,40 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; +/** + * 의약품 이미지 정보를 저장 및 조회하는 리포지토리 포트 인터페이스입니다. + * 이미지 데이터의 조회 및 일괄 저장 기능을 제공합니다. + * + * @since 2025-04-21 + */ +public interface DrugImageRepositoryPort { + /** + * 저장된 모든 의약품 이미지 정보를 조회합니다. + * + * @return List 의약품 이미지 정보 리스트 + * + * @since 2025-04-21 + */ + List getAllGovDrugImage(); + + /** + * 의약품 ID로 단일 의약품 이미지 정보를 조회합니다. + * + * @param drugId 조회할 의약품 ID + * @return 해당 의약품의 이미지 정보 + * + * @since 2025-04-21 + */ + DrugImage getById(Long drugId); + + /** + * 의약품 이미지 정보를 일괄 저장하고 즉시 반영(flush)합니다. + * + * @param imgData 저장할 의약품 이미지 정보 리스트 + * + * @since 2025-04-21 + */ + void saveAllAndFlush(List imgData); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java new file mode 100644 index 0000000..ff9d5ca --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; +/** + * 의약품 원시 데이터를 저장하는 리포지토리 포트 인터페이스입니다. + * + * @since 2025-04-21 + */ +public interface DrugRawDataRepositoryPort { + /** + * 의약품 원시 데이터를 저장합니다. + * + * @param rawData 저장할 의약품 원시 데이터 + * + * @since 2025-04-21 + */ + void save(DrugRawData rawData); + + + /** + * 여러 의약품 원시 데이터를 일괄 저장합니다. + * + * @param rawData 저장할 의약품 원시 데이터 리스트 + * + * @since 2025-04-21 + */ + void saveAll(List rawData); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java new file mode 100644 index 0000000..52e1723 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; + +/** + * 텍스트 임베딩을 위한 포트 인터페이스입니다. + * 다양한 임베딩 모델을 사용하여 텍스트를 벡터(float 배열)로 변환합니다. + * + * @since 2025-04-25 + */ +public interface EmbeddingPort { + + /** + * 주어진 텍스트를 지정한 임베딩 모델을 사용하여 임베딩 벡터(float 배열)로 변환합니다. + * + * @param text 임베딩할 텍스트 + * @param modelType 사용할 임베딩 모델 타입 (enum) + * @return 임베딩된 벡터 (float 배열) + * + * @author 이해창 + * @since 2025-04-25 + */ + float[] getEmbedding(String text, EmbeddingModelType modelType); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java index 8d7ff20..e3a6b65 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java @@ -3,26 +3,38 @@ import org.springframework.stereotype.Service; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugApprovalDetailScraper; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugEmbedProcessor; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugImageGovScraper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugDetailJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugDetailScraperUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugImageScraperUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugScraperUsecase; import lombok.RequiredArgsConstructor; +/** + * @class DrugScraper + * @description + * 약품 관련 데이터 수집을 담당하는 서비스 클래스 + * 1) 약품 상세 정보 + * 2) 약품 이미지 + * 위 항목을 순차적으로 수집하고 전처리 작업을 거쳐 하나의 테이블로 저장한다. + * 약품 효능 값을 임베딩하여 각 임베딩 모델 별로 저장한다. + * + * @since 2025-04-21 + */ @Service @RequiredArgsConstructor -public class DrugScraper { - private final DrugImageGovScraper drugImageGovScraper; - private final DrugEmbedProcessor embedProcessor; - private final DrugApprovalDetailScraper drugApprovalDetailScraper; - private final DrugEmbedProcessor drugEmbedProcessor; - private final GovDrugJpaRepository govDrugJpaRepository; +public class DrugScraper implements DrugScraperUsecase{ + private final DrugImageScraperUsecase drugImageScraperUsecase; + private final DrugDetailScraperUsecase drugDetailScraperUsecase; + private final DrugCombineUsecase drugCombineUsecase; + private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; + @Override public void scraperStart(){ - drugApprovalDetailScraper.requestUpdateAllRawDataByJdbc(); - drugImageGovScraper.getAllApiData(); - drugEmbedProcessor.startEmbedding(); + drugDetailScraperUsecase.requestAllData(); + drugImageScraperUsecase.getAllApiData(); + drugCombineUsecase.mergeTable(); + drugEmbedProcessorUseCase.startEmbedding(); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java new file mode 100644 index 0000000..f1191f3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java @@ -0,0 +1,88 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.combiner; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugImageRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugRawDataRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세 정보와 이미지 정보를 결합하여 원시 데이터를 생성하는 컴포넌트입니다. + * {@link DrugCombineUsecase}를 구현하며, 데이터 병합 및 저장 기능을 제공합니다. + * + * @since 2025-04-21 + */ +@Component +@RequiredArgsConstructor +public class DrugCombiner implements DrugCombineUsecase { + private final DrugDetailRepositoryPort drugDetailRepository; + private final DrugImageRepositoryPort drugImageRepositoryPort; + private final DrugRawDataRepositoryPort drugRawDataRepositoryPort; + + @Transactional + @Override + public void mergeTable(){ + log("API 요청 결과 테이블 병합 시작: 상세 정보 + 이미지"); + + List drugDetails = drugDetailRepository.getAllGovDrugDetail(); + log(LogLevel.DEBUG, "DrugDetail Raw Data: \n" + drugDetails); + + drugDetails.stream() + .map(detail -> buildMergeRawData(detail, getImageDataByDrugDetail(detail))) + .forEach(drugRawDataRepositoryPort::save); + log("API 요청 결과 테이블 병합 완료"); + } + + /** + * 주어진 의약품 상세 정보에 해당하는 이미지 정보를 조회합니다. + * + * @param detail 의약품 상세 정보 + * @return 해당 의약품의 이미지 정보 + * + * @since 2025-04-21 + */ + private DrugImage getImageDataByDrugDetail(DrugDetail detail) { + log(LogLevel.DEBUG, "의약품 이미지 정보 요청 \n detail: " + detail); + DrugImage imageData = drugImageRepositoryPort.getById(detail.getDrugId()); + log(LogLevel.DEBUG, "의약품 이미지 정보 (상세 정보로 Search): " + imageData); + return imageData; + } + + /** + * 의약품 상세 정보와 이미지 정보를 병합하여 {@link DrugRawData} 객체를 생성합니다. + * + * @param d 의약품 상세 정보 + * @param i 의약품 이미지 정보 + * @return 병합된 의약품 원시 데이터 + * + * @since 2025-04-21 + */ + private DrugRawData buildMergeRawData(DrugDetail d, DrugImage i) { + DrugRawData rawData = DrugRawData.builder() + .drugId(d.getDrugId()) + .drugName(d.getDrugName()) + .company(d.getCompany()) + .permitDate(d.getPermitDate()) + .isGeneral(d.isGeneral()) + .materialInfo(d.getMaterialInfo()) + .storeMethod(d.getStoreMethod()) + .validTerm(d.getValidTerm()) + .efficacy(d.getEfficacy()) + .usage(d.getUsage()) + .precaution(d.getPrecaution()) + .imageUrl(i.getImageUrl()) + .build(); + return rawData; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java new file mode 100644 index 0000000..124d698 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java @@ -0,0 +1,192 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugDetailScraperUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.ApiRequestPort; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support.MaterialParser; +import com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support.XMLParser; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세 정보를 외부 API로부터 수집하여 저장하는 서비스 클래스입니다. + * {@link DrugDetailScraperUsecase}를 구현하며, + * 단일 페이지 또는 전체 데이터를 처리할 수 있습니다. + * + * @since 2025-04-21 + */ + +@Component +@RequiredArgsConstructor +public class DrugDetailScraperService implements DrugDetailScraperUsecase { + private final ApiRequestPort apiRequestPort; + private final ObjectMapper objectMapper; + private final DrugDetailRepositoryPort drugDetailRepository; + + @Override + public void requestSingleData(int pageNumber) { + JsonNode items = apiRequestPort.getAllDetailData(pageNumber); + + log("API 응답 처리 시작 - Drug Detail"); + List drugs = toListFromJson(items); + log("API 응답 처리 완료 - Drug Detail"); + log(LogLevel.DEBUG, "완료 데이터 : \n" + drugs); + + drugDetailRepository.saveDrugDetailBulk(drugs); + } + + @Override + public void requestAllData() { + int totalPageCount = apiRequestPort.getDetailTotalPageCount(); + int receivedCount = 0; + int savedCountWithoutDuplicates = 0; + + log("전체 API 데이터 수집 시작 - Drug Detail"); + + for (int i = 1; i <= totalPageCount; i++) { + JsonNode items = apiRequestPort.getAllDetailData(i); + List drugs = toListFromJson(items); + receivedCount += drugs.size(); + + // item_seq 기준 중복 제거된 약품 개수 유지 (실제 db에 저장된 데이터와 같은 지 비교용) + int uniqueItems = deduplicateByItemSeq(drugs); + savedCountWithoutDuplicates += uniqueItems; + + drugDetailRepository.saveDrugDetailBulk(drugs); + + log(LogLevel.DEBUG, + "Page: " + i + + "received: " + drugs.size() + + "saved (unique): " + uniqueItems + + "totalReceived: " + receivedCount + + "totalUniqueSaved: " + savedCountWithoutDuplicates + ); + } + } + + /** + * 외부 API로부터 받은 JSON 데이터를 {@link DrugDetailRequest} 리스트로 변환하고, + * 각 항목의 상세 정보를 가공하여 반환합니다. + * + * @param items API에서 수신한 JSON 노드 + * @return {@link DrugDetailRequest} 리스트 + * + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private List toListFromJson(JsonNode items) { + + log("API 응답 > DrugDetailRequest 객체 변환 시작"); + try { + List apiDataDrugDetails = changeTypeToList(items); + for (int i = 0; i < apiDataDrugDetails.size(); i++) { + DrugDetailRequest drugDetail = apiDataDrugDetails.get(i); + JsonNode item = items.get(i); + log(LogLevel.DEBUG, "item seq: " + item.get("ITEM_SEQ").asText()); + + log(LogLevel.DEBUG, "약품 성분 파싱 시작"); + String materialRawData = item.get("MATERIAL_NAME").asText(); + log(LogLevel.DEBUG, "약품 성분 Raw 데이터 조회 성공: \n" + materialRawData); + + String materialInfo = MaterialParser.parseMaterial(materialRawData); + log(LogLevel.DEBUG, "약품 성분 파싱 성공: \n" + materialInfo); + + drugDetail.changeMaterialInfo(materialInfo); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 성분 저장 완료: \n" + drugDetail); + + log(LogLevel.DEBUG, "약품 효능 데이터 파싱 시작"); + String efficacyXmlText = item.get("EE_DOC_DATA").asText(); + log(LogLevel.DEBUG, "약품 효능 Raw 데이터 조회 성공: \n" + efficacyXmlText); + + String efficacy = XMLParser.toJson(efficacyXmlText); + log(LogLevel.DEBUG, "약품 효능 파싱 성공: \n" + efficacy); + + drugDetail.changeEfficacy(efficacy); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 효능 저장 완료: \n" + drugDetail); + + log(LogLevel.DEBUG, "약품 사용법 데이터 파싱 시작"); + String usageXmlText = items.get(i).get("UD_DOC_DATA").asText(); + log(LogLevel.DEBUG, "약품 사용법 Raw 데이터 조회 성공: \n" + usageXmlText); + + String usages = XMLParser.toJson(usageXmlText); + log(LogLevel.DEBUG, "약품 사용법 파싱 성공: \n" + usages); + + drugDetail.changeUsage(usages); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 사용법 저장 완료: \n" + drugDetail); + + log(LogLevel.DEBUG, "약품 주의사항 데이터 파싱 시작"); + String precautionxmlText = items.get(i).get("NB_DOC_DATA").asText(); + log(LogLevel.DEBUG, "약품 주의사항 Raw 데이터 조회 성공: \n" + precautionxmlText); + + String precautions = XMLParser.toJson(precautionxmlText); + log(LogLevel.DEBUG, "약품 주의사항 파싱 성공: \n" + precautions); + + drugDetail.changePrecaution(precautions); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 주의사항 저장 완료: \n" + drugDetail); + } + return apiDataDrugDetails; + } catch (Exception e) { + throw new ScraperException(ScraperErrorCode.API_DRUG_DETAIL_PARSING_FAIL); + } + } + + /** + * JSON 노드를 {@link DrugDetailRequest} 리스트로 변환합니다. + * + * @param items 변환할 JSON 노드 + * @return {@link DrugDetailRequest} 리스트 + * + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private List changeTypeToList(JsonNode items) { + try { + return objectMapper.readValue(items.toString(), + new TypeReference>() { + }); + } catch (JsonProcessingException e) { + throw new ScraperException(ScraperErrorCode.RESPONSE_TYPE_CHANGE_FAIL); + } + } + // TODO: 추후 삭제 예정 + // private String replaceText(String text){ + // return text.replace("ᆞ ", "&") + // .replace("• ","") + // .replace("〜 ", "~"); + // } + + /** + * 의약품 상세 정보 리스트에서 중복되지 않는 항목 수를 계산합니다. + * 중복 기준은 item_seq (drugId)입니다. + * + * @param drugs 중복 제거 대상 {@link DrugDetailRequest} 리스트 + * @return 중복 제거 후 고유 항목 수 + * + * @author 이해창 + * @since 2025-04-21 + */ + private int deduplicateByItemSeq(List drugs) { + Set uniqueItems = new HashSet<>(); + + for (DrugDetailRequest drug : drugs) { + uniqueItems.add(drug.getDrugId()); + } + return uniqueItems.size(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java new file mode 100644 index 0000000..8583b5a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java @@ -0,0 +1,112 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; + +/** + * 원재료 정보를 파싱하여 JSON 배열 형식의 문자열로 변환하는 유틸리티 클래스입니다. + */ +public class MaterialParser { + + /** + * 원재료 문자열을 파싱하여 JSON 배열 형태의 문자열로 변환합니다. + * + * @param raw 원재료 정보가 담긴 문자열 + * (세미콜론으로 블록 구분, 파이프로 키-값 쌍 구분) + * @return JSON 배열 형태의 문자열 + * + * @author 함예정 + * @since 2025-04-21 + */ + public static String parseMaterial(String raw) { + log("약품 성분 파싱 시작"); + ObjectMapper objectMapper = new ObjectMapper(); + ArrayNode resultArray = objectMapper.createArrayNode(); + String[] blocks = splitBlock(raw); + parsingblocksAndPutArrayItem(blocks, resultArray); + String result = convertString(objectMapper, resultArray); + log("약품 성분 파싱 완료"); + return result; + } + + /** + * 블록 배열을 파싱하여 JSON 배열에 항목으로 추가합니다. + * + * @param blocks 원재료 블록 배열 + * @param resultArray 결과를 저장할 JSON 배열 + */ + private static void parsingblocksAndPutArrayItem(String[] blocks, ArrayNode resultArray) { + for (String block : blocks) { + block = block.trim(); + if (block.isEmpty()) { + continue; + } + String[] pairs = splitByPipe(block); + ObjectNode item = makeItem(pairs); + resultArray.add(item); + } + } + + /** + * JSON 배열을 문자열로 변환합니다. + * + * @param objectMapper Jackson ObjectMapper 인스턴스 + * @param arrayNode 변환할 JSON 배열 + * @return JSON 문자열 + * @throws ScraperException JSON 변환 실패 시 발생 + */ + private static String convertString(ObjectMapper objectMapper, ArrayNode arrayNode) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode); + } catch (JsonProcessingException e) { + //TODO String 변환실패 + throw new ScraperException(ScraperErrorCode.MATERIAL_PARSING_FAIL); + } + } + + /** + * 키-값 쌍 배열로부터 JSON 객체를 생성합니다. + * + * @param pairs 파이프로 구분된 키-값 쌍 배열 + * @return 생성된 JSON 객체 + */ + private static ObjectNode makeItem(String[] pairs) { + ObjectNode item = new ObjectMapper().createObjectNode(); + for (String pair : pairs) { + String[] kv = pair.split(":", 2); + String key = kv[0].trim(); + String value = ""; + if(kv.length == 2){ + value = kv[1].trim(); + } + item.put(key, value); + } + return item; + } + + /** + * 블록 내 키-값 쌍을 파이프(|) 기호로 분리합니다. + * + * @param block 블록 문자열 + * @return 키-값 쌍 배열 + */ + private static String[] splitByPipe(String block) { + + return block.split("\\|"); + } + + /** + * 원재료 정보를 세미콜론(;) 기준으로 블록으로 분리합니다. + * + * @param raw 원재료 문자열 + * @return 블록 배열 + */ + private static String[] splitBlock(String raw) { + return raw.split(";"); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java similarity index 68% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/XMLParser.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java index 8c7680b..040d7a9 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/XMLParser.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.parser; +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support; import java.io.IOException; import java.io.StringReader; @@ -18,8 +18,22 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +/** + * XML 문자열을 파싱하여 JSON 문자열로 변환하는 클래스입니다. + */ public class XMLParser { + private static final ObjectMapper mapper = new ObjectMapper(); + private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + /** + * XML 문자열을 파싱하여 JSON 문자열로 변환합니다. + * + * @param xml 변환할 XML 문자열 + * @return 변환된 JSON 문자열 + */ public static String toJson(String xml) { if(isXmlNull(xml)) { @@ -42,10 +56,14 @@ public static String toJson(String xml) { parseParagraph(root, allParagraphs, articleMap); return convertJson(docTag); } - private static final ObjectMapper mapper = new ObjectMapper(); - - private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + /** + * DocTag 객체를 JSON 문자열로 변환합니다. + * + * @param docTag 변환할 DocTag 객체 + * @return JSON 문자열 + * @throws RuntimeException JSON 변환 실패 시 + */ private static String convertJson(DocTag docTag) { try { return mapper.writeValueAsString(docTag); @@ -55,6 +73,13 @@ private static String convertJson(DocTag docTag) { } } + /** + * XML에서 PARAGRAPH 태그를 파싱하여 ParagraphTag 리스트에 추가합니다. + * + * @param root XML 루트 엘리먼트 + * @param allParagraphs 파싱된 ParagraphTag 리스트 + * @param articleMap ARTICLE 엘리먼트와 ArticleTag 매핑 정보 + */ private static void parseParagraph(Element root, List allParagraphs, Map articleMap) { NodeList paraNodes = root.getElementsByTagName("PARAGRAPH"); @@ -74,6 +99,14 @@ private static void parseParagraph(Element root, List allParagraph } + /** + * XML에서 ARTICLE 태그를 파싱하여 ArticleTag 리스트에 추가합니다. + * + * @param root XML 루트 엘리먼트 + * @param allArticles 파싱된 ArticleTag 리스트 + * @param articleMap ARTICLE 엘리먼트와 ArticleTag 매핑 정보 + * @param sectionMap SECTION 엘리먼트와 SectionTag 매핑 정보 + */ private static void parseArticles(Element root, List allArticles, Map articleMap, Map sectionMap) { @@ -93,6 +126,13 @@ private static void parseArticles(Element root, List allArticles, } + /** + * 상위 엘리먼트를 기반으로 해당 태그를 부모 태그에 연결합니다. + * + * @param map 상위 엘리먼트와 태그 매핑 정보 + * @param tags 현재 태그 + * @param element 현재 엘리먼트 + */ private static void mapSectionFromArticle(Map map, Tags tags, Element element) { Element parentElement = (Element) element.getParentNode(); Tags parentTag = map.get(parentElement); @@ -101,6 +141,13 @@ private static void mapSectionFromArticle(Map map, Tag } } + /** + * XML에서 SECTION 태그를 파싱하여 SectionTag 리스트에 추가합니다. + * + * @param root XML 루트 엘리먼트 + * @param allSections 파싱된 SectionTag 리스트 + * @param sectionMap SECTION 엘리먼트와 SectionTag 매핑 정보 + */ private static void parseSesctions(Element root, List allSections, Map sectionMap) { NodeList secNodes = root.getElementsByTagName("SECTION"); @@ -117,22 +164,27 @@ private static void parseSesctions(Element root, List allSections, M } } + /** + * XML 문자열을 파싱하여 Document 객체로 변환합니다. + * + * @param xml 파싱할 XML 문자열 + * @return 파싱된 Document 객체 + */ private static Document parseXmlString(String xml) { - //TODO: 예외처리 후 삭제 try { return documentBuilderFactory.newDocumentBuilder() .parse(new InputSource(new StringReader(xml))); - } catch (SAXException e) { - // System.out.println(xml); - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (ParserConfigurationException e) { - //TODO DocumentBulider 생성 실패 - throw new RuntimeException(e); + } catch (Exception e) { + throw new ScraperException(ScraperErrorCode.PARSING_ERROR); } } + /** + * XML 문자열이 null 이거나 비어있는지 확인합니다. + * + * @param xml 확인할 XML 문자열 + * @return null 또는 비어있으면 true, 그렇지 않으면 false + */ private static boolean isXmlNull(String xml) { if (xml == null || xml.trim().isEmpty() || xml == "null") { return true; @@ -141,6 +193,9 @@ private static boolean isXmlNull(String xml) { } } + /** + * XML 루트 태그를 표현하는 클래스입니다. + */ private static class DocTag implements Tags { public String title; public String type; @@ -163,6 +218,9 @@ public boolean equalsClass(Tags tags) { } } + /** + * SECTION 태그를 표현하는 클래스입니다. + */ private static class SectionTag implements Tags { public String title; public List articles; @@ -178,6 +236,9 @@ public boolean equalsClass(Tags tags) { } } + /** + * ARTICLE 태그를 표현하는 클래스입니다. + */ private static class ArticleTag implements Tags { public String title; public List paragraphs; @@ -193,7 +254,10 @@ public boolean equalsClass(Tags tags) { } } - public static class ParagraphTag implements Tags { + /** + * PARAGRAPH 태그를 표현하는 클래스입니다. + */ + private static class ParagraphTag implements Tags { public String tagName; public String textIndent; public String marginLeft; @@ -210,7 +274,10 @@ public boolean equalsClass(Tags tags) { } } - public static interface Tags { + /** + * 태그 클래스 간 공통 인터페이스입니다. + */ + private interface Tags { void addTag(Tags tags); boolean equalsClass(Tags tags); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java new file mode 100644 index 0000000..9fe2795 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java @@ -0,0 +1,121 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.embed; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugEmbedRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세 정보를 기반으로 다양한 임베딩 모델을 이용하여 + * 벡터를 생성하고 저장하는 프로세서 클래스입니다. + * + * {@link DrugEmbedProcessorUseCase}를 구현하며, GPT, KmBERT, KorSBERT 임베딩을 수행합니다. + * + * @since 2025-04-25 + */ +@Service +@RequiredArgsConstructor +public class DrugEmbedProcessor implements DrugEmbedProcessorUseCase { + private final DrugDetailRepositoryPort detailRepositoryPort; + private final EmbeddingPort embeddingPort; + private final DrugEmbedRepositoryPort embedRepositoryPort; + + @Override + public void startEmbedding() { + log("약품 효능 임베딩 작업 시작"); + + getAllItem().forEach(detail -> { + String efficacy = convertSingleStringForEfficacy(detail.getEfficacy()); + saveSbertVector(detail, efficacy); + saveKmBertVector(detail, efficacy); + saveGptVector(detail, efficacy); + }); + + log("약품 효능 임베딩 작업 완료"); + } + + /** + * GPT 모델을 사용하여 임베딩 벡터를 생성하고 저장합니다. + * + * @param detail 의약품 상세 정보 + * @param text 임베딩 대상 텍스트 + * + * @since 2025-04-25 + */ + private void saveGptVector(DrugDetail detail, String text) { + float[] openAIVector = embeddingPort.getEmbedding( + text, EmbeddingModelType.OPENAI); + embedRepositoryPort.saveGptEmbed(detail.getDrugId(), openAIVector); + } + + /** + * KmBERT 모델을 사용하여 임베딩 벡터를 생성하고 저장합니다. + * + * @param detail 의약품 상세 정보 + * @param text 임베딩 대상 텍스트 + * + * @since 2025-04-25 + */ + private void saveKmBertVector(DrugDetail detail, String text) { + float[] kmbertVector = embeddingPort.getEmbedding( + text, EmbeddingModelType.KM_BERT); + embedRepositoryPort.saveKmBertEmbed(detail.getDrugId(), kmbertVector); + } + + /** + * KrSBERT 모델을 사용하여 임베딩 벡터를 생성하고 저장합니다. + * + * @param detail 의약품 상세 정보 + * @param text 임베딩 대상 텍스트 + * + * @since 2025-04-25 + */ + private void saveSbertVector(DrugDetail detail, String text) { + float[] sbertVector = embeddingPort.getEmbedding( + text, EmbeddingModelType.SBERT); + embedRepositoryPort.saveKrSbertEmbed(detail.getDrugId(), sbertVector); + } + + /** + * 효능 정보를 단일 문자열로 변환합니다. + * + * @param stringList 효능 정보 리스트 + * @return 공백으로 결합된 단일 문자열 + * + * @since 2025-04-25 + */ + private String convertSingleStringForEfficacy(List stringList) { + log(LogLevel.DEBUG, "약품 효능 정보 단일 문자로 변환 시작"); + StringBuilder stringBuilder = new StringBuilder(); + for (String s : stringList) { + stringBuilder.append(s); + stringBuilder.append(" "); + } + + String s = stringBuilder.toString(); + log(LogLevel.DEBUG, "약품 효능 정보 단일 문자로 변환 완료" + s); + return s; + } + + /** + * 저장된 모든 의약품 상세 정보를 조회합니다. + * + * @return 의약품 상세 정보 리스트 + * + * @since 2025-04-25 + */ + private List getAllItem() { + return detailRepositoryPort.getAllGovDrugDetail(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java new file mode 100644 index 0000000..ec07fee --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java @@ -0,0 +1,72 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.image; + +import java.util.List; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugImageScraperUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.ApiRequestPort; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugImageRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; +import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; + +import lombok.RequiredArgsConstructor; + +/** + * 정부 API로부터 의약품 이미지 정보를 수집하여 저장하는 서비스 클래스입니다. + * {@link DrugImageScraperUsecase}를 구현하며, + * 단일 페이지 또는 전체 페이지의 이미지 데이터를 처리합니다. + */ +@Service +@RequiredArgsConstructor +public class DrugImageGovScraper implements DrugImageScraperUsecase { + private final ApiRequestPort apiRequestPort; + private final DrugImageRepositoryPort drugImageRepositoryPort; + private final ObjectMapper objectMapper; + + @Override + public void getApiData(int pageNumber){ + JsonNode items = apiRequestPort.getAllImageData(pageNumber); + List imgData = changeTypeToList(items); + drugImageRepositoryPort.saveAllAndFlush(imgData); + } + + @Override + public void getAllApiData(){ + int totalPageCount = apiRequestPort.getImageTotalPageCount(); + for(int i=1;i<=totalPageCount;i++){ + JsonNode items = apiRequestPort.getAllImageData(i); + List imgData = null; + try { + imgData = objectMapper.readValue(items.toString(), + new TypeReference>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + drugImageRepositoryPort.saveAllAndFlush(imgData); + } + } + + /** + * JsonNode 형태의 데이터를 {@link DrugImage} 리스트로 변환합니다. + * 변환 실패 시 {@link ScraperException}을 발생시킵니다. + * + * @param items 변환할 JSON 데이터 + * @return {@link DrugImage} 리스트 + */ + private List changeTypeToList(JsonNode items) { + List imgData; + try { + imgData = objectMapper.readValue(items.toString(), + new TypeReference>() {}); + } catch (JsonProcessingException e) { + throw new ScraperException(ScraperErrorCode.RESPONSE_TYPE_CHANGE_FAIL); + } + return imgData; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/ScraperException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/ScraperException.java similarity index 87% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/exception/ScraperException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/ScraperException.java index 7f47c4d..0e10278 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/ScraperException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/ScraperException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.exception; +package com.likelion.backendplus4.yakplus.drug.domain.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/error/ScraperErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/error/ScraperErrorCode.java similarity index 70% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/exception/error/ScraperErrorCode.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/error/ScraperErrorCode.java index 017d57d..32f627d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/error/ScraperErrorCode.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/error/ScraperErrorCode.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.exception.error; +package com.likelion.backendplus4.yakplus.drug.domain.exception.error; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; import lombok.RequiredArgsConstructor; @@ -11,6 +11,9 @@ public enum ScraperErrorCode implements ErrorCode { DB_ERROR_IMAGE_INFO(HttpStatus.INTERNAL_SERVER_ERROR, 300002, "이미지 정보를 조회하는데 실패했습니다."), DB_ERROR_COMBINED_INFO(HttpStatus.INTERNAL_SERVER_ERROR, 300003, "결합된 정보를 조회하는데 실패했습니다."), API_CONNECT_FAIL(HttpStatus.BAD_GATEWAY, 400001, "외부 API 연결에 실패했습니다."), + API_DRUG_DETAIL_PARSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 300004, "허가 정보 API에서 정보 파싱에 실패했습니다."), + MATERIAL_PARSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR,300006, "성분 파싱에 실패 했습니다"), + RESPONSE_TYPE_CHANGE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 300005, "API 응답을 객체 타입으로 변환하는데 실패했습니다."), PARSING_ERROR(HttpStatus.BAD_REQUEST, 400001, "데이터 파싱에 실패했습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java new file mode 100644 index 0000000..d9b143f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java @@ -0,0 +1,61 @@ +package com.likelion.backendplus4.yakplus.drug.domain.model; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +/** + * 의약품 정보를 담는 도메인 객체입니다. + */ + +//TODO swagger/ @ApiModel(description = "의약품 정보") +@Builder +@Getter +@ToString +public class Drug { + + //TODO @ApiModelProperty(value = "의약품 ID", example = "12345") + private Long drugId; + + //TODO @ApiModelProperty(value = "의약품명", example = "타이레놀") + private String drugName; + + //TODO @ApiModelProperty(value = "제조사명", example = "한국얀센") + private String company; + + //TODO @ApiModelProperty(value = "허가일자", example = "2023-01-01") + private LocalDate permitDate; + + //TODO @ApiModelProperty(value = "일반의약품 여부", example = "true") + private boolean isGeneral; + + //TODO @ApiModelProperty(value = "성분 정보 리스트") + private List materialInfo; + + //TODO @ApiModelProperty(value = "보관 방법", example = "밀폐용기, 실온 보관") + private String storeMethod; + + //TODO @ApiModelProperty(value = "유효 기간", example = "36개월") + private String validTerm; + + //TODO @ApiModelProperty(value = "효능 효과", example = "[\"해열\", \"진통\"]") + private List efficacy; + + //TODO @ApiModelProperty(value = "사용 방법", example = "[\"1일 3회\", \"식후 복용\"]") + private List usage; + + //TODO @ApiModelProperty(value = "주의 사항", example = "{\"주의사항\": [\"임산부 주의\", \"운전 금지\"]}") + private Map> precaution; + + //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") + private String imageUrl; + + //TODO @ApiModelProperty(value = "[float 배열]") + private float[] vector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java similarity index 58% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java index 420fc06..20b2854 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java @@ -4,17 +4,16 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; import lombok.Builder; import lombok.Getter; +import lombok.ToString; @Builder @Getter -public class GovDrugDetail { +@ToString +public class DrugDetail { private Long drugId; private String drugName; private String company; @@ -26,13 +25,4 @@ public class GovDrugDetail { private List efficacy; private List usage; private Map> precaution; - - public JsonNode toJson(String json) { - try { - return new ObjectMapper().readValue(json, JsonNode.class); - } catch (JsonProcessingException e) { - //TODO 에러 로그 처리 필요합니다. - throw new RuntimeException(e); - } - } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java new file mode 100644 index 0000000..e15031a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.domain.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +/** + * 의약품 이미지 정보를 담는 객체입니다. + */ + +@Builder +@Getter +@ToString +//TODO swagger / @ApiModel(description = "의약품 이미지 정보") +public class DrugImage { + @JsonProperty("ITEM_SEQ") + //TODO @ApiModelProperty(value = "의약품 ID", example = "12345") + private Long drugId; + + @JsonProperty("BIG_PRDT_IMG_URL") + //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") + private String imageUrl; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java new file mode 100644 index 0000000..6c8e472 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java @@ -0,0 +1,57 @@ +package com.likelion.backendplus4.yakplus.drug.domain.model; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +/** + * 외부 API로부터 수집된 의약품 원시 데이터를 담는 객체입니다. + */ + +//TODO swagger/ @ApiModel(description = "의약품 원시 데이터") +@Builder +@Getter +@ToString +public class DrugRawData { + //TODO @ApiModelProperty(value = "의약품 ID", example = "12345") + private Long drugId; + + //TODO @ApiModelProperty(value = "의약품명", example = "타이레놀") + private String drugName; + + //TODO @ApiModelProperty(value = "제조사명", example = "한국얀센") + private String company; + + //TODO @ApiModelProperty(value = "허가일자", example = "2023-01-01") + private LocalDate permitDate; + + //TODO @ApiModelProperty(value = "일반의약품 여부", example = "true") + private boolean isGeneral; + + //TODO @ApiModelProperty(value = "성분 정보 리스트") + private List materialInfo; + + //TODO @ApiModelProperty(value = "보관 방법", example = "실온 보관") + private String storeMethod; + + //TODO @ApiModelProperty(value = "유효 기간", example = "36개월") + private String validTerm; + + //TODO @ApiModelProperty(value = "효능 효과", example = "[\"해열\", \"진통\"]") + private List efficacy; + + //TODO @ApiModelProperty(value = "사용 방법", example = "[\"1일 3회\", \"식후 복용\"]") + private List usage; + + //TODO @ApiModelProperty(value = "주의 사항", example = "{\"주의사항\": [\"임산부 주의\", \"운전 금지\"]}") + private Map> precaution; + + //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") + private String imageUrl; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java deleted file mode 100644 index 9e828e3..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; - -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - -@Builder -@Getter -@ToString -public class GovDrug { - private Long drugId; - private String drugName; - private String company; - private LocalDate permitDate; - private boolean isGeneral; - private List materialInfo; - private String storeMethod; - private String validTerm; - private List efficacy; - private List usage; - private Map> precaution; - private String imageUrl; - private float[] gptVector; - private float[] sbertVector; - private float[] kmBertVector; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java deleted file mode 100644 index 2a2ff9a..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/port/out/EmbeddingPort.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model.port.out; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; - -public interface EmbeddingPort { - float[] getEmbedding(String text, EmbeddingModelType modelType); //문자 embedding 하여 float 배열로 반환 -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java index fab8c35..d116085 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java @@ -5,27 +5,38 @@ import lombok.Getter; import lombok.ToString; +/** + * 의약품 성분 정보를 나타내는 값 객체(Value Object)입니다. + */ +//TODO: swagger/ @ApiModel(description = "의약품 성분 정보") @Getter @ToString public class Material { + //TODO @ApiModelProperty(value = "성분명", example = "아세트아미노펜") @JsonProperty("성분명") private String name; + //TODO @ApiModelProperty(value = "분량", example = "500") @JsonProperty("분량") private String amount; + //TODO @ApiModelProperty(value = "단위", example = "mg") @JsonProperty("단위") private String unit; + //TODO @ApiModelProperty(value = "총량", example = "1000") @JsonProperty("총량") private String totalAmount; + //TODO @ApiModelProperty(value = "규격", example = "USP") @JsonProperty("규격") private String standard; + //TODO @ApiModelProperty(value = "비고", example = "해열진통제") @JsonProperty("비고") private String note; + //TODO @ApiModelProperty(value = "성분정보", example = "기타 부가 정보") @JsonProperty("성분정보") private String info; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java index 75f29df..3717ecf 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java @@ -2,7 +2,21 @@ import java.util.List; +import lombok.Getter; +import lombok.ToString; + +/** + * 의약품의 성분 총량 및 개별 성분 정보를 포함하는 객체입니다. + */ + +//TODO: swagger/ @ApiModel(description = "의약품 성분 정보 집합") +@Getter +@ToString public class MaterialInfo { + + //TODO @ApiModelProperty(value = "총량", example = "1000") private String totalAmount; + + //TODO @ApiModelProperty(value = "성분 리스트", example = "[{name: '아세트아미노펜', amount: '500', unit: '밀리그램'}]") private List ingredients; } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java deleted file mode 100644 index 65cf5f6..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingAdapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.embedding.Embedding; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiEmbeddingOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.stereotype.Service; - -import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client.EmbeddingClient; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client.OpenaiEmbeddingClient; - -import lombok.RequiredArgsConstructor; - -@Service -public class EmbeddingAdapter implements EmbeddingPort { - private final Map embeddingClientMap; - - public EmbeddingAdapter(List clients) { - this.embeddingClientMap = clients.stream() - .collect(Collectors.toMap(EmbeddingClient::getModelType, client -> client)); - } - @Override - public float[] getEmbedding(String text, EmbeddingModelType embeddingModelType) { - EmbeddingClient client = embeddingClientMap.get(embeddingModelType); - - return client.getEmbedding(text); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java deleted file mode 100644 index ffd7633..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/EmbeddingModelType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding; - -public enum EmbeddingModelType { - OPENAI, - SBERT, - KM_BERT -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java deleted file mode 100644 index 9e96d82..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingClient.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; - -public interface EmbeddingClient { - EmbeddingModelType getModelType(); - float[] getEmbedding(String text); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java deleted file mode 100644 index 12a8800..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity; - -@Repository -public interface ApiDataDrugImgRepo extends JpaRepository { -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java deleted file mode 100644 index 4ca4706..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; - -import jakarta.transaction.Transactional; - -@Repository -public interface GovDrugDetailJpaRepository extends JpaRepository { - - @Override - @Transactional - List saveAllAndFlush(Iterable entities); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java deleted file mode 100644 index f63ceca..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugEmbedJpaRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEmbedEntity; - -@Repository -public interface GovDrugEmbedJpaRepository extends JpaRepository { -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java deleted file mode 100644 index 39ed73b..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; - -import jakarta.transaction.Transactional; - -public interface GovDrugJpaRepository extends JpaRepository { - @Modifying - @Transactional - @Query(value = """ - INSERT INTO gov_drug_raw_data - ( - etc_otc_code, - item_permit_date, - item_seq, - ee_doc_data, - entp_name, - item_name, - material_name, - nb_doc_data, - storage_method, - ud_doc_data, - valid_term, - img_url, - gpt_vector, - kr_sbert_vector, - km_bert_vector - ) - SELECT - d.etc_otc_code, - d.item_permit_date, - d.item_seq, - d.ee_doc_data, - d.entp_name, - d.item_name, - d.material_name, - d.nb_doc_data, - d.storage_method, - d.ud_doc_data, - d.valid_term, - i.img_url, - e.gpt_vector, - e.kr_sbert_vector, - e.km_bert_vector - FROM - gov_drug_detail AS d - LEFT JOIN - api_data_drug_img AS i ON d.item_seq = i.seq - LEFT JOIN - gov_embed_data AS e ON d.item_seq = e.item_seq - ON DUPLICATE KEY UPDATE - etc_otc_code = VALUES(etc_otc_code), - item_permit_date = VALUES(item_permit_date), - ee_doc_data = VALUES(ee_doc_data), - entp_name = VALUES(entp_name), - item_name = VALUES(item_name), - material_name = VALUES(material_name), - nb_doc_data = VALUES(nb_doc_data), - storage_method = VALUES(storage_method), - ud_doc_data = VALUES(ud_doc_data), - valid_term = VALUES(valid_term), - img_url = VALUES(img_url), - gpt_vector = VALUES(gpt_vector), - kr_sbert_vector = VALUES(kr_sbert_vector), - km_bert_vector = VALUES(km_bert_vector) - """, nativeQuery = true) - void createRawDataByApiTable(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java new file mode 100644 index 0000000..8e64322 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java @@ -0,0 +1,61 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.adapter; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.ApiRequestPort; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiPageCounter; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiResponseMapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ApiRequestAdapter implements ApiRequestPort { + private final RestTemplate restTemplate; + private final ApiUriCompBuilder apiUriCompBuilder; + private final ApiPageCounter apiPageCounter; + + @Override + public JsonNode getAllDetailData(int pageNo) { + + try { + String response = fetchDetailPage(pageNo); + return ApiResponseMapper.getItemsFromResponse(response); + } catch (Exception e){ + e.printStackTrace(); + return null; + } + } + + @Override + public JsonNode getAllImageData(int pageNo) { + try { + String response = fetchImagePage(pageNo); + return ApiResponseMapper.getItemsFromResponse(response); + } catch (Exception e){ + e.printStackTrace(); + return null; + } + } + + @Override + public int getDetailTotalPageCount() { + return apiPageCounter.getDetailApiTotalPageCount(); + } + + @Override + public int getImageTotalPageCount() { + return apiPageCounter.getImgApiTotalPageCount(); + } + + private String fetchDetailPage(int pageNo) { + return restTemplate.getForObject(apiUriCompBuilder.getUriForDetailApi(pageNo), String.class); + } + + private String fetchImagePage(int pageNo) { + return restTemplate.getForObject(apiUriCompBuilder.getUriForImgApi(pageNo), String.class); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/ApiRestTemplateConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/config/ApiRestTemplateConfig.java similarity index 84% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/ApiRestTemplateConfig.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/config/ApiRestTemplateConfig.java index a449759..4f4a162 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/ApiRestTemplateConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/config/ApiRestTemplateConfig.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.config; +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java similarity index 87% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java index df70c1a..a7586ca 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiPageCounter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java @@ -1,6 +1,7 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.api; +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.support; import com.fasterxml.jackson.databind.ObjectMapper; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -22,8 +23,9 @@ public ApiPageCounter(ApiUriCompBuilder uriBuilder, } public int getDetailApiTotalPageCount() { - URI uri = uriBuilder.getUriForDetailApiShort(); - return getPageCountFromUri(uri); + return 4; + // URI uri = uriBuilder.getUriForDetailApiShort(); + // return getPageCountFromUri(uri); } public int getImgApiTotalPageCount() { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java similarity index 89% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java index f0c0ada..4c81f8c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java @@ -1,6 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.api; - -import org.springframework.stereotype.Component; +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.support; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiUriCompBuilder.java similarity index 98% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiUriCompBuilder.java index 5f3d3b7..764e1dc 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiUriCompBuilder.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.api; +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.support; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java new file mode 100644 index 0000000..d34802e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.adapter; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client.EmbeddingClient; + +@Service +public class EmbeddingAdapter implements EmbeddingPort { + private final Map embeddingClientMap; + + public EmbeddingAdapter(List clients) { + this.embeddingClientMap = clients.stream() + .collect(Collectors.toMap(EmbeddingClient::getModelType, client -> client)); + } + @Override + public float[] getEmbedding(String text, EmbeddingModelType embeddingModelType) { + EmbeddingClient client = embeddingClientMap.get(embeddingModelType); + + return client.getEmbedding(text); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java new file mode 100644 index 0000000..6288567 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; + +public interface EmbeddingClient { + EmbeddingModelType getModelType(); + float[] getEmbedding(String text); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java similarity index 77% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java index 898e2f6..6484afb 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KmBertEmbeddingClient.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java @@ -1,16 +1,16 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; import java.net.URI; -import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java similarity index 77% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java index 702fa1b..3e54f83 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/KrSbertEmbeddingClient.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; import java.net.URI; @@ -8,8 +8,9 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.api.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/OpenaiEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java similarity index 87% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/OpenaiEmbeddingClient.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java index 5a7cf39..ae12d36 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/OpenaiEmbeddingClient.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; import java.util.List; @@ -11,7 +11,7 @@ import org.springframework.ai.retry.RetryUtils; import org.springframework.stereotype.Component; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; @Component public class OpenaiEmbeddingClient implements EmbeddingClient { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/config/OpenaiConfig.java similarity index 84% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/config/OpenaiConfig.java index a1ae50a..66ec57d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/config/OpenaiConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/config/OpenaiConfig.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.config; +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.config; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java new file mode 100644 index 0000000..083e434 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model; + +public enum EmbeddingModelType { + OPENAI, + SBERT, + KM_BERT +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingRequestText.java similarity index 50% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingRequestText.java index af90ff1..345e35a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/embedding/client/EmbeddingRequestText.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingRequestText.java @@ -1,8 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.client; +package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model; -import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java new file mode 100644 index 0000000..2c1d6d2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailRequestMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailMapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugDetailRepositoryAdapter implements DrugDetailRepositoryPort { + private final GovDrugDetailJpaRepository drugdetailJpaRepository; + + @Override + public void saveDrugDetail(DrugDetailRequest e){ + drugdetailJpaRepository.save(DrugDetailRequestMapper.toEntityFromRequest(e)); + } + + @Override + public void saveDrugDetailBulk(List list){ + drugdetailJpaRepository.saveAll(list.stream() + .map(DrugDetailRequestMapper::toEntityFromRequest) + .collect(Collectors.toList())); + drugdetailJpaRepository.flush(); + } + + @Override + public List getAllGovDrugDetail(){ + return drugdetailJpaRepository.findAll().stream() + .map(DrugDetailMapper::toDomainFromEntity) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java new file mode 100644 index 0000000..d57e814 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java @@ -0,0 +1,112 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; + +import java.util.function.Function; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugEmbedRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKmBertEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKrSbertEmbedJpaRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugEmbedRepositoryAdapter implements DrugEmbedRepositoryPort { + private final GovDrugGptEmbedJpaRepository gptRepository; + private final GovDrugKmBertEmbedJpaRepository kmBertRepository; + private final GovDrugKrSbertEmbedJpaRepository krSbertRepository; + + @Override + public void saveGptEmbed(Long drugId, float[] gptVector){ + gptRepository.save( + buildEmbedEntity(drugId, gptVector, DrugGptEmbedEntity.class) + ); + } + + @Transactional + @Override + public void saveKmBertEmbed(Long drugId, float[] kmBertVector){ + kmBertRepository.save( + buildEmbedEntity(drugId, kmBertVector, DrugKmBertEmbedEntity.class) + ); + } + + @Transactional + @Override + public void saveKrSbertEmbed(Long drugId, float[] krSbertVector){ + krSbertRepository.save( + buildEmbedEntity(drugId, krSbertVector, DrugKrSbertEmbedEntity.class) + ); + } + + @Override + public float[] getGptVector(Long drugId){ + return getVectorFromRepository( + drugId, gptRepository, e -> e.getGptVector() + ); + } + + @Override + public float[] getKmBertVector(Long drugId){ + return getVectorFromRepository( + drugId, kmBertRepository, e -> e.getKmBertVector() + ); + } + + @Override + public float[] getKrSbertVector(Long drugId){ + return getVectorFromRepository( + drugId, krSbertRepository, e -> e.getKrSbertVector() + ); + } + + private T buildEmbedEntity(Long drugId, float[] vector, Class clazz) { + try { + String vectorString = toStringFromFloatArray(vector); + return clazz.getDeclaredConstructor(Long.class, String.class) + .newInstance(drugId, toStringFromFloatArray(vector)); + } catch (Exception e) { + //TODO: 엔터티 생성 실패 + throw new RuntimeException(e); + } + } + + private float[] getVectorFromRepository(Long drugId, JpaRepository repository, Function vectorGetter) { + T entity = repository.findById(drugId).orElse(null); + + if (entity == null) { + return null; + } + + String vectorString = vectorGetter.apply(entity); + return toFloatFromString(vectorString); + } + + private String toStringFromFloatArray(float[] openAIVector) { + try { + return new ObjectMapper().writeValueAsString(openAIVector); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return null; + } + } + + private float[] toFloatFromString(String vector) { + try { + return new ObjectMapper().readValue(vector, float[].class); + } catch (Exception e) { + //TODO: 벡터 변환 실패 예외 처리 + System.out.println("벡터 변환 실패"); + return null; + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java new file mode 100644 index 0000000..3f32055 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java @@ -0,0 +1,44 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugImageRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.ApiDataDrugImgRepo; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugImageMapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugImageRepositoryAdapter implements DrugImageRepositoryPort { + private final ApiDataDrugImgRepo imageRepository; + + @Override + public List getAllGovDrugImage(){ + return imageRepository.findAll().stream() + .map(DrugImageMapper::toDomainFromEntity) + .collect(Collectors.toList()); + } + + @Override + public DrugImage getById(Long drugId) { + return imageRepository.findById(drugId) + .map(DrugImageMapper::toDomainFromEntity) + .orElseGet(() -> getDefaultDomain()); + } + + @Override + public void saveAllAndFlush(List imgData) { + imageRepository.saveAll(DrugImageMapper.toEntityListFromDomainList(imgData)); + imageRepository.flush(); + } + + private static DrugImage getDefaultDomain() { + return DrugImageMapper.toDomainFromEntity(new ApiDataDrugImgEntity()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java new file mode 100644 index 0000000..6e66fea --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java @@ -0,0 +1,35 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugRawDataRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugRawDataMapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugRawDataRepositoryAdapter implements DrugRawDataRepositoryPort { + + private final GovDrugJpaRepository jpaDrugRepository; + + @Override + public void save(DrugRawData drug){ + DrugRawDataEntity entity = DrugRawDataMapper.toEntityFromDomain(drug); + jpaDrugRepository.save(entity); + } + + @Override + public void saveAll(List rawData) { + jpaDrugRepository.saveAll( + rawData.stream() + .map(DrugRawDataMapper::toEntityFromDomain).toList() + ); + jpaDrugRepository.flush(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java new file mode 100644 index 0000000..15ac14a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java @@ -0,0 +1,63 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class DrugDetailRequest { + + @JsonProperty("ITEM_SEQ") + private Long drugId; + + @JsonProperty("ITEM_NAME") + private String drugName; + + @JsonProperty("ENTP_NAME") + private String company; + + @JsonProperty("ITEM_PERMIT_DATE") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") + private LocalDate permitDate; + + + private boolean isGeneral; + private String materialInfo; + + @JsonProperty("STORAGE_METHOD") + private String storeMethod; + + @JsonProperty("VALID_TERM") + private String validTerm; + + private String efficacy; + private String usage; + private String precaution; + + @JsonCreator + public DrugDetailRequest(@JsonProperty("ETC_OTC_CODE") String drugType) { + this.isGeneral = !"전문의약품".equals(drugType); + } + + public void changeMaterialInfo(String materialInfo){ + this.materialInfo = materialInfo; + } + + public void changeUsage(String usage) { + this.usage = usage; + } + + public void changeEfficacy(String efficacy) { + this.efficacy = efficacy; + } + + public void changePrecaution(String precaution) { + this.precaution = precaution; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java similarity index 56% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java index 6cde395..0006f6b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java @@ -1,23 +1,27 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.ToString; @Entity @Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor @ToString @Table(name="API_DATA_DRUG_IMG") public class ApiDataDrugImgEntity { @Id - @JsonProperty("ITEM_SEQ") - private Long seq; + private Long drugId; - @JsonProperty("BIG_PRDT_IMG_URL") private String imgUrl; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugDetailEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java similarity index 90% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugDetailEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java index 6c06157..62e0a27 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugDetailEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonFormat; @@ -23,7 +23,7 @@ @AllArgsConstructor @ToString @Table(name = "gov_drug_detail") -public class GovDrugDetailEntity { +public class DrugDetailEntity { @Id @JsonProperty("ITEM_SEQ") @@ -67,7 +67,7 @@ public class GovDrugDetailEntity { private String precaution; @JsonCreator - public GovDrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { + public DrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { this.isGeneral = !"전문의약품".equals(drugType); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java new file mode 100644 index 0000000..23adb44 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "DRUG_EMBED_GPT") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class DrugGptEmbedEntity { + @Id + @Column( name= "ITEM_SEQ") + private Long drugId; + + @Column( name= "GPT_VECTOR", columnDefinition = "JSON") + private String gptVector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java new file mode 100644 index 0000000..169e5cd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "DRUG_EMBED_KM_BERT") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class DrugKmBertEmbedEntity { + @Id + @Column( name= "ITEM_SEQ") + private Long drugId; + + @Column( name= "KM_BERT_VECTOR", columnDefinition = "JSON") + private String kmBertVector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java similarity index 55% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java index 38f58bd..9aa4109 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEmbedEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -6,24 +6,20 @@ import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity -@Table(name = "GOV_EMBED_DATA") +@Table(name = "DRUG_EMBED_KR-SBERT") @Builder @NoArgsConstructor @AllArgsConstructor -public class GovDrugEmbedEntity { +@Getter +public class DrugKrSbertEmbedEntity { @Id @Column( name= "ITEM_SEQ") private Long drugId; - @Column( name= "GPT_VECTOR", columnDefinition = "JSON") - private String gptVector; - @Column( name= "KR_SBERT_VECTOR", columnDefinition = "JSON") private String krSbertVector; - - @Column( name= "KM_BERT_VECTOR", columnDefinition = "JSON") - private String kmBertVector; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java similarity index 76% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java index 5f6aae8..b525684 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java @@ -1,7 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; import java.time.LocalDate; -import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -20,7 +19,7 @@ @NoArgsConstructor @AllArgsConstructor @Table(name="GOV_DRUG_RAW_DATA") -public class GovDrugEntity { +public class DrugRawDataEntity { @Id @Column(name="ITEM_SEQ") private Long drugId; @@ -58,13 +57,4 @@ public class GovDrugEntity { @Column(name= "IMG_URL") private String imageUrl; - - @Column(name= "gpt_vector", columnDefinition = "JSON") - private String gptVector; - - @Column(name= "KR_SBERT_VECTOR",columnDefinition = "JSON") - private String sbertVector; - - @Column(name= "KM_BERT_VECTOR",columnDefinition = "JSON") - private String kmBertVector; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java similarity index 84% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java index 59e16f6..1bd9517 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java @@ -1,12 +1,12 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jdbc; import java.util.List; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -17,7 +17,7 @@ public class GovDrugJdbcRepository { private final JdbcTemplate jdbc; @Transactional - public void saveAll(List entities) { + public void saveAll(List entities) { String sql = "" + "INSERT INTO gov_drug_detail " + " (ITEM_SEQ, ITEM_NAME, ENTP_NAME, " @@ -40,7 +40,7 @@ public void saveAll(List entities) { } @Transactional - public void mergeAll(List entities) { + public void mergeAll(List entities) { String sql = "" + "INSERT INTO gov_drug_raw_data " + " (ITEM_SEQ, ITEM_NAME, ENTP_NAME, " diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/JdbcBatchSetter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java similarity index 77% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/JdbcBatchSetter.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java index 7310690..8e46c34 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/JdbcBatchSetter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jdbc; import java.sql.Date; import java.sql.PreparedStatement; @@ -9,18 +9,18 @@ import org.springframework.jdbc.core.BatchPreparedStatementSetter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class JdbcBatchSetter implements BatchPreparedStatementSetter { - private final List entities; + private final List entities; @Override public void setValues(PreparedStatement ps, int i) throws SQLException { - GovDrugDetailEntity e = entities.get(i); + DrugDetailEntity e = entities.get(i); ps.setLong (1, e.getDrugId()); ps.setString (2, e.getDrugName()); ps.setString (3, e.getCompany()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java similarity index 67% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java index 4dfaa17..6dabccf 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/MergeBatchSetter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc; +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jdbc; import java.sql.Date; import java.sql.PreparedStatement; @@ -9,19 +9,18 @@ import org.springframework.jdbc.core.BatchPreparedStatementSetter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class MergeBatchSetter implements BatchPreparedStatementSetter { - private final List entities; + private final List entities; @Override public void setValues(PreparedStatement ps, int i) throws SQLException { - GovDrugEntity e = entities.get(i); + DrugRawDataEntity e = entities.get(i); ps.setLong (1, e.getDrugId()); ps.setString (2, e.getDrugName()); ps.setString (3, e.getCompany()); @@ -41,9 +40,6 @@ public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString (10, e.getUsage()); ps.setString (11, e.getPrecaution()); ps.setString(12, e.getImageUrl()); - ps.setString(13, e.getGptVector()); - ps.setString(14, e.getSbertVector()); - ps.setString(15, e.getKmBertVector()); } @Override diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java new file mode 100644 index 0000000..e921a62 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; + +@Repository +public interface ApiDataDrugImgRepo extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java new file mode 100644 index 0000000..b7054c8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java @@ -0,0 +1,21 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; + +import jakarta.transaction.Transactional; + +@Repository +public interface GovDrugDetailJpaRepository extends JpaRepository { + + @Override + @Transactional + List saveAllAndFlush(Iterable entities); + + List findByDrugIdGreaterThanOrderByDrugIdAsc(Long drugIdIsGreaterThan, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java new file mode 100644 index 0000000..652deb2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; + +@Repository +public interface GovDrugGptEmbedJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java new file mode 100644 index 0000000..2f9d84a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; + +public interface GovDrugJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java new file mode 100644 index 0000000..9598b20 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; + +@Repository +public interface GovDrugKmBertEmbedJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java new file mode 100644 index 0000000..8512bbc --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; + +@Repository +public interface GovDrugKrSbertEmbedJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java index effb2c3..2627157 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java @@ -7,19 +7,21 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrugDetail; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; public class DrugDetailMapper { - public static GovDrugDetail toDomainFromEntity(GovDrugDetailEntity e){ - GovDrugDetail domain = GovDrugDetail.builder() + + public static DrugDetail toDomainFromEntity(DrugDetailEntity e){ + DrugDetail domain = DrugDetail.builder() .drugId(e.getDrugId()) .drugName(e.getDrugName()) .company(e.getCompany()) @@ -35,8 +37,8 @@ public static GovDrugDetail toDomainFromEntity(GovDrugDetailEntity e){ return domain; } - public static GovDrug toDomainFromEntity(GovDrugEntity e){ - return GovDrug.builder() + public static Drug toDomainFromEntity(DrugRawDataEntity e){ + return Drug.builder() .drugId(e.getDrugId()) .drugName(e.getDrugName()) .company(e.getCompany()) @@ -48,9 +50,6 @@ public static GovDrug toDomainFromEntity(GovDrugEntity e){ .efficacy(convertEfficacy(e.getEfficacy())) .usage(getUsage(e.getUsage())) .precaution(getPrecaution(e.getPrecaution())) - .gptVector(toArraysFromFloatString(e.getGptVector())) - .sbertVector(toArraysFromFloatString(e.getSbertVector())) - .kmBertVector(toArraysFromFloatString(e.getKmBertVector())) .build(); } @@ -168,6 +167,13 @@ private static Map> getPrecaution(String precaution) { return result; } - + public JsonNode toJson(String json) { + try { + return new ObjectMapper().readValue(json, JsonNode.class); + } catch (JsonProcessingException e) { + //TODO 에러 로그 처리 필요합니다. + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java new file mode 100644 index 0000000..566a13c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java @@ -0,0 +1,23 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; + +public class DrugDetailRequestMapper { + + public static DrugDetailEntity toEntityFromRequest(DrugDetailRequest r){ + return DrugDetailEntity.builder() + .drugId(r.getDrugId()) + .drugName(r.getDrugName()) + .company(r.getCompany()) + .permitDate(r.getPermitDate()) + .isGeneral(r.isGeneral()) + .materialInfo(r.getMaterialInfo()) + .storeMethod(r.getStoreMethod()) + .validTerm(r.getValidTerm()) + .efficacy(r.getEfficacy()) + .usage(r.getUsage()) + .precaution(r.getPrecaution()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java new file mode 100644 index 0000000..4459365 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; + +public class DrugImageMapper { + public static DrugImage toDomainFromEntity(ApiDataDrugImgEntity e){ + return DrugImage.builder() + .drugId(e.getDrugId()) + .imageUrl(e.getImgUrl()) + .build(); + } + + public static ApiDataDrugImgEntity toEntityFromDomain(DrugImage d){ + return ApiDataDrugImgEntity.builder() + .drugId(d.getDrugId()) + .imgUrl(d.getImageUrl()) + .build(); + } + + public static List toEntityListFromDomainList(List drugImageList) { + return drugImageList.stream().map(DrugImageMapper::toEntityFromDomain).toList(); + } + + public static List toDomainListFromEntityList(List drugImageEntityList) { + return drugImageEntityList.stream().map(DrugImageMapper::toDomainFromEntity).toList(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java new file mode 100644 index 0000000..13fd1c0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; + +public class DrugRawDataMapper { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static DrugRawDataEntity toEntityFromDomain(DrugRawData raw) { + return DrugRawDataEntity + .builder() + .drugId(raw.getDrugId()) + .drugName(raw.getDrugName()) + .company(raw.getCompany()) + .permitDate(raw.getPermitDate()) + .isGeneral(raw.isGeneral()) + .materialInfo(toStringFromObj(raw.getMaterialInfo())) + .storeMethod(raw.getStoreMethod()) + .validTerm(raw.getValidTerm()) + .efficacy(toStringFromObj(raw.getEfficacy())) + .usage(toStringFromObj(raw.getUsage())) + .precaution(toStringFromObj(raw.getPrecaution())) + .imageUrl(raw.getImageUrl()) + .build(); + } + + private static String toStringFromObj(Object obj){ + try{ + return objectMapper.writeValueAsString(obj); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("변환 실패"); + return null; + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java deleted file mode 100644 index ee4f867..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.parser; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -public class MaterialParser { - public static String parseMaterial(String raw) throws Exception { - ObjectMapper result = new ObjectMapper(); - ArrayNode resultArray = result.createArrayNode(); - String[] blocks = splitBlock(raw); - parsingblocksAndPutArrayItem(blocks, resultArray); - return convertString(result, resultArray); - } - - private static void parsingblocksAndPutArrayItem(String[] blocks, ArrayNode resultArray) { - for (String block : blocks) { - block = block.trim(); - if (block.isEmpty()) { - continue; - } - String[] pairs = splitByPipe(block); - ObjectNode item = makeItem(pairs); - resultArray.add(item); - } - } - - private static String convertString(ObjectMapper objectMapper, ArrayNode arrayNode) { - try { - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode); - } catch (JsonProcessingException e) { - //TODO String 변환실패 - throw new RuntimeException(e); - } - } - - private static ObjectNode makeItem(String[] pairs) { - ObjectNode item = new ObjectMapper().createObjectNode(); - for (String pair : pairs) { - String[] kv = pair.split(":", 2); - String key = kv[0].trim(); - String value = ""; - if(kv.length == 2){ - value = kv[1].trim(); - } - item.put(key, value); - } - return item; - } - - private static String[] splitByPipe(String block) { - return block.split("\\|"); - } - - private static String[] splitBlock(String raw) { - return raw.split(";"); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index 6773b0b..7301ddb 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -2,37 +2,24 @@ import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; -import java.util.List; - -import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugApprovalDetailScraper; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugDataService; import com.likelion.backendplus4.yakplus.drug.application.service.scraper.DrugScraper; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; -import com.likelion.backendplus4.yakplus.drug.domain.model.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; import lombok.RequiredArgsConstructor; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.embedding.EmbeddingModelType; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; import com.likelion.backendplus4.yakplus.response.ApiResponse; @RestController @RequiredArgsConstructor public class DrugDataTestController { - private final DrugDataService dragDataService; private final EmbeddingPort embeddingPort; private final DrugScraper scraperUseCase; - @GetMapping("/data/all") - public List getAllData(Pageable pageable){ - return dragDataService.findAllRawDrug(pageable); - } - @GetMapping("/test/embed") public ResponseEntity> getEmbedData(){ log("getEmbedData"); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java index 7c87340..4cac8b3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java @@ -2,33 +2,26 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugApprovalDetailScraper; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugDetailScraperUsecase; import lombok.RequiredArgsConstructor; @Controller @RequiredArgsConstructor public class DrugDetailController { - private final DrugApprovalDetailScraper scraperUseCase; + private final DrugDetailScraperUsecase drugDetailScraperUsecase; - @GetMapping("/gov/api/parser/detail/start") + @PostMapping("/gov/api/parser/detail/start") public ResponseEntity saveAPIData(){ - scraperUseCase.requestUpdateRawData(); - return ResponseEntity.ok().build(); - } - - @GetMapping("/gov/api/parser/detail/startAll") - public ResponseEntity saveAPIDataAll(){ - scraperUseCase.requestUpdateAllRawData(); + drugDetailScraperUsecase.requestSingleData(1); return ResponseEntity.ok().build(); } @PostMapping("/gov/api/parser/detail/startAll") - public ResponseEntity saveAPIDataAllByJdbc(){ - scraperUseCase.requestUpdateAllRawDataByJdbc(); + public ResponseEntity saveAPIDataAll(){ + drugDetailScraperUsecase.requestAllData(); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java index cf68610..c818df0 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java @@ -3,7 +3,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.DrugImageGovScraper; +import com.likelion.backendplus4.yakplus.drug.application.service.scraper.image.DrugImageGovScraper; import lombok.RequiredArgsConstructor; @@ -14,6 +14,6 @@ public class DrugImageController { @GetMapping("/gov/api/parser/image/start") public void test(){ - imageScraper.getApiData(); + imageScraper.getApiData(1); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java new file mode 100644 index 0000000..1045a3e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.index.application.port.in; + +import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; + +public interface IndexUseCase { + void index(IndexRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java new file mode 100644 index 0000000..c600a79 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java @@ -0,0 +1,9 @@ +package com.likelion.backendplus4.yakplus.index.application.port.out; + +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; + +import java.util.List; + +public interface DrugIndexRepositoryPort { + void saveAll(String esIndexName, List drugs); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java new file mode 100644 index 0000000..f987af5 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java @@ -0,0 +1,9 @@ +package com.likelion.backendplus4.yakplus.index.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; + +import java.util.List; + +public interface EmbeddingLoadingPort { + List loadAllEmbeddings(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java new file mode 100644 index 0000000..5599201 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java @@ -0,0 +1,5 @@ +package com.likelion.backendplus4.yakplus.index.application.port.out; + +public interface EmbeddingPort { + float[] getEmbedding(String text); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java new file mode 100644 index 0000000..6f8d876 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java @@ -0,0 +1,11 @@ +package com.likelion.backendplus4.yakplus.index.application.port.out; + +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface GovDrugRawDataPort { + List fetchRawData(Long lastSeq, Pageable pageable); + String getEsIndexName(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java new file mode 100644 index 0000000..9d3af4d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -0,0 +1,107 @@ +package com.likelion.backendplus4.yakplus.index.application.service; + +import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; +import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * 약품 색인(인덱싱) 작업을 수행하는 서비스 구현체 + * + * @modified 2025-04-27 + * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 + * - itemSeq -> drugId로 수정 + * @since 2025-04-22 + */ +@Service +@RequiredArgsConstructor +public class DrugIndexer implements IndexUseCase { + private final GovDrugRawDataPort govDrugRawDataPort; + private final DrugIndexRepositoryPort drugIndexRepositoryPort; + private static final String SORT_BY_PROPERTY = "drugId"; + + /** + * 요청으로 전달된 lastSeq, limit 정보를 바탕으로 RDB에서 데이터를 조회하고 + * ES 인덱스에 저장한다. + * + * @param request 색인 기준 및 개수 정보 + * @author 정안식 + * @modified 2025-04-27 + * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 + * @since 2025-04-22 + */ + @Override + public void index(IndexRequest request) { + log("index 서비스 요청 수신"); + Pageable pageable = createPageable(request.limit()); + List drugs = fetchRawData(request, pageable); + String esIndexName = getEsIndexName(); + saveDrugs(esIndexName, drugs); + } + + /** + * limit 크기 및 DrugId 오름차순 정렬 기준의 객체를 생성한다. + * + * @param limit 조회할 최대 건수 + * @return 페이징 객체 + * @author 정안식 + * @modified 2025-04-27 + * 25.04.27 - itemSeq -> drugId로 수정 + * @since 2025-04-22 + */ + private Pageable createPageable(int limit) { + log("pageable 생성"); + return PageRequest.of(0, limit, Sort.by(SORT_BY_PROPERTY).ascending()); + } + + /** + * RDB에서 lastSeq 이후의 원시 데이터를 조회하여 도메인 객체로 변환한다. + * + * @param request 색인 기준 정보 + * @param pageable 페이징 및 정렬 정보 + * @return 도메인 모델 리스트 + * @author 정안식 + * @modified 2025-04-24 + * @since 2025-04-22 + */ + private List fetchRawData(IndexRequest request, Pageable pageable) { + log("RDB에서 원시 데이터 조회"); + return govDrugRawDataPort.fetchRawData(request.lastDrugId(), pageable); + } + + /** + * Elasticsearch 인덱스 이름을 조회한다. + * + * @return Elasticsearch 인덱스 이름 + * @author 정안식 + * @since 2025-04-27 + */ + private String getEsIndexName() { + log("ES 인덱스 이름 조회"); + return govDrugRawDataPort.getEsIndexName(); + } + + /** + * 조회된 도메인 객체들을 ES 인덱스에 저장 처리한다. + * + * @param drugs 저장할 도메인 모델 리스트 + * @author 정안식 + * @modified 2025-04-27 + * 25.04.27 - esIndexName을 인자로 받도록 수정 + * @since 2025-04-22 + */ + private void saveDrugs(String esIndexName, List drugs) { + log("ES 인덱스에 저장"); + drugIndexRepositoryPort.saveAll(esIndexName, drugs); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java new file mode 100644 index 0000000..11c5ef0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.index.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ElasticsearchConfig { + // TODO: 필요 시 RestClient 빈 등록 +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java new file mode 100644 index 0000000..c74cfea --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.index.config; + +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// @Configuration +public class OpenAiConfig { +// TODO: 삭제해야할 클래스 +// - 기존 배치 소스와 중복 +// @Value("${spring.ai.openai.api-key}") +// private String apiKey; +// +// @Bean +// public OpenAiApi openaiApi() { +// return new OpenAiApi(apiKey); +// } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java new file mode 100644 index 0000000..d6bd5c2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.index.domain.model; + +import lombok.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Drug { + private Long drugId; + private String drugName; + private String company; + private List efficacy; + private float[] vector; + private LocalDate permitDate; + private boolean isGeneral; + private List materialInfo; + private String storeMethod; + private String validTerm; + private List usage; + private Map> precaution; + private String imageUrl; +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java b/src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java new file mode 100644 index 0000000..b2b08b9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.index.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class IndexException extends CustomException { + private final ErrorCode errorCode; + + public IndexException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java new file mode 100644 index 0000000..eba0492 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.index.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum IndexErrorCode implements ErrorCode { + RAW_DATA_FETCH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430001, "원시 데이터 조회 실패"), + ES_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430003, "Elasticsearch 저장 실패"), + EMBEDDING_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 440001, "임베딩 API 호출 실패"); + + private final HttpStatus status; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return status; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java new file mode 100644 index 0000000..08a7cc3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; + +import java.util.List; +import java.util.Map; + +public class DrugMapper{ + public static List parseMaterials(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("Material 파싱 실패", e); + } + } + + public static List parseStringToList(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("String to list 파싱 실패", e); + } + } + + public static Map> parsePrecaution(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>>() {}); + } catch (Exception e) { + throw new RuntimeException("precaution 파싱 실패", e); + } + } + + public static float[] parseJsonToFloatArray(String json) { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(json, float[].class); + } catch (Exception e) { + throw new RuntimeException("Failed to parse vector JSON", e); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java new file mode 100644 index 0000000..2106fc6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java @@ -0,0 +1,115 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RestClient; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * Elasticsearch를 통해 Drug 도메인 객체의 색인 기능을 제공하는 어댑터 클래스입니다. + * DrugIndexRepositoryPort를 구현하여 + * Elasticsearch 원격 호출을 캡슐화합니다. + * + * @modified 2025-04-27 + * 25.04.27 - saveAll()를 Bulk 요청으로 전환 + * - buildBulkRequestBody(), createBulkRequest() 메서드 추가 + * @since 2025-04-22 + */ +@Component +public class ElasticsearchDrugAdapter implements DrugIndexRepositoryPort { + private final RestClient restClient; + private final ObjectMapper objectMapper; + + public ElasticsearchDrugAdapter(RestClient restClient, ObjectMapper objectMapper) { + this.restClient = restClient; + this.objectMapper = objectMapper; + } + + /** + * 주어진 Drug 목록을 Elasticsearch에 Bulk API로 일괄 저장합니다. + * + * @param esIndexName Bulk 대상 ES 인덱스 이름 + * @param drugs 저장할 Drug 객체 리스트 + * @throws IndexException 색인 처리 중 오류 발생 시 + * @author 정안식 + * @modified 2025-04-27 + * 25.04.27 - esIndexname을 인자로 받아 Bulk API로 일괄 저장하도록 수정 + * @since 2025-04-22 + */ + @Override + public void saveAll(String esIndexName, List drugs) { + log("saveAll() 메서드 호출, 인덱스 이름: " + esIndexName + ", Drug 개수: " + drugs.size()); + try { + String bulkBody = buildBulkRequestBody(esIndexName, drugs); + Request bulkRequest = createBulkRequest(esIndexName, bulkBody); + restClient.performRequest(bulkRequest); + log("saveAll() 메서드 완료, 인덱스 이름: " + esIndexName + ", Drug 개수: " + drugs.size()); + } catch (Exception e) { + log(LogLevel.ERROR, "Elasticsearch 색인 처리 중 오류 발생", e); + throw new IndexException(IndexErrorCode.ES_SAVE_ERROR); + } + } + + /** + * Bulk API 요청용 NDJSON 바디를 생성합니다. + * + * @param esIndexName Bulk 대상 ES 인덱스 이름 + * @param drugs 색인할 Drug 리스트 + * @return NDJSON 형식의 문자열 + * @throws Exception JSON 직렬화 오류 시 + * @author 정안식 + * @since 2025-04-27 + */ + private String buildBulkRequestBody(String esIndexName, List drugs) throws Exception { + log("buildBulkRequestBody() 메서드 호출, 인덱스 이름: " + esIndexName + ", Drug 개수: " + drugs.size()); + StringBuilder sb = new StringBuilder(); + for (Drug drug : drugs) { + Map action = Map.of("index", Map.of("_index", esIndexName, "_id", drug.getDrugId().toString())); + sb.append(objectMapper.writeValueAsString(action)).append("\n"); + sb.append(objectMapper.writeValueAsString(createDrugDocument(drug))).append("\n"); + } + return sb.toString(); + } + + /** + * Bulk 요청을 위한 Request 객체를 생성합니다. + * + * @param esIndexName Bulk 엔드포인트에 사용할 ES 인덱스 이름 + * @param bulkBody NDJSON 형식의 Bulk 요청 바디 + * @return Bulk 용 Request + * @author 정안식 + * @since 2025-04-27 + */ + private Request createBulkRequest(String esIndexName, String bulkBody) { + log("createBulkRequest() 메서드 호출, 인덱스 이름: " + esIndexName); + Request request = new Request("POST", "/" + esIndexName + "/_bulk"); + request.setEntity(new NStringEntity(bulkBody, ContentType.APPLICATION_JSON)); + return request; + } + + /** + * Drug 객체와 임베딩 벡터를 기반으로 Elasticsearch 색인용 문서 필드 맵을 생성한다. + * + * @param drug 색인할 Drug 도메인 객체 + * @return Elasticsearch에 저장할 문서 필드 맵 + * @author 정안식 + * @modified 2025-04-27 + * 25.04.27 - 변경된 Drug 도메인 객체 내부 필드에 맞춰 수정 + * @since 2025-04-22 + */ + private Map createDrugDocument(Drug drug) { + return Map.of("drugId", drug.getDrugId(), "drugName", drug.getDrugName(), "company", drug.getCompany(), "efficacy", drug.getEfficacy(), "imageUrl", drug.getImageUrl(), "vector", drug.getVector()); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java new file mode 100644 index 0000000..0d5b56d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -0,0 +1,122 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; +import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 공공 API로부터 조회한 원시 약품 데이터를 JPA를 통해 가져와 + * 도메인 객체인 Drug로 변환하는 어댑터 클래스입니다. + * + * @since 2025-04-22 + * @modified 2025-04-24 + */ +@Component +@RequiredArgsConstructor +public class GovDrugRawDataAdapter implements GovDrugRawDataPort { + private final GovDrugDetailJpaRepository rawDataJpaRepository; + + /** + * 마지막으로 처리된 시퀀스 이후의 원시 데이터를 페이징 조건에 맞춰 조회하고 + * Drug 도메인 리스트로 변환하여 반환합니다. + * + * @param lastSeq 마지막 처리 시퀀스 (null이면 0부터 조회) + * @param pageable 페이징 및 정렬 정보 + * @return Drug 도메인 객체 리스트 + * @throws IndexException 데이터베이스 조회 실패 시 발생 + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + @Override + public List fetchRawData(Long lastSeq, Pageable pageable) { + long startSeq = getStartSeq(lastSeq); + List govDrugRawDataEntities = getGovDrugRawDataEntities(startSeq, pageable); + + return convertToDrugDomains(govDrugRawDataEntities); + } + + @Override + public String getEsIndexName() { + //TODO 구현 필요 + return ""; + } + + /** + * lastSeq가 null일 경우 0으로 치환하여 조회 시작점을 결정합니다. + * + * @param lastSeq 마지막 처리 시퀀스 + * @return 실제 조회 시작 시퀀스 + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + private Long getStartSeq(Long lastSeq) { + return (lastSeq == null ? 0L : lastSeq); + } + + /** + * JPA 레포지토리를 이용해 itemSeq 기준으로 정렬된 데이터를 조회합니다. + * + * @param lastSeq 마지막으로 조회된 Seq + * @param pageable 페이징 및 정렬 정보 + * @return 조회된 GovDrugRawDataEntity 리스트 + * @throws IndexException 조회 중 예외가 발생하면 SearchErrorCode.RAW_DATA_FETCH_ERROR로 래핑하여 던집니다. + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + private List getGovDrugRawDataEntities(Long lastSeq, Pageable pageable) { + try { + return rawDataJpaRepository.findByDrugIdGreaterThanOrderByDrugIdAsc(lastSeq, pageable); + } catch (Exception e) { + //TODO: LOG ERROR 처리 요망 +// log(LogLevel.ERROR, "MySQL 데이터 조회 실패", e); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + + } + + /** + * 조회된 엔티티 리스트를 Drug 도메인 객체 리스트로 변환합니다. + * + * @param rawData GovDrugRawDataEntity 리스트 + * @return Drug 도메인 객체 리스트 + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + private List convertToDrugDomains(List rawData) { + return rawData.stream() + .map(this::mapToDrugDomain) + .collect(Collectors.toList()); + } + + /** + * 단일 GovDrugRawDataEntity를 Drug 도메인 객체로 매핑합니다. + * + * @param entity 변환할 GovDrugRawDataEntity + * @return 변환된 Drug 도메인 객체 + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + private Drug mapToDrugDomain(DrugDetailEntity entity) { + //TODO: Mapper로 변경 필요 + return Drug.builder() + // .drugId(entity.getItemSeq()) + // .drugName(entity.getItemName()) + // .company(entity.getEntpName()) + // .efficacy(entity.get()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java new file mode 100644 index 0000000..87498d7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -0,0 +1,67 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +@RequiredArgsConstructor +@Qualifier("gptAdapter") +public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { + private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; + private final GovDrugJpaRepository govDrugJpaRepository; + + + @Override + public List loadAllEmbeddings() { + List rawDataEntities = govDrugJpaRepository.findAll(); + List drugGptEmbedEntities = govDrugGptEmbedJpaRepository.findAll(); + + // drugGptEmbedEntities를 Map으로 변환 (key: drugId) + Map gptEmbedMap = new HashMap<>(); + for (DrugGptEmbedEntity embed : drugGptEmbedEntities) { + gptEmbedMap.put(embed.getDrugId(), embed); + } + + List drugs = new ArrayList<>(); + for (DrugRawDataEntity drugRawData : rawDataEntities) { + DrugGptEmbedEntity embed = gptEmbedMap.get(drugRawData.getDrugId()); + Drug drug = toDomainFromEntity(drugRawData, embed); + drugs.add(drug); + } + + return drugs; + } + + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbedEntity embedEntity) { + return Drug.builder() + .drugId(drugEntity.getDrugId()) + .drugName(drugEntity.getDrugName()) + .company(drugEntity.getCompany()) + .permitDate(drugEntity.getPermitDate()) + .isGeneral(drugEntity.isGeneral()) + .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) + .storeMethod(drugEntity.getStoreMethod()) + .validTerm(drugEntity.getValidTerm()) + .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) + .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) + .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) + .imageUrl(drugEntity.getImageUrl()) + .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getGptVector())) + .build(); + } + + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java new file mode 100644 index 0000000..e189aee --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java @@ -0,0 +1,65 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKmBertEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +@RequiredArgsConstructor +@Qualifier("kmBertAdapter") +public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { + private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; + private final GovDrugJpaRepository govDrugJpaRepository; + + + @Override + public List loadAllEmbeddings() { + List rawDataEntities = govDrugJpaRepository.findAll(); + List drugKmBertEmbedEntities = govDrugKmBertEmbedJpaRepository.findAll(); + + // drugKmBertEmbedEntities를 Map으로 변환 (key: drugId) + Map kmBertEmbedMap = new HashMap<>(); + for (DrugKmBertEmbedEntity embed : drugKmBertEmbedEntities) { + kmBertEmbedMap.put(embed.getDrugId(), embed); + } + + List drugs = new ArrayList<>(); + for (DrugRawDataEntity drugRawData : rawDataEntities) { + DrugKmBertEmbedEntity embed = kmBertEmbedMap.get(drugRawData.getDrugId()); + Drug drug = toDomainFromEntity(drugRawData, embed); + drugs.add(drug); + } + + return drugs; + } + + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKmBertEmbedEntity embedEntity) { + return Drug.builder() + .drugId(drugEntity.getDrugId()) + .drugName(drugEntity.getDrugName()) + .company(drugEntity.getCompany()) + .permitDate(drugEntity.getPermitDate()) + .isGeneral(drugEntity.isGeneral()) + .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) + .storeMethod(drugEntity.getStoreMethod()) + .validTerm(drugEntity.getValidTerm()) + .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) + .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) + .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) + .imageUrl(drugEntity.getImageUrl()) + .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getKmBertVector())) + .build(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java new file mode 100644 index 0000000..8bf0c75 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java @@ -0,0 +1,65 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKrSbertEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +@RequiredArgsConstructor +@Qualifier("krSBertAdapter") +public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { + private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; + private final GovDrugJpaRepository govDrugJpaRepository; + + + @Override + public List loadAllEmbeddings() { + List rawDataEntities = govDrugJpaRepository.findAll(); + List drugKrSBertEmbedEntities = govDrugKrSbertEmbedJpaRepository.findAll(); + + // drugKrSBertEmbedEntities를 Map으로 변환 (key: drugId) + Map krSBertEmbedMap = new HashMap<>(); + for (DrugKrSbertEmbedEntity embed : drugKrSBertEmbedEntities) { + krSBertEmbedMap.put(embed.getDrugId(), embed); + } + + List drugs = new ArrayList<>(); + for (DrugRawDataEntity drugRawData : rawDataEntities) { + DrugKrSbertEmbedEntity embed = krSBertEmbedMap.get(drugRawData.getDrugId()); + Drug drug = toDomainFromEntity(drugRawData, embed); + drugs.add(drug); + } + + return drugs; + } + + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKrSbertEmbedEntity embedEntity) { + return Drug.builder() + .drugId(drugEntity.getDrugId()) + .drugName(drugEntity.getDrugName()) + .company(drugEntity.getCompany()) + .permitDate(drugEntity.getPermitDate()) + .isGeneral(drugEntity.isGeneral()) + .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) + .storeMethod(drugEntity.getStoreMethod()) + .validTerm(drugEntity.getValidTerm()) + .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) + .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) + .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) + .imageUrl(drugEntity.getImageUrl()) + .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getKrSbertVector())) + .build(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java new file mode 100644 index 0000000..bfae023 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java @@ -0,0 +1,75 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +/** + * OpenAI 임베딩 API를 호출하여 텍스트에 대한 벡터 임베딩을 생성하는 어댑터 클래스입니다. + * EmbeddingPort 인터페이스를 구현하며, 오류 발생 시 SearchException으로 래핑합니다. + * + * @since 2025-04-22 + * @modified 2025-04-24 + */ +@Component +@RequiredArgsConstructor +public class OpenAIEmbeddingAdapter implements EmbeddingPort { + private final OpenAiApi openAiApi; + private static final String EMBEDDING_MODEL = "text-embedding-3-small"; + + /** + * 주어진 텍스트를 OpenAI 임베딩 모델에 전달하여 벡터 값을 반환합니다. + * 내부에서 OpenAiEmbeddingModel을 생성하고 retry 템플릿을 적용합니다. + * API 호출 중 예외가 발생하면 SearchException(EMBEDDING_API_ERROR)을 던집니다. + * + * @param text 벡터화할 입력 텍스트 + * @return float 배열 형태의 임베딩 벡터 + * @throws IndexException EMBEDDING_API_ERROR 코드로 래핑하여 발생 + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + @Override + public float[] getEmbedding(String text) { + try { + OpenAiEmbeddingModel embeddingModel = createEmbeddingModel(); + EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text)); + return response.getResults().getFirst().getOutput(); + } catch (Exception e) { + //TODO: LOG ERROR 처리 요망 +// log(LOGLEVEL.ERROR, "임베딩 API에서 문제가 발생하였습니다., e); + throw new IndexException(IndexErrorCode.EMBEDDING_API_ERROR); + } + } + + /** + * OpenAiEmbeddingModel 인스턴스를 생성하여 반환합니다. + * MetadataMode와 모델 이름, RetryUtils 설정이 포함됩니다. + * + * @return 초기화된 OpenAiEmbeddingModel 객체 + * @author 정안식 + * @since 2025-04-22 + * @modified 2025-04-24 + */ + private OpenAiEmbeddingModel createEmbeddingModel() { + return new OpenAiEmbeddingModel( + openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model(EMBEDDING_MODEL) + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java new file mode 100644 index 0000000..a11f4d4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java @@ -0,0 +1,38 @@ +package com.likelion.backendplus4.yakplus.index.presentation.controller; + +import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; +import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * 약품 인덱싱 API 엔드포인트를 제공하는 컨트롤러 클래스 + * + * @modified 2025-04-25 + * @since 2025-04-22 + */ +@RestController +@RequestMapping("/api/drugs/index") +@RequiredArgsConstructor +public class DrugController { + private final IndexUseCase indexUseCase; + + /** + * 색인 생성 요청을 처리한다. + * + * @param request 인덱싱 범위 및 개수 정보를 담은 요청 객체 + * @author 정안식 + * @modified 2025-04-24 + * @since 2025-04-22 + */ + @PostMapping("/save") + public void index(@RequestBody IndexRequest request) { + log("index 컨트롤러 요청 수신" + request.toString()); + indexUseCase.index(request); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java new file mode 100644 index 0000000..5f6985a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java @@ -0,0 +1,13 @@ +package com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request; + +/** + * 인덱싱 요청 정보 DTO + * + * @since 2025-04-22 + * @modified 2025-04-27 + * 25.04.27 - itemSeq -> drugId로 수정 + */ +public record IndexRequest( + Long lastDrugId, + int limit) { +} \ No newline at end of file From 8fa865c5dcba4a40a8e4ab9403d83d212fc00a7d Mon Sep 17 00:00:00 2001 From: chanbyeong <122460524+chanbyoung@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:48:51 +0900 Subject: [PATCH 15/47] =?UTF-8?q?=E2=9C=A8=20Feature/#35=20=EC=A6=9D?= =?UTF-8?q?=EC=83=81=20=EC=9D=B8=EB=8D=B1=EC=8B=B1=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: 증상 Document, Repository 정의 * ✨ Feat: 증상 파싱 로직 구현 * ✨ Feat: 증상 인덱싱 기능 구현 --- .../support/mapper/DrugRawDataMapper.java | 31 +++++ .../application/port/in/IndexUseCase.java | 2 + .../port/out/DrugIndexRepositoryPort.java | 3 + .../port/out/GovDrugRawDataPort.java | 4 + .../application/service/DrugIndexer.java | 33 ++++++ .../persistence/ElasticsearchDrugAdapter.java | 24 +++- .../persistence/GovDrugRawDataAdapter.java | 25 +++++ .../repository/DrugSymptomRepository.java | 7 ++ .../document/DrugSymptomDocument.java | 40 +++++++ .../controller/DrugController.java | 17 +++ .../index/support/mapper/SymptomMapper.java | 43 +++++++ .../support/parser/JsonArrayTextParser.java | 106 ++++++++++++++++++ .../support/parser/SymptomTextParser.java | 61 ++++++++++ 13 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java index 13fd1c0..9c6a26f 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java @@ -1,8 +1,14 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.index.support.parser.JsonArrayTextParser; +import java.io.IOException; +import java.util.List; public class DrugRawDataMapper { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -25,6 +31,31 @@ public static DrugRawDataEntity toEntityFromDomain(DrugRawData raw) { .build(); } + + public static Drug toDomainFromEntity(DrugRawDataEntity e) { + List efficacy; + try { + efficacy = JsonArrayTextParser.extractAndClean(e.getEfficacy()); + } catch (IOException exception) { + throw new ScraperException(ScraperErrorCode.PARSING_ERROR); + } + + return Drug.builder() + .drugId(e.getDrugId()) + .drugName(e.getDrugName()) + .company(e.getCompany()) + .permitDate(e.getPermitDate()) + .isGeneral(e.isGeneral()) +// TODO .materialInfo(JsonArrayTextParser.extractAndClean(e.getMaterialInfo())) + .storeMethod(e.getStoreMethod()) + .validTerm(e.getValidTerm()) + .efficacy(efficacy) +// TODO .usage(e.getUsage()) +// TODO .precaution(e.getPrecaution()) + .imageUrl(e.getImageUrl()) + .build(); + } + private static String toStringFromObj(Object obj){ try{ return objectMapper.writeValueAsString(obj); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java index 1045a3e..d9e5407 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java @@ -4,4 +4,6 @@ public interface IndexUseCase { void index(IndexRequest request); + + void indexSymptom(); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java index c600a79..6b368ad 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java @@ -3,7 +3,10 @@ import com.likelion.backendplus4.yakplus.index.domain.model.Drug; import java.util.List; +import org.springframework.data.domain.Page; public interface DrugIndexRepositoryPort { void saveAll(String esIndexName, List drugs); + + void saveAllSymptom(Page drugPage); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java index 6f8d876..226ed60 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java @@ -1,6 +1,8 @@ package com.likelion.backendplus4.yakplus.index.application.port.out; import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.util.List; @@ -8,4 +10,6 @@ public interface GovDrugRawDataPort { List fetchRawData(Long lastSeq, Pageable pageable); String getEsIndexName(); + + Page findAllDrugs(Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index 9d3af4d..ec6b578 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -6,6 +6,7 @@ import com.likelion.backendplus4.yakplus.index.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -29,6 +30,7 @@ public class DrugIndexer implements IndexUseCase { private final GovDrugRawDataPort govDrugRawDataPort; private final DrugIndexRepositoryPort drugIndexRepositoryPort; private static final String SORT_BY_PROPERTY = "drugId"; + private static final int CHUNK_SIZE = 1_000; /** * 요청으로 전달된 lastSeq, limit 정보를 바탕으로 RDB에서 데이터를 조회하고 @@ -49,6 +51,37 @@ public void index(IndexRequest request) { saveDrugs(esIndexName, drugs); } + /** + * DB에서 약품 데이터를 페이징으로 가져와 Elasticsearch에 일괄 색인합니다. + * 각 페이지는 CHUNK_SIZE만큼 처리되며, 모든 데이터를 순차적으로 색인합니다. + * + * @author 박찬병 + * @since 2025-04-24 + * @modified 2025-04-25 + */ + @Override + public void indexSymptom() { + log("indexSymptom 요청 수신"); + int page = 0; + Page drugPage; + + do { + log("색인 시작: page=" + page); + + // 1. 페이징으로 DB에서 한 청크 가져오기 + drugPage = govDrugRawDataPort.findAllDrugs(PageRequest.of(page, CHUNK_SIZE)); + log(" 조회 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); + + // 2. 청크별 ES에 색인 + drugIndexRepositoryPort.saveAllSymptom(drugPage); + log(" 색인 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); + + // 3. 다음 1000개 값 루프 + page++; + } while (drugPage.hasNext()); + log("indexSymptom 전체 처리 완료"); + } + /** * limit 크기 및 DrugId 오름차순 정렬 기준의 객체를 생성한다. * diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java index 2106fc6..a620e48 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java @@ -6,14 +6,20 @@ import com.likelion.backendplus4.yakplus.index.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.DrugSymptomRepository; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; +import com.likelion.backendplus4.yakplus.index.support.mapper.SymptomMapper; import org.apache.http.entity.ContentType; import org.apache.http.nio.entity.NStringEntity; import org.elasticsearch.client.Request; import org.elasticsearch.client.RestClient; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; @@ -29,10 +35,12 @@ */ @Component public class ElasticsearchDrugAdapter implements DrugIndexRepositoryPort { + private final DrugSymptomRepository symptomRepository; private final RestClient restClient; private final ObjectMapper objectMapper; - public ElasticsearchDrugAdapter(RestClient restClient, ObjectMapper objectMapper) { + public ElasticsearchDrugAdapter(DrugSymptomRepository drugSymptomRepository, RestClient restClient, ObjectMapper objectMapper) { + this.symptomRepository = drugSymptomRepository; this.restClient = restClient; this.objectMapper = objectMapper; } @@ -62,6 +70,20 @@ public void saveAll(String esIndexName, List drugs) { } } + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveAllSymptom(Page drugs) { + // 도메인 → ES Document 변환 + log("saveAllSymptom() 요청 수신"); + List docs = drugs.stream() + .map(SymptomMapper::toDocument) // 내부에서 예외 처리 됨 + .toList(); + log(" 문서 변환 완료: count=" + docs.size()); + + symptomRepository.saveAll(docs); + log(" ES 색인 완료: count=" + docs.size()); + } + /** * Bulk API 요청용 NDJSON 바디를 생성합니다. * diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index 0d5b56d..ade8dd7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -1,12 +1,17 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugRawDataMapper; import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; import com.likelion.backendplus4.yakplus.index.domain.model.Drug; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -24,6 +29,7 @@ @RequiredArgsConstructor public class GovDrugRawDataAdapter implements GovDrugRawDataPort { private final GovDrugDetailJpaRepository rawDataJpaRepository; + private final GovDrugJpaRepository drugJpaRepository; /** * 마지막으로 처리된 시퀀스 이후의 원시 데이터를 페이징 조건에 맞춰 조회하고 @@ -51,6 +57,25 @@ public String getEsIndexName() { return ""; } + + /** + * 주어진 Pageable 정보에 따라 DB에서 한 페이지 분량의 GovDrugEntity를 조회하고, + * 각 엔티티를 도메인 모델(GovDrug)로 변환하여 Page 형태로 반환합니다. + * + * @param pageable 조회할 페이지 번호와 크기를 포함하는 Pageable 객체 + * @return 페이지 단위로 변환된 GovDrug 도메인 객체의 Page + * @author 박찬병 + * @since 2025-04-24 + * @modified 2025-04-25 + * + */ + @Override + public Page findAllDrugs(Pageable pageable) { + log("findAllDrugs() 요청 수신"); + return drugJpaRepository.findAll(pageable) + .map(DrugRawDataMapper::toDomainFromEntity); + } + /** * lastSeq가 null일 경우 0으로 치환하여 조회 시작점을 결정합니다. * diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java new file mode 100644 index 0000000..7d6c8c1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository; + +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface DrugSymptomRepository extends ElasticsearchRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java new file mode 100644 index 0000000..0b91997 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java @@ -0,0 +1,40 @@ +package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document; + + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.CompletionField; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Document(indexName = "eedoc") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DrugSymptomDocument { + + @Id + @Field(type = FieldType.Keyword, name = "ITEM_SEQ") + private Long drugId; + + @Field(type = FieldType.Text, name = "ITEM_NAME") + private String drugName; + + @Field(type = FieldType.Text, name = "company") + private String company; + + @Field(type = FieldType.Text, name = "efficacy") + private List efficacy; + + @Field(type = FieldType.Keyword, name = "imageUrl") + private String imageUrl; + + @CompletionField(analyzer = "symptom_autocomplete") + private List symptomSuggester; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java index a11f4d4..b8b851a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java @@ -2,7 +2,9 @@ import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; +import com.likelion.backendplus4.yakplus.response.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -35,4 +37,19 @@ public void index(@RequestBody IndexRequest request) { log("index 컨트롤러 요청 수신" + request.toString()); indexUseCase.index(request); } + + /** + * 색인 배치 작업을 실행하여, DB로부터 조회한 약품 증상 데이터를 Elasticsearch에 일괄 색인합니다. + * + * @return 색인 작업 성공 여부 응답 (Void) + * @author 박찬병 + * @since 2025-04-24 + * @modified 2025-04-25 + */ + @PostMapping("/symptom") + public ResponseEntity> triggerIndex() { + log("indexSymptom 요청 수신"); + indexUseCase.indexSymptom(); + return ApiResponse.success(); + } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java new file mode 100644 index 0000000..1286532 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java @@ -0,0 +1,43 @@ +package com.likelion.backendplus4.yakplus.index.support.mapper; + +import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; +import com.likelion.backendplus4.yakplus.index.support.parser.SymptomTextParser; +import java.util.List; + +/** + * 증상 관련 Document를 다루는 매퍼 클래스입니다. + * + * @author 박찬병 + * @since 2025-04-25 + * @modified 2025-04-25 + */ +public class SymptomMapper { + + /** + * 주어진 GovDrug 도메인 객체를 기반으로 ES 색인용 DrugSymptomDocument로 변환합니다. 내부에서 JSON 파싱 및 전처리 로직을 실행하며, 파싱 + * 실패 시 ScraperException을 던집니다. + * + * @param entity 변환 대상 GovDrug 도메인 객체 + * @return 변환된 DrugSymptomDocument 객체 + * @author 박찬병 + * @modified 2025-04-25 + * @since 2025-04-25 + */ + public static DrugSymptomDocument toDocument(Drug entity) { + // 1) 추출된 텍스트 리스트를 단일 문자열로 전처리 + String flatText = SymptomTextParser.flattenLines(entity.getEfficacy()); + // 2) 전처리된 문자열을 자동완성용 토큰 리스트로 변환 + List suggestTokens = SymptomTextParser.tokenizeForSuggestion(flatText); + + return DrugSymptomDocument.builder() + .drugId(entity.getDrugId()) + .drugName(entity.getDrugName()) + .efficacy(suggestTokens) + .imageUrl(entity.getImageUrl()) + .company(entity.getCompany()) + .symptomSuggester(suggestTokens) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java new file mode 100644 index 0000000..83a02fb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java @@ -0,0 +1,106 @@ +package com.likelion.backendplus4.yakplus.index.support.parser; + + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 최상위 JSON 배열에서 각 요소(문자열)을 추출하고, + * 정규식을 이용해 HTML 태그 제거, HTML 엔티티(예: •, •) 디코딩,   등을 제거한 뒤 + * 깨끗한 텍스트 리스트를 반환하는 유틸리티 클래스입니다. + * + *

예시 JSON: ["첫번째 텍스트•", "텍스트 예시"]

+ *

→ 리턴: ["첫번째 텍스트•", "텍스트 예시"]

+ * + * @author 박찬병 + * @since 2025-04-27 + * @modified 2025-04-27 + */ +public class JsonArrayTextParser { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + // HTML 태그 제거용 정규식 + private static final Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); + // 10진수 HTML 엔티티 디코딩용 정규식 (예: •) + private static final Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); + // 16진수 HTML 엔티티 디코딩용 정규식 (예: •) + private static final Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); + + /** + * JSON 문자열 최상위가 배열일 때, 각 요소를 텍스트로 파싱하고 HTML 태그, HTML 엔티티,   등을 제거하여 리스트로 반환합니다. + * + * @param json JSON 배열 형태의 문자열 + * @return 정제된 텍스트 리스트 + * @throws IOException JSON 파싱 실패 시 발생 + */ + public static List extractAndClean(String json) throws IOException { + JsonNode root = objectMapper.readTree(json); + List texts = new ArrayList<>(); + + if (!root.isArray()) { + return texts; + } + + for (JsonNode element : root) { + if (element.isTextual()) { + String raw = element.asText().trim(); + if (raw.isEmpty()) { + continue; + } + + // 1) HTML 태그 제거 + String noHtml = TAG_REGEX.matcher(raw).replaceAll(""); + // 2)   등을 일반 공백으로 치환 + String withSpaces = noHtml.replaceAll(" ", " "); + // 3) HTML 엔티티 디코딩 (10진수 및 16진수) + String decoded = decodeHtmlEntities(withSpaces); + // 4) 최종 트리밍 + String clean = decoded.trim(); + + if (!clean.isEmpty()) { + texts.add(clean); + } + } + } + return texts; + } + + /** + * HTML 엔티티(10진수 &#DDD; 및 16진수 &#xHHHH;)를 대응하는 문자로 디코딩합니다. 예: "foo•bar" → "foo•bar", + * "foo•bar" → "foo•bar" + * + * @param input 엔티티를 포함한 문자열 + * @return 디코딩된 문자열 + */ + private static String decodeHtmlEntities(String input) { + String result = input; + + // 10진수 엔티티 디코딩 + Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(result); + StringBuffer sb = new StringBuffer(); + while (decMatcher.find()) { + int code = Integer.parseInt(decMatcher.group(1)); + decMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + decMatcher.appendTail(sb); + result = sb.toString(); + + // 16진수 엔티티 디코딩 + Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(result); + sb = new StringBuffer(); + while (hexMatcher.find()) { + int code = Integer.parseInt(hexMatcher.group(1), 16); + hexMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + hexMatcher.appendTail(sb); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java new file mode 100644 index 0000000..0bd06aa --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java @@ -0,0 +1,61 @@ +package com.likelion.backendplus4.yakplus.index.support.parser; + + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 증상 텍스트 전처리를 위한 유틸리티 클래스입니다. + * - 번호, 헤더, 기호를 제거하여 단일 문자열로 결합하는 기능 + * - 키워드 자동완성을 위한 토큰 생성 기능 + * + * @author 박찬병 + * @since 2025-04-24 + * @modified 2025-04-25 + */ + +public class SymptomTextParser { + + /** + * 주어진 문자열 목록에서 번호(“1.”), 헤더(“효능효과”), 기호(“○•▶”)를 제거하고 하나의 문자열로 결합합니다. + * + * @param raws 원본 문자열 리스트 (각 줄 단위) + * @return 전처리 후 결합된 단일 문자열 + * @author 박찬병 + * @modified 2025-04-25 + * @since 2025-04-24 + */ + public static String flattenLines(List raws) { + // 각 줄에서 번호·헤더·기호를 제거 + return raws.stream() + .map(line -> line.replaceAll("^\\d+\\.\\s*|효능효과|[○•▶]", " ")) + .collect(Collectors.joining(" ")); + } + + /** + * 전처리된 텍스트를 토큰으로 분리하고, 불용어 및 조사를 제거하여 자동완성용 키워드 리스트를 생성합니다. + * + * @param text 전처리된 문자열 + * @return 자동완성용 키워드 리스트 + * @author 박찬병 + * @modified 2025-04-25 + * @since 2025-04-24 + */ + public static List tokenizeForSuggestion(String text) { + // 구분자(쉼표, 구두점, 공백 등)로 텍스트 분할 + return Arrays.stream(text.split("[,·/;:\\s()\\[\\]]+")) + .map(String::trim) + // 최소 2자 이상인 토큰만 유지 + .filter(tok -> tok.length() >= 2) + // 불용어 필터링 + .filter(tok -> !Set.of("특히", "등의", "또는", "및", "의한", "다음", "보급", "에너지") + .contains(tok)) + // 조사(의, 에, 으로 등) 제거 + .map(tok -> tok.replaceAll("(?.+?)(?:의|에|으로|에서|시의|로|가)$", "${base}")) + // 중복 키워드 제거 + .distinct() + .toList(); + } +} \ No newline at end of file From f26f44def34f2cbfb909387b4bb0932e36f6d445 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:39:31 +0900 Subject: [PATCH 16/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Refactor:=20?= =?UTF-8?q?=EC=83=89=EC=9D=B8=ED=8F=AC=ED=8A=B8=20=ED=86=B5=ED=95=A9=20(#3?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/mapper/DrugRawDataMapper.java | 2 +- .../port/out/DrugIndexRepositoryPort.java | 2 +- .../port/out/EmbeddingLoadingPort.java | 2 ++ .../port/out/GovDrugRawDataPort.java | 4 ++- .../application/service/DrugIndexer.java | 2 +- .../yakplus/index/domain/model/Drug.java | 30 ------------------ .../persistence/ElasticsearchDrugAdapter.java | 2 +- .../persistence/GovDrugRawDataAdapter.java | 31 +++++++++++++++++-- .../GptEmbeddingLoadingAdapter.java | 26 ++++++++++++++-- .../KmBertEmbeddingLoadingAdapter.java | 25 +++++++++++++-- .../KrSBertEmbeddingLoadingAdapter.java | 25 +++++++++++++-- .../index/support/mapper/SymptomMapper.java | 2 +- 12 files changed, 107 insertions(+), 46 deletions(-) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java index 9c6a26f..685aa3b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java @@ -5,7 +5,7 @@ import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.support.parser.JsonArrayTextParser; import java.io.IOException; import java.util.List; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java index 6b368ad..f3d2f98 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java @@ -1,6 +1,6 @@ package com.likelion.backendplus4.yakplus.index.application.port.out; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import java.util.List; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java index f987af5..d9a8542 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java @@ -1,9 +1,11 @@ package com.likelion.backendplus4.yakplus.index.application.port.out; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import org.springframework.data.domain.Pageable; import java.util.List; public interface EmbeddingLoadingPort { List loadAllEmbeddings(); + List loadEmbeddingsByPage(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java index 226ed60..45668f8 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java @@ -1,6 +1,6 @@ package com.likelion.backendplus4.yakplus.index.application.port.out; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -12,4 +12,6 @@ public interface GovDrugRawDataPort { String getEsIndexName(); Page findAllDrugs(Pageable pageable); + + List fetchRawDataInt(int pageNo); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index ec6b578..c41478c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -3,7 +3,7 @@ import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java deleted file mode 100644 index d6bd5c2..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/domain/model/Drug.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.domain.model; - -import lombok.*; - -import java.time.LocalDate; -import java.util.List; -import java.util.Map; - -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Drug { - private Long drugId; - private String drugName; - private String company; - private List efficacy; - private float[] vector; - private LocalDate permitDate; - private boolean isGeneral; - private List materialInfo; - private String storeMethod; - private String validTerm; - private List usage; - private Map> precaution; - private String imageUrl; -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java index a620e48..2d87ec6 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.DrugSymptomRepository; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index ade8dd7..b03cc06 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -6,13 +6,18 @@ import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugRawDataMapper; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import java.util.List; @@ -31,6 +36,14 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { private final GovDrugDetailJpaRepository rawDataJpaRepository; private final GovDrugJpaRepository drugJpaRepository; + // 추후 수정... -> 임베딩 받아오는 방식 + List 변환 통일 + @Qualifier("gptAdapter") + private final EmbeddingLoadingPort embeddingLoadingPort; + + @Value("${gov.numOfRows}") + private int numOfRows; + + /** * 마지막으로 처리된 시퀀스 이후의 원시 데이터를 페이징 조건에 맞춰 조회하고 * Drug 도메인 리스트로 변환하여 반환합니다. @@ -47,13 +60,11 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { public List fetchRawData(Long lastSeq, Pageable pageable) { long startSeq = getStartSeq(lastSeq); List govDrugRawDataEntities = getGovDrugRawDataEntities(startSeq, pageable); - return convertToDrugDomains(govDrugRawDataEntities); } @Override public String getEsIndexName() { - //TODO 구현 필요 return ""; } @@ -144,4 +155,18 @@ private Drug mapToDrugDomain(DrugDetailEntity entity) { // .efficacy(entity.get()) .build(); } + + + @Override + public List fetchRawDataInt(int pageNo) { + log("index 서비스 요청 수신"); + Pageable pageable = createPageable(pageNo); + List drugs = embeddingLoadingPort.loadEmbeddingsByPage(pageable); + return drugs; + } + + private Pageable createPageable(int pageNo) { + log("pageable 생성"); + return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.DESC, "drugId")); + } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index 87498d7..aed8f67 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -7,7 +7,7 @@ import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.util.ArrayList; @@ -15,9 +15,8 @@ import java.util.List; import java.util.Map; -@Repository +@Repository("gptAdapter") @RequiredArgsConstructor -@Qualifier("gptAdapter") public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; private final GovDrugJpaRepository govDrugJpaRepository; @@ -44,6 +43,27 @@ public List loadAllEmbeddings() { return drugs; } + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); + List drugGptEmbedEntities = govDrugGptEmbedJpaRepository.findAll(pageable).getContent(); + + // drugGptEmbedEntities를 Map으로 변환 (key: drugId) + Map gptEmbedMap = new HashMap<>(); + for (DrugGptEmbedEntity embed : drugGptEmbedEntities) { + gptEmbedMap.put(embed.getDrugId(), embed); + } + + List drugs = new ArrayList<>(); + for (DrugRawDataEntity drugRawData : rawDataEntities) { + DrugGptEmbedEntity embed = gptEmbedMap.get(drugRawData.getDrugId()); + Drug drug = toDomainFromEntity(drugRawData, embed); + drugs.add(drug); + } + + return drugs; + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbedEntity embedEntity) { return Drug.builder() .drugId(drugEntity.getDrugId()) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java index e189aee..87eb4a4 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java @@ -8,6 +8,7 @@ import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.util.ArrayList; @@ -15,9 +16,8 @@ import java.util.List; import java.util.Map; -@Repository +@Repository("kmBertAdapter") @RequiredArgsConstructor -@Qualifier("kmBertAdapter") public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; private final GovDrugJpaRepository govDrugJpaRepository; @@ -44,6 +44,27 @@ public List loadAllEmbeddings() { return drugs; } + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); + List drugKmBertEmbedEntities = govDrugKmBertEmbedJpaRepository.findAll(pageable).getContent(); + + // drugKmBertEmbedEntities를 Map으로 변환 (key: drugId) + Map kmBertEmbedMap = new HashMap<>(); + for (DrugKmBertEmbedEntity embed : drugKmBertEmbedEntities) { + kmBertEmbedMap.put(embed.getDrugId(), embed); + } + + List drugs = new ArrayList<>(); + for (DrugRawDataEntity drugRawData : rawDataEntities) { + DrugKmBertEmbedEntity embed = kmBertEmbedMap.get(drugRawData.getDrugId()); + Drug drug = toDomainFromEntity(drugRawData, embed); + drugs.add(drug); + } + + return drugs; + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKmBertEmbedEntity embedEntity) { return Drug.builder() .drugId(drugEntity.getDrugId()) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java index 8bf0c75..7bb6ab1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java @@ -8,6 +8,7 @@ import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.util.ArrayList; @@ -15,9 +16,8 @@ import java.util.List; import java.util.Map; -@Repository +@Repository("krSBertAdapter") @RequiredArgsConstructor -@Qualifier("krSBertAdapter") public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; private final GovDrugJpaRepository govDrugJpaRepository; @@ -44,6 +44,27 @@ public List loadAllEmbeddings() { return drugs; } + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); + List drugKrSBertEmbedEntities = govDrugKrSbertEmbedJpaRepository.findAll(pageable).getContent(); + + // drugKrSBertEmbedEntities를 Map으로 변환 (key: drugId) + Map krSBertEmbedMap = new HashMap<>(); + for (DrugKrSbertEmbedEntity embed : drugKrSBertEmbedEntities) { + krSBertEmbedMap.put(embed.getDrugId(), embed); + } + + List drugs = new ArrayList<>(); + for (DrugRawDataEntity drugRawData : rawDataEntities) { + DrugKrSbertEmbedEntity embed = krSBertEmbedMap.get(drugRawData.getDrugId()); + Drug drug = toDomainFromEntity(drugRawData, embed); + drugs.add(drug); + } + + return drugs; + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKrSbertEmbedEntity embedEntity) { return Drug.builder() .drugId(drugEntity.getDrugId()) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java index 1286532..309994a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java @@ -1,6 +1,6 @@ package com.likelion.backendplus4.yakplus.index.support.mapper; -import com.likelion.backendplus4.yakplus.index.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; import com.likelion.backendplus4.yakplus.index.support.parser.SymptomTextParser; import java.util.List; From 284a3c499a2bd3064be3e5c7145dbdca058cd0ff Mon Sep 17 00:00:00 2001 From: JUNG ANSIK Date: Mon, 28 Apr 2025 19:03:49 +0900 Subject: [PATCH 17/47] =?UTF-8?q?=E2=9C=A8=20Feature/#40=20=EC=9E=84?= =?UTF-8?q?=EB=B2=A0=EB=94=A9=20=EB=AA=A8=EB=8D=B8=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/DrugEmbedRepositoryAdapter.java | 21 ------------ .../application/port/in/IndexUseCase.java | 4 +-- .../port/out/GovDrugRawDataPort.java | 5 ++- .../application/service/DrugIndexer.java | 34 ++++++++++--------- .../persistence/GovDrugRawDataAdapter.java | 22 +----------- .../GptEmbeddingLoadingAdapter.java | 2 ++ .../controller/DrugController.java | 16 ++++----- .../controller/dto/request/IndexRequest.java | 4 ++- src/main/resources/application.yml | 2 +- 9 files changed, 35 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java index d57e814..d40b69c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java @@ -48,27 +48,6 @@ public void saveKrSbertEmbed(Long drugId, float[] krSbertVector){ ); } - @Override - public float[] getGptVector(Long drugId){ - return getVectorFromRepository( - drugId, gptRepository, e -> e.getGptVector() - ); - } - - @Override - public float[] getKmBertVector(Long drugId){ - return getVectorFromRepository( - drugId, kmBertRepository, e -> e.getKmBertVector() - ); - } - - @Override - public float[] getKrSbertVector(Long drugId){ - return getVectorFromRepository( - drugId, krSbertRepository, e -> e.getKrSbertVector() - ); - } - private T buildEmbedEntity(Long drugId, float[] vector, Class clazz) { try { String vectorString = toStringFromFloatArray(vector); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java index d9e5407..0df4497 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java @@ -1,9 +1,7 @@ package com.likelion.backendplus4.yakplus.index.application.port.in; -import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; - public interface IndexUseCase { - void index(IndexRequest request); + void index(); void indexSymptom(); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java index 45668f8..04a264a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java @@ -2,16 +2,15 @@ import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.util.List; public interface GovDrugRawDataPort { - List fetchRawData(Long lastSeq, Pageable pageable); + List fetchRawData(int i); + String getEsIndexName(); Page findAllDrugs(Pageable pageable); - List fetchRawDataInt(int pageNo); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index c41478c..ee3353d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -1,10 +1,9 @@ package com.likelion.backendplus4.yakplus.index.application.service; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -19,9 +18,10 @@ /** * 약품 색인(인덱싱) 작업을 수행하는 서비스 구현체 * - * @modified 2025-04-27 + * @modified 2025-04-28 * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 * - itemSeq -> drugId로 수정 + * - 페이징 처리 로직 수정 * @since 2025-04-22 */ @Service @@ -36,19 +36,21 @@ public class DrugIndexer implements IndexUseCase { * 요청으로 전달된 lastSeq, limit 정보를 바탕으로 RDB에서 데이터를 조회하고 * ES 인덱스에 저장한다. * - * @param request 색인 기준 및 개수 정보 * @author 정안식 - * @modified 2025-04-27 + * @modified 2025-04-28 * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 + * 25.04.28 - IndexRequest를 인자로 더 이상 받지 않도록 수정 * @since 2025-04-22 */ @Override - public void index(IndexRequest request) { + public void index() { log("index 서비스 요청 수신"); - Pageable pageable = createPageable(request.limit()); - List drugs = fetchRawData(request, pageable); - String esIndexName = getEsIndexName(); - saveDrugs(esIndexName, drugs); +// Pageable pageable = createPageable(request.limit()); + for (int i = 1; i <= 50; i++) { + List drugs = fetchRawData(i); + String esIndexName = getEsIndexName(); + saveDrugs(esIndexName, drugs); + } } /** @@ -56,8 +58,8 @@ public void index(IndexRequest request) { * 각 페이지는 CHUNK_SIZE만큼 처리되며, 모든 데이터를 순차적으로 색인합니다. * * @author 박찬병 - * @since 2025-04-24 * @modified 2025-04-25 + * @since 2025-04-24 */ @Override public void indexSymptom() { @@ -100,16 +102,16 @@ private Pageable createPageable(int limit) { /** * RDB에서 lastSeq 이후의 원시 데이터를 조회하여 도메인 객체로 변환한다. * - * @param request 색인 기준 정보 - * @param pageable 페이징 및 정렬 정보 + * @param i for문 내부에서 동작하는 i값(pageable의 pageNumber) * @return 도메인 모델 리스트 * @author 정안식 - * @modified 2025-04-24 + * @modified 2025-04-28 + * 25.04.28 - 페이징 처리 로직 수정 * @since 2025-04-22 */ - private List fetchRawData(IndexRequest request, Pageable pageable) { + private List fetchRawData(int i) { log("RDB에서 원시 데이터 조회"); - return govDrugRawDataPort.fetchRawData(request.lastDrugId(), pageable); + return govDrugRawDataPort.fetchRawData(i); } /** diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index b03cc06..16bcac9 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -43,26 +43,6 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { @Value("${gov.numOfRows}") private int numOfRows; - - /** - * 마지막으로 처리된 시퀀스 이후의 원시 데이터를 페이징 조건에 맞춰 조회하고 - * Drug 도메인 리스트로 변환하여 반환합니다. - * - * @param lastSeq 마지막 처리 시퀀스 (null이면 0부터 조회) - * @param pageable 페이징 및 정렬 정보 - * @return Drug 도메인 객체 리스트 - * @throws IndexException 데이터베이스 조회 실패 시 발생 - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - @Override - public List fetchRawData(Long lastSeq, Pageable pageable) { - long startSeq = getStartSeq(lastSeq); - List govDrugRawDataEntities = getGovDrugRawDataEntities(startSeq, pageable); - return convertToDrugDomains(govDrugRawDataEntities); - } - @Override public String getEsIndexName() { return ""; @@ -158,7 +138,7 @@ private Drug mapToDrugDomain(DrugDetailEntity entity) { @Override - public List fetchRawDataInt(int pageNo) { + public List fetchRawData(int pageNo) { log("index 서비스 요청 수신"); Pageable pageable = createPageable(pageNo); List drugs = embeddingLoadingPort.loadEmbeddingsByPage(pageable); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index aed8f67..05969ce 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -7,6 +7,7 @@ import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @@ -16,6 +17,7 @@ import java.util.Map; @Repository("gptAdapter") +@Primary @RequiredArgsConstructor public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java index b8b851a..8c3230d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java @@ -1,12 +1,10 @@ package com.likelion.backendplus4.yakplus.index.presentation.controller; import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; -import com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request.IndexRequest; import com.likelion.backendplus4.yakplus.response.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -15,7 +13,7 @@ /** * 약품 인덱싱 API 엔드포인트를 제공하는 컨트롤러 클래스 * - * @modified 2025-04-25 + * @modified 2025-04-28 * @since 2025-04-22 */ @RestController @@ -27,15 +25,15 @@ public class DrugController { /** * 색인 생성 요청을 처리한다. * - * @param request 인덱싱 범위 및 개수 정보를 담은 요청 객체 * @author 정안식 - * @modified 2025-04-24 + * @modified 2025-04-28 + * 25.04.28 - IndexUseRequest를 인자에서 제거하였습니다.(추후 임베딩 모델 선택 로직 추가시 변경예정) * @since 2025-04-22 */ @PostMapping("/save") - public void index(@RequestBody IndexRequest request) { - log("index 컨트롤러 요청 수신" + request.toString()); - indexUseCase.index(request); + public void index() { + log("컨트롤러 indexAll 요청 수신"); + indexUseCase.index(); } /** @@ -43,8 +41,8 @@ public void index(@RequestBody IndexRequest request) { * * @return 색인 작업 성공 여부 응답 (Void) * @author 박찬병 - * @since 2025-04-24 * @modified 2025-04-25 + * @since 2025-04-24 */ @PostMapping("/symptom") public ResponseEntity> triggerIndex() { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java index 5f6985a..346135c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java @@ -4,8 +4,10 @@ * 인덱싱 요청 정보 DTO * * @since 2025-04-22 - * @modified 2025-04-27 + * @modified 2025-04-28 * 25.04.27 - itemSeq -> drugId로 수정 + * - 페이징 처리 로직 수정 + * 25.04.28 - DrugController에서 사용하지 않도록 수정(추후 필요한 필드로 변경하여 사용할 예정) */ public record IndexRequest( Long lastDrugId, diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3cf3445..36e22a4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: jpa: database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: create + ddl-auto: none # ddl-auto: create # show-sql: true properties: From 4d46af60f62b0bfbfe2d24ba022049646c32bef2 Mon Sep 17 00:00:00 2001 From: thelightway Date: Mon, 28 Apr 2025 19:06:09 +0900 Subject: [PATCH 18/47] =?UTF-8?q?=F0=9F=93=A6=20Chore:=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=B0=ED=8F=AC=20=EC=BD=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/support/ApiPageCounter.java | 10 ++++---- .../api/support/ApiResponseMapper.java | 23 +++++++++---------- src/main/resources/application.yml | 2 +- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java index a7586ca..12a4dd1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java @@ -23,15 +23,13 @@ public ApiPageCounter(ApiUriCompBuilder uriBuilder, } public int getDetailApiTotalPageCount() { - return 4; - // URI uri = uriBuilder.getUriForDetailApiShort(); - // return getPageCountFromUri(uri); + URI uri = uriBuilder.getUriForDetailApiShort(); + return getPageCountFromUri(uri); } public int getImgApiTotalPageCount() { - return 4; - // URI uri = uriBuilder.getUriForImgApiShort(); - // return getPageCountFromUri(uri); + URI uri = uriBuilder.getUriForImgApiShort(); + return getPageCountFromUri(uri); } private int getPageCountFromUri(URI uri) { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java index 4c81f8c..b155c9d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java @@ -23,18 +23,17 @@ public static JsonNode getItemsFromResponse(String response) { } public static int getTotalCountFromResponse(String response) { - return 20; - // log.info("응답에서 데이터 사이즈 추출"); - // try { - // return new ObjectMapper().readTree(response) - // .path("body") - // .path("totalCount") - // .asInt(); - // } catch (JsonProcessingException e) { - // log.error("totalCount 추출 실패"); - // //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 - // throw new RuntimeException(e); - // } + log.info("응답에서 데이터 사이즈 추출"); + try { + return new ObjectMapper().readTree(response) + .path("body") + .path("totalCount") + .asInt(); + } catch (JsonProcessingException e) { + log.error("totalCount 추출 실패"); + //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 + throw new RuntimeException(e); + } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 36e22a4..0557408 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,7 +27,7 @@ logging: gov: host: apis.data.go.kr serviceKey: ${GOV_SERVICE_KEY} - numOfRows: 5 + numOfRows: 100 path: detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnDtlInq05 img: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnInq06 From 747a55f18076a1dffa59c94a18a6b52d6cf21fab Mon Sep 17 00:00:00 2001 From: thelightway Date: Mon, 28 Apr 2025 21:48:50 +0900 Subject: [PATCH 19/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=EC=97=90=EC=84=9C=20items=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EC=8B=A4=ED=8C=A8=ED=95=B4=EB=8F=84=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drug/infrastructure/api/support/ApiResponseMapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java index b155c9d..b5174ef 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java @@ -17,8 +17,8 @@ public static JsonNode getItemsFromResponse(String response) { .path("items"); } catch (JsonProcessingException e) { log.error("items 추출 실패"); - //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 - throw new RuntimeException(e); + log.error("response: {}", response); + return null; } } From a1b5e07426c763ef479c1cc14239b537b650495a Mon Sep 17 00:00:00 2001 From: thelightway Date: Mon, 28 Apr 2025 21:57:20 +0900 Subject: [PATCH 20/47] =?UTF-8?q?=F0=9F=93=A6=20Chore:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EB=A1=9C=EA=B7=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scraper/detail/DrugDetailScraperService.java | 11 +++++------ .../scraper/detail/support/MaterialParser.java | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java index 124d698..8ca21d8 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java @@ -70,12 +70,11 @@ public void requestAllData() { drugDetailRepository.saveDrugDetailBulk(drugs); - log(LogLevel.DEBUG, - "Page: " + i - + "received: " + drugs.size() - + "saved (unique): " + uniqueItems - + "totalReceived: " + receivedCount - + "totalUniqueSaved: " + savedCountWithoutDuplicates + log("Page: " + i + + "received: " + drugs.size() + + "saved (unique): " + uniqueItems + + "totalReceived: " + receivedCount + + "totalUniqueSaved: " + savedCountWithoutDuplicates ); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java index 8583b5a..4994015 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java @@ -24,13 +24,11 @@ public class MaterialParser { * @since 2025-04-21 */ public static String parseMaterial(String raw) { - log("약품 성분 파싱 시작"); ObjectMapper objectMapper = new ObjectMapper(); ArrayNode resultArray = objectMapper.createArrayNode(); String[] blocks = splitBlock(raw); parsingblocksAndPutArrayItem(blocks, resultArray); String result = convertString(objectMapper, resultArray); - log("약품 성분 파싱 완료"); return result; } From eecc5a6e65d29012e224e7d0f1229220ac9b7300 Mon Sep 17 00:00:00 2001 From: thelightway Date: Tue, 29 Apr 2025 00:06:47 +0900 Subject: [PATCH 21/47] =?UTF-8?q?=F0=9F=93=A6=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DrugDataTestController.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index 7301ddb..e89f8bf 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -6,6 +6,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; import com.likelion.backendplus4.yakplus.drug.application.service.scraper.DrugScraper; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; @@ -17,16 +19,9 @@ @RestController @RequiredArgsConstructor public class DrugDataTestController { - private final EmbeddingPort embeddingPort; private final DrugScraper scraperUseCase; - - @GetMapping("/test/embed") - public ResponseEntity> getEmbedData(){ - log("getEmbedData"); - float[] embedding = embeddingPort.getEmbedding("test", EmbeddingModelType.OPENAI); - return ApiResponse.success(embedding); - - } + private final DrugCombineUsecase drugCombineUsecase; + private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; @GetMapping("/test/parse") public ResponseEntity saveAPIData(){ @@ -34,4 +29,15 @@ public ResponseEntity saveAPIData(){ return ResponseEntity.ok().build(); } + @GetMapping("/test/combine") + public ResponseEntity saveCombineData(){ + drugCombineUsecase.mergeTable(); + return ResponseEntity.ok().build(); + } + + @GetMapping("/test/embed") + public ResponseEntity saveEmbedData(){ + drugEmbedProcessorUseCase.startEmbedding(); + return ResponseEntity.ok().build(); + } } From ea7e929585a1cb8db31d4aaac9458b3e05cacfd1 Mon Sep 17 00:00:00 2001 From: thelightway Date: Tue, 29 Apr 2025 05:32:17 +0900 Subject: [PATCH 22/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/scraper/combiner/DrugCombiner.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java index f1191f3..493d970 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java @@ -29,8 +29,7 @@ public class DrugCombiner implements DrugCombineUsecase { private final DrugDetailRepositoryPort drugDetailRepository; private final DrugImageRepositoryPort drugImageRepositoryPort; private final DrugRawDataRepositoryPort drugRawDataRepositoryPort; - - @Transactional + @Override public void mergeTable(){ log("API 요청 결과 테이블 병합 시작: 상세 정보 + 이미지"); From 5cbb4db4f7760f4649e1c8f2e97e76162108f3ef Mon Sep 17 00:00:00 2001 From: thelightway Date: Tue, 29 Apr 2025 05:41:29 +0900 Subject: [PATCH 23/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A6=AC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/out/DrugDetailRepositoryPort.java | 5 +++ .../scraper/combiner/DrugCombiner.java | 31 ++++++++++++++----- .../out/DrugDetailRepositoryAdapter.java | 8 +++++ .../out/DrugRawDataRepositoryAdapter.java | 3 ++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java index 438dc8e..d34ee51 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java @@ -2,6 +2,9 @@ import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; @@ -38,4 +41,6 @@ public interface DrugDetailRepositoryPort { * @since 2025-04-21 */ List getAllGovDrugDetail(); + + Page getGovDrugDetailByPage(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java index 493d970..2ac377b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java @@ -2,6 +2,8 @@ import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Component; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; @@ -29,20 +31,33 @@ public class DrugCombiner implements DrugCombineUsecase { private final DrugDetailRepositoryPort drugDetailRepository; private final DrugImageRepositoryPort drugImageRepositoryPort; private final DrugRawDataRepositoryPort drugRawDataRepositoryPort; - - @Override - public void mergeTable(){ + + public void mergeTable() { log("API 요청 결과 테이블 병합 시작: 상세 정보 + 이미지"); - List drugDetails = drugDetailRepository.getAllGovDrugDetail(); - log(LogLevel.DEBUG, "DrugDetail Raw Data: \n" + drugDetails); + int pageSize = 1_000; + Page firstPage = drugDetailRepository.getGovDrugDetailByPage(PageRequest.of(0, pageSize)); + int totalPages = firstPage.getTotalPages(); + + processDrugDetails(firstPage.getContent(), 1); + + for (int i = 1; i < totalPages; i++) { + Page page = drugDetailRepository.getGovDrugDetailByPage(PageRequest.of(i, pageSize)); + processDrugDetails(page.getContent(), i + 1); + } - drugDetails.stream() - .map(detail -> buildMergeRawData(detail, getImageDataByDrugDetail(detail))) - .forEach(drugRawDataRepositoryPort::save); log("API 요청 결과 테이블 병합 완료"); } + private void processDrugDetails(List drugDetails, int pageNumber) { + log("Processing Page = " + pageNumber); + log(LogLevel.DEBUG, "DrugDetail Raw Data (Page " + pageNumber + "): \n" + drugDetails); + + List rawData = drugDetails.stream() + .map(detail -> buildMergeRawData(detail, getImageDataByDrugDetail(detail))).toList(); + drugRawDataRepositoryPort.saveAll(rawData); + } + /** * 주어진 의약품 상세 정보에 해당하는 이미지 정보를 조회합니다. * diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java index 2c1d6d2..a5deca1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; @@ -38,4 +40,10 @@ public List getAllGovDrugDetail(){ .map(DrugDetailMapper::toDomainFromEntity) .collect(Collectors.toList()); } + + @Override + public Page getGovDrugDetailByPage(Pageable pageable) { + return drugdetailJpaRepository.findAll(pageable) + .map(DrugDetailMapper::toDomainFromEntity); + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java index 6e66fea..0f6c645 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java @@ -10,6 +10,7 @@ import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugRawDataMapper; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @Component @@ -18,12 +19,14 @@ public class DrugRawDataRepositoryAdapter implements DrugRawDataRepositoryPort { private final GovDrugJpaRepository jpaDrugRepository; + @Transactional @Override public void save(DrugRawData drug){ DrugRawDataEntity entity = DrugRawDataMapper.toEntityFromDomain(drug); jpaDrugRepository.save(entity); } + @Transactional @Override public void saveAll(List rawData) { jpaDrugRepository.saveAll( From 11b281322feb0d57971d0a391b489c5b6bda5117 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:27:20 +0900 Subject: [PATCH 24/47] =?UTF-8?q?=F0=9F=93=A6=EF=B8=8F=20=20Chore:=20esInd?= =?UTF-8?q?ex=EB=AA=85=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/persistence/GovDrugRawDataAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index 16bcac9..e47124a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -45,7 +45,7 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { @Override public String getEsIndexName() { - return ""; + return "test-gpt"; } From 001e795d7b6fec93be95180f9fbd03dcaa2d4dea Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:51:46 +0900 Subject: [PATCH 25/47] =?UTF-8?q?=F0=9F=90=9B=20=20Bug:=20es=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8B=B1=20=EC=9D=B4=EB=AF=B8=EC=A7=80url=20null=20?= =?UTF-8?q?=EA=B0=92=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index/application/service/DrugIndexer.java | 14 ++++++++++---- .../persistence/ElasticsearchDrugAdapter.java | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index ee3353d..d9df062 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -1,5 +1,6 @@ package com.likelion.backendplus4.yakplus.index.application.service; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; @@ -46,11 +47,16 @@ public class DrugIndexer implements IndexUseCase { public void index() { log("index 서비스 요청 수신"); // Pageable pageable = createPageable(request.limit()); - for (int i = 1; i <= 50; i++) { - List drugs = fetchRawData(i); - String esIndexName = getEsIndexName(); - saveDrugs(esIndexName, drugs); + try { + for (int i = 1; i <= 50; i++) { + List drugs = fetchRawData(i); + String esIndexName = getEsIndexName(); + saveDrugs(esIndexName, drugs); + } + } catch (Exception e) { + log(LogLevel.ERROR,"indexing 시 데이터 5000개 보다 적어서 에러 발생 (데이터 5000개 이하면 전체 동작)", e); } + } /** diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java index 2d87ec6..74f5ee9 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java @@ -132,6 +132,6 @@ private Request createBulkRequest(String esIndexName, String bulkBody) { * @since 2025-04-22 */ private Map createDrugDocument(Drug drug) { - return Map.of("drugId", drug.getDrugId(), "drugName", drug.getDrugName(), "company", drug.getCompany(), "efficacy", drug.getEfficacy(), "imageUrl", drug.getImageUrl(), "vector", drug.getVector()); + return Map.of("drugId", drug.getDrugId(), "drugName", drug.getDrugName(), "company", drug.getCompany(), "efficacy", drug.getEfficacy(), "imageUrl", drug.getImageUrl() != null ? drug.getImageUrl() : "", "vector", drug.getVector()); } } \ No newline at end of file From 20a0755b1dbf75042cdecb1e6539f3ed4b132152 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:05:04 +0900 Subject: [PATCH 26/47] =?UTF-8?q?=F0=9F=90=9B=20=20Bug:=20pageable=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=200=20=EB=B6=80=ED=84=B0=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/persistence/GovDrugRawDataAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index e47124a..230601d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -147,6 +147,6 @@ public List fetchRawData(int pageNo) { private Pageable createPageable(int pageNo) { log("pageable 생성"); - return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.DESC, "drugId")); + return PageRequest.of(pageNo-1, numOfRows, Sort.by(Sort.Direction.DESC, "drugId")); } } \ No newline at end of file From 214dd2f9be30b6e5a165a12c96d063910e258044 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 29 Apr 2025 15:55:00 +0900 Subject: [PATCH 27/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=9E=84=EB=B2=A0=EB=94=A9=20DB=20=EC=A0=91=EA=B7=BC=20port=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: 임베딩 DB 접근 port 변경 * ♻️ Refactor: JPA findAll OrderBy 삭제 * ♻️ Refactor: JPA 테이블 타입 변경 --- .../scraper/detail/support/XMLParser.java | 74 +++++++++++++++++-- .../scraper/embed/DrugEmbedProcessor.java | 31 +++++--- .../out/DrugDetailRepositoryAdapter.java | 1 + .../repository/entity/DrugRawDataEntity.java | 6 +- 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java index 040d7a9..f42e18d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java @@ -6,6 +6,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -87,18 +89,25 @@ private static void parseParagraph(Element root, List allParagraph for (int i = 0; i < paraNodes.getLength(); i++) { Element paragraphElement = (Element) paraNodes.item(i); ParagraphTag paragraphTag = new ParagraphTag(); - paragraphTag.tagName = paragraphElement.getAttribute("tagName"); - paragraphTag.textIndent = paragraphElement.getAttribute("textIndent"); - paragraphTag.marginLeft = paragraphElement.getAttribute("marginLeft"); - paragraphTag.text = paragraphElement.getTextContent().trim(); + paragraphTag.tagName = cleanText(paragraphElement.getAttribute("tagName")); + paragraphTag.textIndent = cleanText(paragraphElement.getAttribute("textIndent")); + paragraphTag.marginLeft = cleanText(paragraphElement.getAttribute("marginLeft")); + paragraphTag.text = cleanText(paragraphElement.getTextContent().trim()); + + if(!isEmptytagNameOrTagText(paragraphTag)){ + allParagraphs.add(paragraphTag); + } - allParagraphs.add(paragraphTag); mapSectionFromArticle(articleMap, paragraphTag, paragraphElement); } } } + private static boolean isEmptytagNameOrTagText(ParagraphTag paragraphTag) { + return paragraphTag.tagName.isEmpty() || paragraphTag.text.isEmpty(); + } + /** * XML에서 ARTICLE 태그를 파싱하여 ArticleTag 리스트에 추가합니다. * @@ -115,7 +124,7 @@ private static void parseArticles(Element root, List allArticles, for (int i = 0; i < artNodes.getLength(); i++) { Element artElement = (Element) artNodes.item(i); ArticleTag articleTag = new ArticleTag(); - articleTag.title = artElement.getAttribute("title"); + articleTag.title = cleanText(artElement.getAttribute("title")); articleTag.paragraphs = new ArrayList<>(); allArticles.add(articleTag); @@ -155,7 +164,7 @@ private static void parseSesctions(Element root, List allSections, M for (int i = 0; i < secNodes.getLength(); i++) { Element secEl = (Element) secNodes.item(i); SectionTag secDto = new SectionTag(); - secDto.title = secEl.getAttribute("title"); + secDto.title = cleanText(secEl.getAttribute("title")); secDto.articles = new ArrayList<>(); allSections.add(secDto); @@ -202,7 +211,7 @@ private static class DocTag implements Tags { public List sections; DocTag(Element root, List sections) { - this.title = root.getAttribute("title"); + this.title = cleanText(root.getAttribute("title")); this.type = root.getAttribute("type"); this.sections = sections; } @@ -281,4 +290,53 @@ private interface Tags { void addTag(Tags tags); boolean equalsClass(Tags tags); } + + private static String cleanText(String text){ + Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); + String tempText = TAG_REGEX.matcher(text) + .replaceAll("") + .replaceAll(" ", " ") + .replaceAll("● ", "") + .replaceAll("○ ", "") + .replaceAll("∎ ", "") + .replaceAll("- ", ""); + String decodeText = decodeHtml(tempText).trim(); + return decodeText; + } + /** + * HTML 엔티티(10진수 &#DDD; 및 16진수 &#xHHHH;)를 대응하는 문자로 디코딩합니다. 예: "foo•bar" → "foo•bar", + * "foo•bar" → "foo•bar" + * + * @param input 엔티티를 포함한 문자열 + * @return 디코딩된 문자열 + * + * @author 박찬병 + */ + + private static String decodeHtml(String input) { + String result = input; + Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); + Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); + // 10진수 엔티티 디코딩 + Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(result); + StringBuffer sb = new StringBuffer(); + while (decMatcher.find()) { + int code = Integer.parseInt(decMatcher.group(1)); + decMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + decMatcher.appendTail(sb); + result = sb.toString(); + + // 16진수 엔티티 디코딩 + Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(result); + sb = new StringBuffer(); + while (hexMatcher.find()) { + int code = Integer.parseInt(hexMatcher.group(1), 16); + hexMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + hexMatcher.appendTail(sb); + return sb.toString(); + } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java index 9fe2795..63c1239 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java @@ -4,15 +4,19 @@ import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugEmbedRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; +import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; import lombok.RequiredArgsConstructor; @@ -27,20 +31,23 @@ @Service @RequiredArgsConstructor public class DrugEmbedProcessor implements DrugEmbedProcessorUseCase { - private final DrugDetailRepositoryPort detailRepositoryPort; + private final GovDrugRawDataPort drugRawDataPort; private final EmbeddingPort embeddingPort; private final DrugEmbedRepositoryPort embedRepositoryPort; @Override public void startEmbedding() { log("약품 효능 임베딩 작업 시작"); + Page firstPage = getAllItem(0); + for(int i=0; i { + String efficacy = convertSingleStringForEfficacy(data.getEfficacy()); + saveSbertVector(data, efficacy); + saveKmBertVector(data, efficacy); + saveGptVector(data, efficacy); + }); - getAllItem().forEach(detail -> { - String efficacy = convertSingleStringForEfficacy(detail.getEfficacy()); - saveSbertVector(detail, efficacy); - saveKmBertVector(detail, efficacy); - saveGptVector(detail, efficacy); - }); + } log("약품 효능 임베딩 작업 완료"); } @@ -53,7 +60,7 @@ public void startEmbedding() { * * @since 2025-04-25 */ - private void saveGptVector(DrugDetail detail, String text) { + private void saveGptVector(Drug detail, String text) { float[] openAIVector = embeddingPort.getEmbedding( text, EmbeddingModelType.OPENAI); embedRepositoryPort.saveGptEmbed(detail.getDrugId(), openAIVector); @@ -67,7 +74,7 @@ private void saveGptVector(DrugDetail detail, String text) { * * @since 2025-04-25 */ - private void saveKmBertVector(DrugDetail detail, String text) { + private void saveKmBertVector(Drug detail, String text) { float[] kmbertVector = embeddingPort.getEmbedding( text, EmbeddingModelType.KM_BERT); embedRepositoryPort.saveKmBertEmbed(detail.getDrugId(), kmbertVector); @@ -81,7 +88,7 @@ private void saveKmBertVector(DrugDetail detail, String text) { * * @since 2025-04-25 */ - private void saveSbertVector(DrugDetail detail, String text) { + private void saveSbertVector(Drug detail, String text) { float[] sbertVector = embeddingPort.getEmbedding( text, EmbeddingModelType.SBERT); embedRepositoryPort.saveKrSbertEmbed(detail.getDrugId(), sbertVector); @@ -115,7 +122,7 @@ private String convertSingleStringForEfficacy(List stringList) { * * @since 2025-04-25 */ - private List getAllItem() { - return detailRepositoryPort.getAllGovDrugDetail(); + private Page getAllItem(int i) { + return drugRawDataPort.findAllDrugs(PageRequest.of(i, 100)); } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java index a5deca1..781d41c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java index b525684..083b786 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java @@ -46,13 +46,13 @@ public class DrugRawDataEntity { @Column(name = "VALID_TERM") private String validTerm; - @Column(name = "EE_DOC_DATA", columnDefinition = "TEXT") + @Column(name = "EE_DOC_DATA", columnDefinition = "JSON") private String efficacy; - @Column(name = "UD_DOC_DATA", columnDefinition = "TEXT") + @Column(name = "UD_DOC_DATA", columnDefinition = "JSON") private String usage; - @Column(name = "NB_DOC_DATA", columnDefinition = "TEXT") + @Column(name = "NB_DOC_DATA", columnDefinition = "JSON") private String precaution; @Column(name= "IMG_URL") From d334d2c85e59a07de78fadb792e8d9de10dda27f Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:22:02 +0900 Subject: [PATCH 28/47] =?UTF-8?q?=F0=9F=93=A6=EF=B8=8F=20Chore:=20gptEmbed?= =?UTF-8?q?dingLoadingAdapter=20=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index/application/service/DrugIndexer.java | 3 ++- .../persistence/GptEmbeddingLoadingAdapter.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index d9df062..4129c0b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -54,7 +54,8 @@ public void index() { saveDrugs(esIndexName, drugs); } } catch (Exception e) { - log(LogLevel.ERROR,"indexing 시 데이터 5000개 보다 적어서 에러 발생 (데이터 5000개 이하면 전체 동작)", e); + log(LogLevel.ERROR,"indexing 시 데이터 5000개 보다 적어서 에러 발생", e); + e.printStackTrace(); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index 05969ce..82c561a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.Map; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + @Repository("gptAdapter") @Primary @RequiredArgsConstructor @@ -49,20 +51,29 @@ public List loadAllEmbeddings() { public List loadEmbeddingsByPage(Pageable pageable) { List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); List drugGptEmbedEntities = govDrugGptEmbedJpaRepository.findAll(pageable).getContent(); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 상세정보 수: " + rawDataEntities.size()); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 임베딩 벡터수: " + drugGptEmbedEntities.size()); // drugGptEmbedEntities를 Map으로 변환 (key: drugId) Map gptEmbedMap = new HashMap<>(); for (DrugGptEmbedEntity embed : drugGptEmbedEntities) { + log("loadEmbeddingsByPage - " +pageable.getPageNumber()+"페이지에서 임베딩 벡터 받아온 약품 ID: " + embed.getDrugId()); gptEmbedMap.put(embed.getDrugId(), embed); } List drugs = new ArrayList<>(); + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 시작"); for (DrugRawDataEntity drugRawData : rawDataEntities) { DrugGptEmbedEntity embed = gptEmbedMap.get(drugRawData.getDrugId()); + if(embed == null) { + log("loadEmbeddingsByPage - " + "Drug 도메인 객체 생성 대상 " + drugRawData.getDrugId() + "의 벡터가 없으므로 skip"); + continue; + } + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 " + drugRawData.getDrugId() + "의 벡터 길이: " + embed.getGptVector().length()); Drug drug = toDomainFromEntity(drugRawData, embed); drugs.add(drug); } - + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); return drugs; } From 6f0cb8e72ac7a648d8cb8c3962548c6a1fac3dd3 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 29 Apr 2025 17:17:22 +0900 Subject: [PATCH 29/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=9E=84=EB=B2=A0=EB=94=A9=20=EB=AA=A8=EB=8D=B8=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: 임베딩 DB 접근 port 변경 * ♻️ Refactor: JPA findAll OrderBy 삭제 * ♻️ Refactor: JPA 테이블 타입 변경 * ♻️ Refactor: 임베딩 모델 변경 --- .../infrastructure/embedding/client/OpenaiEmbeddingClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java index ae12d36..71e6654 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java @@ -32,7 +32,7 @@ public float[] getEmbedding(String text) { this.openAiApi, MetadataMode.EMBED, OpenAiEmbeddingOptions.builder() - .model("text-embedding-ada-002") + .model("text-embedding-3-small") .build(), RetryUtils.DEFAULT_RETRY_TEMPLATE); From a15090ee3160e5e203f88cad64f4889cfb412099 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:08:21 +0900 Subject: [PATCH 30/47] =?UTF-8?q?=F0=9F=90=9B=20=20Bug:=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=95=EB=A0=AC=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/persistence/GovDrugRawDataAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index 230601d..2d93f47 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -147,6 +147,6 @@ public List fetchRawData(int pageNo) { private Pageable createPageable(int pageNo) { log("pageable 생성"); - return PageRequest.of(pageNo-1, numOfRows, Sort.by(Sort.Direction.DESC, "drugId")); + return PageRequest.of(pageNo-1, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); } } \ No newline at end of file From 8548019980a070b15adbea8c96891e9e5780cb2e Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 29 Apr 2025 18:44:28 +0900 Subject: [PATCH 31/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=95=BD=ED=92=88?= =?UTF-8?q?=20=ED=9A=A8=EB=8A=A5=20=ED=8C=8C=EC=8B=B1=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/support/mapper/DrugDetailMapper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java index 2627157..5f38068 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java @@ -139,7 +139,11 @@ private static List tryParseParagraphs(JsonNode json, List effic for (JsonNode section : json.get("sections")) { for (JsonNode article : section.get("articles")) { for (JsonNode paragraph : article.get("paragraphs")) { - efficacys.add(paragraph.get("text").asText()); + String text = paragraph.get("text").asText(); + if(text != null && !text.isEmpty()){ + efficacys.add(paragraph.get("text").asText()); + } + } } } From 927b9b7935afc513d26f9caab0770459856eeabe Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Wed, 30 Apr 2025 10:57:59 +0900 Subject: [PATCH 32/47] =?UTF-8?q?=E2=9C=A8Feature:=20=ED=95=9C=EC=95=BD=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=20=EB=B0=8F=20=ED=97=88=EA=B0=80=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=9D=BC=EC=9E=90=20=EC=B6=94=EA=B0=80=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/scraper/combiner/DrugCombiner.java | 3 +++ .../detail/DrugDetailScraperService.java | 7 +++++++ .../yakplus/drug/domain/model/Drug.java | 6 ++++++ .../yakplus/drug/domain/model/DrugDetail.java | 3 +++ .../yakplus/drug/domain/model/DrugRawData.java | 7 +++++++ .../persistence/dto/DrugDetailRequest.java | 13 +++++++++++++ .../repository/entity/DrugDetailEntity.java | 17 +++++++++-------- .../repository/entity/DrugRawDataEntity.java | 9 +++++++++ .../support/mapper/DrugDetailMapper.java | 6 ++++++ .../support/mapper/DrugDetailRequestMapper.java | 3 +++ .../support/mapper/DrugRawDataMapper.java | 6 ++++++ 11 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java index 2ac377b..fdc31a7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java @@ -96,6 +96,9 @@ private DrugRawData buildMergeRawData(DrugDetail d, DrugImage i) { .usage(d.getUsage()) .precaution(d.getPrecaution()) .imageUrl(i.getImageUrl()) + .cancelDate(d.getCancelDate()) + .cancelName(d.getCancelName()) + .isHerbal(d.isHerbal()) .build(); return rawData; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java index 8ca21d8..a976f17 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java @@ -138,6 +138,13 @@ private List toListFromJson(JsonNode items) { drugDetail.changePrecaution(precautions); log(LogLevel.DEBUG, "drugDetail 객체에 약품 주의사항 저장 완료: \n" + drugDetail); + + String precaution = drugDetail.getPrecaution(); + if(precaution != null) { + if(precaution.contains("한의사") || precaution.contains("한약사")){ + drugDetail.changeIsHerbal(true); + } + } } return apiDataDrugDetails; } catch (Exception e) { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java index d9b143f..eeb94ce 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java @@ -58,4 +58,10 @@ public class Drug { //TODO @ApiModelProperty(value = "[float 배열]") private float[] vector; + + private LocalDate cancelDate; + + private String cancelName; + + private boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java index 20b2854..9318f24 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java @@ -25,4 +25,7 @@ public class DrugDetail { private List efficacy; private List usage; private Map> precaution; + private LocalDate cancelDate; + private String cancelName; + private boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java index 6c8e472..5d461ce 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java @@ -6,6 +6,7 @@ import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import jakarta.persistence.Column; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -54,4 +55,10 @@ public class DrugRawData { //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") private String imageUrl; + + private LocalDate cancelDate; + + private String cancelName; + + private boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java index 15ac14a..5c9f441 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java @@ -26,8 +26,17 @@ public class DrugDetailRequest { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") private LocalDate permitDate; + @JsonProperty("CANCEL_DATE") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") + private LocalDate cancelDate; + + @JsonProperty("CANCEL_NAME") + private String cancelName; private boolean isGeneral; + + private boolean isHerbal; + private String materialInfo; @JsonProperty("STORAGE_METHOD") @@ -60,4 +69,8 @@ public void changeEfficacy(String efficacy) { public void changePrecaution(String precaution) { this.precaution = precaution; } + + public void changeIsHerbal(boolean isHerbal) { + this.isHerbal = isHerbal; + } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java index 62e0a27..70692ad 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java @@ -26,20 +26,15 @@ public class DrugDetailEntity { @Id - @JsonProperty("ITEM_SEQ") @Column( name= "ITEM_SEQ") private Long drugId; - @JsonProperty("ITEM_NAME") @Column( name= "ITEM_NAME", columnDefinition = "TEXT") private String drugName; - @JsonProperty("ENTP_NAME") @Column( name= "ENTP_NAME") private String company; - @JsonProperty("ITEM_PERMIT_DATE") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") @Column( name= "ITEM_PERMIT_DATE") private LocalDate permitDate; @@ -49,11 +44,9 @@ public class DrugDetailEntity { @Column(name = "MATERIAL_NAME", columnDefinition = "JSON") private String materialInfo; - @JsonProperty("STORAGE_METHOD") @Column(name = "STORAGE_METHOD", columnDefinition = "TEXT") private String storeMethod; - @JsonProperty("VALID_TERM") @Column(name = "VALID_TERM") private String validTerm; @@ -66,7 +59,15 @@ public class DrugDetailEntity { @Column(name = "NB_DOC_DATA", columnDefinition = "JSON") private String precaution; - @JsonCreator + @Column(name="CANCEL_DATE") + private LocalDate cancelDate; + + @Column(name="CANCEL_NAME") + private String cancelName; + + @Column(name="IS_HERBAL") + private boolean isHerbal = false; + public DrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { this.isGeneral = !"전문의약품".equals(drugType); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java index 083b786..b614ebf 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java @@ -57,4 +57,13 @@ public class DrugRawDataEntity { @Column(name= "IMG_URL") private String imageUrl; + + @Column(name="CANCEL_DATE") + private LocalDate cancelDate; + + @Column(name="CANCEL_NAME") + private String cancelName; + + @Column(name="IS_HERBAL") + private boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java index 5f38068..689824a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java @@ -33,6 +33,9 @@ public static DrugDetail toDomainFromEntity(DrugDetailEntity e){ .efficacy(convertEfficacy(e.getEfficacy())) .usage(getUsage(e.getUsage())) .precaution(getPrecaution(e.getPrecaution())) + .cancelDate(e.getCancelDate()) + .cancelName(e.getCancelName()) + .isHerbal(e.isHerbal()) .build(); return domain; } @@ -50,6 +53,9 @@ public static Drug toDomainFromEntity(DrugRawDataEntity e){ .efficacy(convertEfficacy(e.getEfficacy())) .usage(getUsage(e.getUsage())) .precaution(getPrecaution(e.getPrecaution())) + .cancelDate(e.getCancelDate()) + .cancelName(e.getCancelName()) + .isHerbal(e.isHerbal()) .build(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java index 566a13c..dea606b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java @@ -18,6 +18,9 @@ public static DrugDetailEntity toEntityFromRequest(DrugDetailRequest r){ .efficacy(r.getEfficacy()) .usage(r.getUsage()) .precaution(r.getPrecaution()) + .cancelDate(r.getCancelDate()) + .cancelName(r.getCancelName()) + .isHerbal(r.isHerbal()) .build(); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java index 685aa3b..fa44aba 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java @@ -28,6 +28,9 @@ public static DrugRawDataEntity toEntityFromDomain(DrugRawData raw) { .usage(toStringFromObj(raw.getUsage())) .precaution(toStringFromObj(raw.getPrecaution())) .imageUrl(raw.getImageUrl()) + .cancelDate(raw.getCancelDate()) + .cancelName(raw.getCancelName()) + .isHerbal(raw.isHerbal()) .build(); } @@ -53,6 +56,9 @@ public static Drug toDomainFromEntity(DrugRawDataEntity e) { // TODO .usage(e.getUsage()) // TODO .precaution(e.getPrecaution()) .imageUrl(e.getImageUrl()) + .cancelDate(e.getCancelDate()) + .cancelName(e.getCancelName()) + .isHerbal(e.isHerbal()) .build(); } From 89bc6e53713c9b460af4b43e64682da6e9cd7872 Mon Sep 17 00:00:00 2001 From: leelise Date: Wed, 30 Apr 2025 11:24:16 +0900 Subject: [PATCH 33/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20DrugDetailEntity=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EC=9D=98=20=ED=95=84=EB=93=9C=20=20isHerbal?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9D=84=20false?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=98=EC=98=80=EC=9C=BC?= =?UTF-8?q?=EB=82=98=20@Builder.Default=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=B4=20=EC=97=86=EC=96=B4=20=EB=B9=8C?= =?UTF-8?q?=EB=8D=94=EB=A1=9C=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=20=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9D=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=98=EC=98=80?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/repository/entity/DrugDetailEntity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java index 70692ad..2501d0b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java @@ -66,6 +66,7 @@ public class DrugDetailEntity { private String cancelName; @Column(name="IS_HERBAL") + @Builder.Default private boolean isHerbal = false; public DrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { From 234792a9252d0f26b16952629afdecc6dbb3db00 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Wed, 30 Apr 2025 11:38:53 +0900 Subject: [PATCH 34/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20JPA=20Entity=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/repository/entity/DrugDetailEntity.java | 2 +- .../persistence/repository/entity/DrugRawDataEntity.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java index 2501d0b..426a546 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java @@ -67,7 +67,7 @@ public class DrugDetailEntity { @Column(name="IS_HERBAL") @Builder.Default - private boolean isHerbal = false; + private Boolean isHerbal = false; public DrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { this.isGeneral = !"전문의약품".equals(drugType); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java index b614ebf..b9d10ee 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java @@ -65,5 +65,5 @@ public class DrugRawDataEntity { private String cancelName; @Column(name="IS_HERBAL") - private boolean isHerbal; + private Boolean isHerbal; } From dcbcb3de8c6e3714930b9b8982d0e09c038d8910 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Wed, 30 Apr 2025 11:44:02 +0900 Subject: [PATCH 35/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20#58=20jpa=20entity?= =?UTF-8?q?=20null=20=EC=98=A4=EB=A5=98=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drug/infrastructure/support/mapper/DrugDetailMapper.java | 4 ++-- .../drug/infrastructure/support/mapper/DrugRawDataMapper.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java index 689824a..d3b04d1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java @@ -35,7 +35,7 @@ public static DrugDetail toDomainFromEntity(DrugDetailEntity e){ .precaution(getPrecaution(e.getPrecaution())) .cancelDate(e.getCancelDate()) .cancelName(e.getCancelName()) - .isHerbal(e.isHerbal()) + .isHerbal(e.getIsHerbal()) .build(); return domain; } @@ -55,7 +55,7 @@ public static Drug toDomainFromEntity(DrugRawDataEntity e){ .precaution(getPrecaution(e.getPrecaution())) .cancelDate(e.getCancelDate()) .cancelName(e.getCancelName()) - .isHerbal(e.isHerbal()) + .isHerbal(e.getIsHerbal()) .build(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java index fa44aba..c772ad7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java @@ -58,7 +58,7 @@ public static Drug toDomainFromEntity(DrugRawDataEntity e) { .imageUrl(e.getImageUrl()) .cancelDate(e.getCancelDate()) .cancelName(e.getCancelName()) - .isHerbal(e.isHerbal()) + .isHerbal(e.getIsHerbal()) .build(); } From cf54051e48d91450d188ccc00355fb5adf590205 Mon Sep 17 00:00:00 2001 From: chanbyeong <122460524+chanbyoung@users.noreply.github.com> Date: Thu, 1 May 2025 20:27:09 +0900 Subject: [PATCH 36/47] =?UTF-8?q?=E2=9C=A8=20Feature:=20#52=20=EC=95=BD?= =?UTF-8?q?=ED=92=88=EB=AA=85=20=EC=83=89=EC=9D=B8=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=ED=86=B5=ED=95=A9=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: 임시 파일 정의 * ✨ Feat: 일반 의약품만 가져오도록 변경 * ✨ Feat: 증상 사전 RDB 저장로직 구현 * ✨ Feat: 증상 ElasticSearch 색인 로직 구현 * ✨ Feat: 키워드 검색 테이블 통합 및 색인 로직 구현 * ♻️ Refactor: Document 객체 필드 이름 변경 * ✨ Feat: 성분명도 포함해서 색인하도록 변경 * ✨ Feat: 증상 사전 로그 및 에러 처리 추가 --- .../port/in/DictionaryUseCase.java | 7 ++ .../SymptomDictionaryElsRepositoryPort.java | 9 +++ .../SymptomDictionaryJpaRepositoryPort.java | 9 +++ .../port/out/SymptomDictionaryLoaderPort.java | 9 +++ .../service/DictionaryService.java | 51 ++++++++++++++ .../exception/DictionaryException.java | 19 ++++++ .../exception/error/DictionaryErrorCode.java | 31 +++++++++ .../out/JsonSymptomDictionaryLoader.java | 63 ++++++++++++++++++ .../out/SymptomDictionaryElsAdapter.java | 56 ++++++++++++++++ .../out/SymptomDictionaryJpaAdapter.java | 66 +++++++++++++++++++ .../SymptomDictionaryDocRepository.java | 8 +++ .../SymptomDictionaryRepository.java | 8 +++ .../document/SymptomDictionaryDocument.java | 31 +++++++++ .../repository/entity/SymptomDictionary.java | 28 ++++++++ .../support/mapper/DictionaryMapper.java | 46 +++++++++++++ .../controller/DictionaryController.java | 50 ++++++++++++++ .../repository/jpa/GovDrugJpaRepository.java | 5 ++ .../application/port/in/IndexTempUseCase.java | 6 ++ .../port/out/TempDrugIndexRepositoryPort.java | 11 ++++ .../application/port/out/TempRawDataPort.java | 10 +++ .../application/service/DrugTempIndexer.java | 45 +++++++++++++ .../TempElasticsearchDrugAdapter.java | 39 +++++++++++ .../TempGovDrugRawDataAdapter.java | 26 ++++++++ .../document/DrugKeywordDocument.java | 55 ++++++++++++++++ .../repository/DrugKeywordRepository.java | 8 +++ .../controller/DrugTempController.java | 35 ++++++++++ .../temp/support/mapper/KeywordMapper.java | 42 ++++++++++++ .../support/mapper/TempDrugRawDataMapper.java | 41 ++++++++++++ 28 files changed, 814 insertions(+) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java new file mode 100644 index 0000000..fc2feac --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.dictionary.application.port.in; + +public interface DictionaryUseCase { + + void setDictionary(); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java new file mode 100644 index 0000000..4694071 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java @@ -0,0 +1,9 @@ +package com.likelion.backendplus4.yakplus.dictionary.application.port.out; + +import java.util.List; + +public interface SymptomDictionaryElsRepositoryPort { + + void setDictionary(List symptoms); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java new file mode 100644 index 0000000..634cd56 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java @@ -0,0 +1,9 @@ +package com.likelion.backendplus4.yakplus.dictionary.application.port.out; + +import java.util.List; + +public interface SymptomDictionaryJpaRepositoryPort { + + void setDictionary(List symptoms); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java new file mode 100644 index 0000000..08bf9cd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java @@ -0,0 +1,9 @@ +package com.likelion.backendplus4.yakplus.dictionary.application.port.out; + +import java.util.List; + +public interface SymptomDictionaryLoaderPort { + + List loadDictionary(); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java new file mode 100644 index 0000000..96e538d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.dictionary.application.service; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.dictionary.application.port.in.DictionaryUseCase; +import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryElsRepositoryPort; +import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out.JsonSymptomDictionaryLoader; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 애플리케이션 계층에서 증상 사전 관리 기능을 제공하는 서비스 클래스입니다. + * JsonSymptomDictionaryLoader를 통해 로컬 JSON 파일에서 증상 리스트를 로드하고, + * 이를 JPA 및 Elasticsearch에 차례로 저장합니다. + * + * @since 2025-05-01 + * @modified 2025-05-01 + */ +@Service +@RequiredArgsConstructor +public class DictionaryService implements DictionaryUseCase { + + private final JsonSymptomDictionaryLoader jsonSymptomDictionaryLoader; + private final SymptomDictionaryJpaRepositoryPort dictionaryRepositoryPort; + private final SymptomDictionaryElsRepositoryPort dictionaryElsRepositoryPort; + + /** + * 로컬 JSON 파일에서 증상 리스트를 읽어들여 + * DB(JPA) 및 Elasticsearch에 차례로 저장합니다. + */ + @Override + public void setDictionary() { + log("DictionaryService.setDictionary() 호출 시작"); + + // 1) JSON 파일에서 증상 리스트 로드 + List symptomList = jsonSymptomDictionaryLoader.loadDictionary(); + log(" loadDictionary() 완료, 증상 수: " + symptomList.size()); + + // 2) JPA 저장 + dictionaryRepositoryPort.setDictionary(symptomList); + log(" SymptomDictionaryJpaAdapter.setDictionary() 완료"); + + // 3) Elasticsearch 저장 + dictionaryElsRepositoryPort.setDictionary(symptomList); + log(" SymptomDictionaryElsAdapter.setDictionary() 완료"); + + log("DictionaryService.setDictionary() 호출 종료"); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java new file mode 100644 index 0000000..edb9d89 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.dictionary.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class DictionaryException extends CustomException { + + private final ErrorCode errorCode; + + public DictionaryException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java new file mode 100644 index 0000000..56310ef --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.dictionary.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum DictionaryErrorCode implements ErrorCode { + + INVALID_FILE_TYPE(HttpStatus.BAD_REQUEST, 11001, "사전 파일 형식이 잘못되었습니다: .json 파일만 허용됩니다"), + DICTIONARY_LOAD_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, 54001, "증상 사전 로딩에 실패했습니다"); + + private final HttpStatus status; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return status; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java new file mode 100644 index 0000000..2b4dbcb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java @@ -0,0 +1,63 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryLoaderPort; +import com.likelion.backendplus4.yakplus.dictionary.exception.DictionaryException; +import com.likelion.backendplus4.yakplus.dictionary.exception.error.DictionaryErrorCode; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +/** + * classpath에 위치한 JSON 파일로부터 증상 사전 데이터를 로드하는 클래스입니다. + * SymptomDictionaryLoaderPort를 구현하여 JSON 파싱 로직을 캡슐화합니다. + * + * @since 2025-04-30 + * @modified 2025-05-01 + */ +@RequiredArgsConstructor +@Component +public class JsonSymptomDictionaryLoader implements SymptomDictionaryLoaderPort { + + private final ObjectMapper objectMapper; + private static final String DICT_PATH = "unique_symptoms.json"; + + /** + * ClassPathResource를 통해 지정된 JSON 파일을 읽고, + * List 형태로 변환하여 반환합니다. + * + * @return JSON에 정의된 증상 문자열 리스트 + * @throws DictionaryException 파일 형식 오류 또는 파싱/IO 실패 시 발생 + * @since 2025-04-30 + * @author 박찬병 + * @modified 2025-05-01 + * */ + @Override + public List loadDictionary() { + log("JsonSymptomDictionaryLoader.loadDictionary() 호출, 경로: " + DICT_PATH); + + // 1) 확장자 검증 + if (!DICT_PATH.toLowerCase().endsWith(".json")) { + log(LogLevel.ERROR, "loadDictionary() 파일 형식 오류: JSON(.json) 파일만 허용됩니다."); + throw new DictionaryException(DictionaryErrorCode.INVALID_FILE_TYPE); + } + + ClassPathResource resource = new ClassPathResource(DICT_PATH); + try (InputStream in = resource.getInputStream()) { + // 2) JSON 읽기 및 파싱 + List symptoms = objectMapper.readValue(in, new TypeReference>() {}); + log("loadDictionary() 완료, 로드된 증상 수: " + symptoms.size()); + return symptoms; + } catch (IOException e) { + log(LogLevel.ERROR, "loadDictionary() 증상 사전 로드 실패", e); + throw new DictionaryException(DictionaryErrorCode.DICTIONARY_LOAD_FAILURE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java new file mode 100644 index 0000000..3843bce --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java @@ -0,0 +1,56 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryElsRepositoryPort; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.SymptomDictionaryDocRepository; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.support.mapper.DictionaryMapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Elasticsearch를 이용해 증상 사전 문서를 관리하는 어댑터 클래스입니다. + * + * @since 2025-04-30 + * @modified 2025-05-01 + */ +@Component +@RequiredArgsConstructor +public class SymptomDictionaryElsAdapter implements SymptomDictionaryElsRepositoryPort { + + private final SymptomDictionaryDocRepository repository; + + /** + * 주어진 증상 단어 리스트를 바탕으로 Elasticsearch에 신규 증상 사전 문서를 일괄 저장합니다. + * 1) 입력된 리스트에서 중복을 제거하고, + * 2) DictionaryMapper를 통해 Document 객체로 변환한 뒤, + * 3) repository.saveAll을 + * 호출하여 일괄 색인합니다. + * + * @param symptoms 저장할 증상 단어 리스트 + * @since 2025-04-30 + * @author 박찬병 + * @modified 2025-05-01 + */ + @Override + @Transactional + public void setDictionary(List symptoms) { + log("setDictionary() 메서드 호출, 입력 증상 수: " + symptoms.size()); + // 중복 제거 및 Document 변환 + List docs = symptoms.stream() + .distinct() + .map(DictionaryMapper::toDocument) + .toList(); + log("setDictionary() distinct 처리 후 문서 수: " + docs.size()); + + // 일괄 저장 + repository.saveAll(docs); + log("setDictionary() 완료, 색인된 문서 수: " + docs.size()); + + } + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java new file mode 100644 index 0000000..07a7481 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java @@ -0,0 +1,66 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.SymptomDictionaryRepository; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.support.mapper.DictionaryMapper; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * JPA를 이용해 증상 사전 데이터를 관리하는 어댑터 클래스입니다. + * SymptomDictionaryJpaRepositoryPort를 구현하여 + * 데이터베이스 접근 로직을 캡슐화합니다. + * + * @since 2025-05-01 + * @modified 2025-05-01 + */ +@Component +@RequiredArgsConstructor +public class SymptomDictionaryJpaAdapter implements SymptomDictionaryJpaRepositoryPort { + + private final SymptomDictionaryRepository repository; + + /** + * 주어진 증상 단어 리스트를 바탕으로 + * 데이터베이스에 신규 증상 단어만 저장합니다. + * + * 1) 기존에 저장된 증상명을 조회하고, + * 2) 전달받은 리스트에서 중복되지 않는 단어만 필터링한 후, + * 3) 엔티티로 변환하여 일괄 저장합니다. + * + * @param symptoms 저장할 증상 단어 리스트 + * @since 2025-04-30 + * @author 박찬병 + * @modified 2025-05-01 + */ + @Override + @Transactional + public void setDictionary(List symptoms) { + log("setDictionary() 메서드 호출, 입력 증상 수: " + symptoms.size()); + + // 1) 기존에 존재하는 증상 단어 조회 + Set existing = repository.findAll().stream() + .map(SymptomDictionary::getName) + .collect(Collectors.toSet()); + log("setDictionary() 기존 저장된 증상 수: " + existing.size()); + + // 2) 신규 단어만 필터링 + List toInsert = symptoms.stream() + .filter(s -> !existing.contains(s)) + .toList(); + log("setDictionary() 신규 저장 대상 증상 수: " + toInsert.size()); + + // 3) 엔티티 변환 및 저장 + List entities = toInsert.stream() + .map(DictionaryMapper::toEntity) + .toList(); + repository.saveAll(entities); + log("setDictionary() 완료, 저장된 엔티티 수: " + entities.size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java new file mode 100644 index 0000000..c06ac09 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository; + +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface SymptomDictionaryDocRepository extends ElasticsearchRepository { + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java new file mode 100644 index 0000000..877dea8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository; + +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SymptomDictionaryRepository extends JpaRepository { + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java new file mode 100644 index 0000000..50c76c7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.CompletionField; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Document(indexName = "symptom_dictionary") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SymptomDictionaryDocument { + + @Id + @Field(type = FieldType.Keyword, name = "symptom") + private String symptom; + + @CompletionField( + analyzer = "symptom_autocomplete", + searchAnalyzer = "symptom_search_autocomplete" + ) + private List symptomSuggester; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java new file mode 100644 index 0000000..2eb816e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +public class SymptomDictionary { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "symptom_dictionary_id") + private Long id; + + @Column(name = "symptom_name", unique = true) + private String name; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java new file mode 100644 index 0000000..7b7c62c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.dictionary.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import java.util.List; + +/** + * 증상 사전 엔티티 및 문서 객체 간 변환을 담당하는 매퍼 클래스입니다. + * + * + * @since 2025-05-01 + * @modified 2025-05-01 + */ +public class DictionaryMapper { + + /** + * 단일 증상명을 JPA 엔티티로 변환합니다. + * + * @param name 증상명 문자열 + * @return SymptomDictionary JPA 엔티티 + * @author 박찬병 + * @since 2025-05-01 + * @modified 2025-05-01 + */ + public static SymptomDictionary toEntity(String name) { + SymptomDictionary entity = new SymptomDictionary(null, name); + return entity; + } + + /** + * 단일 증상명을 Elasticsearch 색인용 문서로 변환합니다. + * + * @param name 증상명 문자열 + * @return SymptomDictionaryDocument ES 문서 객체 + * @author 박찬병 + * @since 2025-05-01 + * @modified 2025-05-01 + */ + public static SymptomDictionaryDocument toDocument(String name) { + SymptomDictionaryDocument doc = SymptomDictionaryDocument.builder() + .symptom(name) + .symptomSuggester(List.of(name)) + .build(); + return doc; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java new file mode 100644 index 0000000..4a1dfaa --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java @@ -0,0 +1,50 @@ +package com.likelion.backendplus4.yakplus.dictionary.presentation.controller; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.dictionary.application.port.in.DictionaryUseCase; +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + + +/** + * 사전 관리용 REST API를 제공하는 컨트롤러 클래스입니다. + * + * @since 2025-05-01 + * @modified 2025-05-01 + */ +@RestController +@RequestMapping("/api/dictionary") +@RequiredArgsConstructor +public class DictionaryController { + + private final DictionaryUseCase dictionaryUseCase; + + /** + * 증상 사전 데이터를 JSON 파일로부터 로드하여 + * DB 및 Elasticsearch에 저장하는 작업을 수행합니다. + * + * 1) JSON 로더를 통해 증상 리스트 로드 + * 2) JPA 어댑터로 DB 저장 + * 3) Elasticsearch 어댑터로 색인 저장 + * + * @return 성공 여부를 담은 ApiResponse + * @throws RuntimeException 처리 중 오류 발생 시 전달 + * @author 박찬병 + * @since 2025-05-01 + * @modified 2025-05-01 + */ + @PostMapping("/set") + public ResponseEntity> setDictionary() { + log("setDictionary() 호출"); + dictionaryUseCase.setDictionary(); + + return ApiResponse.success(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java index 2f9d84a..8155dfa 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java @@ -1,8 +1,13 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; +import aj.org.objectweb.asm.commons.Remapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; public interface GovDrugJpaRepository extends JpaRepository { + + Page findByIsGeneralIsTrue(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java new file mode 100644 index 0000000..cd7dd5c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java @@ -0,0 +1,6 @@ +package com.likelion.backendplus4.yakplus.temp.application.port.in; + +public interface IndexTempUseCase { + + void indexKeyword(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java new file mode 100644 index 0000000..31286c9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java @@ -0,0 +1,11 @@ +package com.likelion.backendplus4.yakplus.temp.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import org.springframework.data.domain.Page; + +public interface TempDrugIndexRepositoryPort { + + void saveAllSymptom(Page drugPage); + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java new file mode 100644 index 0000000..5034354 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.temp.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface TempRawDataPort { + + Page findAllDrugs(Pageable pageable); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java new file mode 100644 index 0000000..088ba87 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.temp.application.service; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.temp.application.port.in.IndexTempUseCase; +import com.likelion.backendplus4.yakplus.temp.application.port.out.TempDrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.temp.application.port.out.TempRawDataPort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DrugTempIndexer implements IndexTempUseCase { + + private final TempRawDataPort govDrugRawDataPort; + private final TempDrugIndexRepositoryPort drugIndexRepositoryPort; + + private static final int CHUNK_SIZE = 1_000; + + @Override + public void indexKeyword() { + log("indexKeyword 요청 수신"); + int page = 0; + Page drugPage; + + do { + log("색인 시작: page=" + page); + + // 1. 페이징으로 DB에서 한 청크 가져오기 + drugPage = govDrugRawDataPort.findAllDrugs(PageRequest.of(page, CHUNK_SIZE)); + log(" 조회 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); + + // 2. 청크별 ES에 색인 + drugIndexRepositoryPort.saveAllSymptom(drugPage); + log(" 색인 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); + + // 3. 다음 1000개 값 루프 + page++; + } while (drugPage.hasNext()); + log("indexSymptom 전체 처리 완료"); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java new file mode 100644 index 0000000..7c83897 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java @@ -0,0 +1,39 @@ +package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.DrugSymptomRepository; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; +import com.likelion.backendplus4.yakplus.temp.application.port.out.TempDrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document.DrugKeywordDocument; +import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.repository.DrugKeywordRepository; +import com.likelion.backendplus4.yakplus.temp.support.mapper.KeywordMapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class TempElasticsearchDrugAdapter implements TempDrugIndexRepositoryPort { + + private final DrugKeywordRepository keywordRepository; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveAllSymptom(Page drugs) { + // 도메인 → ES Document 변환 + log("saveAllSymptom() 요청 수신"); + List docs = drugs.stream() + .map(KeywordMapper::toDocument) // 내부에서 예외 처리 됨 + .toList(); + log(" 문서 변환 완료: count=" + docs.size()); + + keywordRepository.saveAll(docs); + log(" ES 색인 완료: count=" + docs.size()); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java new file mode 100644 index 0000000..9d3e3b3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java @@ -0,0 +1,26 @@ +package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.temp.application.port.out.TempRawDataPort; +import com.likelion.backendplus4.yakplus.temp.support.mapper.TempDrugRawDataMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TempGovDrugRawDataAdapter implements TempRawDataPort { + + private final GovDrugJpaRepository drugJpaRepository; + + public Page findAllDrugs(Pageable pageable) { + log("findAllDrugs() 요청 수신"); + return drugJpaRepository.findByIsGeneralIsTrue(pageable) + .map(TempDrugRawDataMapper::toDomainFromEntity); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java new file mode 100644 index 0000000..ddbe9e6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java @@ -0,0 +1,55 @@ +package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.CompletionField; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Document(indexName = "drug_keyword") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DrugKeywordDocument { + + @Id + @Field(type = FieldType.Keyword, name = "drugId") + private Long drugId; + + @Field(type = FieldType.Text, name = "drugName") + private String drugName; + + @Field(type = FieldType.Text, name = "company") + private String company; + + @Field(type = FieldType.Keyword, name = "imageUrl") + private String imageUrl; + + @Field(type = FieldType.Text, name = "efficacy") + private List efficacy; + + @Field(type = FieldType.Text, name = "efficacy_list") + private List efficacyList; + + @Field(type = FieldType.Text, name = "ingredientName") + private List ingredientName; + + @CompletionField( + analyzer = "drugName_autocomplete", + searchAnalyzer = "drugName_autocomplete" + ) + private List drugNameSuggester; + + @CompletionField( + analyzer = "drugName_autocomplete", + searchAnalyzer = "drugName_autocomplete" + ) + private List ingredientNameSuggester; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java new file mode 100644 index 0000000..e736569 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.repository; + +import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document.DrugKeywordDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface DrugKeywordRepository extends ElasticsearchRepository { + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java new file mode 100644 index 0000000..5f5076f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java @@ -0,0 +1,35 @@ +package com.likelion.backendplus4.yakplus.temp.presentation.controller; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.temp.application.port.in.IndexTempUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/drugs/index") +@RequiredArgsConstructor +public class DrugTempController { + + private final IndexTempUseCase indexUseCase; + + /** + * 색인 배치 작업을 실행하여, DB로부터 조회한 약품 증상 데이터를 Elasticsearch에 일괄 색인합니다. + * + * @return 색인 작업 성공 여부 응답 (Void) + * @author 박찬병 + * @modified 2025-04-25 + * @since 2025-04-24 + */ + @PostMapping("/keyword") + public ResponseEntity> triggerIndex() { + log("indexSymptom 요청 수신"); + indexUseCase.indexKeyword(); + return ApiResponse.success(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java new file mode 100644 index 0000000..5aa770f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java @@ -0,0 +1,42 @@ +package com.likelion.backendplus4.yakplus.temp.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import com.likelion.backendplus4.yakplus.index.support.parser.SymptomTextParser; +import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document.DrugKeywordDocument; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class KeywordMapper { + + public static DrugKeywordDocument toDocument(Drug entity) { + // 1) 추출된 텍스트 리스트를 단일 문자열로 전처리 + String flatText = SymptomTextParser.flattenLines(entity.getEfficacy()); + // 2) 전처리된 문자열을 자동완성용 토큰 리스트로 변환 + List suggestTokens = SymptomTextParser.tokenizeForSuggestion(flatText); + + // 3) 성분명 자동완성 필드 + List ingredientSuggest = Optional.ofNullable(entity.getMaterialInfo()) + .orElse(Collections.emptyList()) + .stream() + .map(Material::getName) + .filter(Objects::nonNull) // MaterialName이 null일 때 걸러주기 + .toList(); + + return DrugKeywordDocument.builder() + .drugId(entity.getDrugId()) + .drugName(entity.getDrugName()) + .efficacy(entity.getEfficacy()) + .efficacyList(suggestTokens) + .imageUrl(entity.getImageUrl()) + .company(entity.getCompany()) + .drugNameSuggester(List.of(entity.getDrugName())) + .ingredientName(ingredientSuggest) + .ingredientNameSuggester(ingredientSuggest) + .build(); + } + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java new file mode 100644 index 0000000..2bf30ee --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.temp.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.DrugMapper; +import com.likelion.backendplus4.yakplus.index.support.parser.JsonArrayTextParser; +import java.io.IOException; +import java.util.List; + +public class TempDrugRawDataMapper { + + public static Drug toDomainFromEntity(DrugRawDataEntity e) { + List efficacy; + try { + efficacy = JsonArrayTextParser.extractAndClean(e.getEfficacy()); + } catch (IOException exception) { + throw new ScraperException(ScraperErrorCode.PARSING_ERROR); + } + + return Drug.builder() + .drugId(e.getDrugId()) + .drugName(e.getDrugName()) + .company(e.getCompany()) + .permitDate(e.getPermitDate()) + .isGeneral(e.isGeneral()) + .materialInfo(DrugMapper.parseMaterials(e.getMaterialInfo())) + .storeMethod(e.getStoreMethod()) + .validTerm(e.getValidTerm()) + .efficacy(efficacy) + .usage(DrugMapper.parseStringToList(e.getUsage())) + .precaution(DrugMapper.parsePrecaution(e.getPrecaution())) + .imageUrl(e.getImageUrl()) + .cancelDate(e.getCancelDate()) + .cancelName(e.getCancelName()) + .isHerbal(e.isHerbal()) + .build(); + } + +} From 9ac1a271a7366dd1c0e2c1fbf9568df61950cb69 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Thu, 1 May 2025 20:39:51 +0900 Subject: [PATCH 37/47] =?UTF-8?q?=F0=9F=90=9BFix:=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=ED=98=B8=EC=B6=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feat: 임시 파일 정의 * ✨ Feat: 일반 의약품만 가져오도록 변경 * ✨ Feat: 증상 사전 RDB 저장로직 구현 * ✨ Feat: 증상 ElasticSearch 색인 로직 구현 * ✨ Feat: 키워드 검색 테이블 통합 및 색인 로직 구현 * ♻️ Refactor: Document 객체 필드 이름 변경 * ✨ Feat: 성분명도 포함해서 색인하도록 변경 * ✨ Feat: 증상 사전 로그 및 에러 처리 추가 * 🐛 메소드 호출 오류 수정 --------- Co-authored-by: pcb7893@naver.com <122460524+chanbyoung@users.noreply.github.com> --- .../yakplus/temp/support/mapper/TempDrugRawDataMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java index 2bf30ee..30fc798 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java @@ -34,7 +34,7 @@ public static Drug toDomainFromEntity(DrugRawDataEntity e) { .imageUrl(e.getImageUrl()) .cancelDate(e.getCancelDate()) .cancelName(e.getCancelName()) - .isHerbal(e.isHerbal()) + .isHerbal(e.getIsHerbal()) .build(); } From 82e73871369a3ae03e59b3fef7c94e943ad80421 Mon Sep 17 00:00:00 2001 From: JUNG ANSIK Date: Fri, 2 May 2025 11:55:09 +0900 Subject: [PATCH 38/47] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9E=84=EB=B2=A0?= =?UTF-8?q?=EB=94=A9=20=EB=AA=A8=EB=8D=B8=20=EC=84=A0=ED=83=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feature: 임베딩 모델 선택 기능 * ♻️ Refactor: 스위치 모델을 구조에 맞게 수정하였습니다. (RDB -> ES) * ♻️ Refactor: 이제 RAWDATA를 꺼내어 Embedding한 후 Embedding테이블에 넣을 때 스위칭로직을 이용하여 한 모델씩만 사용 가능합니다. --- .../common/util/log/LoggerWithTraceId.java | 6 +- .../port/in/DrugEmbedProcessorUseCase.java | 28 ++++-- .../scraper/embed/DrugEmbedProcessor.java | 85 +++++++++---------- .../controller/DrugDataTestController.java | 15 ++++ .../port/out/EmbeddingLoadingPort.java | 1 - .../application/port/out/EmbeddingPort.java | 5 -- .../persistence/GovDrugRawDataAdapter.java | 2 - .../GptEmbeddingLoadingAdapter.java | 25 +----- .../KmBertEmbeddingLoadingAdapter.java | 24 +----- .../KrSBertEmbeddingLoadingAdapter.java | 24 +----- .../persistence/OpenAIEmbeddingAdapter.java | 75 ---------------- .../switcher/application/EmbeddingRouter.java | 28 ++++++ .../port/in/EmbeddingRoutingUseCase.java | 6 ++ .../port/out/EmbeddingSwitchPort.java | 6 ++ .../route/adapter/EmbeddingRouterAdapter.java | 63 ++++++++++++++ .../controller/EmbeddingRouterController.java | 30 +++++++ 16 files changed, 218 insertions(+), 205 deletions(-) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java index 75d2e98..4c8b503 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java @@ -5,6 +5,8 @@ import org.slf4j.LoggerFactory; import org.slf4j.MDC; +import java.util.UUID; + /** * TraceId를 포함한 로거 클래스 * @@ -76,7 +78,9 @@ private static Logger makeLogger() { */ private static String makeTraceId() { String traceId = MDC.get("traceId"); - validateTraceId(traceId); + if (traceId == null || traceId.trim().isEmpty()) { + return "no-trace"; + } return traceId; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java index 53ac43a..7accc0e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java @@ -1,5 +1,7 @@ package com.likelion.backendplus4.yakplus.drug.application.service.port.in; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; + /** * 의약품 효능 정보를 임베딩 처리를 위한 유스케이스 인터페이스입니다. * @@ -7,10 +9,24 @@ */ public interface DrugEmbedProcessorUseCase { - /** - * 의약품 데이터를 기반으로 임베딩 프로세스를 시작합니다. - * - * @since 2025-04-25 - */ - void startEmbedding(); + /** + * 의약품 데이터를 기반으로 임베딩 프로세스를 시작합니다. + * + * @since 2025-04-25 + */ + void startEmbedding(); + + /** + * 임베딩 모델을 스위칭하는 메서드입니다. + * + * @param modelType 전환할 임베딩 모델 타입 (GPT, KmBERT, KrSBERT) + */ + void switchEmbeddingModel(String modelType); + + /** + * 현재 사용 중인 임베딩 모델을 조회하는 메서드입니다. + * + * @return 현재 사용 중인 임베딩 모델 + */ + EmbeddingModelType getCurrentEmbeddingModel(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java index 63c1239..f4531b6 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java @@ -35,63 +35,40 @@ public class DrugEmbedProcessor implements DrugEmbedProcessorUseCase { private final EmbeddingPort embeddingPort; private final DrugEmbedRepositoryPort embedRepositoryPort; + private volatile EmbeddingModelType currentEmbeddingModel = EmbeddingModelType.OPENAI; // 기본 모델 + @Override public void startEmbedding() { log("약품 효능 임베딩 작업 시작"); Page firstPage = getAllItem(0); - for(int i=0; i { String efficacy = convertSingleStringForEfficacy(data.getEfficacy()); - saveSbertVector(data, efficacy); - saveKmBertVector(data, efficacy); - saveGptVector(data, efficacy); + saveVector(data, efficacy); }); - } - log("약품 효능 임베딩 작업 완료"); } - /** - * GPT 모델을 사용하여 임베딩 벡터를 생성하고 저장합니다. - * - * @param detail 의약품 상세 정보 - * @param text 임베딩 대상 텍스트 - * - * @since 2025-04-25 - */ - private void saveGptVector(Drug detail, String text) { - float[] openAIVector = embeddingPort.getEmbedding( - text, EmbeddingModelType.OPENAI); - embedRepositoryPort.saveGptEmbed(detail.getDrugId(), openAIVector); - } - - /** - * KmBERT 모델을 사용하여 임베딩 벡터를 생성하고 저장합니다. - * - * @param detail 의약품 상세 정보 - * @param text 임베딩 대상 텍스트 - * - * @since 2025-04-25 - */ - private void saveKmBertVector(Drug detail, String text) { - float[] kmbertVector = embeddingPort.getEmbedding( - text, EmbeddingModelType.KM_BERT); - embedRepositoryPort.saveKmBertEmbed(detail.getDrugId(), kmbertVector); - } + // 임베딩 벡터를 생성하고 저장하는 공통 메서드 + private void saveVector(Drug detail, String text) { + // 현재 선택된 임베딩 모델에 따라 벡터를 생성 + float[] vector = embeddingPort.getEmbedding(text, currentEmbeddingModel); - /** - * KrSBERT 모델을 사용하여 임베딩 벡터를 생성하고 저장합니다. - * - * @param detail 의약품 상세 정보 - * @param text 임베딩 대상 텍스트 - * - * @since 2025-04-25 - */ - private void saveSbertVector(Drug detail, String text) { - float[] sbertVector = embeddingPort.getEmbedding( - text, EmbeddingModelType.SBERT); - embedRepositoryPort.saveKrSbertEmbed(detail.getDrugId(), sbertVector); + // 모델에 따른 저장 처리 + switch (currentEmbeddingModel) { + case OPENAI: + embedRepositoryPort.saveGptEmbed(detail.getDrugId(), vector); + break; + case KM_BERT: + embedRepositoryPort.saveKmBertEmbed(detail.getDrugId(), vector); + break; + case SBERT: + embedRepositoryPort.saveKrSbertEmbed(detail.getDrugId(), vector); + break; + default: + throw new IllegalArgumentException("Unknown embedding model: " + currentEmbeddingModel); + } } /** @@ -125,4 +102,22 @@ private String convertSingleStringForEfficacy(List stringList) { private Page getAllItem(int i) { return drugRawDataPort.findAllDrugs(PageRequest.of(i, 100)); } + + /** + * 임베딩 모델을 스위칭하는 메서드입니다. + * + * @param modelType 전환할 임베딩 모델 타입 (GPT, KmBERT, KrSBERT) + */ + @Override + public void switchEmbeddingModel(String modelType) { + // 유효하지 않으면 기본값(OPENAI)로 처리 + this.currentEmbeddingModel = EmbeddingModelType.valueOf(modelType); // valueOf로 직접 변환 + log("임베딩 모델 스위치 완료 - 현재 모델: " + currentEmbeddingModel); + } + + @Override + public EmbeddingModelType getCurrentEmbeddingModel() { + log("현재 사용 중인 임베딩 모델 조회 - 현재 모델: " + currentEmbeddingModel); + return currentEmbeddingModel; // Enum 반환 + } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index e89f8bf..cd3d876 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -4,6 +4,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; @@ -40,4 +41,18 @@ public ResponseEntity saveEmbedData(){ drugEmbedProcessorUseCase.startEmbedding(); return ResponseEntity.ok().build(); } + + // 임베딩 모델을 스위칭하는 엔드포인트 + @GetMapping("/test/switchEmbed") + public ResponseEntity switchEmbedding(@RequestParam String modelType) { + drugEmbedProcessorUseCase.switchEmbeddingModel(modelType); + return ResponseEntity.ok().build(); + } + + // 현재 사용 중인 임베딩 모델을 조회하는 엔드포인트 + @GetMapping("/test/currentEmbed") + public ResponseEntity getCurrentEmbedding() { + EmbeddingModelType currentModel = drugEmbedProcessorUseCase.getCurrentEmbeddingModel(); + return ResponseEntity.ok(currentModel); // 현재 모델을 반환 + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java index d9a8542..66abc62 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java @@ -6,6 +6,5 @@ import java.util.List; public interface EmbeddingLoadingPort { - List loadAllEmbeddings(); List loadEmbeddingsByPage(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java deleted file mode 100644 index 5599201..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingPort.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.application.port.out; - -public interface EmbeddingPort { - float[] getEmbedding(String text); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index 2d93f47..4e1a552 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -36,8 +36,6 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { private final GovDrugDetailJpaRepository rawDataJpaRepository; private final GovDrugJpaRepository drugJpaRepository; - // 추후 수정... -> 임베딩 받아오는 방식 + List 변환 통일 - @Qualifier("gptAdapter") private final EmbeddingLoadingPort embeddingLoadingPort; @Value("${gov.numOfRows}") diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index 82c561a..a158457 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -18,35 +18,12 @@ import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -@Repository("gptAdapter") -@Primary +@Repository @RequiredArgsConstructor public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; private final GovDrugJpaRepository govDrugJpaRepository; - - @Override - public List loadAllEmbeddings() { - List rawDataEntities = govDrugJpaRepository.findAll(); - List drugGptEmbedEntities = govDrugGptEmbedJpaRepository.findAll(); - - // drugGptEmbedEntities를 Map으로 변환 (key: drugId) - Map gptEmbedMap = new HashMap<>(); - for (DrugGptEmbedEntity embed : drugGptEmbedEntities) { - gptEmbedMap.put(embed.getDrugId(), embed); - } - - List drugs = new ArrayList<>(); - for (DrugRawDataEntity drugRawData : rawDataEntities) { - DrugGptEmbedEntity embed = gptEmbedMap.get(drugRawData.getDrugId()); - Drug drug = toDomainFromEntity(drugRawData, embed); - drugs.add(drug); - } - - return drugs; - } - @Override public List loadEmbeddingsByPage(Pageable pageable) { List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java index 87eb4a4..4a14a9d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java @@ -16,34 +16,12 @@ import java.util.List; import java.util.Map; -@Repository("kmBertAdapter") +@Repository @RequiredArgsConstructor public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; private final GovDrugJpaRepository govDrugJpaRepository; - - @Override - public List loadAllEmbeddings() { - List rawDataEntities = govDrugJpaRepository.findAll(); - List drugKmBertEmbedEntities = govDrugKmBertEmbedJpaRepository.findAll(); - - // drugKmBertEmbedEntities를 Map으로 변환 (key: drugId) - Map kmBertEmbedMap = new HashMap<>(); - for (DrugKmBertEmbedEntity embed : drugKmBertEmbedEntities) { - kmBertEmbedMap.put(embed.getDrugId(), embed); - } - - List drugs = new ArrayList<>(); - for (DrugRawDataEntity drugRawData : rawDataEntities) { - DrugKmBertEmbedEntity embed = kmBertEmbedMap.get(drugRawData.getDrugId()); - Drug drug = toDomainFromEntity(drugRawData, embed); - drugs.add(drug); - } - - return drugs; - } - @Override public List loadEmbeddingsByPage(Pageable pageable) { List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java index 7bb6ab1..576fafe 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java @@ -16,34 +16,12 @@ import java.util.List; import java.util.Map; -@Repository("krSBertAdapter") +@Repository @RequiredArgsConstructor public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; private final GovDrugJpaRepository govDrugJpaRepository; - - @Override - public List loadAllEmbeddings() { - List rawDataEntities = govDrugJpaRepository.findAll(); - List drugKrSBertEmbedEntities = govDrugKrSbertEmbedJpaRepository.findAll(); - - // drugKrSBertEmbedEntities를 Map으로 변환 (key: drugId) - Map krSBertEmbedMap = new HashMap<>(); - for (DrugKrSbertEmbedEntity embed : drugKrSBertEmbedEntities) { - krSBertEmbedMap.put(embed.getDrugId(), embed); - } - - List drugs = new ArrayList<>(); - for (DrugRawDataEntity drugRawData : rawDataEntities) { - DrugKrSbertEmbedEntity embed = krSBertEmbedMap.get(drugRawData.getDrugId()); - Drug drug = toDomainFromEntity(drugRawData, embed); - drugs.add(drug); - } - - return drugs; - } - @Override public List loadEmbeddingsByPage(Pageable pageable) { List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java deleted file mode 100644 index bfae023..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; - -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingPort; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiEmbeddingOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.stereotype.Component; - -import java.util.Arrays; -import java.util.List; - -/** - * OpenAI 임베딩 API를 호출하여 텍스트에 대한 벡터 임베딩을 생성하는 어댑터 클래스입니다. - * EmbeddingPort 인터페이스를 구현하며, 오류 발생 시 SearchException으로 래핑합니다. - * - * @since 2025-04-22 - * @modified 2025-04-24 - */ -@Component -@RequiredArgsConstructor -public class OpenAIEmbeddingAdapter implements EmbeddingPort { - private final OpenAiApi openAiApi; - private static final String EMBEDDING_MODEL = "text-embedding-3-small"; - - /** - * 주어진 텍스트를 OpenAI 임베딩 모델에 전달하여 벡터 값을 반환합니다. - * 내부에서 OpenAiEmbeddingModel을 생성하고 retry 템플릿을 적용합니다. - * API 호출 중 예외가 발생하면 SearchException(EMBEDDING_API_ERROR)을 던집니다. - * - * @param text 벡터화할 입력 텍스트 - * @return float 배열 형태의 임베딩 벡터 - * @throws IndexException EMBEDDING_API_ERROR 코드로 래핑하여 발생 - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - @Override - public float[] getEmbedding(String text) { - try { - OpenAiEmbeddingModel embeddingModel = createEmbeddingModel(); - EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text)); - return response.getResults().getFirst().getOutput(); - } catch (Exception e) { - //TODO: LOG ERROR 처리 요망 -// log(LOGLEVEL.ERROR, "임베딩 API에서 문제가 발생하였습니다., e); - throw new IndexException(IndexErrorCode.EMBEDDING_API_ERROR); - } - } - - /** - * OpenAiEmbeddingModel 인스턴스를 생성하여 반환합니다. - * MetadataMode와 모델 이름, RetryUtils 설정이 포함됩니다. - * - * @return 초기화된 OpenAiEmbeddingModel 객체 - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - private OpenAiEmbeddingModel createEmbeddingModel() { - return new OpenAiEmbeddingModel( - openAiApi, - MetadataMode.EMBED, - OpenAiEmbeddingOptions.builder() - .model(EMBEDDING_MODEL) - .build(), - RetryUtils.DEFAULT_RETRY_TEMPLATE - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java new file mode 100644 index 0000000..8749196 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.switcher.application; + +import com.likelion.backendplus4.yakplus.switcher.application.port.in.EmbeddingRoutingUseCase; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import org.springframework.stereotype.Service; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +@Service +public class EmbeddingRouter implements EmbeddingRoutingUseCase { + private final EmbeddingSwitchPort switchPort; + + public EmbeddingRouter(EmbeddingSwitchPort switchPort) { + this.switchPort = switchPort; + } + + @Override + public void switchEmbedding(String adapterBeanName) { + log("임베딩 스위치 요청 수신 - 어댑터명: " + adapterBeanName); + switchPort.switchTo(adapterBeanName); + } + + @Override + public String getAdapterBeanName() { + log("현재 선택된 어댑터 빈 이름 요청"); + return switchPort.getAdapterBeanName(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java new file mode 100644 index 0000000..0e01bf3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java @@ -0,0 +1,6 @@ +package com.likelion.backendplus4.yakplus.switcher.application.port.in; + +public interface EmbeddingRoutingUseCase { + void switchEmbedding(String adapterBeanName); + String getAdapterBeanName(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java new file mode 100644 index 0000000..75b74bc --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java @@ -0,0 +1,6 @@ +package com.likelion.backendplus4.yakplus.switcher.application.port.out; + +public interface EmbeddingSwitchPort { + void switchTo(String adapterBeanName); + String getAdapterBeanName(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java new file mode 100644 index 0000000..b6335ca --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java @@ -0,0 +1,63 @@ +package com.likelion.backendplus4.yakplus.switcher.infrastructure.route.adapter; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +@Component("embeddingRouterAdapter") +@Primary +public class EmbeddingRouterAdapter implements EmbeddingLoadingPort, EmbeddingSwitchPort { + private static final String DEFAULT_ADAPTER = "gptEmbeddingLoadingAdapter"; + private final Map adapters; + private volatile EmbeddingLoadingPort embeddingLoadingPort; + private volatile String adapterBeanName; + + public EmbeddingRouterAdapter(Map allAdapters) { + this.adapters = allAdapters; + log("구현체 목록: " + adapters.keySet()); + } + + @PostConstruct + public void init() { + log("EmbeddingRouterAdapter 초기화 - 어댑터명: " + DEFAULT_ADAPTER); + switchTo(DEFAULT_ADAPTER); + } + + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + if (embeddingLoadingPort == null) { + log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); + throw new IllegalStateException("No adapter selected"); + } + return embeddingLoadingPort.loadEmbeddingsByPage(pageable); + } + + @Override + public void switchTo(String adapterBeanName) { + log("어댑터 스위치 시도 - 어댑터명: " + adapterBeanName); + EmbeddingLoadingPort target = adapters.get(adapterBeanName); + if (target == null) { + log(LogLevel.ERROR, "어댑터 빈을 찾을 수 없습니다: " + adapterBeanName); + throw new IllegalArgumentException("Unknown adapter: " + adapterBeanName); + } + this.embeddingLoadingPort = target; + this.adapterBeanName = adapterBeanName; + log("어댑터 스위치 완료 - 현재 어댑터: " + adapterBeanName); + } + + @Override + public String getAdapterBeanName() { + log("어댑터 빈 이름 요청 - 현재 선택된 어댑터: " + adapterBeanName); + return adapterBeanName; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java new file mode 100644 index 0000000..825b657 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.switcher.presentation.controller; + +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.switcher.application.port.in.EmbeddingRoutingUseCase; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +@RestController +@RequestMapping("/switch/embeddings") +public class EmbeddingRouterController { + private final EmbeddingRoutingUseCase routerUseCase; + + public EmbeddingRouterController(EmbeddingRoutingUseCase routerUseCase) { + this.routerUseCase = routerUseCase; + } + + @PostMapping("/switch/{adapterBeanName}") + public ResponseEntity> switchAdapter(@PathVariable String adapterBeanName) { + log("스위치 대상 인덱스명 : " + adapterBeanName); + routerUseCase.switchEmbedding(adapterBeanName); + return ApiResponse.success("어댑터 변경됨 - 어댑터명: " + adapterBeanName); + } + + @GetMapping("/current/adapter") + public ResponseEntity> checkCurrentAdapter() { + return ApiResponse.success(routerUseCase.getAdapterBeanName()); + } +} From aa20e77139f85f731fb671b1274e22681a7566bb Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Fri, 2 May 2025 15:43:11 +0900 Subject: [PATCH 39/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Refactor:=20?= =?UTF-8?q?=EC=9E=84=EB=B2=A0=EB=94=A9=20=EC=96=B4=EB=8E=81=ED=84=B0=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jpa/GovDrugGptEmbedJpaRepository.java | 14 +++++++ .../jpa/GovDrugKmBertEmbedJpaRepository.java | 14 +++++++ .../jpa/GovDrugKrSbertEmbedJpaRepository.java | 14 +++++++ .../port/out/GovDrugRawDataPort.java | 2 - .../application/service/DrugIndexer.java | 7 +++- .../persistence/GovDrugRawDataAdapter.java | 7 +--- .../GptEmbeddingLoadingAdapter.java | 39 ++++++++----------- .../KmBertEmbeddingLoadingAdapter.java | 33 +++++++++------- .../KrSBertEmbeddingLoadingAdapter.java | 33 +++++++++------- 9 files changed, 103 insertions(+), 60 deletions(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java index 652deb2..2a7501b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java @@ -1,10 +1,24 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; +import java.util.List; + @Repository public interface GovDrugGptEmbedJpaRepository extends JpaRepository { + @Query( + value = """ + SELECT r, e + FROM DrugGptEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.gptVector IS NOT NULL + """ + ) + List findRawAndEmbed(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java index 9598b20..fb5983e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java @@ -1,10 +1,24 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; +import java.util.List; + @Repository public interface GovDrugKmBertEmbedJpaRepository extends JpaRepository { + @Query( + value = """ + SELECT r, e + FROM DrugKmBertEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.kmBertVector IS NOT NULL + """ + ) + List findRawAndEmbed(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java index 8512bbc..a1364c0 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java @@ -1,10 +1,24 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; +import java.util.List; + @Repository public interface GovDrugKrSbertEmbedJpaRepository extends JpaRepository { + @Query( + value = """ + SELECT r, e + FROM DrugKrSbertEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.krSbertVector IS NOT NULL + """ + ) + List findRawAndEmbed(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java index 04a264a..3a0ffa9 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java @@ -9,8 +9,6 @@ public interface GovDrugRawDataPort { List fetchRawData(int i); - String getEsIndexName(); - Page findAllDrugs(Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index 4129c0b..3b21747 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -5,6 +5,7 @@ import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -30,6 +31,7 @@ public class DrugIndexer implements IndexUseCase { private final GovDrugRawDataPort govDrugRawDataPort; private final DrugIndexRepositoryPort drugIndexRepositoryPort; + private final EmbeddingSwitchPort embeddingSwitchPort; private static final String SORT_BY_PROPERTY = "drugId"; private static final int CHUNK_SIZE = 1_000; @@ -127,10 +129,13 @@ private List fetchRawData(int i) { * @return Elasticsearch 인덱스 이름 * @author 정안식 * @since 2025-04-27 + * @modified 2025-05-02 이해창
+ * 2025-05-02 - 하드코딩 된 문자를 받어오던 것을 + * 임베딩 모델 BeanName을 가져오도록 수정 */ private String getEsIndexName() { log("ES 인덱스 이름 조회"); - return govDrugRawDataPort.getEsIndexName(); + return embeddingSwitchPort.getAdapterBeanName(); } /** diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index 4e1a552..3848270 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -35,16 +35,11 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { private final GovDrugDetailJpaRepository rawDataJpaRepository; private final GovDrugJpaRepository drugJpaRepository; - private final EmbeddingLoadingPort embeddingLoadingPort; @Value("${gov.numOfRows}") private int numOfRows; - @Override - public String getEsIndexName() { - return "test-gpt"; - } /** @@ -145,6 +140,6 @@ public List fetchRawData(int pageNo) { private Pageable createPageable(int pageNo) { log("pageable 생성"); - return PageRequest.of(pageNo-1, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); + return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index a158457..4adba09 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -1,11 +1,14 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Primary; import org.springframework.data.domain.Pageable; @@ -22,33 +25,20 @@ @RequiredArgsConstructor public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; - private final GovDrugJpaRepository govDrugJpaRepository; @Override public List loadEmbeddingsByPage(Pageable pageable) { - List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); - List drugGptEmbedEntities = govDrugGptEmbedJpaRepository.findAll(pageable).getContent(); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 상세정보 수: " + rawDataEntities.size()); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 임베딩 벡터수: " + drugGptEmbedEntities.size()); - - // drugGptEmbedEntities를 Map으로 변환 (key: drugId) - Map gptEmbedMap = new HashMap<>(); - for (DrugGptEmbedEntity embed : drugGptEmbedEntities) { - log("loadEmbeddingsByPage - " +pageable.getPageNumber()+"페이지에서 임베딩 벡터 받아온 약품 ID: " + embed.getDrugId()); - gptEmbedMap.put(embed.getDrugId(), embed); - } - List drugs = new ArrayList<>(); - log("loadEmbeddingsByPage - Drug 도메인 객체 생성 시작"); - for (DrugRawDataEntity drugRawData : rawDataEntities) { - DrugGptEmbedEntity embed = gptEmbedMap.get(drugRawData.getDrugId()); - if(embed == null) { - log("loadEmbeddingsByPage - " + "Drug 도메인 객체 생성 대상 " + drugRawData.getDrugId() + "의 벡터가 없으므로 skip"); - continue; - } - log("loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 " + drugRawData.getDrugId() + "의 벡터 길이: " + embed.getGptVector().length()); - Drug drug = toDomainFromEntity(drugRawData, embed); - drugs.add(drug); + List rows = govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR,"loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + for (Object[] arr : rows) { + DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; + DrugGptEmbedEntity embed = (DrugGptEmbedEntity) arr[1]; + drugs.add(toDomainFromEntity(raw, embed)); } log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); return drugs; @@ -68,6 +58,9 @@ private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbe .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) .imageUrl(drugEntity.getImageUrl()) + .cancelDate(drugEntity.getCancelDate()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getGptVector())) .build(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java index 4a14a9d..f475a96 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java @@ -1,11 +1,14 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKmBertEmbedJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Pageable; @@ -16,29 +19,28 @@ import java.util.List; import java.util.Map; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + @Repository @RequiredArgsConstructor public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; - private final GovDrugJpaRepository govDrugJpaRepository; @Override public List loadEmbeddingsByPage(Pageable pageable) { - List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); - List drugKmBertEmbedEntities = govDrugKmBertEmbedJpaRepository.findAll(pageable).getContent(); - - // drugKmBertEmbedEntities를 Map으로 변환 (key: drugId) - Map kmBertEmbedMap = new HashMap<>(); - for (DrugKmBertEmbedEntity embed : drugKmBertEmbedEntities) { - kmBertEmbedMap.put(embed.getDrugId(), embed); - } - List drugs = new ArrayList<>(); - for (DrugRawDataEntity drugRawData : rawDataEntities) { - DrugKmBertEmbedEntity embed = kmBertEmbedMap.get(drugRawData.getDrugId()); - Drug drug = toDomainFromEntity(drugRawData, embed); - drugs.add(drug); + List rows = govDrugKmBertEmbedJpaRepository.findRawAndEmbed(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR,"loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + for (Object[] arr : rows) { + DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; + DrugKmBertEmbedEntity embed = (DrugKmBertEmbedEntity) arr[1]; + drugs.add(toDomainFromEntity(raw, embed)); } + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); return drugs; } @@ -57,6 +59,9 @@ private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKmBertE .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) .imageUrl(drugEntity.getImageUrl()) + .cancelDate(drugEntity.getCancelDate()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getKmBertVector())) .build(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java index 576fafe..978a0c3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java @@ -1,11 +1,14 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKrSbertEmbedJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Pageable; @@ -16,29 +19,28 @@ import java.util.List; import java.util.Map; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + @Repository @RequiredArgsConstructor public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; - private final GovDrugJpaRepository govDrugJpaRepository; @Override public List loadEmbeddingsByPage(Pageable pageable) { - List rawDataEntities = govDrugJpaRepository.findAll(pageable).getContent(); - List drugKrSBertEmbedEntities = govDrugKrSbertEmbedJpaRepository.findAll(pageable).getContent(); - - // drugKrSBertEmbedEntities를 Map으로 변환 (key: drugId) - Map krSBertEmbedMap = new HashMap<>(); - for (DrugKrSbertEmbedEntity embed : drugKrSBertEmbedEntities) { - krSBertEmbedMap.put(embed.getDrugId(), embed); - } - List drugs = new ArrayList<>(); - for (DrugRawDataEntity drugRawData : rawDataEntities) { - DrugKrSbertEmbedEntity embed = krSBertEmbedMap.get(drugRawData.getDrugId()); - Drug drug = toDomainFromEntity(drugRawData, embed); - drugs.add(drug); + List rows = govDrugKrSbertEmbedJpaRepository.findRawAndEmbed(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR,"loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + for (Object[] arr : rows) { + DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; + DrugKrSbertEmbedEntity embed = (DrugKrSbertEmbedEntity) arr[1]; + drugs.add(toDomainFromEntity(raw, embed)); } + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); return drugs; } @@ -57,6 +59,9 @@ private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKrSbert .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) .imageUrl(drugEntity.getImageUrl()) + .cancelDate(drugEntity.getCancelDate()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getKrSbertVector())) .build(); } From fcd117c6f931f461c6c3b9b4213ea3201b450983 Mon Sep 17 00:00:00 2001 From: HaechangLee <112938092+HaechangLee@users.noreply.github.com> Date: Fri, 2 May 2025 18:28:09 +0900 Subject: [PATCH 40/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Refactor:=20?= =?UTF-8?q?=EC=9E=84=EB=B2=A0=EB=94=A9=20=EC=A0=80=EC=9E=A5/=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=ED=8F=AC=ED=8A=B8=ED=86=B5=ED=95=A9=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scraper/embed/DrugEmbedProcessor.java | 23 ++------- .../controller/DrugDataTestController.java | 2 - .../port/out/EmbeddingLoadingPort.java | 4 ++ .../port/out/GovDrugRawDataPort.java | 3 +- .../application/service/DrugIndexer.java | 39 ++++++++------- .../persistence/GovDrugRawDataAdapter.java | 24 +++++++--- .../GptEmbeddingLoadingAdapter.java | 48 ++++++++++++++----- .../KmBertEmbeddingLoadingAdapter.java | 39 +++++++++++++-- .../KrSBertEmbeddingLoadingAdapter.java | 43 +++++++++++++++-- .../EmbeddingUtil/EmbedEntityBuilder.java | 27 +++++++++++ .../route/adapter/EmbeddingRouterAdapter.java | 18 +++++++ 11 files changed, 206 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java index f4531b6..e26dfc5 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java @@ -4,17 +4,15 @@ import java.util.List; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugEmbedRepositoryPort; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; @@ -32,7 +30,7 @@ @RequiredArgsConstructor public class DrugEmbedProcessor implements DrugEmbedProcessorUseCase { private final GovDrugRawDataPort drugRawDataPort; - private final EmbeddingPort embeddingPort; + private final EmbeddingLoadingPort embeddingLoadingPort; private final DrugEmbedRepositoryPort embedRepositoryPort; private volatile EmbeddingModelType currentEmbeddingModel = EmbeddingModelType.OPENAI; // 기본 모델 @@ -53,22 +51,9 @@ public void startEmbedding() { // 임베딩 벡터를 생성하고 저장하는 공통 메서드 private void saveVector(Drug detail, String text) { // 현재 선택된 임베딩 모델에 따라 벡터를 생성 - float[] vector = embeddingPort.getEmbedding(text, currentEmbeddingModel); - + float[] vector = embeddingLoadingPort.getEmbedding(text); // 모델에 따른 저장 처리 - switch (currentEmbeddingModel) { - case OPENAI: - embedRepositoryPort.saveGptEmbed(detail.getDrugId(), vector); - break; - case KM_BERT: - embedRepositoryPort.saveKmBertEmbed(detail.getDrugId(), vector); - break; - case SBERT: - embedRepositoryPort.saveKrSbertEmbed(detail.getDrugId(), vector); - break; - default: - throw new IllegalArgumentException("Unknown embedding model: " + currentEmbeddingModel); - } + embeddingLoadingPort.saveEmbedding(detail.getDrugId(),vector); } /** diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java index cd3d876..2ae6634 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java @@ -10,12 +10,10 @@ import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; import com.likelion.backendplus4.yakplus.drug.application.service.scraper.DrugScraper; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; import lombok.RequiredArgsConstructor; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.response.ApiResponse; @RestController @RequiredArgsConstructor diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java index 66abc62..c3558c3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java @@ -7,4 +7,8 @@ public interface EmbeddingLoadingPort { List loadEmbeddingsByPage(Pageable pageable); + + float[] getEmbedding(String text); + + void saveEmbedding(Long drugId, float[] embedding); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java index 3a0ffa9..622f70c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java @@ -7,8 +7,9 @@ import java.util.List; public interface GovDrugRawDataPort { - List fetchRawData(int i); + List fetchRawData(int pageNo, int numOfRows); Page findAllDrugs(Pageable pageable); + long getDrugTotalSize(); } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java index 3b21747..045884f 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java @@ -40,26 +40,31 @@ public class DrugIndexer implements IndexUseCase { * ES 인덱스에 저장한다. * * @author 정안식 - * @modified 2025-04-28 - * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 + * @modified 2025-05-02 이해창 + * 25.05.02 - 저장된 약물 상세정보 데이터 크기를 기준으로 ES에 색인하는 loop를 만들도록 수정 * 25.04.28 - IndexRequest를 인자로 더 이상 받지 않도록 수정 + * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 * @since 2025-04-22 */ @Override public void index() { log("index 서비스 요청 수신"); -// Pageable pageable = createPageable(request.limit()); - try { - for (int i = 1; i <= 50; i++) { - List drugs = fetchRawData(i); - String esIndexName = getEsIndexName(); - saveDrugs(esIndexName, drugs); + String esIndexName = getEsIndexName(); + long totalDataSize = govDrugRawDataPort.getDrugTotalSize(); + int totalPages = (int) ((totalDataSize + CHUNK_SIZE - 1) / CHUNK_SIZE); // 전체 페이지 수 계산 (올림) + List drugs; + for(int currentPage = 0; currentPage < totalPages; currentPage++) { + log("색인 시작: page=" + currentPage); + try{ + drugs = fetchRawData(currentPage, CHUNK_SIZE); + } catch (Exception e) { + log(LogLevel.ERROR, String.format("%d 페이지 색인용 데이터 로딩 실패", currentPage), e); + continue; } - } catch (Exception e) { - log(LogLevel.ERROR,"indexing 시 데이터 5000개 보다 적어서 에러 발생", e); - e.printStackTrace(); + log(" 조회 완료: page=" + currentPage + ", 건수=" + drugs.size()); + saveDrugs(esIndexName, drugs); + log(" 색인 완료: page=" + currentPage + ", 건수=" + drugs.size()); } - } /** @@ -111,16 +116,18 @@ private Pageable createPageable(int limit) { /** * RDB에서 lastSeq 이후의 원시 데이터를 조회하여 도메인 객체로 변환한다. * - * @param i for문 내부에서 동작하는 i값(pageable의 pageNumber) + * @param pageNum 현재 페이지 번호 + * @param numOfRows 한 페이지당 조회할 건수 * @return 도메인 모델 리스트 * @author 정안식 - * @modified 2025-04-28 + * @modified 2025-05-02 이해창
+ * 25.05.02 - 페이징 처리 시 페이지 사이즈 받도록 수정
* 25.04.28 - 페이징 처리 로직 수정 * @since 2025-04-22 */ - private List fetchRawData(int i) { + private List fetchRawData(int pageNum, int numOfRows) { log("RDB에서 원시 데이터 조회"); - return govDrugRawDataPort.fetchRawData(i); + return govDrugRawDataPort.fetchRawData(pageNum, numOfRows); } /** diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java index 3848270..d1bbf0b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java @@ -37,10 +37,6 @@ public class GovDrugRawDataAdapter implements GovDrugRawDataPort { private final GovDrugJpaRepository drugJpaRepository; private final EmbeddingLoadingPort embeddingLoadingPort; - @Value("${gov.numOfRows}") - private int numOfRows; - - /** * 주어진 Pageable 정보에 따라 DB에서 한 페이지 분량의 GovDrugEntity를 조회하고, @@ -131,15 +127,29 @@ private Drug mapToDrugDomain(DrugDetailEntity entity) { @Override - public List fetchRawData(int pageNo) { + public List fetchRawData(int pageNo, int numOfRows) { log("index 서비스 요청 수신"); - Pageable pageable = createPageable(pageNo); + Pageable pageable = createPageable(pageNo, numOfRows); List drugs = embeddingLoadingPort.loadEmbeddingsByPage(pageable); return drugs; } - private Pageable createPageable(int pageNo) { + private Pageable createPageable(int pageNo, int numOfRows) { log("pageable 생성"); return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); } + + + /** + * JPA 레포지토리를 이용해 GovDrugJpaRepository의 전체 데이터 수를 조회합니다. + * + * @return GovDrugJpaRepository의 전체 데이터 수 + * @author 이해창 + * @since 2025-05-02 + * @modified + */ + @Override + public long getDrugTotalSize() { + return drugJpaRepository.count(); + } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index 4adba09..0135079 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -1,23 +1,29 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Primary; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.Embedding; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; @@ -25,25 +31,46 @@ @RequiredArgsConstructor public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; + private final OpenAiApi openAiApi; @Override public List loadEmbeddingsByPage(Pageable pageable) { List drugs = new ArrayList<>(); List rows = govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); if (rows.isEmpty()) { - log(LogLevel.ERROR,"loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); } for (Object[] arr : rows) { - DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; - DrugGptEmbedEntity embed = (DrugGptEmbedEntity) arr[1]; + DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; + DrugGptEmbedEntity embed = (DrugGptEmbedEntity) arr[1]; drugs.add(toDomainFromEntity(raw, embed)); } log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); return drugs; } + @Override + public float[] getEmbedding(String text) { + OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( + this.openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model("text-embedding-3-small") + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE); + EmbeddingResponse embeddingResponse = openAiEmbeddingModel + .embedForResponse(List.of(text)); + Embedding embedding = embeddingResponse.getResults().getFirst(); + return embedding.getOutput(); + } + + @Override + public void saveEmbedding(Long drugId, float[] embedding) { + govDrugGptEmbedJpaRepository.save(EmbedEntityBuilder.buildEmbedEntity(drugId, embedding, DrugGptEmbedEntity.class)); + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbedEntity embedEntity) { return Drug.builder() .drugId(drugEntity.getDrugId()) @@ -59,12 +86,11 @@ private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbe .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) .imageUrl(drugEntity.getImageUrl()) .cancelDate(drugEntity.getCancelDate()) - .cancelName(drugEntity.getCancelName()) - .isHerbal(drugEntity.getIsHerbal()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getGptVector())) .build(); } - } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java index f475a96..db6a854 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java @@ -2,22 +2,26 @@ import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKmBertEmbedJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Repository; +import org.springframework.web.client.RestTemplate; +import java.net.URI; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; @@ -25,6 +29,8 @@ @RequiredArgsConstructor public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; + private final ApiUriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; @Override public List loadEmbeddingsByPage(Pageable pageable) { @@ -45,6 +51,31 @@ public List loadEmbeddingsByPage(Pageable pageable) { return drugs; } + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + @Override + public void saveEmbedding(Long drugId, float[] embedding) { + govDrugKmBertEmbedJpaRepository.save(EmbedEntityBuilder.buildEmbedEntity(drugId, embedding, DrugKmBertEmbedEntity.class)); + } + + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKmbertEmbeding(); + } + + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKmBertEmbedEntity embedEntity) { return Drug.builder() .drugId(drugEntity.getDrugId()) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java index 978a0c3..7a2ef12 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java @@ -2,6 +2,8 @@ import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; @@ -9,11 +11,17 @@ import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Repository; +import org.springframework.web.client.RestTemplate; +import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -25,19 +33,21 @@ @RequiredArgsConstructor public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; + private final ApiUriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; @Override public List loadEmbeddingsByPage(Pageable pageable) { List drugs = new ArrayList<>(); List rows = govDrugKrSbertEmbedJpaRepository.findRawAndEmbed(pageable); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); if (rows.isEmpty()) { - log(LogLevel.ERROR,"loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); } for (Object[] arr : rows) { - DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; - DrugKrSbertEmbedEntity embed = (DrugKrSbertEmbedEntity) arr[1]; + DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; + DrugKrSbertEmbedEntity embed = (DrugKrSbertEmbedEntity) arr[1]; drugs.add(toDomainFromEntity(raw, embed)); } log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); @@ -45,6 +55,31 @@ public List loadEmbeddingsByPage(Pageable pageable) { return drugs; } + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + @Override + public void saveEmbedding(Long drugId, float[] embedding) { + govDrugKrSbertEmbedJpaRepository.save(EmbedEntityBuilder.buildEmbedEntity(drugId, embedding, DrugKrSbertEmbedEntity.class)); + } + + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKrSbertEmbeding(); + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKrSbertEmbedEntity embedEntity) { return Drug.builder() .drugId(drugEntity.getDrugId()) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java new file mode 100644 index 0000000..b7271d1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class EmbedEntityBuilder { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static T buildEmbedEntity(Long drugId, float[] vector, Class clazz) { + try { + return clazz.getDeclaredConstructor(Long.class, String.class) + .newInstance(drugId, toStringFromFloatArray(vector)); + } catch (Exception e) { + //TODO: 엔터티 생성 실패 + throw new RuntimeException(e); + } + } + + private static String toStringFromFloatArray(float[] vector) { + try { + return MAPPER.writeValueAsString(vector); + } catch (JsonProcessingException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java index b6335ca..17cff83 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java @@ -60,4 +60,22 @@ public String getAdapterBeanName() { log("어댑터 빈 이름 요청 - 현재 선택된 어댑터: " + adapterBeanName); return adapterBeanName; } + + @Override + public float[] getEmbedding(String text) { + if (embeddingLoadingPort == null) { + log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); + throw new IllegalStateException("No adapter selected"); + } + return embeddingLoadingPort.getEmbedding(text); + } + + @Override + public void saveEmbedding(Long drugId, float[] embedding) { + if (embeddingLoadingPort == null) { + log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); + throw new IllegalStateException("No adapter selected"); + } + embeddingLoadingPort.saveEmbedding(drugId, embedding); + } } From 53db068918710ed33b1a210de40149968208c25b Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Sat, 3 May 2025 13:17:53 +0900 Subject: [PATCH 41/47] =?UTF-8?q?=E2=9C=A8=20Feature:=20#68=20=EC=8A=A4?= =?UTF-8?q?=ED=94=84=EB=A7=81=20=EB=B0=B0=EC=B9=98=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?(#69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feature: 스프링 배치 의존성 및 Job으로 전환 * †✨Feature: 테이블 병합 실패 로직 추가 및 불필요 파일 삭제 * ♻️ Refactor: 패키지 구조 수정 및 기타 오류 수정 * 📦 Chore: PR템플릿 위치 변경 * 🐛 Fix: embed Step 오류 수정 --- ...t_template.md => PULL_REQUEST_TEMPLATE.md} | 0 build.gradle | 4 + .../embed/DrugEmbedProcessorService.java | 51 +++++ .../service}/exception/ScraperException.java | 2 +- .../exception/error/ScraperErrorCode.java | 2 +- .../service/port/in/DrugCombineUsecase.java | 15 -- .../port/in/DrugEmbedProcessorUseCase.java | 32 --- .../port/in/DrugImageScraperUsecase.java | 23 -- .../service/port/in/DrugScraperUsecase.java | 13 -- .../in/embed/DrugEmbedProcessorUseCase.java | 62 +++++ .../DrugScraperDetailUseCase.java} | 22 +- .../in/scraper/DrugScraperImageUsecase.java | 37 +++ .../DrugScraperTableCombineUsecase.java | 29 +++ .../port/in/scraper/DrugScraperUsecase.java | 46 ++++ .../service/port/out/ApiRequestPort.java | 48 ---- .../service/port/out/BatchJobPort.java | 171 ++++++++++++++ .../port/out/DrugDetailRepositoryPort.java | 46 ---- .../port/out/DrugEmbedRepositoryPort.java | 40 ---- .../port/out/DrugImageRepositoryPort.java | 40 ---- .../port/out/DrugRawDataRepositoryPort.java | 30 --- .../service/port/out/EmbeddingPort.java | 24 -- .../service/scraper/DrugScraper.java | 40 ---- .../service/scraper/DrugScraperService.java | 34 +++ .../DrugScraperTableCombineService.java | 29 +++ .../scraper/combiner/DrugCombiner.java | 105 --------- .../detail/DrugDetailScraperService.java | 198 ---------------- .../detail/DrugScraperDetailService.java | 29 +++ .../scraper/embed/DrugEmbedProcessor.java | 108 --------- .../scraper/image/DrugImageGovScraper.java | 72 ------ .../image/DrugScraperServiceImage.java | 29 +++ .../yakplus/drug/domain/model/Drug.java | 31 --- .../yakplus/drug/domain/model/DrugDetail.java | 31 --- .../yakplus/drug/domain/model/DrugImage.java | 25 --- .../drug/domain/model/DrugRawData.java | 28 --- .../drug/domain/model/vo/Material.java | 8 - .../drug/domain/model/vo/MaterialInfo.java | 22 -- .../api/adapter/ApiRequestAdapter.java | 61 ----- .../api/exception/RestApiError.java | 33 +++ .../api/exception/RestApiException.java | 18 ++ .../api/support/ApiPageCounter.java | 48 ---- .../api/support/ApiResponseMapper.java | 39 ---- .../api/util/ApiRequestManager.java | 158 +++++++++++++ .../UriCompBuilder.java} | 72 +++--- .../batch/adapter/BatchJobAdapter.java | 111 +++++++++ .../batch/adapter/JobManager.java | 189 ++++++++++++++++ .../combine/config/CombineJobConfig.java | 19 ++ .../combine/config/CombineStepConfig.java | 85 +++++++ .../batch/combine/dto/TableCombineDto.java | 27 +++ .../processor/TableCombineProcessor.java} | 129 +++++------ .../combine/writer/TableCombineWriter.java | 34 +++ .../batch/common/config/BatchConfig.java | 51 +++++ .../batch/common/config/CommonJobConfig.java | 51 +++++ .../batch/common/util}/MaterialParser.java | 6 +- .../batch/common/util/MdcTaskDecorator.java | 37 +++ .../batch/common/util/StepSkipDecider.java | 21 ++ .../batch/common/util}/XMLParser.java | 9 +- .../batch/detail/config/DetailJobConfig.java | 24 ++ .../batch/detail/config/DetailStepConfig.java | 122 ++++++++++ .../detail}/dto/DrugDetailRequest.java | 2 +- .../processor/DetailTotalPageCalculator.java | 30 +++ .../detail/processor/DrugDetailProcessor.java | 90 ++++++++ .../detail/reader/DetailPageNumberReader.java | 51 +++++ .../batch/detail/writer/DrugDetailWriter.java | 28 +++ .../batch/embed/config/EmbedJobConfig.java | 31 +++ .../batch/embed/config/EmbedStepConfig.java | 211 ++++++++++++++++++ .../batch/embed/dto/DrugVectorDto.java | 11 + .../batch/embed/processor/EmbedProcessor.java | 51 +++++ .../embed/reader/DrugIdRangePartitioner.java | 42 ++++ .../batch/embed/writer/EmbedWriter.java | 34 +++ .../batch/exception/ParserBatchError.java | 34 +++ .../batch/exception/ParserBatchException.java | 18 ++ .../batch/image/config/ImageJobConfig.java | 22 ++ .../batch/image/config/ImageStepConfig.java | 136 +++++++++++ .../image/processor/ImageScrapProcessor.java | 49 ++++ .../processor/ImageTotalPageCalculator.java | 31 +++ .../image/reader/ImagePageNumberReader.java | 53 +++++ .../image/reader/PageRangePartitioner.java | 36 +++ .../image/reader/PartitionedPageReader.java | 34 +++ .../batch/image/writer/DrugImageWriter.java | 31 +++ .../batch/mapper/ApiResponseMapper.java | 45 ++++ .../mapper/DrugDetailRequestMapper.java | 4 +- .../embedding/adapter/EmbeddingAdapter.java | 27 --- .../client/KmBertEmbeddingClient.java | 4 +- .../client/KrSbertEmbeddingClient.java | 4 +- .../out/DrugDetailRepositoryAdapter.java | 50 ----- .../out/DrugEmbedRepositoryAdapter.java | 91 -------- .../out/DrugImageRepositoryAdapter.java | 44 ---- .../out/DrugRawDataRepositoryAdapter.java | 38 ---- .../persistence/dto/DrugImageRequest.java | 28 +++ .../entity/ApiDataDrugImgEntity.java | 12 +- .../jdbc/GovDrugJdbcRepository.java | 69 ------ .../repository/jdbc/JdbcBatchSetter.java | 48 ---- .../repository/jdbc/MergeBatchSetter.java | 49 ---- .../repository/jpa/GovDrugJpaRepository.java | 7 + .../support/mapper/DrugImageMapper.java | 30 --- .../mapper/DrugImageRequestMapper.java | 14 ++ .../support/mapper/DrugRawDataMapper.java | 4 +- .../controller/DrugDataTestController.java | 56 ----- .../controller/DrugDetailController.java | 27 --- .../controller/DrugImageController.java | 19 -- .../controller/embed/DrugEmbedController.java | 67 ++++++ .../embed/dto/ModelSwitchRequeset.java | 15 ++ .../embed/model/embedModelController.java | 60 +++++ .../scraper/DrugScraperController.java | 48 ++++ .../DrugScraperTableCombineController.java | 66 ++++++ .../details/DrugScraperDetailController.java | 66 ++++++ .../image/DrugScraperImageController.java | 67 ++++++ .../port/out/EmbeddingLoadingPort.java | 12 +- .../adapter/persistence/DrugMapper.java | 16 ++ .../GptEmbeddingLoadingAdapter.java | 151 +++++++------ .../KmBertEmbeddingLoadingAdapter.java | 14 +- .../KrSBertEmbeddingLoadingAdapter.java | 19 +- .../EmbeddingUtil/EmbedEntityBuilder.java | 5 +- .../route/adapter/EmbeddingRouterAdapter.java | 5 +- .../support/mapper/TempDrugRawDataMapper.java | 4 +- src/main/resources/application.yml | 7 + 116 files changed, 3279 insertions(+), 1908 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE/pull_request_template.md => PULL_REQUEST_TEMPLATE.md} (100%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{domain => application/service}/exception/ScraperException.java (85%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{domain => application/service}/exception/error/ScraperErrorCode.java (94%) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/{DrugDetailScraperUsecase.java => scraper/DrugScraperDetailUseCase.java} (50%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/{support/ApiUriCompBuilder.java => util/UriCompBuilder.java} (71%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{support/mapper/DrugDetailMapper.java => batch/combine/processor/TableCombineProcessor.java} (59%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/scraper/detail/support => infrastructure/batch/common/util}/MaterialParser.java (92%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/scraper/detail/support => infrastructure/batch/common/util}/XMLParser.java (96%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{persistence => batch/detail}/dto/DrugDetailRequest.java (95%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/{support => batch}/mapper/DrugDetailRequestMapper.java (80%) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/build.gradle b/build.gradle index 3df4e2c..8d88829 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,10 @@ dependencies { // OpenAi implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M5' + + // Spring-Batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + // build.gradle if (project.hasProperty('env') && project.env == 'test') { dependencies { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java new file mode 100644 index 0000000..9b44a16 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.embed; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; + +import lombok.RequiredArgsConstructor; + +/** + * DrugEmbedProcessorService는 약품 효능에 대한 텍스트를 임베딩하는 작업을 처리하는 서비스입니다. + * 이 서비스는 임베딩 작업의 시작, 중지, 상태 확인 및 임베딩 모델 스위칭 기능을 제공합니다. + * batchJobPort: 배치 작업 실행을 위한 포트 + * switchPort: 임베딩 모델 스위치를 위한 포트 + * + * @since 2025-04-25 + * @modify 2025-05-02 함예정 + * - 스프링 배치로 전환, 배치 포트로 외부서비스 요청하도록 변경 + */ +@Service +@RequiredArgsConstructor +public class DrugEmbedProcessorService implements DrugEmbedProcessorUseCase { + private final BatchJobPort batchJobPort; + private final EmbeddingSwitchPort switchPort; + + @Override + public String startEmbedding() { + return batchJobPort.embedJobStart(); + } + + @Override + public void switchEmbeddingModel(String modelType) { + switchPort.switchTo(modelType); + } + + @Override + public String getCurrentEmbeddingModel() { + return switchPort.getAdapterBeanName(); + } + + @Override + public String statusEmbedding() { + return batchJobPort.embedjobStatus(); + } + + @Override + public String stopEmbedding() { + return batchJobPort.embedJobStop(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/ScraperException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/ScraperException.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/ScraperException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/ScraperException.java index 0e10278..c692dd3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/ScraperException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/ScraperException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.domain.exception; +package com.likelion.backendplus4.yakplus.drug.application.service.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/error/ScraperErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/error/ScraperErrorCode.java similarity index 94% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/error/ScraperErrorCode.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/error/ScraperErrorCode.java index 32f627d..777ed4e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/exception/error/ScraperErrorCode.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/error/ScraperErrorCode.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.domain.exception.error; +package com.likelion.backendplus4.yakplus.drug.application.service.exception.error; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java deleted file mode 100644 index fbb3dca..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugCombineUsecase.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in; - -import jakarta.transaction.Transactional; - -public interface DrugCombineUsecase { - - /** - * API 요청으로 받아온 RAW 데이터 테이블 2개를 병합해 - * 1개의 테이블로 만드는 기능을 수행합니다. - * - * @since 2025-04-21 - */ - @Transactional - void mergeTable(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java deleted file mode 100644 index 7accc0e..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugEmbedProcessorUseCase.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; - -/** - * 의약품 효능 정보를 임베딩 처리를 위한 유스케이스 인터페이스입니다. - * - * @since 2025-04-25 - */ -public interface DrugEmbedProcessorUseCase { - - /** - * 의약품 데이터를 기반으로 임베딩 프로세스를 시작합니다. - * - * @since 2025-04-25 - */ - void startEmbedding(); - - /** - * 임베딩 모델을 스위칭하는 메서드입니다. - * - * @param modelType 전환할 임베딩 모델 타입 (GPT, KmBERT, KrSBERT) - */ - void switchEmbeddingModel(String modelType); - - /** - * 현재 사용 중인 임베딩 모델을 조회하는 메서드입니다. - * - * @return 현재 사용 중인 임베딩 모델 - */ - EmbeddingModelType getCurrentEmbeddingModel(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java deleted file mode 100644 index 2d33d8f..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugImageScraperUsecase.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in; - -/** - * 의약품 이미지 정보를 수집하는 유스케이스 인터페이스입니다. - * API를 통해 단일 페이지 또는 전체 이미지 데이터를 수집하는 기능을 정의합니다. - */ -public interface DrugImageScraperUsecase { - - /** - * 주어진 페이지 번호에 해당하는 의약품 이미지 데이터를 외부 API로부터 수집하여 저장합니다. - * - * @param pageNumber 수집할 페이지 번호 - * @since 2025-04-21 - */ - void getApiData(int pageNumber); - - /** - * 모든 의약품 이미지 정보를 외부 API를 통해 일괄 수집합니다. - * - * @since 2025-04-21 - */ - void getAllApiData(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java deleted file mode 100644 index 17fa370..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugScraperUsecase.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in; - -public interface DrugScraperUsecase { - - /** - * 의약품 데이터 수집 및 임베딩 프로세스를 순차적으로 실행합니다. - * 1. 상세 정보 수집 후 RDB 테이블 저장 - * 2. 이미지 정보 수집 후 RDB 테이블 저장 - * 3. 상세 정보와 이미지 병합 후 통합 테이블 저장 - * 4. 임베딩 벡터 생성 및 각각 벡터 테이블 저장 - */ - void scraperStart(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java new file mode 100644 index 0000000..bfe9d13 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java @@ -0,0 +1,62 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed; + +/** + * 의약품 효능 정보를 임베딩 처리를 위한 유스케이스 인터페이스입니다. + * + * @since 2025-04-25 + * @modify 2025-05-02 함예정 + * - 스프링 배치로 전환, 배치 포트로 외부서비스 요청하도록 변경 + */ +public interface DrugEmbedProcessorUseCase { + + /** + * 의약품 데이터를 기반으로 임베딩 프로세스를 시작합니다. + * + * @since 2025-04-25 + * @modify 2025-05-02 함예정 + */ + String startEmbedding(); + + /** + * 임베딩 모델을 전환합니다. + * + * @param modelType 전환할 임베딩 모델의 타입 + * (예: gptEmbeddingLoadingAdapter, kmBertEmbeddingLoadingAdapter, krSBertEmbeddingLoadingAdapter) + * + * @author 정안식 + * @since 2025-04-25 + * @modify 2025-05-02 함예정 + */ + void switchEmbeddingModel(String modelType); + + /** + * 현재 사용 중인 임베딩 모델을 조회하는 메서드입니다. + * + * @return 현재 사용 중인 임베딩 모델 + * + * @author 정안식 + * @since 2025-04-25 + * @modify 2025-05-02 함예정 + */ + String getCurrentEmbeddingModel(); + + /** + * 현재 임베딩 작업의 상태를 조회합니다. + * + * @return 임베딩 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String statusEmbedding(); + + /** + * 실행 중인 임베딩 작업을 중지합니다. + * + * @return 임베딩 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String stopEmbedding(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperDetailUseCase.java similarity index 50% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperDetailUseCase.java index a012c48..fb36bf3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/DrugDetailScraperUsecase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperDetailUseCase.java @@ -1,22 +1,30 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in; +package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; /** * 의약품 상세 정보를 수집하는 유스케이스 인터페이스입니다. - * 1페이지 요청 및 전체 데이터 요청을 정의합니다. * * @since 2025-04-21 */ -public interface DrugDetailScraperUsecase { +public interface DrugScraperDetailUseCase { /** - * 특정 페이지의 의약품 상세 정보를 요청하여 수집합니다. + * 모든 의약품의 상세 정보를 일괄적으로 요청하여 수집합니다. * * @since 2025-04-21 */ - void requestSingleData(int pageNumber); + String requestAllData(); /** - * 모든 의약품의 상세 정보를 일괄적으로 요청하여 수집합니다. + * 실행 중인 상세 정보 조회 배치를 중단합니다. + * + * @author 함예정 + * @since 2025-04-21 + */ + String stop(); + + /** + * 실행 중인 상세 정보 조회 상태를 가져옵니다. * + * @author 함예정 * @since 2025-04-21 */ - void requestAllData(); + String getStatus(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java new file mode 100644 index 0000000..b5819fd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; + +/** + * 의약품 이미지 정보를 수집하는 유스케이스 인터페이스입니다. + * + * @since 2025-04-21 + */ +public interface DrugScraperImageUsecase { + + /** + * 의약품 이미지 데이터를 수집하는 배치 작업을 시작합니다. + * + * @return 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-04-21 + * @modify 2025-05-02 + * - 스프링 배치 형태로 수정 + */ + String requestAllData(); + + /** + * 진행 중인 이미지 수집 배치 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * @since 2025-05-02 + */ + String stop(); + + /** + * 이미지 수집 작업의 현재 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @since 2025-05-02 + */ + String getStatus(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java new file mode 100644 index 0000000..7194171 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; + +public interface DrugScraperTableCombineUsecase { + + /** + * API 요청으로 받아온 RAW 데이터 테이블 2개를 병합해 + * 1개의 테이블로 만드는 기능을 수행합니다. + * + * @author 함예정 + * @since 2025-04-21 + */ + String mergeTable(); + + /** + * 현재 작업 상태를 조회합니다 + * + * @author 함예정 + * @since 2025-05-02 + */ + String getStatus(); + + /** + * 현재 진행 중인 작업을 중지합니다. + * + * @author 함예정 + * @since 2025-05-02 + */ + String stop(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java new file mode 100644 index 0000000..da2ac3b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; + +public interface DrugScraperUsecase { + + /** + * 의약품 데이터 수집 및 임베딩 프로세스를 순차적으로 실행합니다. + * 1. 상세 정보 수집 후 RDB 테이블 저장 + * 2. 이미지 정보 수집 후 RDB 테이블 저장 + * 3. 상세 정보와 이미지 병합 후 통합 테이블 저장 + * 4. 임베딩 벡터 생성 및 각각 벡터 테이블 저장 + * + * @author 함예정 + * @since 2025-04-21 + */ + String scraperStart(); + + /** + * 현재 스크래핑 및 임베딩 작업의 상태를 조회합니다. + * + * @return 현재 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String getStatus(); + + /** + * 실행 중인 스크래핑 및 임베딩 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String stop(); + + /** + * 중지된 모든 배치 작업을 조회하고, 다시 실행합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String restart(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java deleted file mode 100644 index b1f978d..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/ApiRequestPort.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * 외부 API로부터 의약품 상세 정보 및 이미지 데이터를 요청하는 포트 인터페이스입니다. - * 페이징 처리된 데이터를 수집하고, 전체 페이지 수를 조회하는 기능을 제공합니다. - */ -public interface ApiRequestPort { - - /** - * 주어진 페이지 번호에 해당하는 의약품 상세 정보를 외부 API로부터 조회합니다. - * - * @param pageNo 조회할 페이지 번호 - * @return 해당 페이지의 의약품 상세 정보를 포함하는 JsonNode - * - * @since 2025-04-21 - */ - JsonNode getAllDetailData(int pageNo); - - /** - * 주어진 페이지 번호에 해당하는 의약품 이미지 정보를 외부 API로부터 조회합니다. - * - * @param pageNo 조회할 페이지 번호 - * @return 해당 페이지의 의약품 이미지 정보를 포함하는 JsonNode - * - * @since 2025-04-21 - */ - JsonNode getAllImageData(int pageNo); - - /** - * 의약품 상세 정보의 전체 페이지 수를 외부 API로부터 조회합니다. - * - * @return 상세 정보의 총 페이지 수 - * - * @since 2025-04-21 - */ - int getDetailTotalPageCount(); - - /** - * 의약품 이미지 정보의 전체 페이지 수를 외부 API로부터 조회합니다. - * - * @return 이미지 정보의 총 페이지 수 - * - * @since 2025-04-21 - */ - int getImageTotalPageCount(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java new file mode 100644 index 0000000..e30b596 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java @@ -0,0 +1,171 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.port.out; + +/** + * 의약품 데이터 수집 및 임베딩과 관련된 배치 작업을 제어하기 위한 포트 인터페이스입니다. + * 각 배치 작업의 시작, 중지, 상태 조회 기능을 정의합니다. + * + * @since 2025-05-02 + */ +public interface BatchJobPort { + + /** + * 전체 배치 작업을 순차적으로 시작합니다. + * + * @return 전체 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String allJobStart(); + + /** + * 전체 배치 작업을 중지합니다. + * + * @return 전체 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String allJobStop(); + + /** + * 중지된 전체 배치 작업을 재개합니다. + * + * @return 전체 작업 재개 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String allJobResume(); + + + /** + * 전체 배치 작업의 상태를 조회합니다. + * + * @return 전체 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String allJobStatus(); + + /** + * 상세 정보 스크래핑 작업을 시작합니다. + * + * @return 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String detailScrapJobStart(); + + /** + * 상세 정보 스크래핑 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String detailScrapJobStop(); + + /** + * 상세 정보 스크래핑 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String detailScrapJobStatus(); + + /** + * 이미지 스크래핑 작업을 시작합니다. + * + * @return 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String imageScrapJobStart(); + + /** + * 이미지 스크래핑 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String imageScrapJobStop(); + + /** + * 이미지 스크래핑 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String imageScrapJobStatus(); + + /** + * 상세 정보와 이미지 데이터를 병합하는 작업을 시작합니다. + * + * @return 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String tableCombineJobStart(); + + /** + * 테이블 병합 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String tableCombineJobStop(); + + /** + * 테이블 병합 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String tableCombineJobStatus(); + + /** + * 임베딩 작업을 시작합니다. + * + * @return 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String embedJobStart(); + + /** + * 임베딩 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String embedJobStop(); + + /** + * 임베딩 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + String embedjobStatus(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java deleted file mode 100644 index d34ee51..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugDetailRepositoryPort.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; - -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; - -/** - * 의약품 상세 정보를 저장 및 조회하는 리포지토리 포트 인터페이스입니다. - * 개별 및 대량 저장 기능과 저장된 전체 데이터를 조회하는 기능을 제공합니다. - * - * @since 2025-04-21 - */ -public interface DrugDetailRepositoryPort { - /** - * 단일 의약품 상세 정보를 저장합니다. - * - * @param e 저장할 의약품 상세 정보 저장 요청 객체 - * - * @since 2025-04-21 - */ - void saveDrugDetail(DrugDetailRequest e); - - /** - * 의약품 상세 정보를 일괄 저장합니다. - * - * @param list 저장할 의약품 상세 정보 저장 요청 객체 리스트 - * - * @since 2025-04-21 - */ - void saveDrugDetailBulk(List list); - - /** - * 저장된 모든 의약품 상세 정보를 조회합니다. - * - * @return 의약품 상세 정보 리스트 - * - * @since 2025-04-21 - */ - List getAllGovDrugDetail(); - - Page getGovDrugDetailByPage(Pageable pageable); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java deleted file mode 100644 index 9c0be8d..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugEmbedRepositoryPort.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; - -/** - * 의약품 임베딩 벡터를 저장 및 조회하는 리포지토리 포트 인터페이스입니다. - * 다양한 임베딩 모델(GPT, KmBERT, KorSBERT)의 벡터를 저장하고 조회하는 기능을 제공합니다. - * - * @since 2025-04-21 - */ -public interface DrugEmbedRepositoryPort { - - /** - * GPT 기반 임베딩 벡터를 저장합니다. - * - * @param drugId 임베딩할 의약품 ID - * @param vector GPT 임베딩 벡터 - * - * @since 2025-04-21 - */ - void saveGptEmbed(Long drugId, float[] vector); - - /** - * KmBERT 기반 임베딩 벡터를 저장합니다. - * - * @param drugId 임베딩할 의약품 ID - * @param gptVector KmBERT 임베딩 벡터 - * - * @since 2025-04-21 - */ - void saveKmBertEmbed(Long drugId, float[] gptVector); - - /** - * KrSBERT 기반 임베딩 벡터를 저장합니다. - * - * @param drugId 임베딩할 의약품 ID - * @param krSbertVector KorSBERT 임베딩 벡터 - * - * @since 2025-04-21 - */ - void saveKrSbertEmbed(Long drugId, float[] krSbertVector); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java deleted file mode 100644 index 33e121c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugImageRepositoryPort.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; - -import java.util.List; - -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; -/** - * 의약품 이미지 정보를 저장 및 조회하는 리포지토리 포트 인터페이스입니다. - * 이미지 데이터의 조회 및 일괄 저장 기능을 제공합니다. - * - * @since 2025-04-21 - */ -public interface DrugImageRepositoryPort { - /** - * 저장된 모든 의약품 이미지 정보를 조회합니다. - * - * @return List 의약품 이미지 정보 리스트 - * - * @since 2025-04-21 - */ - List getAllGovDrugImage(); - - /** - * 의약품 ID로 단일 의약품 이미지 정보를 조회합니다. - * - * @param drugId 조회할 의약품 ID - * @return 해당 의약품의 이미지 정보 - * - * @since 2025-04-21 - */ - DrugImage getById(Long drugId); - - /** - * 의약품 이미지 정보를 일괄 저장하고 즉시 반영(flush)합니다. - * - * @param imgData 저장할 의약품 이미지 정보 리스트 - * - * @since 2025-04-21 - */ - void saveAllAndFlush(List imgData); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java deleted file mode 100644 index ff9d5ca..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/DrugRawDataRepositoryPort.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; - -import java.util.List; - -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; -/** - * 의약품 원시 데이터를 저장하는 리포지토리 포트 인터페이스입니다. - * - * @since 2025-04-21 - */ -public interface DrugRawDataRepositoryPort { - /** - * 의약품 원시 데이터를 저장합니다. - * - * @param rawData 저장할 의약품 원시 데이터 - * - * @since 2025-04-21 - */ - void save(DrugRawData rawData); - - - /** - * 여러 의약품 원시 데이터를 일괄 저장합니다. - * - * @param rawData 저장할 의약품 원시 데이터 리스트 - * - * @since 2025-04-21 - */ - void saveAll(List rawData); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java deleted file mode 100644 index 52e1723..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/EmbeddingPort.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; - -/** - * 텍스트 임베딩을 위한 포트 인터페이스입니다. - * 다양한 임베딩 모델을 사용하여 텍스트를 벡터(float 배열)로 변환합니다. - * - * @since 2025-04-25 - */ -public interface EmbeddingPort { - - /** - * 주어진 텍스트를 지정한 임베딩 모델을 사용하여 임베딩 벡터(float 배열)로 변환합니다. - * - * @param text 임베딩할 텍스트 - * @param modelType 사용할 임베딩 모델 타입 (enum) - * @return 임베딩된 벡터 (float 배열) - * - * @author 이해창 - * @since 2025-04-25 - */ - float[] getEmbedding(String text, EmbeddingModelType modelType); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java deleted file mode 100644 index e3a6b65..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraper.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper; - -import org.springframework.stereotype.Service; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugDetailScraperUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugImageScraperUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugScraperUsecase; - -import lombok.RequiredArgsConstructor; - -/** - * @class DrugScraper - * @description - * 약품 관련 데이터 수집을 담당하는 서비스 클래스 - * 1) 약품 상세 정보 - * 2) 약품 이미지 - * 위 항목을 순차적으로 수집하고 전처리 작업을 거쳐 하나의 테이블로 저장한다. - * 약품 효능 값을 임베딩하여 각 임베딩 모델 별로 저장한다. - * - * @since 2025-04-21 - */ -@Service -@RequiredArgsConstructor -public class DrugScraper implements DrugScraperUsecase{ - private final DrugImageScraperUsecase drugImageScraperUsecase; - private final DrugDetailScraperUsecase drugDetailScraperUsecase; - private final DrugCombineUsecase drugCombineUsecase; - private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; - - @Override - public void scraperStart(){ - drugDetailScraperUsecase.requestAllData(); - drugImageScraperUsecase.getAllApiData(); - drugCombineUsecase.mergeTable(); - drugEmbedProcessorUseCase.startEmbedding(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java new file mode 100644 index 0000000..ba597d2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DrugScraperService implements DrugScraperUsecase { + private final BatchJobPort batchJobPort; + + @Override + public String scraperStart() { + return batchJobPort.allJobStart(); + } + + @Override + public String stop() { + return batchJobPort.allJobStop(); + } + + @Override + public String restart() { + return batchJobPort.allJobResume(); + } + + @Override + public String getStatus() { + return batchJobPort.allJobStatus(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java new file mode 100644 index 0000000..409a3f9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.combine; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperTableCombineUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DrugScraperTableCombineService implements DrugScraperTableCombineUsecase { + private final BatchJobPort batchJobPort; + + @Override + public String mergeTable() { + return batchJobPort.tableCombineJobStart(); + } + + @Override + public String getStatus() { + return batchJobPort.tableCombineJobStatus(); + } + + @Override + public String stop() { + return batchJobPort.tableCombineJobStop(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java deleted file mode 100644 index fdc31a7..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combiner/DrugCombiner.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.combiner; - -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Component; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugImageRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugRawDataRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; - -/** - * 의약품 상세 정보와 이미지 정보를 결합하여 원시 데이터를 생성하는 컴포넌트입니다. - * {@link DrugCombineUsecase}를 구현하며, 데이터 병합 및 저장 기능을 제공합니다. - * - * @since 2025-04-21 - */ -@Component -@RequiredArgsConstructor -public class DrugCombiner implements DrugCombineUsecase { - private final DrugDetailRepositoryPort drugDetailRepository; - private final DrugImageRepositoryPort drugImageRepositoryPort; - private final DrugRawDataRepositoryPort drugRawDataRepositoryPort; - - public void mergeTable() { - log("API 요청 결과 테이블 병합 시작: 상세 정보 + 이미지"); - - int pageSize = 1_000; - Page firstPage = drugDetailRepository.getGovDrugDetailByPage(PageRequest.of(0, pageSize)); - int totalPages = firstPage.getTotalPages(); - - processDrugDetails(firstPage.getContent(), 1); - - for (int i = 1; i < totalPages; i++) { - Page page = drugDetailRepository.getGovDrugDetailByPage(PageRequest.of(i, pageSize)); - processDrugDetails(page.getContent(), i + 1); - } - - log("API 요청 결과 테이블 병합 완료"); - } - - private void processDrugDetails(List drugDetails, int pageNumber) { - log("Processing Page = " + pageNumber); - log(LogLevel.DEBUG, "DrugDetail Raw Data (Page " + pageNumber + "): \n" + drugDetails); - - List rawData = drugDetails.stream() - .map(detail -> buildMergeRawData(detail, getImageDataByDrugDetail(detail))).toList(); - drugRawDataRepositoryPort.saveAll(rawData); - } - - /** - * 주어진 의약품 상세 정보에 해당하는 이미지 정보를 조회합니다. - * - * @param detail 의약품 상세 정보 - * @return 해당 의약품의 이미지 정보 - * - * @since 2025-04-21 - */ - private DrugImage getImageDataByDrugDetail(DrugDetail detail) { - log(LogLevel.DEBUG, "의약품 이미지 정보 요청 \n detail: " + detail); - DrugImage imageData = drugImageRepositoryPort.getById(detail.getDrugId()); - log(LogLevel.DEBUG, "의약품 이미지 정보 (상세 정보로 Search): " + imageData); - return imageData; - } - - /** - * 의약품 상세 정보와 이미지 정보를 병합하여 {@link DrugRawData} 객체를 생성합니다. - * - * @param d 의약품 상세 정보 - * @param i 의약품 이미지 정보 - * @return 병합된 의약품 원시 데이터 - * - * @since 2025-04-21 - */ - private DrugRawData buildMergeRawData(DrugDetail d, DrugImage i) { - DrugRawData rawData = DrugRawData.builder() - .drugId(d.getDrugId()) - .drugName(d.getDrugName()) - .company(d.getCompany()) - .permitDate(d.getPermitDate()) - .isGeneral(d.isGeneral()) - .materialInfo(d.getMaterialInfo()) - .storeMethod(d.getStoreMethod()) - .validTerm(d.getValidTerm()) - .efficacy(d.getEfficacy()) - .usage(d.getUsage()) - .precaution(d.getPrecaution()) - .imageUrl(i.getImageUrl()) - .cancelDate(d.getCancelDate()) - .cancelName(d.getCancelName()) - .isHerbal(d.isHerbal()) - .build(); - return rawData; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java deleted file mode 100644 index a976f17..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugDetailScraperService.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugDetailScraperUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.ApiRequestPort; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; -import com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support.MaterialParser; -import com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support.XMLParser; - -import lombok.RequiredArgsConstructor; - -/** - * 의약품 상세 정보를 외부 API로부터 수집하여 저장하는 서비스 클래스입니다. - * {@link DrugDetailScraperUsecase}를 구현하며, - * 단일 페이지 또는 전체 데이터를 처리할 수 있습니다. - * - * @since 2025-04-21 - */ - -@Component -@RequiredArgsConstructor -public class DrugDetailScraperService implements DrugDetailScraperUsecase { - private final ApiRequestPort apiRequestPort; - private final ObjectMapper objectMapper; - private final DrugDetailRepositoryPort drugDetailRepository; - - @Override - public void requestSingleData(int pageNumber) { - JsonNode items = apiRequestPort.getAllDetailData(pageNumber); - - log("API 응답 처리 시작 - Drug Detail"); - List drugs = toListFromJson(items); - log("API 응답 처리 완료 - Drug Detail"); - log(LogLevel.DEBUG, "완료 데이터 : \n" + drugs); - - drugDetailRepository.saveDrugDetailBulk(drugs); - } - - @Override - public void requestAllData() { - int totalPageCount = apiRequestPort.getDetailTotalPageCount(); - int receivedCount = 0; - int savedCountWithoutDuplicates = 0; - - log("전체 API 데이터 수집 시작 - Drug Detail"); - - for (int i = 1; i <= totalPageCount; i++) { - JsonNode items = apiRequestPort.getAllDetailData(i); - List drugs = toListFromJson(items); - receivedCount += drugs.size(); - - // item_seq 기준 중복 제거된 약품 개수 유지 (실제 db에 저장된 데이터와 같은 지 비교용) - int uniqueItems = deduplicateByItemSeq(drugs); - savedCountWithoutDuplicates += uniqueItems; - - drugDetailRepository.saveDrugDetailBulk(drugs); - - log("Page: " + i - + "received: " + drugs.size() - + "saved (unique): " + uniqueItems - + "totalReceived: " + receivedCount - + "totalUniqueSaved: " + savedCountWithoutDuplicates - ); - } - } - - /** - * 외부 API로부터 받은 JSON 데이터를 {@link DrugDetailRequest} 리스트로 변환하고, - * 각 항목의 상세 정보를 가공하여 반환합니다. - * - * @param items API에서 수신한 JSON 노드 - * @return {@link DrugDetailRequest} 리스트 - * - * @author 함예정, 이해창 - * @since 2025-04-21 - */ - private List toListFromJson(JsonNode items) { - - log("API 응답 > DrugDetailRequest 객체 변환 시작"); - try { - List apiDataDrugDetails = changeTypeToList(items); - for (int i = 0; i < apiDataDrugDetails.size(); i++) { - DrugDetailRequest drugDetail = apiDataDrugDetails.get(i); - JsonNode item = items.get(i); - log(LogLevel.DEBUG, "item seq: " + item.get("ITEM_SEQ").asText()); - - log(LogLevel.DEBUG, "약품 성분 파싱 시작"); - String materialRawData = item.get("MATERIAL_NAME").asText(); - log(LogLevel.DEBUG, "약품 성분 Raw 데이터 조회 성공: \n" + materialRawData); - - String materialInfo = MaterialParser.parseMaterial(materialRawData); - log(LogLevel.DEBUG, "약품 성분 파싱 성공: \n" + materialInfo); - - drugDetail.changeMaterialInfo(materialInfo); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 성분 저장 완료: \n" + drugDetail); - - log(LogLevel.DEBUG, "약품 효능 데이터 파싱 시작"); - String efficacyXmlText = item.get("EE_DOC_DATA").asText(); - log(LogLevel.DEBUG, "약품 효능 Raw 데이터 조회 성공: \n" + efficacyXmlText); - - String efficacy = XMLParser.toJson(efficacyXmlText); - log(LogLevel.DEBUG, "약품 효능 파싱 성공: \n" + efficacy); - - drugDetail.changeEfficacy(efficacy); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 효능 저장 완료: \n" + drugDetail); - - log(LogLevel.DEBUG, "약품 사용법 데이터 파싱 시작"); - String usageXmlText = items.get(i).get("UD_DOC_DATA").asText(); - log(LogLevel.DEBUG, "약품 사용법 Raw 데이터 조회 성공: \n" + usageXmlText); - - String usages = XMLParser.toJson(usageXmlText); - log(LogLevel.DEBUG, "약품 사용법 파싱 성공: \n" + usages); - - drugDetail.changeUsage(usages); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 사용법 저장 완료: \n" + drugDetail); - - log(LogLevel.DEBUG, "약품 주의사항 데이터 파싱 시작"); - String precautionxmlText = items.get(i).get("NB_DOC_DATA").asText(); - log(LogLevel.DEBUG, "약품 주의사항 Raw 데이터 조회 성공: \n" + precautionxmlText); - - String precautions = XMLParser.toJson(precautionxmlText); - log(LogLevel.DEBUG, "약품 주의사항 파싱 성공: \n" + precautions); - - drugDetail.changePrecaution(precautions); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 주의사항 저장 완료: \n" + drugDetail); - - String precaution = drugDetail.getPrecaution(); - if(precaution != null) { - if(precaution.contains("한의사") || precaution.contains("한약사")){ - drugDetail.changeIsHerbal(true); - } - } - } - return apiDataDrugDetails; - } catch (Exception e) { - throw new ScraperException(ScraperErrorCode.API_DRUG_DETAIL_PARSING_FAIL); - } - } - - /** - * JSON 노드를 {@link DrugDetailRequest} 리스트로 변환합니다. - * - * @param items 변환할 JSON 노드 - * @return {@link DrugDetailRequest} 리스트 - * - * @author 함예정, 이해창 - * @since 2025-04-21 - */ - private List changeTypeToList(JsonNode items) { - try { - return objectMapper.readValue(items.toString(), - new TypeReference>() { - }); - } catch (JsonProcessingException e) { - throw new ScraperException(ScraperErrorCode.RESPONSE_TYPE_CHANGE_FAIL); - } - } - // TODO: 추후 삭제 예정 - // private String replaceText(String text){ - // return text.replace("ᆞ ", "&") - // .replace("• ","") - // .replace("〜 ", "~"); - // } - - /** - * 의약품 상세 정보 리스트에서 중복되지 않는 항목 수를 계산합니다. - * 중복 기준은 item_seq (drugId)입니다. - * - * @param drugs 중복 제거 대상 {@link DrugDetailRequest} 리스트 - * @return 중복 제거 후 고유 항목 수 - * - * @author 이해창 - * @since 2025-04-21 - */ - private int deduplicateByItemSeq(List drugs) { - Set uniqueItems = new HashSet<>(); - - for (DrugDetailRequest drug : drugs) { - uniqueItems.add(drug.getDrugId()); - } - return uniqueItems.size(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java new file mode 100644 index 0000000..7ed032d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperDetailUseCase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugScraperDetailService implements DrugScraperDetailUseCase { + private final BatchJobPort batchJobPort; + + @Override + public String requestAllData() { + return batchJobPort.detailScrapJobStart(); + } + + @Override + public String stop() { + return batchJobPort.detailScrapJobStop(); + } + + @Override + public String getStatus() { + return batchJobPort.detailScrapJobStatus(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java deleted file mode 100644 index e26dfc5..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/embed/DrugEmbedProcessor.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.embed; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import java.util.List; - -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugEmbedRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; - -import lombok.RequiredArgsConstructor; - -/** - * 의약품 상세 정보를 기반으로 다양한 임베딩 모델을 이용하여 - * 벡터를 생성하고 저장하는 프로세서 클래스입니다. - * - * {@link DrugEmbedProcessorUseCase}를 구현하며, GPT, KmBERT, KorSBERT 임베딩을 수행합니다. - * - * @since 2025-04-25 - */ -@Service -@RequiredArgsConstructor -public class DrugEmbedProcessor implements DrugEmbedProcessorUseCase { - private final GovDrugRawDataPort drugRawDataPort; - private final EmbeddingLoadingPort embeddingLoadingPort; - private final DrugEmbedRepositoryPort embedRepositoryPort; - - private volatile EmbeddingModelType currentEmbeddingModel = EmbeddingModelType.OPENAI; // 기본 모델 - - @Override - public void startEmbedding() { - log("약품 효능 임베딩 작업 시작"); - Page firstPage = getAllItem(0); - for (int i = 0; i < firstPage.getTotalPages(); i++) { - getAllItem(i).forEach(data -> { - String efficacy = convertSingleStringForEfficacy(data.getEfficacy()); - saveVector(data, efficacy); - }); - } - log("약품 효능 임베딩 작업 완료"); - } - - // 임베딩 벡터를 생성하고 저장하는 공통 메서드 - private void saveVector(Drug detail, String text) { - // 현재 선택된 임베딩 모델에 따라 벡터를 생성 - float[] vector = embeddingLoadingPort.getEmbedding(text); - // 모델에 따른 저장 처리 - embeddingLoadingPort.saveEmbedding(detail.getDrugId(),vector); - } - - /** - * 효능 정보를 단일 문자열로 변환합니다. - * - * @param stringList 효능 정보 리스트 - * @return 공백으로 결합된 단일 문자열 - * - * @since 2025-04-25 - */ - private String convertSingleStringForEfficacy(List stringList) { - log(LogLevel.DEBUG, "약품 효능 정보 단일 문자로 변환 시작"); - StringBuilder stringBuilder = new StringBuilder(); - for (String s : stringList) { - stringBuilder.append(s); - stringBuilder.append(" "); - } - - String s = stringBuilder.toString(); - log(LogLevel.DEBUG, "약품 효능 정보 단일 문자로 변환 완료" + s); - return s; - } - - /** - * 저장된 모든 의약품 상세 정보를 조회합니다. - * - * @return 의약품 상세 정보 리스트 - * - * @since 2025-04-25 - */ - private Page getAllItem(int i) { - return drugRawDataPort.findAllDrugs(PageRequest.of(i, 100)); - } - - /** - * 임베딩 모델을 스위칭하는 메서드입니다. - * - * @param modelType 전환할 임베딩 모델 타입 (GPT, KmBERT, KrSBERT) - */ - @Override - public void switchEmbeddingModel(String modelType) { - // 유효하지 않으면 기본값(OPENAI)로 처리 - this.currentEmbeddingModel = EmbeddingModelType.valueOf(modelType); // valueOf로 직접 변환 - log("임베딩 모델 스위치 완료 - 현재 모델: " + currentEmbeddingModel); - } - - @Override - public EmbeddingModelType getCurrentEmbeddingModel() { - log("현재 사용 중인 임베딩 모델 조회 - 현재 모델: " + currentEmbeddingModel); - return currentEmbeddingModel; // Enum 반환 - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java deleted file mode 100644 index ec07fee..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugImageGovScraper.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.image; - -import java.util.List; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugImageScraperUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.ApiRequestPort; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugImageRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; -import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; - -import lombok.RequiredArgsConstructor; - -/** - * 정부 API로부터 의약품 이미지 정보를 수집하여 저장하는 서비스 클래스입니다. - * {@link DrugImageScraperUsecase}를 구현하며, - * 단일 페이지 또는 전체 페이지의 이미지 데이터를 처리합니다. - */ -@Service -@RequiredArgsConstructor -public class DrugImageGovScraper implements DrugImageScraperUsecase { - private final ApiRequestPort apiRequestPort; - private final DrugImageRepositoryPort drugImageRepositoryPort; - private final ObjectMapper objectMapper; - - @Override - public void getApiData(int pageNumber){ - JsonNode items = apiRequestPort.getAllImageData(pageNumber); - List imgData = changeTypeToList(items); - drugImageRepositoryPort.saveAllAndFlush(imgData); - } - - @Override - public void getAllApiData(){ - int totalPageCount = apiRequestPort.getImageTotalPageCount(); - for(int i=1;i<=totalPageCount;i++){ - JsonNode items = apiRequestPort.getAllImageData(i); - List imgData = null; - try { - imgData = objectMapper.readValue(items.toString(), - new TypeReference>() {}); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - drugImageRepositoryPort.saveAllAndFlush(imgData); - } - } - - /** - * JsonNode 형태의 데이터를 {@link DrugImage} 리스트로 변환합니다. - * 변환 실패 시 {@link ScraperException}을 발생시킵니다. - * - * @param items 변환할 JSON 데이터 - * @return {@link DrugImage} 리스트 - */ - private List changeTypeToList(JsonNode items) { - List imgData; - try { - imgData = objectMapper.readValue(items.toString(), - new TypeReference>() {}); - } catch (JsonProcessingException e) { - throw new ScraperException(ScraperErrorCode.RESPONSE_TYPE_CHANGE_FAIL); - } - return imgData; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java new file mode 100644 index 0000000..60b0508 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.application.service.scraper.image; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperImageUsecase; +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugScraperServiceImage implements DrugScraperImageUsecase { + private final BatchJobPort batchJobPort; + + @Override + public String requestAllData() { + return batchJobPort.imageScrapJobStart(); + } + + @Override + public String stop() { + return batchJobPort.imageScrapJobStop(); + } + + @Override + public String getStatus() { + return batchJobPort.imageScrapJobStatus(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java index eeb94ce..ba1ec85 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java @@ -13,55 +13,24 @@ /** * 의약품 정보를 담는 도메인 객체입니다. */ - -//TODO swagger/ @ApiModel(description = "의약품 정보") @Builder @Getter @ToString public class Drug { - - //TODO @ApiModelProperty(value = "의약품 ID", example = "12345") private Long drugId; - - //TODO @ApiModelProperty(value = "의약품명", example = "타이레놀") private String drugName; - - //TODO @ApiModelProperty(value = "제조사명", example = "한국얀센") private String company; - - //TODO @ApiModelProperty(value = "허가일자", example = "2023-01-01") private LocalDate permitDate; - - //TODO @ApiModelProperty(value = "일반의약품 여부", example = "true") private boolean isGeneral; - - //TODO @ApiModelProperty(value = "성분 정보 리스트") private List materialInfo; - - //TODO @ApiModelProperty(value = "보관 방법", example = "밀폐용기, 실온 보관") private String storeMethod; - - //TODO @ApiModelProperty(value = "유효 기간", example = "36개월") private String validTerm; - - //TODO @ApiModelProperty(value = "효능 효과", example = "[\"해열\", \"진통\"]") private List efficacy; - - //TODO @ApiModelProperty(value = "사용 방법", example = "[\"1일 3회\", \"식후 복용\"]") private List usage; - - //TODO @ApiModelProperty(value = "주의 사항", example = "{\"주의사항\": [\"임산부 주의\", \"운전 금지\"]}") private Map> precaution; - - //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") private String imageUrl; - - //TODO @ApiModelProperty(value = "[float 배열]") private float[] vector; - private LocalDate cancelDate; - private String cancelName; - private boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java deleted file mode 100644 index 9318f24..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugDetail.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model; - -import java.time.LocalDate; -import java.util.List; -import java.util.Map; - -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; - -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - -@Builder -@Getter -@ToString -public class DrugDetail { - private Long drugId; - private String drugName; - private String company; - private LocalDate permitDate; - private boolean isGeneral; - private List materialInfo; - private String storeMethod; - private String validTerm; - private List efficacy; - private List usage; - private Map> precaution; - private LocalDate cancelDate; - private String cancelName; - private boolean isHerbal; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java deleted file mode 100644 index e15031a..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugImage.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; - -/** - * 의약품 이미지 정보를 담는 객체입니다. - */ - -@Builder -@Getter -@ToString -//TODO swagger / @ApiModel(description = "의약품 이미지 정보") -public class DrugImage { - @JsonProperty("ITEM_SEQ") - //TODO @ApiModelProperty(value = "의약품 ID", example = "12345") - private Long drugId; - - @JsonProperty("BIG_PRDT_IMG_URL") - //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") - private String imageUrl; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java index 5d461ce..7f5ccff 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java @@ -14,51 +14,23 @@ /** * 외부 API로부터 수집된 의약품 원시 데이터를 담는 객체입니다. */ - -//TODO swagger/ @ApiModel(description = "의약품 원시 데이터") @Builder @Getter @ToString public class DrugRawData { - //TODO @ApiModelProperty(value = "의약품 ID", example = "12345") private Long drugId; - - //TODO @ApiModelProperty(value = "의약품명", example = "타이레놀") private String drugName; - - //TODO @ApiModelProperty(value = "제조사명", example = "한국얀센") private String company; - - //TODO @ApiModelProperty(value = "허가일자", example = "2023-01-01") private LocalDate permitDate; - - //TODO @ApiModelProperty(value = "일반의약품 여부", example = "true") private boolean isGeneral; - - //TODO @ApiModelProperty(value = "성분 정보 리스트") private List materialInfo; - - //TODO @ApiModelProperty(value = "보관 방법", example = "실온 보관") private String storeMethod; - - //TODO @ApiModelProperty(value = "유효 기간", example = "36개월") private String validTerm; - - //TODO @ApiModelProperty(value = "효능 효과", example = "[\"해열\", \"진통\"]") private List efficacy; - - //TODO @ApiModelProperty(value = "사용 방법", example = "[\"1일 3회\", \"식후 복용\"]") private List usage; - - //TODO @ApiModelProperty(value = "주의 사항", example = "{\"주의사항\": [\"임산부 주의\", \"운전 금지\"]}") private Map> precaution; - - //TODO @ApiModelProperty(value = "의약품 이미지 URL", example = "http://example.com/image.jpg") private String imageUrl; - private LocalDate cancelDate; - private String cancelName; - private boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java index d116085..0d5d9e5 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java @@ -8,35 +8,27 @@ /** * 의약품 성분 정보를 나타내는 값 객체(Value Object)입니다. */ -//TODO: swagger/ @ApiModel(description = "의약품 성분 정보") @Getter @ToString public class Material { - //TODO @ApiModelProperty(value = "성분명", example = "아세트아미노펜") @JsonProperty("성분명") private String name; - //TODO @ApiModelProperty(value = "분량", example = "500") @JsonProperty("분량") private String amount; - //TODO @ApiModelProperty(value = "단위", example = "mg") @JsonProperty("단위") private String unit; - //TODO @ApiModelProperty(value = "총량", example = "1000") @JsonProperty("총량") private String totalAmount; - //TODO @ApiModelProperty(value = "규격", example = "USP") @JsonProperty("규격") private String standard; - //TODO @ApiModelProperty(value = "비고", example = "해열진통제") @JsonProperty("비고") private String note; - //TODO @ApiModelProperty(value = "성분정보", example = "기타 부가 정보") @JsonProperty("성분정보") private String info; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java deleted file mode 100644 index 3717ecf..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.domain.model.vo; - -import java.util.List; - -import lombok.Getter; -import lombok.ToString; - -/** - * 의약품의 성분 총량 및 개별 성분 정보를 포함하는 객체입니다. - */ - -//TODO: swagger/ @ApiModel(description = "의약품 성분 정보 집합") -@Getter -@ToString -public class MaterialInfo { - - //TODO @ApiModelProperty(value = "총량", example = "1000") - private String totalAmount; - - //TODO @ApiModelProperty(value = "성분 리스트", example = "[{name: '아세트아미노펜', amount: '500', unit: '밀리그램'}]") - private List ingredients; -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java deleted file mode 100644 index 8e64322..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/adapter/ApiRequestAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.adapter; - -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -import com.fasterxml.jackson.databind.JsonNode; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.ApiRequestPort; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiPageCounter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiResponseMapper; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class ApiRequestAdapter implements ApiRequestPort { - private final RestTemplate restTemplate; - private final ApiUriCompBuilder apiUriCompBuilder; - private final ApiPageCounter apiPageCounter; - - @Override - public JsonNode getAllDetailData(int pageNo) { - - try { - String response = fetchDetailPage(pageNo); - return ApiResponseMapper.getItemsFromResponse(response); - } catch (Exception e){ - e.printStackTrace(); - return null; - } - } - - @Override - public JsonNode getAllImageData(int pageNo) { - try { - String response = fetchImagePage(pageNo); - return ApiResponseMapper.getItemsFromResponse(response); - } catch (Exception e){ - e.printStackTrace(); - return null; - } - } - - @Override - public int getDetailTotalPageCount() { - return apiPageCounter.getDetailApiTotalPageCount(); - } - - @Override - public int getImageTotalPageCount() { - return apiPageCounter.getImgApiTotalPageCount(); - } - - private String fetchDetailPage(int pageNo) { - return restTemplate.getForObject(apiUriCompBuilder.getUriForDetailApi(pageNo), String.class); - } - - private String fetchImagePage(int pageNo) { - return restTemplate.getForObject(apiUriCompBuilder.getUriForImgApi(pageNo), String.class); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java new file mode 100644 index 0000000..08f66db --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception; + +import org.springframework.http.HttpStatus; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum RestApiError implements ErrorCode { + PAGE_COUNT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 500001, "API 전체 페이지 개수를 확인하지 못했습니다."), + ITEM_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 500002, "API 응답에서 적절한 items를 추출하지 못했습니다."); + + private final HttpStatus httpStatus; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java new file mode 100644 index 0000000..80a2ad4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class RestApiException extends CustomException { + private final ErrorCode errorCode; + + public RestApiException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java deleted file mode 100644 index 12a4dd1..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiPageCounter.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.support; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -import java.net.URI; - -@Component -public class ApiPageCounter { - - private final RestTemplate restTemplate = new RestTemplate(); - private final ObjectMapper objectMapper = new ObjectMapper(); - private final ApiUriCompBuilder uriBuilder; - private final int numOfRows; - - public ApiPageCounter(ApiUriCompBuilder uriBuilder, - @Value("${gov.numOfRows}") int numOfRows) { - this.uriBuilder = uriBuilder; - this.numOfRows = numOfRows; - } - - public int getDetailApiTotalPageCount() { - URI uri = uriBuilder.getUriForDetailApiShort(); - return getPageCountFromUri(uri); - } - - public int getImgApiTotalPageCount() { - URI uri = uriBuilder.getUriForImgApiShort(); - return getPageCountFromUri(uri); - } - - private int getPageCountFromUri(URI uri) { - try { - String response = restTemplate.getForObject(uri, String.class); - int totalRows = objectMapper.readTree(response).path("body").path("totalCount").asInt(); - int pageCount = totalRows / numOfRows; - if (totalRows % numOfRows > 0) { - pageCount += 1; - } - return pageCount; - } catch (Exception e) { - throw new RuntimeException("페이지 수 계산 중 오류 발생", e); - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java deleted file mode 100644 index b5174ef..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiResponseMapper.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.support; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ApiResponseMapper { - - public static JsonNode getItemsFromResponse(String response) { - log.info("응답에서 items 값 추출"); - try { - return new ObjectMapper().readTree(response) - .path("body") - .path("items"); - } catch (JsonProcessingException e) { - log.error("items 추출 실패"); - log.error("response: {}", response); - return null; - } - } - - public static int getTotalCountFromResponse(String response) { - log.info("응답에서 데이터 사이즈 추출"); - try { - return new ObjectMapper().readTree(response) - .path("body") - .path("totalCount") - .asInt(); - } catch (JsonProcessingException e) { - log.error("totalCount 추출 실패"); - //TODO: CustomException 만들고, ControllerAdvice로 예외처리 필요 - throw new RuntimeException(e); - } - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java new file mode 100644 index 0000000..bb320e2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java @@ -0,0 +1,158 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.util; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception.RestApiError; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception.RestApiException; + +/** + * 공공의약품 API 요청을 처리하는 매니저 클래스입니다. + * 상세정보 및 이미지 데이터를 조회하고, 총 페이지 수 계산 및 응답 파싱 기능을 제공합니다. + * + * @since 2025-04-21 + */ +@Component +public class ApiRequestManager { + private final RestTemplate restTemplate; + private final UriCompBuilder uriCompBuilder; + private final ObjectMapper objectMapper; + private final int NUM_OF_ROWS; + + /** + * 생성자 주입을 통해 필요한 컴포넌트를 초기화합니다. + * + * @param restTemplate REST API 호출을 위한 RestTemplate + * @param uriCompBuilder API URI 조립 유틸리티 + * @param objectMapper JSON 파싱용 ObjectMapper + * @param NUM_OF_ROWS 페이지당 데이터 수 + */ + public ApiRequestManager(RestTemplate restTemplate, + UriCompBuilder uriCompBuilder, + ObjectMapper objectMapper, + @Value("${gov.numOfRows}") + int NUM_OF_ROWS) { + this.restTemplate = restTemplate; + this.uriCompBuilder = uriCompBuilder; + this.objectMapper = objectMapper; + this.NUM_OF_ROWS = NUM_OF_ROWS; + } + + /** + * 상세 정보 API 응답으로부터 전체 페이지 수를 계산합니다. + * + * @return 전체 페이지 수 + * + * @author 이해창 + * @since 2025-04-21 + */ + public int getDetailTotalPage() { + return getTotalPageCountFromResponse(fetchDetailData(1)); + } + + /** + * 이미지 정보 API 응답으로부터 전체 페이지 수를 계산합니다. + * + * @return 전체 페이지 수 + * + * @author 이해창 + * @since 2025-04-21 + */ + public int getImageTotalPage() { + return getTotalPageCountFromResponse(fetchImageData(1)); + } + + /** + * 의약품 ID를 통해 약품 이미지를 가져옵니다. + * + * @param drugId 의약품 고유 ID + * @return 이미지 데이터 jpeg(base64) + * + * @author 함예정 + * @since 2025-05-01 + */ + public String getImage(Long drugId) { + return restTemplate.getForObject("https://nedrug.mfds.go.kr/pbp/ezdrug/" + drugId.toString(), JsonNode.class) + .get("item") + .get("extimgImageDocid") + .asText(); + } + + /** + * 특정 페이지의 상세 정보를 API에서 가져옵니다. + * + * @param pageNo 페이지 번호 + * @return 상세 정보 JSON 문자열 + * + * @author 함예정 + * @since 2025-04-21 + */ + public String fetchDetailData(int pageNo) { + return restTemplate.getForObject(uriCompBuilder.getUriForDetailApi(pageNo), String.class); + } + + /** + * 특정 페이지의 낱알 이미지 정보를 API에서 가져옵니다. + * + * @param pageNo 페이지 번호 + * @return 낱알 이미지 정보 JSON 문자열 + * + * @author 함예정 + * @since 2025-04-21 + */ + public String fetchImageData(int pageNo) { + return restTemplate.getForObject(uriCompBuilder.getUriForImgApi(pageNo), String.class); + } + + /** + * 응답 문자열에서 items 노드를 추출합니다. + * + * @param response API 응답 문자열 + * @return items 노드, 실패 시 null + * + * @author 함예정 + * @since 2025-04-21 + */ + public JsonNode getItemsFromResponse(String response) { + log("응답에서 items 값 추출"); + try { + return new ObjectMapper().readTree(response) + .path("body") + .path("items"); + } catch (JsonProcessingException e) { + log(LogLevel.ERROR, "items 추출 실패"); + log(LogLevel.ERROR, "response: " + response); + throw new RestApiException(RestApiError.ITEM_NOT_FOUND); + } + } + + /** + * 전체 데이터 수에 기반하여 총 페이지 수를 계산합니다. + * + * @param response API 응답 문자열 + * @return 전체 페이지 수 + * + * @author 이해창 + * @since 2025-04-21 + */ + private int getTotalPageCountFromResponse(String response) { + try { + int totalRows = objectMapper.readTree(response).path("body").path("totalCount").asInt(); + int pageCount = totalRows / NUM_OF_ROWS; + if (totalRows % NUM_OF_ROWS > 0) { + pageCount += 1; + } + return pageCount; + } catch (Exception e) { + log(LogLevel.ERROR, "전체 페이지 개수 확인 실패"); + throw new RestApiException(RestApiError.PAGE_COUNT_FAIL); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiUriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/UriCompBuilder.java similarity index 71% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiUriCompBuilder.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/UriCompBuilder.java index 764e1dc..7548e41 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/support/ApiUriCompBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/UriCompBuilder.java @@ -1,11 +1,11 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.support; +package com.likelion.backendplus4.yakplus.drug.infrastructure.api.util; + +import java.net.URI; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import java.net.URI; - /*** * API 요청 URI 객체 생성 빌더 * @@ -13,10 +13,9 @@ * API HOST, PATH를 확인해 URI 객체를 만듭니다. * * @since 2025-04-15 - * @author 함예정 */ @Component -public class ApiUriCompBuilder { +public class UriCompBuilder { private final String SERVICE_KEY; private final String HOST; private final String API_DETAIL_PATH; @@ -25,7 +24,7 @@ public class ApiUriCompBuilder { private final String API_KM_BERT; private final String API_KR_SBERT; - public ApiUriCompBuilder(@Value("${gov.host}") String host, + public UriCompBuilder(@Value("${gov.host}") String host, @Value("${gov.serviceKey}") String serviceKey, @Value("${gov.path.detail}") String pathDetail, @Value("${gov.path.img}") String pathImg, @@ -47,10 +46,10 @@ public ApiUriCompBuilder(@Value("${gov.host}") String host, * @param path API 요청 경로 * @return URI * - * @since 2025-04-15 * @author 함예정 + * @since 2025-04-15 */ - private URI getUri(String path, int pageNo) { + private URI getUri(String path, int pageNo, int size) { return UriComponentsBuilder.newInstance() .scheme("https") .host(HOST) @@ -59,7 +58,7 @@ private URI getUri(String path, int pageNo) { .queryParam("serviceKey", SERVICE_KEY) .queryParam("type", "json") .queryParam("pageNo", pageNo) - .queryParam("numOfRows", NUM_OF_ROWS) + .queryParam("numOfRows", size) .build(true) .toUri(); } @@ -67,53 +66,33 @@ private URI getUri(String path, int pageNo) { /*** * 식품의약품안전처 의약품 제품 허가 상세 정보 URI 반환 * @return URI 제품 허가 상세 정보 - * - * @since 2025-04-15 + * * @author 함예정 + * @since 2025-04-15 */ public URI getUriForDetailApi(int pageNo) { - return getUri(API_DETAIL_PATH, pageNo); + return getUri(API_DETAIL_PATH, pageNo, NUM_OF_ROWS); } /*** * 식품의약품안전처 의약품 제품 허가 목록 URI 반환 * @return URI 제품 허가 목록 * - * @since 2025-04-15 * @author 함예정 + * @since 2025-04-15 */ public URI getUriForImgApi(int pageNo) { - return getUri(API_IMG_PATH, pageNo); - } - - public URI getUriForDetailApiShort() { - return UriComponentsBuilder.newInstance() - .scheme("https") - .host(HOST) - .port(443) - .path(API_DETAIL_PATH) - .queryParam("serviceKey", SERVICE_KEY) - .queryParam("type", "json") - .queryParam("pageNo", 1) - .queryParam("numOfRows", 1) - .build(true) - .toUri(); - } - - public URI getUriForImgApiShort() { - return UriComponentsBuilder.newInstance() - .scheme("https") - .host(HOST) - .port(443) - .path(API_IMG_PATH) - .queryParam("serviceKey", SERVICE_KEY) - .queryParam("type", "json") - .queryParam("pageNo", 1) - .queryParam("numOfRows", 1) - .build(true) - .toUri(); + return getUri(API_IMG_PATH, pageNo, NUM_OF_ROWS); } + /** + * KmBERT 임베딩 API 요청용 URI를 반환합니다. + * + * @return KmBERT API URI + * + * @author 함예정 + * @since 2025-04-21 + */ public URI getUriForKmbertEmbeding() { return UriComponentsBuilder.newInstance() .scheme("https") @@ -124,6 +103,14 @@ public URI getUriForKmbertEmbeding() { .toUri(); } + /** + * KrSBERT 임베딩 API 요청용 URI를 반환합니다. + * + * @return KrSBERT API URI + * + * @author 함예정 + * @since 2025-04-21 + */ public URI getUriForKrSbertEmbeding() { return UriComponentsBuilder.newInstance() .scheme("https") @@ -133,4 +120,5 @@ public URI getUriForKrSbertEmbeding() { .build(true) .toUri(); } + } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java new file mode 100644 index 0000000..aab1bd0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java @@ -0,0 +1,111 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.adapter; + +import org.springframework.batch.core.Job; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +/** + * BatchJobPort 인터페이스의 구현체로, + * Spring Batch Job 객체들을 제어하는 어댑터 클래스입니다. + * JobManager를 통해 배치 작업의 실행, 중지, 상태 조회 기능을 제공합니다. + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class BatchJobAdapter implements BatchJobPort { + + private final JobManager jobManager; + private final Job drugScrapJob; + private final Job drugDetailScrapJob; + private final Job drugImageScrapJob; + private final Job drugTableCombineJob; + private final Job embedJob; + + @Override + public String allJobStart() { + jobManager.IfAlreadyRunThrowException(); + return jobManager.startJob(drugScrapJob); + } + + @Override + public String allJobStop() { + return jobManager.stopRunningBatch(drugScrapJob); + } + + @Override + public String allJobResume() { + return jobManager.restart(); + } + + @Override + public String allJobStatus() { + return jobManager.getJobStatus(drugScrapJob); + } + + @Override + public String detailScrapJobStart() { + jobManager.IfAlreadyRunThrowException(); + return jobManager.startJob(drugDetailScrapJob); + } + + @Override + public String detailScrapJobStop() { + return jobManager.stopRunningBatch(drugDetailScrapJob); + } + + @Override + public String detailScrapJobStatus() { + return jobManager.getJobStatus(drugDetailScrapJob); + } + + @Override + public String imageScrapJobStart() { + jobManager.IfAlreadyRunThrowException(); + return jobManager.startJob(drugImageScrapJob); + } + + @Override + public String imageScrapJobStop() { + return jobManager.stopRunningBatch(drugImageScrapJob); + } + + @Override + public String imageScrapJobStatus() { + return jobManager.getJobStatus(drugImageScrapJob); + } + + @Override + public String tableCombineJobStart() { + jobManager.IfAlreadyRunThrowException(); + return jobManager.startJob(drugTableCombineJob); + } + + @Override + public String tableCombineJobStop() { + return jobManager.stopRunningBatch(drugTableCombineJob); + } + + @Override + public String tableCombineJobStatus() { + return jobManager.getJobStatus(drugTableCombineJob); + } + + @Override + public String embedJobStart(){ + return jobManager.startJob(embedJob); + } + + @Override + public String embedJobStop(){ + return jobManager.stopRunningBatch(embedJob); + } + + @Override + public String embedjobStatus(){ + return jobManager.getJobStatus(embedJob); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java new file mode 100644 index 0000000..68106ef --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java @@ -0,0 +1,189 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.adapter; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.task.TaskExecutor; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchException; + +import lombok.RequiredArgsConstructor; + +/** + * Spring Batch Job 실행 및 제어를 위한 매니저 클래스입니다. + * Job 실행, 중단, 상태 조회, 재시작 기능을 제공합니다. + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class JobManager { + private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final JobLauncher jobLauncher; + private final JobOperator jobOperator; + private final JobExplorer jobExplorer; + + private final Map taskExecutorMap; + + private Set stoppedExecutionIds = new ConcurrentSkipListSet<>(); + + /** + * 지정된 Job을 비동기로 실행합니다. + * + * @param job 실행할 Job + * @return 실행 요청 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + public String startJob(Job job) { + TaskExecutor taskExecutor = taskExecutorMap.get("batchExecutor"); + log("배치 실행 시작 - Job: " + job.getName()); + isRunning.set(true); + taskExecutor.execute(() -> { + try { + JobParameters params = new JobParametersBuilder() + .addLong("requestTime", System.currentTimeMillis()) + .toJobParameters(); + + jobLauncher.run(job, params); + } catch (Exception e) { + log(LogLevel.ERROR, "Job 실행 요청은 정상적으로 받았으나 실행에 실패하였습니다. " + job.getName()); + throw new ParserBatchException(ParserBatchError.JOB_RUN_FAIL); + } finally { + isRunning.set(false); + } + }); + + return "배치 실행 요청 수락됨"; + } + + /** + * 중단된 JobExecution을 재시작합니다. + * + * @return 재시작 요청 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + public String restart() { + IfAlreadyRunThrowException(); + TaskExecutor taskExecutor = taskExecutorMap.get("batchExecutor"); + taskExecutor.execute(() -> { + for (Long id : new HashSet<>(stoppedExecutionIds)) { + try { + isRunning.set(true); + jobOperator.restart(id); + } catch (Exception e) { + log(LogLevel.ERROR, "Job 재 실행 요청은 정상적으로 받았으나 실행에 실패하였습니다."); + throw new ParserBatchException(ParserBatchError.JOB_RUN_FAIL); + } finally { + isRunning.set(false); + } + } + }); + + stoppedExecutionIds.clear(); + return "재시작 요청 성공"; + } + + /** + * 지정된 Job의 최근 실행 상태를 조회합니다. + * + * @param job 조회할 Job + * @return 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + public String getJobStatus(Job job) { + List instances = jobExplorer.getJobInstances(job.getName(), 0, 30); + if (instances.isEmpty()) { + return "실행된 Job이 없습니다."; + } + + for (JobInstance instance : instances) { + List executions = jobExplorer.getJobExecutions(instance); + for (JobExecution execution : executions) { + if (execution.isRunning()) { + StringBuilder status = new StringBuilder("실행 중인 Step 상태: {"); + for (StepExecution step : execution.getStepExecutions()) { + long read = step.getReadCount(); + status.append("Step: ").append(step.getStepName()) + .append(", Read: ").append(read) + .append(", Skip: ").append(step.getSkipCount()); + } + return status.toString().trim(); + } + } + } + return "실행 중인 Job이 없습니다."; + } + + /** + * 지정된 Job을 중지합니다. + * + * @param job 중지할 Job + * @return 중지 요청 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + public String stopRunningBatch(Job job) { + String jobName = job.getName(); + log("배치 작업 중단 요청:" + jobName); + Collection instances = jobExplorer.getJobInstances(jobName, 0, 30); + if (!instances.isEmpty()) { + instances.stream() + .map(jobExplorer::getJobExecutions) + .flatMap(List::stream) + .filter(JobExecution::isRunning) + .forEach(exec -> { + try { + jobOperator.stop(exec.getId()); + } catch (Exception e) { + log("이미 중단 중이거나 중단 불가 상태입니다. executionId=" + exec.getId()); + } finally { + stoppedExecutionIds.add(exec.getId()); + } + }); + } + isRunning.set(false); + return "중단 요청 완료"; + } + + /** + * 실행 중인 배치 작업이 있을 경우 예외를 발생시킵니다. + * + * @throws ParserBatchException 중복 실행 예외 + * + * @author 함예정 + * @since 2025-05-02 + */ + public void IfAlreadyRunThrowException() { + if (isRunning.get()) { + throw new ParserBatchException(ParserBatchError.ALREADY_RUN); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java new file mode 100644 index 0000000..c44fbac --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CombineJobConfig { + @Bean + public Job drugTableCombineJob(JobRepository jobRepository, + Step tableCombineStep) { + return new JobBuilder("drugTableCombineJob", jobRepository) + .start(tableCombineStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java new file mode 100644 index 0000000..bc624a0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java @@ -0,0 +1,85 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.config; + +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto.TableCombineDto; + +import jakarta.persistence.EntityManagerFactory; +import lombok.Getter; + +@Configuration +public class CombineStepConfig { + @Getter + private final String STEP_NAME = "drugCombineStep"; + private final String READER_NAME = "drugDetailReader"; + private final int PAGE_SIZE = 1000; + + @Bean + public Step tableCombineStep( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + return new StepBuilder(STEP_NAME, jobRepository) + .chunk(PAGE_SIZE, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + @Bean + @StepScope + public JpaPagingItemReader drugDetailReader(EntityManagerFactory entityManagerFactory) { + JpaPagingItemReader reader = new JpaPagingItemReader<>(); + reader.setEntityManagerFactory(entityManagerFactory); + reader.setQueryString(getJoinTableSql()); + reader.setPageSize(PAGE_SIZE); + reader.setSaveState(true); + reader.setName(READER_NAME); + return reader; + } + + + private String getJoinTableSql(){ + return """ + SELECT new com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.TableCombineEntity( + d.drugId, + d.drugName, + d.company, + d.permitDate, + d.isGeneral, + d.materialInfo, + d.storeMethod, + d.validTerm, + d.efficacy, + d.usage, + d.precaution, + d.cancelDate, + d.cancelName, + d.isHerbal, + i.productImage, + i.pillImage + ) + FROM DrugDetailEntity d + LEFT JOIN ApiDataDrugImgEntity i ON d.drugId = i.drugId + """; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java new file mode 100644 index 0000000..696f9f9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto; + +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TableCombineDto { + private Long drugId; + private String drugName; + private String company; + private LocalDate permitDate; + private boolean isGeneral; + private String materialInfo; + private String storeMethod; + private String validTerm; + private String efficacy; + private String usage; + private String precaution; + private LocalDate cancelDate; + private String cancelName; + private Boolean isHerbal; + private String productImage; + private String pillImage; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java similarity index 59% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java index d3b04d1..681c1c1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.processor; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; @@ -7,70 +7,72 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; - -public class DrugDetailMapper { - - public static DrugDetail toDomainFromEntity(DrugDetailEntity e){ - DrugDetail domain = DrugDetail.builder() - .drugId(e.getDrugId()) - .drugName(e.getDrugName()) - .company(e.getCompany()) - .permitDate(e.getPermitDate()) - .isGeneral(e.isGeneral()) - .materialInfo(convertMaterialInfo(e.getMaterialInfo())) - .storeMethod(e.getStoreMethod()) - .validTerm(e.getValidTerm()) - .efficacy(convertEfficacy(e.getEfficacy())) - .usage(getUsage(e.getUsage())) - .precaution(getPrecaution(e.getPrecaution())) - .cancelDate(e.getCancelDate()) - .cancelName(e.getCancelName()) - .isHerbal(e.getIsHerbal()) - .build(); - return domain; - } +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto.TableCombineDto; - public static Drug toDomainFromEntity(DrugRawDataEntity e){ - return Drug.builder() - .drugId(e.getDrugId()) - .drugName(e.getDrugName()) - .company(e.getCompany()) - .permitDate(e.getPermitDate()) - .isGeneral(e.isGeneral()) - .materialInfo(convertMaterialInfo(e.getMaterialInfo())) - .storeMethod(e.getStoreMethod()) - .validTerm(e.getValidTerm()) - .efficacy(convertEfficacy(e.getEfficacy())) - .usage(getUsage(e.getUsage())) - .precaution(getPrecaution(e.getPrecaution())) - .cancelDate(e.getCancelDate()) - .cancelName(e.getCancelName()) - .isHerbal(e.getIsHerbal()) - .build(); +import lombok.RequiredArgsConstructor; - } +@Component +@RequiredArgsConstructor +public class TableCombineProcessor implements ItemProcessor { - private static float[] toArraysFromFloatString(String floatString){ - try { - return new ObjectMapper().readValue(floatString, float[].class); - } catch (Exception e) { - throw new RuntimeException("float 배열로 변환 실패"); + @Override + public DrugRawDataEntity process(TableCombineDto entity) throws Exception { + String imgUrl = null; + + String productImage = entity.getProductImage(); + if (productImage != null && entity.getProductImage().length() > 10) { + imgUrl = entity.getProductImage(); + } else { + imgUrl = entity.getPillImage(); } + + return DrugRawDataEntity.builder() + .drugId(entity.getDrugId()) + .drugName(entity.getDrugName()) + .company(entity.getCompany()) + .permitDate(entity.getPermitDate()) + .isGeneral(entity.isGeneral()) + .materialInfo( + toStringFromObj( + convertMaterialInfo(entity.getMaterialInfo()) + ) + ) + .storeMethod(entity.getStoreMethod()) + .validTerm(entity.getValidTerm()) + .efficacy( + toStringFromObj( + convertEfficacy(entity.getEfficacy()) + ) + ) + .usage(toStringFromObj( + getUsage(entity.getUsage()) + ) + ) + .precaution(toStringFromObj( + getPrecaution(entity.getPrecaution()) + ) + ) + .imageUrl(imgUrl) + .cancelDate(entity.getCancelDate()) + .cancelName(entity.getCancelName()) + .isHerbal(entity.getIsHerbal()) + .build(); } - private static List getUsage(String usage){ + + private static List getUsage(String usage) { List usages = new ArrayList<>(); JsonNode json = toJsonNodeFromString(usage); - if(!json.isNull() && json.has("sections")){ + if (!json.isNull() && json.has("sections")) { for (JsonNode section : json.get("sections")) { for (JsonNode article : section.get("articles")) { for (JsonNode paragraph : article.get("paragraphs")) { @@ -101,7 +103,7 @@ private static List mapFromMaterialJson(JsonNode json) { matrerials.add(ingredient); } return matrerials; - } catch (Exception e){ + } catch (Exception e) { log(LogLevel.ERROR, "객체 맵핑 실패", e); return null; } @@ -122,14 +124,14 @@ private static List convertEfficacy(String efficacy) { List efficacys = new ArrayList<>(); tryParseParagraphs(json, efficacys); - if(efficacys.size() == 0){ + if (efficacys.size() == 0) { tryParseTitle(json, efficacys); } return efficacys; } private static List tryParseTitle(JsonNode json, List efficacys) { - if(json.has("sections")){ + if (json.has("sections")) { for (JsonNode section : json.get("sections")) { for (JsonNode article : section.get("articles")) { efficacys.add(article.get("title").asText()); @@ -141,12 +143,12 @@ private static List tryParseTitle(JsonNode json, List efficacys) } private static List tryParseParagraphs(JsonNode json, List efficacys) { - if(json.has("sections")){ + if (json.has("sections")) { for (JsonNode section : json.get("sections")) { for (JsonNode article : section.get("articles")) { for (JsonNode paragraph : article.get("paragraphs")) { String text = paragraph.get("text").asText(); - if(text != null && !text.isEmpty()){ + if (text != null && !text.isEmpty()) { efficacys.add(paragraph.get("text").asText()); } @@ -162,7 +164,7 @@ private static Map> getPrecaution(String precaution) { Map> result = new LinkedHashMap<>(); JsonNode json = toJsonNodeFromString(precaution); - if(json.has("sections")){ + if (json.has("sections")) { JsonNode articles = json.get("sections").get(0).get("articles"); for (JsonNode article : articles) { String title = article.get("title").asText(); @@ -177,12 +179,13 @@ private static Map> getPrecaution(String precaution) { return result; } - public JsonNode toJson(String json) { + private static String toStringFromObj(Object obj) { try { - return new ObjectMapper().readValue(json, JsonNode.class); - } catch (JsonProcessingException e) { - //TODO 에러 로그 처리 필요합니다. - throw new RuntimeException(e); + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("변환 실패"); + return null; } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java new file mode 100644 index 0000000..17a3d28 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.writer; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@StepScope +@RequiredArgsConstructor +public class TableCombineWriter implements ItemWriter { + private final GovDrugJpaRepository drugRawDataRepository; + private AtomicInteger count = new AtomicInteger(); + + @Override + public void write(Chunk entity) { + List items = new ArrayList<>(entity.getItems()); + drugRawDataRepository.saveAll(items); + log("테이블 병합 작업 - 쓰기 완료: " + count.addAndGet(items.size())); + + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java new file mode 100644 index 0000000..38954a0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MdcTaskDecorator; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 + */ +@Configuration +@RequiredArgsConstructor +public class BatchConfig { + private final MdcTaskDecorator mdcTaskDecorator; + + /** + * 병렬 처리를 위한 ThreadPool 기반 TaskExecutor 설정 + * + * @return TaskExecutor 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean("batchExecutor") + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(10); + executor.setTaskDecorator(mdcTaskDecorator); + executor.setThreadNamePrefix("batch-task-"); + executor.initialize(); + return executor; + } + + @Bean("batchExecutorManyThread") + public TaskExecutor taskExecutorMoreThreads() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(20); + executor.setTaskDecorator(mdcTaskDecorator); + executor.setThreadNamePrefix("batch-task(m)-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java new file mode 100644 index 0000000..95dda10 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class CommonJobConfig { + /** + * 의약품 정보를 수집하는 Batch Job 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param totalPageCheckStep 전체 페이지 수 확인 Step + * @param drugDetailStep 상세 정보 수집 Step + * @return 구성된 Job 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public Job drugScrapJob(JobRepository jobRepository, + Step totalPageCheckStep, + Step drugDetailStep, + Step imageTotalPageCheckStep, + Step imageMasterStep, + Step switchModelStepToGpt, + Step gptEmbedStep, + Step switchModelStepToKmbert, + Step kmbertEmbedStep, + Step switchModelStepToKrsbert, + Step krsbertEmbedStep) { + return new JobBuilder("drugScrapJob", jobRepository) + .start(totalPageCheckStep) + .next(drugDetailStep) + .next(imageTotalPageCheckStep) + .next(imageMasterStep) + .next(switchModelStepToGpt) + .next(gptEmbedStep) + .next(switchModelStepToKmbert) + .next(kmbertEmbedStep) + .next(switchModelStepToKrsbert) + .next(krsbertEmbedStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java similarity index 92% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java index 4994015..0f8b999 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/MaterialParser.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java @@ -1,12 +1,12 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support; +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; /** * 원재료 정보를 파싱하여 JSON 배열 형식의 문자열로 변환하는 유틸리티 클래스입니다. diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java new file mode 100644 index 0000000..a9ad910 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; + +import java.util.Map; + +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; +import org.springframework.stereotype.Component; +/** + * 스레드 풀에서 실행되는 Task에 MDC(Context Map)를 전파하기 위한 TaskDecorator 구현체 + * MDC 정보를 부모 스레드에서 자식 스레드로 복사하여 로그 추적 정보를 유지하도록 한다. + * + */ +@Component +public class MdcTaskDecorator implements TaskDecorator { + + /** + * Runnable 실행 시 부모 스레드의 MDC(Context Map)를 자식 스레드로 복사하여 설정한다. + * 실행 후 MDC를 반드시 clear하여 메모리 누수를 방지한다. + * + * @param runnable 실행할 원본 Runnable + * @return MDC context를 설정한 새로운 Runnable + */ + @Override + public Runnable decorate(Runnable runnable) { + Map contextMap = MDC.getCopyOfContextMap(); + return () -> { + if (contextMap != null) { + MDC.setContextMap(contextMap); + } + try { + runnable.run(); + } finally { + MDC.clear(); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java new file mode 100644 index 0000000..5210fec --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java @@ -0,0 +1,21 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.StepExecution; + +public class StepSkipDecider { + + public static boolean shouldSkip(StepExecution stepExecution) { + JobParameters params = stepExecution.getJobParameters(); + String skipParam = params.getString("skipStepName", ""); + String stepName = stepExecution.getStepName(); + + List stepsToSkip = Arrays.stream(skipParam.split(",")) + .map(String::trim).toList(); + + return stepsToSkip.contains(stepName); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java similarity index 96% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java index f42e18d..3381310 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/support/XMLParser.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java @@ -1,6 +1,5 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail.support; +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; -import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; @@ -10,18 +9,16 @@ import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; -import org.xml.sax.SAXException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; /** * XML 문자열을 파싱하여 JSON 문자열로 변환하는 클래스입니다. diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java new file mode 100644 index 0000000..2c3400a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class DetailJobConfig { + @Bean + public Job drugDetailScrapJob(JobRepository jobRepository, + Step totalPageCheckStep, + Step drugDetailStep) { + return new JobBuilder("drugDetailScrapJob", jobRepository) + .start(totalPageCheckStep) + .next(drugDetailStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java new file mode 100644 index 0000000..d1c8a9a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java @@ -0,0 +1,122 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.config; + +import java.util.List; + +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MdcTaskDecorator; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor.DetailTotalPageCalculator; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor.DrugDetailProcessor; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.reader.DetailPageNumberReader; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.writer.DrugDetailWriter; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; + +/** + * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 + */ +@Configuration +public class DetailStepConfig { + private final DetailPageNumberReader detailPageNumberReader; + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + private final TaskExecutor taskExecutor; + + public DetailStepConfig(DetailPageNumberReader detailPageNumberReader, + ApiRequestManager apiRequestManager, + MdcTaskDecorator mdcTaskDecorator, + ApiResponseMapper apiResponseMapper, + @Qualifier("batchExecutor") + TaskExecutor taskExecutor) { + this.detailPageNumberReader = detailPageNumberReader; + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + this.taskExecutor = taskExecutor; + } + + /** + * 전체 페이지 수를 계산하는 Tasklet 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param detailTotalPageCalculator 페이지 계산 로직을 수행하는 Tasklet + * @return Step 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + Step totalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, + DetailTotalPageCalculator detailTotalPageCalculator) { + return new StepBuilder("totalPageCheck", jobRepository) + .tasklet(detailTotalPageCalculator, txManager) + .build(); + } + + /** + * 상세 페이지별로 데이터를 수집하고 처리 및 저장하는 Chunk 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param processor 데이터를 처리하는 Processor + * @param writer 데이터를 저장하는 Writer + * @return Step 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public Step drugDetailStep(JobRepository jobRepository, + PlatformTransactionManager txManager, + DrugDetailProcessor processor, + DrugDetailWriter writer) { + return new StepBuilder("drugDetailStep", jobRepository) + .>chunk(1, txManager) + .reader(detailPageNumberReader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .taskExecutor(taskExecutor) + .build(); + } + + /** + * 의약품 상세정보 처리용 Processor Bean 정의 + * + * @return CombineProcessor 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public DrugDetailProcessor processor() { + return new DrugDetailProcessor(apiRequestManager, apiResponseMapper); + } + + /** + * 처리된 의약품 상세정보를 DB에 저장하는 Writer Bean 정의 + * + * @param repository 의약품 상세정보 저장용 JPA Repository + * @return DrugDetailWriter 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public DrugDetailWriter writer(GovDrugDetailJpaRepository repository) { + return new DrugDetailWriter(repository); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java similarity index 95% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java index 5c9f441..c26ad83 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugDetailRequest.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto; +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto; import java.time.LocalDate; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java new file mode 100644 index 0000000..a6be79a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor; + +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.reader.DetailPageNumberReader; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DetailTotalPageCalculator implements Tasklet { + + private final DetailPageNumberReader detailPageNumberReader; + private final ApiRequestManager apiRequestManager; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int totalPage = apiRequestManager.getDetailTotalPage(); + detailPageNumberReader.setTotalPage(totalPage); + + LogUtil.log("[CombineProcessor] 총 페이지 수 계산 완료: " + totalPage); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java new file mode 100644 index 0000000..4d36f1d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java @@ -0,0 +1,90 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.List; + +import org.springframework.batch.item.ItemProcessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MaterialParser; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.XMLParser; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.DrugDetailRequestMapper; + +/** + * pageNumber를 받아 외부 REST API 호출 → JSON → DTO 리스트 변환 → + * Entity 리스트로 매핑 + */ + +public class DrugDetailProcessor implements ItemProcessor> { + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + + public DrugDetailProcessor(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + } + + @Override + public List process(Integer pageNumber) throws Exception { + String response = apiRequestManager.fetchDetailData(pageNumber); + JsonNode items = apiRequestManager.getItemsFromResponse(response); + List drugItems = apiResponseMapper.toListFromDrugDetails(items); + + for (int i = 0; i < drugItems.size(); i++) { + DrugDetailRequest drugDetail = drugItems.get(i); + JsonNode item = items.get(i); + + String materialRawData = item.get("MATERIAL_NAME").asText(); + String materialInfo = MaterialParser.parseMaterial(materialRawData); + + drugDetail.changeMaterialInfo(materialInfo); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 성분 저장 완료: \n" + drugDetail); + + log(LogLevel.DEBUG, "약품 효능 데이터 파싱 시작"); + String efficacyXmlText = item.get("EE_DOC_DATA").asText(); + log(LogLevel.DEBUG, "약품 효능 Raw 데이터 조회 성공: \n" + efficacyXmlText); + + String efficacy = XMLParser.toJson(efficacyXmlText); + log(LogLevel.DEBUG, "약품 효능 파싱 성공: \n" + efficacy); + + drugDetail.changeEfficacy(efficacy); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 효능 저장 완료: \n" + drugDetail); + + log(LogLevel.DEBUG, "약품 사용법 데이터 파싱 시작"); + String usageXmlText = item.get("UD_DOC_DATA").asText(); + log(LogLevel.DEBUG, "약품 사용법 Raw 데이터 조회 성공: \n" + usageXmlText); + + String usages = XMLParser.toJson(usageXmlText); + log(LogLevel.DEBUG, "약품 사용법 파싱 성공: \n" + usages); + + drugDetail.changeUsage(usages); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 사용법 저장 완료: \n" + drugDetail); + + log(LogLevel.DEBUG, "약품 주의사항 데이터 파싱 시작"); + String precautionxmlText = item.get("NB_DOC_DATA").asText(); + log(LogLevel.DEBUG, "약품 주의사항 Raw 데이터 조회 성공: \n" + precautionxmlText); + + String precautions = XMLParser.toJson(precautionxmlText); + log(LogLevel.DEBUG, "약품 주의사항 파싱 성공: \n" + precautions); + + drugDetail.changePrecaution(precautions); + log(LogLevel.DEBUG, "drugDetail 객체에 약품 주의사항 저장 완료: \n" + drugDetail); + + String precaution = drugDetail.getPrecaution(); + if (precaution != null && (precaution.contains("한의사") || precaution.contains("한약사"))) { + drugDetail.changeIsHerbal(true); + } + } + return drugItems.stream() + .map(DrugDetailRequestMapper::toEntityFromRequest) + .toList(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java new file mode 100644 index 0000000..34e25eb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.reader; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.batch.item.ItemReader; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; + +import lombok.Getter; +import lombok.Setter; + +/** + * Spring Batch에서 각 Step 실행 시 처리할 페이지 번호를 순차적으로 제공하는 Reader 클래스 + * + * 총 페이지 수를 기준으로 1부터 시작하여 차례대로 page 번호를 반환하며, + * 모든 페이지가 반환되면 null을 반환하여 반복을 종료한다. + * + * @author 함예정 + */ + +@Component +@Setter +@Getter +public class DetailPageNumberReader implements ItemReader { + + private static final Queue pageQueue = new ConcurrentLinkedQueue<>(); + + public static void setTotalPage(int totalPage) { + pageQueue.clear(); + for (int i = 1; i <= totalPage; i++) { + pageQueue.add(i); + } + } + /** + * 현재 페이지 번호를 반환하고, 다음 호출을 위해 내부 카운터를 증가시킨다. + * 총 페이지 수를 초과하면 null을 반환하여 종료를 알린다. + * + * @return 현재 처리할 페이지 번호 또는 null(모든 페이지 처리 완료 시) + */ + @Override + public Integer read() { + Integer page = pageQueue.poll(); + if (page != null) { + LogUtil.log(Thread.currentThread().getName() + " - Page 할당: " + page); + } + return page; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java new file mode 100644 index 0000000..057e9fe --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.writer; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; + +/** + * Entity 리스트를 받아 JPA Repository로 한 번에 저장 + */ +public class DrugDetailWriter implements ItemWriter> { + + private final GovDrugDetailJpaRepository repository; + + public DrugDetailWriter(GovDrugDetailJpaRepository repository) { + this.repository = repository; + } + + @Override + public void write(Chunk> chunk) throws Exception { + + for (List items : chunk.getItems()) { + repository.saveAll(items); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java new file mode 100644 index 0000000..b69a863 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; + +@Configuration +public class EmbedJobConfig { + @Bean + public Job embedJob(JobRepository jobRepository, + Step switchModelStepToGpt, + Step gptEmbedStep, + Step switchModelStepToKmbert, + Step kmbertEmbedStep, + Step switchModelStepToKrsbert, + Step krsbertEmbedStep){ + return new JobBuilder("embedJob", jobRepository) + .start(switchModelStepToGpt) + .next(gptEmbedStep) + .next(switchModelStepToKmbert) + .next(kmbertEmbedStep) + .next(switchModelStepToKrsbert) + .next(krsbertEmbedStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java new file mode 100644 index 0000000..d4434b6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java @@ -0,0 +1,211 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.config; + +import java.util.Map; + +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.reader.DrugIdRangePartitioner; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; + +import jakarta.persistence.EntityManagerFactory; + +@Configuration +public class EmbedStepConfig { + private final String READER_NAME = "rawDataReader"; + private final int PAGE_SIZE = 50; + private final DrugIdRangePartitioner drugIdRangePartitioner; + private final TaskExecutor taskExecutor; + + public EmbedStepConfig(DrugIdRangePartitioner drugIdRangePartitioner, + @Qualifier("batchExecutorManyThread") + TaskExecutor taskExecutor){ + this.drugIdRangePartitioner = drugIdRangePartitioner; + this.taskExecutor = taskExecutor; + } + + @Bean + public Step switchModelStepToGpt(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo("gptEmbeddingLoadingAdapter"); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder("switchModelStepToGpt", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + @Bean + public Step switchModelStepToKmbert(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo("kmBertEmbeddingLoadingAdapter"); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder("switchModelStepToKmbert", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + @Bean + public Step switchModelStepToKrsbert(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo("krSBertEmbeddingLoadingAdapter"); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder("switchModelStepToKrsbert", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + @Bean + public Step gptEmbedSlaveStep( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder("gptEmbedSlaveStep", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + @Bean + public Step gptEmbedStep( + JobRepository jobRepository, + Step gptEmbedSlaveStep) { + + return new StepBuilder("gptEmbedStep", jobRepository) + .partitioner(gptEmbedSlaveStep.getName(), drugIdRangePartitioner) + .step(gptEmbedSlaveStep) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + + @Bean + public Step kmbertEmbedSlaveStep( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder("kmbertEmbedSlaveStep", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + @Bean + public Step kmbertEmbedStep( + JobRepository jobRepository, + Step kmbertEmbedSlaveStep) { + + return new StepBuilder("kmbertEmbedStep", jobRepository) + .partitioner(kmbertEmbedSlaveStep.getName(), drugIdRangePartitioner) + .step(kmbertEmbedSlaveStep) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + @Bean + public Step krsbertEmbedSlaveStep( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder("krsbertEmbedSlaveStep", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + @Bean + public Step krsbertEmbedStep( + JobRepository jobRepository, + Step krsbertEmbedSlaveStep) { + return new StepBuilder("krsbertEmbedStep", jobRepository) + .partitioner(krsbertEmbedSlaveStep.getName(), drugIdRangePartitioner) + .step(krsbertEmbedSlaveStep) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + @Bean + @StepScope + public JpaPagingItemReader embedItemReader( + @Value("#{stepExecutionContext['minId']}") Long minId, + @Value("#{stepExecutionContext['maxId']}") Long maxId, + EntityManagerFactory emf + ) { + LogUtil.log(Thread.currentThread().getName() + "Item Read: " + minId + "~" + maxId); + return new JpaPagingItemReaderBuilder() + .name("partitionedReader") + .entityManagerFactory(emf) + .queryString("SELECT d FROM DrugRawDataEntity d WHERE d.drugId BETWEEN :minId AND :maxId ORDER BY d.drugId") + .parameterValues(Map.of("minId", minId, "maxId", maxId)) + .pageSize(100) + .saveState(false) + .build(); + } + + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java new file mode 100644 index 0000000..3f985ac --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java @@ -0,0 +1,11 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class DrugVectorDto { + private Long drugId; + private float[] vector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java new file mode 100644 index 0000000..27dc997 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.processor; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto.TableCombineDto; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.DrugMapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class EmbedProcessor implements ItemProcessor { + private final EmbeddingLoadingPort embeddingLoadingPort; + + @Override + public DrugVectorDto process(DrugRawDataEntity item) throws Exception { + Long id = item.getDrugId(); + String embeddingText = getEmbedTextFromItem(item); + float[] embeddingVector = embeddingLoadingPort.getEmbedding(embeddingText); + return DrugVectorDto + .builder() + .drugId(id) + .vector(embeddingVector) + .build(); + } + + private String getEmbedTextFromItem(DrugRawDataEntity item){ + return DrugMapper.convertSingleStringForEfficacy( + DrugMapper.parseStringToList(item.getEfficacy()) + ); + } + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java new file mode 100644 index 0000000..62f0444 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java @@ -0,0 +1,42 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.reader; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; + +@Component +public class DrugIdRangePartitioner implements Partitioner { + + private final GovDrugJpaRepository repository; + + public DrugIdRangePartitioner(GovDrugJpaRepository repository) { + this.repository = repository; + } + + @Override + public Map partition(int gridSize) { + Long minId = repository.findMinDrugId(); + Long maxId = repository.findMaxDrugId(); + + long targetSize = (maxId - minId) / gridSize + 1; + + Map result = new HashMap<>(); + long start = minId; + long end = start + targetSize - 1; + + for (int i = 0; i < gridSize; i++) { + ExecutionContext context = new ExecutionContext(); + context.putLong("minId", start); + context.putLong("maxId", Math.min(end, maxId)); + result.put("partition" + i, context); + start += targetSize; + end += targetSize; + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java new file mode 100644 index 0000000..e976ff0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.writer; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; + +import lombok.RequiredArgsConstructor; + +@Component +@StepScope +@RequiredArgsConstructor +public class EmbedWriter implements ItemWriter { + private final EmbeddingLoadingPort embeddingLoadingPort; + private AtomicInteger count = new AtomicInteger(); + + @Override + public void write(Chunk dto) throws Exception { + List items = new ArrayList<>(dto.getItems()); + embeddingLoadingPort.saveEmbedding(items); + log("임베딩 작업 - 쓰기 완료: " + count.addAndGet(items.size())); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java new file mode 100644 index 0000000..db902c9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception; + +import org.springframework.http.HttpStatus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ParserBatchError implements ErrorCode { + ALREADY_RUN(HttpStatus.CONFLICT, 400001, "이미 실행 중인 배치가 있습니다."), + JOB_RUN_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 500000, "JOB 실행 요청은 정상적으로 도달했으나 실행에 실패했습니다."), + JSON_TYPE_CHANGE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 500001, "JSON을 자바 타입으로 변환하는데 실패했습니다."); + private final HttpStatus httpStatus; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java new file mode 100644 index 0000000..f9c7fe7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class ParserBatchException extends CustomException { + private final ErrorCode errorCode; + + public ParserBatchException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java new file mode 100644 index 0000000..d72a6e9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ImageJobConfig { + @Bean + public Job drugImageScrapJob(JobRepository jobRepository, + Step imageTotalPageCheckStep, + Step imageMasterStep) { + return new JobBuilder("drugImageScrapJob", jobRepository) + .start(imageTotalPageCheckStep) + .next(imageMasterStep) + .build(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java new file mode 100644 index 0000000..23fcee7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java @@ -0,0 +1,136 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.config; + +import java.util.List; + +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MdcTaskDecorator; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor.ImageScrapProcessor; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor.ImageTotalPageCalculator; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader.PageRangePartitioner; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader.PartitionedPageReader; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.writer.DrugImageWriter; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.ApiDataDrugImgRepo; + +/** + * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 + */ +@Configuration +public class ImageStepConfig { + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + private final TaskExecutor taskExecutor; + + public ImageStepConfig(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper, + @Qualifier("batchExecutorManyThread") + TaskExecutor taskExecutor) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + this.taskExecutor = taskExecutor; + } + + + + /** + * 전체 페이지 수를 계산하는 Tasklet 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param imageTotalPageCalculator 페이지 계산 로직을 수행하는 Tasklet + * @return Step 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + Step imageTotalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, + ImageTotalPageCalculator imageTotalPageCalculator) { + return new StepBuilder("imageTotalPageCheck", jobRepository) + .tasklet(imageTotalPageCalculator, txManager) + .build(); + } + + @Bean + public Step imageMasterStep(JobRepository jobRepository, + PageRangePartitioner partitioner, + Step imageScrapStep) { + return new StepBuilder("imageMasterStep", jobRepository) + .partitioner(imageScrapStep.getName(), partitioner) + .step(imageScrapStep) + .gridSize(15) + .taskExecutor(taskExecutor) + .build(); + } + + /** + * 상세 페이지별로 데이터를 수집하고 처리 및 저장하는 Chunk 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param processor 데이터를 처리하는 Processor + * @param writer 데이터를 저장하는 Writer + * @return Step 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public Step imageScrapStep(JobRepository jobRepository, + PlatformTransactionManager txManager, + ImageScrapProcessor processor, + DrugImageWriter writer, + PartitionedPageReader reader) { + return new StepBuilder("imageScrapStep", jobRepository) + .>chunk(1, txManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * 의약품 상세정보 처리용 Processor Bean 정의 + * + * @return CombineProcessor 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public ImageScrapProcessor imageScrapProcessor() { + return new ImageScrapProcessor(apiRequestManager, apiResponseMapper); + } + + /** + * 처리된 의약품 상세정보를 DB에 저장하는 Writer Bean 정의 + * + * @param repository 의약품 상세정보 저장용 JPA Repository + * @return DrugDetailWriter 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public DrugImageWriter imageScrapWriter(ApiDataDrugImgRepo repository) { + return new DrugImageWriter(repository); + } + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java new file mode 100644 index 0000000..106bf4d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java @@ -0,0 +1,49 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor; + +import java.util.List; + +import org.springframework.batch.item.ItemProcessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugImageRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugImageRequestMapper; + +/** + * pageNumber를 받아 외부 REST API 호출 → JSON → DTO 리스트 변환 → + * Entity 리스트로 매핑 + */ +public class ImageScrapProcessor implements ItemProcessor> { + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + + public ImageScrapProcessor(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + } + + @Override + public List process(Integer pageNumber) throws Exception { + LogUtil.log(Thread.currentThread().getName() + " - " + pageNumber + " page 처리 시작"); + String response = apiRequestManager.fetchImageData(pageNumber); + JsonNode items = apiRequestManager.getItemsFromResponse(response); + List drugItems = apiResponseMapper.toListFromDrugImages(items); + + for (int i = 0; i < drugItems.size(); i++) { + DrugImageRequest item = drugItems.get(i); + String productImage = apiRequestManager.getImage(item.getDrugId()); + if (productImage != null && productImage.length() > 10) { + item.changeProductImageUrl(productImage); + } + } + + return drugItems.stream() + .map(DrugImageRequestMapper::toEntityFromRequest) + .toList(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java new file mode 100644 index 0000000..ac797e8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader.PageRangePartitioner; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ImageTotalPageCalculator implements Tasklet { + + private final PageRangePartitioner pageRangePartitioner; + private final ApiRequestManager apiRequestManager; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int totalPage = apiRequestManager.getImageTotalPage(); + pageRangePartitioner.setTotalPages(totalPage); + + log("[Image-Total-Page-Calculator] 총 페이지 수 계산 완료: " + totalPage); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java new file mode 100644 index 0000000..273d539 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java @@ -0,0 +1,53 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; + +import lombok.Getter; +import lombok.Setter; + +/** + * Spring Batch에서 각 Step 실행 시 처리할 페이지 번호를 순차적으로 제공하는 Reader 클래스 + * + * 총 페이지 수를 기준으로 1부터 시작하여 차례대로 page 번호를 반환하며, + * 모든 페이지가 반환되면 null을 반환하여 반복을 종료한다. + * + * @author 함예정 + */ + +@Component +@StepScope +@Setter +@Getter +public class ImagePageNumberReader implements ItemReader { + + private static final Queue pageQueue = new ConcurrentLinkedQueue<>(); + + public static void setTotalPage(int totalPage) { + pageQueue.clear(); + for (int i = 1; i <= totalPage; i++) { + pageQueue.add(i); + } + } + /** + * 현재 페이지 번호를 반환하고, 다음 호출을 위해 내부 카운터를 증가시킨다. + * 총 페이지 수를 초과하면 null을 반환하여 종료를 알린다. + * + * @return 현재 처리할 페이지 번호 또는 null(모든 페이지 처리 완료 시) + */ + @Override + public Integer read() { + Integer page = pageQueue.poll(); + if (page != null) { + LogUtil.log(Thread.currentThread().getName() + " - Page 할당: " + page); + } + return page; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java new file mode 100644 index 0000000..9711e31 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java @@ -0,0 +1,36 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.stereotype.Component; + +@Component +public class PageRangePartitioner implements Partitioner { + private int totalPages = 0; + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + + @Override + public Map partition(int gridSize) { + int range = totalPages / gridSize; + Map result = new HashMap<>(); + + for (int i = 0; i < gridSize; i++) { + int start = i * range + 1; + int end = (i == gridSize - 1) ? totalPages : start + range - 1; + + ExecutionContext context = new ExecutionContext(); + context.putInt("startPage", start); + context.putInt("endPage", end); + + result.put("partition" + i, context); + } + + return result; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java new file mode 100644 index 0000000..deaa758 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; + +@Component +@StepScope +public class PartitionedPageReader implements ItemReader { + private int currentPage; + private final int endPage; + + public PartitionedPageReader( + @Value("#{stepExecutionContext['startPage']}") int startPage, + @Value("#{stepExecutionContext['endPage']}") int endPage) { + log("[Reader bean 생성] startPage=" + startPage + ", endPage=" + endPage); + this.currentPage = startPage; + this.endPage = endPage; + } + + @Override + public Integer read() { + log("이미지 페이지 Read: " + currentPage); + if (currentPage > endPage) { + return null; + } + return currentPage++; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java new file mode 100644 index 0000000..3d547ab --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.writer; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.List; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.ApiDataDrugImgRepo; + +/** + * Entity 리스트를 받아 JPA Repository로 한 번에 저장 + */ +public class DrugImageWriter implements ItemWriter> { + + private final ApiDataDrugImgRepo repository; + + public DrugImageWriter(ApiDataDrugImgRepo repository) { + this.repository = repository; + } + + @Override + public void write(Chunk> chunk) throws Exception { + log(Thread.currentThread().getName() + " - Start Write"); + for (List items : chunk.getItems()) { + repository.saveAllAndFlush(items); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java new file mode 100644 index 0000000..85d7f01 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugImageRequest; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ApiResponseMapper { + private final ObjectMapper objectMapper; + public List toListFromDrugDetails(JsonNode items) { + try { + return objectMapper.readValue( + items.toString(), new TypeReference<>() {} + ); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "타입변환 실패(JsonNode -> List"); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + + public List toListFromDrugImages(JsonNode items) { + try { + return objectMapper.readValue( + items.toString(), new TypeReference<>() {} + ); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "타입변환 실패(JsonNode -> List"); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java similarity index 80% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java index dea606b..b949d10 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDetailRequestMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java @@ -1,6 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; +package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto.DrugDetailRequest; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; public class DrugDetailRequestMapper { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java deleted file mode 100644 index d34802e..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/adapter/EmbeddingAdapter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.adapter; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.EmbeddingPort; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client.EmbeddingClient; - -@Service -public class EmbeddingAdapter implements EmbeddingPort { - private final Map embeddingClientMap; - - public EmbeddingAdapter(List clients) { - this.embeddingClientMap = clients.stream() - .collect(Collectors.toMap(EmbeddingClient::getModelType, client -> client)); - } - @Override - public float[] getEmbedding(String text, EmbeddingModelType embeddingModelType) { - EmbeddingClient client = embeddingClientMap.get(embeddingModelType); - - return client.getEmbedding(text); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java index 6484afb..3730bcd 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java @@ -8,16 +8,16 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor public class KmBertEmbeddingClient implements EmbeddingClient { - private final ApiUriCompBuilder apiUriCompBuilder; + private final UriCompBuilder apiUriCompBuilder; private final RestTemplate restTemplate; @Override diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java index 3e54f83..d182618 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java @@ -8,16 +8,16 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor public class KrSbertEmbeddingClient implements EmbeddingClient { - private final ApiUriCompBuilder apiUriCompBuilder; + private final UriCompBuilder apiUriCompBuilder; private final RestTemplate restTemplate; @Override diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java deleted file mode 100644 index 781d41c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugDetailRepositoryAdapter.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; - -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugDetailRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugDetail; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugDetailRequest; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailRequestMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugDetailMapper; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class DrugDetailRepositoryAdapter implements DrugDetailRepositoryPort { - private final GovDrugDetailJpaRepository drugdetailJpaRepository; - - @Override - public void saveDrugDetail(DrugDetailRequest e){ - drugdetailJpaRepository.save(DrugDetailRequestMapper.toEntityFromRequest(e)); - } - - @Override - public void saveDrugDetailBulk(List list){ - drugdetailJpaRepository.saveAll(list.stream() - .map(DrugDetailRequestMapper::toEntityFromRequest) - .collect(Collectors.toList())); - drugdetailJpaRepository.flush(); - } - - @Override - public List getAllGovDrugDetail(){ - return drugdetailJpaRepository.findAll().stream() - .map(DrugDetailMapper::toDomainFromEntity) - .collect(Collectors.toList()); - } - - @Override - public Page getGovDrugDetailByPage(Pageable pageable) { - return drugdetailJpaRepository.findAll(pageable) - .map(DrugDetailMapper::toDomainFromEntity); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java deleted file mode 100644 index d40b69c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugEmbedRepositoryAdapter.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; - -import java.util.function.Function; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugEmbedRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKmBertEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKrSbertEmbedJpaRepository; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class DrugEmbedRepositoryAdapter implements DrugEmbedRepositoryPort { - private final GovDrugGptEmbedJpaRepository gptRepository; - private final GovDrugKmBertEmbedJpaRepository kmBertRepository; - private final GovDrugKrSbertEmbedJpaRepository krSbertRepository; - - @Override - public void saveGptEmbed(Long drugId, float[] gptVector){ - gptRepository.save( - buildEmbedEntity(drugId, gptVector, DrugGptEmbedEntity.class) - ); - } - - @Transactional - @Override - public void saveKmBertEmbed(Long drugId, float[] kmBertVector){ - kmBertRepository.save( - buildEmbedEntity(drugId, kmBertVector, DrugKmBertEmbedEntity.class) - ); - } - - @Transactional - @Override - public void saveKrSbertEmbed(Long drugId, float[] krSbertVector){ - krSbertRepository.save( - buildEmbedEntity(drugId, krSbertVector, DrugKrSbertEmbedEntity.class) - ); - } - - private T buildEmbedEntity(Long drugId, float[] vector, Class clazz) { - try { - String vectorString = toStringFromFloatArray(vector); - return clazz.getDeclaredConstructor(Long.class, String.class) - .newInstance(drugId, toStringFromFloatArray(vector)); - } catch (Exception e) { - //TODO: 엔터티 생성 실패 - throw new RuntimeException(e); - } - } - - private float[] getVectorFromRepository(Long drugId, JpaRepository repository, Function vectorGetter) { - T entity = repository.findById(drugId).orElse(null); - - if (entity == null) { - return null; - } - - String vectorString = vectorGetter.apply(entity); - return toFloatFromString(vectorString); - } - - private String toStringFromFloatArray(float[] openAIVector) { - try { - return new ObjectMapper().writeValueAsString(openAIVector); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return null; - } - } - - private float[] toFloatFromString(String vector) { - try { - return new ObjectMapper().readValue(vector, float[].class); - } catch (Exception e) { - //TODO: 벡터 변환 실패 예외 처리 - System.out.println("벡터 변환 실패"); - return null; - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java deleted file mode 100644 index 3f32055..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugImageRepositoryAdapter.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; - -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugImageRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.ApiDataDrugImgRepo; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugImageMapper; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class DrugImageRepositoryAdapter implements DrugImageRepositoryPort { - private final ApiDataDrugImgRepo imageRepository; - - @Override - public List getAllGovDrugImage(){ - return imageRepository.findAll().stream() - .map(DrugImageMapper::toDomainFromEntity) - .collect(Collectors.toList()); - } - - @Override - public DrugImage getById(Long drugId) { - return imageRepository.findById(drugId) - .map(DrugImageMapper::toDomainFromEntity) - .orElseGet(() -> getDefaultDomain()); - } - - @Override - public void saveAllAndFlush(List imgData) { - imageRepository.saveAll(DrugImageMapper.toEntityListFromDomainList(imgData)); - imageRepository.flush(); - } - - private static DrugImage getDefaultDomain() { - return DrugImageMapper.toDomainFromEntity(new ApiDataDrugImgEntity()); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java deleted file mode 100644 index 0f6c645..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/adapter/out/DrugRawDataRepositoryAdapter.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.adapter.out; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.DrugRawDataRepositoryPort; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugRawDataMapper; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class DrugRawDataRepositoryAdapter implements DrugRawDataRepositoryPort { - - private final GovDrugJpaRepository jpaDrugRepository; - - @Transactional - @Override - public void save(DrugRawData drug){ - DrugRawDataEntity entity = DrugRawDataMapper.toEntityFromDomain(drug); - jpaDrugRepository.save(entity); - } - - @Transactional - @Override - public void saveAll(List rawData) { - jpaDrugRepository.saveAll( - rawData.stream() - .map(DrugRawDataMapper::toEntityFromDomain).toList() - ); - jpaDrugRepository.flush(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java new file mode 100644 index 0000000..87353cd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class DrugImageRequest { + + @JsonProperty("ITEM_SEQ") + private Long drugId; + + private String productImage; + + @JsonProperty("BIG_PRDT_IMG_URL") + private String pillImageUrl; + + public DrugImageRequest changeProductImageUrl(String productImage){ + this.productImage = productImage; + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java index 0006f6b..57bcf33 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java @@ -1,7 +1,6 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; -import com.fasterxml.jackson.annotation.JsonProperty; - +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; @@ -17,12 +16,15 @@ @NoArgsConstructor @AllArgsConstructor @ToString -@Table(name="API_DATA_DRUG_IMG") +@Table(name = "API_DATA_DRUG_IMG") public class ApiDataDrugImgEntity { @Id + @Column(name = "ITEM_SEQ") private Long drugId; - private String imgUrl; - + @Column(name = "PRODUCT_IMAGE", columnDefinition = "LONGTEXT") + private String productImage; + @Column(name = "PILL_IMAGE", columnDefinition = "LONGTEXT") + private String pillImage; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java deleted file mode 100644 index 1bd9517..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/GovDrugJdbcRepository.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jdbc; - -import java.util.List; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; - -@Repository -@RequiredArgsConstructor -public class GovDrugJdbcRepository { - private final JdbcTemplate jdbc; - - @Transactional - public void saveAll(List entities) { - String sql = "" - + "INSERT INTO gov_drug_detail " - + " (ITEM_SEQ, ITEM_NAME, ENTP_NAME, " - + " ITEM_PERMIT_DATE, ETC_OTC_CODE, MATERIAL_NAME, " - + " STORAGE_METHOD, VALID_TERM, EE_DOC_DATA, " - + " UD_DOC_DATA, NB_DOC_DATA) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " - + "ON DUPLICATE KEY UPDATE " - + " ITEM_NAME = VALUES(ITEM_NAME), " - + " ENTP_NAME = VALUES(ENTP_NAME), " - + " ITEM_PERMIT_DATE = VALUES(ITEM_PERMIT_DATE), " - + " ETC_OTC_CODE = VALUES(ETC_OTC_CODE), " - + " MATERIAL_NAME = VALUES(MATERIAL_NAME), " - + " STORAGE_METHOD = VALUES(STORAGE_METHOD), " - + " VALID_TERM = VALUES(VALID_TERM), " - + " EE_DOC_DATA = VALUES(EE_DOC_DATA), " - + " UD_DOC_DATA = VALUES(UD_DOC_DATA), " - + " NB_DOC_DATA = VALUES(NB_DOC_DATA)"; - jdbc.batchUpdate(sql, new JdbcBatchSetter(entities)); - } - - @Transactional - public void mergeAll(List entities) { - String sql = "" - + "INSERT INTO gov_drug_raw_data " - + " (ITEM_SEQ, ITEM_NAME, ENTP_NAME, " - + " ITEM_PERMIT_DATE, ETC_OTC_CODE, MATERIAL_NAME, " - + " STORAGE_METHOD, VALID_TERM, EE_DOC_DATA, " - + " UD_DOC_DATA, NB_DOC_DATA, IMG_URL, " - + " GPT_VECTOR, KR_SBERT_VECTOR, KM_BERT_VECTOR) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " - + "ON DUPLICATE KEY UPDATE " - + " ITEM_NAME = VALUES(ITEM_NAME), " - + " ENTP_NAME = VALUES(ENTP_NAME), " - + " ITEM_PERMIT_DATE = VALUES(ITEM_PERMIT_DATE), " - + " ETC_OTC_CODE = VALUES(ETC_OTC_CODE), " - + " MATERIAL_NAME = VALUES(MATERIAL_NAME), " - + " STORAGE_METHOD = VALUES(STORAGE_METHOD), " - + " VALID_TERM = VALUES(VALID_TERM), " - + " EE_DOC_DATA = VALUES(EE_DOC_DATA), " - + " UD_DOC_DATA = VALUES(UD_DOC_DATA), " - + " NB_DOC_DATA = VALUES(NB_DOC_DATA), " - + " IMG_URL = VALUES(IMG_URL), " - + " GPT_VECTOR = VALUES(GPT_VECTOR), " - + " KR_SBERT_VECTOR = VALUES(KR_SBERT_VECTOR), " - + " KM_BERT_VECTOR = VALUES(KM_BERT_VECTOR) "; - jdbc.batchUpdate(sql, new MergeBatchSetter(entities)); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java deleted file mode 100644 index 8e46c34..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/JdbcBatchSetter.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jdbc; - -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Types; -import java.time.LocalDate; -import java.util.List; - -import org.springframework.jdbc.core.BatchPreparedStatementSetter; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class JdbcBatchSetter implements BatchPreparedStatementSetter { - - private final List entities; - - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - DrugDetailEntity e = entities.get(i); - ps.setLong (1, e.getDrugId()); - ps.setString (2, e.getDrugName()); - ps.setString (3, e.getCompany()); - - LocalDate permit = e.getPermitDate(); - if (permit != null) { - ps.setDate(4, Date.valueOf(permit)); - } else { - ps.setNull(4, Types.DATE); - } - - ps.setBoolean(5, e.isGeneral()); - ps.setString (6, e.getMaterialInfo()); - ps.setString (7, e.getStoreMethod()); - ps.setString (8, e.getValidTerm()); - ps.setString (9, e.getEfficacy()); - ps.setString (10, e.getUsage()); - ps.setString (11, e.getPrecaution()); - } - - @Override - public int getBatchSize() { - return entities.size(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java deleted file mode 100644 index 6dabccf..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jdbc/MergeBatchSetter.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jdbc; - -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Types; -import java.time.LocalDate; -import java.util.List; - -import org.springframework.jdbc.core.BatchPreparedStatementSetter; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class MergeBatchSetter implements BatchPreparedStatementSetter { - - private final List entities; - - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - DrugRawDataEntity e = entities.get(i); - ps.setLong (1, e.getDrugId()); - ps.setString (2, e.getDrugName()); - ps.setString (3, e.getCompany()); - - LocalDate permit = e.getPermitDate(); - if (permit != null) { - ps.setDate(4, Date.valueOf(permit)); - } else { - ps.setNull(4, Types.DATE); - } - - ps.setBoolean(5, e.isGeneral()); - ps.setString (6, e.getMaterialInfo()); - ps.setString (7, e.getStoreMethod()); - ps.setString (8, e.getValidTerm()); - ps.setString (9, e.getEfficacy()); - ps.setString (10, e.getUsage()); - ps.setString (11, e.getPrecaution()); - ps.setString(12, e.getImageUrl()); - } - - @Override - public int getBatchSize() { - return entities.size(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java index 8155dfa..79bb823 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java @@ -4,10 +4,17 @@ 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 com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; public interface GovDrugJpaRepository extends JpaRepository { Page findByIsGeneralIsTrue(Pageable pageable); + + @Query("SELECT MIN(d.drugId) FROM DrugRawDataEntity d") + Long findMinDrugId(); + + @Query("SELECT MAX(d.drugId) FROM DrugRawDataEntity d") + Long findMaxDrugId(); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java deleted file mode 100644 index 4459365..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageMapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; - -import java.util.List; - -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugImage; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; - -public class DrugImageMapper { - public static DrugImage toDomainFromEntity(ApiDataDrugImgEntity e){ - return DrugImage.builder() - .drugId(e.getDrugId()) - .imageUrl(e.getImgUrl()) - .build(); - } - - public static ApiDataDrugImgEntity toEntityFromDomain(DrugImage d){ - return ApiDataDrugImgEntity.builder() - .drugId(d.getDrugId()) - .imgUrl(d.getImageUrl()) - .build(); - } - - public static List toEntityListFromDomainList(List drugImageList) { - return drugImageList.stream().map(DrugImageMapper::toEntityFromDomain).toList(); - } - - public static List toDomainListFromEntityList(List drugImageEntityList) { - return drugImageEntityList.stream().map(DrugImageMapper::toDomainFromEntity).toList(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java new file mode 100644 index 0000000..6425c90 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java @@ -0,0 +1,14 @@ +package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugImageRequest; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; + +public class DrugImageRequestMapper { + public static ApiDataDrugImgEntity toEntityFromRequest(DrugImageRequest r){ + return ApiDataDrugImgEntity.builder() + .drugId(r.getDrugId()) + .productImage(r.getProductImage()) + .pillImage(r.getPillImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java index c772ad7..3fecf61 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java @@ -1,8 +1,8 @@ package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java deleted file mode 100644 index 2ae6634..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugCombineUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.drug.application.service.scraper.DrugScraper; - -import lombok.RequiredArgsConstructor; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; - -@RestController -@RequiredArgsConstructor -public class DrugDataTestController { - private final DrugScraper scraperUseCase; - private final DrugCombineUsecase drugCombineUsecase; - private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; - - @GetMapping("/test/parse") - public ResponseEntity saveAPIData(){ - scraperUseCase.scraperStart(); - return ResponseEntity.ok().build(); - } - - @GetMapping("/test/combine") - public ResponseEntity saveCombineData(){ - drugCombineUsecase.mergeTable(); - return ResponseEntity.ok().build(); - } - - @GetMapping("/test/embed") - public ResponseEntity saveEmbedData(){ - drugEmbedProcessorUseCase.startEmbedding(); - return ResponseEntity.ok().build(); - } - - // 임베딩 모델을 스위칭하는 엔드포인트 - @GetMapping("/test/switchEmbed") - public ResponseEntity switchEmbedding(@RequestParam String modelType) { - drugEmbedProcessorUseCase.switchEmbeddingModel(modelType); - return ResponseEntity.ok().build(); - } - - // 현재 사용 중인 임베딩 모델을 조회하는 엔드포인트 - @GetMapping("/test/currentEmbed") - public ResponseEntity getCurrentEmbedding() { - EmbeddingModelType currentModel = drugEmbedProcessorUseCase.getCurrentEmbeddingModel(); - return ResponseEntity.ok(currentModel); // 현재 모델을 반환 - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java deleted file mode 100644 index 4cac8b3..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PostMapping; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.DrugDetailScraperUsecase; - -import lombok.RequiredArgsConstructor; - -@Controller -@RequiredArgsConstructor -public class DrugDetailController { - private final DrugDetailScraperUsecase drugDetailScraperUsecase; - - @PostMapping("/gov/api/parser/detail/start") - public ResponseEntity saveAPIData(){ - drugDetailScraperUsecase.requestSingleData(1); - return ResponseEntity.ok().build(); - } - - @PostMapping("/gov/api/parser/detail/startAll") - public ResponseEntity saveAPIDataAll(){ - drugDetailScraperUsecase.requestAllData(); - return ResponseEntity.ok().build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java deleted file mode 100644 index c818df0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.likelion.backendplus4.yakplus.drug.application.service.scraper.image.DrugImageGovScraper; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class DrugImageController { - private final DrugImageGovScraper imageScraper; - - @GetMapping("/gov/api/parser/image/start") - public void test(){ - imageScraper.getApiData(1); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java new file mode 100644 index 0000000..dd514a4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java @@ -0,0 +1,67 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.embed; + +import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 약품 임베딩 벡터 생성 작업을 제어하는 컨트롤러입니다. + * 벡터 생성 시작, 중지, 상태 조회 API를 제공합니다. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/embed") +@RequiredArgsConstructor +public class DrugEmbedController { + private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; + + /** + * 임베딩 벡터 생성 작업을 시작합니다. + * + * @return 작업 시작 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @PostMapping("/start") + public ResponseEntity> start() { + return success(drugEmbedProcessorUseCase.startEmbedding()); + } + + /** + * 임베딩 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugEmbedProcessorUseCase.stopEmbedding()); + } + + /** + * 임베딩 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @GetMapping("/status") + public ResponseEntity> status() { + return success(drugEmbedProcessorUseCase.statusEmbedding()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java new file mode 100644 index 0000000..5397f00 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java @@ -0,0 +1,15 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.embed.dto; + +import lombok.Getter; +import lombok.Setter; + +/** + * 임베딩 모델 전환 요청을 위한 DTO 클래스입니다. + * + * @since 2025-05-02 + */ +@Getter +@Setter +public class ModelSwitchRequeset { + String modelType; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java new file mode 100644 index 0000000..5dcb7bb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java @@ -0,0 +1,60 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.embed.model; + +import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.drug.presentation.controller.embed.dto.ModelSwitchRequeset; +import com.likelion.backendplus4.yakplus.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 임베딩 모델 전환 및 현재 모델 조회를 위한 컨트롤러입니다. + * + * @since 2025-05-02 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/embed/model") +public class embedModelController { + private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; + + /** + * 임베딩 모델을 전환합니다. + * + * @param request 전환할 모델 타입 요청 DTO + * @return 전환된 모델 정보 응답 + * + * @author 정안식 + * @since 2025-05-01 + * @modify 2025-05-02 함예정 + * - 스프링 배치 전환에 따른 수정 + */ + @PostMapping("/switch") + public ResponseEntity switchEmbedding(@RequestBody ModelSwitchRequeset request) { + drugEmbedProcessorUseCase.switchEmbeddingModel(request.getModelType()); + return success("요청 성공: "+ drugEmbedProcessorUseCase.getCurrentEmbeddingModel()); + } + + /** + * 현재 사용 중인 임베딩 모델을 조회합니다. + * + * @return 현재 모델 이름 + * + * @author 정안식 + * @since 2025-05-01 + * @modify 2025-05-02 함예정 + * - 스프링 배치 전환에 따른 수정 + */ + @GetMapping + public ResponseEntity> getModel(){ + return success(drugEmbedProcessorUseCase.getCurrentEmbeddingModel()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java new file mode 100644 index 0000000..21a9d1c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java @@ -0,0 +1,48 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper; + +import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperUsecase; +import com.likelion.backendplus4.yakplus.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 정보 수집 전체 작업을 제어하는 컨트롤러입니다. + * 작업 시작, 중지, 상태 조회 API를 제공합니다. + * + * @since 2025-05-02 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/scraper") +public class DrugScraperController { + private final DrugScraperUsecase drugScraperUsecase; + + @PostMapping("/start") + public ResponseEntity> start() { + return success(drugScraperUsecase.scraperStart()); + } + + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperUsecase.stop()); + } + + @PostMapping("/restart") + public ResponseEntity> restart() { + return success(drugScraperUsecase.restart()); + } + + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperUsecase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java new file mode 100644 index 0000000..a685af7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java @@ -0,0 +1,66 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper.combine; + +import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperTableCombineUsecase; +import com.likelion.backendplus4.yakplus.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세정보와 이미지 테이블 병합 작업을 제어하는 컨트롤러입니다. + * 작업 시작, 중지, 상태 조회 API를 제공합니다. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/scraper/combine") +@RequiredArgsConstructor +public class DrugScraperTableCombineController { + private final DrugScraperTableCombineUsecase drugScraperTableCombineUsecase; + + /** + * 의약품 상세정보와 이미지 정보 테이블을 병합하는 작업을 시작합니다. + * + * @return 성공 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @PostMapping("/start") + public ResponseEntity> start(){ + return success(drugScraperTableCombineUsecase.mergeTable()); + } + + /** + * 진행 중인 작업을 중지합니다. + * + * @return 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperTableCombineUsecase.stop()); + } + + /** + * 작업의 현재 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperTableCombineUsecase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java new file mode 100644 index 0000000..79273cd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java @@ -0,0 +1,66 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper.details; + +import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperDetailUseCase; +import com.likelion.backendplus4.yakplus.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세정보 수집 작업을 제어하는 컨트롤러입니다. + * 작업 시작, 중지, 상태 조회 API를 제공합니다. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/scraper/details") +@RequiredArgsConstructor +public class DrugScraperDetailController { + private final DrugScraperDetailUseCase drugScraperDetailUseCase; + + /** + * 의약품 상세정보 수집 작업을 시작합니다. + * + * @return 성공 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @PostMapping("/start") + public ResponseEntity> start() { + return success(drugScraperDetailUseCase.requestAllData()); + } + + /** + * 의약품 상세정보 수집 작업을 중지합니다. + * + * @return 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperDetailUseCase.stop()); + } + + /** + * 작업의 현재 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperDetailUseCase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java new file mode 100644 index 0000000..cbf644d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java @@ -0,0 +1,67 @@ +package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper.image; + +import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperImageUsecase; +import com.likelion.backendplus4.yakplus.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 이미지 수집 작업을 제어하는 컨트롤러입니다. + * 작업 시작, 중지, 상태 조회 API를 제공합니다. + * + * @since 2025-05-02 + */ + +@RestController +@RequestMapping("/scraper/images") +@RequiredArgsConstructor +public class DrugScraperImageController { + private final DrugScraperImageUsecase drugScraperImageUsecase; + + /** + * 의약품 이미지 수집 작업을 시작합니다. + * + * @return 성공 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @PostMapping("/start") + public ResponseEntity> start(){ + return success(drugScraperImageUsecase.requestAllData()); + } + + /** + * 의약품 이미지 수집 작업을 중지합니다. + * + * @return 중지 결과 메시지 + * + * @author 함예정 + * @since 2025-05-02 + */ + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperImageUsecase.stop()); + } + + /** + * 작업의 현재 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperImageUsecase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java index c3558c3..7ee25f7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java @@ -1,14 +1,16 @@ package com.likelion.backendplus4.yakplus.index.application.port.out; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import java.util.List; + import org.springframework.data.domain.Pageable; -import java.util.List; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; public interface EmbeddingLoadingPort { - List loadEmbeddingsByPage(Pageable pageable); + List loadEmbeddingsByPage(Pageable pageable); - float[] getEmbedding(String text); + float[] getEmbedding(String text); - void saveEmbedding(Long drugId, float[] embedding); + void saveEmbedding(List dtos); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java index 08a7cc3..3811a5c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java @@ -1,7 +1,10 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; import java.util.List; @@ -43,4 +46,17 @@ public static float[] parseJsonToFloatArray(String json) { throw new RuntimeException("Failed to parse vector JSON", e); } } + + public static String convertSingleStringForEfficacy(List stringList) { + log(LogLevel.DEBUG, "약품 효능 정보 단일 문자로 변환 시작"); + StringBuilder stringBuilder = new StringBuilder(); + for (String s : stringList) { + stringBuilder.append(s); + stringBuilder.append(" "); + } + + String s = stringBuilder.toString(); + log(LogLevel.DEBUG, "약품 효능 정보 단일 문자로 변환 완료" + s); + return s; + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java index 0135079..68003cf 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java @@ -1,17 +1,10 @@ package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; -import lombok.RequiredArgsConstructor; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; + +import java.util.ArrayList; +import java.util.List; + import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.Embedding; import org.springframework.ai.embedding.EmbeddingResponse; @@ -22,75 +15,87 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; -import java.util.ArrayList; -import java.util.List; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; +import com.likelion.backendplus4.yakplus.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import lombok.RequiredArgsConstructor; @Repository @RequiredArgsConstructor public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { - private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; - private final OpenAiApi openAiApi; - - @Override - public List loadEmbeddingsByPage(Pageable pageable) { - List drugs = new ArrayList<>(); - List rows = govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); - if (rows.isEmpty()) { - log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); - throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); - } - for (Object[] arr : rows) { - DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; - DrugGptEmbedEntity embed = (DrugGptEmbedEntity) arr[1]; - drugs.add(toDomainFromEntity(raw, embed)); - } - log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); - return drugs; - } + private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; + private final OpenAiApi openAiApi; - @Override - public float[] getEmbedding(String text) { - OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( - this.openAiApi, - MetadataMode.EMBED, - OpenAiEmbeddingOptions.builder() - .model("text-embedding-3-small") - .build(), - RetryUtils.DEFAULT_RETRY_TEMPLATE); - EmbeddingResponse embeddingResponse = openAiEmbeddingModel - .embedForResponse(List.of(text)); - Embedding embedding = embeddingResponse.getResults().getFirst(); - return embedding.getOutput(); - } + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List drugs = new ArrayList<>(); + List rows = govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + for (Object[] arr : rows) { + DrugRawDataEntity raw = (DrugRawDataEntity)arr[0]; + DrugGptEmbedEntity embed = (DrugGptEmbedEntity)arr[1]; + drugs.add(toDomainFromEntity(raw, embed)); + } + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); + return drugs; + } - @Override - public void saveEmbedding(Long drugId, float[] embedding) { - govDrugGptEmbedJpaRepository.save(EmbedEntityBuilder.buildEmbedEntity(drugId, embedding, DrugGptEmbedEntity.class)); - } + @Override + public float[] getEmbedding(String text) { + OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( + this.openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model("text-embedding-3-small") + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE); + EmbeddingResponse embeddingResponse = openAiEmbeddingModel + .embedForResponse(List.of(text)); + Embedding embedding = embeddingResponse.getResults().getFirst(); + return embedding.getOutput(); + } - private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbedEntity embedEntity) { - return Drug.builder() - .drugId(drugEntity.getDrugId()) - .drugName(drugEntity.getDrugName()) - .company(drugEntity.getCompany()) - .permitDate(drugEntity.getPermitDate()) - .isGeneral(drugEntity.isGeneral()) - .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) - .storeMethod(drugEntity.getStoreMethod()) - .validTerm(drugEntity.getValidTerm()) - .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) - .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) - .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) - .imageUrl(drugEntity.getImageUrl()) - .cancelDate(drugEntity.getCancelDate()) - .cancelName(drugEntity.getCancelName()) - .isHerbal(drugEntity.getIsHerbal()) - .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getGptVector())) - .build(); - } + @Override + public void saveEmbedding(List dtos) { + govDrugGptEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugGptEmbedEntity.class)) + .toList() + ); + govDrugGptEmbedJpaRepository.flush(); + } + private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbedEntity embedEntity) { + return Drug.builder() + .drugId(drugEntity.getDrugId()) + .drugName(drugEntity.getDrugName()) + .company(drugEntity.getCompany()) + .permitDate(drugEntity.getPermitDate()) + .isGeneral(drugEntity.isGeneral()) + .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) + .storeMethod(drugEntity.getStoreMethod()) + .validTerm(drugEntity.getValidTerm()) + .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) + .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) + .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) + .imageUrl(drugEntity.getImageUrl()) + .cancelDate(drugEntity.getCancelDate()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) + .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getGptVector())) + .build(); + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java index db6a854..f4a1c23 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java @@ -2,7 +2,8 @@ import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; @@ -29,7 +30,7 @@ @RequiredArgsConstructor public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; - private final ApiUriCompBuilder apiUriCompBuilder; + private final UriCompBuilder apiUriCompBuilder; private final RestTemplate restTemplate; @Override @@ -58,8 +59,13 @@ public float[] getEmbedding(String text) { } @Override - public void saveEmbedding(Long drugId, float[] embedding) { - govDrugKmBertEmbedJpaRepository.save(EmbedEntityBuilder.buildEmbedEntity(drugId, embedding, DrugKmBertEmbedEntity.class)); + public void saveEmbedding(List dtos) { + govDrugKmBertEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKmBertEmbedEntity.class)) + .toList() + ); + govDrugKmBertEmbedJpaRepository.flush(); } private URI getEmbeddingURI() { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java index 7a2ef12..d37e498 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java @@ -2,18 +2,18 @@ import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.support.ApiUriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKrSbertEmbedJpaRepository; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import com.likelion.backendplus4.yakplus.index.exception.IndexException; import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; + import org.springframework.data.domain.Pageable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -23,9 +23,7 @@ import java.net.URI; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; @@ -33,7 +31,7 @@ @RequiredArgsConstructor public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; - private final ApiUriCompBuilder apiUriCompBuilder; + private final UriCompBuilder apiUriCompBuilder; private final RestTemplate restTemplate; @Override @@ -62,8 +60,13 @@ public float[] getEmbedding(String text) { } @Override - public void saveEmbedding(Long drugId, float[] embedding) { - govDrugKrSbertEmbedJpaRepository.save(EmbedEntityBuilder.buildEmbedEntity(drugId, embedding, DrugKrSbertEmbedEntity.class)); + public void saveEmbedding(List dtos) { + govDrugKrSbertEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKrSbertEmbedEntity.class)) + .toList() + ); + govDrugKrSbertEmbedJpaRepository.flush(); } private float[] getEmbeddingVector(URI embedUri, String text) { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java index b7271d1..580d5cb 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java @@ -2,14 +2,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; public class EmbedEntityBuilder { private static final ObjectMapper MAPPER = new ObjectMapper(); - public static T buildEmbedEntity(Long drugId, float[] vector, Class clazz) { + public static T buildEmbedEntity(DrugVectorDto dto, Class clazz) { try { return clazz.getDeclaredConstructor(Long.class, String.class) - .newInstance(drugId, toStringFromFloatArray(vector)); + .newInstance(dto.getDrugId(), toStringFromFloatArray(dto.getVector())); } catch (Exception e) { //TODO: 엔터티 생성 실패 throw new RuntimeException(e); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java index 17cff83..6fdced3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java @@ -2,6 +2,7 @@ import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; import jakarta.annotation.PostConstruct; @@ -71,11 +72,11 @@ public float[] getEmbedding(String text) { } @Override - public void saveEmbedding(Long drugId, float[] embedding) { + public void saveEmbedding(List dtos) { if (embeddingLoadingPort == null) { log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); throw new IllegalStateException("No adapter selected"); } - embeddingLoadingPort.saveEmbedding(drugId, embedding); + embeddingLoadingPort.saveEmbedding(dtos); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java index 30fc798..3548cf6 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java @@ -1,7 +1,7 @@ package com.likelion.backendplus4.yakplus.temp.support.mapper; -import com.likelion.backendplus4.yakplus.drug.domain.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.domain.exception.error.ScraperErrorCode; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.DrugMapper; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0557408..b5f334b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,6 +11,8 @@ spring: username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 30 jpa: database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: @@ -20,6 +22,11 @@ spring: properties: hibernate: format_sql: true + batch: + job: + enabled: false + jdbc: + initialize-schema: embedded logging: level: org.springframework.jdbc: DEBUG From 5471177b20c93391dce55438ec2bcb3465c07827 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Mon, 5 May 2025 16:36:56 +0900 Subject: [PATCH 42/47] =?UTF-8?q?=E2=9C=A8=20=20Feature:=20#70=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=86=B5=ED=95=A9=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Feature: yakplus-batch의 코드 통합 초안 * 🐛 Fix: 컴파일 오류 수정 -1 * 🐛 Fix: 컴파일 오류 수정 -2 * 🐛 Fix: Job 매니저 TaskExecuter 이름 변경에 따른 오류 수정 * ♻️ Refactor: 패키지 구조 변경 * 📦 Chore: Swagger 의존성 추가 및 설정 추가 * ✨ Feat: 스웨거 적용 * ✨ Feat: `DrugControllerDocs` 설명 추가 * ♻️ Refactor: 키워드 색인시 한약 제외하도록 쿼리 변경 * 🐛 Fix: 임베딩 오류 수정 및 불필요한 로그 삭제 --------- Co-authored-by: leelise Co-authored-by: pcb7893@naver.com <122460524+chanbyoung@users.noreply.github.com> --- build.gradle | 3 + .../yakplus/YakplusApplication.java | 9 +- .../common/configuration/AllJobConfig.java | 60 +++ .../configuration}/ApiRestTemplateConfig.java | 12 +- .../configuration/BatchExecutorConfig.java | 58 +++ .../common/configuration/LogbackConfig.java | 16 +- .../configuration}/OpenaiConfig.java | 5 +- .../common/configuration/SwaggerConfig.java | 21 + .../common/configuration/WebConfig.java | 2 +- .../handler/GlobalExceptionHandler.java | 152 ++++--- .../common/interceptor/LogInterceptor.java | 4 +- .../trace}/LoggerWithTraceId.java | 22 +- .../trace/decorator/MdcTaskDecorator.java | 37 ++ .../{util/log => logging/util}/LogLevel.java | 26 +- .../log => logging/util}/LogMessage.java | 2 +- .../{util/log => logging/util}/LogUtil.java | 4 +- .../{ => common}/response/ApiResponse.java | 2 +- .../port/in/DictionaryUseCase.java | 7 - .../SymptomDictionaryElsRepositoryPort.java | 9 - .../SymptomDictionaryJpaRepositoryPort.java | 9 - .../port/out/SymptomDictionaryLoaderPort.java | 9 - .../service/DictionaryService.java | 51 --- .../out/JsonSymptomDictionaryLoader.java | 63 --- .../SymptomDictionaryDocRepository.java | 8 - .../SymptomDictionaryRepository.java | 8 - .../yakplus/drug/domain/model/Drug.java | 40 +- .../port/in}/DrugEmbedProcessorUseCase.java | 2 +- .../port/in/EmbeddingRoutingUseCase.java | 22 + .../port/out/EmbeddingSwitchPort.java | 23 + .../service}/DrugEmbedProcessorService.java | 8 +- .../application/service/EmbeddingRouter.java | 42 ++ .../batch/job/config/EmbedJobConfig.java | 58 +++ .../batch/step/config/EmbedStepConfig.java | 358 +++++++++++++++ .../batch/step/dto/DrugVectorDto.java | 19 + .../batch/step/processor/EmbedProcessor.java | 62 +++ .../step/reader/DrugIdRangePartitioner.java | 59 +++ .../batch/step/writer/EmbedWriter.java | 47 ++ .../entity/DrugGptEmbedEntity.java | 9 +- .../entity/DrugKmBertEmbedEntity.java | 9 +- .../entity/DrugKrSBertEmbedEntity.java} | 11 +- .../persistence/entity/EmbeddingEntity.java | 18 + .../jpa/adapter/EmbeddingRouterAdapter.java | 141 ++++++ .../jpa/adapter/KmBertEmbeddingAdapter.java | 151 +++++++ .../jpa/adapter/KrSBertEmbeddingAdapter.java | 150 ++++++ .../jpa/adapter/OpenAiEmbeddingAdapter.java | 144 ++++++ .../repository/KmBertEmbedJpaRepository.java} | 14 +- .../repository/KrSBertEmbedJpaRepository.java | 25 + .../repository/OpenAiEmbedJpaRepository.java} | 14 +- .../presentation}/DrugEmbedController.java | 16 +- .../EmbeddingRouterController.java | 53 +++ .../docs/DrugEmbedControllerDocs.java | 26 ++ .../docs/EmbeddingRouterControllerDocs.java | 37 ++ .../application/port/in/IndexUseCase.java | 28 ++ .../port/out/DrugIndexRepositoryPort.java | 37 ++ .../application/port/out/DrugRawDataPort.java | 34 ++ .../application/port/out/EmbeddingPort.java | 46 ++ .../application/service/DrugIndexer.java | 110 +++-- .../index/exception/IndexException.java | 2 +- .../index/exception/error/IndexErrorCode.java | 4 +- .../adapter}/ElasticsearchDrugAdapter.java | 50 +- .../document/DrugKeywordDocument.java | 5 +- .../repository/DrugKeywordRepository.java | 8 + .../controller/DrugIndexingController.java} | 21 +- .../docs/DrugIndexingControllerDocs.java | 27 ++ .../EmbeddingUtil/DrugEntityMapper.java | 39 ++ .../EmbeddingUtil/EmbedEntityBuilder.java | 8 +- .../index/support/mapper/KeywordMapper.java | 27 ++ .../support/parser/SymptomTextParser.java | 2 +- .../api/exception/RestApiError.java | 33 -- .../batch/adapter/BatchJobAdapter.java | 111 ----- .../combine/config/CombineJobConfig.java | 19 - .../combine/config/CombineStepConfig.java | 85 ---- .../batch/combine/dto/TableCombineDto.java | 27 -- .../processor/TableCombineProcessor.java | 192 -------- .../combine/writer/TableCombineWriter.java | 34 -- .../batch/common/config/BatchConfig.java | 51 --- .../batch/common/config/CommonJobConfig.java | 51 --- .../batch/common/util/MaterialParser.java | 110 ----- .../batch/common/util/MdcTaskDecorator.java | 37 -- .../batch/common/util/StepSkipDecider.java | 21 - .../batch/common/util/XMLParser.java | 339 -------------- .../batch/detail/config/DetailJobConfig.java | 24 - .../batch/detail/config/DetailStepConfig.java | 122 ----- .../batch/detail/dto/DrugDetailRequest.java | 76 ---- .../processor/DetailTotalPageCalculator.java | 30 -- .../detail/processor/DrugDetailProcessor.java | 90 ---- .../detail/reader/DetailPageNumberReader.java | 51 --- .../batch/detail/writer/DrugDetailWriter.java | 28 -- .../batch/embed/config/EmbedJobConfig.java | 31 -- .../batch/embed/config/EmbedStepConfig.java | 211 --------- .../batch/embed/dto/DrugVectorDto.java | 11 - .../batch/embed/processor/EmbedProcessor.java | 51 --- .../embed/reader/DrugIdRangePartitioner.java | 42 -- .../batch/embed/writer/EmbedWriter.java | 34 -- .../batch/exception/ParserBatchError.java | 34 -- .../batch/image/config/ImageJobConfig.java | 22 - .../batch/image/config/ImageStepConfig.java | 136 ------ .../image/processor/ImageScrapProcessor.java | 49 -- .../processor/ImageTotalPageCalculator.java | 31 -- .../image/reader/ImagePageNumberReader.java | 53 --- .../image/reader/PartitionedPageReader.java | 34 -- .../batch/image/writer/DrugImageWriter.java | 31 -- .../batch/mapper/ApiResponseMapper.java | 45 -- .../batch/mapper/DrugDetailRequestMapper.java | 26 -- .../embedding/client/EmbeddingClient.java | 8 - .../client/KmBertEmbeddingClient.java | 47 -- .../client/KrSbertEmbeddingClient.java | 47 -- .../client/OpenaiEmbeddingClient.java | 45 -- .../embedding/model/EmbeddingModelType.java | 7 - .../repository/jpa/ApiDataDrugImgRepo.java | 10 - .../jpa/GovDrugDetailJpaRepository.java | 21 - .../repository/jpa/GovDrugJpaRepository.java | 20 - .../jpa/GovDrugKrSbertEmbedJpaRepository.java | 24 - .../mapper/DrugImageRequestMapper.java | 14 - .../support/mapper/DrugRawDataMapper.java | 74 --- .../embed/dto/ModelSwitchRequeset.java | 15 - .../embed/model/embedModelController.java | 60 --- .../exception/ScraperException.java | 2 +- .../exception/error/ScraperErrorCode.java | 8 +- .../port/in}/DrugScraperDetailUseCase.java | 2 +- .../port/in/DrugScraperImageUseCase.java} | 4 +- .../in/DrugScraperTableCombineUseCase.java} | 4 +- .../port/in/DrugScraperUseCase.java} | 4 +- .../application}/port/out/BatchJobPort.java | 2 +- .../service}/DrugScraperDetailService.java | 6 +- .../service}/DrugScraperService.java | 8 +- .../service}/DrugScraperServiceImage.java | 8 +- .../DrugScraperTableCombineService.java | 8 +- .../api/exception/RestApiError.java | 36 ++ .../api/exception/RestApiException.java | 2 +- .../api/util/ApiRequestManager.java | 11 +- .../api/util/UriCompBuilder.java | 2 +- .../batch/adapter/BatchJobAdapter.java | 238 ++++++++++ .../batch/exception/ParserBatchException.java | 2 +- .../exception/error/ParserBatchError.java | 41 ++ .../batch/job/config/CombineJobConfig.java | 35 ++ .../batch/job/config/DetailJobConfig.java | 43 ++ .../batch/job/config/ImageJobConfig.java | 47 ++ .../step/combine/dto/TableCombineDto.java | 51 +++ .../processor/TableCombineProcessor.java | 264 +++++++++++ .../combine/writer/TableCombineWriter.java | 47 ++ .../batch/step/config/CombineStepConfig.java | 141 ++++++ .../batch/step/config/DetailStepConfig.java | 120 +++++ .../batch/step/config/ImageStepConfig.java | 135 ++++++ .../step/detail/dto/DrugDetailRequest.java | 106 +++++ .../processor/DetailTotalPageCalculator.java | 44 ++ .../detail/processor/DrugDetailProcessor.java | 109 +++++ .../detail/reader/DetailPageNumberReader.java | 47 ++ .../step/detail/writer/DrugDetailWriter.java | 45 ++ .../image/processor/ImageScrapProcessor.java | 74 +++ .../processor/ImageTotalPageCalculator.java | 49 ++ .../image/reader/PageRangePartitioner.java | 22 +- .../image/reader/PartitionedPageReader.java | 65 +++ .../step/image/writer/DrugImageWriter.java | 41 ++ .../batch/util/mapper/ApiResponseMapper.java | 74 +++ .../util/mapper/DrugDetailRequestMapper.java | 38 ++ .../batch/util/parser/MaterialParser.java | 108 +++++ .../batch/util/parser/XMLParser.java | 426 ++++++++++++++++++ .../persistence/dto/DrugImageRequest.java | 6 +- .../dto}/EmbeddingRequestText.java | 2 +- .../repository/entity/DrugDetailEntity.java | 21 +- .../repository/entity/DrugImgEntity.java} | 4 +- .../repository/entity/DrugRawDataEntity.java | 2 +- .../jpa/adapter/DrugRawDataAdapter.java | 101 +++++ .../repository/DrugDetailJpaRepository.java | 10 + .../jpa/repository/DrugImgRepository.java | 10 + .../jpa/repository/DrugJpaRepository.java | 25 + .../support/mapper/DrugFieldTypeMapper.java} | 8 +- .../mapper/DrugImageRequestMapper.java | 14 + .../support/mapper/DrugRawDataMapper.java | 27 ++ .../support/parser/JsonArrayTextParser.java | 145 ++++++ .../controller}/DrugScraperController.java | 19 +- .../DrugScraperTableCombineController.java | 16 +- .../details/DrugScraperDetailController.java | 14 +- .../docs/DrugScraperControllerDocs.java | 45 ++ .../docs/DrugScraperDetailControllerDocs.java | 33 ++ .../docs/DrugScraperImageControllerDocs.java | 33 ++ ...DrugScraperTableCombineControllerDocs.java | 33 ++ .../image/DrugScraperImageController.java | 16 +- .../batch/adapter => support}/JobManager.java | 27 +- .../application/port/in/IndexUseCase.java | 7 - .../port/out/DrugIndexRepositoryPort.java | 12 - .../port/out/EmbeddingLoadingPort.java | 16 - .../port/out/GovDrugRawDataPort.java | 15 - .../index/config/ElasticsearchConfig.java | 8 - .../yakplus/index/config/OpenAiConfig.java | 19 - .../persistence/GovDrugRawDataAdapter.java | 155 ------- .../GptEmbeddingLoadingAdapter.java | 101 ----- .../KmBertEmbeddingLoadingAdapter.java | 106 ----- .../KrSBertEmbeddingLoadingAdapter.java | 107 ----- .../repository/DrugSymptomRepository.java | 7 - .../document/DrugSymptomDocument.java | 40 -- .../controller/dto/request/IndexRequest.java | 15 - .../index/support/mapper/SymptomMapper.java | 43 -- .../support/parser/JsonArrayTextParser.java | 106 ----- .../yakplus/logtest/MyController.java | 48 -- .../yakplus/logtest/MyService.java | 54 --- .../switcher/application/EmbeddingRouter.java | 28 -- .../port/in/EmbeddingRoutingUseCase.java | 6 - .../port/out/EmbeddingSwitchPort.java | 6 - .../route/adapter/EmbeddingRouterAdapter.java | 82 ---- .../controller/EmbeddingRouterController.java | 30 -- .../port/in/DictionaryUseCase.java | 20 + .../SymptomDictionaryElsRepositoryPort.java | 19 + .../SymptomDictionaryJpaRepositoryPort.java | 23 + .../port/out/SymptomDictionaryLoaderPort.java | 21 + .../service/DictionaryService.java | 59 +++ .../exception/DictionaryException.java | 2 +- .../exception/error/DictionaryErrorCode.java | 2 +- .../out/JsonSymptomDictionaryLoader.java | 66 +++ .../out/SymptomDictionaryElsAdapter.java | 27 +- .../out/SymptomDictionaryJpaAdapter.java | 16 +- .../SymptomDictionaryDocRepository.java | 7 + .../SymptomDictionaryRepository.java | 7 + .../document/SymptomDictionaryDocument.java | 2 +- .../repository/entity/SymptomDictionary.java | 2 +- .../support/mapper/DictionaryMapper.java | 21 +- .../controller/DictionaryController.java | 24 +- .../docs/DictionaryControllerDocs.java | 22 + .../application/port/in/IndexTempUseCase.java | 6 - .../port/out/TempDrugIndexRepositoryPort.java | 11 - .../application/port/out/TempRawDataPort.java | 10 - .../application/service/DrugTempIndexer.java | 45 -- .../TempElasticsearchDrugAdapter.java | 39 -- .../TempGovDrugRawDataAdapter.java | 26 -- .../repository/DrugKeywordRepository.java | 8 - .../controller/DrugTempController.java | 35 -- .../temp/support/mapper/KeywordMapper.java | 42 -- .../support/mapper/TempDrugRawDataMapper.java | 41 -- src/main/resources/application.yml | 13 +- 230 files changed, 5530 insertions(+), 4596 deletions(-) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/common/configuration/AllJobConfig.java rename src/main/java/com/likelion/backendplus4/yakplus/{drug/infrastructure/api/config => common/configuration}/ApiRestTemplateConfig.java (65%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/common/configuration/BatchExecutorConfig.java rename src/main/java/com/likelion/backendplus4/yakplus/{drug/infrastructure/embedding/config => common/configuration}/OpenaiConfig.java (71%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/common/configuration/SwaggerConfig.java rename src/main/java/com/likelion/backendplus4/yakplus/common/{util/log => logging/trace}/LoggerWithTraceId.java (80%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/decorator/MdcTaskDecorator.java rename src/main/java/com/likelion/backendplus4/yakplus/common/{util/log => logging/util}/LogLevel.java (85%) rename src/main/java/com/likelion/backendplus4/yakplus/common/{util/log => logging/util}/LogMessage.java (92%) rename src/main/java/com/likelion/backendplus4/yakplus/common/{util/log => logging/util}/LogUtil.java (92%) rename src/main/java/com/likelion/backendplus4/yakplus/{ => common}/response/ApiResponse.java (97%) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/port/in/embed => embed/application/port/in}/DrugEmbedProcessorUseCase.java (95%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/EmbeddingRoutingUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/out/EmbeddingSwitchPort.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/embed => embed/application/service}/DrugEmbedProcessorService.java (78%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/EmbeddingRouter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/config/EmbedStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/persistence/repository => embed/infrastructure/persistence}/entity/DrugGptEmbedEntity.java (69%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/persistence/repository => embed/infrastructure/persistence}/entity/DrugKmBertEmbedEntity.java (69%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java => embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java} (65%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/EmbeddingEntity.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KmBertEmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java => embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java} (55%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java => embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java} (55%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{presentation/controller/embed => embed/presentation}/DrugEmbedController.java (74%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java rename src/main/java/com/likelion/backendplus4/yakplus/{ => drug}/index/application/service/DrugIndexer.java (56%) rename src/main/java/com/likelion/backendplus4/yakplus/{ => drug}/index/exception/IndexException.java (87%) rename src/main/java/com/likelion/backendplus4/yakplus/{ => drug}/index/exception/error/IndexErrorCode.java (85%) rename src/main/java/com/likelion/backendplus4/yakplus/{index/infrastructure/adapter/persistence => drug/index/infrastructure/persistence/elasticsearch/adapter}/ElasticsearchDrugAdapter.java (73%) rename src/main/java/com/likelion/backendplus4/yakplus/{temp/infrastructure/adapter/persistence => drug/index/infrastructure/persistence/elasticsearch}/document/DrugKeywordDocument.java (93%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java rename src/main/java/com/likelion/backendplus4/yakplus/{index/presentation/controller/DrugController.java => drug/index/presentation/controller/DrugIndexingController.java} (65%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java rename src/main/java/com/likelion/backendplus4/yakplus/{ => drug}/index/support/EmbeddingUtil/EmbedEntityBuilder.java (75%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java rename src/main/java/com/likelion/backendplus4/yakplus/{ => drug}/index/support/parser/SymptomTextParser.java (97%) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service => scraper/application}/exception/ScraperException.java (85%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service => scraper/application}/exception/error/ScraperErrorCode.java (90%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/port/in/scraper => scraper/application/port/in}/DrugScraperDetailUseCase.java (87%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/port/in/scraper/DrugScraperImageUsecase.java => scraper/application/port/in/DrugScraperImageUseCase.java} (85%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/port/in/scraper/DrugScraperTableCombineUsecase.java => scraper/application/port/in/DrugScraperTableCombineUseCase.java} (77%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/port/in/scraper/DrugScraperUsecase.java => scraper/application/port/in/DrugScraperUseCase.java} (89%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service => scraper/application}/port/out/BatchJobPort.java (97%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/scraper/detail => scraper/application/service}/DrugScraperDetailService.java (64%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/scraper => scraper/application/service}/DrugScraperService.java (59%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/scraper/image => scraper/application/service}/DrugScraperServiceImage.java (63%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{application/service/scraper/combine => scraper/application/service}/DrugScraperTableCombineService.java (61%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/api/exception/RestApiException.java (83%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/api/util/ApiRequestManager.java (90%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/api/util/UriCompBuilder.java (97%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/batch/exception/ParserBatchException.java (83%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/batch => scraper/infrastructure/batch/step}/image/reader/PageRangePartitioner.java (54%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/persistence/dto/DrugImageRequest.java (66%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/embedding/model => scraper/infrastructure/persistence/dto}/EmbeddingRequestText.java (64%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/persistence/repository/entity/DrugDetailEntity.java (74%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java => scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java} (82%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{ => scraper}/infrastructure/persistence/repository/entity/DrugRawDataEntity.java (93%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java rename src/main/java/com/likelion/backendplus4/yakplus/{index/infrastructure/adapter/persistence/DrugMapper.java => drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java} (88%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{presentation/controller/scraper => scraper/presentation/controller}/DrugScraperController.java (64%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{presentation/controller/scraper => scraper/presentation/controller}/combine/DrugScraperTableCombineController.java (70%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{presentation/controller/scraper => scraper/presentation/controller}/details/DrugScraperDetailController.java (73%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java rename src/main/java/com/likelion/backendplus4/yakplus/drug/{presentation/controller/scraper => scraper/presentation/controller}/image/DrugScraperImageController.java (70%) rename src/main/java/com/likelion/backendplus4/yakplus/drug/{infrastructure/batch/adapter => support}/JobManager.java (84%) delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/exception/DictionaryException.java (86%) rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/exception/error/DictionaryErrorCode.java (91%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java (52%) rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java (73%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java (89%) rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/infrastructure/persistence/repository/entity/SymptomDictionary.java (85%) rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/infrastructure/support/mapper/DictionaryMapper.java (50%) rename src/main/java/com/likelion/backendplus4/yakplus/{dictionary => symptomdictionary}/presentation/controller/DictionaryController.java (64%) create mode 100644 src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java delete mode 100644 src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java diff --git a/build.gradle b/build.gradle index 8d88829..7f28cb8 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,9 @@ dependencies { // Spring-Batch implementation 'org.springframework.boot:spring-boot-starter-batch' + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + // build.gradle if (project.hasProperty('env') && project.env == 'test') { dependencies { diff --git a/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java b/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java index 0e8d32a..46dc920 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java @@ -1,14 +1,11 @@ package com.likelion.backendplus4.yakplus; -import com.likelion.backendplus4.yakplus.common.configuration.LogbackConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class YakplusApplication { - public static void main(String[] args) { - LogbackConfig logbackConfig = new LogbackConfig(); - logbackConfig.configure(); - SpringApplication.run(YakplusApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(YakplusApplication.class, args); + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/AllJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/AllJobConfig.java new file mode 100644 index 0000000..2857796 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/AllJobConfig.java @@ -0,0 +1,60 @@ +package com.likelion.backendplus4.yakplus.common.configuration; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 공통 배치 Job 설정을 위한 클래스입니다. + *

+ * 이 클래스는 의약품 정보를 수집하는 배치 Job을 정의합니다. + *

+ * @field jobName Job 이름 + * @since 2025-05-02 + */ +@Configuration +@RequiredArgsConstructor +public class AllJobConfig { + + private final String jobName = "drugScrapJob"; + + /** + * 의약품 정보를 수집하는 Batch Job 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param totalPageCheckStep 전체 페이지 수 확인 Step + * @param drugDetailStep 상세 정보 수집 Step + * @return 구성된 Job 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Job drugScrapJob(JobRepository jobRepository, + Step totalPageCheckStep, + Step drugDetailStep, + Step imageTotalPageCheckStep, + Step imageMasterStep, + Step switchModelStepToOpenAi, + Step openAiEmbedStep, + Step switchModelStepToKmBert, + Step kmBertEmbedStep, + Step switchModelStepToKrSBert, + Step krSBertEmbedStep) { + return new JobBuilder(jobName, jobRepository) + .start(totalPageCheckStep) + .next(drugDetailStep) + .next(imageTotalPageCheckStep) + .next(imageMasterStep) + .next(switchModelStepToOpenAi) + .next(openAiEmbedStep) + .next(switchModelStepToKmBert) + .next(kmBertEmbedStep) + .next(switchModelStepToKrSBert) + .next(krSBertEmbedStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/config/ApiRestTemplateConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/ApiRestTemplateConfig.java similarity index 65% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/config/ApiRestTemplateConfig.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/configuration/ApiRestTemplateConfig.java index 4f4a162..3bb672b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/config/ApiRestTemplateConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/ApiRestTemplateConfig.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.config; +package com.likelion.backendplus4.yakplus.common.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -7,11 +7,17 @@ /** * Api 요청을 보내기 위한 RestTemplate 빈 생성 * - * @since 2025-04-15 - * @author 함예정 */ @Configuration public class ApiRestTemplateConfig { + + /** + * RestTemplate 빈을 생성합니다. + * + * @return RestTemplate + * @since 2025-04-15 + * @author 함예정 + */ @Bean public RestTemplate restTemplate() { return new RestTemplate(); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/BatchExecutorConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/BatchExecutorConfig.java new file mode 100644 index 0000000..0fc184b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/BatchExecutorConfig.java @@ -0,0 +1,58 @@ +package com.likelion.backendplus4.yakplus.common.configuration; + +import com.likelion.backendplus4.yakplus.common.logging.trace.decorator.MdcTaskDecorator; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 + */ +@Configuration +@RequiredArgsConstructor +public class BatchExecutorConfig { + private final MdcTaskDecorator mdcTaskDecorator; + private final String normalExecutorName = "normalExecutor"; + private final String singleItemExecutorName = "singleItemExecutor"; + + /** + * 일반적인 작업처리를 위한 ThreadPool 기반 TaskExecutor 설정 + * + * @return TaskExecutor 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean(normalExecutorName) + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(10); + executor.setTaskDecorator(mdcTaskDecorator); + executor.setThreadNamePrefix("normalExecutor-"); + executor.initialize(); + return executor; + } + + /** + * 단일 아이템 단위로 처리하는 작업을 위한 + * ThreadPool 기반 TaskExecutor 설정 + * + * @return TaskExecutor 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean(singleItemExecutorName) + public TaskExecutor taskExecutorMoreThreads() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(20); + executor.setTaskDecorator(mdcTaskDecorator); + executor.setThreadNamePrefix("singleItemExecutor-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/LogbackConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/LogbackConfig.java index 5aaa270..71d73e3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/LogbackConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/LogbackConfig.java @@ -11,6 +11,7 @@ import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import java.nio.file.Files; @@ -25,11 +26,16 @@ */ @Configuration public class LogbackConfig { - private static final String LOG_DIRECTORY = "logs"; - private static final String LOG_FILE_NAME = "like-lion.log"; - private static final String LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n"; - private static final int MAX_HISTORY = 30; - private static final String TOTAL_SIZE_CAP = "1GB"; + @Value("${log.rolling.directory}") + private String LOG_DIRECTORY; + @Value("${log.rolling.file-name}") + private String LOG_FILE_NAME; + @Value("${log.rolling.pattern}") + private String LOG_PATTERN; + @Value("${log.rolling.max-history}") + private int MAX_HISTORY; + @Value("${log.rolling.total-size-cap}") + private String TOTAL_SIZE_CAP; /** * 로깅 설정을 초기화하는 메서드 diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/config/OpenaiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/OpenaiConfig.java similarity index 71% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/config/OpenaiConfig.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/configuration/OpenaiConfig.java index 66ec57d..50e3760 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/config/OpenaiConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/OpenaiConfig.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.config; +package com.likelion.backendplus4.yakplus.common.configuration; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.beans.factory.annotation.Value; @@ -12,7 +12,6 @@ public class OpenaiConfig { @Bean public OpenAiApi openaiApi() { - OpenAiApi openaiApiKey = new OpenAiApi(apiKey); - return openaiApiKey; + return new OpenAiApi(apiKey); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/SwaggerConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/SwaggerConfig.java new file mode 100644 index 0000000..59650ad --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/SwaggerConfig.java @@ -0,0 +1,21 @@ +package com.likelion.backendplus4.yakplus.common.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("YakPlus API") + .description("YakPlus 프로젝트의 API 문서입니다.") + .version("1.0.0")); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/WebConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/WebConfig.java index e2d1ff8..74b3b50 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/WebConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/WebConfig.java @@ -2,7 +2,7 @@ import com.likelion.backendplus4.yakplus.common.interceptor.LogInterceptor; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/handler/GlobalExceptionHandler.java index ea70228..a82a982 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/handler/GlobalExceptionHandler.java @@ -1,137 +1,173 @@ package com.likelion.backendplus4.yakplus.common.exception.handler; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; -import com.likelion.backendplus4.yakplus.response.ApiResponse; -import lombok.extern.slf4j.Slf4j; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import java.util.stream.Collectors; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.validation.BindException; -import java.util.stream.Collectors; /** - * 전역 예외 처리 클래스 컨트롤러에서 발생한 예외를 공통적으로 처리한다. + * 전역 예외 처리 클래스 + * 컨트롤러에서 발생한 예외를 공통적으로 처리한다. * - * @modified 2025-04-18 + * @modified 2025-05-03 * @since 2025-04-16 */ -@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { - /** - * 공통 에러 응답 생성 메서드 - * - * 예외 로깅 후 ApiResponse.error를 통해 표준화된 에러 응답을 생성한다. - * - * @param status HTTP 상태 코드 - * @param errorCode 에러 코드 문자열 - * @param message 에러 메시지 - * @param ex 발생한 예외 객체 - * @return ResponseEntity> 형태의 에러 응답 - * @author 박찬병 - * @modified 2025-04-18 박찬병 - * @since 2025-04-18 - */ - private ResponseEntity> buildErrorResponse( - HttpStatus status, String errorCode, String message, Throwable ex) { - log.error("{}: {}", ex.getClass().getSimpleName(), ex.getMessage(), ex); - return ApiResponse.error(status, errorCode, message); - } - + // 에러 코드 상수 정의 (정수형 코드 사용) + private static final int ILLEGAL_ARGUMENT_CODE = 300000; + private static final int METHOD_ARGUMENT_NOT_VALID_CODE = 300001; + private static final int BIND_EXCEPTION_CODE = 300002; + private static final int INTERNAL_SERVER_ERROR_CODE = 500000; /** - * CustomException 처리 ErrorCode 인터페이스 기반으로 확장 가능한 방식으로 처리한다. + * CustomException 처리 + * ErrorCode 인터페이스 기반으로 확장 가능한 방식으로 처리한다. * * @param ex CustomException 객체 * @return 에러 응답 * @author 정안식 - * @modified 2025-04-18 박찬병 + * @modified 2025-05-03 박찬병 * @since 2025-04-16 */ @ExceptionHandler(CustomException.class) public ResponseEntity> handleCustomException(CustomException ex) { ErrorCode errorCode = ex.getErrorCode(); return buildErrorResponse( - errorCode.httpStatus(), - String.valueOf(errorCode.codeNumber()), - errorCode.message(), - ex + errorCode.httpStatus(), + errorCode.codeNumber(), + errorCode.message(), + ex ); } /** - * IllegalArgumentException 처리 잘못된 파라미터에 대한 예외 응답 처리 + * IllegalArgumentException 처리 + * 잘못된 파라미터에 대한 예외 응답 처리 * * @param ex 예외 객체 * @return 에러 응답 * @author 정안식 - * @modified 2025-04-18 박찬병 + * @modified 2025-05-03 박찬병 * @since 2025-04-16 */ @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { - return buildErrorResponse(HttpStatus.BAD_REQUEST, "300000", ex.getMessage(), ex); + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + ILLEGAL_ARGUMENT_CODE, + ex.getMessage(), + ex + ); } /** - * MethodArgumentNotValidException 처리 유효성 검사 실패에 대한 응답 처리 + * MethodArgumentNotValidException 처리 + * 유효성 검사 실패에 대한 응답 처리 * * @param ex 예외 객체 * @return 에러 응답 * @author 정안식 - * @modified 2025-04-18 박찬병 + * @modified 2025-05-03 박찬병 * @since 2025-04-16 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { String errorMessage = getErrorMessage(ex); - return buildErrorResponse(HttpStatus.BAD_REQUEST, "300001", errorMessage, ex); + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + METHOD_ARGUMENT_NOT_VALID_CODE, + errorMessage, + ex + ); } /** - * BindException 처리 - GET 요청 파라미터나 폼 바인딩 유효성 실패 시 처리 + * BindException 처리 + * GET 요청 파라미터나 폼 바인딩 유효성 실패 시 처리 * * @param ex BindException 오류 * @return 에러 응답 * @author 박찬병 - * @modified 2025-04-18 박찬병 + * @modified 2025-05-03 박찬병 * @since 2025-04-17 */ @ExceptionHandler(BindException.class) public ResponseEntity> handleBindException(BindException ex) { String errorMessage = getErrorMessage(ex); - return buildErrorResponse(HttpStatus.BAD_REQUEST, "300004", errorMessage, ex); + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + BIND_EXCEPTION_CODE, + errorMessage, + ex + ); } /** - * BindingResult 분석 후 필드별 오류 메시지 조합 + * 기타 모든 예외 처리 + * 정의되지 않은 예외는 내부 서버 오류로 응답 * + * @param ex 예외 객체 * @return 에러 응답 - * @author 박찬병 - * @modified 2025-04-18 박찬병 + * @author 정안식 + * @modified 2025-05-03 박찬병 * @since 2025-04-16 */ - private static String getErrorMessage(BindException ex) { - return ex.getBindingResult().getFieldErrors().stream() - .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) - .collect(Collectors.joining(", ")); + @ExceptionHandler(Exception.class) + public ResponseEntity> handleAllExceptions(Exception ex) { + return buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + INTERNAL_SERVER_ERROR_CODE, + "내부 서버 오류", + ex + ); } /** - * 기타 모든 예외 처리 정의되지 않은 예외는 내부 서버 오류로 응답 + * 공통 에러 응답 생성 메서드 + * 예외 로깅 후 ApiResponse.error를 통해 표준화된 에러 응답을 생성한다. * - * @param ex 예외 객체 - * @return 에러 응답 - * @author 정안식 - * @modified 2025-04-18 박찬병 + * @param status HTTP 상태 코드 + * @param errorCode 에러 코드 (정수형) + * @param message 에러 메시지 + * @param ex 발생한 예외 객체 + * @return ResponseEntity> 형태의 에러 응답 + * @author 박찬병 + * @modified 2025-05-03 박찬병 + * @since 2025-04-18 + */ + private ResponseEntity> buildErrorResponse( + HttpStatus status, + int errorCode, + String message, + Throwable ex + ) { + log(LogLevel.ERROR, ex.getClass().getSimpleName() + ": " + ex.getMessage(), ex); + return ApiResponse.error(status, String.valueOf(errorCode), message); + } + + /** + * BindingResult 분석 후 필드별 오류 메시지 조합 + * + * @param ex BindException 또는 MethodArgumentNotValidException 객체 + * @return 필드명과 메시지를 콤마로 연결한 오류 문자열 + * @author 박찬병 + * @modified 2025-05-03 박찬병 * @since 2025-04-16 */ - @ExceptionHandler(Exception.class) - public ResponseEntity> handleAllExceptions(Exception ex) { - return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "500000", "내부 서버 오류", ex); + private static String getErrorMessage(BindException ex) { + return ex.getBindingResult().getFieldErrors().stream() + .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) + .collect(Collectors.joining(", ")); } -} +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/interceptor/LogInterceptor.java b/src/main/java/com/likelion/backendplus4/yakplus/common/interceptor/LogInterceptor.java index 34592ab..2042df4 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/interceptor/LogInterceptor.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/interceptor/LogInterceptor.java @@ -1,6 +1,6 @@ package com.likelion.backendplus4.yakplus.common.interceptor; -import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; +import com.likelion.backendplus4.yakplus.common.logging.util.LogMessage; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; @@ -9,7 +9,7 @@ import java.util.UUID; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; /** * 로깅을 위한 인터셉터 클래스 diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/LoggerWithTraceId.java similarity index 80% rename from src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/LoggerWithTraceId.java index 4c8b503..21f19e7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/LoggerWithTraceId.java @@ -1,12 +1,10 @@ -package com.likelion.backendplus4.yakplus.common.util.log; +package com.likelion.backendplus4.yakplus.common.logging.trace; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import java.util.UUID; - /** * TraceId를 포함한 로거 클래스 * @@ -84,24 +82,6 @@ private static String makeTraceId() { return traceId; } - /** - * TraceId를 검증하는 메서드 - * - * @param traceId String 검증할 TraceId - * @throws IllegalStateException 유효하지 않은 TraceId - * @author 정안식 - * @modified 2025-04-18 - * @since 2025-04-16 - */ - private static void validateTraceId(String traceId) { - if (traceId == null) { - throw new IllegalStateException("TraceId가 null입니다. MDC에 traceId가 설정되어 있는지 확인하세요."); - } - if (traceId.trim().isEmpty()) { - throw new IllegalStateException("TraceId가 빈 문자열입니다. 유효한 traceId를 설정해주세요."); - } - } - /** * 호출한 클래스의 이름을 가져오는 메서드 * diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/decorator/MdcTaskDecorator.java b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/decorator/MdcTaskDecorator.java new file mode 100644 index 0000000..ae83e2d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/trace/decorator/MdcTaskDecorator.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.common.logging.trace.decorator; + +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 스레드 풀에서 실행되는 Task에 MDC(Context Map)를 전파하기 위한 TaskDecorator 구현체 + * MDC 정보를 부모 스레드에서 자식 스레드로 복사하여 로그 추적 정보를 유지하도록 한다. + */ +@Component +public class MdcTaskDecorator implements TaskDecorator { + + /** + * Runnable 실행 시 부모 스레드의 MDC(Context Map)를 자식 스레드로 복사하여 설정한다. + * 실행 후 MDC를 반드시 clear하여 메모리 누수를 방지한다. + * + * @param runnable 실행할 원본 Runnable + * @return MDC context를 설정한 새로운 Runnable + */ + @Override + public Runnable decorate(Runnable runnable) { + Map contextMap = MDC.getCopyOfContextMap(); + return () -> { + if (contextMap != null) { + MDC.setContextMap(contextMap); + } + try { + runnable.run(); + } finally { + MDC.clear(); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogLevel.java b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogLevel.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogLevel.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogLevel.java index fe2084f..cf1c67d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogLevel.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogLevel.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.common.util.log; +package com.likelion.backendplus4.yakplus.common.logging.util; import org.slf4j.Logger; @@ -10,12 +10,12 @@ */ public enum LogLevel { /** - * INFO 레벨 로그 + * TRACE 레벨 로그 */ - INFO { + TRACE { @Override public void log(Logger logger, String traceId, String message) { - logMessage(logger::info, traceId, message); + logMessage(logger::trace, traceId, message); } }, /** @@ -27,6 +27,24 @@ public void log(Logger logger, String traceId, String message) { logMessage(logger::debug, traceId, message); } }, + /** + * INFO 레벨 로그 + */ + INFO { + @Override + public void log(Logger logger, String traceId, String message) { + logMessage(logger::info, traceId, message); + } + }, + /** + * WARN 레벨 로그 + */ + WARN { + @Override + public void log(Logger logger, String traceId, String message) { + logMessage(logger::warn, traceId, message); + } + }, /** * ERROR 레벨 로그 */ diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogMessage.java b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogMessage.java similarity index 92% rename from src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogMessage.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogMessage.java index ac1b7a4..b3ca5a7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogMessage.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogMessage.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.common.util.log; +package com.likelion.backendplus4.yakplus.common.logging.util; import lombok.Getter; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogUtil.java b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogUtil.java similarity index 92% rename from src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogUtil.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogUtil.java index 92c6880..f4c95d5 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogUtil.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/logging/util/LogUtil.java @@ -1,4 +1,6 @@ -package com.likelion.backendplus4.yakplus.common.util.log; +package com.likelion.backendplus4.yakplus.common.logging.util; + +import com.likelion.backendplus4.yakplus.common.logging.trace.LoggerWithTraceId; /** * 로깅 유틸리티 클래스 diff --git a/src/main/java/com/likelion/backendplus4/yakplus/response/ApiResponse.java b/src/main/java/com/likelion/backendplus4/yakplus/common/response/ApiResponse.java similarity index 97% rename from src/main/java/com/likelion/backendplus4/yakplus/response/ApiResponse.java rename to src/main/java/com/likelion/backendplus4/yakplus/common/response/ApiResponse.java index 7f7102a..7bab144 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/response/ApiResponse.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/response/ApiResponse.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.response; +package com.likelion.backendplus4.yakplus.common.response; import com.fasterxml.jackson.annotation.JsonInclude; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java deleted file mode 100644 index fc2feac..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/in/DictionaryUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.application.port.in; - -public interface DictionaryUseCase { - - void setDictionary(); - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java deleted file mode 100644 index 4694071..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.application.port.out; - -import java.util.List; - -public interface SymptomDictionaryElsRepositoryPort { - - void setDictionary(List symptoms); - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java deleted file mode 100644 index 634cd56..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.application.port.out; - -import java.util.List; - -public interface SymptomDictionaryJpaRepositoryPort { - - void setDictionary(List symptoms); - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java deleted file mode 100644 index 08bf9cd..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/port/out/SymptomDictionaryLoaderPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.application.port.out; - -import java.util.List; - -public interface SymptomDictionaryLoaderPort { - - List loadDictionary(); - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java deleted file mode 100644 index 96e538d..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/application/service/DictionaryService.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.application.service; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.dictionary.application.port.in.DictionaryUseCase; -import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryElsRepositoryPort; -import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out.JsonSymptomDictionaryLoader; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -/** - * 애플리케이션 계층에서 증상 사전 관리 기능을 제공하는 서비스 클래스입니다. - * JsonSymptomDictionaryLoader를 통해 로컬 JSON 파일에서 증상 리스트를 로드하고, - * 이를 JPA 및 Elasticsearch에 차례로 저장합니다. - * - * @since 2025-05-01 - * @modified 2025-05-01 - */ -@Service -@RequiredArgsConstructor -public class DictionaryService implements DictionaryUseCase { - - private final JsonSymptomDictionaryLoader jsonSymptomDictionaryLoader; - private final SymptomDictionaryJpaRepositoryPort dictionaryRepositoryPort; - private final SymptomDictionaryElsRepositoryPort dictionaryElsRepositoryPort; - - /** - * 로컬 JSON 파일에서 증상 리스트를 읽어들여 - * DB(JPA) 및 Elasticsearch에 차례로 저장합니다. - */ - @Override - public void setDictionary() { - log("DictionaryService.setDictionary() 호출 시작"); - - // 1) JSON 파일에서 증상 리스트 로드 - List symptomList = jsonSymptomDictionaryLoader.loadDictionary(); - log(" loadDictionary() 완료, 증상 수: " + symptomList.size()); - - // 2) JPA 저장 - dictionaryRepositoryPort.setDictionary(symptomList); - log(" SymptomDictionaryJpaAdapter.setDictionary() 완료"); - - // 3) Elasticsearch 저장 - dictionaryElsRepositoryPort.setDictionary(symptomList); - log(" SymptomDictionaryElsAdapter.setDictionary() 완료"); - - log("DictionaryService.setDictionary() 호출 종료"); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java deleted file mode 100644 index 2b4dbcb..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryLoaderPort; -import com.likelion.backendplus4.yakplus.dictionary.exception.DictionaryException; -import com.likelion.backendplus4.yakplus.dictionary.exception.error.DictionaryErrorCode; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.core.io.ClassPathResource; -import org.springframework.stereotype.Component; - -/** - * classpath에 위치한 JSON 파일로부터 증상 사전 데이터를 로드하는 클래스입니다. - * SymptomDictionaryLoaderPort를 구현하여 JSON 파싱 로직을 캡슐화합니다. - * - * @since 2025-04-30 - * @modified 2025-05-01 - */ -@RequiredArgsConstructor -@Component -public class JsonSymptomDictionaryLoader implements SymptomDictionaryLoaderPort { - - private final ObjectMapper objectMapper; - private static final String DICT_PATH = "unique_symptoms.json"; - - /** - * ClassPathResource를 통해 지정된 JSON 파일을 읽고, - * List 형태로 변환하여 반환합니다. - * - * @return JSON에 정의된 증상 문자열 리스트 - * @throws DictionaryException 파일 형식 오류 또는 파싱/IO 실패 시 발생 - * @since 2025-04-30 - * @author 박찬병 - * @modified 2025-05-01 - * */ - @Override - public List loadDictionary() { - log("JsonSymptomDictionaryLoader.loadDictionary() 호출, 경로: " + DICT_PATH); - - // 1) 확장자 검증 - if (!DICT_PATH.toLowerCase().endsWith(".json")) { - log(LogLevel.ERROR, "loadDictionary() 파일 형식 오류: JSON(.json) 파일만 허용됩니다."); - throw new DictionaryException(DictionaryErrorCode.INVALID_FILE_TYPE); - } - - ClassPathResource resource = new ClassPathResource(DICT_PATH); - try (InputStream in = resource.getInputStream()) { - // 2) JSON 읽기 및 파싱 - List symptoms = objectMapper.readValue(in, new TypeReference>() {}); - log("loadDictionary() 완료, 로드된 증상 수: " + symptoms.size()); - return symptoms; - } catch (IOException e) { - log(LogLevel.ERROR, "loadDictionary() 증상 사전 로드 실패", e); - throw new DictionaryException(DictionaryErrorCode.DICTIONARY_LOAD_FAILURE); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java deleted file mode 100644 index c06ac09..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository; - -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; - -public interface SymptomDictionaryDocRepository extends ElasticsearchRepository { - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java deleted file mode 100644 index 877dea8..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository; - -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity.SymptomDictionary; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SymptomDictionaryRepository extends JpaRepository { - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java index ba1ec85..2417189 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java @@ -1,11 +1,11 @@ package com.likelion.backendplus4.yakplus.drug.domain.model; import java.time.LocalDate; -import java.util.List; -import java.util.Map; +import java.util.*; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import com.likelion.backendplus4.yakplus.drug.index.support.parser.SymptomTextParser; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -33,4 +33,40 @@ public class Drug { private LocalDate cancelDate; private String cancelName; private boolean isHerbal; + + /** + * 효능 설명 리스트(`efficacy`)를 자동완성에 적합한 토큰 리스트로 변환합니다. + * 내부적으로 효능 내용을 하나의 문자열로 평탄화한 후, + * 검색 자동완성에 적합한 형태로 토큰화합니다. + * + * @return 자동완성용 효능 토큰 리스트 (비어있을 경우 빈 리스트 반환) + * @author 박찬병 + * @since 2025-05-03 + */ + public List generateEfficacySuggestions() { + if (efficacy == null || efficacy.isEmpty()) return List.of(); + String flat = SymptomTextParser.flattenLines(efficacy); + return SymptomTextParser.tokenizeForSuggestion(flat); + } + + /** + * 의약품 성분 정보(`materialInfo`)에서 성분명을 추출하여 + * 자동완성에 사용할 성분명 리스트를 생성합니다. + * + * null 값이나 이름이 없는 성분은 자동으로 제외되며, + * 중복 제거는 호출 측에서 수행합니다. + * + * @return 자동완성용 성분명 리스트 (비어있을 경우 빈 리스트 반환) + * @author 박찬병 + * @since 2025-05-03 + */ + public List generateIngredientSuggestions() { + return Optional.ofNullable(materialInfo) + .orElse(Collections.emptyList()) + .stream() + .map(Material::getName) + .filter(Objects::nonNull) + .toList(); + } + } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/DrugEmbedProcessorUseCase.java similarity index 95% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/DrugEmbedProcessorUseCase.java index bfe9d13..e42366a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/embed/DrugEmbedProcessorUseCase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/DrugEmbedProcessorUseCase.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed; +package com.likelion.backendplus4.yakplus.drug.embed.application.port.in; /** * 의약품 효능 정보를 임베딩 처리를 위한 유스케이스 인터페이스입니다. diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/EmbeddingRoutingUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/EmbeddingRoutingUseCase.java new file mode 100644 index 0000000..03b2311 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/EmbeddingRoutingUseCase.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.drug.embed.application.port.in; + +public interface EmbeddingRoutingUseCase { + + /** + * 지정된 adapter Bean 이름으로 Embedding adapter를 전환합니다. + * + * @param adapterBeanName 전환할 adapter Bean 이름 + * @author 정안식 + * @since 2025-05-02 + */ + void switchEmbedding(String adapterBeanName); + + /** + * 현재 활성화된 Embedding adapter Bean 이름을 반환합니다. + * + * @return 현재 adapter Bean 이름 + * @author 정안식 + * @since 2025-05-02 + */ + String getAdapterBeanName(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/out/EmbeddingSwitchPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/out/EmbeddingSwitchPort.java new file mode 100644 index 0000000..41fc97c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/out/EmbeddingSwitchPort.java @@ -0,0 +1,23 @@ +package com.likelion.backendplus4.yakplus.drug.embed.application.port.out; + +public interface EmbeddingSwitchPort { + + /** + * 지정된 Bean 이름에 해당하는 어댑터로 전환합니다. + * + * @param adapterBeanName 전환할 어댑터 Bean 이름 + * @throws IllegalArgumentException 지원되지 않는 어댑터 이름인 경우 + * @author 정안식 + * @since 2025-05-02 + */ + void switchTo(String adapterBeanName); + + /** + * 현재 활성화된 어댑터 Bean 이름을 반환합니다. + * + * @return 활성화된 어댑터 Bean 이름 + * @author 정안식 + * @since 2025-05-02 + */ + String getAdapterBeanName(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/DrugEmbedProcessorService.java similarity index 78% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/DrugEmbedProcessorService.java index 9b44a16..ff15330 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/embed/DrugEmbedProcessorService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/DrugEmbedProcessorService.java @@ -1,10 +1,10 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.embed; +package com.likelion.backendplus4.yakplus.drug.embed.application.service; import org.springframework.stereotype.Service; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; -import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.in.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.out.EmbeddingSwitchPort; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/EmbeddingRouter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/EmbeddingRouter.java new file mode 100644 index 0000000..527987f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/EmbeddingRouter.java @@ -0,0 +1,42 @@ +package com.likelion.backendplus4.yakplus.drug.embed.application.service; + +import com.likelion.backendplus4.yakplus.drug.embed.application.port.in.EmbeddingRoutingUseCase; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.out.EmbeddingSwitchPort; +import org.springframework.stereotype.Service; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Service +public class EmbeddingRouter implements EmbeddingRoutingUseCase { + private final EmbeddingSwitchPort switchPort; + + public EmbeddingRouter(EmbeddingSwitchPort switchPort) { + this.switchPort = switchPort; + } + + /** + * 지정된 adapter Bean 이름으로 Embedding adapter를 전환합니다. + * + * @param adapterBeanName 전환할 adapter Bean 이름 + * @author 정안식 + * @since 2025-05-02 + */ + @Override + public void switchEmbedding(String adapterBeanName) { + log("임베딩 스위치 요청 수신 - 어댑터명: " + adapterBeanName); + switchPort.switchTo(adapterBeanName); + } + + /** + * 현재 활성화된 Embedding adapter Bean 이름을 반환합니다. + * + * @return 현재 adapter Bean 이름 + * @author 정안식 + * @since 2025-05-02 + */ + @Override + public String getAdapterBeanName() { + log("현재 선택된 어댑터 빈 이름 요청"); + return switchPort.getAdapterBeanName(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java new file mode 100644 index 0000000..2cd8797 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java @@ -0,0 +1,58 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.job.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 임베딩 처리 작업을 구성하는 Spring Batch 설정 클래스입니다. + *

+ * 모델 스위칭과 임베딩 작업을 순차적으로 구성하여 실행합니다. + * OpenAI → KM-BERT → KR-SBERT 순서로 각각 임베딩을 수행합니다. + * + * @since 2025-05-02 + */ +@Configuration +public class EmbedJobConfig { + + /** + * 여러 임베딩 모델을 순차적으로 실행하는 Job을 정의합니다. + * 각 단계는 다음을 포함합니다: + *

    + *
  • 모델 스위칭 (OpenAI, KM-BERT, KR-SBERT)
  • + *
  • 각 모델에 대한 임베딩 실행
  • + *
+ * + * @param jobRepository Spring Batch JobRepository + * @param switchModelStepToOpenAi OpenAI 모델로 스위칭하는 Step + * @param openAiEmbedStep OpenAI 모델을 이용한 임베딩 Step + * @param switchModelStepToKmBert KM-BERT 모델로 스위칭하는 Step + * @param kmBertEmbedStep KM-BERT 모델을 이용한 임베딩 Step + * @param switchModelStepToKrSBert KR-SBERT 모델로 스위칭하는 Step + * @param krSBertEmbedStep KR-SBERT 모델을 이용한 임베딩 Step + * @return 구성된 Job 인스턴스 + * + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Job embedJob(JobRepository jobRepository, + Step switchModelStepToOpenAi, + Step openAiEmbedStep, + Step switchModelStepToKmBert, + Step kmBertEmbedStep, + Step switchModelStepToKrSBert, + Step krSBertEmbedStep) { + return new JobBuilder("embedJob", jobRepository) + .start(switchModelStepToOpenAi) + .next(openAiEmbedStep) + .next(switchModelStepToKmBert) + .next(kmBertEmbedStep) + .next(switchModelStepToKrSBert) + .next(krSBertEmbedStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/config/EmbedStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/config/EmbedStepConfig.java new file mode 100644 index 0000000..2a4bd02 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/config/EmbedStepConfig.java @@ -0,0 +1,358 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.reader.DrugIdRangePartitioner; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.out.EmbeddingSwitchPort; +import jakarta.persistence.EntityManagerFactory; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.Map; + +/** + * 임베딩 배치 작업에서 사용되는 Step들을 정의하는 Spring Batch 설정 클래스입니다. + *

+ * 각 임베딩 모델(OpenAI, KM-BERT, KR-SBERT)에 대해 다음과 같은 Step 구조를 가집니다: + *

    + *
  • 모델 스위칭 Step(Tasklet)
  • + *
  • 분산 처리를 위한 Master Step + Slave Step (partition + chunk 기반)
  • + *
+ *

+ * 또한, 각 Slave Step은 JPA Paging Reader와 Retry/Skip 정책이 적용된 chunk 기반 처리 구조입니다. + * Partitioning을 위해 Drug ID 범위 기반 파티셔너가 사용되며, + * 병렬 처리를 위해 TaskExecutor가 주입됩니다. + *

+ *

+ * batchExecutorName: 멀티스레드 처리를 위한 TaskExecutor 이름 + * openAiEmbeddingAdapterName: OpenAI 임베딩 어댑터 이름 + * kmBertEmbeddingAdapterName: KM-BERT 임베딩 어댑터 이름 + * krSBertEmbeddingAdapterName: KR-SBERT 임베딩 어댑터 이름 + * retryLimit: 재시도 횟수 + * retryException: 재시도할 예외 클래스 + * modelSwitchStepName: 모델 스위칭 Step 이름 + * openAiStepName: OpenAI 임베딩 Step 이름 + * kmBertStepName: KM-BERT 임베딩 Step 이름 + * krSBertStepName: KR-SBERT 임베딩 Step 이름 + * rawDataSelectQuery: 원본 데이터 조회 쿼리 + * + * @since 2025-05-02 + */ +@Configuration +public class EmbedStepConfig { + private final String batchExecutorName = "singleItemExecutor"; + + private final String openAiEmbeddingAdapterName = "openAiEmbeddingAdapter"; + private final String kmBertEmbeddingAdapterName = "kmBertEmbeddingAdapter"; + private final String krSBertEmbeddingAdapterName = "krSBertEmbeddingAdapter"; + + private final int retryLimit = 3; + private final Class retryException = Exception.class; + + private final String modelSwitchStepName = "switchModelStep"; + + private final String openAiStepName = "openAiEmbedStep"; + private final String kmBertStepName = "kmBertEmbedStep"; + private final String krSBertStepName = "krSBertEmbedStep"; + private final String rawDataSelectQuery = "SELECT d FROM DrugRawDataEntity d WHERE d.drugId BETWEEN :minId AND :maxId ORDER BY d.drugId"; + + + private final DrugIdRangePartitioner drugIdRangePartitioner; + private final TaskExecutor taskExecutor; + + public EmbedStepConfig(DrugIdRangePartitioner drugIdRangePartitioner, + @Qualifier(batchExecutorName) + TaskExecutor taskExecutor) { + this.drugIdRangePartitioner = drugIdRangePartitioner; + this.taskExecutor = taskExecutor; + } + + /** + * OpenAI 임베딩 모델로 전환하는 Tasklet 기반 Step입니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param tx: 트랜잭션 매니저 + * @param switchPort: EmbeddingSwitchPort 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step switchModelStepToOpenAi(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo(openAiEmbeddingAdapterName); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder(modelSwitchStepName + "ToOpenAi", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + /** + * KM-BERT 임베딩 모델로 전환하는 Tasklet 기반 Step입니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param tx: 트랜잭션 매니저 + * @param switchPort: EmbeddingSwitchPort 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step switchModelStepToKmBert(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo(kmBertEmbeddingAdapterName); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder(modelSwitchStepName + "ToKmBert", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + /** + * KR-SBERT 임베딩 모델로 전환하는 Tasklet 기반 Step입니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param tx: 트랜잭션 매니저 + * @param switchPort: EmbeddingSwitchPort 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step switchModelStepToKrSBert(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo(krSBertEmbeddingAdapterName); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder(modelSwitchStepName + "ToKrSbert", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + /** + * OpenAI 임베딩을 수행하는 Slave Step입니다. + * chunk 기반으로 엔티티를 읽고 임베딩 벡터로 변환하여 저장합니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param transactionManager: 트랜잭션 매니저 + * @param reader: JpaPagingItemReader 인스턴스 + * @param processor: ItemProcessor 인스턴스 + * @param writer: ItemWriter 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step openAiEmbedStepSlave( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder(openAiStepName + "Slave", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * OpenAI 임베딩을 병렬로 수행하는 Master Step입니다. + * 각 Slave Step을 파티셔닝하여 병렬로 처리합니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param openAiEmbedStepSlave: Slave Step 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step openAiEmbedStep( + JobRepository jobRepository, + Step openAiEmbedStepSlave) { + + return new StepBuilder(openAiStepName, jobRepository) + .partitioner(openAiEmbedStepSlave.getName(), drugIdRangePartitioner) + .step(openAiEmbedStepSlave) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + /** + * KM-BERT 임베딩을 수행하는 Slave Step입니다. + * chunk 기반으로 엔티티를 읽고 임베딩 벡터로 변환하여 저장합니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param transactionManager: 트랜잭션 매니저 + * @param reader: JpaPagingItemReader 인스턴스 + * @param processor: ItemProcessor 인스턴스 + * @param writer: ItemWriter 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step kmBertEmbedStepSlave( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder(kmBertStepName + "Slave", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * KM-BERT 임베딩을 병렬로 수행하는 Master Step입니다. + * 각 Slave Step을 파티셔닝하여 병렬로 처리합니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param kmBertEmbedStepSlave: Slave Step 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step kmBertEmbedStep( + JobRepository jobRepository, + Step kmBertEmbedStepSlave) { + + return new StepBuilder(kmBertStepName, jobRepository) + .partitioner(kmBertEmbedStepSlave.getName(), drugIdRangePartitioner) + .step(kmBertEmbedStepSlave) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + /** + * KR-SBERT 임베딩을 수행하는 Slave Step입니다. + * chunk 기반으로 엔티티를 읽고 임베딩 벡터로 변환하여 저장합니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param transactionManager: 트랜잭션 매니저 + * @param reader: JpaPagingItemReader 인스턴스 + * @param processor: ItemProcessor 인스턴스 + * @param writer: ItemWriter 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step krSBertEmbedStepSlave( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder(krSBertStepName + "Slave", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * KR-SBERT 임베딩을 병렬로 수행하는 Master Step입니다. + * 각 Slave Step을 파티셔닝하여 병렬로 처리합니다. + * + * @param jobRepository: JobRepository 인스턴스 + * @param krSBertEmbedStepSlave: Slave Step 인스턴스 + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step krSBertEmbedStep( + JobRepository jobRepository, + Step krSBertEmbedStepSlave) { + return new StepBuilder(krSBertStepName, jobRepository) + .partitioner(krSBertEmbedStepSlave.getName(), drugIdRangePartitioner) + .step(krSBertEmbedStepSlave) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + /** + * 파티션 내 DrugRawDataEntity를 읽기 위한 JPA 기반 ItemReader입니다. + * 각 파티션의 minId와 maxId를 기준으로 데이터를 읽어옵니다. + * + * @param minId: 파티션의 최소 Drug ID + * @param maxId: 파티션의 최대 Drug ID + * @param emf: EntityManagerFactory 인스턴스 + * @return JpaPagingItemReader 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + @StepScope + public JpaPagingItemReader embedItemReader( + @Value("#{stepExecutionContext['minId']}") Long minId, + @Value("#{stepExecutionContext['maxId']}") Long maxId, + EntityManagerFactory emf) { + LogUtil.log(Thread.currentThread().getName() + "Item Read: " + minId + "~" + maxId); + return new JpaPagingItemReaderBuilder() + .name("partitionedReader") + .entityManagerFactory(emf) + .queryString(rawDataSelectQuery) + .parameterValues(Map.of("minId", minId, "maxId", maxId)) + .pageSize(100) + .saveState(false) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java new file mode 100644 index 0000000..cdab2c4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto; + +import lombok.Builder; +import lombok.Getter; + +/** + * 의약품 임베딩 결과를 담는 DTO 클래스입니다. + * drugId는 대상 의약품의 식별자이며, vector는 모델로부터 생성된 임베딩 벡터입니다. + * + * @field drugId 의약품의 식별자 + * @field vector 모델로부터 생성된 임베딩 벡터 + * @since 2025-05-02 + */ +@Builder +@Getter +public class DrugVectorDto { + private Long drugId; + private float[] vector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java new file mode 100644 index 0000000..14fa209 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java @@ -0,0 +1,62 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.processor; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugFieldTypeMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +/** + * 의약품의 효능 정보를 기반으로 임베딩 벡터를 생성하는 Spring Batch ItemProcessor입니다. + *

+ * 입력으로 받은 DrugRawDataEntity에서 효능 정보를 추출 및 전처리하여, + * EmbeddingLoadingPort를 통해 임베딩 벡터를 생성한 후 DrugVectorDto로 변환합니다. + * + * @field embeddingPort EmbeddingPort 인터페이스를 통해 임베딩 벡터를 생성하는 포트 + * @since 2025-04-22 + */ +@Component +@RequiredArgsConstructor +public class EmbedProcessor implements ItemProcessor { + private final EmbeddingPort embeddingPort; + + /** + * 효능 정보를 추출하고, 해당 텍스트를 임베딩하여 DrugVectorDto로 반환합니다. + * + * @param item 임베딩 대상 의약품 엔티티 + * @return 임베딩 벡터를 포함한 DTO + * @author 이해창 + * @modified 2025-05-02: 함예정 + * - 스프링 배치 적용 + * @since 2025-04-22 + */ + @Override + public DrugVectorDto process(DrugRawDataEntity item) { + Long id = item.getDrugId(); + String embeddingText = getEmbedTextFromItem(item); + float[] embeddingVector = embeddingPort.getEmbedding(embeddingText); + return DrugVectorDto + .builder() + .drugId(id) + .vector(embeddingVector) + .build(); + } + + /** + * DrugRawDataEntity의 효능 정보를 추출하고 전처리하여 임베딩 입력 텍스트로 변환합니다. + * + * @param item 임베딩 대상 의약품 엔티티 + * @return String 전처리된 임베딩 입력 텍스트 + * @author 이해창 + * @modified 2025-05-02: 함예정 + * - 스프링 배치 적용 + * @since 2025-04-22 + */ + private String getEmbedTextFromItem(DrugRawDataEntity item) { + return DrugFieldTypeMapper.convertSingleStringForEfficacy( + DrugFieldTypeMapper.parseStringToList(item.getEfficacy()) + ); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java new file mode 100644 index 0000000..34232de --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java @@ -0,0 +1,59 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.reader; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugJpaRepository; + +/** + * 의약품 ID 범위를 기반으로 파티션을 분할하는 Partitioner 구현 클래스입니다. + *

+ * GovDrugJpaRepository를 통해 drugId의 최소/최대 값을 조회하고, + * 주어진 gridSize에 따라 각 파티션의 범위를 계산하여 ExecutionContext에 전달합니다. + * 각 Step은 이 ExecutionContext를 통해 자신의 ID 범위를 인식하고 독립적으로 데이터를 처리할 수 있습니다. + */ +@Component +public class DrugIdRangePartitioner implements Partitioner { + + private final DrugJpaRepository repository; + + public DrugIdRangePartitioner(DrugJpaRepository repository) { + this.repository = repository; + } + + /** + * 의약품 ID 범위를 기준으로 주어진 gridSize만큼 파티션을 나누어 ExecutionContext에 담아 반환합니다. + * + * @param gridSize 병렬 실행할 파티션 수 + * @return Map 파티션 이름(String)과 그에 해당하는 ExecutionContext의 매핑 + * @throws IllegalArgumentException gridSize가 0 이하인 경우 + * @throws IllegalStateException drugId의 최소/최대 값이 null인 경우 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public Map partition(int gridSize) { + Long minId = repository.findMinDrugId(); + Long maxId = repository.findMaxDrugId(); + + long targetSize = (maxId - minId) / gridSize + 1; + + Map result = new HashMap<>(); + long start = minId; + long end = start + targetSize - 1; + + for (int i = 0; i < gridSize; i++) { + ExecutionContext context = new ExecutionContext(); + context.putLong("minId", start); + context.putLong("maxId", Math.min(end, maxId)); + result.put("partition" + i, context); + start += targetSize; + end += targetSize; + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java new file mode 100644 index 0000000..b817a43 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.writer; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * 임베딩 벡터를 외부 저장소 또는 인프라에 저장하는 Spring Batch ItemWriter 구현 클래스입니다. + *

+ * EmbeddingLoadingPort를 통해 전달된 DrugVectorDto 리스트를 저장하고, + * 처리된 항목 수를 로깅합니다. 주로 벡터 저장 API 호출 또는 벡터 DB 연동 작업에 사용됩니다. + * + * @fields embeddingPort 임베딩 저장을 위한 포트 + * @fields count 처리된 항목 수 카운트를 위한 AtomicInteger + * @since 2025-05-02 + */ +@Component +@StepScope +@RequiredArgsConstructor +public class EmbedWriter implements ItemWriter { + private final EmbeddingPort embeddingPort; + private static final AtomicInteger count = new AtomicInteger(); + + /** + * 전달된 임베딩 결과 리스트를 저장소에 저장합니다. + * + * @param dto 임베딩된 벡터를 포함한 DTO 목록 (Chunk 단위) + * @since 2025-05-02 + * @author 함예정 + */ + @Override + public void write(Chunk dto) { + List items = new ArrayList<>(dto.getItems()); + embeddingPort.saveEmbedding(items); + log("임베딩 작업 - 쓰기 완료: " + count.addAndGet(items.size())); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugGptEmbedEntity.java similarity index 69% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugGptEmbedEntity.java index 23adb44..c1c0696 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugGptEmbedEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugGptEmbedEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -15,11 +15,16 @@ @NoArgsConstructor @AllArgsConstructor @Getter -public class DrugGptEmbedEntity { +public class DrugGptEmbedEntity implements EmbeddingEntity{ @Id @Column( name= "ITEM_SEQ") private Long drugId; @Column( name= "GPT_VECTOR", columnDefinition = "JSON") private String gptVector; + + @Override + public String getVector() { + return gptVector; + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKmBertEmbedEntity.java similarity index 69% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKmBertEmbedEntity.java index 169e5cd..7f365ea 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKmBertEmbedEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKmBertEmbedEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -15,11 +15,16 @@ @NoArgsConstructor @AllArgsConstructor @Getter -public class DrugKmBertEmbedEntity { +public class DrugKmBertEmbedEntity implements EmbeddingEntity { @Id @Column( name= "ITEM_SEQ") private Long drugId; @Column( name= "KM_BERT_VECTOR", columnDefinition = "JSON") private String kmBertVector; + + @Override + public String getVector() { + return kmBertVector; + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java similarity index 65% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java index 9aa4109..5e5a7a9 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugKrSbertEmbedEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -15,11 +15,16 @@ @NoArgsConstructor @AllArgsConstructor @Getter -public class DrugKrSbertEmbedEntity { +public class DrugKrSBertEmbedEntity implements EmbeddingEntity{ @Id @Column( name= "ITEM_SEQ") private Long drugId; @Column( name= "KR_SBERT_VECTOR", columnDefinition = "JSON") - private String krSbertVector; + private String krSBertVector; + + @Override + public String getVector() { + return krSBertVector; + } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/EmbeddingEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/EmbeddingEntity.java new file mode 100644 index 0000000..4f8a8b5 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/EmbeddingEntity.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity; + +/** + * 임베딩 데이터를 JSON 문자열 형태로 제공하는 공통 인터페이스입니다. + * + *

이 인터페이스를 구현하는 엔티티는 내부에 저장된 임베딩 벡터를 + * 문자열로 반환하는 메서드를 반드시 제공해야 합니다.

+ * + * @since 2025-05-03 + */ +public interface EmbeddingEntity { + /** + *

각 엔티티가 내부적으로 사용하는 컬럼명이 달라도 + * 이 메서드를 통해 항상 동일한 방식으로 벡터 정보를 가져올 수 있습니다.

+ * @return 임베딩 벡터 문자열 + */ + String getVector(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java new file mode 100644 index 0000000..2de4ad1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java @@ -0,0 +1,141 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.adapter; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.out.EmbeddingSwitchPort; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Component("embeddingRouterAdapter") +@Primary +public class EmbeddingRouterAdapter implements EmbeddingPort, EmbeddingSwitchPort { + @Value("${embed.switcher.default-adapter}") + private String DEFAULT_ADAPTER; + private final Map adapters; + private volatile EmbeddingPort embeddingPort; + private volatile String adapterBeanName; + + /** + * 모든 EmbeddingPort 구현체를 주입받아 어댑터 라우팅 맵을 초기화합니다. + * + * @param allAdapters 어댑터 빈 이름을 키로 하고 EmbeddingPort 구현체를 값으로 갖는 맵 + * @author 정안식 + * @since 2025-05-02 + */ + public EmbeddingRouterAdapter(Map allAdapters) { + this.adapters = allAdapters; + log("구현체 목록: " + adapters.keySet()); + } + + /** + * 컴포넌트 초기화 후 기본 어댑터로 전환합니다. + * + * @author 정안식 + * @since 2025-05-02 + */ + @PostConstruct + public void init() { + log("EmbeddingRouterAdapter 초기화 - 어댑터명: " + DEFAULT_ADAPTER); + switchTo(DEFAULT_ADAPTER); + } + + /** + * 주어진 페이지 정보(Pageable)에 따라 데이터베이스에서 원시 약품 데이터와 임베딩 데이터를 + * 조인하여 한 페이지 분량의 Drug 도메인 객체 목록을 조회합니다. + * + * @param pageable 조회할 페이지 번호와 크기, 정렬 정보 등을 포함하는 Pageable 객체 + * @return 지정된 페이지 범위에 해당하는 Drug 도메인 객체들의 리스트 + * @author 이해창 + * @since 2025-05-03 + */ + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + if (embeddingPort == null) { + log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); + throw new IllegalStateException("No adapter selected"); + } + return embeddingPort.loadEmbeddingsByPage(pageable); + } + + /** + * 현재 선택된 어댑터로부터 임베딩 벡터를 반환합니다. + * + * @param text 임베딩을 생성할 입력 문자열 + * @return 입력 문자열에 대한 임베딩 벡터 배열 + * @throws IllegalStateException 어댑터가 선택되지 않은 경우 + * @author 정안식 + * @since 2025-05-02 + */ + @Override + public float[] getEmbedding(String text) { + if (embeddingPort == null) { + log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); + throw new IllegalStateException("No adapter selected"); + } + return embeddingPort.getEmbedding(text); + } + + /** + * 전달된 DrugVectorDto 리스트에 포함된 약품 ID와 해당 임베딩 벡터를 + * 데이터베이스에 일괄 저장합니다. + * + * @param dtos 저장할 약품 임베딩 정보 리스트 (DrugVectorDto 객체) + * @author 이해창 + * @since 2025-04-25 + * @modified + * 2025-05-02 - 배치 적용으로 인한 입력 타입 변경 + * 2025-04-30 - 임베딩 저장/로딩포트 통합으로 인한 위치 이동 + */ + @Override + public void saveEmbedding(List dtos) { + if (embeddingPort == null) { + log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); + throw new IllegalStateException("No adapter selected"); + } + embeddingPort.saveEmbedding(dtos); + } + + /** + * 지정된 Bean 이름에 해당하는 어댑터로 전환합니다. + * + * @param adapterBeanName 전환할 어댑터 Bean 이름 + * @throws IllegalArgumentException 지원되지 않는 어댑터 이름인 경우 + * @author 정안식 + * @since 2025-05-02 + */ + @Override + public void switchTo(String adapterBeanName) { + log("어댑터 스위치 시도 - 어댑터명: " + adapterBeanName); + EmbeddingPort target = adapters.get(adapterBeanName); + if (target == null) { + log(LogLevel.ERROR, "어댑터 빈을 찾을 수 없습니다: " + adapterBeanName); + throw new IllegalArgumentException("Unknown adapter: " + adapterBeanName); + } + this.embeddingPort = target; + this.adapterBeanName = adapterBeanName; + log("어댑터 스위치 완료 - 현재 어댑터: " + adapterBeanName); + } + + /** + * 현재 활성화된 어댑터 Bean 이름을 반환합니다. + * + * @return 활성화된 어댑터 Bean 이름 + * @author 정안식 + * @since 2025-05-02 + */ + @Override + public String getAdapterBeanName() { + log("어댑터 빈 이름 요청 - 현재 선택된 어댑터: " + adapterBeanName); + return adapterBeanName; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KmBertEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KmBertEmbeddingAdapter.java new file mode 100644 index 0000000..2b8ce86 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KmBertEmbeddingAdapter.java @@ -0,0 +1,151 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.adapter; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.UriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.EmbeddingRequestText; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKmBertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository.KmBertEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.DrugEntityMapper; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.EmbedEntityBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Repository; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Repository +@RequiredArgsConstructor +public class KmBertEmbeddingAdapter implements EmbeddingPort { + private final KmBertEmbedJpaRepository govKmBertEmbedJpaRepository; + private final UriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + /** + * 주어진 텍스트에 대해 외부 KM-BERT 임베딩 API를 호출하여 임베딩 벡터를 생성하고 반환합니다. + * + * @param text 임베딩을 생성할 원본 텍스트 + * @return 생성된 임베딩 벡터 (float 배열) + * @author 함예정 + * @since 2025-04-25 + */ + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + /** + * 전달된 DrugVectorDto 리스트를 DrugKmBertEmbedEntity로 변환하여 + * JPA 저장소에 일괄 저장하고 즉시 플러시(flush)합니다. + * + * @param dtos 저장할 약품 임베딩 정보가 담긴 DTO 리스트 + * @author 함예정 + * @since 2025-04-25 + * @modified + * 2025-05-02 - 배치 적용을 위한 입력값 변경 + */ + @Override + public void saveEmbedding(List dtos) { + govKmBertEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKmBertEmbedEntity.class)) + .toList() + ); + govKmBertEmbedJpaRepository.flush(); + } + + /** + * Pageable 정보를 이용해 원시 약품 데이터와 KM-BERT 임베딩 데이터를 페이징 처리하여 조회하고, + * Drug 도메인 객체 리스트로 변환하여 반환합니다. + * + * @param pageable 페이지 번호, 크기, 정렬 정보를 포함하는 Pageable 객체 + * @return 조회된 Drug 도메인 객체 리스트 + * @throws IndexException 조회된 데이터가 없거나 조회 중 오류가 발생한 경우 + * @author 이해창 + * @since 2025-05-03 + */ + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rows = fetchRawAndEmbedPage(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + List drugs = rows.stream().map(this::combineRawAndEmbed).toList(); + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); + + return drugs; + } + + /** + * KM-BERT 임베딩 API 호출을 위한 URI를 생성하여 반환합니다. + * + * @return Embedding API 호출에 사용할 URI + * @author 함예정 + * @since 2025-04-25 + */ + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKmbertEmbeding(); + } + + /** + * 지정된 URI에 텍스트를 포함한 요청을 전송하여 임베딩 벡터를 받아옵니다. + * + * @param embedUri 임베딩 API 호출 대상 URI + * @param text 임베딩할 원본 텍스트 + * @return API 응답으로 받은 임베딩 벡터 (float 배열) + * @author 함예정 + * @since 2025-04-25 + */ + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + /** + * Pageable 정보를 기반으로 원시 약품 데이터와 임베딩 데이터를 조인하여 조회합니다. + * + * @param pageable 페이지 번호, 크기, 정렬 정보를 포함한 Pageable 객체 + * @return Object[] 배열 리스트; [0]에는 DrugRawDataEntity, [1]에는 DrugKmBertEmbedEntity + * @author 이해창 + * @since 2025-05-03 + */ + private List fetchRawAndEmbedPage(Pageable pageable) { + return govKmBertEmbedJpaRepository.findRawAndEmbed(pageable); + } + + /** + * Object 배열로 전달된 원시 데이터 엔티티와 임베딩 엔티티를 결합하여 + * Drug 도메인 객체로 변환합니다. + * + * @param pair Object 배열; index 0은 DrugRawDataEntity, index 1은 DrugKmBertEmbedEntity + * @return 결합된 데이터를 담은 Drug 도메인 객체 + * @author 이해창 + * @since 2025-05-03 + */ + private Drug combineRawAndEmbed(Object[] pair) { + DrugRawDataEntity raw = (DrugRawDataEntity) pair[0]; + DrugKmBertEmbedEntity embed = (DrugKmBertEmbedEntity) pair[1]; + return DrugEntityMapper.toDomainFromEntity(raw, embed); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java new file mode 100644 index 0000000..309934b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java @@ -0,0 +1,150 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.adapter; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKrSBertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.UriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.EmbeddingRequestText; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository.KrSBertEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.DrugEntityMapper; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.EmbedEntityBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Repository; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Repository +@RequiredArgsConstructor +public class KrSBertEmbeddingAdapter implements EmbeddingPort { + private final KrSBertEmbedJpaRepository krSBertEmbedJpaRepository; + private final UriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + /** + * 주어진 텍스트에 대해 외부 KR-SBERT 임베딩 API를 호출하여 임베딩 벡터를 생성하고 반환합니다. + * + * @param text 임베딩을 생성할 원본 텍스트 + * @return 생성된 임베딩 벡터 (float 배열) + * @author 함예정 + * @since 2025-04-25 + */ + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + /** + * 전달된 DrugVectorDto 리스트를 DrugKrSBertEmbedEntity로 변환하여 + * JPA 저장소에 일괄 저장하고 즉시 플러시(flush)합니다. + * + * @param dtos 저장할 약품 임베딩 정보가 담긴 DTO 리스트 + * @author 함예정 + * @since 2025-04-25 + * @modified + * 2025-05-02 - 배치 적용을 위한 입력값 변경 + */ + @Override + public void saveEmbedding(List dtos) { + krSBertEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKrSBertEmbedEntity.class)) + .toList() + ); + krSBertEmbedJpaRepository.flush(); + } + + /** + * Pageable 정보를 이용해 원시 약품 데이터와 KR-SBERT 임베딩 데이터를 조인하여 + * 페이징 처리된 Drug 도메인 객체 리스트를 조회합니다. + * + * @param pageable 페이지 번호, 크기, 정렬 정보를 포함하는 Pageable 객체 + * @return 조회된 Drug 도메인 객체 리스트 + * @throws IndexException 조회된 데이터가 없거나 조회 중 오류가 발생한 경우 + * @author 이해창 + * @since 2025-05-03 + */ + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rows = fetchRawAndEmbedPage(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + List drugs = rows.stream().map(this::combineRawAndEmbed).toList(); + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); + return drugs; + } + + /** + * KR-SBERT 임베딩 API 호출에 사용할 URI를 생성하여 반환합니다. + * + * @return Embedding API 호출용 URI + * @author 함예정 + * @since 2025-04-25 + */ + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKrSbertEmbeding(); + } + + /** + * 지정된 URI에 텍스트를 포함한 HTTP 요청을 전송하여 임베딩 벡터를 받아옵니다. + * + * @param embedUri 임베딩 API 호출 대상 URI + * @param text 임베딩할 원본 텍스트 + * @return API 응답으로 받은 임베딩 벡터 (float 배열) + * @author 함예정 + * @since 2025-04-25 + */ + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + /** + * Pageable 정보를 기반으로 원시 약품 데이터와 임베딩 데이터를 조인하여 조회합니다. + * + * @param pageable 페이지 번호, 크기, 정렬 정보를 포함하는 Pageable 객체 + * @return Object[] 배열 리스트; index 0은 DrugRawDataEntity, index 1은 DrugKrSBertEmbedEntity + * @author 이해창 + * @since 2025-05-03 + */ + private List fetchRawAndEmbedPage(Pageable pageable) { + return krSBertEmbedJpaRepository.findRawAndEmbed(pageable); + } + + /** + * Object 배열로 전달된 원시 데이터 엔티티와 임베딩 엔티티를 결합하여 + * Drug 도메인 객체로 변환합니다. + * + * @param pair Object 배열; index 0은 DrugRawDataEntity, index 1은 DrugKrSBertEmbedEntity + * @return 결합된 데이터를 담은 Drug 도메인 객체 + * @author 이해창 + * @since 2025-05-03 + */ + private Drug combineRawAndEmbed(Object[] pair) { + DrugRawDataEntity raw = (DrugRawDataEntity) pair[0]; + DrugKrSBertEmbedEntity embed = (DrugKrSBertEmbedEntity) pair[1]; + return DrugEntityMapper.toDomainFromEntity(raw, embed); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java new file mode 100644 index 0000000..418b4d7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java @@ -0,0 +1,144 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.adapter; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugGptEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository.OpenAiEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.DrugEntityMapper; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.EmbedEntityBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Repository +@RequiredArgsConstructor +public class OpenAiEmbeddingAdapter implements EmbeddingPort { + private final OpenAiEmbedJpaRepository govDrugGptEmbedJpaRepository; + private final OpenAiApi openAiApi; + private static final String EMBEDDING_MODEL_NAME = "text-embedding-3-small"; // yml에서 받아오기 + + + /** + * 주어진 텍스트를 OpenAI 임베딩 모델에 전달하여 임베딩 벡터를 생성하고 반환합니다. + * + * @param text 임베딩을 생성할 입력 문자열 + * @return OpenAI 모델이 생성한 임베딩 벡터 (float 배열) + * @author 이해창 + * @since 2025-04-25 + * @modified + * 25.05.03 - 가독성 개선을 위한 리팩토링 + */ + @Override + public float[] getEmbedding(String text) { + OpenAiEmbeddingModel openAiEmbeddingModel = getOpenAiEmbeddingModel(); + EmbeddingResponse embeddingResponse = openAiEmbeddingModel.embedForResponse(List.of(text)); + return embeddingResponse.getResults().getFirst().getOutput(); + } + + /** + * 전달된 DrugVectorDto 리스트를 DrugGptEmbedEntity로 변환하여 + * JPA 저장소에 일괄 저장하고 즉시 플러시(flush)합니다. + * + *

각 DTO는 EmbedEntityBuilder.buildEmbedEntity를 통해 + * DrugGptEmbedEntity 타입의 임베딩 엔티티로 변환됩니다.

+ * + * @param dtos 저장할 약품 임베딩 정보가 담긴 DTO 리스트 + * @author 함예정 + * @since 2025-04-25 + */ + @Override + public void saveEmbedding(List dtos) { + govDrugGptEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugGptEmbedEntity.class)) + .toList() + ); + govDrugGptEmbedJpaRepository.flush(); + } + + /** + * Pageable 정보를 이용해 원시 약품 데이터와 임베딩 데이터를 페이징 처리하여 조회하고, + * Drug 도메인 객체 리스트로 변환하여 반환합니다. + * + *

조회된 데이터가 없을 경우 IndexException을 발생시킵니다.

+ * + * @param pageable 페이지 번호, 크기, 정렬 정보를 포함하는 Pageable 객체 + * @return 조회된 Drug 도메인 객체 리스트 + * @throws IndexException 원시 데이터 및 임베딩 데이터를 조회하지 못했거나 조회된 데이터가 없을 경우 + * @author 이해창 + * @since 2025-05-03 + */ + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rows = fetchRawAndEmbedPage(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + List drugs = rows.stream().map(this::combineRawAndEmbed).toList(); + log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); + return drugs; + } + + /** + * OpenAI 임베딩 모델을 EMBED 모드로 초기화하여 반환합니다. + * + * @return EMBED 모드로 구성된 OpenAiEmbeddingModel 인스턴스 + * @author 이해창 + * @since 2025-05-03 + */ + private OpenAiEmbeddingModel getOpenAiEmbeddingModel() { + OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( + this.openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model(EMBEDDING_MODEL_NAME) + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE); + return openAiEmbeddingModel; + } + + /** + * Pageable 정보를 기반으로 원시 약품 데이터와 임베딩 데이터를 조인하여 조회합니다. + * + * @param pageable 페이지 번호, 크기, 정렬 정보를 포함한 Pageable 객체 + * @return Object[] 배열 리스트; [0]에는 DrugRawDataEntity, [1]에는 DrugGptEmbedEntity + * @author 이해창 + * @since 2025-05-03 + */ + private List fetchRawAndEmbedPage(Pageable pageable) { + return govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); + } + + /** + * Object 배열로 전달된 원시 데이터 엔티티와 임베딩 엔티티를 결합하여 + * Drug 도메인 객체로 변환합니다. + * + * @param pair Object 배열; index 0은 DrugRawDataEntity, index 1은 DrugGptEmbedEntity + * @return 결합된 데이터를 담은 Drug 도메인 객체 + * @author 이해창 + * @since 2025-05-03 + */ + private Drug combineRawAndEmbed(Object[] pair) { + DrugRawDataEntity raw = (DrugRawDataEntity) pair[0]; + DrugGptEmbedEntity embed = (DrugGptEmbedEntity) pair[1]; + return DrugEntityMapper.toDomainFromEntity(raw, embed); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java similarity index 55% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java index fb5983e..bd65dcf 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKmBertEmbedJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java @@ -1,24 +1,24 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKmBertEmbedEntity; import java.util.List; @Repository -public interface GovDrugKmBertEmbedJpaRepository extends JpaRepository { - @Query( - value = """ +public interface KmBertEmbedJpaRepository extends JpaRepository { + String QUERY = """ SELECT r, e FROM DrugKmBertEmbedEntity e JOIN DrugRawDataEntity r ON r.drugId = e.drugId WHERE e.kmBertVector IS NOT NULL - """ - ) + AND r.isHerbal is FALSE + """; + @Query(QUERY) List findRawAndEmbed(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java new file mode 100644 index 0000000..1d405b6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKrSBertEmbedEntity; + +@Repository +public interface KrSBertEmbedJpaRepository extends JpaRepository { + String QUERY = """ + SELECT r, e + FROM DrugKrSBertEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.krSBertVector IS NOT NULL + AND r.isHerbal is FALSE + """; + + @Query(QUERY) + List findRawAndEmbed(Pageable pageable); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java similarity index 55% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java index 2a7501b..14069aa 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugGptEmbedJpaRepository.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java @@ -1,24 +1,24 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugGptEmbedEntity; import java.util.List; @Repository -public interface GovDrugGptEmbedJpaRepository extends JpaRepository { - @Query( - value = """ +public interface OpenAiEmbedJpaRepository extends JpaRepository { + String QUERY = """ SELECT r, e FROM DrugGptEmbedEntity e JOIN DrugRawDataEntity r ON r.drugId = e.drugId WHERE e.gptVector IS NOT NULL - """ - ) + AND r.isHerbal is FALSE + """; + @Query(QUERY) List findRawAndEmbed(Pageable pageable); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/DrugEmbedController.java similarity index 74% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/DrugEmbedController.java index dd514a4..5102901 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/DrugEmbedController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/DrugEmbedController.java @@ -1,7 +1,8 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.embed; +package com.likelion.backendplus4.yakplus.drug.embed.presentation; -import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; +import com.likelion.backendplus4.yakplus.drug.embed.presentation.docs.DrugEmbedControllerDocs; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -9,8 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.in.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; import lombok.RequiredArgsConstructor; @@ -21,9 +22,9 @@ * @since 2025-05-02 */ @RestController -@RequestMapping("/embed") +@RequestMapping("/job/embed") @RequiredArgsConstructor -public class DrugEmbedController { +public class DrugEmbedController implements DrugEmbedControllerDocs { private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; /** @@ -34,6 +35,7 @@ public class DrugEmbedController { * @author 함예정 * @since 2025-05-02 */ + @Override @PostMapping("/start") public ResponseEntity> start() { return success(drugEmbedProcessorUseCase.startEmbedding()); @@ -47,6 +49,7 @@ public ResponseEntity> start() { * @author 함예정 * @since 2025-05-02 */ + @Override @DeleteMapping("/stop") public ResponseEntity> stop() { return success(drugEmbedProcessorUseCase.stopEmbedding()); @@ -60,6 +63,7 @@ public ResponseEntity> stop() { * @author 함예정 * @since 2025-05-02 */ + @Override @GetMapping("/status") public ResponseEntity> status() { return success(drugEmbedProcessorUseCase.statusEmbedding()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java new file mode 100644 index 0000000..d1c7fc1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java @@ -0,0 +1,53 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.in.EmbeddingRoutingUseCase; +import com.likelion.backendplus4.yakplus.drug.embed.presentation.docs.EmbeddingRouterControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * Embedding 라우팅 어댑터를 전환하고 조회하는 REST 컨트롤러 + * + * 요청에 따라 활성화된 Embedding adapter를 변경하거나 현재 adapter를 확인합니다. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/switch/embeddings") +public class EmbeddingRouterController implements EmbeddingRouterControllerDocs { + private final EmbeddingRoutingUseCase routerUseCase; + + public EmbeddingRouterController(EmbeddingRoutingUseCase routerUseCase) { + this.routerUseCase = routerUseCase; + } + + /** + * 지정된 adapterBeanName에 해당하는 embedding adapter로 전환합니다. + * + * @param adapterBeanName 전환할 adapter Bean 이름 + * @return 어댑터 변경 결과 메시지를 담은 ApiResponse + * @author 정안식 + * @since 2025-05-02 + */ + @PostMapping("/switch/{adapterBeanName}") + public ResponseEntity> switchAdapter(@PathVariable String adapterBeanName) { + log("스위치 대상 인덱스명 : " + adapterBeanName); + routerUseCase.switchEmbedding(adapterBeanName); + return ApiResponse.success("어댑터 변경됨 - 어댑터명: " + adapterBeanName); + } + + /** + * 현재 활성화된 embedding adapter Bean 이름을 조회합니다. + * + * @return 현재 adapter Bean 이름을 담은 ApiResponse + * @author 정안식 + * @since 2025-05-02 + */ + @GetMapping("/current/adapter") + public ResponseEntity> checkCurrentAdapter() { + return ApiResponse.success(routerUseCase.getAdapterBeanName()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java new file mode 100644 index 0000000..f5518bc --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java @@ -0,0 +1,26 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation.docs; + +import org.springframework.http.ResponseEntity; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * 약품 임베딩 벡터 생성 작업 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Embed", description = "약품 임베딩 벡터 생성 작업 API") +public interface DrugEmbedControllerDocs { + + @Operation(summary = "임베딩 벡터 생성 작업 시작", description = "임베딩 벡터 생성 작업을 시작합니다.") + ResponseEntity> start(); + + @Operation(summary = "임베딩 작업 중지", description = "진행 중인 임베딩 작업을 중지합니다.") + ResponseEntity> stop(); + + @Operation(summary = "임베딩 작업 상태 조회", description = "현재 임베딩 작업의 상태를 조회합니다.") + ResponseEntity> status(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java new file mode 100644 index 0000000..6b2156b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * Embedding 라우팅 어댑터 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Embedding Router", description = "Embedding 라우팅 어댑터 전환 및 조회 API") +public interface EmbeddingRouterControllerDocs { + + @Operation( + summary = "Embedding adapter 전환", + description = "지정된 adapterBeanName에 해당하는 embedding adapter로 전환합니다." + ) + ResponseEntity> switchAdapter( + @Parameter( + name = "adapterBeanName", + description = "전환할 adapter Bean 이름", + in = ParameterIn.PATH, + required = true + ) + String adapterBeanName + ); + + @Operation( + summary = "현재 활성화된 adapter 조회", + description = "현재 사용 중인 embedding adapter Bean 이름을 반환합니다." + ) + ResponseEntity> checkCurrentAdapter(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java new file mode 100644 index 0000000..bc30806 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.in; + +public interface IndexUseCase { + + /** + * 요청으로 전달된 lastSeq, limit 정보를 바탕으로 RDB에서 데이터를 조회하고 + * ES 인덱스에 저장한다. + * + * @author 정안식 + * @modified 2025-05-02 이해창 + * 25.05.02 - 저장된 약물 상세정보 데이터 크기를 기준으로 ES에 색인하는 loop를 만들도록 수정 + * 25.04.28 - IndexRequest를 인자로 더 이상 받지 않도록 수정 + * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 + * @since 2025-04-22 + */ + void index(); + + /** + * DB에서 약품 데이터를 페이징으로 가져와 Elasticsearch에 일괄 색인합니다. + * 각 페이지는 CHUNK_SIZE만큼 처리되며, 모든 데이터를 순차적으로 색인합니다. + * + * @author 박찬병 + * @modified 2025-04-25 + * @since 2025-04-24 + */ + void indexKeyword(); + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java new file mode 100644 index 0000000..d9b195b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import org.springframework.data.domain.Page; + +public interface DrugIndexRepositoryPort { + + /** + * 주어진 Drug 목록을 Elasticsearch에 Bulk API로 일괄 저장합니다. + * + * @param esIndexName Bulk 대상 ES 인덱스 이름 + * @param drugs 저장할 Drug 객체 리스트 + * @throws IndexException 색인 처리 중 오류 발생 시 + * @author 정안식 + * @modified 2025-04-27 + * 25.04.27 - esIndexname을 인자로 받아 Bulk API로 일괄 저장하도록 수정 + * @since 2025-04-22 + */ + void saveAll(String esIndexName, List drugs); + + /** + * 약품 도메인 객체 페이지를 Elasticsearch 문서로 변환하여 색인합니다. + * + * 1) Drug 도메인 객체 → DrugKeywordDocument로 변환 + * 2) Elasticsearch에 saveAll로 일괄 색인 + * + * @param drugPage 색인할 약품 도메인 페이지 + * @author 박찬병 + * @since 2025-05-03 + * @modified 2025-05-03 + */ + void saveAllKeyword(Page drugPage); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java new file mode 100644 index 0000000..9b42115 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface DrugRawDataPort { + List fetchRawData(int pageNo, int numOfRows); + + + /** + * 주어진 Pageable 정보에 따라 DB에서 한 페이지 분량의 GovDrugEntity를 조회하고, + * 각 엔티티를 도메인 모델(GovDrug)로 변환하여 Page 형태로 반환합니다. + * + * @param pageable 조회할 페이지 번호와 크기를 포함하는 Pageable 객체 + * @return 페이지 단위로 변환된 GovDrug 도메인 객체의 Page + * @author 박찬병 + * @since 2025-04-24 + * @modified 2025-04-25 + */ + Page findAllDrugs(Pageable pageable); + + /** + * JPA 레포지토리를 이용해 GovDrugJpaRepository의 전체 데이터 수를 조회합니다. + * + * @return GovDrugJpaRepository의 전체 데이터 수 + * @author 이해창 + * @since 2025-05-02 + * @modified + */ + long getDrugTotalSize(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java new file mode 100644 index 0000000..eca2a94 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.out; + +import java.util.List; + +import org.springframework.data.domain.Pageable; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; + +public interface EmbeddingPort { + /** + * 주어진 페이지 정보(Pageable)에 따라 데이터베이스에서 원시 약품 데이터와 임베딩 데이터를 + * 조인하여 한 페이지 분량의 Drug 도메인 객체 목록을 조회합니다. + * + * @param pageable 조회할 페이지 번호와 크기, 정렬 정보 등을 포함하는 Pageable 객체 + * @return 지정된 페이지 범위에 해당하는 Drug 도메인 객체들의 리스트 + * @author 이해창 + * @since 2025-05-03 + */ + List loadEmbeddingsByPage(Pageable pageable); + + /** + * 주어진 텍스트에 대해 OpenAI 임베딩 모델을 호출하여 임베딩 벡터를 생성하고 반환합니다. + * + * @param text 임베딩을 생성할 원본 텍스트 + * @return 생성된 임베딩 벡터 (float 배열) + * @author 이해창 + * @since 2025-04-25 + * @modified + * 2025-04-30 - 임베딩 저장/로딩포트 통합으로 인한 위치 이동 + */ + float[] getEmbedding(String text); + + /** + * 전달된 DrugVectorDto 리스트에 포함된 약품 ID와 해당 임베딩 벡터를 + * 데이터베이스에 일괄 저장합니다. + * + * @param dtos 저장할 약품 임베딩 정보 리스트 (DrugVectorDto 객체) + * @author 함예정 + * @since 2025-04-25 + * @modified + * 2025-05-02 - 배치 적용으로 인한 입력 타입 변경 + * 2025-04-30 - 임베딩 저장/로딩포트 통합으로 인한 위치 이동 + */ + void saveEmbedding(List dtos); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java similarity index 56% rename from src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java index 045884f..8488d85 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java @@ -1,21 +1,18 @@ -package com.likelion.backendplus4.yakplus.index.application.service; +package com.likelion.backendplus4.yakplus.drug.index.application.service; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; -import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; -import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; -import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import com.likelion.backendplus4.yakplus.drug.index.application.port.in.IndexUseCase; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugRawDataPort; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.out.EmbeddingSwitchPort; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.util.List; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; /** * 약품 색인(인덱싱) 작업을 수행하는 서비스 구현체 @@ -29,10 +26,11 @@ @Service @RequiredArgsConstructor public class DrugIndexer implements IndexUseCase { - private final GovDrugRawDataPort govDrugRawDataPort; + private final DrugRawDataPort drugRawDataPort; private final DrugIndexRepositoryPort drugIndexRepositoryPort; private final EmbeddingSwitchPort embeddingSwitchPort; - private static final String SORT_BY_PROPERTY = "drugId"; + + private static final String INDENT = " "; private static final int CHUNK_SIZE = 1_000; /** @@ -40,8 +38,8 @@ public class DrugIndexer implements IndexUseCase { * ES 인덱스에 저장한다. * * @author 정안식 - * @modified 2025-05-02 이해창 - * 25.05.02 - 저장된 약물 상세정보 데이터 크기를 기준으로 ES에 색인하는 loop를 만들도록 수정 + * @modified + * 25.05.02 - 이해창: 저장된 약물 상세정보 데이터 크기를 기준으로 ES에 색인하는 loop를 만들도록 수정 * 25.04.28 - IndexRequest를 인자로 더 이상 받지 않도록 수정 * 25.04.27 - esIndexname을 인자로 받아 saveAll 메서드에 전달하도록 수정 * @since 2025-04-22 @@ -49,21 +47,19 @@ public class DrugIndexer implements IndexUseCase { @Override public void index() { log("index 서비스 요청 수신"); + String esIndexName = getEsIndexName(); - long totalDataSize = govDrugRawDataPort.getDrugTotalSize(); - int totalPages = (int) ((totalDataSize + CHUNK_SIZE - 1) / CHUNK_SIZE); // 전체 페이지 수 계산 (올림) - List drugs; + long totalDataSize = drugRawDataPort.getDrugTotalSize(); + int totalPages = getTotalPages(totalDataSize); + for(int currentPage = 0; currentPage < totalPages; currentPage++) { log("색인 시작: page=" + currentPage); - try{ - drugs = fetchRawData(currentPage, CHUNK_SIZE); - } catch (Exception e) { - log(LogLevel.ERROR, String.format("%d 페이지 색인용 데이터 로딩 실패", currentPage), e); - continue; - } - log(" 조회 완료: page=" + currentPage + ", 건수=" + drugs.size()); + + List drugs = fetchRawData(currentPage, CHUNK_SIZE); + log(INDENT+"조회 완료: page=" + currentPage + ", 건수=" + drugs.size()); + saveDrugs(esIndexName, drugs); - log(" 색인 완료: page=" + currentPage + ", 건수=" + drugs.size()); + log("색인 완료: page=" + currentPage + ", 건수=" + drugs.size()); } } @@ -76,8 +72,8 @@ public void index() { * @since 2025-04-24 */ @Override - public void indexSymptom() { - log("indexSymptom 요청 수신"); + public void indexKeyword() { + log("indexKeyword 요청 수신"); int page = 0; Page drugPage; @@ -85,12 +81,12 @@ public void indexSymptom() { log("색인 시작: page=" + page); // 1. 페이징으로 DB에서 한 청크 가져오기 - drugPage = govDrugRawDataPort.findAllDrugs(PageRequest.of(page, CHUNK_SIZE)); - log(" 조회 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); + drugPage = drugRawDataPort.findAllDrugs(PageRequest.of(page, CHUNK_SIZE)); + log(INDENT+"조회 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); // 2. 청크별 ES에 색인 - drugIndexRepositoryPort.saveAllSymptom(drugPage); - log(" 색인 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); + drugIndexRepositoryPort.saveAllKeyword(drugPage); + log(INDENT+"색인 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); // 3. 다음 1000개 값 루프 page++; @@ -99,18 +95,33 @@ public void indexSymptom() { } /** - * limit 크기 및 DrugId 오름차순 정렬 기준의 객체를 생성한다. + * Elasticsearch 인덱스 이름을 조회한다. * - * @param limit 조회할 최대 건수 - * @return 페이징 객체 + * @return Elasticsearch 인덱스 이름 * @author 정안식 - * @modified 2025-04-27 - * 25.04.27 - itemSeq -> drugId로 수정 - * @since 2025-04-22 + * @since 2025-04-27 + * @modified + * 2025-05-02 - 이해창: 하드코딩 된 문자를 받어오던 것을 임베딩 모델 BeanName을 가져오도록 수정 */ - private Pageable createPageable(int limit) { - log("pageable 생성"); - return PageRequest.of(0, limit, Sort.by(SORT_BY_PROPERTY).ascending()); + private String getEsIndexName() { + log("ES 인덱스 이름 조회"); + return embeddingSwitchPort.getAdapterBeanName(); + } + + /** + * 전체 데이터 개수를 기준으로 CHUNK_SIZE 단위로 나누어 필요한 페이지 수를 계산합니다. + * + *

예를 들어, 전체 데이터가 25개이고 CHUNK_SIZE가 10이라면 + * (25 + 10 - 1) / 10 = 3페이지가 계산됩니다.

+ * + * @param totalDataSize 전체 데이터 개수 + * @return 필요한 전체 페이지 수 (마지막 페이지에 남는 데이터도 한 페이지로 처리) + * @author 이해창 + * @since 2025-05-03 + */ + private static int getTotalPages(long totalDataSize) { + int totalPages = (int) ((totalDataSize + CHUNK_SIZE - 1) / CHUNK_SIZE); + return totalPages; } /** @@ -120,29 +131,14 @@ private Pageable createPageable(int limit) { * @param numOfRows 한 페이지당 조회할 건수 * @return 도메인 모델 리스트 * @author 정안식 - * @modified 2025-05-02 이해창
- * 25.05.02 - 페이징 처리 시 페이지 사이즈 받도록 수정
+ * @modified + * 25.05.02 - 이해창: 페이징 처리 시 페이지 사이즈 받도록 수정
* 25.04.28 - 페이징 처리 로직 수정 * @since 2025-04-22 */ private List fetchRawData(int pageNum, int numOfRows) { log("RDB에서 원시 데이터 조회"); - return govDrugRawDataPort.fetchRawData(pageNum, numOfRows); - } - - /** - * Elasticsearch 인덱스 이름을 조회한다. - * - * @return Elasticsearch 인덱스 이름 - * @author 정안식 - * @since 2025-04-27 - * @modified 2025-05-02 이해창
- * 2025-05-02 - 하드코딩 된 문자를 받어오던 것을 - * 임베딩 모델 BeanName을 가져오도록 수정 - */ - private String getEsIndexName() { - log("ES 인덱스 이름 조회"); - return embeddingSwitchPort.getAdapterBeanName(); + return drugRawDataPort.fetchRawData(pageNum, numOfRows); } /** diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/IndexException.java similarity index 87% rename from src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/IndexException.java index b2b08b9..e90072e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/exception/IndexException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/IndexException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.index.exception; +package com.likelion.backendplus4.yakplus.drug.index.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/error/IndexErrorCode.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/error/IndexErrorCode.java index eba0492..dc51528 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/exception/error/IndexErrorCode.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/error/IndexErrorCode.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.index.exception.error; +package com.likelion.backendplus4.yakplus.drug.index.exception.error; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; import lombok.RequiredArgsConstructor; @@ -7,7 +7,7 @@ @RequiredArgsConstructor public enum IndexErrorCode implements ErrorCode { RAW_DATA_FETCH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430001, "원시 데이터 조회 실패"), - ES_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430003, "Elasticsearch 저장 실패"), + ES_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430002, "Elasticsearch 저장 실패"), EMBEDDING_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 440001, "임베딩 API 호출 실패"); private final HttpStatus status; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/adapter/ElasticsearchDrugAdapter.java similarity index 73% rename from src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/adapter/ElasticsearchDrugAdapter.java index 74f5ee9..12eff3a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/adapter/ElasticsearchDrugAdapter.java @@ -1,17 +1,19 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +package com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.adapter; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.index.application.port.out.DrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugIndexRepositoryPort; import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.DrugSymptomRepository; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; -import com.likelion.backendplus4.yakplus.index.support.mapper.SymptomMapper; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; + +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.repository.DrugKeywordRepository; +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document.DrugKeywordDocument; +import com.likelion.backendplus4.yakplus.drug.index.support.mapper.KeywordMapper; import org.apache.http.entity.ContentType; import org.apache.http.nio.entity.NStringEntity; import org.elasticsearch.client.Request; + import org.elasticsearch.client.RestClient; import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; @@ -21,7 +23,7 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; /** * Elasticsearch를 통해 Drug 도메인 객체의 색인 기능을 제공하는 어댑터 클래스입니다. @@ -35,12 +37,12 @@ */ @Component public class ElasticsearchDrugAdapter implements DrugIndexRepositoryPort { - private final DrugSymptomRepository symptomRepository; + private final DrugKeywordRepository keywordRepository; private final RestClient restClient; private final ObjectMapper objectMapper; - public ElasticsearchDrugAdapter(DrugSymptomRepository drugSymptomRepository, RestClient restClient, ObjectMapper objectMapper) { - this.symptomRepository = drugSymptomRepository; + public ElasticsearchDrugAdapter(DrugKeywordRepository drugKeywordRepository, RestClient restClient, ObjectMapper objectMapper) { + this.keywordRepository = drugKeywordRepository; this.restClient = restClient; this.objectMapper = objectMapper; } @@ -70,17 +72,33 @@ public void saveAll(String esIndexName, List drugs) { } } + /** + * 약품 도메인 객체 페이지를 Elasticsearch 문서로 변환하여 색인합니다. + * + * 1) Drug 도메인 객체 → DrugKeywordDocument로 변환 + * 2) Elasticsearch에 saveAll로 일괄 색인 + * + * @param drugs 색인할 약품 도메인 페이지 + * @author 박찬병 + * @since 2025-05-03 + * @modified 2025-05-03 + */ @Override @Transactional(propagation = Propagation.REQUIRES_NEW) - public void saveAllSymptom(Page drugs) { + public void saveAllKeyword(Page drugs) { // 도메인 → ES Document 변환 log("saveAllSymptom() 요청 수신"); - List docs = drugs.stream() - .map(SymptomMapper::toDocument) // 내부에서 예외 처리 됨 + List docs = drugs.stream() + .map(drug -> KeywordMapper.toDocument( + drug, + drug.generateEfficacySuggestions(), + drug.generateIngredientSuggestions() + )) .toList(); + log(" 문서 변환 완료: count=" + docs.size()); - symptomRepository.saveAll(docs); + keywordRepository.saveAll(docs); log(" ES 색인 완료: count=" + docs.size()); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java similarity index 93% rename from src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java index ddbe9e6..9fc125a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/document/DrugKeywordDocument.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java @@ -1,6 +1,5 @@ -package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document; +package com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document; -import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -11,6 +10,8 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import java.util.List; + @Document(indexName = "drug_keyword") @Getter @NoArgsConstructor diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java new file mode 100644 index 0000000..a3f7a4e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.repository; + +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document.DrugKeywordDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface DrugKeywordRepository extends ElasticsearchRepository { + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/DrugIndexingController.java similarity index 65% rename from src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/DrugIndexingController.java index 8c3230d..f1fb017 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/DrugController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/DrugIndexingController.java @@ -1,14 +1,15 @@ -package com.likelion.backendplus4.yakplus.index.presentation.controller; +package com.likelion.backendplus4.yakplus.drug.index.presentation.controller; -import com.likelion.backendplus4.yakplus.index.application.port.in.IndexUseCase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.index.application.port.in.IndexUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.index.presentation.controller.docs.DrugIndexingControllerDocs; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; /** * 약품 인덱싱 API 엔드포인트를 제공하는 컨트롤러 클래스 @@ -17,9 +18,9 @@ * @since 2025-04-22 */ @RestController -@RequestMapping("/api/drugs/index") +@RequestMapping("/drugs/index") @RequiredArgsConstructor -public class DrugController { +public class DrugIndexingController implements DrugIndexingControllerDocs { private final IndexUseCase indexUseCase; /** @@ -30,6 +31,7 @@ public class DrugController { * 25.04.28 - IndexUseRequest를 인자에서 제거하였습니다.(추후 임베딩 모델 선택 로직 추가시 변경예정) * @since 2025-04-22 */ + @Override @PostMapping("/save") public void index() { log("컨트롤러 indexAll 요청 수신"); @@ -37,17 +39,18 @@ public void index() { } /** - * 색인 배치 작업을 실행하여, DB로부터 조회한 약품 증상 데이터를 Elasticsearch에 일괄 색인합니다. + * 색인 배치 작업을 실행하여, DB로부터 조회한 약품 데이터를 Elasticsearch에 일괄 색인합니다. * * @return 색인 작업 성공 여부 응답 (Void) * @author 박찬병 * @modified 2025-04-25 * @since 2025-04-24 */ - @PostMapping("/symptom") + @Override + @PostMapping("/keyword") public ResponseEntity> triggerIndex() { log("indexSymptom 요청 수신"); - indexUseCase.indexSymptom(); + indexUseCase.indexKeyword(); return ApiResponse.success(); } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java new file mode 100644 index 0000000..8dbe6e5 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.index.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * 약품 인덱싱 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Indexing", description = "약품 인덱싱 API") +public interface DrugIndexingControllerDocs { + + @Operation( + summary = "자연어 인덱싱 작업 실행", + description = "약품 인덱싱 작업을 시작합니다." + ) + void index(); + + @Operation( + summary = "키워드 인덱싱 작업 실행", + description = "DB로부터 조회한 약품 데이터를 Elasticsearch에 일괄 색인합니다." + ) + ResponseEntity> triggerIndex(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java new file mode 100644 index 0000000..6d46b70 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java @@ -0,0 +1,39 @@ +package com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.EmbeddingEntity; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugFieldTypeMapper; + +public class DrugEntityMapper { + /** + * DrugRawDataEntity와 EmbeddingEntity를 결합하여 Drug 도메인 객체로 변환합니다. + * + * @param EmbeddingEntity를 상속하는 임베딩 엔티티 타입 + * @param drugEntity DB에서 조회한 원시 약품 데이터 엔티티 + * @param embedEntity 해당 약품의 임베딩 정보를 담고 있는 엔티티 + * @return 원시 데이터와 임베딩 정보를 포함한 Drug 도메인 객체 + * @author 이해창 + * @since 2025-05-03 + */ + public static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, E embedEntity) { + return Drug.builder() + .drugId(drugEntity.getDrugId()) + .drugName(drugEntity.getDrugName()) + .company(drugEntity.getCompany()) + .permitDate(drugEntity.getPermitDate()) + .isGeneral(drugEntity.isGeneral()) + .materialInfo(DrugFieldTypeMapper.parseMaterials(drugEntity.getMaterialInfo())) + .storeMethod(drugEntity.getStoreMethod()) + .validTerm(drugEntity.getValidTerm()) + .efficacy(DrugFieldTypeMapper.parseStringToList(drugEntity.getEfficacy())) + .usage(DrugFieldTypeMapper.parseStringToList(drugEntity.getUsage())) + .precaution(DrugFieldTypeMapper.parsePrecaution(drugEntity.getPrecaution())) + .imageUrl(drugEntity.getImageUrl()) + .cancelDate(drugEntity.getCancelDate()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) + .vector(DrugFieldTypeMapper.parseJsonToFloatArray(embedEntity.getVector())) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/EmbedEntityBuilder.java similarity index 75% rename from src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/EmbedEntityBuilder.java index 580d5cb..98dce1a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/support/EmbeddingUtil/EmbedEntityBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/EmbedEntityBuilder.java @@ -1,8 +1,8 @@ -package com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil; +package com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; public class EmbedEntityBuilder { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -21,8 +21,8 @@ private static String toStringFromFloatArray(float[] vector) { try { return MAPPER.writeValueAsString(vector); } catch (JsonProcessingException e) { - e.printStackTrace(); - return null; + //TODO: 변환 실패 로그 + throw new RuntimeException(e); } } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java new file mode 100644 index 0000000..d51c2e2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.index.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document.DrugKeywordDocument; + +import java.util.List; + +public class KeywordMapper { + + public static DrugKeywordDocument toDocument( + Drug entity, + List efficacyTokens, + List ingredientTokens + ) { + return DrugKeywordDocument.builder() + .drugId(entity.getDrugId()) + .drugName(entity.getDrugName()) + .efficacy(entity.getEfficacy()) + .efficacyList(efficacyTokens) + .imageUrl(entity.getImageUrl()) + .company(entity.getCompany()) + .drugNameSuggester(List.of(entity.getDrugName())) + .ingredientName(ingredientTokens) + .ingredientNameSuggester(ingredientTokens) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/parser/SymptomTextParser.java similarity index 97% rename from src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/parser/SymptomTextParser.java index 0bd06aa..e242d9e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/SymptomTextParser.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/parser/SymptomTextParser.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.index.support.parser; +package com.likelion.backendplus4.yakplus.drug.index.support.parser; import java.util.Arrays; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java deleted file mode 100644 index 08f66db..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiError.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception; - -import org.springframework.http.HttpStatus; - -import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public enum RestApiError implements ErrorCode { - PAGE_COUNT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 500001, "API 전체 페이지 개수를 확인하지 못했습니다."), - ITEM_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 500002, "API 응답에서 적절한 items를 추출하지 못했습니다."); - - private final HttpStatus httpStatus; - private final int code; - private final String message; - - @Override - public HttpStatus httpStatus() { - return httpStatus; - } - - @Override - public int codeNumber() { - return code; - } - - @Override - public String message() { - return message; - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java deleted file mode 100644 index aab1bd0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/BatchJobAdapter.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.adapter; - -import org.springframework.batch.core.Job; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; - -import lombok.RequiredArgsConstructor; - -/** - * BatchJobPort 인터페이스의 구현체로, - * Spring Batch Job 객체들을 제어하는 어댑터 클래스입니다. - * JobManager를 통해 배치 작업의 실행, 중지, 상태 조회 기능을 제공합니다. - * - * @since 2025-05-02 - */ -@Component -@RequiredArgsConstructor -public class BatchJobAdapter implements BatchJobPort { - - private final JobManager jobManager; - private final Job drugScrapJob; - private final Job drugDetailScrapJob; - private final Job drugImageScrapJob; - private final Job drugTableCombineJob; - private final Job embedJob; - - @Override - public String allJobStart() { - jobManager.IfAlreadyRunThrowException(); - return jobManager.startJob(drugScrapJob); - } - - @Override - public String allJobStop() { - return jobManager.stopRunningBatch(drugScrapJob); - } - - @Override - public String allJobResume() { - return jobManager.restart(); - } - - @Override - public String allJobStatus() { - return jobManager.getJobStatus(drugScrapJob); - } - - @Override - public String detailScrapJobStart() { - jobManager.IfAlreadyRunThrowException(); - return jobManager.startJob(drugDetailScrapJob); - } - - @Override - public String detailScrapJobStop() { - return jobManager.stopRunningBatch(drugDetailScrapJob); - } - - @Override - public String detailScrapJobStatus() { - return jobManager.getJobStatus(drugDetailScrapJob); - } - - @Override - public String imageScrapJobStart() { - jobManager.IfAlreadyRunThrowException(); - return jobManager.startJob(drugImageScrapJob); - } - - @Override - public String imageScrapJobStop() { - return jobManager.stopRunningBatch(drugImageScrapJob); - } - - @Override - public String imageScrapJobStatus() { - return jobManager.getJobStatus(drugImageScrapJob); - } - - @Override - public String tableCombineJobStart() { - jobManager.IfAlreadyRunThrowException(); - return jobManager.startJob(drugTableCombineJob); - } - - @Override - public String tableCombineJobStop() { - return jobManager.stopRunningBatch(drugTableCombineJob); - } - - @Override - public String tableCombineJobStatus() { - return jobManager.getJobStatus(drugTableCombineJob); - } - - @Override - public String embedJobStart(){ - return jobManager.startJob(embedJob); - } - - @Override - public String embedJobStop(){ - return jobManager.stopRunningBatch(embedJob); - } - - @Override - public String embedjobStatus(){ - return jobManager.getJobStatus(embedJob); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java deleted file mode 100644 index c44fbac..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineJobConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.config; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class CombineJobConfig { - @Bean - public Job drugTableCombineJob(JobRepository jobRepository, - Step tableCombineStep) { - return new JobBuilder("drugTableCombineJob", jobRepository) - .start(tableCombineStep) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java deleted file mode 100644 index bc624a0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/config/CombineStepConfig.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.config; - -import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.database.JpaPagingItemReader; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto.TableCombineDto; - -import jakarta.persistence.EntityManagerFactory; -import lombok.Getter; - -@Configuration -public class CombineStepConfig { - @Getter - private final String STEP_NAME = "drugCombineStep"; - private final String READER_NAME = "drugDetailReader"; - private final int PAGE_SIZE = 1000; - - @Bean - public Step tableCombineStep( - JobRepository jobRepository, - PlatformTransactionManager transactionManager, - ItemReader reader, - ItemProcessor processor, - ItemWriter writer) { - return new StepBuilder(STEP_NAME, jobRepository) - .chunk(PAGE_SIZE, transactionManager) - .reader(reader) - .processor(processor) - .writer(writer) - .faultTolerant() - .retry(Exception.class) - .retryLimit(3) - .skip(Exception.class) - .skipLimit(Integer.MAX_VALUE) - .build(); - } - - @Bean - @StepScope - public JpaPagingItemReader drugDetailReader(EntityManagerFactory entityManagerFactory) { - JpaPagingItemReader reader = new JpaPagingItemReader<>(); - reader.setEntityManagerFactory(entityManagerFactory); - reader.setQueryString(getJoinTableSql()); - reader.setPageSize(PAGE_SIZE); - reader.setSaveState(true); - reader.setName(READER_NAME); - return reader; - } - - - private String getJoinTableSql(){ - return """ - SELECT new com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.TableCombineEntity( - d.drugId, - d.drugName, - d.company, - d.permitDate, - d.isGeneral, - d.materialInfo, - d.storeMethod, - d.validTerm, - d.efficacy, - d.usage, - d.precaution, - d.cancelDate, - d.cancelName, - d.isHerbal, - i.productImage, - i.pillImage - ) - FROM DrugDetailEntity d - LEFT JOIN ApiDataDrugImgEntity i ON d.drugId = i.drugId - """; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java deleted file mode 100644 index 696f9f9..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/dto/TableCombineDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto; - -import java.time.LocalDate; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class TableCombineDto { - private Long drugId; - private String drugName; - private String company; - private LocalDate permitDate; - private boolean isGeneral; - private String materialInfo; - private String storeMethod; - private String validTerm; - private String efficacy; - private String usage; - private String precaution; - private LocalDate cancelDate; - private String cancelName; - private Boolean isHerbal; - private String productImage; - private String pillImage; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java deleted file mode 100644 index 681c1c1..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/processor/TableCombineProcessor.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.processor; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto.TableCombineDto; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class TableCombineProcessor implements ItemProcessor { - - @Override - public DrugRawDataEntity process(TableCombineDto entity) throws Exception { - String imgUrl = null; - - String productImage = entity.getProductImage(); - if (productImage != null && entity.getProductImage().length() > 10) { - imgUrl = entity.getProductImage(); - } else { - imgUrl = entity.getPillImage(); - } - - return DrugRawDataEntity.builder() - .drugId(entity.getDrugId()) - .drugName(entity.getDrugName()) - .company(entity.getCompany()) - .permitDate(entity.getPermitDate()) - .isGeneral(entity.isGeneral()) - .materialInfo( - toStringFromObj( - convertMaterialInfo(entity.getMaterialInfo()) - ) - ) - .storeMethod(entity.getStoreMethod()) - .validTerm(entity.getValidTerm()) - .efficacy( - toStringFromObj( - convertEfficacy(entity.getEfficacy()) - ) - ) - .usage(toStringFromObj( - getUsage(entity.getUsage()) - ) - ) - .precaution(toStringFromObj( - getPrecaution(entity.getPrecaution()) - ) - ) - .imageUrl(imgUrl) - .cancelDate(entity.getCancelDate()) - .cancelName(entity.getCancelName()) - .isHerbal(entity.getIsHerbal()) - .build(); - } - - private static List getUsage(String usage) { - List usages = new ArrayList<>(); - JsonNode json = toJsonNodeFromString(usage); - if (!json.isNull() && json.has("sections")) { - for (JsonNode section : json.get("sections")) { - for (JsonNode article : section.get("articles")) { - for (JsonNode paragraph : article.get("paragraphs")) { - usages.add(paragraph.get("text").asText()); - } - } - } - } - return usages; - } - - private static List convertMaterialInfo(String material) { - JsonNode json = toJsonNodeFromString(material); - if (json.isArray()) { - return mapFromMaterialJson(json); - } - return null; - } - - private static List mapFromMaterialJson(JsonNode json) { - List matrerials = new ArrayList<>(); - - ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - try { - for (JsonNode node : json) { - Material ingredient = objectMapper.treeToValue(node, Material.class); - matrerials.add(ingredient); - } - return matrerials; - } catch (Exception e) { - log(LogLevel.ERROR, "객체 맵핑 실패", e); - return null; - } - } - - private static JsonNode toJsonNodeFromString(String json) { - ObjectMapper objectMapper = new ObjectMapper(); - try { - return objectMapper.readTree(json); - } catch (Exception e) { - log(LogLevel.ERROR, "json 객체 생성 에러", e); - return null; - } - } - - private static List convertEfficacy(String efficacy) { - JsonNode json = toJsonNodeFromString(efficacy); - List efficacys = new ArrayList<>(); - tryParseParagraphs(json, efficacys); - - if (efficacys.size() == 0) { - tryParseTitle(json, efficacys); - } - return efficacys; - } - - private static List tryParseTitle(JsonNode json, List efficacys) { - if (json.has("sections")) { - for (JsonNode section : json.get("sections")) { - for (JsonNode article : section.get("articles")) { - efficacys.add(article.get("title").asText()); - } - } - } - - return efficacys; - } - - private static List tryParseParagraphs(JsonNode json, List efficacys) { - if (json.has("sections")) { - for (JsonNode section : json.get("sections")) { - for (JsonNode article : section.get("articles")) { - for (JsonNode paragraph : article.get("paragraphs")) { - String text = paragraph.get("text").asText(); - if (text != null && !text.isEmpty()) { - efficacys.add(paragraph.get("text").asText()); - } - - } - } - } - } - - return efficacys; - } - - private static Map> getPrecaution(String precaution) { - Map> result = new LinkedHashMap<>(); - - JsonNode json = toJsonNodeFromString(precaution); - if (json.has("sections")) { - JsonNode articles = json.get("sections").get(0).get("articles"); - for (JsonNode article : articles) { - String title = article.get("title").asText(); - List texts = new ArrayList<>(); - for (JsonNode paragraph : article.get("paragraphs")) { - texts.add(paragraph.get("text").asText()); - } - result.put(title, texts); - } - } - - return result; - } - - private static String toStringFromObj(Object obj) { - try { - return new ObjectMapper().writeValueAsString(obj); - } catch (Exception e) { - e.printStackTrace(); - System.out.println("변환 실패"); - return null; - } - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java deleted file mode 100644 index 17a3d28..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/combine/writer/TableCombineWriter.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.writer; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; - -import lombok.RequiredArgsConstructor; - -@Component -@StepScope -@RequiredArgsConstructor -public class TableCombineWriter implements ItemWriter { - private final GovDrugJpaRepository drugRawDataRepository; - private AtomicInteger count = new AtomicInteger(); - - @Override - public void write(Chunk entity) { - List items = new ArrayList<>(entity.getItems()); - drugRawDataRepository.saveAll(items); - log("테이블 병합 작업 - 쓰기 완료: " + count.addAndGet(items.size())); - - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java deleted file mode 100644 index 38954a0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/BatchConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MdcTaskDecorator; - -import lombok.RequiredArgsConstructor; - -/** - * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 - */ -@Configuration -@RequiredArgsConstructor -public class BatchConfig { - private final MdcTaskDecorator mdcTaskDecorator; - - /** - * 병렬 처리를 위한 ThreadPool 기반 TaskExecutor 설정 - * - * @return TaskExecutor 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean("batchExecutor") - public TaskExecutor taskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(10); - executor.setTaskDecorator(mdcTaskDecorator); - executor.setThreadNamePrefix("batch-task-"); - executor.initialize(); - return executor; - } - - @Bean("batchExecutorManyThread") - public TaskExecutor taskExecutorMoreThreads() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(20); - executor.setTaskDecorator(mdcTaskDecorator); - executor.setThreadNamePrefix("batch-task(m)-"); - executor.initialize(); - return executor; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java deleted file mode 100644 index 95dda10..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/config/CommonJobConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.config; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import lombok.RequiredArgsConstructor; - -@Configuration -@RequiredArgsConstructor -public class CommonJobConfig { - /** - * 의약품 정보를 수집하는 Batch Job 정의 - * - * @param jobRepository JobRepository 인스턴스 - * @param totalPageCheckStep 전체 페이지 수 확인 Step - * @param drugDetailStep 상세 정보 수집 Step - * @return 구성된 Job 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public Job drugScrapJob(JobRepository jobRepository, - Step totalPageCheckStep, - Step drugDetailStep, - Step imageTotalPageCheckStep, - Step imageMasterStep, - Step switchModelStepToGpt, - Step gptEmbedStep, - Step switchModelStepToKmbert, - Step kmbertEmbedStep, - Step switchModelStepToKrsbert, - Step krsbertEmbedStep) { - return new JobBuilder("drugScrapJob", jobRepository) - .start(totalPageCheckStep) - .next(drugDetailStep) - .next(imageTotalPageCheckStep) - .next(imageMasterStep) - .next(switchModelStepToGpt) - .next(gptEmbedStep) - .next(switchModelStepToKmbert) - .next(kmbertEmbedStep) - .next(switchModelStepToKrsbert) - .next(krsbertEmbedStep) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java deleted file mode 100644 index 0f8b999..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MaterialParser.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; - -/** - * 원재료 정보를 파싱하여 JSON 배열 형식의 문자열로 변환하는 유틸리티 클래스입니다. - */ -public class MaterialParser { - - /** - * 원재료 문자열을 파싱하여 JSON 배열 형태의 문자열로 변환합니다. - * - * @param raw 원재료 정보가 담긴 문자열 - * (세미콜론으로 블록 구분, 파이프로 키-값 쌍 구분) - * @return JSON 배열 형태의 문자열 - * - * @author 함예정 - * @since 2025-04-21 - */ - public static String parseMaterial(String raw) { - ObjectMapper objectMapper = new ObjectMapper(); - ArrayNode resultArray = objectMapper.createArrayNode(); - String[] blocks = splitBlock(raw); - parsingblocksAndPutArrayItem(blocks, resultArray); - String result = convertString(objectMapper, resultArray); - return result; - } - - /** - * 블록 배열을 파싱하여 JSON 배열에 항목으로 추가합니다. - * - * @param blocks 원재료 블록 배열 - * @param resultArray 결과를 저장할 JSON 배열 - */ - private static void parsingblocksAndPutArrayItem(String[] blocks, ArrayNode resultArray) { - for (String block : blocks) { - block = block.trim(); - if (block.isEmpty()) { - continue; - } - String[] pairs = splitByPipe(block); - ObjectNode item = makeItem(pairs); - resultArray.add(item); - } - } - - /** - * JSON 배열을 문자열로 변환합니다. - * - * @param objectMapper Jackson ObjectMapper 인스턴스 - * @param arrayNode 변환할 JSON 배열 - * @return JSON 문자열 - * @throws ScraperException JSON 변환 실패 시 발생 - */ - private static String convertString(ObjectMapper objectMapper, ArrayNode arrayNode) { - try { - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode); - } catch (JsonProcessingException e) { - //TODO String 변환실패 - throw new ScraperException(ScraperErrorCode.MATERIAL_PARSING_FAIL); - } - } - - /** - * 키-값 쌍 배열로부터 JSON 객체를 생성합니다. - * - * @param pairs 파이프로 구분된 키-값 쌍 배열 - * @return 생성된 JSON 객체 - */ - private static ObjectNode makeItem(String[] pairs) { - ObjectNode item = new ObjectMapper().createObjectNode(); - for (String pair : pairs) { - String[] kv = pair.split(":", 2); - String key = kv[0].trim(); - String value = ""; - if(kv.length == 2){ - value = kv[1].trim(); - } - item.put(key, value); - } - return item; - } - - /** - * 블록 내 키-값 쌍을 파이프(|) 기호로 분리합니다. - * - * @param block 블록 문자열 - * @return 키-값 쌍 배열 - */ - private static String[] splitByPipe(String block) { - - return block.split("\\|"); - } - - /** - * 원재료 정보를 세미콜론(;) 기준으로 블록으로 분리합니다. - * - * @param raw 원재료 문자열 - * @return 블록 배열 - */ - private static String[] splitBlock(String raw) { - return raw.split(";"); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java deleted file mode 100644 index a9ad910..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/MdcTaskDecorator.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; - -import java.util.Map; - -import org.slf4j.MDC; -import org.springframework.core.task.TaskDecorator; -import org.springframework.stereotype.Component; -/** - * 스레드 풀에서 실행되는 Task에 MDC(Context Map)를 전파하기 위한 TaskDecorator 구현체 - * MDC 정보를 부모 스레드에서 자식 스레드로 복사하여 로그 추적 정보를 유지하도록 한다. - * - */ -@Component -public class MdcTaskDecorator implements TaskDecorator { - - /** - * Runnable 실행 시 부모 스레드의 MDC(Context Map)를 자식 스레드로 복사하여 설정한다. - * 실행 후 MDC를 반드시 clear하여 메모리 누수를 방지한다. - * - * @param runnable 실행할 원본 Runnable - * @return MDC context를 설정한 새로운 Runnable - */ - @Override - public Runnable decorate(Runnable runnable) { - Map contextMap = MDC.getCopyOfContextMap(); - return () -> { - if (contextMap != null) { - MDC.setContextMap(contextMap); - } - try { - runnable.run(); - } finally { - MDC.clear(); - } - }; - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java deleted file mode 100644 index 5210fec..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/StepSkipDecider.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; - -import java.util.Arrays; -import java.util.List; - -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.StepExecution; - -public class StepSkipDecider { - - public static boolean shouldSkip(StepExecution stepExecution) { - JobParameters params = stepExecution.getJobParameters(); - String skipParam = params.getString("skipStepName", ""); - String stepName = stepExecution.getStepName(); - - List stepsToSkip = Arrays.stream(skipParam.split(",")) - .map(String::trim).toList(); - - return stepsToSkip.contains(stepName); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java deleted file mode 100644 index 3381310..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/common/util/XMLParser.java +++ /dev/null @@ -1,339 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util; - -import java.io.StringReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.xml.parsers.DocumentBuilderFactory; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; - -/** - * XML 문자열을 파싱하여 JSON 문자열로 변환하는 클래스입니다. - */ -public class XMLParser { - private static final ObjectMapper mapper = new ObjectMapper(); - private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - - /** - * XML 문자열을 파싱하여 JSON 문자열로 변환합니다. - * - * @param xml 변환할 XML 문자열 - * @return 변환된 JSON 문자열 - */ - public static String toJson(String xml) { - - if(isXmlNull(xml)) { - return "{\"\": \"\"}"; - } - - Document doc = parseXmlString(xml); - Element root = doc.getDocumentElement(); - - List allSections = new ArrayList<>(); - List allArticles = new ArrayList<>(); - List allParagraphs = new ArrayList<>(); - - Map sectionMap = new HashMap<>(); - Map articleMap = new HashMap<>(); - - DocTag docTag = new DocTag(root, allSections); - parseSesctions(root, allSections, sectionMap); - parseArticles(root, allArticles, articleMap, sectionMap); - parseParagraph(root, allParagraphs, articleMap); - return convertJson(docTag); - } - - /** - * DocTag 객체를 JSON 문자열로 변환합니다. - * - * @param docTag 변환할 DocTag 객체 - * @return JSON 문자열 - * @throws RuntimeException JSON 변환 실패 시 - */ - private static String convertJson(DocTag docTag) { - try { - return mapper.writeValueAsString(docTag); - //TODO: 예외처리 후 삭제 - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - /** - * XML에서 PARAGRAPH 태그를 파싱하여 ParagraphTag 리스트에 추가합니다. - * - * @param root XML 루트 엘리먼트 - * @param allParagraphs 파싱된 ParagraphTag 리스트 - * @param articleMap ARTICLE 엘리먼트와 ArticleTag 매핑 정보 - */ - private static void parseParagraph(Element root, List allParagraphs, Map articleMap) { - NodeList paraNodes = root.getElementsByTagName("PARAGRAPH"); - - if(paraNodes.getLength() != 0){ - for (int i = 0; i < paraNodes.getLength(); i++) { - Element paragraphElement = (Element) paraNodes.item(i); - ParagraphTag paragraphTag = new ParagraphTag(); - paragraphTag.tagName = cleanText(paragraphElement.getAttribute("tagName")); - paragraphTag.textIndent = cleanText(paragraphElement.getAttribute("textIndent")); - paragraphTag.marginLeft = cleanText(paragraphElement.getAttribute("marginLeft")); - paragraphTag.text = cleanText(paragraphElement.getTextContent().trim()); - - if(!isEmptytagNameOrTagText(paragraphTag)){ - allParagraphs.add(paragraphTag); - } - - mapSectionFromArticle(articleMap, paragraphTag, paragraphElement); - } - } - - } - - private static boolean isEmptytagNameOrTagText(ParagraphTag paragraphTag) { - return paragraphTag.tagName.isEmpty() || paragraphTag.text.isEmpty(); - } - - /** - * XML에서 ARTICLE 태그를 파싱하여 ArticleTag 리스트에 추가합니다. - * - * @param root XML 루트 엘리먼트 - * @param allArticles 파싱된 ArticleTag 리스트 - * @param articleMap ARTICLE 엘리먼트와 ArticleTag 매핑 정보 - * @param sectionMap SECTION 엘리먼트와 SectionTag 매핑 정보 - */ - private static void parseArticles(Element root, List allArticles, - Map articleMap, - Map sectionMap) { - NodeList artNodes = root.getElementsByTagName("ARTICLE"); - if(artNodes.getLength() > 0) { - for (int i = 0; i < artNodes.getLength(); i++) { - Element artElement = (Element) artNodes.item(i); - ArticleTag articleTag = new ArticleTag(); - articleTag.title = cleanText(artElement.getAttribute("title")); - articleTag.paragraphs = new ArrayList<>(); - - allArticles.add(articleTag); - articleMap.put(artElement, articleTag); - mapSectionFromArticle(sectionMap, articleTag, artElement); - } - } - - } - - /** - * 상위 엘리먼트를 기반으로 해당 태그를 부모 태그에 연결합니다. - * - * @param map 상위 엘리먼트와 태그 매핑 정보 - * @param tags 현재 태그 - * @param element 현재 엘리먼트 - */ - private static void mapSectionFromArticle(Map map, Tags tags, Element element) { - Element parentElement = (Element) element.getParentNode(); - Tags parentTag = map.get(parentElement); - if (parentTag != null) { - parentTag.addTag(tags); - } - } - - /** - * XML에서 SECTION 태그를 파싱하여 SectionTag 리스트에 추가합니다. - * - * @param root XML 루트 엘리먼트 - * @param allSections 파싱된 SectionTag 리스트 - * @param sectionMap SECTION 엘리먼트와 SectionTag 매핑 정보 - */ - private static void parseSesctions(Element root, List allSections, Map sectionMap) { - NodeList secNodes = root.getElementsByTagName("SECTION"); - - if(secNodes.getLength() > 0) { - for (int i = 0; i < secNodes.getLength(); i++) { - Element secEl = (Element) secNodes.item(i); - SectionTag secDto = new SectionTag(); - secDto.title = cleanText(secEl.getAttribute("title")); - secDto.articles = new ArrayList<>(); - - allSections.add(secDto); - sectionMap.put(secEl, secDto); - } - } - } - - /** - * XML 문자열을 파싱하여 Document 객체로 변환합니다. - * - * @param xml 파싱할 XML 문자열 - * @return 파싱된 Document 객체 - */ - private static Document parseXmlString(String xml) { - try { - return documentBuilderFactory.newDocumentBuilder() - .parse(new InputSource(new StringReader(xml))); - } catch (Exception e) { - throw new ScraperException(ScraperErrorCode.PARSING_ERROR); - } - } - - /** - * XML 문자열이 null 이거나 비어있는지 확인합니다. - * - * @param xml 확인할 XML 문자열 - * @return null 또는 비어있으면 true, 그렇지 않으면 false - */ - private static boolean isXmlNull(String xml) { - if (xml == null || xml.trim().isEmpty() || xml == "null") { - return true; - } else { - return false; - } - } - - /** - * XML 루트 태그를 표현하는 클래스입니다. - */ - private static class DocTag implements Tags { - public String title; - public String type; - public List sections; - - DocTag(Element root, List sections) { - this.title = cleanText(root.getAttribute("title")); - this.type = root.getAttribute("type"); - this.sections = sections; - } - - @Override - public void addTag(Tags tags) { - sections.add((SectionTag) tags); - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof DocTag; - } - } - - /** - * SECTION 태그를 표현하는 클래스입니다. - */ - private static class SectionTag implements Tags { - public String title; - public List articles; - - @Override - public void addTag(Tags tags) { - articles.add((ArticleTag)tags); - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof SectionTag; - } - } - - /** - * ARTICLE 태그를 표현하는 클래스입니다. - */ - private static class ArticleTag implements Tags { - public String title; - public List paragraphs; - - @Override - public void addTag(Tags tags) { - paragraphs.add((ParagraphTag) tags); - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof ArticleTag; - } - } - - /** - * PARAGRAPH 태그를 표현하는 클래스입니다. - */ - private static class ParagraphTag implements Tags { - public String tagName; - public String textIndent; - public String marginLeft; - public String text; - - @Override - public void addTag(Tags tags) { - //TODO: addTag Exception 하위 없음 - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof ParagraphTag; - } - } - - /** - * 태그 클래스 간 공통 인터페이스입니다. - */ - private interface Tags { - void addTag(Tags tags); - boolean equalsClass(Tags tags); - } - - private static String cleanText(String text){ - Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); - String tempText = TAG_REGEX.matcher(text) - .replaceAll("") - .replaceAll(" ", " ") - .replaceAll("● ", "") - .replaceAll("○ ", "") - .replaceAll("∎ ", "") - .replaceAll("- ", ""); - String decodeText = decodeHtml(tempText).trim(); - return decodeText; - } - /** - * HTML 엔티티(10진수 &#DDD; 및 16진수 &#xHHHH;)를 대응하는 문자로 디코딩합니다. 예: "foo•bar" → "foo•bar", - * "foo•bar" → "foo•bar" - * - * @param input 엔티티를 포함한 문자열 - * @return 디코딩된 문자열 - * - * @author 박찬병 - */ - - private static String decodeHtml(String input) { - String result = input; - Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); - Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); - // 10진수 엔티티 디코딩 - Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(result); - StringBuffer sb = new StringBuffer(); - while (decMatcher.find()) { - int code = Integer.parseInt(decMatcher.group(1)); - decMatcher.appendReplacement(sb, - Matcher.quoteReplacement(Character.toString((char) code))); - } - decMatcher.appendTail(sb); - result = sb.toString(); - - // 16진수 엔티티 디코딩 - Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(result); - sb = new StringBuffer(); - while (hexMatcher.find()) { - int code = Integer.parseInt(hexMatcher.group(1), 16); - hexMatcher.appendReplacement(sb, - Matcher.quoteReplacement(Character.toString((char) code))); - } - hexMatcher.appendTail(sb); - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java deleted file mode 100644 index 2c3400a..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailJobConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.config; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import lombok.RequiredArgsConstructor; - -@Configuration -@RequiredArgsConstructor -public class DetailJobConfig { - @Bean - public Job drugDetailScrapJob(JobRepository jobRepository, - Step totalPageCheckStep, - Step drugDetailStep) { - return new JobBuilder("drugDetailScrapJob", jobRepository) - .start(totalPageCheckStep) - .next(drugDetailStep) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java deleted file mode 100644 index d1c8a9a..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/config/DetailStepConfig.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.config; - -import java.util.List; - -import org.springframework.batch.core.Step; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; -import org.springframework.transaction.PlatformTransactionManager; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MdcTaskDecorator; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor.DetailTotalPageCalculator; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor.DrugDetailProcessor; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.reader.DetailPageNumberReader; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.writer.DrugDetailWriter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; - -/** - * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 - */ -@Configuration -public class DetailStepConfig { - private final DetailPageNumberReader detailPageNumberReader; - private final ApiRequestManager apiRequestManager; - private final ApiResponseMapper apiResponseMapper; - private final TaskExecutor taskExecutor; - - public DetailStepConfig(DetailPageNumberReader detailPageNumberReader, - ApiRequestManager apiRequestManager, - MdcTaskDecorator mdcTaskDecorator, - ApiResponseMapper apiResponseMapper, - @Qualifier("batchExecutor") - TaskExecutor taskExecutor) { - this.detailPageNumberReader = detailPageNumberReader; - this.apiRequestManager = apiRequestManager; - this.apiResponseMapper = apiResponseMapper; - this.taskExecutor = taskExecutor; - } - - /** - * 전체 페이지 수를 계산하는 Tasklet 기반 Step 정의 - * - * @param jobRepository JobRepository 인스턴스 - * @param txManager 트랜잭션 매니저 - * @param detailTotalPageCalculator 페이지 계산 로직을 수행하는 Tasklet - * @return Step 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - Step totalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, - DetailTotalPageCalculator detailTotalPageCalculator) { - return new StepBuilder("totalPageCheck", jobRepository) - .tasklet(detailTotalPageCalculator, txManager) - .build(); - } - - /** - * 상세 페이지별로 데이터를 수집하고 처리 및 저장하는 Chunk 기반 Step 정의 - * - * @param jobRepository JobRepository 인스턴스 - * @param txManager 트랜잭션 매니저 - * @param processor 데이터를 처리하는 Processor - * @param writer 데이터를 저장하는 Writer - * @return Step 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public Step drugDetailStep(JobRepository jobRepository, - PlatformTransactionManager txManager, - DrugDetailProcessor processor, - DrugDetailWriter writer) { - return new StepBuilder("drugDetailStep", jobRepository) - .>chunk(1, txManager) - .reader(detailPageNumberReader) - .processor(processor) - .writer(writer) - .faultTolerant() - .retry(Exception.class) - .retryLimit(3) - .skip(Exception.class) - .skipLimit(Integer.MAX_VALUE) - .taskExecutor(taskExecutor) - .build(); - } - - /** - * 의약품 상세정보 처리용 Processor Bean 정의 - * - * @return CombineProcessor 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public DrugDetailProcessor processor() { - return new DrugDetailProcessor(apiRequestManager, apiResponseMapper); - } - - /** - * 처리된 의약품 상세정보를 DB에 저장하는 Writer Bean 정의 - * - * @param repository 의약품 상세정보 저장용 JPA Repository - * @return DrugDetailWriter 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public DrugDetailWriter writer(GovDrugDetailJpaRepository repository) { - return new DrugDetailWriter(repository); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java deleted file mode 100644 index c26ad83..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/dto/DrugDetailRequest.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto; - -import java.time.LocalDate; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Getter; -import lombok.ToString; - -@Getter -@ToString -public class DrugDetailRequest { - - @JsonProperty("ITEM_SEQ") - private Long drugId; - - @JsonProperty("ITEM_NAME") - private String drugName; - - @JsonProperty("ENTP_NAME") - private String company; - - @JsonProperty("ITEM_PERMIT_DATE") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") - private LocalDate permitDate; - - @JsonProperty("CANCEL_DATE") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") - private LocalDate cancelDate; - - @JsonProperty("CANCEL_NAME") - private String cancelName; - - private boolean isGeneral; - - private boolean isHerbal; - - private String materialInfo; - - @JsonProperty("STORAGE_METHOD") - private String storeMethod; - - @JsonProperty("VALID_TERM") - private String validTerm; - - private String efficacy; - private String usage; - private String precaution; - - @JsonCreator - public DrugDetailRequest(@JsonProperty("ETC_OTC_CODE") String drugType) { - this.isGeneral = !"전문의약품".equals(drugType); - } - - public void changeMaterialInfo(String materialInfo){ - this.materialInfo = materialInfo; - } - - public void changeUsage(String usage) { - this.usage = usage; - } - - public void changeEfficacy(String efficacy) { - this.efficacy = efficacy; - } - - public void changePrecaution(String precaution) { - this.precaution = precaution; - } - - public void changeIsHerbal(boolean isHerbal) { - this.isHerbal = isHerbal; - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java deleted file mode 100644 index a6be79a..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DetailTotalPageCalculator.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.reader.DetailPageNumberReader; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class DetailTotalPageCalculator implements Tasklet { - - private final DetailPageNumberReader detailPageNumberReader; - private final ApiRequestManager apiRequestManager; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { - int totalPage = apiRequestManager.getDetailTotalPage(); - detailPageNumberReader.setTotalPage(totalPage); - - LogUtil.log("[CombineProcessor] 총 페이지 수 계산 완료: " + totalPage); - return RepeatStatus.FINISHED; - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java deleted file mode 100644 index 4d36f1d..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/processor/DrugDetailProcessor.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.processor; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.List; - -import org.springframework.batch.item.ItemProcessor; - -import com.fasterxml.jackson.databind.JsonNode; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MaterialParser; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.XMLParser; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto.DrugDetailRequest; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.DrugDetailRequestMapper; - -/** - * pageNumber를 받아 외부 REST API 호출 → JSON → DTO 리스트 변환 → - * Entity 리스트로 매핑 - */ - -public class DrugDetailProcessor implements ItemProcessor> { - - private final ApiRequestManager apiRequestManager; - private final ApiResponseMapper apiResponseMapper; - - public DrugDetailProcessor(ApiRequestManager apiRequestManager, - ApiResponseMapper apiResponseMapper) { - this.apiRequestManager = apiRequestManager; - this.apiResponseMapper = apiResponseMapper; - } - - @Override - public List process(Integer pageNumber) throws Exception { - String response = apiRequestManager.fetchDetailData(pageNumber); - JsonNode items = apiRequestManager.getItemsFromResponse(response); - List drugItems = apiResponseMapper.toListFromDrugDetails(items); - - for (int i = 0; i < drugItems.size(); i++) { - DrugDetailRequest drugDetail = drugItems.get(i); - JsonNode item = items.get(i); - - String materialRawData = item.get("MATERIAL_NAME").asText(); - String materialInfo = MaterialParser.parseMaterial(materialRawData); - - drugDetail.changeMaterialInfo(materialInfo); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 성분 저장 완료: \n" + drugDetail); - - log(LogLevel.DEBUG, "약품 효능 데이터 파싱 시작"); - String efficacyXmlText = item.get("EE_DOC_DATA").asText(); - log(LogLevel.DEBUG, "약품 효능 Raw 데이터 조회 성공: \n" + efficacyXmlText); - - String efficacy = XMLParser.toJson(efficacyXmlText); - log(LogLevel.DEBUG, "약품 효능 파싱 성공: \n" + efficacy); - - drugDetail.changeEfficacy(efficacy); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 효능 저장 완료: \n" + drugDetail); - - log(LogLevel.DEBUG, "약품 사용법 데이터 파싱 시작"); - String usageXmlText = item.get("UD_DOC_DATA").asText(); - log(LogLevel.DEBUG, "약품 사용법 Raw 데이터 조회 성공: \n" + usageXmlText); - - String usages = XMLParser.toJson(usageXmlText); - log(LogLevel.DEBUG, "약품 사용법 파싱 성공: \n" + usages); - - drugDetail.changeUsage(usages); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 사용법 저장 완료: \n" + drugDetail); - - log(LogLevel.DEBUG, "약품 주의사항 데이터 파싱 시작"); - String precautionxmlText = item.get("NB_DOC_DATA").asText(); - log(LogLevel.DEBUG, "약품 주의사항 Raw 데이터 조회 성공: \n" + precautionxmlText); - - String precautions = XMLParser.toJson(precautionxmlText); - log(LogLevel.DEBUG, "약품 주의사항 파싱 성공: \n" + precautions); - - drugDetail.changePrecaution(precautions); - log(LogLevel.DEBUG, "drugDetail 객체에 약품 주의사항 저장 완료: \n" + drugDetail); - - String precaution = drugDetail.getPrecaution(); - if (precaution != null && (precaution.contains("한의사") || precaution.contains("한약사"))) { - drugDetail.changeIsHerbal(true); - } - } - return drugItems.stream() - .map(DrugDetailRequestMapper::toEntityFromRequest) - .toList(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java deleted file mode 100644 index 34e25eb..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/reader/DetailPageNumberReader.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.reader; - -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.batch.item.ItemReader; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; - -import lombok.Getter; -import lombok.Setter; - -/** - * Spring Batch에서 각 Step 실행 시 처리할 페이지 번호를 순차적으로 제공하는 Reader 클래스 - * - * 총 페이지 수를 기준으로 1부터 시작하여 차례대로 page 번호를 반환하며, - * 모든 페이지가 반환되면 null을 반환하여 반복을 종료한다. - * - * @author 함예정 - */ - -@Component -@Setter -@Getter -public class DetailPageNumberReader implements ItemReader { - - private static final Queue pageQueue = new ConcurrentLinkedQueue<>(); - - public static void setTotalPage(int totalPage) { - pageQueue.clear(); - for (int i = 1; i <= totalPage; i++) { - pageQueue.add(i); - } - } - /** - * 현재 페이지 번호를 반환하고, 다음 호출을 위해 내부 카운터를 증가시킨다. - * 총 페이지 수를 초과하면 null을 반환하여 종료를 알린다. - * - * @return 현재 처리할 페이지 번호 또는 null(모든 페이지 처리 완료 시) - */ - @Override - public Integer read() { - Integer page = pageQueue.poll(); - if (page != null) { - LogUtil.log(Thread.currentThread().getName() + " - Page 할당: " + page); - } - return page; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java deleted file mode 100644 index 057e9fe..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/detail/writer/DrugDetailWriter.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.writer; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; - -import java.util.List; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; - -/** - * Entity 리스트를 받아 JPA Repository로 한 번에 저장 - */ -public class DrugDetailWriter implements ItemWriter> { - - private final GovDrugDetailJpaRepository repository; - - public DrugDetailWriter(GovDrugDetailJpaRepository repository) { - this.repository = repository; - } - - @Override - public void write(Chunk> chunk) throws Exception { - - for (List items : chunk.getItems()) { - repository.saveAll(items); - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java deleted file mode 100644 index b69a863..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedJobConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.config; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; - -@Configuration -public class EmbedJobConfig { - @Bean - public Job embedJob(JobRepository jobRepository, - Step switchModelStepToGpt, - Step gptEmbedStep, - Step switchModelStepToKmbert, - Step kmbertEmbedStep, - Step switchModelStepToKrsbert, - Step krsbertEmbedStep){ - return new JobBuilder("embedJob", jobRepository) - .start(switchModelStepToGpt) - .next(gptEmbedStep) - .next(switchModelStepToKmbert) - .next(kmbertEmbedStep) - .next(switchModelStepToKrsbert) - .next(krsbertEmbedStep) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java deleted file mode 100644 index d4434b6..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/config/EmbedStepConfig.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.config; - -import java.util.Map; - -import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.database.JpaPagingItemReader; -import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; -import org.springframework.transaction.PlatformTransactionManager; - -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.reader.DrugIdRangePartitioner; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; - -import jakarta.persistence.EntityManagerFactory; - -@Configuration -public class EmbedStepConfig { - private final String READER_NAME = "rawDataReader"; - private final int PAGE_SIZE = 50; - private final DrugIdRangePartitioner drugIdRangePartitioner; - private final TaskExecutor taskExecutor; - - public EmbedStepConfig(DrugIdRangePartitioner drugIdRangePartitioner, - @Qualifier("batchExecutorManyThread") - TaskExecutor taskExecutor){ - this.drugIdRangePartitioner = drugIdRangePartitioner; - this.taskExecutor = taskExecutor; - } - - @Bean - public Step switchModelStepToGpt(JobRepository jobRepository, - PlatformTransactionManager tx, - EmbeddingSwitchPort switchPort) { - - Tasklet tasklet = (contribution, chunkContext) -> { - switchPort.switchTo("gptEmbeddingLoadingAdapter"); - return RepeatStatus.FINISHED; - }; - - return new StepBuilder("switchModelStepToGpt", jobRepository) - .tasklet(tasklet, tx) - .build(); - } - - @Bean - public Step switchModelStepToKmbert(JobRepository jobRepository, - PlatformTransactionManager tx, - EmbeddingSwitchPort switchPort) { - - Tasklet tasklet = (contribution, chunkContext) -> { - switchPort.switchTo("kmBertEmbeddingLoadingAdapter"); - return RepeatStatus.FINISHED; - }; - - return new StepBuilder("switchModelStepToKmbert", jobRepository) - .tasklet(tasklet, tx) - .build(); - } - - @Bean - public Step switchModelStepToKrsbert(JobRepository jobRepository, - PlatformTransactionManager tx, - EmbeddingSwitchPort switchPort) { - - Tasklet tasklet = (contribution, chunkContext) -> { - switchPort.switchTo("krSBertEmbeddingLoadingAdapter"); - return RepeatStatus.FINISHED; - }; - - return new StepBuilder("switchModelStepToKrsbert", jobRepository) - .tasklet(tasklet, tx) - .build(); - } - - @Bean - public Step gptEmbedSlaveStep( - JobRepository jobRepository, - PlatformTransactionManager transactionManager, - ItemReader reader, - ItemProcessor processor, - ItemWriter writer) { - - return new StepBuilder("gptEmbedSlaveStep", jobRepository) - .chunk(100, transactionManager) - .reader(reader) - .processor(processor) - .writer(writer) - .faultTolerant() - .retry(Exception.class) - .retryLimit(3) - .skip(Exception.class) - .skipLimit(Integer.MAX_VALUE) - .build(); - } - - @Bean - public Step gptEmbedStep( - JobRepository jobRepository, - Step gptEmbedSlaveStep) { - - return new StepBuilder("gptEmbedStep", jobRepository) - .partitioner(gptEmbedSlaveStep.getName(), drugIdRangePartitioner) - .step(gptEmbedSlaveStep) - .taskExecutor(taskExecutor) - .gridSize(10) - .build(); - } - - - @Bean - public Step kmbertEmbedSlaveStep( - JobRepository jobRepository, - PlatformTransactionManager transactionManager, - ItemReader reader, - ItemProcessor processor, - ItemWriter writer) { - - return new StepBuilder("kmbertEmbedSlaveStep", jobRepository) - .chunk(100, transactionManager) - .reader(reader) - .processor(processor) - .writer(writer) - .faultTolerant() - .retry(Exception.class) - .retryLimit(3) - .skip(Exception.class) - .skipLimit(Integer.MAX_VALUE) - .build(); - } - - @Bean - public Step kmbertEmbedStep( - JobRepository jobRepository, - Step kmbertEmbedSlaveStep) { - - return new StepBuilder("kmbertEmbedStep", jobRepository) - .partitioner(kmbertEmbedSlaveStep.getName(), drugIdRangePartitioner) - .step(kmbertEmbedSlaveStep) - .taskExecutor(taskExecutor) - .gridSize(10) - .build(); - } - - @Bean - public Step krsbertEmbedSlaveStep( - JobRepository jobRepository, - PlatformTransactionManager transactionManager, - ItemReader reader, - ItemProcessor processor, - ItemWriter writer) { - - return new StepBuilder("krsbertEmbedSlaveStep", jobRepository) - .chunk(100, transactionManager) - .reader(reader) - .processor(processor) - .writer(writer) - .faultTolerant() - .retry(Exception.class) - .retryLimit(3) - .skip(Exception.class) - .skipLimit(Integer.MAX_VALUE) - .build(); - } - - @Bean - public Step krsbertEmbedStep( - JobRepository jobRepository, - Step krsbertEmbedSlaveStep) { - return new StepBuilder("krsbertEmbedStep", jobRepository) - .partitioner(krsbertEmbedSlaveStep.getName(), drugIdRangePartitioner) - .step(krsbertEmbedSlaveStep) - .taskExecutor(taskExecutor) - .gridSize(10) - .build(); - } - @Bean - @StepScope - public JpaPagingItemReader embedItemReader( - @Value("#{stepExecutionContext['minId']}") Long minId, - @Value("#{stepExecutionContext['maxId']}") Long maxId, - EntityManagerFactory emf - ) { - LogUtil.log(Thread.currentThread().getName() + "Item Read: " + minId + "~" + maxId); - return new JpaPagingItemReaderBuilder() - .name("partitionedReader") - .entityManagerFactory(emf) - .queryString("SELECT d FROM DrugRawDataEntity d WHERE d.drugId BETWEEN :minId AND :maxId ORDER BY d.drugId") - .parameterValues(Map.of("minId", minId, "maxId", maxId)) - .pageSize(100) - .saveState(false) - .build(); - } - - - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java deleted file mode 100644 index 3f985ac..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/dto/DrugVectorDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DrugVectorDto { - private Long drugId; - private float[] vector; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java deleted file mode 100644 index 27dc997..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/processor/EmbedProcessor.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.processor; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.combine.dto.TableCombineDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.DrugMapper; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class EmbedProcessor implements ItemProcessor { - private final EmbeddingLoadingPort embeddingLoadingPort; - - @Override - public DrugVectorDto process(DrugRawDataEntity item) throws Exception { - Long id = item.getDrugId(); - String embeddingText = getEmbedTextFromItem(item); - float[] embeddingVector = embeddingLoadingPort.getEmbedding(embeddingText); - return DrugVectorDto - .builder() - .drugId(id) - .vector(embeddingVector) - .build(); - } - - private String getEmbedTextFromItem(DrugRawDataEntity item){ - return DrugMapper.convertSingleStringForEfficacy( - DrugMapper.parseStringToList(item.getEfficacy()) - ); - } - - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java deleted file mode 100644 index 62f0444..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/reader/DrugIdRangePartitioner.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.reader; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.batch.core.partition.support.Partitioner; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; - -@Component -public class DrugIdRangePartitioner implements Partitioner { - - private final GovDrugJpaRepository repository; - - public DrugIdRangePartitioner(GovDrugJpaRepository repository) { - this.repository = repository; - } - - @Override - public Map partition(int gridSize) { - Long minId = repository.findMinDrugId(); - Long maxId = repository.findMaxDrugId(); - - long targetSize = (maxId - minId) / gridSize + 1; - - Map result = new HashMap<>(); - long start = minId; - long end = start + targetSize - 1; - - for (int i = 0; i < gridSize; i++) { - ExecutionContext context = new ExecutionContext(); - context.putLong("minId", start); - context.putLong("maxId", Math.min(end, maxId)); - result.put("partition" + i, context); - start += targetSize; - end += targetSize; - } - return result; - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java deleted file mode 100644 index e976ff0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/embed/writer/EmbedWriter.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.writer; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; - -import lombok.RequiredArgsConstructor; - -@Component -@StepScope -@RequiredArgsConstructor -public class EmbedWriter implements ItemWriter { - private final EmbeddingLoadingPort embeddingLoadingPort; - private AtomicInteger count = new AtomicInteger(); - - @Override - public void write(Chunk dto) throws Exception { - List items = new ArrayList<>(dto.getItems()); - embeddingLoadingPort.saveEmbedding(items); - log("임베딩 작업 - 쓰기 완료: " + count.addAndGet(items.size())); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java deleted file mode 100644 index db902c9..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchError.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception; - -import org.springframework.http.HttpStatus; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public enum ParserBatchError implements ErrorCode { - ALREADY_RUN(HttpStatus.CONFLICT, 400001, "이미 실행 중인 배치가 있습니다."), - JOB_RUN_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 500000, "JOB 실행 요청은 정상적으로 도달했으나 실행에 실패했습니다."), - JSON_TYPE_CHANGE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 500001, "JSON을 자바 타입으로 변환하는데 실패했습니다."); - private final HttpStatus httpStatus; - private final int code; - private final String message; - - @Override - public HttpStatus httpStatus() { - return httpStatus; - } - - @Override - public int codeNumber() { - return code; - } - - @Override - public String message() { - return message; - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java deleted file mode 100644 index d72a6e9..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageJobConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.config; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ImageJobConfig { - @Bean - public Job drugImageScrapJob(JobRepository jobRepository, - Step imageTotalPageCheckStep, - Step imageMasterStep) { - return new JobBuilder("drugImageScrapJob", jobRepository) - .start(imageTotalPageCheckStep) - .next(imageMasterStep) - .build(); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java deleted file mode 100644 index 23fcee7..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/config/ImageStepConfig.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.config; - -import java.util.List; - -import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; -import org.springframework.transaction.PlatformTransactionManager; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.common.util.MdcTaskDecorator; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor.ImageScrapProcessor; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor.ImageTotalPageCalculator; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader.PageRangePartitioner; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader.PartitionedPageReader; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.writer.DrugImageWriter; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.ApiDataDrugImgRepo; - -/** - * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 - */ -@Configuration -public class ImageStepConfig { - private final ApiRequestManager apiRequestManager; - private final ApiResponseMapper apiResponseMapper; - private final TaskExecutor taskExecutor; - - public ImageStepConfig(ApiRequestManager apiRequestManager, - ApiResponseMapper apiResponseMapper, - @Qualifier("batchExecutorManyThread") - TaskExecutor taskExecutor) { - this.apiRequestManager = apiRequestManager; - this.apiResponseMapper = apiResponseMapper; - this.taskExecutor = taskExecutor; - } - - - - /** - * 전체 페이지 수를 계산하는 Tasklet 기반 Step 정의 - * - * @param jobRepository JobRepository 인스턴스 - * @param txManager 트랜잭션 매니저 - * @param imageTotalPageCalculator 페이지 계산 로직을 수행하는 Tasklet - * @return Step 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - Step imageTotalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, - ImageTotalPageCalculator imageTotalPageCalculator) { - return new StepBuilder("imageTotalPageCheck", jobRepository) - .tasklet(imageTotalPageCalculator, txManager) - .build(); - } - - @Bean - public Step imageMasterStep(JobRepository jobRepository, - PageRangePartitioner partitioner, - Step imageScrapStep) { - return new StepBuilder("imageMasterStep", jobRepository) - .partitioner(imageScrapStep.getName(), partitioner) - .step(imageScrapStep) - .gridSize(15) - .taskExecutor(taskExecutor) - .build(); - } - - /** - * 상세 페이지별로 데이터를 수집하고 처리 및 저장하는 Chunk 기반 Step 정의 - * - * @param jobRepository JobRepository 인스턴스 - * @param txManager 트랜잭션 매니저 - * @param processor 데이터를 처리하는 Processor - * @param writer 데이터를 저장하는 Writer - * @return Step 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public Step imageScrapStep(JobRepository jobRepository, - PlatformTransactionManager txManager, - ImageScrapProcessor processor, - DrugImageWriter writer, - PartitionedPageReader reader) { - return new StepBuilder("imageScrapStep", jobRepository) - .>chunk(1, txManager) - .reader(reader) - .processor(processor) - .writer(writer) - .faultTolerant() - .retry(Exception.class) - .retryLimit(3) - .skip(Exception.class) - .skipLimit(Integer.MAX_VALUE) - .build(); - } - - /** - * 의약품 상세정보 처리용 Processor Bean 정의 - * - * @return CombineProcessor 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public ImageScrapProcessor imageScrapProcessor() { - return new ImageScrapProcessor(apiRequestManager, apiResponseMapper); - } - - /** - * 처리된 의약품 상세정보를 DB에 저장하는 Writer Bean 정의 - * - * @param repository 의약품 상세정보 저장용 JPA Repository - * @return DrugDetailWriter 인스턴스 - * - * @author 함예정 - * @since 2025-05-01 - */ - @Bean - public DrugImageWriter imageScrapWriter(ApiDataDrugImgRepo repository) { - return new DrugImageWriter(repository); - } - - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java deleted file mode 100644 index 106bf4d..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageScrapProcessor.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor; - -import java.util.List; - -import org.springframework.batch.item.ItemProcessor; - -import com.fasterxml.jackson.databind.JsonNode; -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper.ApiResponseMapper; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugImageRequest; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugImageRequestMapper; - -/** - * pageNumber를 받아 외부 REST API 호출 → JSON → DTO 리스트 변환 → - * Entity 리스트로 매핑 - */ -public class ImageScrapProcessor implements ItemProcessor> { - - private final ApiRequestManager apiRequestManager; - private final ApiResponseMapper apiResponseMapper; - - public ImageScrapProcessor(ApiRequestManager apiRequestManager, - ApiResponseMapper apiResponseMapper) { - this.apiRequestManager = apiRequestManager; - this.apiResponseMapper = apiResponseMapper; - } - - @Override - public List process(Integer pageNumber) throws Exception { - LogUtil.log(Thread.currentThread().getName() + " - " + pageNumber + " page 처리 시작"); - String response = apiRequestManager.fetchImageData(pageNumber); - JsonNode items = apiRequestManager.getItemsFromResponse(response); - List drugItems = apiResponseMapper.toListFromDrugImages(items); - - for (int i = 0; i < drugItems.size(); i++) { - DrugImageRequest item = drugItems.get(i); - String productImage = apiRequestManager.getImage(item.getDrugId()); - if (productImage != null && productImage.length() > 10) { - item.changeProductImageUrl(productImage); - } - } - - return drugItems.stream() - .map(DrugImageRequestMapper::toEntityFromRequest) - .toList(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java deleted file mode 100644 index ac797e8..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/processor/ImageTotalPageCalculator.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.processor; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.ApiRequestManager; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader.PageRangePartitioner; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class ImageTotalPageCalculator implements Tasklet { - - private final PageRangePartitioner pageRangePartitioner; - private final ApiRequestManager apiRequestManager; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { - int totalPage = apiRequestManager.getImageTotalPage(); - pageRangePartitioner.setTotalPages(totalPage); - - log("[Image-Total-Page-Calculator] 총 페이지 수 계산 완료: " + totalPage); - return RepeatStatus.FINISHED; - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java deleted file mode 100644 index 273d539..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/ImagePageNumberReader.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader; - -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; - -import lombok.Getter; -import lombok.Setter; - -/** - * Spring Batch에서 각 Step 실행 시 처리할 페이지 번호를 순차적으로 제공하는 Reader 클래스 - * - * 총 페이지 수를 기준으로 1부터 시작하여 차례대로 page 번호를 반환하며, - * 모든 페이지가 반환되면 null을 반환하여 반복을 종료한다. - * - * @author 함예정 - */ - -@Component -@StepScope -@Setter -@Getter -public class ImagePageNumberReader implements ItemReader { - - private static final Queue pageQueue = new ConcurrentLinkedQueue<>(); - - public static void setTotalPage(int totalPage) { - pageQueue.clear(); - for (int i = 1; i <= totalPage; i++) { - pageQueue.add(i); - } - } - /** - * 현재 페이지 번호를 반환하고, 다음 호출을 위해 내부 카운터를 증가시킨다. - * 총 페이지 수를 초과하면 null을 반환하여 종료를 알린다. - * - * @return 현재 처리할 페이지 번호 또는 null(모든 페이지 처리 완료 시) - */ - @Override - public Integer read() { - Integer page = pageQueue.poll(); - if (page != null) { - LogUtil.log(Thread.currentThread().getName() + " - Page 할당: " + page); - } - return page; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java deleted file mode 100644 index deaa758..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PartitionedPageReader.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; - -@Component -@StepScope -public class PartitionedPageReader implements ItemReader { - private int currentPage; - private final int endPage; - - public PartitionedPageReader( - @Value("#{stepExecutionContext['startPage']}") int startPage, - @Value("#{stepExecutionContext['endPage']}") int endPage) { - log("[Reader bean 생성] startPage=" + startPage + ", endPage=" + endPage); - this.currentPage = startPage; - this.endPage = endPage; - } - - @Override - public Integer read() { - log("이미지 페이지 Read: " + currentPage); - if (currentPage > endPage) { - return null; - } - return currentPage++; - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java deleted file mode 100644 index 3d547ab..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/writer/DrugImageWriter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.writer; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.List; - -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.ApiDataDrugImgRepo; - -/** - * Entity 리스트를 받아 JPA Repository로 한 번에 저장 - */ -public class DrugImageWriter implements ItemWriter> { - - private final ApiDataDrugImgRepo repository; - - public DrugImageWriter(ApiDataDrugImgRepo repository) { - this.repository = repository; - } - - @Override - public void write(Chunk> chunk) throws Exception { - log(Thread.currentThread().getName() + " - Start Write"); - for (List items : chunk.getItems()) { - repository.saveAllAndFlush(items); - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java deleted file mode 100644 index 85d7f01..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/ApiResponseMapper.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchError; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchException; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto.DrugDetailRequest; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugImageRequest; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class ApiResponseMapper { - private final ObjectMapper objectMapper; - public List toListFromDrugDetails(JsonNode items) { - try { - return objectMapper.readValue( - items.toString(), new TypeReference<>() {} - ); - } catch (JsonProcessingException e) { - LogUtil.log(LogLevel.ERROR, "타입변환 실패(JsonNode -> List"); - throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); - } - } - - public List toListFromDrugImages(JsonNode items) { - try { - return objectMapper.readValue( - items.toString(), new TypeReference<>() {} - ); - } catch (JsonProcessingException e) { - LogUtil.log(LogLevel.ERROR, "타입변환 실패(JsonNode -> List"); - throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java deleted file mode 100644 index b949d10..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/mapper/DrugDetailRequestMapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.mapper; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.detail.dto.DrugDetailRequest; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; - -public class DrugDetailRequestMapper { - - public static DrugDetailEntity toEntityFromRequest(DrugDetailRequest r){ - return DrugDetailEntity.builder() - .drugId(r.getDrugId()) - .drugName(r.getDrugName()) - .company(r.getCompany()) - .permitDate(r.getPermitDate()) - .isGeneral(r.isGeneral()) - .materialInfo(r.getMaterialInfo()) - .storeMethod(r.getStoreMethod()) - .validTerm(r.getValidTerm()) - .efficacy(r.getEfficacy()) - .usage(r.getUsage()) - .precaution(r.getPrecaution()) - .cancelDate(r.getCancelDate()) - .cancelName(r.getCancelName()) - .isHerbal(r.isHerbal()) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java deleted file mode 100644 index 6288567..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/EmbeddingClient.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; - -public interface EmbeddingClient { - EmbeddingModelType getModelType(); - float[] getEmbedding(String text); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java deleted file mode 100644 index 3730bcd..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KmBertEmbeddingClient.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; - -import java.net.URI; - -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class KmBertEmbeddingClient implements EmbeddingClient { - private final UriCompBuilder apiUriCompBuilder; - private final RestTemplate restTemplate; - - @Override - public EmbeddingModelType getModelType() { - return EmbeddingModelType.KM_BERT; - } - - @Override - public float[] getEmbedding(String text) { - URI embeddingURI = getEmbeddingURI(); - return getEmbeddingVetor(embeddingURI, text); - } - - private float[] getEmbeddingVetor(URI embedUri, String text) { - EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); - embeddingRequestText.setText(text); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); - return restTemplate.postForObject(embedUri, request, float[].class); - } - - private URI getEmbeddingURI() { - return apiUriCompBuilder.getUriForKmbertEmbeding(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java deleted file mode 100644 index d182618..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/KrSbertEmbeddingClient.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; - -import java.net.URI; - -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class KrSbertEmbeddingClient implements EmbeddingClient { - private final UriCompBuilder apiUriCompBuilder; - private final RestTemplate restTemplate; - - @Override - public EmbeddingModelType getModelType() { - return EmbeddingModelType.SBERT; - } - - @Override - public float[] getEmbedding(String text) { - URI embeddingURI = getEmbeddingURI(); - return getEmbeddingVetor(embeddingURI, text); - } - - private float[] getEmbeddingVetor(URI embedUri, String text) { - EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); - embeddingRequestText.setText(text); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); - return restTemplate.postForObject(embedUri, request, float[].class); - } - - private URI getEmbeddingURI() { - return apiUriCompBuilder.getUriForKrSbertEmbeding(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java deleted file mode 100644 index 71e6654..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/client/OpenaiEmbeddingClient.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.client; - -import java.util.List; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.embedding.Embedding; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiEmbeddingOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.stereotype.Component; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingModelType; - -@Component -public class OpenaiEmbeddingClient implements EmbeddingClient { - private final OpenAiApi openAiApi; - - public OpenaiEmbeddingClient(OpenAiApi openAiApi) { - this.openAiApi = openAiApi; - } - - @Override - public EmbeddingModelType getModelType() { - return EmbeddingModelType.OPENAI; - } - - @Override - public float[] getEmbedding(String text) { - OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( - this.openAiApi, - MetadataMode.EMBED, - OpenAiEmbeddingOptions.builder() - .model("text-embedding-3-small") - .build(), - RetryUtils.DEFAULT_RETRY_TEMPLATE); - - EmbeddingResponse embeddingResponse = openAiEmbeddingModel - .embedForResponse(List.of(text)); - - Embedding embedding = embeddingResponse.getResults().getFirst(); - return embedding.getOutput(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java deleted file mode 100644 index 083e434..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingModelType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model; - -public enum EmbeddingModelType { - OPENAI, - SBERT, - KM_BERT -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java deleted file mode 100644 index e921a62..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/ApiDataDrugImgRepo.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; - -@Repository -public interface ApiDataDrugImgRepo extends JpaRepository { -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java deleted file mode 100644 index b7054c8..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugDetailJpaRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; - -import java.util.List; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; - -import jakarta.transaction.Transactional; - -@Repository -public interface GovDrugDetailJpaRepository extends JpaRepository { - - @Override - @Transactional - List saveAllAndFlush(Iterable entities); - - List findByDrugIdGreaterThanOrderByDrugIdAsc(Long drugIdIsGreaterThan, Pageable pageable); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java deleted file mode 100644 index 79bb823..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugJpaRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; - -import aj.org.objectweb.asm.commons.Remapper; -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 com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; - -public interface GovDrugJpaRepository extends JpaRepository { - - Page findByIsGeneralIsTrue(Pageable pageable); - - @Query("SELECT MIN(d.drugId) FROM DrugRawDataEntity d") - Long findMinDrugId(); - - @Query("SELECT MAX(d.drugId) FROM DrugRawDataEntity d") - Long findMaxDrugId(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java deleted file mode 100644 index a1364c0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/jpa/GovDrugKrSbertEmbedJpaRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; - -import java.util.List; - -@Repository -public interface GovDrugKrSbertEmbedJpaRepository extends JpaRepository { - @Query( - value = """ - SELECT r, e - FROM DrugKrSbertEmbedEntity e - JOIN DrugRawDataEntity r - ON r.drugId = e.drugId - WHERE e.krSbertVector IS NOT NULL - """ - ) - List findRawAndEmbed(Pageable pageable); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java deleted file mode 100644 index 6425c90..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugImageRequestMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto.DrugImageRequest; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.ApiDataDrugImgEntity; - -public class DrugImageRequestMapper { - public static ApiDataDrugImgEntity toEntityFromRequest(DrugImageRequest r){ - return ApiDataDrugImgEntity.builder() - .drugId(r.getDrugId()) - .productImage(r.getProductImage()) - .pillImage(r.getPillImageUrl()) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java deleted file mode 100644 index 3fecf61..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugRawDataMapper.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; -import com.likelion.backendplus4.yakplus.drug.domain.model.DrugRawData; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.index.support.parser.JsonArrayTextParser; -import java.io.IOException; -import java.util.List; - -public class DrugRawDataMapper { - private static final ObjectMapper objectMapper = new ObjectMapper(); - - public static DrugRawDataEntity toEntityFromDomain(DrugRawData raw) { - return DrugRawDataEntity - .builder() - .drugId(raw.getDrugId()) - .drugName(raw.getDrugName()) - .company(raw.getCompany()) - .permitDate(raw.getPermitDate()) - .isGeneral(raw.isGeneral()) - .materialInfo(toStringFromObj(raw.getMaterialInfo())) - .storeMethod(raw.getStoreMethod()) - .validTerm(raw.getValidTerm()) - .efficacy(toStringFromObj(raw.getEfficacy())) - .usage(toStringFromObj(raw.getUsage())) - .precaution(toStringFromObj(raw.getPrecaution())) - .imageUrl(raw.getImageUrl()) - .cancelDate(raw.getCancelDate()) - .cancelName(raw.getCancelName()) - .isHerbal(raw.isHerbal()) - .build(); - } - - - public static Drug toDomainFromEntity(DrugRawDataEntity e) { - List efficacy; - try { - efficacy = JsonArrayTextParser.extractAndClean(e.getEfficacy()); - } catch (IOException exception) { - throw new ScraperException(ScraperErrorCode.PARSING_ERROR); - } - - return Drug.builder() - .drugId(e.getDrugId()) - .drugName(e.getDrugName()) - .company(e.getCompany()) - .permitDate(e.getPermitDate()) - .isGeneral(e.isGeneral()) -// TODO .materialInfo(JsonArrayTextParser.extractAndClean(e.getMaterialInfo())) - .storeMethod(e.getStoreMethod()) - .validTerm(e.getValidTerm()) - .efficacy(efficacy) -// TODO .usage(e.getUsage()) -// TODO .precaution(e.getPrecaution()) - .imageUrl(e.getImageUrl()) - .cancelDate(e.getCancelDate()) - .cancelName(e.getCancelName()) - .isHerbal(e.getIsHerbal()) - .build(); - } - - private static String toStringFromObj(Object obj){ - try{ - return objectMapper.writeValueAsString(obj); - } catch (Exception e) { - e.printStackTrace(); - System.out.println("변환 실패"); - return null; - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java deleted file mode 100644 index 5397f00..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/dto/ModelSwitchRequeset.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.embed.dto; - -import lombok.Getter; -import lombok.Setter; - -/** - * 임베딩 모델 전환 요청을 위한 DTO 클래스입니다. - * - * @since 2025-05-02 - */ -@Getter -@Setter -public class ModelSwitchRequeset { - String modelType; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java deleted file mode 100644 index 5dcb7bb..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/embed/model/embedModelController.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.embed.model; - -import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.embed.DrugEmbedProcessorUseCase; -import com.likelion.backendplus4.yakplus.drug.presentation.controller.embed.dto.ModelSwitchRequeset; -import com.likelion.backendplus4.yakplus.response.ApiResponse; - -import lombok.RequiredArgsConstructor; - -/** - * 임베딩 모델 전환 및 현재 모델 조회를 위한 컨트롤러입니다. - * - * @since 2025-05-02 - */ -@RestController -@RequiredArgsConstructor -@RequestMapping("/embed/model") -public class embedModelController { - private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; - - /** - * 임베딩 모델을 전환합니다. - * - * @param request 전환할 모델 타입 요청 DTO - * @return 전환된 모델 정보 응답 - * - * @author 정안식 - * @since 2025-05-01 - * @modify 2025-05-02 함예정 - * - 스프링 배치 전환에 따른 수정 - */ - @PostMapping("/switch") - public ResponseEntity switchEmbedding(@RequestBody ModelSwitchRequeset request) { - drugEmbedProcessorUseCase.switchEmbeddingModel(request.getModelType()); - return success("요청 성공: "+ drugEmbedProcessorUseCase.getCurrentEmbeddingModel()); - } - - /** - * 현재 사용 중인 임베딩 모델을 조회합니다. - * - * @return 현재 모델 이름 - * - * @author 정안식 - * @since 2025-05-01 - * @modify 2025-05-02 함예정 - * - 스프링 배치 전환에 따른 수정 - */ - @GetMapping - public ResponseEntity> getModel(){ - return success(drugEmbedProcessorUseCase.getCurrentEmbeddingModel()); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/ScraperException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/ScraperException.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/ScraperException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/ScraperException.java index c692dd3..f77daab 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/ScraperException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/ScraperException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.exception; +package com.likelion.backendplus4.yakplus.drug.scraper.application.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/error/ScraperErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/error/ScraperErrorCode.java similarity index 90% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/error/ScraperErrorCode.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/error/ScraperErrorCode.java index 777ed4e..32ea7ae 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/exception/error/ScraperErrorCode.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/error/ScraperErrorCode.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.exception.error; +package com.likelion.backendplus4.yakplus.drug.scraper.application.exception.error; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; import lombok.RequiredArgsConstructor; @@ -22,16 +22,16 @@ public enum ScraperErrorCode implements ErrorCode { @Override public HttpStatus httpStatus() { - return null; + return httpStatus; } @Override public int codeNumber() { - return 0; + return code; } @Override public String message() { - return ""; + return message; } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperDetailUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperDetailUseCase.java similarity index 87% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperDetailUseCase.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperDetailUseCase.java index fb36bf3..a4c1dce 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperDetailUseCase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperDetailUseCase.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; /** * 의약품 상세 정보를 수집하는 유스케이스 인터페이스입니다. * diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperImageUseCase.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperImageUseCase.java index b5819fd..42213f3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperImageUsecase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperImageUseCase.java @@ -1,11 +1,11 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; /** * 의약품 이미지 정보를 수집하는 유스케이스 인터페이스입니다. * * @since 2025-04-21 */ -public interface DrugScraperImageUsecase { +public interface DrugScraperImageUseCase { /** * 의약품 이미지 데이터를 수집하는 배치 작업을 시작합니다. diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperTableCombineUseCase.java similarity index 77% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperTableCombineUseCase.java index 7194171..db8176a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperTableCombineUsecase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperTableCombineUseCase.java @@ -1,6 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; -public interface DrugScraperTableCombineUsecase { +public interface DrugScraperTableCombineUseCase { /** * API 요청으로 받아온 RAW 데이터 테이블 2개를 병합해 diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperUseCase.java similarity index 89% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperUseCase.java index da2ac3b..213db86 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/in/scraper/DrugScraperUsecase.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperUseCase.java @@ -1,6 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper; +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; -public interface DrugScraperUsecase { +public interface DrugScraperUseCase { /** * 의약품 데이터 수집 및 임베딩 프로세스를 순차적으로 실행합니다. diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/out/BatchJobPort.java similarity index 97% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/out/BatchJobPort.java index e30b596..30f519d 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/port/out/BatchJobPort.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/out/BatchJobPort.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.port.out; +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.out; /** * 의약품 데이터 수집 및 임베딩과 관련된 배치 작업을 제어하기 위한 포트 인터페이스입니다. diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperDetailService.java similarity index 64% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperDetailService.java index 7ed032d..7b9f3c3 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/detail/DrugScraperDetailService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperDetailService.java @@ -1,9 +1,9 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.detail; +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; import org.springframework.stereotype.Component; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperDetailUseCase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperDetailUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperService.java similarity index 59% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperService.java index ba597d2..5367992 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/DrugScraperService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperService.java @@ -1,15 +1,15 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper; +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; import org.springframework.stereotype.Service; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor -public class DrugScraperService implements DrugScraperUsecase { +public class DrugScraperService implements DrugScraperUseCase { private final BatchJobPort batchJobPort; @Override diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperServiceImage.java similarity index 63% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperServiceImage.java index 60b0508..59c108b 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/image/DrugScraperServiceImage.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperServiceImage.java @@ -1,15 +1,15 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.image; +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; import org.springframework.stereotype.Component; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperImageUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperImageUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor -public class DrugScraperServiceImage implements DrugScraperImageUsecase { +public class DrugScraperServiceImage implements DrugScraperImageUseCase { private final BatchJobPort batchJobPort; @Override diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperTableCombineService.java similarity index 61% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperTableCombineService.java index 409a3f9..694dcc1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/scraper/combine/DrugScraperTableCombineService.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperTableCombineService.java @@ -1,15 +1,15 @@ -package com.likelion.backendplus4.yakplus.drug.application.service.scraper.combine; +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; import org.springframework.stereotype.Service; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperTableCombineUsecase; -import com.likelion.backendplus4.yakplus.drug.application.service.port.out.BatchJobPort; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperTableCombineUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor -public class DrugScraperTableCombineService implements DrugScraperTableCombineUsecase { +public class DrugScraperTableCombineService implements DrugScraperTableCombineUseCase { private final BatchJobPort batchJobPort; @Override diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java new file mode 100644 index 0000000..5430b5a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java @@ -0,0 +1,36 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * API 호출 시 발생하는 에러 코드 + * + * @since 2025-04-15 + */ +@RequiredArgsConstructor +public enum RestApiError implements ErrorCode { + PAGE_COUNT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 550001, "API 전체 페이지 개수를 확인하지 못했습니다."), + ITEM_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 550002, "API 응답에서 적절한 items를 추출하지 못했습니다."); + + private final HttpStatus httpStatus; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiException.java similarity index 83% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiException.java index 80a2ad4..2284f53 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/exception/RestApiException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/ApiRequestManager.java similarity index 90% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/ApiRequestManager.java index bb320e2..81127b1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/ApiRequestManager.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/ApiRequestManager.java @@ -1,6 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.util; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -9,9 +9,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception.RestApiError; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.exception.RestApiException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception.RestApiError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception.RestApiException; /** * 공공의약품 API 요청을 처리하는 매니저 클래스입니다. @@ -121,7 +121,6 @@ public String fetchImageData(int pageNo) { * @since 2025-04-21 */ public JsonNode getItemsFromResponse(String response) { - log("응답에서 items 값 추출"); try { return new ObjectMapper().readTree(response) .path("body") diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/UriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/UriCompBuilder.java similarity index 97% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/UriCompBuilder.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/UriCompBuilder.java index 7548e41..a6e2dc7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/api/util/UriCompBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/UriCompBuilder.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.api.util; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util; import java.net.URI; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java new file mode 100644 index 0000000..1b5873d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java @@ -0,0 +1,238 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.adapter; + +import com.likelion.backendplus4.yakplus.drug.support.JobManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import org.springframework.batch.core.Job; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +/** + * BatchJobPort 인터페이스의 구현체로, + * Spring Batch Job 객체들을 제어하는 어댑터 클래스입니다. + * JobManager를 통해 배치 작업의 실행, 중지, 상태 조회 기능을 제공합니다. + * + * JobManager: 스프링 배치의 Job을 관리해주는 유틸 클래스 + * Job: 스프링 배치 작업을 정의한 작업 클래스 + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class BatchJobAdapter implements BatchJobPort { + + private final JobManager jobManager; + private final Job drugScrapJob; + private final Job drugDetailScrapJob; + private final Job drugImageScrapJob; + private final Job drugTableCombineJob; + private final Job embedJob; + + /** + * 전체 데이터 수집 및 임베딩 작업을 시작합니다. + * 이미 실행 중인 작업이 있을 경우 예외를 발생시킵니다. + * + * @return 작업 시작 결과 메시지 + * @throws ParserBatchException 중복 실행 예외 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String allJobStart() { + return jobManager.startJob(drugScrapJob); + } + + /** + * 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * @@author 함예정 + * @since 2025-05-02 + */ + @Override + public String allJobStop() { + return jobManager.stopRunningBatch(drugScrapJob); + } + + /** + * 중단된 작업을 조회하고, 재개합니다. + * 이미 실행 중인 작업이 있을 경우 예외를 발생시킵니다. + * + * @return 작업 재개 결과 메시지 + * @throws ParserBatchException 중복 실행 예외 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String allJobResume() { + return jobManager.restart(); + } + + /** + * 전체 작업 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String allJobStatus() { + return jobManager.getJobStatus(drugScrapJob); + } + + /** + * 의약품 상세 정보 수집 작업을 시작합니다. + * 이미 실행 중인 작업이 있을 경우 예외를 발생시킵니다. + * + * @return 작업 시작 결과 메시지 + * @throws ParserBatchException 중복 실행 예외 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String detailScrapJobStart() { + return jobManager.startJob(drugDetailScrapJob); + } + + /** + * 의약품 상세 정보 수집 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String detailScrapJobStop() { + return jobManager.stopRunningBatch(drugDetailScrapJob); + } + + /** + * 의약품 상세 정보 수집 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String detailScrapJobStatus() { + return jobManager.getJobStatus(drugDetailScrapJob); + } + + /** + * 의약품 이미지 수집 작업을 시작합니다. + * 이미 실행 중인 작업이 있을 경우 예외를 발생시킵니다. + * + * @return 작업 시작 결과 메시지 + * @throws ParserBatchException 중복 실행 예외 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String imageScrapJobStart() { + return jobManager.startJob(drugImageScrapJob); + } + + /** + * 의약품 이미지 수집 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String imageScrapJobStop() { + return jobManager.stopRunningBatch(drugImageScrapJob); + } + + /** + * 의약품 이미지 수집 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String imageScrapJobStatus() { + return jobManager.getJobStatus(drugImageScrapJob); + } + + /** + * 의약품 테이블 통합 작업을 시작합니다. + * 이미 실행 중인 작업이 있을 경우 예외를 발생시킵니다. + * + * @return 작업 시작 결과 메시지 + * @throws ParserBatchException 중복 실행 예외 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String tableCombineJobStart() { + return jobManager.startJob(drugTableCombineJob); + } + + + /** + * 의약품 테이블 통합 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String tableCombineJobStop() { + return jobManager.stopRunningBatch(drugTableCombineJob); + } + + /** + * 의약품 테이블 통합 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String tableCombineJobStatus() { + return jobManager.getJobStatus(drugTableCombineJob); + } + + /** + * 증상 텍스트 임베딩 생성 작업을 시작합니다. + * 이미 실행 중인 작업이 있을 경우 예외를 발생시킵니다. + * + * @return 작업 시작 결과 메시지 + * @throws ParserBatchException 중복 실행 예외 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String embedJobStart(){ + return jobManager.startJob(embedJob); + } + + + /** + * 증상 텍스트 임베딩 생성 작업을 중지합니다. + * + * @return 작업 중지 결과 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String embedJobStop(){ + return jobManager.stopRunningBatch(embedJob); + } + + /** + * 임베딩 생성 작업의 상태를 조회합니다. + * + * @return 작업 상태 메시지 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public String embedjobStatus(){ + return jobManager.getJobStatus(embedJob); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/ParserBatchException.java similarity index 83% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/ParserBatchException.java index f9c7fe7..9578492 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/exception/ParserBatchException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/ParserBatchException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java new file mode 100644 index 0000000..f63a806 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * ParserBatchError Enum + * 배치 작업에서 발생할 수 있는 에러 코드들을 정의합니다. + * 각 에러 코드는 HTTP 상태 코드, 에러 코드, 에러 메시지를 포함합니다. + * + * @field httpStatus HTTP 상태 코드 + * @field code 에러 코드 + * @field message 에러 메시지 + * @since 2025-05-02 + */ +@RequiredArgsConstructor +public enum ParserBatchError implements ErrorCode { + ALREADY_RUN(HttpStatus.CONFLICT, 450001, "이미 실행 중인 배치가 있습니다."), + JOB_RUN_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 550001, "JOB 실행 요청은 정상적으로 도달했으나 실행에 실패했습니다."), + JSON_TYPE_CHANGE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 550002, "JSON을 자바 타입으로 변환하는데 실패했습니다."); + private final HttpStatus httpStatus; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java new file mode 100644 index 0000000..bd1e48e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java @@ -0,0 +1,35 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.job.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 의약품 테이블 통합 작업을 정의하는 Spring Batch Job 설정 클래스입니다. + *

+ * 개별 테이블로부터 수집된 데이터를 통합하여 단일 테이블로 병합하는 작업을 수행합니다. + * + * @since 2025-05-02 + */ +@Configuration +public class CombineJobConfig { + + /** + * 테이블 통합 작업을 수행하는 Job을 정의합니다. + * 단일 Step(tableCombineStep)으로 구성되며, 의약품 관련 데이터 병합 로직을 포함합니다. + * + * @param jobRepository Spring Batch JobRepository + * @param tableCombineStep 실제 테이블 병합을 수행하는 Step + * @return Job 구성된 Job 인스턴스 + */ + @Bean + public Job drugTableCombineJob(JobRepository jobRepository, + Step tableCombineStep) { + return new JobBuilder("drugTableCombineJob", jobRepository) + .start(tableCombineStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java new file mode 100644 index 0000000..61366cf --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java @@ -0,0 +1,43 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.job.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 의약품 상세정보 수집 배치 작업을 구성하는 설정 클래스입니다. + *

+ * 작업 순서는 아래와 같습니다. + * 1. 전체 페이지 수를 계산하는 Step: 의약품 상세정보의 전체 페이지 수를 계산합니다. + * 2. 의약품 상세정보를 수집하는 Step: 계산된 페이지 수를 기반으로 상세정보를 수집합니다. + * + * @since 2025-05-02 + */ +@Configuration +@RequiredArgsConstructor +public class DetailJobConfig { + + /** + * 의약품 상세정보 수집을 위한 Job을 정의합니다. + * + * @param jobRepository Job 실행 정보 저장소 + * @param totalPageCheckStep 전체 페이지 수 확인 스텝 + * @param drugDetailStep 의약품 상세정보 수집 스텝 + * @return Job 구성된 Job 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Job drugDetailScrapJob(JobRepository jobRepository, + Step totalPageCheckStep, + Step drugDetailStep) { + return new JobBuilder("drugDetailScrapJob", jobRepository) + .start(totalPageCheckStep) + .next(drugDetailStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java new file mode 100644 index 0000000..3ea12d2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.job.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 의약품 이미지 크롤링 작업을 정의하는 Spring Batch Job 설정 클래스입니다. + *

+ * 총 페이지 수 확인 후, 병렬로 이미지를 수집하는 Step을 실행합니다. + * + * @since 2025-05-02 + */ +@Configuration +public class ImageJobConfig { + + + /** + * 의약품 이미지 수집을 위한 Job을 정의합니다. + *

+ * Step 순서: + *

    + *
  1. imageTotalPageCheckStep: 전체 페이지 수를 계산
  2. + *
  3. imageMasterStep: 병렬로 이미지 크롤링 수행
  4. + *
+ * + * @param jobRepository Spring Batch JobRepository + * @param imageTotalPageCheckStep 페이지 수 확인 Step + * @param imageMasterStep 이미지 수집 Master Step (partition 기반) + * @return 구성된 Job 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Job drugImageScrapJob(JobRepository jobRepository, + Step imageTotalPageCheckStep, + Step imageMasterStep) { + return new JobBuilder("drugImageScrapJob", jobRepository) + .start(imageTotalPageCheckStep) + .next(imageMasterStep) + .build(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java new file mode 100644 index 0000000..07b6f0d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDate; + +/** + * 의약품 상세 정보와 이미지 데이터를 병합한 DTO 클래스입니다. + *

+ * DrugDetailEntity와 ApiDataDrugImgEntity를 조인하여 구성되며, + * 병합 테이블 생성 또는 저장을 위한 중간 구조로 사용됩니다. + * + * @field drugId 의약품 ID + * @field drugName 의약품 이름 + * @field company 제조사 + * @field permitDate 허가일 + * @field isGeneral 일반의약품 여부 + * @field materialInfo 성분 정보 + * @field storeMethod 보관 방법 + * @field validTerm 유효기간 + * @field efficacy 효능 + * @field usage 사용법 + * @field precaution 주의사항 + * @field cancelDate 취소일 + * @field cancelName 취소 유형명 + * @field isHerbal 한약 여부 + * @field productImage 제품 이미지 URL + * @field pillImage 낱알 이미지 URL + * @since 2025-05-02 + */ +@Getter +@AllArgsConstructor +public class TableCombineDto { + private Long drugId; + private String drugName; + private String company; + private LocalDate permitDate; + private boolean isGeneral; + private String materialInfo; + private String storeMethod; + private String validTerm; + private String efficacy; + private String usage; + private String precaution; + private LocalDate cancelDate; + private String cancelName; + private Boolean isHerbal; + private String productImage; + private String pillImage; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java new file mode 100644 index 0000000..4677854 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java @@ -0,0 +1,264 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.processor; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto.TableCombineDto; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.StreamSupport; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * TableCombineDto를 DrugRawDataEntity로 변환하는 Spring Batch ItemProcessor입니다. + *

+ * 상세 정보와 이미지 정보를 병합하여 최종 저장 가능한 구조로 가공하며, + * 효능, 사용법, 주의사항, 성분 정보는 JSON 파싱 및 구조 변환을 통해 정제됩니다. + * + * @modified 2025-05-02 함예정 + * - 스프링 배치로 수정 + * @since 2025-04-21 + */ +@Component +@RequiredArgsConstructor +public class TableCombineProcessor implements ItemProcessor { + + /** + * TableCombineDto를 DrugRawDataEntity로 변환합니다. + *

+ * 이미지 정보는 productImage가 유효할 경우 우선 사용하며, 그렇지 않으면 pillImage를 사용합니다. + * 효능, 사용법, 주의사항, 성분 정보는 JSON 문자열을 구조화된 형태로 파싱 후 직렬화합니다. + * + * @param dto 병합된 의약품 상세 + 이미지 DTO + * @return DrugRawDataEntity 저장 가능한 형태의 엔티티 객체 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public DrugRawDataEntity process(TableCombineDto dto) { + String imgUrl = getCoverImageFromProductAndPill(dto); + + return DrugRawDataEntity.builder() + .drugId(dto.getDrugId()) + .drugName(dto.getDrugName()) + .company(dto.getCompany()) + .permitDate(dto.getPermitDate()) + .isGeneral(dto.isGeneral()) + .materialInfo(toStringFromObj(convertMaterialInfo(dto.getMaterialInfo()))) + .storeMethod(dto.getStoreMethod()) + .validTerm(dto.getValidTerm()) + .efficacy(toStringFromObj(convertEfficacy(dto.getEfficacy()))) + .usage(toStringFromObj(getUsage(dto.getUsage()))) + .precaution(toStringFromObj(getPrecaution(dto.getPrecaution()))) + .imageUrl(imgUrl) + .cancelDate(dto.getCancelDate()) + .cancelName(dto.getCancelName()) + .isHerbal(dto.getIsHerbal()) + .build(); + } + + /** + * 제품 이미지가 유효할 경우 우선 사용하고, 그렇지 않으면 알약 이미지를 반환합니다. + * + * @param dto TableCombineDto 병합된 의약품 상세 + 이미지 DTO + */ + private String getCoverImageFromProductAndPill(TableCombineDto dto) { + String imgUrl = (dto.getProductImage() != null && dto.getProductImage().length() > 10) + ? dto.getProductImage() + : dto.getPillImage(); + return imgUrl; + } + + /** + * 사용법 텍스트를 파싱하여 문단별 설명 리스트를 추출합니다. + * + * @param usage 사용법 JSON 문자열 + * @return List 사용법 문장 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + private List getUsage(String usage) { + JsonNode json = toJsonNodeFromString(usage); + + if (json.isNull() || !json.has("sections")) { + return Collections.emptyList(); + } + + return StreamSupport.stream(json.get("sections").spliterator(), false) + .flatMap(section -> StreamSupport.stream(section.get("articles").spliterator(), false)) + .flatMap(article -> StreamSupport.stream(article.get("paragraphs").spliterator(), false)) + .map(paragraph -> paragraph.get("text").asText()) + .toList(); + } + + /** + * 성분 정보를 JSON 문자열로 변환합니다. + * + * @param material 성분 정보 JSON 문자열 + * @return List 성분 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + private List convertMaterialInfo(String material) { + JsonNode json = toJsonNodeFromString(material); + if (json.isArray()) { + return mapFromMaterialJson(json); + } + return null; + } + + /** + * JSON 노드를 Material 객체 리스트로 변환합니다. + * + * @param json JSON 노드 + * @return List 성분 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + private List mapFromMaterialJson(JsonNode json) { + List materials = new ArrayList<>(); + + ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + for (JsonNode node : json) { + Material ingredient = objectMapper.treeToValue(node, Material.class); + materials.add(ingredient); + } + return materials; + } catch (Exception e) { + log(LogLevel.ERROR, "객체 맵핑 실패", e); + return null; + } + } + + /** + * JSON 문자열을 JsonNode 객체로 변환합니다. + * + * @param json JSON 문자열 + * @return JsonNode 변환된 JSON 노드 + * @author 함예정 + * @since 2025-05-02 + */ + private JsonNode toJsonNodeFromString(String json) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readTree(json); + } catch (Exception e) { + log(LogLevel.ERROR, "json 객체 생성 에러", e); + return null; + } + } + + /** + * 효능 정보를 JSON 문자열로 변환합니다. + * + * @param efficacyJsonString 효능 정보 JSON 문자열 + * @return List 효능 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + private List convertEfficacy(String efficacyJsonString) { + JsonNode json = toJsonNodeFromString(efficacyJsonString); + List efficacy = new ArrayList<>(); + tryParseParagraphs(json, efficacy); + + if (efficacy.isEmpty()) { + tryParseTitle(json, efficacy); + } + return efficacy; + } + + /** + * JSON 노드에서 제목을 추출하여 효능 리스트에 추가합니다. + * + * @param json JSON 노드 + * @param efficacy 효능 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + private void tryParseTitle(JsonNode json, List efficacy) { + if (json.has("sections")) { + for (JsonNode section : json.get("sections")) { + for (JsonNode article : section.get("articles")) { + efficacy.add(article.get("title").asText()); + } + } + } + } + + /** + * JSON 노드에서 문단을 추출하여 효능 리스트에 추가합니다. + * + * @param json JSON 노드 + * @param efficacy 효능 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + private void tryParseParagraphs(JsonNode json, List efficacy) { + if (json.has("sections")) { + List parsed = StreamSupport.stream(json.get("sections").spliterator(), false) + .flatMap(section -> StreamSupport.stream(section.get("articles").spliterator(), false)) + .flatMap(article -> StreamSupport.stream(article.get("paragraphs").spliterator(), false)) + .map(paragraph -> paragraph.get("text").asText()) + .filter(text -> text != null && !text.isEmpty()) + .toList(); + + efficacy.addAll(parsed); + } + } + + /** + * 주의사항 정보를 JSON 문자열로 변환합니다. + * + * @param precaution 주의사항 JSON 문자열 + * @return Map> 주의사항 맵 + * @author 함예정 + * @since 2025-05-02 + */ + private Map> getPrecaution(String precaution) { + Map> result = new LinkedHashMap<>(); + + JsonNode json = toJsonNodeFromString(precaution); + if (json.has("sections")) { + JsonNode articles = json.get("sections").get(0).get("articles"); + for (JsonNode article : articles) { + String title = article.get("title").asText(); + List texts = new ArrayList<>(); + for (JsonNode paragraph : article.get("paragraphs")) { + texts.add(paragraph.get("text").asText()); + } + result.put(title, texts); + } + } + + return result; + } + + /** + * 객체를 JSON 문자열로 변환합니다. + * + * @param obj 변환할 객체 + * @return JSON 문자열 + * @author 함예정 + * @since 2025-05-02 + */ + private String toStringFromObj(Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + log(LogLevel.ERROR, "JSON 변환 에러", e); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java new file mode 100644 index 0000000..124b335 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.writer; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * DrugRawDataEntity 리스트를 JPA를 통해 저장하는 Spring Batch ItemWriter 구현체입니다. + *

+ * 테이블 병합 후 생성된 엔티티를 DB에 일괄 저장하며, 처리 건수를 로그로 출력합니다. + * + * @fields drugRawDataRepository JPA를 통해 DrugRawDataEntity를 저장하는 레포지토리 + * @fields count 저장된 엔티티의 개수를 세기 위한 AtomicInteger + * @since 2025-05-02 + */ +@Component +@StepScope +@RequiredArgsConstructor +public class TableCombineWriter implements ItemWriter { + private final DrugJpaRepository drugRawDataRepository; + private final AtomicInteger count = new AtomicInteger(); + + /** + * 병합된 DrugRawDataEntity 리스트를 JPA를 통해 저장합니다. + * + * @param entity Chunk 단위로 전달된 엔티티 목록 + * @since 2025-05-02 + * @author 함예정 + */ + @Override + public void write(Chunk entity) { + List items = new ArrayList<>(entity.getItems()); + drugRawDataRepository.saveAll(items); + log("테이블 병합 작업 - 쓰기 완료: " + count.addAndGet(items.size())); + + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java new file mode 100644 index 0000000..3f533de --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java @@ -0,0 +1,141 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto.TableCombineDto; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import jakarta.persistence.EntityManagerFactory; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * 의약품 상세 정보와 이미지 정보를 병합하여 DrugRawDataEntity로 저장하는 Step 설정 클래스입니다. + *

+ * DrugDetailEntity와 ApiDataDrugImgEntity를 조인하여 TableCombineDto로 읽고, + * 이를 DrugRawDataEntity로 변환한 뒤 저장합니다. + * + * @field combineStepName 테이블 병합 Step의 이름 + * @field drugDetailReaderName ItemReader의 이름 + * @field tableCombineEntityFqn 병합 테이블 DTO의 경로 Full Path + * @field drugDetailEntity 의약품 상세 정보 Entity 클래스명 + * @field drugImageEntity 의약품 이미지 Entity 클래스명 + * @field detailAlias 의약품 상세 정보 Entity의 별칭 + * @field imageAlias 의약품 이미지 Entity의 별칭 + * @field RETRY_LIMIT 재시도 횟수 제한 설정 값 + * @field SKIP_LIMIT 스킵 횟수 제한 설정 값 + * @field PAGE_SIZE 페이지 크기 + * @since 2025-05-02 + */ +@Configuration +public class CombineStepConfig { + private static final String COMBINE_STEP_NAME = "drugCombineStep"; + private static final String DRUG_DETAIL_READER_NAME = "drugDetailReader"; + + private static final String TABLE_COMBINE_ENTITY_FQN = + "com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto.TableCombineDto"; + private static final String DRUG_DETAIL_ENTITY = "DrugDetailEntity"; + private static final String DRUG_IMAGE_ENTITY = "DrugImgEntity"; + private static final String DETAIL_ALIAS = "d"; + private static final String IMAGE_ALIAS = "i"; + private static final int RETRY_LIMIT = 3; + private static final int SKIP_LIMIT = 5_000; + private static final int PAGE_SIZE = 1_000; + + /** + * 병합 작업을 수행하는 Step을 정의합니다. + *

+ * TableCombineDto → DrugRawDataEntity로 변환하여 저장하며, 예외 허용 정책도 포함됩니다. + * + * @param jobRepository Job 저장소 + * @param transactionManager 트랜잭션 관리자 + * @param reader 병합 대상 데이터를 읽는 Reader + * @param processor 병합 및 매핑 처리기 + * @param writer 결과 데이터를 저장하는 Writer + * @return 구성된 Step + */ + @Bean + public Step tableCombineStep( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + return new StepBuilder(COMBINE_STEP_NAME, jobRepository) + .chunk(PAGE_SIZE, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(RETRY_LIMIT) + .skip(Exception.class) + .skipLimit(SKIP_LIMIT) + .build(); + } + + /** + * 의약품 상세 정보와 이미지 정보를 병합하여 TableCombineDto로 읽어오는 Reader입니다. + *

+ * DrugDetailEntity와 ApiDataDrugImgEntity를 LEFT JOIN하여 필요한 필드를 조합합니다. + * + * @param entityManagerFactory JPA EntityManagerFactory + * @return JpaPagingItemReader JPA 페이징 ItemReader + */ + @Bean + @StepScope + public JpaPagingItemReader drugDetailReader(EntityManagerFactory entityManagerFactory) { + JpaPagingItemReader reader = new JpaPagingItemReader<>(); + reader.setEntityManagerFactory(entityManagerFactory); + reader.setQueryString(getJoinTableSql()); + reader.setPageSize(PAGE_SIZE); + reader.setSaveState(true); + reader.setName(DRUG_DETAIL_READER_NAME); + return reader; + } + + /** + * DrugDetailEntity와 ApiDataDrugImgEntity를 조인하여 TableCombineDto를 생성하는 JPQL 쿼리를 반환합니다. + * + * @return 병합을 위한 JPQL 쿼리 문자열 + */ + private String getJoinTableSql() { + return String.format(""" + SELECT new %s( + %s.drugId, + %s.drugName, + %s.company, + %s.permitDate, + %s.isGeneral, + %s.materialInfo, + %s.storeMethod, + %s.validTerm, + %s.efficacy, + %s.usage, + %s.precaution, + %s.cancelDate, + %s.cancelName, + %s.isHerbal, + %s.productImage, + %s.pillImage + ) + FROM %s %s + LEFT JOIN %s %s ON %s.drugId = %s.drugId + """, + TABLE_COMBINE_ENTITY_FQN, + DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, + DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, + DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, + IMAGE_ALIAS, IMAGE_ALIAS, + DRUG_DETAIL_ENTITY, DETAIL_ALIAS, + DRUG_IMAGE_ENTITY, IMAGE_ALIAS, + DETAIL_ALIAS, IMAGE_ALIAS + ); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java new file mode 100644 index 0000000..0478d61 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java @@ -0,0 +1,120 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor.DetailTotalPageCalculator; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor.DrugDetailProcessor; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.reader.DetailPageNumberReader; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.writer.DrugDetailWriter; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.List; + +/** + * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 + *

+ * DetailPageNumberReader: 상세 페이지 번호를 읽어오는 Reader + * ApiRequestManager: API 요청을 관리하는 클래스 + * ApiResponseMapper: API 응답을 매핑하는 클래스 + * TaskExecutor: 멀티스레드 처리를 위한 TaskExecutor + * + * @since 2025-05-02 + */ +@Configuration +public class DetailStepConfig { + private final String executorName = "normalExecutor"; + + private final DetailPageNumberReader detailPageNumberReader; + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + private final TaskExecutor taskExecutor; + + public DetailStepConfig(DetailPageNumberReader detailPageNumberReader, + ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper, + @Qualifier(executorName) + TaskExecutor taskExecutor) { + this.detailPageNumberReader = detailPageNumberReader; + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + this.taskExecutor = taskExecutor; + } + + /** + * 전체 페이지 수를 계산하는 Tasklet 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param detailTotalPageCalculator 페이지 계산 로직을 수행하는 Tasklet + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + Step totalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, + DetailTotalPageCalculator detailTotalPageCalculator) { + return new StepBuilder("totalPageCheck", jobRepository) + .tasklet(detailTotalPageCalculator, txManager) + .build(); + } + + /** + * 상세 페이지별로 데이터를 수집하고 처리 및 저장하는 Chunk 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param processor 데이터를 처리하는 Processor + * @param writer 데이터를 저장하는 Writer + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public Step drugDetailStep(JobRepository jobRepository, + PlatformTransactionManager txManager, + DrugDetailProcessor processor, + DrugDetailWriter writer) { + return new StepBuilder("drugDetailStep", jobRepository) + .>chunk(1, txManager) + .reader(detailPageNumberReader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .taskExecutor(taskExecutor) + .build(); + } + + /** + * 의약품 상세정보 처리용 Processor Bean 정의 + * + * @return CombineProcessor 인스턴스 + * @author 함예정 + * @since 2025-05-01 + */ + @Bean + public DrugDetailProcessor processor() { + return new DrugDetailProcessor(apiRequestManager, apiResponseMapper); + } + + /** + * 처리된 의약품 상세정보를 DB에 저장하는 Writer Bean 정의 + * + * @param repository 의약품 상세정보 저장용 JPA Repository + * @return DrugDetailWriter 인스턴스 + * + * @author 함예정 + * @since 2025-05-01 + */ +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java new file mode 100644 index 0000000..b5f52cd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java @@ -0,0 +1,135 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor.ImageScrapProcessor; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor.ImageTotalPageCalculator; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader.PageRangePartitioner; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader.PartitionedPageReader; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.writer.DrugImageWriter; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugImgRepository; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.List; + +/** + * 의약품 상세정보 수집을 위한 Spring Batch 설정 클래스 + * + * @field imageTotalPageCheckStep 전체 페이지 수를 계산하는 Step + * @field imageMasterStep 의약품 상세정보 수집을 위한 Master Step + * @field imageScrapStep 의약품 상세정보 수집을 위한 Step + * @since 2025-05-02 + */ +@Configuration +public class ImageStepConfig { + private static final String TASK_EXECUTOR = "singleItemExecutor"; + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + private final TaskExecutor taskExecutor; + + public ImageStepConfig(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper, + @Qualifier(TASK_EXECUTOR) + TaskExecutor taskExecutor) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + this.taskExecutor = taskExecutor; + } + + + /** + * 전체 페이지 수를 계산하는 Tasklet 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param imageTotalPageCalculator 페이지 계산 로직을 수행하는 Tasklet + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + Step imageTotalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, + ImageTotalPageCalculator imageTotalPageCalculator) { + return new StepBuilder("imageTotalPageCheck", jobRepository) + .tasklet(imageTotalPageCalculator, txManager) + .build(); + } + + @Bean + public Step imageMasterStep(JobRepository jobRepository, + PageRangePartitioner partitioner, + Step imageScrapStep) { + return new StepBuilder("imageMasterStep", jobRepository) + .partitioner(imageScrapStep.getName(), partitioner) + .step(imageScrapStep) + .gridSize(15) + .taskExecutor(taskExecutor) + .build(); + } + + /** + * 상세 페이지별로 데이터를 수집하고 처리 및 저장하는 Chunk 기반 Step 정의 + * + * @param jobRepository JobRepository 인스턴스 + * @param txManager 트랜잭션 매니저 + * @param processor 데이터를 처리하는 Processor + * @param writer 데이터를 저장하는 Writer + * @return Step 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public Step imageScrapStep(JobRepository jobRepository, + PlatformTransactionManager txManager, + ImageScrapProcessor processor, + DrugImageWriter writer, + PartitionedPageReader reader) { + return new StepBuilder("imageScrapStep", jobRepository) + .>chunk(1, txManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * 의약품 상세정보 처리용 Processor Bean 정의 + * + * @return CombineProcessor 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public ImageScrapProcessor imageScrapProcessor() { + return new ImageScrapProcessor(apiRequestManager, apiResponseMapper); + } + + /** + * 처리된 의약품 상세정보를 DB에 저장하는 Writer Bean 정의 + * + * @param repository 의약품 상세정보 저장용 JPA Repository + * @return DrugDetailWriter 인스턴스 + * @author 함예정 + * @since 2025-05-02 + */ + @Bean + public DrugImageWriter imageScrapWriter(DrugImgRepository repository) { + return new DrugImageWriter(repository); + } + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java new file mode 100644 index 0000000..b6377c9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java @@ -0,0 +1,106 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDate; + + +/** + * ObjectMapper를 통해 JSON 응답 데이터를 매핑하기 위한 DTO 클래스입니다. + * 의약품 상세 정보 수집 배치 작업에서 사용됩니다. + *

+ * 일부 필드는 @JsonProperty를 통해 외부 필드명과 매핑되며, + * 생성자에서는 일반의약품 여부(isGeneral)를 ETC_OTC_CODE 값으로 판별합니다. + * + * @field drugId 의약품 ID + * @field drugName 의약품 이름 + * @field company 제조사/판매사 이름 + * @field permitDate 허가일 + * @field cancelDate 취소일 + * @field cancelName 취소 유형명 + * @field isGeneral 일반의약품 여부 + * @field isHerbal 한약재 여부 + * @field materialInfo 성분 정보 + * @field storeMethod 보관 방법 + * @field validTerm 유효기간 + * @field efficacy 효능 + * @field usage 사용법 + * @field precaution 주의사항 + * @since 2025-04-21 + */ +@Getter +@ToString +public class DrugDetailRequest { + + @JsonProperty("ITEM_SEQ") + private Long drugId; + + @JsonProperty("ITEM_NAME") + private String drugName; + + @JsonProperty("ENTP_NAME") + private String company; + + @JsonProperty("ITEM_PERMIT_DATE") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") + private LocalDate permitDate; + + @JsonProperty("CANCEL_DATE") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") + private LocalDate cancelDate; + + @JsonProperty("CANCEL_NAME") + private String cancelName; + + private boolean isGeneral; + + private boolean isHerbal; + + private String materialInfo; + + @JsonProperty("STORAGE_METHOD") + private String storeMethod; + + @JsonProperty("VALID_TERM") + private String validTerm; + + private String efficacy; + private String usage; + private String precaution; + + /** + * ETC_OTC_CODE 값을 기반으로 일반의약품 여부를 판단합니다. + * + * @param drugType "전문의약품"이면 false, 그 외는 true + * @author 함예정 + * @since 2025-04-21 + */ + @JsonCreator + public DrugDetailRequest(@JsonProperty("ETC_OTC_CODE") String drugType) { + this.isGeneral = !"전문의약품".equals(drugType); + } + + public void changeMaterialInfo(String materialInfo) { + this.materialInfo = materialInfo; + } + + public void changeUsage(String usage) { + this.usage = usage; + } + + public void changeEfficacy(String efficacy) { + this.efficacy = efficacy; + } + + public void changePrecaution(String precaution) { + this.precaution = precaution; + } + + public void changeIsHerbal(boolean isHerbal) { + this.isHerbal = isHerbal; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java new file mode 100644 index 0000000..a46702f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java @@ -0,0 +1,44 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.reader.DetailPageNumberReader; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * 의약품 상세정보 수집을 위한 총 페이지 수를 계산하는 Tasklet입니다. + * + * ApiRequestManager: API 요청을 관리하는 클래스 + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class DetailTotalPageCalculator implements Tasklet { + private final ApiRequestManager apiRequestManager; + + /** + * 총 페이지 수를 API에서 조회한 후, DetailPageNumberReader에 설정합니다. + * DetailPageNumberReader: 상세 페이지 번호를 읽어오는 Reader + * + * @param contribution 현재 step의 기여도 + * @param chunkContext 현재 chunk 실행 컨텍스트 + * @return RepeatStatus.FINISHED (작업 완료 신호) + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int totalPage = apiRequestManager.getDetailTotalPage(); + DetailPageNumberReader.setTotalPage(totalPage); + + log("[CombineProcessor] 총 페이지 수 계산 완료: " + totalPage); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java new file mode 100644 index 0000000..f5e2c37 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java @@ -0,0 +1,109 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser.MaterialParser; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser.XMLParser; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.DrugDetailRequestMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +import org.springframework.batch.item.ItemProcessor; + +import java.util.List; + +/** + * 의약품 상세정보 API 응답을 처리하는 ItemProcessor 구현체입니다. + * 입력으로 페이지 번호를 받아 해당 페이지의 데이터를 API로 조회하고, + * XML 및 기타 데이터를 파싱하여 도메인 엔티티 리스트로 변환합니다. + * 성분, 효능, 사용법, 주의사항 등의 세부 항목을 파싱하며, + * 한약 여부는 주의사항 텍스트에 특정 키워드가 포함되어 있는지를 기반으로 판단합니다. + *

+ * ApiRequestManager: API 요청을 관리하는 클래스 + * ApiResponseMapper: API 응답을 매핑하는 클래스 + * + * @since 2025-05-02 + */ +public class DrugDetailProcessor implements ItemProcessor> { + private final String materialTagName = "MATERIAL_NAME"; + private final String efficacyTagName = "EE_DOC_DATA"; + private final String usageTagName = "UD_DOC_DATA"; + private final String precautionTagName = "NB_DOC_DATA"; + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + + public DrugDetailProcessor(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + } + + /** + * 입력된 페이지 번호에 해당하는 상세 API 데이터를 조회하고, + * 필요한 필드를 파싱 및 변환하여 엔티티 리스트로 반환합니다. + *

+ * 작업 순서는 아래와 같습니다: + * 1) API 요청을 통해 JSON 응답을 가져옵니다. + * 2) JSON 응답에서 필요한 항목을 추출합니다. + * 3) 각 항목에 대해 XML 데이터를 파싱합니다. + * 4) 성분, 효능, 사용법, 주의사항을 파싱하여 DTO에 설정합니다. + * 5) 주의사항에 한약 관련 키워드가 포함되어 있는지 확인하여 플래그를 설정합니다. + * 6) DTO를 엔티티로 변환하여 리스트로 반환합니다. + * + * @param pageNumber API 조회에 사용할 페이지 번호 + * @return 변환된 DrugDetailEntity 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public List process(Integer pageNumber) { + + String response = apiRequestManager.fetchDetailData(pageNumber); + JsonNode items = apiRequestManager.getItemsFromResponse(response); + List drugItems = apiResponseMapper.toListFromDrugDetails(items); + + for (int i = 0; i < drugItems.size(); i++) { + DrugDetailRequest drugDetail = drugItems.get(i); + JsonNode item = items.get(i); + + String materialRawData = item.get(materialTagName).asText(); + String materialInfo = MaterialParser.parseMaterial(materialRawData); + + drugDetail.changeMaterialInfo(materialInfo); + + String efficacyXmlText = item.get(efficacyTagName).asText(); + String efficacy = XMLParser.toJson(efficacyXmlText); + drugDetail.changeEfficacy(efficacy); + + String usageXmlText = item.get(usageTagName).asText(); + String usages = XMLParser.toJson(usageXmlText); + drugDetail.changeUsage(usages); + + String precautionXmlText = item.get(precautionTagName).asText(); + String precautions = XMLParser.toJson(precautionXmlText); + drugDetail.changePrecaution(precautions); + + String precaution = drugDetail.getPrecaution(); + if (isContainHerbalText(precaution)) { + drugDetail.changeIsHerbal(true); + } + } + return drugItems.stream() + .map(DrugDetailRequestMapper::toEntityFromRequest) + .toList(); + } + + /** + * 주의사항 텍스트에 한약 관련 키워드가 포함되어 있는지 확인합니다. + * + * @param precaution 주의사항 텍스트 + * @return boolean + * true: 포함됨, false: 포함되지 않음 + * @author 함예정 + * @since 2025-05-02 + */ + private static boolean isContainHerbalText(String precaution) { + return precaution != null && (precaution.contains("한의사") || precaution.contains("한약사")); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java new file mode 100644 index 0000000..8bf8b5d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.reader; + +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import lombok.Getter; +import org.springframework.batch.item.ItemReader; +import org.springframework.stereotype.Component; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Spring Batch에서 각 Step 실행 시 처리할 페이지 번호를 순차적으로 제공하는 Reader 클래스 + *

+ * 총 페이지 수를 기준으로 1부터 시작하여 차례대로 page 번호를 반환하며, + * 모든 페이지가 반환되면 null을 반환하여 반복을 종료한다. + * + * @field pageQueue 페이지 번호를 저장하는 큐 + * @since 2025-05-02 + */ +@Component +@Getter +public class DetailPageNumberReader implements ItemReader { + + private static final Queue pageQueue = new ConcurrentLinkedQueue<>(); + + public static void setTotalPage(int totalPage) { + pageQueue.clear(); + for (int i = 1; i <= totalPage; i++) { + pageQueue.add(i); + } + } + + /** + * 현재 페이지 번호를 반환하고, 다음 호출을 위해 내부 카운터를 증가시킨다. + * 총 페이지 수를 초과하면 null을 반환하여 종료를 알린다. + * + * @return 현재 처리할 페이지 번호 또는 null(모든 페이지 처리 완료 시) + */ + @Override + public Integer read() { + Integer page = pageQueue.poll(); + if (page != null) { + LogUtil.log(Thread.currentThread().getName() + " - Page 할당: " + page); + } + return page; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java new file mode 100644 index 0000000..6382ef2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.writer; + +import java.util.List; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugDetailJpaRepository; + +import lombok.RequiredArgsConstructor; + +/** + * 의약품 상세정보 Entity 리스트를 JPA Repository를 통해 일괄 저장하는 Writer입니다. + *

+ * Chunk 내부의 각 List 항목을 반복 처리하며, + * 각 리스트는 한 API 페이지에서 파싱된 다수의 의약품 데이터를 나타냅니다. + *

+ * DrugDetailJpaRepository: 의약품 상세정보를 저장하는 JPA Repository + * + * @field repository 의약품 상세정보를 저장하는 JPA Repository + * @since 2025-05-02 + */ + +@Component +@RequiredArgsConstructor +public class DrugDetailWriter implements ItemWriter> { + + private final DrugDetailJpaRepository repository; + /** + * Chunk 단위로 전달된 Entity 리스트를 순회하며 JPA를 통해 일괄 저장합니다. + * + * @param chunk 페이지별로 수집된 의약품 상세정보 엔티티 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public void write(Chunk> chunk) { + + for (List items : chunk.getItems()) { + repository.saveAll(items); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java new file mode 100644 index 0000000..7921da2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java @@ -0,0 +1,74 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor; + +import java.util.List; + +import org.springframework.batch.item.ItemProcessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.DrugImageRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugImageRequestMapper; + +/** + * pageNumber를 받아 + * 외부 REST API 호출 → JSON → DTO 리스트 변환 → Entity 리스트로 매핑 + * 작업을 수행하는 프로세서 클래스입니다. + * + * @field apiRequestManager 외부 API 호출을 위한 매니저 + * @field apiResponseMapper API 응답을 DTO로 변환하는 매퍼 + * @since 2025-05-02 + */ +public class ImageScrapProcessor implements ItemProcessor> { + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + + public ImageScrapProcessor(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + } + + /** + * 주어진 페이지 번호에 대해 외부 API를 호출하고, + * 응답을 DTO 리스트로 변환한 후, Entity 리스트로 매핑합니다. + * + * @param pageNumber 처리할 페이지 번호 + * @return DrugImgEntity 리스트 + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public List process(Integer pageNumber) { + LogUtil.log(Thread.currentThread().getName() + " - " + pageNumber + " page 처리 시작"); + String response = apiRequestManager.fetchImageData(pageNumber); + JsonNode items = apiRequestManager.getItemsFromResponse(response); + List drugItems = apiResponseMapper.toListFromDrugImages(items); + + for (DrugImageRequest item : drugItems) { + String productImage = apiRequestManager.getImage(item.getDrugId()); + if (notEmptyProductImage(productImage)) { + item.changeProductImageUrl(productImage); + } + } + + return drugItems.stream() + .map(DrugImageRequestMapper::toEntityFromRequest) + .toList(); + } + + /** + * 주어진 productImage가 null이 아니고 길이가 10보다 큰지 확인합니다. + * + * @param productImage 검사할 이미지 URL + * @return true: 유효한 이미지 URL, false: 유효하지 않은 이미지 URL + * @author 함예정 + * @since 2025-05-02 + */ + private boolean notEmptyProductImage(String productImage) { + return productImage != null && productImage.length() > 10; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java new file mode 100644 index 0000000..87f9cc1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java @@ -0,0 +1,49 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader.PageRangePartitioner; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * 이미지 총 페이지 수를 계산하는 Tasklet 클래스입니다. + *

+ * 이 클래스는 Spring Batch의 Tasklet 인터페이스를 구현하여, + * 이미지 API에서 총 페이지 수를 가져와 PageRangePartitioner에 설정합니다. + *

+ * + * @field pageRangePartitioner 페이지 범위를 나누는 Partitioner + * @field apiRequestManager API 요청을 관리하는 매니저 + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class ImageTotalPageCalculator implements Tasklet { + + private final PageRangePartitioner pageRangePartitioner; + private final ApiRequestManager apiRequestManager; + + /** + * 이미지 API에서 총 페이지 수를 가져와 PageRangePartitioner에 설정합니다. + * + * @param contribution 현재 step의 기여도 + * @param chunkContext 현재 chunk 실행 컨텍스트 + * @return RepeatStatus.FINISHED (작업 완료 신호) + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int totalPage = apiRequestManager.getImageTotalPage(); + pageRangePartitioner.setTotalPages(totalPage); + + log("[Image-Total-Page-Calculator] 총 페이지 수 계산 완료: " + totalPage); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PageRangePartitioner.java similarity index 54% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PageRangePartitioner.java index 9711e31..3836617 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/image/reader/PageRangePartitioner.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PageRangePartitioner.java @@ -1,20 +1,32 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.image.reader; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader; import java.util.HashMap; import java.util.Map; +import lombok.Setter; import org.springframework.batch.core.partition.support.Partitioner; import org.springframework.batch.item.ExecutionContext; import org.springframework.stereotype.Component; +/** + * PageRangePartitioner는 페이지 범위를 기준으로 파티션을 나누는 클래스입니다. + * 주어진 총 페이지 수를 gridSize에 따라 나누어 각 파티션의 시작과 끝 페이지를 설정합니다. + * + * @field totalPages 총 페이지 수 + * @since 2025-05-02 + */ @Component +@Setter public class PageRangePartitioner implements Partitioner { private int totalPages = 0; - public void setTotalPages(int totalPages) { - this.totalPages = totalPages; - } - + /** + * partition 메서드는 주어진 gridSize에 따라 페이지 범위를 나누어 + * 각 파티션의 시작과 끝 페이지를 설정합니다. + * + * @param gridSize 파티션의 개수 + * @return 각 파티션의 시작과 끝 페이지를 포함하는 맵 + */ @Override public Map partition(int gridSize) { int range = totalPages / gridSize; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java new file mode 100644 index 0000000..647128f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java @@ -0,0 +1,65 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * PartitionedPageReader는 Spring Batch의 ItemReader를 구현하여 + * 이미지 페이지를 읽어오는 역할을 합니다. + * 이 클래스는 StepScope로 설정되어, 각 파티션에 대해 독립적인 상태를 유지합니다. + * + * @field currentPage 현재 읽고 있는 페이지 번호 + * @field endPage 읽을 페이지의 마지막 번호 + * @since 2025-05-02 + * @author 함예정 + */ +@Component +@StepScope +public class PartitionedPageReader implements ItemReader { + private int currentPage; + private final int endPage; + + /** + * PartitionedPageReader의 생성자. + *

+ * Spring Batch의 파티셔닝에서 사용되는 {@code stepExecutionContext}로부터 startPage와 endPage 값을 주입받아, + * 각 슬레이브 Step이 처리해야 할 페이지 범위를 설정합니다. + *

+ * {@code @Value}는 Spring의 Expression Language(SpEL)를 사용하여 실행 컨텍스트에서 값을 추출합니다. + * 예를 들어, 파티셔너가 {@code startPage=1, endPage=10}으로 설정하면 이 값이 해당 슬레이브에 주입됩니다. + * + * @param startPage 파티션에서 처리할 시작 페이지 번호 + * @param endPage 파티션에서 처리할 마지막 페이지 번호 + * @author 함예정 + * @since 2025-05-02 + */ + public PartitionedPageReader( + @Value("#{stepExecutionContext['startPage']}") int startPage, + @Value("#{stepExecutionContext['endPage']}") int endPage) { + log("[Reader bean 생성] startPage=" + startPage + ", endPage=" + endPage); + this.currentPage = startPage; + this.endPage = endPage; + } + + /** + * 현재 페이지 번호를 반환하고 다음 페이지로 증가시킵니다. + *

+ * 지정된 endPage를 초과하면 {@code null}을 반환하여 더 이상 읽을 데이터가 없음을 Spring Batch에 알립니다. + * + * @return 현재 페이지 번호, 또는 endPage를 초과하면 null + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public Integer read() { + log("이미지 페이지 Read: " + currentPage); + if (currentPage > endPage) { + return null; + } + return currentPage++; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java new file mode 100644 index 0000000..ffbc774 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.writer; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import java.util.List; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugImgRepository; + +/** + * Entity 리스트를 받아 JPA Repository로 한 번에 저장하는 Writer입니다. + * + * @field repository 이미지 정보를 저장하는 JPA Repository + * @since 2025-05-02 + */ +public class DrugImageWriter implements ItemWriter> { + + private final DrugImgRepository repository; + + public DrugImageWriter(DrugImgRepository repository) { + this.repository = repository; + } + + /** + * Chunk 단위로 전달된 DrugImgEntity 리스트를 JPA Repository에 저장합니다. + * + * @param chunk 저장할 DrugImgEntity 리스트를 포함하는 Chunk + * @author 함예정 + * @since 2025-05-02 + */ + @Override + public void write(Chunk> chunk) { + log(Thread.currentThread().getName() + " - Start Write"); + for (List items : chunk.getItems()) { + repository.saveAllAndFlush(items); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java new file mode 100644 index 0000000..17d9dfe --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java @@ -0,0 +1,74 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.DrugImageRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * API 응답(JsonNode)을 DTO로 변환하는 Mapper 클래스입니다. + * JsonNode를 DTO 리스트로 변환하는 메서드를 제공합니다. + * + * @fields objectMapper Jackson ObjectMapper 인스턴스 + * @modified 2025-05-02 + * @since 2025-04-22 + */ +@Component +@RequiredArgsConstructor +public class ApiResponseMapper { + private final ObjectMapper objectMapper; + + /** + * JsonNode를 DrugDetailRequest 리스트로 변환합니다. + * + * @param items JsonNode 형태의 데이터 + * @return DrugDetailRequest 리스트 + * @throws ParserBatchException JSON 타입 변환 실패 시 발생 + * @author 함예정 + * @modified 2025-05-02 + * @since 2025-04-22 + */ + public List toListFromDrugDetails(JsonNode items) { + try { + return objectMapper.readValue( + items.toString(), new TypeReference<>() { + } + ); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "타입변환 실패(JsonNode -> List"); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + + /** + * JsonNode를 DrugImageRequest 리스트로 변환합니다. + * + * @param items JsonNode 형태의 데이터 + * @return DrugImageRequest 리스트 + * @throws ParserBatchException JSON 타입 변환 실패 시 발생 + * @author 함예정 + * @modified 2025-05-02 + * @since 2025-04-22 + */ + public List toListFromDrugImages(JsonNode items) { + try { + return objectMapper.readValue( + items.toString(), new TypeReference<>() { + } + ); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "타입변환 실패(JsonNode -> List"); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java new file mode 100644 index 0000000..20aebec --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java @@ -0,0 +1,38 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +/** + * 약품 상세 정보 요청을 엔티티로 변환하는 매퍼 클래스입니다. + * + * @since 2025-05-02 + */ +public class DrugDetailRequestMapper { + + /** + * DrugDetailRequest 객체를 DrugDetailEntity 객체로 변환합니다. + * + * @param r 변환할 DrugDetailRequest 객체 + * @return 변환된 DrugDetailEntity 객체 + * @author 함예정 + * @since 2025-05-02 + */ + public static DrugDetailEntity toEntityFromRequest(DrugDetailRequest r) { + return DrugDetailEntity.builder() + .drugId(r.getDrugId()) + .drugName(r.getDrugName()) + .company(r.getCompany()) + .permitDate(r.getPermitDate()) + .isGeneral(r.isGeneral()) + .materialInfo(r.getMaterialInfo()) + .storeMethod(r.getStoreMethod()) + .validTerm(r.getValidTerm()) + .efficacy(r.getEfficacy()) + .usage(r.getUsage()) + .precaution(r.getPrecaution()) + .cancelDate(r.getCancelDate()) + .cancelName(r.getCancelName()) + .isHerbal(r.isHerbal()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java new file mode 100644 index 0000000..fa96107 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java @@ -0,0 +1,108 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.error.ScraperErrorCode; + +/** + * 원재료 정보를 파싱하여 JSON 배열 형식의 문자열로 변환하는 유틸리티 클래스입니다. + */ +public class MaterialParser { + + /** + * 원재료 문자열을 파싱하여 JSON 배열 형태의 문자열로 변환합니다. + * + * @param raw 원재료 정보가 담긴 문자열 + * (세미콜론으로 블록 구분, 파이프로 키-값 쌍 구분) + * @return JSON 배열 형태의 문자열 + * @author 함예정 + * @since 2025-04-21 + */ + public static String parseMaterial(String raw) { + ObjectMapper objectMapper = new ObjectMapper(); + ArrayNode resultArray = objectMapper.createArrayNode(); + String[] blocks = splitBlock(raw); + parsingBlocksAndPutArrayItem(blocks, resultArray); + return convertString(objectMapper, resultArray); + } + + /** + * 블록 배열을 파싱하여 JSON 배열에 항목으로 추가합니다. + * + * @param blocks 원재료 블록 배열 + * @param resultArray 결과를 저장할 JSON 배열 + */ + private static void parsingBlocksAndPutArrayItem(String[] blocks, ArrayNode resultArray) { + for (String block : blocks) { + block = block.trim(); + if (block.isEmpty()) { + continue; + } + String[] pairs = splitByPipe(block); + ObjectNode item = makeItem(pairs); + resultArray.add(item); + } + } + + /** + * JSON 배열을 문자열로 변환합니다. + * + * @param objectMapper Jackson ObjectMapper 인스턴스 + * @param arrayNode 변환할 JSON 배열 + * @return JSON 문자열 + * @throws ScraperException JSON 변환 실패 시 발생 + */ + private static String convertString(ObjectMapper objectMapper, ArrayNode arrayNode) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "JSON 문자열을 String으로 변환하는 중 오류 발생: " + e.getMessage()); + throw new ScraperException(ScraperErrorCode.MATERIAL_PARSING_FAIL); + } + } + + /** + * 키-값 쌍 배열로부터 JSON 객체를 생성합니다. + * + * @param pairs 파이프로 구분된 키-값 쌍 배열 + * @return 생성된 JSON 객체 + */ + private static ObjectNode makeItem(String[] pairs) { + ObjectNode item = new ObjectMapper().createObjectNode(); + for (String pair : pairs) { + String[] kv = pair.split(":", 2); + String key = kv[0].trim(); + String value = ""; + if (kv.length == 2) { + value = kv[1].trim(); + } + item.put(key, value); + } + return item; + } + + /** + * 블록 내 키-값 쌍을 파이프(|) 기호로 분리합니다. + * + * @param block 블록 문자열 + * @return 키-값 쌍 배열 + */ + private static String[] splitByPipe(String block) { + return block.split("\\|"); + } + + /** + * 원재료 정보를 세미콜론(;) 기준으로 블록으로 분리합니다. + * + * @param raw 원재료 문자열 + * @return 블록 배열 + */ + private static String[] splitBlock(String raw) { + return raw.split(";"); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java new file mode 100644 index 0000000..9040788 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java @@ -0,0 +1,426 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.error.ScraperErrorCode; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * XML 문자열을 파싱하여 JSON 문자열로 변환하는 클래스입니다. + * + * @since 2025-04-21 + */ +public class XMLParser { + private static final Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); + private static final Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); + + private static final ObjectMapper mapper = new ObjectMapper(); + private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + /** + * XML 문자열을 파싱하여 JSON 문자열로 변환합니다. + * + * @param xml 변환할 XML 문자열 + * @return 변환된 JSON 문자열 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + public static String toJson(String xml) { + + if (isXmlNull(xml)) { + return "{\"\": \"\"}"; + } + + Document doc = parseXmlString(xml); + Element root = doc.getDocumentElement(); + + List allSections = new ArrayList<>(); + List allArticles = new ArrayList<>(); + List allParagraphs = new ArrayList<>(); + + Map sectionMap = new HashMap<>(); + Map articleMap = new HashMap<>(); + + DocTag docTag = new DocTag(root, allSections); + parseSections(root, allSections, sectionMap); + parseArticles(root, allArticles, articleMap, sectionMap); + parseParagraph(root, allParagraphs, articleMap); + return convertJson(docTag); + } + + /** + * DocTag 객체를 JSON 문자열로 변환합니다. + * + * @param docTag 변환할 DocTag 객체 + * @return JSON 문자열 + * @throws RuntimeException JSON 변환 실패 시 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static String convertJson(DocTag docTag) { + try { + return mapper.writeValueAsString(docTag); + } catch (JsonProcessingException e) { + log(LogLevel.ERROR, "JSON 변환 실패", e); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + + /** + * XML에서 PARAGRAPH 태그를 파싱하여 ParagraphTag 리스트에 추가합니다. + * + * @param root XML 루트 엘리먼트 + * @param allParagraphs 파싱된 ParagraphTag 리스트 + * @param articleMap ARTICLE 엘리먼트와 ArticleTag 매핑 정보 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static void parseParagraph(Element root, List allParagraphs, Map articleMap) { + NodeList paraNodes = root.getElementsByTagName("PARAGRAPH"); + + if (paraNodes.getLength() != 0) { + for (int i = 0; i < paraNodes.getLength(); i++) { + Element paragraphElement = (Element) paraNodes.item(i); + ParagraphTag paragraphTag = new ParagraphTag(); + paragraphTag.tagName = cleanText(paragraphElement.getAttribute("tagName")); + paragraphTag.textIndent = cleanText(paragraphElement.getAttribute("textIndent")); + paragraphTag.marginLeft = cleanText(paragraphElement.getAttribute("marginLeft")); + paragraphTag.text = cleanText(paragraphElement.getTextContent().trim()); + + if (!isEmptyTagNameOrTagText(paragraphTag)) { + allParagraphs.add(paragraphTag); + } + + mapSectionFromArticle(articleMap, paragraphTag, paragraphElement); + } + } + + } + + private static boolean isEmptyTagNameOrTagText(ParagraphTag paragraphTag) { + return paragraphTag.tagName.isEmpty() || paragraphTag.text.isEmpty(); + } + + /** + * XML에서 ARTICLE 태그를 파싱하여 ArticleTag 리스트에 추가합니다. + * + * @param root XML 루트 엘리먼트 + * @param allArticles 파싱된 ArticleTag 리스트 + * @param articleMap ARTICLE 엘리먼트와 ArticleTag 매핑 정보 + * @param sectionMap SECTION 엘리먼트와 SectionTag 매핑 정보 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static void parseArticles(Element root, List allArticles, + Map articleMap, + Map sectionMap) { + NodeList artNodes = root.getElementsByTagName("ARTICLE"); + if (artNodes.getLength() > 0) { + for (int i = 0; i < artNodes.getLength(); i++) { + Element artElement = (Element) artNodes.item(i); + ArticleTag articleTag = new ArticleTag(); + articleTag.title = cleanText(artElement.getAttribute("title")); + articleTag.paragraphs = new ArrayList<>(); + + allArticles.add(articleTag); + articleMap.put(artElement, articleTag); + mapSectionFromArticle(sectionMap, articleTag, artElement); + } + } + + } + + /** + * 상위 엘리먼트를 기반으로 해당 태그를 부모 태그에 연결합니다. + * + * @param map 상위 엘리먼트와 태그 매핑 정보 + * @param tags 현재 태그 + * @param element 현재 엘리먼트 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static void mapSectionFromArticle(Map map, Tags tags, Element element) { + Element parentElement = (Element) element.getParentNode(); + Tags parentTag = map.get(parentElement); + if (parentTag != null) { + parentTag.addTag(tags); + } + } + + /** + * XML에서 SECTION 태그를 파싱하여 SectionTag 리스트에 추가합니다. + * + * @param root XML 루트 엘리먼트 + * @param allSections 파싱된 SectionTag 리스트 + * @param sectionMap SECTION 엘리먼트와 SectionTag 매핑 정보 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static void parseSections(Element root, List allSections, Map sectionMap) { + NodeList secNodes = root.getElementsByTagName("SECTION"); + + if (secNodes.getLength() > 0) { + for (int i = 0; i < secNodes.getLength(); i++) { + Element secEl = (Element) secNodes.item(i); + SectionTag secDto = new SectionTag(); + secDto.title = cleanText(secEl.getAttribute("title")); + secDto.articles = new ArrayList<>(); + + allSections.add(secDto); + sectionMap.put(secEl, secDto); + } + } + } + + /** + * XML 문자열을 파싱하여 Document 객체로 변환합니다. + * + * @param xml 파싱할 XML 문자열 + * @return 파싱된 Document 객체 + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static Document parseXmlString(String xml) { + try { + return documentBuilderFactory.newDocumentBuilder() + .parse(new InputSource(new StringReader(xml))); + } catch (Exception e) { + log(LogLevel.ERROR, "XML 파싱 실패", e); + throw new ScraperException(ScraperErrorCode.PARSING_ERROR); + } + } + + /** + * XML 문자열이 null 이거나 비어있는지 확인합니다. + * + * @param xml 확인할 XML 문자열 + * @return null 또는 비어있으면 true, 그렇지 않으면 false + * @author 함예정, 이해창 + * @since 2025-04-21 + */ + private static boolean isXmlNull(String xml) { + if (xml == null || xml.trim().isEmpty() || xml == "null") { + return true; + } else { + return false; + } + } + + /** + * XML 루트 태그를 표현하는 클래스입니다. + * XML Parser 내부에서만 사용되는 클래스입니다. + * + * @since 2025-04-21 + */ + private static class DocTag implements Tags { + public String title; + public String type; + public List sections; + + DocTag(Element root, List sections) { + this.title = cleanText(root.getAttribute("title")); + this.type = root.getAttribute("type"); + this.sections = sections; + } + + /** + * DocTag에 Section 태그를 추가합니다. + * Json 형태의 객체를 만드는 데 사용됩니다. + * + * @param tags 추가할 태그 + * @author 함예정 + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + sections.add((SectionTag) tags); + } + + } + + /** + * SECTION 태그를 표현하는 클래스입니다. + * + * @since 2025-04-21 + */ + private static class SectionTag implements Tags { + public String title; + public List articles; + + /** + * SectionTag에 Article 태그를 추가합니다. + * Json 형태의 객체를 만드는 데 사용됩니다. + * + * @param tags + * @author 함예정 + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + articles.add((ArticleTag) tags); + } + + } + + /** + * ARTICLE 태그를 표현하는 클래스입니다. + * + * @since 2025-04-21 + */ + private static class ArticleTag implements Tags { + public String title; + public List paragraphs; + + /** + * ArticleTag에 Paragraph 태그를 추가합니다. + * Json 형태의 객체를 만드는 데 사용됩니다. + * + * @param tags + * @author 함예정 + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + paragraphs.add((ParagraphTag) tags); + } + + } + + /** + * PARAGRAPH 태그를 표현하는 클래스입니다. + * + * @since 2025-04-21 + */ + private static class ParagraphTag implements Tags { + public String tagName; + public String textIndent; + public String marginLeft; + public String text; + + /** + * ParagraphTag는 하위 태그를 가지지 않으므로 addTag 메서드는 구현하지 않습니다. + * + * @param tags + * @author 함예정 + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + log(LogLevel.WARN, "ParagraphTag는 하위 태그를 가지지 않습니다."); + } + + } + + /** + * 태그 클래스 간 공통 인터페이스입니다. + * + * @since 2025-04-21 + */ + private interface Tags { + /** + * 해당 클래스의 하위 태그를 추가하는 메서드입니다. + * Json 형태의 객체를 만드는 데 사용됩니다. + * + * @param tags 추가할 태그 + * @author 함예정 + * @since 2025-04-21 + */ + void addTag(Tags tags); + } + + /** + * 불필요한 문자를 제거하여 텍스트를 정리합니다. + * + * @param text + * @return String 정리된 텍스트 + * @author 박찬병 + * @since 2025-04-21 + */ + private static String cleanText(String text) { + Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); + String tempText = TAG_REGEX.matcher(text) + .replaceAll("") + .replaceAll(" ", " ") + .replaceAll("● ", "") + .replaceAll("○ ", "") + .replaceAll("∎ ", "") + .replaceAll("- ", ""); + return decodeHtml(tempText).trim(); + } + + /** + * HTML 엔티티(10진수 &#DDD; 및 16진수 &#xHHHH;)를 대응하는 문자로 디코딩합니다. 예: "foo•bar" → "foo•bar", + * "foo•bar" → "foo•bar" + * + * @param input 엔티티를 포함한 문자열 + * @return 디코딩된 문자열 + * @author 박찬병 + * @modify 2025-05-03 함예정 + * - 메소드 분리 + */ + private static String decodeHtml(String input) { + String tempText = decimalEntityDecode(input); + String result = hexEntityDecode(tempText); + return result; + } + + /** + * 문자열 내의 16진수 HTML 엔티티(&#xHHHH;)를 해당 유니코드 문자로 변환합니다. + * + * @param result 16진수 엔티티를 포함한 문자열 + * @return 변환된 문자열 + * @author 박찬병 + * @modify 2025-05-03 함예정 + * - 메소드 분리 + */ + private static String hexEntityDecode(String result) { + Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(result); + StringBuffer sb = new StringBuffer(); + while (hexMatcher.find()) { + int code = Integer.parseInt(hexMatcher.group(1), 16); + hexMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + hexMatcher.appendTail(sb); + return sb.toString(); + } + + /** + * 문자열 내의 10진수 HTML 엔티티(&#DDD;)를 해당 유니코드 문자로 변환합니다. + * + * @param result 10진수 엔티티를 포함한 문자열 + * @return 변환된 문자열 + * @author 박찬병 + * @modify 2025-05-03 함예정 + * - 메소드 분리 + */ + private static String decimalEntityDecode(String result) { + Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(result); + StringBuffer sb = new StringBuffer(); + while (decMatcher.find()) { + int code = Integer.parseInt(decMatcher.group(1)); + decMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + decMatcher.appendTail(sb); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/DrugImageRequest.java similarity index 66% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/DrugImageRequest.java index 87353cd..8bc4761 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/dto/DrugImageRequest.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/DrugImageRequest.java @@ -1,9 +1,5 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.dto; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto; -import java.time.LocalDate; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingRequestText.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/EmbeddingRequestText.java similarity index 64% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingRequestText.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/EmbeddingRequestText.java index 345e35a..de76814 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/embedding/model/EmbeddingRequestText.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/EmbeddingRequestText.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugDetailEntity.java similarity index 74% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugDetailEntity.java index 426a546..fec641a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugDetailEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugDetailEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonFormat; @@ -69,23 +69,4 @@ public class DrugDetailEntity { @Builder.Default private Boolean isHerbal = false; - public DrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { - this.isGeneral = !"전문의약품".equals(drugType); - } - - public void changeMaterialInfo(String materialInfo){ - this.materialInfo = materialInfo; - } - - public void changeUsage(String usage) { - this.usage = usage; - } - - public void changeEfficacy(String efficacy) { - this.efficacy = efficacy; - } - - public void changePrecaution(String precaution) { - this.precaution = precaution; - } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java similarity index 82% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java index 57bcf33..e93548e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/ApiDataDrugImgEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -17,7 +17,7 @@ @AllArgsConstructor @ToString @Table(name = "API_DATA_DRUG_IMG") -public class ApiDataDrugImgEntity { +public class DrugImgEntity { @Id @Column(name = "ITEM_SEQ") private Long drugId; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugRawDataEntity.java similarity index 93% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugRawDataEntity.java index b9d10ee..b8a119e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/persistence/repository/entity/DrugRawDataEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugRawDataEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity; import java.time.LocalDate; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java new file mode 100644 index 0000000..1ad7b70 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java @@ -0,0 +1,101 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.adapter; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugRawDataMapper; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugRawDataPort; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; + +import lombok.RequiredArgsConstructor; + +/** + * 공공 API로부터 조회한 원시 약품 데이터를 JPA를 통해 가져와 + * 도메인 객체인 Drug로 변환하는 어댑터 클래스입니다. + * + * @since 2025-04-22 + * @modified 2025-04-24 + */ +@Component +@RequiredArgsConstructor +public class DrugRawDataAdapter implements DrugRawDataPort { + private final DrugJpaRepository drugJpaRepository; + private final EmbeddingPort embeddingPort; + + /** + * 주어진 Pageable 정보에 따라 DB에서 한 페이지 분량의 GovDrugEntity를 조회하고, + * 각 엔티티를 도메인 모델(GovDrug)로 변환하여 Page 형태로 반환합니다. + * + * @param pageable 조회할 페이지 번호와 크기를 포함하는 Pageable 객체 + * @return 페이지 단위로 변환된 GovDrug 도메인 객체의 Page + * @author 박찬병 + * @since 2025-04-24 + * @modified + * 2025-05-04 - 박찬병: Mapper에 넘겨주기 전 먼저 파싱하도록 변경 + */ + @Override + public Page findAllDrugs(Pageable pageable) { + log("findAllDrugs() 요청 수신"); + + return drugJpaRepository.findByIsGeneral(pageable) + .map(DrugRawDataMapper::toDomainFromEntity); + } + + /** + * 지정된 페이지 번호와 페이지 크기에 따라 Pageable 객체를 생성하고, + * 원시 약품 데이터와 임베딩 데이터를 조인하여 한 페이지 분량의 Drug 도메인 객체 리스트를 조회합니다. + * + * @param pageNo 조회할 페이지 번호 (0부터 시작) + * @param numOfRows 한 페이지에 포함할 데이터 개수 + * @return 페이지 범위에 해당하는 Drug 도메인 객체들의 리스트 + * @author 정안식 + * @since 2025-04-24 + * @modified + * 2025-05-02 - 이해창: numOfRows 파라미터 추가 + */ + @Override + public List fetchRawData(int pageNo, int numOfRows) { + log("index 서비스 요청 수신"); + Pageable pageable = createPageable(pageNo, numOfRows); + return embeddingPort.loadEmbeddingsByPage(pageable); + } + + /** + * 페이지 번호와 페이지 크기를 기반으로 drugId 오름차순 정렬이 적용된 Pageable 객체를 생성합니다. + * + * @param pageNo 조회할 페이지 번호 (0부터 시작) + * @param numOfRows 한 페이지에 포함할 데이터 개수 + * @return 지정된 페이지 정보와 정렬 조건을 포함한 Pageable 객체 + * @author 정안식 + * @since 2025-04-24 + * @modified + * 2025-04-24 + */ + private Pageable createPageable(int pageNo, int numOfRows) { + log("pageable 생성"); + return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); + } + + /** + * JPA 레포지토리를 이용해 GovDrugJpaRepository의 전체 데이터 수를 조회합니다. + * + * @return GovDrugJpaRepository의 전체 데이터 수 + * @author 이해창 + * @since 2025-05-02 + * @modified + * 2025-05-02 - 이해창: numOfRows 파라미터 추가 + */ + @Override + public long getDrugTotalSize() { + return drugJpaRepository.count(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java new file mode 100644 index 0000000..fcbc333 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; + +@Repository +public interface DrugDetailJpaRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java new file mode 100644 index 0000000..5b6c46f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; + +@Repository +public interface DrugImgRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java new file mode 100644 index 0000000..a21d2ba --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository; + +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 com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; + +public interface DrugJpaRepository extends JpaRepository { + + @Query(""" + SELECT d + FROM DrugRawDataEntity d + WHERE d.isGeneral = true + AND d.isHerbal = false + """) + Page findByIsGeneral(Pageable pageable); + + @Query("SELECT MIN(d.drugId) FROM DrugRawDataEntity d") + Long findMinDrugId(); + + @Query("SELECT MAX(d.drugId) FROM DrugRawDataEntity d") + Long findMaxDrugId(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java similarity index 88% rename from src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java index 3811a5c..9b6a355 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/DrugMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java @@ -1,16 +1,16 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; import java.util.List; import java.util.Map; -public class DrugMapper{ +public class DrugFieldTypeMapper { public static List parseMaterials(String json) { try { ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java new file mode 100644 index 0000000..4f6403a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java @@ -0,0 +1,14 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.DrugImageRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; + +public class DrugImageRequestMapper { + public static DrugImgEntity toEntityFromRequest(DrugImageRequest r){ + return DrugImgEntity.builder() + .drugId(r.getDrugId()) + .productImage(r.getProductImage()) + .pillImage(r.getPillImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java new file mode 100644 index 0000000..3699ca7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.parser.JsonArrayTextParser; + +public class DrugRawDataMapper { + public static Drug toDomainFromEntity(DrugRawDataEntity e) { + return Drug.builder() + .drugId(e.getDrugId()) + .drugName(e.getDrugName()) + .company(e.getCompany()) + .permitDate(e.getPermitDate()) + .isGeneral(e.isGeneral()) + .materialInfo(DrugFieldTypeMapper.parseMaterials(e.getMaterialInfo())) + .storeMethod(e.getStoreMethod()) + .validTerm(e.getValidTerm()) + .efficacy(JsonArrayTextParser.extractAndClean(e.getEfficacy())) + .usage(DrugFieldTypeMapper.parseStringToList(e.getUsage())) + .precaution(DrugFieldTypeMapper.parsePrecaution(e.getPrecaution())) + .imageUrl(e.getImageUrl()) + .cancelDate(e.getCancelDate()) + .cancelName(e.getCancelName()) + .isHerbal(e.getIsHerbal()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java new file mode 100644 index 0000000..9a11065 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java @@ -0,0 +1,145 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.parser; + + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 최상위 JSON 배열에서 각 요소(문자열)을 추출하고, + * 정규식을 이용해 HTML 태그 제거, HTML 엔티티(예: •, •) 디코딩,   등을 제거한 뒤 + * 깨끗한 텍스트 리스트를 반환하는 유틸리티 클래스입니다. + * + *

예시 JSON: ["첫번째 텍스트•", "텍스트 예시"]

+ *

→ 리턴: ["첫번째 텍스트•", "텍스트 예시"]

+ * + * @author 박찬병 + * @since 2025-04-27 + * @modified 2025-04-27 + */ +public class JsonArrayTextParser { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + // HTML 태그 제거용 정규식 + private static final Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); + // 10진수 HTML 엔티티 디코딩용 정규식 (예: •) + private static final Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); + // 16진수 HTML 엔티티 디코딩용 정규식 (예: •) + private static final Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); + + /** + * JSON 문자열 최상위가 배열일 때, 각 요소를 텍스트로 파싱하고 HTML 태그, HTML 엔티티,   등을 제거하여 리스트로 반환합니다. + * + * @param json JSON 배열 형태의 문자열 + * @return 정제된 텍스트 리스트 + * @throws IOException JSON 파싱 실패 시 발생 + */ + public static List extractAndClean(String json) { + try{ + JsonNode root = objectMapper.readTree(json); + List texts = new ArrayList<>(); + + if (!root.isArray()) { + return texts; + } + + for (JsonNode element : root) { + if (element.isTextual()) { + String raw = element.asText().trim(); + if (raw.isEmpty()) { + continue; + } + + // 1) HTML 태그 제거 + String noHtml = TAG_REGEX.matcher(raw).replaceAll(""); + // 2)   등을 일반 공백으로 치환 + String withSpaces = noHtml.replaceAll(" ", " "); + // 3) HTML 엔티티 디코딩 (10진수 및 16진수) + String decoded = decodeHtmlEntities(withSpaces); + // 4) 최종 트리밍 + String clean = decoded.trim(); + + if (!clean.isEmpty()) { + texts.add(clean); + } + } + } + return texts; + } catch (Exception e){ + LogUtil.log(LogLevel.ERROR, "텍스트 정제 실패", e); + return null; + } + + + } + + /** + * HTML 엔티티(10진수 &#DDD; 및 16진수 &#xHHHH;)를 대응하는 문자로 디코딩합니다. + * 예: "foo•bar" → "foo•bar", "foo•bar" → "foo•bar" + * + * @param input 엔티티를 포함한 문자열 + * @return 디코딩된 문자열 + * @author 박찬병 + * @since 2025-05-03 + * @modified 2025-05-03 + */ + private static String decodeHtmlEntities(String input) { + String result = decodeDecimalEntities(input); + return decodeHexEntities(result); + } + + /** + * 10진수 HTML 엔티티(예: •)를 문자로 디코딩합니다. + * 내부적으로 정규표현식을 이용해 &#숫자; 패턴을 찾아 대응하는 문자로 변환합니다. + * + * @param input 디코딩할 문자열 + * @return 10진수 엔티티가 디코딩된 문자열 + * @author 박찬병 + * @since 2025-05-03 + * @modified 2025-05-03 + */ + private static String decodeDecimalEntities(String input) { + Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(input); + StringBuffer sb = new StringBuffer(); + + while (decMatcher.find()) { + int code = Integer.parseInt(decMatcher.group(1)); + decMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + + decMatcher.appendTail(sb); + return sb.toString(); + } + + /** + * 16진수 HTML 엔티티(예: •)를 문자로 디코딩합니다. + * 내부적으로 정규표현식을 이용해 &#x헥사값; 패턴을 찾아 대응하는 문자로 변환합니다. + * + * @param input 디코딩할 문자열 + * @return 16진수 엔티티가 디코딩된 문자열 + * @author 박찬병 + * @since 2025-05-03 + * @modified 2025-05-03 + */ + private static String decodeHexEntities(String input) { + Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(input); + StringBuffer sb = new StringBuffer(); + + while (hexMatcher.find()) { + int code = Integer.parseInt(hexMatcher.group(1), 16); + hexMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + + hexMatcher.appendTail(sb); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/DrugScraperController.java similarity index 64% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/DrugScraperController.java index 21a9d1c..28d8272 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/DrugScraperController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/DrugScraperController.java @@ -1,7 +1,8 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper; +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller; -import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperControllerDocs; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -9,8 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperUsecase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; import lombok.RequiredArgsConstructor; @@ -22,25 +23,29 @@ */ @RestController @RequiredArgsConstructor -@RequestMapping("/scraper") -public class DrugScraperController { - private final DrugScraperUsecase drugScraperUsecase; +@RequestMapping("/job/scraper") +public class DrugScraperController implements DrugScraperControllerDocs { + private final DrugScraperUseCase drugScraperUsecase; + @Override @PostMapping("/start") public ResponseEntity> start() { return success(drugScraperUsecase.scraperStart()); } + @Override @DeleteMapping("/stop") public ResponseEntity> stop() { return success(drugScraperUsecase.stop()); } + @Override @PostMapping("/restart") public ResponseEntity> restart() { return success(drugScraperUsecase.restart()); } + @Override @GetMapping("/status") public ResponseEntity> getBatchProgress() { return success(drugScraperUsecase.getStatus()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/combine/DrugScraperTableCombineController.java similarity index 70% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/combine/DrugScraperTableCombineController.java index a685af7..364b88c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/combine/DrugScraperTableCombineController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/combine/DrugScraperTableCombineController.java @@ -1,7 +1,8 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper.combine; +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.combine; -import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperTableCombineControllerDocs; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -9,8 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperTableCombineUsecase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperTableCombineUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; import lombok.RequiredArgsConstructor; @@ -23,8 +24,8 @@ @RestController @RequestMapping("/scraper/combine") @RequiredArgsConstructor -public class DrugScraperTableCombineController { - private final DrugScraperTableCombineUsecase drugScraperTableCombineUsecase; +public class DrugScraperTableCombineController implements DrugScraperTableCombineControllerDocs { + private final DrugScraperTableCombineUseCase drugScraperTableCombineUsecase; /** * 의약품 상세정보와 이미지 정보 테이블을 병합하는 작업을 시작합니다. @@ -34,6 +35,7 @@ public class DrugScraperTableCombineController { * @author 함예정 * @since 2025-05-02 */ + @Override @PostMapping("/start") public ResponseEntity> start(){ return success(drugScraperTableCombineUsecase.mergeTable()); @@ -47,6 +49,7 @@ public ResponseEntity> start(){ * @author 함예정 * @since 2025-05-02 */ + @Override @DeleteMapping("/stop") public ResponseEntity> stop() { return success(drugScraperTableCombineUsecase.stop()); @@ -59,6 +62,7 @@ public ResponseEntity> stop() { * @author 함예정 * @since 2025-05-02 */ + @Override @GetMapping("/status") public ResponseEntity> getBatchProgress() { return success(drugScraperTableCombineUsecase.getStatus()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/details/DrugScraperDetailController.java similarity index 73% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/details/DrugScraperDetailController.java index 79273cd..ea1ef30 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/details/DrugScraperDetailController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/details/DrugScraperDetailController.java @@ -1,7 +1,8 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper.details; +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.details; -import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperDetailControllerDocs; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -9,8 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperDetailUseCase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperDetailUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; import lombok.RequiredArgsConstructor; @@ -23,7 +24,7 @@ @RestController @RequestMapping("/scraper/details") @RequiredArgsConstructor -public class DrugScraperDetailController { +public class DrugScraperDetailController implements DrugScraperDetailControllerDocs { private final DrugScraperDetailUseCase drugScraperDetailUseCase; /** @@ -34,6 +35,7 @@ public class DrugScraperDetailController { * @author 함예정 * @since 2025-05-02 */ + @Override @PostMapping("/start") public ResponseEntity> start() { return success(drugScraperDetailUseCase.requestAllData()); @@ -47,6 +49,7 @@ public ResponseEntity> start() { * @author 함예정 * @since 2025-05-02 */ + @Override @DeleteMapping("/stop") public ResponseEntity> stop() { return success(drugScraperDetailUseCase.stop()); @@ -59,6 +62,7 @@ public ResponseEntity> stop() { * @author 함예정 * @since 2025-05-02 */ + @Override @GetMapping("/status") public ResponseEntity> getBatchProgress() { return success(drugScraperDetailUseCase.getStatus()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java new file mode 100644 index 0000000..ba32d8c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * 의약품 정보 수집 전체 작업 API 문서 정의 인터페이스 + * + * 이 API는 의약품 상세정보 수집, 이미지 정보 수집, 테이블 병합, 임베딩 벡터 생성 등 + * 전체 스크래핑 파이프라인을 제어하기 위한 엔드포인트를 제공합니다. + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper", description = "의약품 정보 수집 전체 작업을 제어하는 API") +public interface DrugScraperControllerDocs { + + @Operation( + summary = "전체 스크래핑 작업 시작", + description = "의약품 상세정보 수집, 이미지 수집, 병합, 임베딩 벡터 생성을 포함한 전체 스크래핑 파이프라인을 순차적으로 실행합니다. " + + "작업이 정상적으로 시작되면 작업 ID 또는 시작 메시지를 반환합니다." + ) + ResponseEntity> start(); + + @Operation( + summary = "진행 중인 스크래핑 작업 중지", + description = "현재 실행 중인 전체 스크래핑 파이프라인 작업을 안전하게 중단합니다. " + + "중단된 상태는 유지되어 재시작 시 중단된 지점부터 재개할 수 있습니다." + ) + ResponseEntity> stop(); + + @Operation( + summary = "스크래핑 작업 재시작", + description = "중단된 스크래핑 작업을 중단 시점부터 재개하여 다시 실행합니다. " + + "restart 엔드포인트 호출 시 이전 작업 상태를 기반으로 이어서 수행합니다." + ) + ResponseEntity> restart(); + + @Operation( + summary = "스크래핑 작업 상태 조회", + description = "현재 전체 스크래핑 작업의 진행 상태(단계, 완료 비율, 오류 여부 등)를 조회하여 반환합니다." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java new file mode 100644 index 0000000..9b3728d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * 의약품 상세정보 수집 작업 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper Detail", description = "의약품 상세정보 수집 작업 API") +public interface DrugScraperDetailControllerDocs { + + @Operation( + summary = "상세정보 수집 작업 시작", + description = "의약품 상세정보 수집 작업을 시작합니다." + ) + ResponseEntity> start(); + + @Operation( + summary = "상세정보 수집 작업 중지", + description = "진행 중인 상세정보 수집 작업을 중지합니다." + ) + ResponseEntity> stop(); + + @Operation( + summary = "상세정보 수집 작업 상태 조회", + description = "현재 상세정보 수집 작업의 상태를 조회합니다." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java new file mode 100644 index 0000000..a039151 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * 의약품 이미지 수집 작업 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper Image", description = "의약품 이미지 수집 작업 API") +public interface DrugScraperImageControllerDocs { + + @Operation( + summary = "이미지 수집 작업 시작", + description = "의약품 이미지 수집 작업을 시작합니다." + ) + ResponseEntity> start(); + + @Operation( + summary = "이미지 수집 작업 중지", + description = "진행 중인 이미지 수집 작업을 중지합니다." + ) + ResponseEntity> stop(); + + @Operation( + summary = "이미지 수집 작업 상태 조회", + description = "현재 이미지 수집 작업의 상태를 조회합니다." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java new file mode 100644 index 0000000..7e25e69 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * 의약품 상세정보와 이미지 테이블 병합 작업 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper Combine", description = "의약품 상세정보와 이미지 테이블 병합 작업 API") +public interface DrugScraperTableCombineControllerDocs { + + @Operation( + summary = "테이블 병합 작업 시작", + description = "의약품 상세정보와 이미지 정보 테이블을 병합하는 작업을 시작합니다." + ) + ResponseEntity> start(); + + @Operation( + summary = "병합 작업 중지", + description = "진행 중인 병합 작업을 중지합니다." + ) + ResponseEntity> stop(); + + @Operation( + summary = "병합 작업 상태 조회", + description = "현재 병합 작업의 상태를 조회합니다." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/image/DrugScraperImageController.java similarity index 70% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/image/DrugScraperImageController.java index cbf644d..8619848 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/scraper/image/DrugScraperImageController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/image/DrugScraperImageController.java @@ -1,7 +1,8 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller.scraper.image; +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.image; -import static com.likelion.backendplus4.yakplus.response.ApiResponse.*; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperImageControllerDocs; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -9,8 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.likelion.backendplus4.yakplus.drug.application.service.port.in.scraper.DrugScraperImageUsecase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperImageUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; import lombok.RequiredArgsConstructor; @@ -24,8 +25,8 @@ @RestController @RequestMapping("/scraper/images") @RequiredArgsConstructor -public class DrugScraperImageController { - private final DrugScraperImageUsecase drugScraperImageUsecase; +public class DrugScraperImageController implements DrugScraperImageControllerDocs { + private final DrugScraperImageUseCase drugScraperImageUsecase; /** * 의약품 이미지 수집 작업을 시작합니다. @@ -35,6 +36,7 @@ public class DrugScraperImageController { * @author 함예정 * @since 2025-05-02 */ + @Override @PostMapping("/start") public ResponseEntity> start(){ return success(drugScraperImageUsecase.requestAllData()); @@ -48,6 +50,7 @@ public ResponseEntity> start(){ * @author 함예정 * @since 2025-05-02 */ + @Override @DeleteMapping("/stop") public ResponseEntity> stop() { return success(drugScraperImageUsecase.stop()); @@ -60,6 +63,7 @@ public ResponseEntity> stop() { * @author 함예정 * @since 2025-05-02 */ + @Override @GetMapping("/status") public ResponseEntity> getBatchProgress() { return success(drugScraperImageUsecase.getStatus()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/support/JobManager.java similarity index 84% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/support/JobManager.java index 68106ef..6edc3e8 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/batch/adapter/JobManager.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/support/JobManager.java @@ -1,6 +1,6 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.batch.adapter; +package com.likelion.backendplus4.yakplus.drug.support; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; import java.util.Collection; import java.util.HashSet; @@ -19,14 +19,12 @@ import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobOperator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.task.TaskExecutor; import org.springframework.stereotype.Component; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogUtil; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchError; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; import lombok.RequiredArgsConstructor; @@ -39,6 +37,8 @@ @Component @RequiredArgsConstructor public class JobManager { + private final String normalExecutorName = "normalExecutor"; + private final AtomicBoolean isRunning = new AtomicBoolean(false); private final JobLauncher jobLauncher; private final JobOperator jobOperator; @@ -46,19 +46,21 @@ public class JobManager { private final Map taskExecutorMap; - private Set stoppedExecutionIds = new ConcurrentSkipListSet<>(); + private final Set stoppedExecutionIds = new ConcurrentSkipListSet<>(); /** * 지정된 Job을 비동기로 실행합니다. + * 만약 이미 실행 중인 Job이 있다면 예외를 발생시킵니다. * * @param job 실행할 Job * @return 실행 요청 결과 메시지 - * + * @throws ParserBatchException 중복 실행 예외 * @author 함예정 * @since 2025-05-02 */ public String startJob(Job job) { - TaskExecutor taskExecutor = taskExecutorMap.get("batchExecutor"); + IfAlreadyRunThrowException(); + TaskExecutor taskExecutor = taskExecutorMap.get(normalExecutorName); log("배치 실행 시작 - Job: " + job.getName()); isRunning.set(true); taskExecutor.execute(() -> { @@ -89,7 +91,7 @@ public String startJob(Job job) { */ public String restart() { IfAlreadyRunThrowException(); - TaskExecutor taskExecutor = taskExecutorMap.get("batchExecutor"); + TaskExecutor taskExecutor = taskExecutorMap.get(normalExecutorName); taskExecutor.execute(() -> { for (Long id : new HashSet<>(stoppedExecutionIds)) { try { @@ -138,6 +140,7 @@ public String getJobStatus(Job job) { } } } + return "실행 중인 Job이 없습니다."; } @@ -181,7 +184,7 @@ public String stopRunningBatch(Job job) { * @author 함예정 * @since 2025-05-02 */ - public void IfAlreadyRunThrowException() { + private void IfAlreadyRunThrowException() { if (isRunning.get()) { throw new ParserBatchException(ParserBatchError.ALREADY_RUN); } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java deleted file mode 100644 index 0df4497..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/in/IndexUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.application.port.in; - -public interface IndexUseCase { - void index(); - - void indexSymptom(); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java deleted file mode 100644 index f3d2f98..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/DrugIndexRepositoryPort.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.application.port.out; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; - -import java.util.List; -import org.springframework.data.domain.Page; - -public interface DrugIndexRepositoryPort { - void saveAll(String esIndexName, List drugs); - - void saveAllSymptom(Page drugPage); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java deleted file mode 100644 index 7ee25f7..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/EmbeddingLoadingPort.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.application.port.out; - -import java.util.List; - -import org.springframework.data.domain.Pageable; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; - -public interface EmbeddingLoadingPort { - List loadEmbeddingsByPage(Pageable pageable); - - float[] getEmbedding(String text); - - void saveEmbedding(List dtos); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java deleted file mode 100644 index 622f70c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/application/port/out/GovDrugRawDataPort.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.application.port.out; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.List; - -public interface GovDrugRawDataPort { - List fetchRawData(int pageNo, int numOfRows); - - Page findAllDrugs(Pageable pageable); - - long getDrugTotalSize(); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java deleted file mode 100644 index 11c5ef0..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/config/ElasticsearchConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.config; - -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ElasticsearchConfig { - // TODO: 필요 시 RestClient 빈 등록 -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java deleted file mode 100644 index c74cfea..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/config/OpenAiConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.config; - -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -// @Configuration -public class OpenAiConfig { -// TODO: 삭제해야할 클래스 -// - 기존 배치 소스와 중복 -// @Value("${spring.ai.openai.api-key}") -// private String apiKey; -// -// @Bean -// public OpenAiApi openaiApi() { -// return new OpenAiApi(apiKey); -// } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java deleted file mode 100644 index d1bbf0b..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GovDrugRawDataAdapter.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugDetailEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugDetailJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper.DrugRawDataMapper; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.index.application.port.out.GovDrugRawDataPort; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * 공공 API로부터 조회한 원시 약품 데이터를 JPA를 통해 가져와 - * 도메인 객체인 Drug로 변환하는 어댑터 클래스입니다. - * - * @since 2025-04-22 - * @modified 2025-04-24 - */ -@Component -@RequiredArgsConstructor -public class GovDrugRawDataAdapter implements GovDrugRawDataPort { - private final GovDrugDetailJpaRepository rawDataJpaRepository; - private final GovDrugJpaRepository drugJpaRepository; - private final EmbeddingLoadingPort embeddingLoadingPort; - - - /** - * 주어진 Pageable 정보에 따라 DB에서 한 페이지 분량의 GovDrugEntity를 조회하고, - * 각 엔티티를 도메인 모델(GovDrug)로 변환하여 Page 형태로 반환합니다. - * - * @param pageable 조회할 페이지 번호와 크기를 포함하는 Pageable 객체 - * @return 페이지 단위로 변환된 GovDrug 도메인 객체의 Page - * @author 박찬병 - * @since 2025-04-24 - * @modified 2025-04-25 - * - */ - @Override - public Page findAllDrugs(Pageable pageable) { - log("findAllDrugs() 요청 수신"); - return drugJpaRepository.findAll(pageable) - .map(DrugRawDataMapper::toDomainFromEntity); - } - - /** - * lastSeq가 null일 경우 0으로 치환하여 조회 시작점을 결정합니다. - * - * @param lastSeq 마지막 처리 시퀀스 - * @return 실제 조회 시작 시퀀스 - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - private Long getStartSeq(Long lastSeq) { - return (lastSeq == null ? 0L : lastSeq); - } - - /** - * JPA 레포지토리를 이용해 itemSeq 기준으로 정렬된 데이터를 조회합니다. - * - * @param lastSeq 마지막으로 조회된 Seq - * @param pageable 페이징 및 정렬 정보 - * @return 조회된 GovDrugRawDataEntity 리스트 - * @throws IndexException 조회 중 예외가 발생하면 SearchErrorCode.RAW_DATA_FETCH_ERROR로 래핑하여 던집니다. - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - private List getGovDrugRawDataEntities(Long lastSeq, Pageable pageable) { - try { - return rawDataJpaRepository.findByDrugIdGreaterThanOrderByDrugIdAsc(lastSeq, pageable); - } catch (Exception e) { - //TODO: LOG ERROR 처리 요망 -// log(LogLevel.ERROR, "MySQL 데이터 조회 실패", e); - throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); - } - - } - - /** - * 조회된 엔티티 리스트를 Drug 도메인 객체 리스트로 변환합니다. - * - * @param rawData GovDrugRawDataEntity 리스트 - * @return Drug 도메인 객체 리스트 - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - private List convertToDrugDomains(List rawData) { - return rawData.stream() - .map(this::mapToDrugDomain) - .collect(Collectors.toList()); - } - - /** - * 단일 GovDrugRawDataEntity를 Drug 도메인 객체로 매핑합니다. - * - * @param entity 변환할 GovDrugRawDataEntity - * @return 변환된 Drug 도메인 객체 - * @author 정안식 - * @since 2025-04-22 - * @modified 2025-04-24 - */ - private Drug mapToDrugDomain(DrugDetailEntity entity) { - //TODO: Mapper로 변경 필요 - return Drug.builder() - // .drugId(entity.getItemSeq()) - // .drugName(entity.getItemName()) - // .company(entity.getEntpName()) - // .efficacy(entity.get()) - .build(); - } - - - @Override - public List fetchRawData(int pageNo, int numOfRows) { - log("index 서비스 요청 수신"); - Pageable pageable = createPageable(pageNo, numOfRows); - List drugs = embeddingLoadingPort.loadEmbeddingsByPage(pageable); - return drugs; - } - - private Pageable createPageable(int pageNo, int numOfRows) { - log("pageable 생성"); - return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); - } - - - /** - * JPA 레포지토리를 이용해 GovDrugJpaRepository의 전체 데이터 수를 조회합니다. - * - * @return GovDrugJpaRepository의 전체 데이터 수 - * @author 이해창 - * @since 2025-05-02 - * @modified - */ - @Override - public long getDrugTotalSize() { - return drugJpaRepository.count(); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java deleted file mode 100644 index 68003cf..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/GptEmbeddingLoadingAdapter.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.*; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.embedding.Embedding; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiEmbeddingOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugGptEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugGptEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; - -import lombok.RequiredArgsConstructor; - -@Repository -@RequiredArgsConstructor -public class GptEmbeddingLoadingAdapter implements EmbeddingLoadingPort { - private final GovDrugGptEmbedJpaRepository govDrugGptEmbedJpaRepository; - private final OpenAiApi openAiApi; - - @Override - public List loadEmbeddingsByPage(Pageable pageable) { - List drugs = new ArrayList<>(); - List rows = govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); - if (rows.isEmpty()) { - log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); - throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); - } - for (Object[] arr : rows) { - DrugRawDataEntity raw = (DrugRawDataEntity)arr[0]; - DrugGptEmbedEntity embed = (DrugGptEmbedEntity)arr[1]; - drugs.add(toDomainFromEntity(raw, embed)); - } - log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); - return drugs; - } - - @Override - public float[] getEmbedding(String text) { - OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( - this.openAiApi, - MetadataMode.EMBED, - OpenAiEmbeddingOptions.builder() - .model("text-embedding-3-small") - .build(), - RetryUtils.DEFAULT_RETRY_TEMPLATE); - EmbeddingResponse embeddingResponse = openAiEmbeddingModel - .embedForResponse(List.of(text)); - Embedding embedding = embeddingResponse.getResults().getFirst(); - return embedding.getOutput(); - } - - @Override - public void saveEmbedding(List dtos) { - govDrugGptEmbedJpaRepository.saveAll( - dtos.stream() - .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugGptEmbedEntity.class)) - .toList() - ); - govDrugGptEmbedJpaRepository.flush(); - } - - private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugGptEmbedEntity embedEntity) { - return Drug.builder() - .drugId(drugEntity.getDrugId()) - .drugName(drugEntity.getDrugName()) - .company(drugEntity.getCompany()) - .permitDate(drugEntity.getPermitDate()) - .isGeneral(drugEntity.isGeneral()) - .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) - .storeMethod(drugEntity.getStoreMethod()) - .validTerm(drugEntity.getValidTerm()) - .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) - .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) - .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) - .imageUrl(drugEntity.getImageUrl()) - .cancelDate(drugEntity.getCancelDate()) - .cancelName(drugEntity.getCancelName()) - .isHerbal(drugEntity.getIsHerbal()) - .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getGptVector())) - .build(); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java deleted file mode 100644 index f4a1c23..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KmBertEmbeddingLoadingAdapter.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKmBertEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKmBertEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Repository; -import org.springframework.web.client.RestTemplate; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -@Repository -@RequiredArgsConstructor -public class KmBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { - private final GovDrugKmBertEmbedJpaRepository govDrugKmBertEmbedJpaRepository; - private final UriCompBuilder apiUriCompBuilder; - private final RestTemplate restTemplate; - - @Override - public List loadEmbeddingsByPage(Pageable pageable) { - List drugs = new ArrayList<>(); - List rows = govDrugKmBertEmbedJpaRepository.findRawAndEmbed(pageable); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() +"페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); - if (rows.isEmpty()) { - log(LogLevel.ERROR,"loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); - throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); - } - for (Object[] arr : rows) { - DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; - DrugKmBertEmbedEntity embed = (DrugKmBertEmbedEntity) arr[1]; - drugs.add(toDomainFromEntity(raw, embed)); - } - log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); - - return drugs; - } - - @Override - public float[] getEmbedding(String text) { - URI embeddingURI = getEmbeddingURI(); - return getEmbeddingVector(embeddingURI, text); - } - - @Override - public void saveEmbedding(List dtos) { - govDrugKmBertEmbedJpaRepository.saveAll( - dtos.stream() - .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKmBertEmbedEntity.class)) - .toList() - ); - govDrugKmBertEmbedJpaRepository.flush(); - } - - private URI getEmbeddingURI() { - return apiUriCompBuilder.getUriForKmbertEmbeding(); - } - - private float[] getEmbeddingVector(URI embedUri, String text) { - EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); - embeddingRequestText.setText(text); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); - return restTemplate.postForObject(embedUri, request, float[].class); - } - - private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKmBertEmbedEntity embedEntity) { - return Drug.builder() - .drugId(drugEntity.getDrugId()) - .drugName(drugEntity.getDrugName()) - .company(drugEntity.getCompany()) - .permitDate(drugEntity.getPermitDate()) - .isGeneral(drugEntity.isGeneral()) - .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) - .storeMethod(drugEntity.getStoreMethod()) - .validTerm(drugEntity.getValidTerm()) - .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) - .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) - .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) - .imageUrl(drugEntity.getImageUrl()) - .cancelDate(drugEntity.getCancelDate()) - .cancelName(drugEntity.getCancelName()) - .isHerbal(drugEntity.getIsHerbal()) - .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getKmBertVector())) - .build(); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java deleted file mode 100644 index d37e498..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/KrSBertEmbeddingLoadingAdapter.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.api.util.UriCompBuilder; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.drug.infrastructure.embedding.model.EmbeddingRequestText; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugKrSbertEmbedEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugKrSbertEmbedJpaRepository; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.index.exception.IndexException; -import com.likelion.backendplus4.yakplus.index.exception.error.IndexErrorCode; -import com.likelion.backendplus4.yakplus.index.support.EmbeddingUtil.EmbedEntityBuilder; -import lombok.RequiredArgsConstructor; - -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Repository; -import org.springframework.web.client.RestTemplate; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -@Repository -@RequiredArgsConstructor -public class KrSBertEmbeddingLoadingAdapter implements EmbeddingLoadingPort { - private final GovDrugKrSbertEmbedJpaRepository govDrugKrSbertEmbedJpaRepository; - private final UriCompBuilder apiUriCompBuilder; - private final RestTemplate restTemplate; - - @Override - public List loadEmbeddingsByPage(Pageable pageable) { - List drugs = new ArrayList<>(); - List rows = govDrugKrSbertEmbedJpaRepository.findRawAndEmbed(pageable); - log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "페이지 에서 받아온 drug 객체 제작 대상 데이터 수: " + rows.size()); - if (rows.isEmpty()) { - log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug 도메인 객체 생성 대상 데이터 없음"); - throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); - } - for (Object[] arr : rows) { - DrugRawDataEntity raw = (DrugRawDataEntity) arr[0]; - DrugKrSbertEmbedEntity embed = (DrugKrSbertEmbedEntity) arr[1]; - drugs.add(toDomainFromEntity(raw, embed)); - } - log("loadEmbeddingsByPage - Drug 도메인 객체 생성 완료"); - - return drugs; - } - - @Override - public float[] getEmbedding(String text) { - URI embeddingURI = getEmbeddingURI(); - return getEmbeddingVector(embeddingURI, text); - } - - @Override - public void saveEmbedding(List dtos) { - govDrugKrSbertEmbedJpaRepository.saveAll( - dtos.stream() - .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKrSbertEmbedEntity.class)) - .toList() - ); - govDrugKrSbertEmbedJpaRepository.flush(); - } - - private float[] getEmbeddingVector(URI embedUri, String text) { - EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); - embeddingRequestText.setText(text); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); - return restTemplate.postForObject(embedUri, request, float[].class); - } - - private URI getEmbeddingURI() { - return apiUriCompBuilder.getUriForKrSbertEmbeding(); - } - - private static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, DrugKrSbertEmbedEntity embedEntity) { - return Drug.builder() - .drugId(drugEntity.getDrugId()) - .drugName(drugEntity.getDrugName()) - .company(drugEntity.getCompany()) - .permitDate(drugEntity.getPermitDate()) - .isGeneral(drugEntity.isGeneral()) - .materialInfo(DrugMapper.parseMaterials(drugEntity.getMaterialInfo())) - .storeMethod(drugEntity.getStoreMethod()) - .validTerm(drugEntity.getValidTerm()) - .efficacy(DrugMapper.parseStringToList(drugEntity.getEfficacy())) - .usage(DrugMapper.parseStringToList(drugEntity.getUsage())) - .precaution(DrugMapper.parsePrecaution(drugEntity.getPrecaution())) - .imageUrl(drugEntity.getImageUrl()) - .cancelDate(drugEntity.getCancelDate()) - .cancelName(drugEntity.getCancelName()) - .isHerbal(drugEntity.getIsHerbal()) - .vector(DrugMapper.parseJsonToFloatArray(embedEntity.getKrSbertVector())) - .build(); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java deleted file mode 100644 index 7d6c8c1..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/DrugSymptomRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository; - -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; - -public interface DrugSymptomRepository extends ElasticsearchRepository { -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java deleted file mode 100644 index 0b91997..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/infrastructure/adapter/persistence/repository/document/DrugSymptomDocument.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document; - - -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.CompletionField; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -@Document(indexName = "eedoc") -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class DrugSymptomDocument { - - @Id - @Field(type = FieldType.Keyword, name = "ITEM_SEQ") - private Long drugId; - - @Field(type = FieldType.Text, name = "ITEM_NAME") - private String drugName; - - @Field(type = FieldType.Text, name = "company") - private String company; - - @Field(type = FieldType.Text, name = "efficacy") - private List efficacy; - - @Field(type = FieldType.Keyword, name = "imageUrl") - private String imageUrl; - - @CompletionField(analyzer = "symptom_autocomplete") - private List symptomSuggester; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java deleted file mode 100644 index 346135c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/presentation/controller/dto/request/IndexRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.presentation.controller.dto.request; - -/** - * 인덱싱 요청 정보 DTO - * - * @since 2025-04-22 - * @modified 2025-04-28 - * 25.04.27 - itemSeq -> drugId로 수정 - * - 페이징 처리 로직 수정 - * 25.04.28 - DrugController에서 사용하지 않도록 수정(추후 필요한 필드로 변경하여 사용할 예정) - */ -public record IndexRequest( - Long lastDrugId, - int limit) { -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java deleted file mode 100644 index 309994a..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/support/mapper/SymptomMapper.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.support.mapper; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; -import com.likelion.backendplus4.yakplus.index.support.parser.SymptomTextParser; -import java.util.List; - -/** - * 증상 관련 Document를 다루는 매퍼 클래스입니다. - * - * @author 박찬병 - * @since 2025-04-25 - * @modified 2025-04-25 - */ -public class SymptomMapper { - - /** - * 주어진 GovDrug 도메인 객체를 기반으로 ES 색인용 DrugSymptomDocument로 변환합니다. 내부에서 JSON 파싱 및 전처리 로직을 실행하며, 파싱 - * 실패 시 ScraperException을 던집니다. - * - * @param entity 변환 대상 GovDrug 도메인 객체 - * @return 변환된 DrugSymptomDocument 객체 - * @author 박찬병 - * @modified 2025-04-25 - * @since 2025-04-25 - */ - public static DrugSymptomDocument toDocument(Drug entity) { - // 1) 추출된 텍스트 리스트를 단일 문자열로 전처리 - String flatText = SymptomTextParser.flattenLines(entity.getEfficacy()); - // 2) 전처리된 문자열을 자동완성용 토큰 리스트로 변환 - List suggestTokens = SymptomTextParser.tokenizeForSuggestion(flatText); - - return DrugSymptomDocument.builder() - .drugId(entity.getDrugId()) - .drugName(entity.getDrugName()) - .efficacy(suggestTokens) - .imageUrl(entity.getImageUrl()) - .company(entity.getCompany()) - .symptomSuggester(suggestTokens) - .build(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java deleted file mode 100644 index 83a02fb..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/index/support/parser/JsonArrayTextParser.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.likelion.backendplus4.yakplus.index.support.parser; - - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * 최상위 JSON 배열에서 각 요소(문자열)을 추출하고, - * 정규식을 이용해 HTML 태그 제거, HTML 엔티티(예: •, •) 디코딩,   등을 제거한 뒤 - * 깨끗한 텍스트 리스트를 반환하는 유틸리티 클래스입니다. - * - *

예시 JSON: ["첫번째 텍스트•", "텍스트 예시"]

- *

→ 리턴: ["첫번째 텍스트•", "텍스트 예시"]

- * - * @author 박찬병 - * @since 2025-04-27 - * @modified 2025-04-27 - */ -public class JsonArrayTextParser { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - // HTML 태그 제거용 정규식 - private static final Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); - // 10진수 HTML 엔티티 디코딩용 정규식 (예: •) - private static final Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); - // 16진수 HTML 엔티티 디코딩용 정규식 (예: •) - private static final Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); - - /** - * JSON 문자열 최상위가 배열일 때, 각 요소를 텍스트로 파싱하고 HTML 태그, HTML 엔티티,   등을 제거하여 리스트로 반환합니다. - * - * @param json JSON 배열 형태의 문자열 - * @return 정제된 텍스트 리스트 - * @throws IOException JSON 파싱 실패 시 발생 - */ - public static List extractAndClean(String json) throws IOException { - JsonNode root = objectMapper.readTree(json); - List texts = new ArrayList<>(); - - if (!root.isArray()) { - return texts; - } - - for (JsonNode element : root) { - if (element.isTextual()) { - String raw = element.asText().trim(); - if (raw.isEmpty()) { - continue; - } - - // 1) HTML 태그 제거 - String noHtml = TAG_REGEX.matcher(raw).replaceAll(""); - // 2)   등을 일반 공백으로 치환 - String withSpaces = noHtml.replaceAll(" ", " "); - // 3) HTML 엔티티 디코딩 (10진수 및 16진수) - String decoded = decodeHtmlEntities(withSpaces); - // 4) 최종 트리밍 - String clean = decoded.trim(); - - if (!clean.isEmpty()) { - texts.add(clean); - } - } - } - return texts; - } - - /** - * HTML 엔티티(10진수 &#DDD; 및 16진수 &#xHHHH;)를 대응하는 문자로 디코딩합니다. 예: "foo•bar" → "foo•bar", - * "foo•bar" → "foo•bar" - * - * @param input 엔티티를 포함한 문자열 - * @return 디코딩된 문자열 - */ - private static String decodeHtmlEntities(String input) { - String result = input; - - // 10진수 엔티티 디코딩 - Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(result); - StringBuffer sb = new StringBuffer(); - while (decMatcher.find()) { - int code = Integer.parseInt(decMatcher.group(1)); - decMatcher.appendReplacement(sb, - Matcher.quoteReplacement(Character.toString((char) code))); - } - decMatcher.appendTail(sb); - result = sb.toString(); - - // 16진수 엔티티 디코딩 - Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(result); - sb = new StringBuffer(); - while (hexMatcher.find()) { - int code = Integer.parseInt(hexMatcher.group(1), 16); - hexMatcher.appendReplacement(sb, - Matcher.quoteReplacement(Character.toString((char) code))); - } - hexMatcher.appendTail(sb); - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java b/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java deleted file mode 100644 index 580fc12..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.likelion.backendplus4.yakplus.logtest; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -/** - * 데이터 처리를 위한 컨트롤러 클래스 - * - * @modified 2025-04-18 - * @since 2025-04-16 - */ -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class MyController { - - private final MyService myService; - - /** - * 데이터 처리 요청을 처리하는 메서드 - * - * @return ResponseEntity 처리 결과 - * @author 정안식 - * @modified 2025-04-18 - * @since 2025-04-16 - */ - @GetMapping("/process") - public ResponseEntity process() { - log(LogLevel.INFO, LogMessage.DATA_PROCESSING_START.getMessage()); - - try { - String result = myService.processData(); - log(LogLevel.INFO, LogMessage.DATA_PROCESSING_SUCCESS.getMessage()); - return ResponseEntity.ok(result); - } catch (Exception e) { - log(LogLevel.ERROR, LogMessage.DATA_PROCESSING_ERROR.getMessage(), e); - return ResponseEntity.internalServerError() - .body(LogMessage.DATA_PROCESSING_ERROR.getMessage()); - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java b/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java deleted file mode 100644 index 3480559..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.likelion.backendplus4.yakplus.logtest; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; -import org.springframework.stereotype.Service; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -/** - * 데이터 처리를 위한 서비스 클래스 - * - * @modified 2025-04-18 - * @since 2025-04-16 - */ -@Service -public class MyService { - - private static final long PROCESSING_DELAY = 1000L; - - /** - * 데이터를 처리하는 메서드 - * - * @return String 처리된 데이터 결과 - * @throws RuntimeException 처리 중 오류 발생 시 - * @author 정안식 - * @modified 2025-04-18 - * @since 2025-04-16 - */ - public String processData() { - log(LogLevel.INFO, LogMessage.SERVICE_DATA_PROCESSING_START.getMessage()); - - try { - simulateProcessing(); - log(LogLevel.INFO, LogMessage.SERVICE_DATA_PROCESSING_SUCCESS.getMessage()); - return LogMessage.PROCESSED_DATA_RESULT.getMessage(); - } catch (InterruptedException e) { - log(LogLevel.ERROR, LogMessage.SERVICE_DATA_PROCESSING_ERROR.getMessage(), e); - Thread.currentThread().interrupt(); - throw new RuntimeException(LogMessage.SERVICE_DATA_PROCESSING_ERROR.getMessage(), e); - } - } - - /** - * 처리 과정을 시뮬레이션하는 private 메서드 - * - * @throws InterruptedException 인터럽트 발생 시 - * @author 정안식 - * @modified 2025-04-18 - * @since 2025-04-16 - */ - private void simulateProcessing() throws InterruptedException { - Thread.sleep(PROCESSING_DELAY); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java deleted file mode 100644 index 8749196..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.likelion.backendplus4.yakplus.switcher.application; - -import com.likelion.backendplus4.yakplus.switcher.application.port.in.EmbeddingRoutingUseCase; -import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; -import org.springframework.stereotype.Service; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -@Service -public class EmbeddingRouter implements EmbeddingRoutingUseCase { - private final EmbeddingSwitchPort switchPort; - - public EmbeddingRouter(EmbeddingSwitchPort switchPort) { - this.switchPort = switchPort; - } - - @Override - public void switchEmbedding(String adapterBeanName) { - log("임베딩 스위치 요청 수신 - 어댑터명: " + adapterBeanName); - switchPort.switchTo(adapterBeanName); - } - - @Override - public String getAdapterBeanName() { - log("현재 선택된 어댑터 빈 이름 요청"); - return switchPort.getAdapterBeanName(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java deleted file mode 100644 index 0e01bf3..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.likelion.backendplus4.yakplus.switcher.application.port.in; - -public interface EmbeddingRoutingUseCase { - void switchEmbedding(String adapterBeanName); - String getAdapterBeanName(); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java deleted file mode 100644 index 75b74bc..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.likelion.backendplus4.yakplus.switcher.application.port.out; - -public interface EmbeddingSwitchPort { - void switchTo(String adapterBeanName); - String getAdapterBeanName(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java deleted file mode 100644 index 6fdced3..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.likelion.backendplus4.yakplus.switcher.infrastructure.route.adapter; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.batch.embed.dto.DrugVectorDto; -import com.likelion.backendplus4.yakplus.index.application.port.out.EmbeddingLoadingPort; -import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -@Component("embeddingRouterAdapter") -@Primary -public class EmbeddingRouterAdapter implements EmbeddingLoadingPort, EmbeddingSwitchPort { - private static final String DEFAULT_ADAPTER = "gptEmbeddingLoadingAdapter"; - private final Map adapters; - private volatile EmbeddingLoadingPort embeddingLoadingPort; - private volatile String adapterBeanName; - - public EmbeddingRouterAdapter(Map allAdapters) { - this.adapters = allAdapters; - log("구현체 목록: " + adapters.keySet()); - } - - @PostConstruct - public void init() { - log("EmbeddingRouterAdapter 초기화 - 어댑터명: " + DEFAULT_ADAPTER); - switchTo(DEFAULT_ADAPTER); - } - - @Override - public List loadEmbeddingsByPage(Pageable pageable) { - if (embeddingLoadingPort == null) { - log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); - throw new IllegalStateException("No adapter selected"); - } - return embeddingLoadingPort.loadEmbeddingsByPage(pageable); - } - - @Override - public void switchTo(String adapterBeanName) { - log("어댑터 스위치 시도 - 어댑터명: " + adapterBeanName); - EmbeddingLoadingPort target = adapters.get(adapterBeanName); - if (target == null) { - log(LogLevel.ERROR, "어댑터 빈을 찾을 수 없습니다: " + adapterBeanName); - throw new IllegalArgumentException("Unknown adapter: " + adapterBeanName); - } - this.embeddingLoadingPort = target; - this.adapterBeanName = adapterBeanName; - log("어댑터 스위치 완료 - 현재 어댑터: " + adapterBeanName); - } - - @Override - public String getAdapterBeanName() { - log("어댑터 빈 이름 요청 - 현재 선택된 어댑터: " + adapterBeanName); - return adapterBeanName; - } - - @Override - public float[] getEmbedding(String text) { - if (embeddingLoadingPort == null) { - log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); - throw new IllegalStateException("No adapter selected"); - } - return embeddingLoadingPort.getEmbedding(text); - } - - @Override - public void saveEmbedding(List dtos) { - if (embeddingLoadingPort == null) { - log(LogLevel.ERROR, "임베딩 어댑터가 선택되지 않았습니다."); - throw new IllegalStateException("No adapter selected"); - } - embeddingLoadingPort.saveEmbedding(dtos); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java deleted file mode 100644 index 825b657..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.likelion.backendplus4.yakplus.switcher.presentation.controller; - -import com.likelion.backendplus4.yakplus.response.ApiResponse; -import com.likelion.backendplus4.yakplus.switcher.application.port.in.EmbeddingRoutingUseCase; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -@RestController -@RequestMapping("/switch/embeddings") -public class EmbeddingRouterController { - private final EmbeddingRoutingUseCase routerUseCase; - - public EmbeddingRouterController(EmbeddingRoutingUseCase routerUseCase) { - this.routerUseCase = routerUseCase; - } - - @PostMapping("/switch/{adapterBeanName}") - public ResponseEntity> switchAdapter(@PathVariable String adapterBeanName) { - log("스위치 대상 인덱스명 : " + adapterBeanName); - routerUseCase.switchEmbedding(adapterBeanName); - return ApiResponse.success("어댑터 변경됨 - 어댑터명: " + adapterBeanName); - } - - @GetMapping("/current/adapter") - public ResponseEntity> checkCurrentAdapter() { - return ApiResponse.success(routerUseCase.getAdapterBeanName()); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java new file mode 100644 index 0000000..ac3bd75 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java @@ -0,0 +1,20 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.in; + +public interface DictionaryUseCase { + + /** + * 증상 사전 데이터 설정 메서드 + *

+ * 1) JSON 파일에서 증상 단어 리스트를 읽어들여, + * 2) 중복되지 않는 항목을 JPA DB에 저장하고, + * 3) Elasticsearch에도 저장합니다. + *

+ * 내부 로그는 각 단계별 완료 시 출력됩니다. + * + * @author 박찬병 + * @modified 2025-05-03 + * @since 2025-05-01 + */ + void setDictionary(); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java new file mode 100644 index 0000000..95b0442 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out; + +import java.util.Set; + +public interface SymptomDictionaryElsRepositoryPort { + + /** + * 주어진 증상 단어 리스트를 바탕으로 Elasticsearch에 신규 증상 사전 문서를 일괄 저장합니다. + * 1) DictionaryMapper를 통해 Document 객체로 변환한 뒤, + * 2) repository.saveAll을 + * 호출하여 일괄 색인합니다. + * + * @param symptoms 저장할 증상 단어 리스트 + * @author 박찬병 + * @modified 2025-05-01 + * @since 2025-04-30 + */ + void setDictionary(Set symptoms); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java new file mode 100644 index 0000000..0ba9a3c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java @@ -0,0 +1,23 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out; + +import java.util.Set; + +public interface SymptomDictionaryJpaRepositoryPort { + + + /** + * 주어진 증상 단어 리스트를 바탕으로 + * 데이터베이스에 신규 증상 단어만 저장합니다. + * + * 1) 기존에 저장된 증상명을 조회하고, + * 2) 전달받은 리스트에서 중복되지 않는 단어만 필터링한 후, + * 3) 엔티티로 변환하여 일괄 저장합니다. + * + * @param symptoms 저장할 증상 단어 리스트 + * @author 박찬병 + * @modified 2025-05-01 + * @since 2025-04-30 + */ + void setDictionary(Set symptoms); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java new file mode 100644 index 0000000..27f7bd9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java @@ -0,0 +1,21 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out; + +import com.likelion.backendplus4.yakplus.symptomdictionary.exception.DictionaryException; + +import java.util.Set; + +public interface SymptomDictionaryLoaderPort { + + /** + * ClassPathResource를 통해 지정된 JSON 파일을 읽고, + * Set 형태로 변환하여 반환합니다. + * + * @return JSON에 정의된 증상 문자열 리스트 + * @throws DictionaryException 파일 형식 오류 또는 파싱/IO 실패 시 발생 + * @since 2025-04-30 + * @author 박찬병 + * @modified 2025-05-01 + */ + Set loadDictionary(); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java new file mode 100644 index 0000000..640d41a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java @@ -0,0 +1,59 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.service; + +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.in.DictionaryUseCase; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryElsRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out.JsonSymptomDictionaryLoader; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Set; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * 증상 사전 데이터를 관리하는 서비스 클래스입니다. + * + * + * @author 박찬병 + * @since 2025-05-01 + * @modified 2025-05-03 + */ +@Service +@RequiredArgsConstructor +public class DictionaryService implements DictionaryUseCase { + + private static final String INDENT = " "; + + private final JsonSymptomDictionaryLoader jsonSymptomDictionaryLoader; + private final SymptomDictionaryJpaRepositoryPort dictionaryRepositoryPort; + private final SymptomDictionaryElsRepositoryPort dictionaryElsRepositoryPort; + + /** + * 증상 사전 데이터 설정 메서드 + * 1) JSON 파일에서 증상 단어 리스트를 읽어들여, + * 2) 중복되지 않는 항목을 JPA DB에 저장하고, + * 3) Elasticsearch에도 저장합니다. + * + * 내부 로그는 각 단계별 완료 시 출력됩니다. + * + * @author 박찬병 + * @modified 2025-05-03 + * @since 2025-05-01 + */ + @Override + public void setDictionary() { + log("DictionaryService.setDictionary() 호출 시작"); + + Set symptomList = jsonSymptomDictionaryLoader.loadDictionary(); + log(INDENT+"loadDictionary() 완료, 증상 수: " + symptomList.size()); + + dictionaryRepositoryPort.setDictionary(symptomList); + log(INDENT+"SymptomDictionaryJpaAdapter.setDictionary() 완료"); + + dictionaryElsRepositoryPort.setDictionary(symptomList); + log(INDENT+"SymptomDictionaryElsAdapter.setDictionary() 완료"); + + log("DictionaryService.setDictionary() 호출 종료"); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/DictionaryException.java similarity index 86% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/DictionaryException.java index edb9d89..4e0e93e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/DictionaryException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/DictionaryException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.dictionary.exception; +package com.likelion.backendplus4.yakplus.symptomdictionary.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/error/DictionaryErrorCode.java similarity index 91% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/error/DictionaryErrorCode.java index 56310ef..ed94e85 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/exception/error/DictionaryErrorCode.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/error/DictionaryErrorCode.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.dictionary.exception.error; +package com.likelion.backendplus4.yakplus.symptomdictionary.exception.error; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java new file mode 100644 index 0000000..851b416 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java @@ -0,0 +1,66 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryLoaderPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.exception.DictionaryException; +import com.likelion.backendplus4.yakplus.symptomdictionary.exception.error.DictionaryErrorCode; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +/** + * classpath에 위치한 JSON 파일로부터 증상 사전 데이터를 로드하는 클래스입니다. + * SymptomDictionaryLoaderPort를 구현하여 JSON 파싱 로직을 캡슐화합니다. + * + * @since 2025-04-30 + * @modified 2025-05-03 + */ +@RequiredArgsConstructor +@Component +public class JsonSymptomDictionaryLoader implements SymptomDictionaryLoaderPort { + + private final ObjectMapper objectMapper; + private final ResourceLoader resourceLoader; + + + @Value("${dictionary.path}") + private String DICTIONARY_FILE_PATH; + + /** + * ClassLoader를 통해 지정된 JSON 파일을 읽고, + * Set 형태로 변환하여 반환합니다. + * + * @return JSON에 정의된 증상 문자열 리스트 + * @throws DictionaryException 파일 형식 오류 또는 파싱/IO 실패 시 발생 + * @since 2025-04-30 + * @author 박찬병 + * @modified 2025-05-04 + */ + @Override + public Set loadDictionary() { + log("loadDictionary() 호출 - 경로: " + DICTIONARY_FILE_PATH); + + if (!DICTIONARY_FILE_PATH.toLowerCase().endsWith(".json")) { + throw new DictionaryException(DictionaryErrorCode.INVALID_FILE_TYPE); + } + + try (InputStream in = resourceLoader.getResource(DICTIONARY_FILE_PATH).getInputStream()) { + Set result = objectMapper.readValue(in, new TypeReference<>() {}); + log("사전 로딩 완료 - 크기: " + result.size()); + return result; + } catch (IOException e) { + log(LogLevel.ERROR, "사전 파일 로딩 실패", e); + throw new DictionaryException(DictionaryErrorCode.DICTIONARY_LOAD_FAILURE); + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java similarity index 52% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java index 3843bce..9417c36 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java @@ -1,13 +1,14 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out; +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryElsRepositoryPort; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.SymptomDictionaryDocRepository; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.support.mapper.DictionaryMapper; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryElsRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.SymptomDictionaryDocRepository; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.support.mapper.DictionaryMapper; import java.util.List; +import java.util.Set; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -26,9 +27,8 @@ public class SymptomDictionaryElsAdapter implements SymptomDictionaryElsReposito /** * 주어진 증상 단어 리스트를 바탕으로 Elasticsearch에 신규 증상 사전 문서를 일괄 저장합니다. - * 1) 입력된 리스트에서 중복을 제거하고, - * 2) DictionaryMapper를 통해 Document 객체로 변환한 뒤, - * 3) repository.saveAll을 + * 1) DictionaryMapper를 통해 Document 객체로 변환한 뒤, + * 2) repository.saveAll을 * 호출하여 일괄 색인합니다. * * @param symptoms 저장할 증상 단어 리스트 @@ -38,14 +38,11 @@ public class SymptomDictionaryElsAdapter implements SymptomDictionaryElsReposito */ @Override @Transactional - public void setDictionary(List symptoms) { + public void setDictionary(Set symptoms) { log("setDictionary() 메서드 호출, 입력 증상 수: " + symptoms.size()); - // 중복 제거 및 Document 변환 List docs = symptoms.stream() - .distinct() - .map(DictionaryMapper::toDocument) + .map(DictionaryMapper::toSymptomDocument) .toList(); - log("setDictionary() distinct 처리 후 문서 수: " + docs.size()); // 일괄 저장 repository.saveAll(docs); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java similarity index 73% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java index 07a7481..45bb24c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java @@ -1,10 +1,10 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.adapter.out; +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; -import com.likelion.backendplus4.yakplus.dictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.SymptomDictionaryRepository; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity.SymptomDictionary; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.support.mapper.DictionaryMapper; +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.SymptomDictionaryRepository; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.support.mapper.DictionaryMapper; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -41,7 +41,7 @@ public class SymptomDictionaryJpaAdapter implements SymptomDictionaryJpaReposito */ @Override @Transactional - public void setDictionary(List symptoms) { + public void setDictionary(Set symptoms) { log("setDictionary() 메서드 호출, 입력 증상 수: " + symptoms.size()); // 1) 기존에 존재하는 증상 단어 조회 @@ -58,7 +58,7 @@ public void setDictionary(List symptoms) { // 3) 엔티티 변환 및 저장 List entities = toInsert.stream() - .map(DictionaryMapper::toEntity) + .map(DictionaryMapper::toSymptomEntity) .toList(); repository.saveAll(entities); log("setDictionary() 완료, 저장된 엔티티 수: " + entities.size()); diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java new file mode 100644 index 0000000..00e6774 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository; + +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface SymptomDictionaryDocRepository extends ElasticsearchRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java new file mode 100644 index 0000000..d8b56bb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository; + +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SymptomDictionaryRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java similarity index 89% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java index 50c76c7..0300808 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document; +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document; import java.util.List; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java index 2eb816e..ec637ac 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/support/mapper/DictionaryMapper.java similarity index 50% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/support/mapper/DictionaryMapper.java index 7b7c62c..8ecfb41 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/infrastructure/support/mapper/DictionaryMapper.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/support/mapper/DictionaryMapper.java @@ -1,7 +1,7 @@ -package com.likelion.backendplus4.yakplus.dictionary.infrastructure.support.mapper; +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.support.mapper; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; -import com.likelion.backendplus4.yakplus.dictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity.SymptomDictionary; import java.util.List; /** @@ -22,25 +22,24 @@ public class DictionaryMapper { * @since 2025-05-01 * @modified 2025-05-01 */ - public static SymptomDictionary toEntity(String name) { - SymptomDictionary entity = new SymptomDictionary(null, name); - return entity; + public static SymptomDictionary toSymptomEntity(String name) { + return SymptomDictionary.builder() + .name(name) + .build(); } /** * 단일 증상명을 Elasticsearch 색인용 문서로 변환합니다. * - * @param name 증상명 문자열 - * @return SymptomDictionaryDocument ES 문서 객체 + * @param name 증상명 문자열 * @return SymptomDictionaryDocument ES 문서 객체 * @author 박찬병 * @since 2025-05-01 * @modified 2025-05-01 */ - public static SymptomDictionaryDocument toDocument(String name) { - SymptomDictionaryDocument doc = SymptomDictionaryDocument.builder() + public static SymptomDictionaryDocument toSymptomDocument(String name) { + return SymptomDictionaryDocument.builder() .symptom(name) .symptomSuggester(List.of(name)) .build(); - return doc; } } \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/DictionaryController.java similarity index 64% rename from src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java rename to src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/DictionaryController.java index 4a1dfaa..d9dd9fb 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/dictionary/presentation/controller/DictionaryController.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/DictionaryController.java @@ -1,34 +1,34 @@ -package com.likelion.backendplus4.yakplus.dictionary.presentation.controller; +package com.likelion.backendplus4.yakplus.symptomdictionary.presentation.controller; -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.dictionary.application.port.in.DictionaryUseCase; -import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.in.DictionaryUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import com.likelion.backendplus4.yakplus.symptomdictionary.presentation.controller.docs.DictionaryControllerDocs; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; - +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; /** * 사전 관리용 REST API를 제공하는 컨트롤러 클래스입니다. * - * @since 2025-05-01 * @modified 2025-05-01 + * @since 2025-05-01 */ @RestController -@RequestMapping("/api/dictionary") +@RequestMapping("/dictionary") @RequiredArgsConstructor -public class DictionaryController { +public class DictionaryController implements DictionaryControllerDocs { private final DictionaryUseCase dictionaryUseCase; /** * 증상 사전 데이터를 JSON 파일로부터 로드하여 * DB 및 Elasticsearch에 저장하는 작업을 수행합니다. - * + *

* 1) JSON 로더를 통해 증상 리스트 로드 * 2) JPA 어댑터로 DB 저장 * 3) Elasticsearch 어댑터로 색인 저장 @@ -36,15 +36,15 @@ public class DictionaryController { * @return 성공 여부를 담은 ApiResponse * @throws RuntimeException 처리 중 오류 발생 시 전달 * @author 박찬병 - * @since 2025-05-01 * @modified 2025-05-01 + * @since 2025-05-01 */ @PostMapping("/set") public ResponseEntity> setDictionary() { log("setDictionary() 호출"); dictionaryUseCase.setDictionary(); - return ApiResponse.success(); + return success(); } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java new file mode 100644 index 0000000..4b6d181 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * 증상 사전 관리 API 문서 정의 인터페이스 + * + * @since 2025-05-04 + */ +@Tag(name = "Dictionary", description = "증상 사전 관리 API") +public interface DictionaryControllerDocs { + + @Operation( + summary = "증상 사전 데이터 로드 및 저장", + description = "JSON 파일로부터 증상 사전 데이터를 로드하여 DB 및 Elasticsearch에 저장하는 작업을 수행합니다." + ) + ResponseEntity> setDictionary(); +} + diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java deleted file mode 100644 index cd7dd5c..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/in/IndexTempUseCase.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.application.port.in; - -public interface IndexTempUseCase { - - void indexKeyword(); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java deleted file mode 100644 index 31286c9..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempDrugIndexRepositoryPort.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.application.port.out; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import org.springframework.data.domain.Page; - -public interface TempDrugIndexRepositoryPort { - - void saveAllSymptom(Page drugPage); - - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java deleted file mode 100644 index 5034354..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/port/out/TempRawDataPort.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.application.port.out; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -public interface TempRawDataPort { - - Page findAllDrugs(Pageable pageable); -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java deleted file mode 100644 index 088ba87..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/application/service/DrugTempIndexer.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.application.service; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.temp.application.port.in.IndexTempUseCase; -import com.likelion.backendplus4.yakplus.temp.application.port.out.TempDrugIndexRepositoryPort; -import com.likelion.backendplus4.yakplus.temp.application.port.out.TempRawDataPort; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class DrugTempIndexer implements IndexTempUseCase { - - private final TempRawDataPort govDrugRawDataPort; - private final TempDrugIndexRepositoryPort drugIndexRepositoryPort; - - private static final int CHUNK_SIZE = 1_000; - - @Override - public void indexKeyword() { - log("indexKeyword 요청 수신"); - int page = 0; - Page drugPage; - - do { - log("색인 시작: page=" + page); - - // 1. 페이징으로 DB에서 한 청크 가져오기 - drugPage = govDrugRawDataPort.findAllDrugs(PageRequest.of(page, CHUNK_SIZE)); - log(" 조회 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); - - // 2. 청크별 ES에 색인 - drugIndexRepositoryPort.saveAllSymptom(drugPage); - log(" 색인 완료: page=" + page + ", 건수=" + drugPage.getNumberOfElements()); - - // 3. 다음 1000개 값 루프 - page++; - } while (drugPage.hasNext()); - log("indexSymptom 전체 처리 완료"); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java deleted file mode 100644 index 7c83897..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempElasticsearchDrugAdapter.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.DrugSymptomRepository; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.repository.document.DrugSymptomDocument; -import com.likelion.backendplus4.yakplus.temp.application.port.out.TempDrugIndexRepositoryPort; -import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document.DrugKeywordDocument; -import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.repository.DrugKeywordRepository; -import com.likelion.backendplus4.yakplus.temp.support.mapper.KeywordMapper; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class TempElasticsearchDrugAdapter implements TempDrugIndexRepositoryPort { - - private final DrugKeywordRepository keywordRepository; - - @Override - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void saveAllSymptom(Page drugs) { - // 도메인 → ES Document 변환 - log("saveAllSymptom() 요청 수신"); - List docs = drugs.stream() - .map(KeywordMapper::toDocument) // 내부에서 예외 처리 됨 - .toList(); - log(" 문서 변환 완료: count=" + docs.size()); - - keywordRepository.saveAll(docs); - log(" ES 색인 완료: count=" + docs.size()); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java deleted file mode 100644 index 9d3e3b3..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/TempGovDrugRawDataAdapter.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.jpa.GovDrugJpaRepository; -import com.likelion.backendplus4.yakplus.temp.application.port.out.TempRawDataPort; -import com.likelion.backendplus4.yakplus.temp.support.mapper.TempDrugRawDataMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class TempGovDrugRawDataAdapter implements TempRawDataPort { - - private final GovDrugJpaRepository drugJpaRepository; - - public Page findAllDrugs(Pageable pageable) { - log("findAllDrugs() 요청 수신"); - return drugJpaRepository.findByIsGeneralIsTrue(pageable) - .map(TempDrugRawDataMapper::toDomainFromEntity); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java deleted file mode 100644 index e736569..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/infrastructure/adapter/persistence/repository/DrugKeywordRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.repository; - -import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document.DrugKeywordDocument; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; - -public interface DrugKeywordRepository extends ElasticsearchRepository { - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java deleted file mode 100644 index 5f5076f..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/presentation/controller/DrugTempController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.presentation.controller; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -import com.likelion.backendplus4.yakplus.response.ApiResponse; -import com.likelion.backendplus4.yakplus.temp.application.port.in.IndexTempUseCase; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/drugs/index") -@RequiredArgsConstructor -public class DrugTempController { - - private final IndexTempUseCase indexUseCase; - - /** - * 색인 배치 작업을 실행하여, DB로부터 조회한 약품 증상 데이터를 Elasticsearch에 일괄 색인합니다. - * - * @return 색인 작업 성공 여부 응답 (Void) - * @author 박찬병 - * @modified 2025-04-25 - * @since 2025-04-24 - */ - @PostMapping("/keyword") - public ResponseEntity> triggerIndex() { - log("indexSymptom 요청 수신"); - indexUseCase.indexKeyword(); - return ApiResponse.success(); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java deleted file mode 100644 index 5aa770f..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/KeywordMapper.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.support.mapper; - -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; -import com.likelion.backendplus4.yakplus.index.support.parser.SymptomTextParser; -import com.likelion.backendplus4.yakplus.temp.infrastructure.adapter.persistence.document.DrugKeywordDocument; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class KeywordMapper { - - public static DrugKeywordDocument toDocument(Drug entity) { - // 1) 추출된 텍스트 리스트를 단일 문자열로 전처리 - String flatText = SymptomTextParser.flattenLines(entity.getEfficacy()); - // 2) 전처리된 문자열을 자동완성용 토큰 리스트로 변환 - List suggestTokens = SymptomTextParser.tokenizeForSuggestion(flatText); - - // 3) 성분명 자동완성 필드 - List ingredientSuggest = Optional.ofNullable(entity.getMaterialInfo()) - .orElse(Collections.emptyList()) - .stream() - .map(Material::getName) - .filter(Objects::nonNull) // MaterialName이 null일 때 걸러주기 - .toList(); - - return DrugKeywordDocument.builder() - .drugId(entity.getDrugId()) - .drugName(entity.getDrugName()) - .efficacy(entity.getEfficacy()) - .efficacyList(suggestTokens) - .imageUrl(entity.getImageUrl()) - .company(entity.getCompany()) - .drugNameSuggester(List.of(entity.getDrugName())) - .ingredientName(ingredientSuggest) - .ingredientNameSuggester(ingredientSuggest) - .build(); - } - - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java deleted file mode 100644 index 3548cf6..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/temp/support/mapper/TempDrugRawDataMapper.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.likelion.backendplus4.yakplus.temp.support.mapper; - -import com.likelion.backendplus4.yakplus.drug.application.service.exception.ScraperException; -import com.likelion.backendplus4.yakplus.drug.application.service.exception.error.ScraperErrorCode; -import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; -import com.likelion.backendplus4.yakplus.drug.infrastructure.persistence.repository.entity.DrugRawDataEntity; -import com.likelion.backendplus4.yakplus.index.infrastructure.adapter.persistence.DrugMapper; -import com.likelion.backendplus4.yakplus.index.support.parser.JsonArrayTextParser; -import java.io.IOException; -import java.util.List; - -public class TempDrugRawDataMapper { - - public static Drug toDomainFromEntity(DrugRawDataEntity e) { - List efficacy; - try { - efficacy = JsonArrayTextParser.extractAndClean(e.getEfficacy()); - } catch (IOException exception) { - throw new ScraperException(ScraperErrorCode.PARSING_ERROR); - } - - return Drug.builder() - .drugId(e.getDrugId()) - .drugName(e.getDrugName()) - .company(e.getCompany()) - .permitDate(e.getPermitDate()) - .isGeneral(e.isGeneral()) - .materialInfo(DrugMapper.parseMaterials(e.getMaterialInfo())) - .storeMethod(e.getStoreMethod()) - .validTerm(e.getValidTerm()) - .efficacy(efficacy) - .usage(DrugMapper.parseStringToList(e.getUsage())) - .precaution(DrugMapper.parsePrecaution(e.getPrecaution())) - .imageUrl(e.getImageUrl()) - .cancelDate(e.getCancelDate()) - .cancelName(e.getCancelName()) - .isHerbal(e.getIsHerbal()) - .build(); - } - -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b5f334b..031b79c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,9 +38,20 @@ gov: path: detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnDtlInq05 img: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnInq06 - +log: + rolling: + directory: logs + file-name: yakplus-batch.log + pattern: "%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n" + max-history: 30 + total-size-cap: 10MB embed: kmbert: embed.techlog.dev krsbert: embedb.techlog.dev + switcher: + default-adapter: openAiEmbeddingAdapter + +dictionary: + path: classpath:unique_symptoms.json server: port: 8077 From 04de952df1b14ab3edb8bddd8795cf6350148f8b Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Mon, 5 May 2025 23:33:15 +0900 Subject: [PATCH 43/47] =?UTF-8?q?=F0=9F=93=A6=20Chore:=20=EC=A6=9D?= =?UTF-8?q?=EC=83=81=20=EC=82=AC=EC=A0=84=20=EB=B9=8C=EB=93=9C=20=EC=8B=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-deploy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 577a40b..a5cafdb 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -24,6 +24,11 @@ jobs: - name: Make gradlew executable run: chmod +x ./gradlew + - name: Copy Dictionary + run: | + mkdir -p src/main/resources + echo '${{ secrets.DICTIONARY }}' > src/main/resources/unique_symptoms.json + - name: Build Spring Boot (JAR) run: ./gradlew bootJar -Penv=test From f4ee62431a5022f395124cc86e450775248d241c Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Mon, 5 May 2025 23:36:47 +0900 Subject: [PATCH 44/47] =?UTF-8?q?Feature/#72=20=EC=A6=9D=EC=83=81=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=20=EC=B6=94=EA=B0=80=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📦 Chore: 증상 사전 빌드 시 추가 * 📦 Chore: 증상 사전 빌드용 수정 --- .../embed/infrastructure/batch/job/config/EmbedJobConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java index 2cd8797..79f0cc1 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/job/config/EmbedJobConfig.java @@ -7,7 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -/** +/*** * 임베딩 처리 작업을 구성하는 Spring Batch 설정 클래스입니다. *

* 모델 스위칭과 임베딩 작업을 순차적으로 구성하여 실행합니다. From 30b802a902f07d84de95b3669f24391644ae4c52 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 6 May 2025 00:09:25 +0900 Subject: [PATCH 45/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=20=EC=B2=AD=ED=81=AC=20=EC=82=AC=EC=9D=B4=EC=A6=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yakplus/drug/index/application/service/DrugIndexer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java index 8488d85..660534c 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java @@ -31,7 +31,7 @@ public class DrugIndexer implements IndexUseCase { private final EmbeddingSwitchPort embeddingSwitchPort; private static final String INDENT = " "; - private static final int CHUNK_SIZE = 1_000; + private static final int CHUNK_SIZE = 100; /** * 요청으로 전달된 lastSeq, limit 정보를 바탕으로 RDB에서 데이터를 조회하고 From b177515d1db9d9a63aadc6bb5805cfc28cef1588 Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 6 May 2025 00:21:42 +0900 Subject: [PATCH 46/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20url=20ES=ED=95=84=EB=93=9C=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Fix: 인덱스 청크 사이즈 수정 * 🐛 Fix: 이미지 url ES필드 타입 수정 --- .../persistence/elasticsearch/document/DrugKeywordDocument.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java index 9fc125a..17f1180 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java @@ -29,7 +29,7 @@ public class DrugKeywordDocument { @Field(type = FieldType.Text, name = "company") private String company; - @Field(type = FieldType.Keyword, name = "imageUrl") + @Field(type = FieldType.Text, name = "imageUrl") private String imageUrl; @Field(type = FieldType.Text, name = "efficacy") From efd462ab8402b5e0d1714bf5ae483b7041b4417c Mon Sep 17 00:00:00 2001 From: "Yejeong, Ham" Date: Tue, 6 May 2025 01:15:59 +0900 Subject: [PATCH 47/47] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EC=83=89=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=96=B4=EB=8C=91=ED=84=B0=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=98=81=EB=AC=B8=20=EC=86=8C=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jpa/adapter/EmbeddingRouterAdapter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java index 2de4ad1..d1cff91 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Component; import java.util.List; +import java.util.Locale; import java.util.Map; import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; @@ -136,6 +137,6 @@ public void switchTo(String adapterBeanName) { @Override public String getAdapterBeanName() { log("어댑터 빈 이름 요청 - 현재 선택된 어댑터: " + adapterBeanName); - return adapterBeanName; + return adapterBeanName.toLowerCase(); } }