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/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml
index e7b999b..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
@@ -38,7 +43,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/build.gradle b/build.gradle
index 172f63c..7f28cb8 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,16 @@ 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'
+
+
+ // 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/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/config/ApiRestTemplateConfig.java
rename to src/main/java/com/likelion/backendplus4/yakplus/common/configuration/ApiRestTemplateConfig.java
index a449759..3bb672b 100644
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/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.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/common/configuration/OpenaiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/OpenaiConfig.java
new file mode 100644
index 0000000..50e3760
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/OpenaiConfig.java
@@ -0,0 +1,17 @@
+package com.likelion.backendplus4.yakplus.common.configuration;
+
+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() {
+ 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 75d2e98..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,4 +1,4 @@
-package com.likelion.backendplus4.yakplus.common.util.log;
+package com.likelion.backendplus4.yakplus.common.logging.trace;
import lombok.Getter;
import org.slf4j.Logger;
@@ -76,26 +76,10 @@ private static Logger makeLogger() {
*/
private static String makeTraceId() {
String traceId = MDC.get("traceId");
- validateTraceId(traceId);
- 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๋ฅผ ์ค์ ํด์ฃผ์ธ์.");
+ if (traceId == null || traceId.trim().isEmpty()) {
+ return "no-trace";
}
+ return 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/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 eabec2b..0000000
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataService.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.likelion.backendplus4.yakplus.drug.application.service;
-
-import java.util.List;
-
-import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug;
-
-public interface DrugDataService {
- List findAllRawDrug();
-}
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 9ce3101..0000000
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugDataServiceImpl.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.likelion.backendplus4.yakplus.drug.application.service;
-
-import static java.util.stream.Collectors.*;
-
-import java.util.List;
-
-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.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() {
- log.info("findAllRawDrug called");
- return govDrugJpaRepository.findAll().stream()
- .map(DrugDataMapper::toDomainFromEntity)
- .collect(toList());
- }
-}
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 78a4359..0000000
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/application/service/DrugImageGovScraper.java
+++ /dev/null
@@ -1,48 +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.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;
-
- @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);
- }
-}
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..2417189
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/Drug.java
@@ -0,0 +1,72 @@
+package com.likelion.backendplus4.yakplus.drug.domain.model;
+
+import java.time.LocalDate;
+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;
+
+/**
+ * ์์ฝํ ์ ๋ณด๋ฅผ ๋ด๋ ๋๋ฉ์ธ ๊ฐ์ฒด์
๋๋ค.
+ */
+@Builder
+@Getter
+@ToString
+public class Drug {
+ 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[] vector;
+ 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/domain/model/DrugRawData.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java
new file mode 100644
index 0000000..7f5ccff
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/DrugRawData.java
@@ -0,0 +1,36 @@
+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 jakarta.persistence.Column;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+
+/**
+ * ์ธ๋ถ API๋ก๋ถํฐ ์์ง๋ ์์ฝํ ์์ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ๊ฐ์ฒด์
๋๋ค.
+ */
+@Builder
+@Getter
+@ToString
+public class DrugRawData {
+ 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 LocalDate cancelDate;
+ private String cancelName;
+ private boolean isHerbal;
+}
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 aa20b2d..0000000
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrug.java
+++ /dev/null
@@ -1,102 +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 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;
-
-@Builder
-@Getter
-public class GovDrug {
- 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 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 rawTitle = article.get("title").asText();
- WarningType type = WarningType.fromLabel(rawTitle);
-
- List texts = new ArrayList<>();
- for (JsonNode paragraph : article.get("paragraphs")) {
- texts.add(paragraph.get("text").asText());
- }
-
- result.put(type, texts);
- }
-
- } catch (JsonProcessingException e) {
- throw new RuntimeException(e);
- }
-
-
- return result;
- }
-}
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
deleted file mode 100644
index bb7ceb0..0000000
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/GovDrugDetail.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.likelion.backendplus4.yakplus.drug.domain.model;
-
-import java.time.LocalDate;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import lombok.Builder;
-import lombok.Getter;
-
-@Builder
-@Getter
-public class GovDrugDetail {
- 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;
-
- 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/vo/Material.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/Material.java
index fab8c35..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
@@ -5,6 +5,9 @@
import lombok.Getter;
import lombok.ToString;
+/**
+ * ์์ฝํ ์ฑ๋ถ ์ ๋ณด๋ฅผ ๋ํ๋ด๋ ๊ฐ ๊ฐ์ฒด(Value Object)์
๋๋ค.
+ */
@Getter
@ToString
public class Material {
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 75f29df..0000000
--- a/src/main/java/com/likelion/backendplus4/yakplus/drug/domain/model/vo/MaterialInfo.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.likelion.backendplus4.yakplus.drug.domain.model.vo;
-
-import java.util.List;
-
-public class MaterialInfo {
- private String totalAmount;
- private List ingredients;
-}
\ No newline at end of file
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/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/DrugEmbedProcessorUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/DrugEmbedProcessorUseCase.java
new file mode 100644
index 0000000..e42366a
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/port/in/DrugEmbedProcessorUseCase.java
@@ -0,0 +1,62 @@
+package com.likelion.backendplus4.yakplus.drug.embed.application.port.in;
+
+/**
+ * ์์ฝํ ํจ๋ฅ ์ ๋ณด๋ฅผ ์๋ฒ ๋ฉ ์ฒ๋ฆฌ๋ฅผ ์ํ ์ ์ค์ผ์ด์ค ์ธํฐํ์ด์ค์
๋๋ค.
+ *
+ * @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/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/embed/application/service/DrugEmbedProcessorService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/DrugEmbedProcessorService.java
new file mode 100644
index 0000000..ff15330
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/application/service/DrugEmbedProcessorService.java
@@ -0,0 +1,51 @@
+package com.likelion.backendplus4.yakplus.drug.embed.application.service;
+
+import org.springframework.stereotype.Service;
+
+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;
+
+/**
+ * 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/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..79f0cc1
--- /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 extends DrugVectorDto> 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/embed/infrastructure/persistence/entity/DrugGptEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugGptEmbedEntity.java
new file mode 100644
index 0000000..c1c0696
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugGptEmbedEntity.java
@@ -0,0 +1,30 @@
+package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.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 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/embed/infrastructure/persistence/entity/DrugKmBertEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKmBertEmbedEntity.java
new file mode 100644
index 0000000..7f365ea
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKmBertEmbedEntity.java
@@ -0,0 +1,30 @@
+package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.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 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/embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java
new file mode 100644
index 0000000..5e5a7a9
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/entity/DrugKrSBertEmbedEntity.java
@@ -0,0 +1,30 @@
+package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.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_KR-SBERT")
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+public class DrugKrSBertEmbedEntity implements EmbeddingEntity{
+ @Id
+ @Column( name= "ITEM_SEQ")
+ private Long drugId;
+
+ @Column( name= "KR_SBERT_VECTOR", columnDefinition = "JSON")
+ 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..d1cff91
--- /dev/null
+++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/EmbeddingRouterAdapter.java
@@ -0,0 +1,142 @@
+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.Locale;
+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.toLowerCase();
+ }
+}
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