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์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ ๋‹จ๊ณ„๋Š” ๋‹ค์Œ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค: + *

+ * + * @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 ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค: + *

+ *

+ * ๋˜ํ•œ, ๊ฐ Slave Step์€ JPA Paging Reader์™€ Retry/Skip ์ •์ฑ…์ด ์ ์šฉ๋œ chunk ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. + * Partitioning์„ ์œ„ํ•ด Drug ID ๋ฒ”์œ„ ๊ธฐ๋ฐ˜ ํŒŒํ‹ฐ์…”๋„ˆ๊ฐ€ ์‚ฌ์šฉ๋˜๋ฉฐ, + * ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด TaskExecutor๊ฐ€ ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค. + *

+ *

+ * batchExecutorName: ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ TaskExecutor ์ด๋ฆ„ + * openAiEmbeddingAdapterName: OpenAI ์ž„๋ฒ ๋”ฉ ์–ด๋Œ‘ํ„ฐ ์ด๋ฆ„ + * kmBertEmbeddingAdapterName: KM-BERT ์ž„๋ฒ ๋”ฉ ์–ด๋Œ‘ํ„ฐ ์ด๋ฆ„ + * krSBertEmbeddingAdapterName: KR-SBERT ์ž„๋ฒ ๋”ฉ ์–ด๋Œ‘ํ„ฐ ์ด๋ฆ„ + * retryLimit: ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + * retryException: ์žฌ์‹œ๋„ํ•  ์˜ˆ์™ธ ํด๋ž˜์Šค + * modelSwitchStepName: ๋ชจ๋ธ ์Šค์œ„์นญ Step ์ด๋ฆ„ + * openAiStepName: OpenAI ์ž„๋ฒ ๋”ฉ Step ์ด๋ฆ„ + * kmBertStepName: KM-BERT ์ž„๋ฒ ๋”ฉ Step ์ด๋ฆ„ + * krSBertStepName: KR-SBERT ์ž„๋ฒ ๋”ฉ Step ์ด๋ฆ„ + * rawDataSelectQuery: ์›๋ณธ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ฟผ๋ฆฌ + * + * @since 2025-05-02 + */ +@Configuration +public class EmbedStepConfig { + private final String batchExecutorName = "singleItemExecutor"; + + private final String openAiEmbeddingAdapterName = "openAiEmbeddingAdapter"; + private final String kmBertEmbeddingAdapterName = "kmBertEmbeddingAdapter"; + private final String krSBertEmbeddingAdapterName = "krSBertEmbeddingAdapter"; + + private final int retryLimit = 3; + private final Class retryException = Exception.class; + + private final String modelSwitchStepName = "switchModelStep"; + + private final String openAiStepName = "openAiEmbedStep"; + private final String kmBertStepName = "kmBertEmbedStep"; + private final String krSBertStepName = "krSBertEmbedStep"; + private final String rawDataSelectQuery = "SELECT d FROM DrugRawDataEntity d WHERE d.drugId BETWEEN :minId AND :maxId ORDER BY d.drugId"; + + + private final DrugIdRangePartitioner drugIdRangePartitioner; + private final TaskExecutor taskExecutor; + + public EmbedStepConfig(DrugIdRangePartitioner drugIdRangePartitioner, + @Qualifier(batchExecutorName) + TaskExecutor taskExecutor) { + this.drugIdRangePartitioner = drugIdRangePartitioner; + this.taskExecutor = taskExecutor; + } + + /** + * OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ๋กœ ์ „ํ™˜ํ•˜๋Š” Tasklet ๊ธฐ๋ฐ˜ Step์ž…๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param tx: ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param switchPort: EmbeddingSwitchPort ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step switchModelStepToOpenAi(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo(openAiEmbeddingAdapterName); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder(modelSwitchStepName + "ToOpenAi", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + /** + * KM-BERT ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ๋กœ ์ „ํ™˜ํ•˜๋Š” Tasklet ๊ธฐ๋ฐ˜ Step์ž…๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param tx: ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param switchPort: EmbeddingSwitchPort ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step switchModelStepToKmBert(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo(kmBertEmbeddingAdapterName); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder(modelSwitchStepName + "ToKmBert", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + /** + * KR-SBERT ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ๋กœ ์ „ํ™˜ํ•˜๋Š” Tasklet ๊ธฐ๋ฐ˜ Step์ž…๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param tx: ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param switchPort: EmbeddingSwitchPort ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step switchModelStepToKrSBert(JobRepository jobRepository, + PlatformTransactionManager tx, + EmbeddingSwitchPort switchPort) { + + Tasklet tasklet = (contribution, chunkContext) -> { + switchPort.switchTo(krSBertEmbeddingAdapterName); + return RepeatStatus.FINISHED; + }; + + return new StepBuilder(modelSwitchStepName + "ToKrSbert", jobRepository) + .tasklet(tasklet, tx) + .build(); + } + + /** + * OpenAI ์ž„๋ฒ ๋”ฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” Slave Step์ž…๋‹ˆ๋‹ค. + * chunk ๊ธฐ๋ฐ˜์œผ๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฝ๊ณ  ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param transactionManager: ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param reader: JpaPagingItemReader ์ธ์Šคํ„ด์Šค + * @param processor: ItemProcessor ์ธ์Šคํ„ด์Šค + * @param writer: ItemWriter ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step openAiEmbedStepSlave( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder(openAiStepName + "Slave", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * OpenAI ์ž„๋ฒ ๋”ฉ์„ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” Master Step์ž…๋‹ˆ๋‹ค. + * ๊ฐ Slave Step์„ ํŒŒํ‹ฐ์…”๋‹ํ•˜์—ฌ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param openAiEmbedStepSlave: Slave Step ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step openAiEmbedStep( + JobRepository jobRepository, + Step openAiEmbedStepSlave) { + + return new StepBuilder(openAiStepName, jobRepository) + .partitioner(openAiEmbedStepSlave.getName(), drugIdRangePartitioner) + .step(openAiEmbedStepSlave) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + /** + * KM-BERT ์ž„๋ฒ ๋”ฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” Slave Step์ž…๋‹ˆ๋‹ค. + * chunk ๊ธฐ๋ฐ˜์œผ๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฝ๊ณ  ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param transactionManager: ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param reader: JpaPagingItemReader ์ธ์Šคํ„ด์Šค + * @param processor: ItemProcessor ์ธ์Šคํ„ด์Šค + * @param writer: ItemWriter ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step kmBertEmbedStepSlave( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder(kmBertStepName + "Slave", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * KM-BERT ์ž„๋ฒ ๋”ฉ์„ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” Master Step์ž…๋‹ˆ๋‹ค. + * ๊ฐ Slave Step์„ ํŒŒํ‹ฐ์…”๋‹ํ•˜์—ฌ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param kmBertEmbedStepSlave: Slave Step ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step kmBertEmbedStep( + JobRepository jobRepository, + Step kmBertEmbedStepSlave) { + + return new StepBuilder(kmBertStepName, jobRepository) + .partitioner(kmBertEmbedStepSlave.getName(), drugIdRangePartitioner) + .step(kmBertEmbedStepSlave) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + /** + * KR-SBERT ์ž„๋ฒ ๋”ฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” Slave Step์ž…๋‹ˆ๋‹ค. + * chunk ๊ธฐ๋ฐ˜์œผ๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฝ๊ณ  ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param transactionManager: ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param reader: JpaPagingItemReader ์ธ์Šคํ„ด์Šค + * @param processor: ItemProcessor ์ธ์Šคํ„ด์Šค + * @param writer: ItemWriter ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step krSBertEmbedStepSlave( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + + return new StepBuilder(krSBertStepName + "Slave", jobRepository) + .chunk(100, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * KR-SBERT ์ž„๋ฒ ๋”ฉ์„ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” Master Step์ž…๋‹ˆ๋‹ค. + * ๊ฐ Slave Step์„ ํŒŒํ‹ฐ์…”๋‹ํ•˜์—ฌ ๋ณ‘๋ ฌ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository: JobRepository ์ธ์Šคํ„ด์Šค + * @param krSBertEmbedStepSlave: Slave Step ์ธ์Šคํ„ด์Šค + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step krSBertEmbedStep( + JobRepository jobRepository, + Step krSBertEmbedStepSlave) { + return new StepBuilder(krSBertStepName, jobRepository) + .partitioner(krSBertEmbedStepSlave.getName(), drugIdRangePartitioner) + .step(krSBertEmbedStepSlave) + .taskExecutor(taskExecutor) + .gridSize(10) + .build(); + } + + /** + * ํŒŒํ‹ฐ์…˜ ๋‚ด DrugRawDataEntity๋ฅผ ์ฝ๊ธฐ ์œ„ํ•œ JPA ๊ธฐ๋ฐ˜ ItemReader์ž…๋‹ˆ๋‹ค. + * ๊ฐ ํŒŒํ‹ฐ์…˜์˜ minId์™€ maxId๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์˜ต๋‹ˆ๋‹ค. + * + * @param minId: ํŒŒํ‹ฐ์…˜์˜ ์ตœ์†Œ Drug ID + * @param maxId: ํŒŒํ‹ฐ์…˜์˜ ์ตœ๋Œ€ Drug ID + * @param emf: EntityManagerFactory ์ธ์Šคํ„ด์Šค + * @return JpaPagingItemReader ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + @StepScope + public JpaPagingItemReader embedItemReader( + @Value("#{stepExecutionContext['minId']}") Long minId, + @Value("#{stepExecutionContext['maxId']}") Long maxId, + EntityManagerFactory emf) { + LogUtil.log(Thread.currentThread().getName() + "Item Read: " + minId + "~" + maxId); + return new JpaPagingItemReaderBuilder() + .name("partitionedReader") + .entityManagerFactory(emf) + .queryString(rawDataSelectQuery) + .parameterValues(Map.of("minId", minId, "maxId", maxId)) + .pageSize(100) + .saveState(false) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java new file mode 100644 index 0000000..cdab2c4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/dto/DrugVectorDto.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto; + +import lombok.Builder; +import lombok.Getter; + +/** + * ์˜์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด๋Š” DTO ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * drugId๋Š” ๋Œ€์ƒ ์˜์•ฝํ’ˆ์˜ ์‹๋ณ„์ž์ด๋ฉฐ, vector๋Š” ๋ชจ๋ธ๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑ๋œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ์ž…๋‹ˆ๋‹ค. + * + * @field drugId ์˜์•ฝํ’ˆ์˜ ์‹๋ณ„์ž + * @field vector ๋ชจ๋ธ๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑ๋œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ + * @since 2025-05-02 + */ +@Builder +@Getter +public class DrugVectorDto { + private Long drugId; + private float[] vector; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java new file mode 100644 index 0000000..14fa209 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/processor/EmbedProcessor.java @@ -0,0 +1,62 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.processor; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugFieldTypeMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +/** + * ์˜์•ฝํ’ˆ์˜ ํšจ๋Šฅ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” Spring Batch ItemProcessor์ž…๋‹ˆ๋‹ค. + *

+ * ์ž…๋ ฅ์œผ๋กœ ๋ฐ›์€ DrugRawDataEntity์—์„œ ํšจ๋Šฅ ์ •๋ณด๋ฅผ ์ถ”์ถœ ๋ฐ ์ „์ฒ˜๋ฆฌํ•˜์—ฌ, + * EmbeddingLoadingPort๋ฅผ ํ†ตํ•ด ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•œ ํ›„ DrugVectorDto๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @field embeddingPort EmbeddingPort ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํฌํŠธ + * @since 2025-04-22 + */ +@Component +@RequiredArgsConstructor +public class EmbedProcessor implements ItemProcessor { + private final EmbeddingPort embeddingPort; + + /** + * ํšจ๋Šฅ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ณ , ํ•ด๋‹น ํ…์ŠคํŠธ๋ฅผ ์ž„๋ฒ ๋”ฉํ•˜์—ฌ DrugVectorDto๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param item ์ž„๋ฒ ๋”ฉ ๋Œ€์ƒ ์˜์•ฝํ’ˆ ์—”ํ‹ฐํ‹ฐ + * @return ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ํฌํ•จํ•œ DTO + * @author ์ดํ•ด์ฐฝ + * @modified 2025-05-02: ํ•จ์˜ˆ์ • + * - ์Šคํ”„๋ง ๋ฐฐ์น˜ ์ ์šฉ + * @since 2025-04-22 + */ + @Override + public DrugVectorDto process(DrugRawDataEntity item) { + Long id = item.getDrugId(); + String embeddingText = getEmbedTextFromItem(item); + float[] embeddingVector = embeddingPort.getEmbedding(embeddingText); + return DrugVectorDto + .builder() + .drugId(id) + .vector(embeddingVector) + .build(); + } + + /** + * DrugRawDataEntity์˜ ํšจ๋Šฅ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๊ณ  ์ „์ฒ˜๋ฆฌํ•˜์—ฌ ์ž„๋ฒ ๋”ฉ ์ž…๋ ฅ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param item ์ž„๋ฒ ๋”ฉ ๋Œ€์ƒ ์˜์•ฝํ’ˆ ์—”ํ‹ฐํ‹ฐ + * @return String ์ „์ฒ˜๋ฆฌ๋œ ์ž„๋ฒ ๋”ฉ ์ž…๋ ฅ ํ…์ŠคํŠธ + * @author ์ดํ•ด์ฐฝ + * @modified 2025-05-02: ํ•จ์˜ˆ์ • + * - ์Šคํ”„๋ง ๋ฐฐ์น˜ ์ ์šฉ + * @since 2025-04-22 + */ + private String getEmbedTextFromItem(DrugRawDataEntity item) { + return DrugFieldTypeMapper.convertSingleStringForEfficacy( + DrugFieldTypeMapper.parseStringToList(item.getEfficacy()) + ); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java new file mode 100644 index 0000000..34232de --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/reader/DrugIdRangePartitioner.java @@ -0,0 +1,59 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.reader; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugJpaRepository; + +/** + * ์˜์•ฝํ’ˆ ID ๋ฒ”์œ„๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒํ‹ฐ์…˜์„ ๋ถ„ํ• ํ•˜๋Š” Partitioner ๊ตฌํ˜„ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * GovDrugJpaRepository๋ฅผ ํ†ตํ•ด drugId์˜ ์ตœ์†Œ/์ตœ๋Œ€ ๊ฐ’์„ ์กฐํšŒํ•˜๊ณ , + * ์ฃผ์–ด์ง„ gridSize์— ๋”ฐ๋ผ ๊ฐ ํŒŒํ‹ฐ์…˜์˜ ๋ฒ”์œ„๋ฅผ ๊ณ„์‚ฐํ•˜์—ฌ ExecutionContext์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ Step์€ ์ด ExecutionContext๋ฅผ ํ†ตํ•ด ์ž์‹ ์˜ ID ๋ฒ”์œ„๋ฅผ ์ธ์‹ํ•˜๊ณ  ๋…๋ฆฝ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + */ +@Component +public class DrugIdRangePartitioner implements Partitioner { + + private final DrugJpaRepository repository; + + public DrugIdRangePartitioner(DrugJpaRepository repository) { + this.repository = repository; + } + + /** + * ์˜์•ฝํ’ˆ ID ๋ฒ”์œ„๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฃผ์–ด์ง„ gridSize๋งŒํผ ํŒŒํ‹ฐ์…˜์„ ๋‚˜๋ˆ„์–ด ExecutionContext์— ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param gridSize ๋ณ‘๋ ฌ ์‹คํ–‰ํ•  ํŒŒํ‹ฐ์…˜ ์ˆ˜ + * @return Map ํŒŒํ‹ฐ์…˜ ์ด๋ฆ„(String)๊ณผ ๊ทธ์— ํ•ด๋‹นํ•˜๋Š” ExecutionContext์˜ ๋งคํ•‘ + * @throws IllegalArgumentException gridSize๊ฐ€ 0 ์ดํ•˜์ธ ๊ฒฝ์šฐ + * @throws IllegalStateException drugId์˜ ์ตœ์†Œ/์ตœ๋Œ€ ๊ฐ’์ด null์ธ ๊ฒฝ์šฐ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public Map partition(int gridSize) { + Long minId = repository.findMinDrugId(); + Long maxId = repository.findMaxDrugId(); + + long targetSize = (maxId - minId) / gridSize + 1; + + Map result = new HashMap<>(); + long start = minId; + long end = start + targetSize - 1; + + for (int i = 0; i < gridSize; i++) { + ExecutionContext context = new ExecutionContext(); + context.putLong("minId", start); + context.putLong("maxId", Math.min(end, maxId)); + result.put("partition" + i, context); + start += targetSize; + end += targetSize; + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java new file mode 100644 index 0000000..b817a43 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/batch/step/writer/EmbedWriter.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.writer; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์™ธ๋ถ€ ์ €์žฅ์†Œ ๋˜๋Š” ์ธํ”„๋ผ์— ์ €์žฅํ•˜๋Š” Spring Batch ItemWriter ๊ตฌํ˜„ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * EmbeddingLoadingPort๋ฅผ ํ†ตํ•ด ์ „๋‹ฌ๋œ DrugVectorDto ๋ฆฌ์ŠคํŠธ๋ฅผ ์ €์žฅํ•˜๊ณ , + * ์ฒ˜๋ฆฌ๋œ ํ•ญ๋ชฉ ์ˆ˜๋ฅผ ๋กœ๊น…ํ•ฉ๋‹ˆ๋‹ค. ์ฃผ๋กœ ๋ฒกํ„ฐ ์ €์žฅ API ํ˜ธ์ถœ ๋˜๋Š” ๋ฒกํ„ฐ DB ์—ฐ๋™ ์ž‘์—…์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @fields embeddingPort ์ž„๋ฒ ๋”ฉ ์ €์žฅ์„ ์œ„ํ•œ ํฌํŠธ + * @fields count ์ฒ˜๋ฆฌ๋œ ํ•ญ๋ชฉ ์ˆ˜ ์นด์šดํŠธ๋ฅผ ์œ„ํ•œ AtomicInteger + * @since 2025-05-02 + */ +@Component +@StepScope +@RequiredArgsConstructor +public class EmbedWriter implements ItemWriter { + private final EmbeddingPort embeddingPort; + private static final AtomicInteger count = new AtomicInteger(); + + /** + * ์ „๋‹ฌ๋œ ์ž„๋ฒ ๋”ฉ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ €์žฅ์†Œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param dto ์ž„๋ฒ ๋”ฉ๋œ ๋ฒกํ„ฐ๋ฅผ ํฌํ•จํ•œ DTO ๋ชฉ๋ก (Chunk ๋‹จ์œ„) + * @since 2025-05-02 + * @author ํ•จ์˜ˆ์ • + */ + @Override + public void write(Chunk dto) { + List items = new ArrayList<>(dto.getItems()); + embeddingPort.saveEmbedding(items); + log("์ž„๋ฒ ๋”ฉ ์ž‘์—… - ์“ฐ๊ธฐ ์™„๋ฃŒ: " + count.addAndGet(items.size())); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/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 rows = fetchRawAndEmbedPage(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "ํŽ˜์ด์ง€ ์—์„œ ๋ฐ›์•„์˜จ drug ๊ฐ์ฒด ์ œ์ž‘ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ ์ˆ˜: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ ์—†์Œ"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + List drugs = rows.stream().map(this::combineRawAndEmbed).toList(); + log("loadEmbeddingsByPage - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ"); + + return drugs; + } + + /** + * KM-BERT ์ž„๋ฒ ๋”ฉ API ํ˜ธ์ถœ์„ ์œ„ํ•œ URI๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Embedding API ํ˜ธ์ถœ์— ์‚ฌ์šฉํ•  URI + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + */ + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKmbertEmbeding(); + } + + /** + * ์ง€์ •๋œ URI์— ํ…์ŠคํŠธ๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ์„ ์ „์†กํ•˜์—ฌ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค. + * + * @param embedUri ์ž„๋ฒ ๋”ฉ API ํ˜ธ์ถœ ๋Œ€์ƒ URI + * @param text ์ž„๋ฒ ๋”ฉํ•  ์›๋ณธ ํ…์ŠคํŠธ + * @return API ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (float ๋ฐฐ์—ด) + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + */ + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + /** + * Pageable ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ Pageable ๊ฐ์ฒด + * @return Object[] ๋ฐฐ์—ด ๋ฆฌ์ŠคํŠธ; [0]์—๋Š” DrugRawDataEntity, [1]์—๋Š” DrugKmBertEmbedEntity + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private List fetchRawAndEmbedPage(Pageable pageable) { + return govKmBertEmbedJpaRepository.findRawAndEmbed(pageable); + } + + /** + * Object ๋ฐฐ์—ด๋กœ ์ „๋‹ฌ๋œ ์›์‹œ ๋ฐ์ดํ„ฐ ์—”ํ‹ฐํ‹ฐ์™€ ์ž„๋ฒ ๋”ฉ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ + * Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pair Object ๋ฐฐ์—ด; index 0์€ DrugRawDataEntity, index 1์€ DrugKmBertEmbedEntity + * @return ๊ฒฐํ•ฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private Drug combineRawAndEmbed(Object[] pair) { + DrugRawDataEntity raw = (DrugRawDataEntity) pair[0]; + DrugKmBertEmbedEntity embed = (DrugKmBertEmbedEntity) pair[1]; + return DrugEntityMapper.toDomainFromEntity(raw, embed); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java new file mode 100644 index 0000000..309934b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/KrSBertEmbeddingAdapter.java @@ -0,0 +1,150 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.adapter; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKrSBertEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.UriCompBuilder; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.EmbeddingRequestText; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository.KrSBertEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.DrugEntityMapper; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.EmbedEntityBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Repository; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Repository +@RequiredArgsConstructor +public class KrSBertEmbeddingAdapter implements EmbeddingPort { + private final KrSBertEmbedJpaRepository krSBertEmbedJpaRepository; + private final UriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + /** + * ์ฃผ์–ด์ง„ ํ…์ŠคํŠธ์— ๋Œ€ํ•ด ์™ธ๋ถ€ KR-SBERT ์ž„๋ฒ ๋”ฉ API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ž„๋ฒ ๋”ฉ์„ ์ƒ์„ฑํ•  ์›๋ณธ ํ…์ŠคํŠธ + * @return ์ƒ์„ฑ๋œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (float ๋ฐฐ์—ด) + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + */ + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + /** + * ์ „๋‹ฌ๋œ DrugVectorDto ๋ฆฌ์ŠคํŠธ๋ฅผ DrugKrSBertEmbedEntity๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ + * JPA ์ €์žฅ์†Œ์— ์ผ๊ด„ ์ €์žฅํ•˜๊ณ  ์ฆ‰์‹œ ํ”Œ๋Ÿฌ์‹œ(flush)ํ•ฉ๋‹ˆ๋‹ค. + * + * @param dtos ์ €์žฅํ•  ์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด DTO ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + * @modified + * 2025-05-02 - ๋ฐฐ์น˜ ์ ์šฉ์„ ์œ„ํ•œ ์ž…๋ ฅ๊ฐ’ ๋ณ€๊ฒฝ + */ + @Override + public void saveEmbedding(List dtos) { + krSBertEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugKrSBertEmbedEntity.class)) + .toList() + ); + krSBertEmbedJpaRepository.flush(); + } + + /** + * Pageable ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ KR-SBERT ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜์—ฌ + * ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” Pageable ๊ฐ์ฒด + * @return ์กฐํšŒ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws IndexException ์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rows = fetchRawAndEmbedPage(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "ํŽ˜์ด์ง€ ์—์„œ ๋ฐ›์•„์˜จ drug ๊ฐ์ฒด ์ œ์ž‘ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ ์ˆ˜: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ ์—†์Œ"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + List drugs = rows.stream().map(this::combineRawAndEmbed).toList(); + log("loadEmbeddingsByPage - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ"); + return drugs; + } + + /** + * KR-SBERT ์ž„๋ฒ ๋”ฉ API ํ˜ธ์ถœ์— ์‚ฌ์šฉํ•  URI๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Embedding API ํ˜ธ์ถœ์šฉ URI + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + */ + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKrSbertEmbeding(); + } + + /** + * ์ง€์ •๋œ URI์— ํ…์ŠคํŠธ๋ฅผ ํฌํ•จํ•œ HTTP ์š”์ฒญ์„ ์ „์†กํ•˜์—ฌ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค. + * + * @param embedUri ์ž„๋ฒ ๋”ฉ API ํ˜ธ์ถœ ๋Œ€์ƒ URI + * @param text ์ž„๋ฒ ๋”ฉํ•  ์›๋ณธ ํ…์ŠคํŠธ + * @return API ์‘๋‹ต์œผ๋กœ ๋ฐ›์€ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (float ๋ฐฐ์—ด) + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + */ + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } + + /** + * Pageable ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” Pageable ๊ฐ์ฒด + * @return Object[] ๋ฐฐ์—ด ๋ฆฌ์ŠคํŠธ; index 0์€ DrugRawDataEntity, index 1์€ DrugKrSBertEmbedEntity + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private List fetchRawAndEmbedPage(Pageable pageable) { + return krSBertEmbedJpaRepository.findRawAndEmbed(pageable); + } + + /** + * Object ๋ฐฐ์—ด๋กœ ์ „๋‹ฌ๋œ ์›์‹œ ๋ฐ์ดํ„ฐ ์—”ํ‹ฐํ‹ฐ์™€ ์ž„๋ฒ ๋”ฉ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ + * Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pair Object ๋ฐฐ์—ด; index 0์€ DrugRawDataEntity, index 1์€ DrugKrSBertEmbedEntity + * @return ๊ฒฐํ•ฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private Drug combineRawAndEmbed(Object[] pair) { + DrugRawDataEntity raw = (DrugRawDataEntity) pair[0]; + DrugKrSBertEmbedEntity embed = (DrugKrSBertEmbedEntity) pair[1]; + return DrugEntityMapper.toDomainFromEntity(raw, embed); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java new file mode 100644 index 0000000..418b4d7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/adapter/OpenAiEmbeddingAdapter.java @@ -0,0 +1,144 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.adapter; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugGptEmbedEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository.OpenAiEmbedJpaRepository; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.DrugEntityMapper; +import com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil.EmbedEntityBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +@Repository +@RequiredArgsConstructor +public class OpenAiEmbeddingAdapter implements EmbeddingPort { + private final OpenAiEmbedJpaRepository govDrugGptEmbedJpaRepository; + private final OpenAiApi openAiApi; + private static final String EMBEDDING_MODEL_NAME = "text-embedding-3-small"; // yml์—์„œ ๋ฐ›์•„์˜ค๊ธฐ + + + /** + * ์ฃผ์–ด์ง„ ํ…์ŠคํŠธ๋ฅผ OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์— ์ „๋‹ฌํ•˜์—ฌ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ž„๋ฒ ๋”ฉ์„ ์ƒ์„ฑํ•  ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return OpenAI ๋ชจ๋ธ์ด ์ƒ์„ฑํ•œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (float ๋ฐฐ์—ด) + * @author ์ดํ•ด์ฐฝ + * @since 2025-04-25 + * @modified + * 25.05.03 - ๊ฐ€๋…์„ฑ ๊ฐœ์„ ์„ ์œ„ํ•œ ๋ฆฌํŒฉํ† ๋ง + */ + @Override + public float[] getEmbedding(String text) { + OpenAiEmbeddingModel openAiEmbeddingModel = getOpenAiEmbeddingModel(); + EmbeddingResponse embeddingResponse = openAiEmbeddingModel.embedForResponse(List.of(text)); + return embeddingResponse.getResults().getFirst().getOutput(); + } + + /** + * ์ „๋‹ฌ๋œ DrugVectorDto ๋ฆฌ์ŠคํŠธ๋ฅผ DrugGptEmbedEntity๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ + * JPA ์ €์žฅ์†Œ์— ์ผ๊ด„ ์ €์žฅํ•˜๊ณ  ์ฆ‰์‹œ ํ”Œ๋Ÿฌ์‹œ(flush)ํ•ฉ๋‹ˆ๋‹ค. + * + *

๊ฐ DTO๋Š” EmbedEntityBuilder.buildEmbedEntity๋ฅผ ํ†ตํ•ด + * DrugGptEmbedEntity ํƒ€์ž…์˜ ์ž„๋ฒ ๋”ฉ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

+ * + * @param dtos ์ €์žฅํ•  ์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด DTO ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + */ + @Override + public void saveEmbedding(List dtos) { + govDrugGptEmbedJpaRepository.saveAll( + dtos.stream() + .map(dto -> EmbedEntityBuilder.buildEmbedEntity(dto, DrugGptEmbedEntity.class)) + .toList() + ); + govDrugGptEmbedJpaRepository.flush(); + } + + /** + * Pageable ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌํ•˜์—ฌ ์กฐํšŒํ•˜๊ณ , + * Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + *

์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ IndexException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

+ * + * @param pageable ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” Pageable ๊ฐ์ฒด + * @return ์กฐํšŒ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws IndexException ์›์‹œ ๋ฐ์ดํ„ฐ ๋ฐ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜์ง€ ๋ชปํ–ˆ๊ฑฐ๋‚˜ ์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + @Override + public List loadEmbeddingsByPage(Pageable pageable) { + List rows = fetchRawAndEmbedPage(pageable); + log("loadEmbeddingsByPage - " + pageable.getPageNumber() + "ํŽ˜์ด์ง€ ์—์„œ ๋ฐ›์•„์˜จ drug ๊ฐ์ฒด ์ œ์ž‘ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ ์ˆ˜: " + rows.size()); + if (rows.isEmpty()) { + log(LogLevel.ERROR, "loadEmbeddingsByPage - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ ์—†์Œ"); + throw new IndexException(IndexErrorCode.RAW_DATA_FETCH_ERROR); + } + List drugs = rows.stream().map(this::combineRawAndEmbed).toList(); + log("loadEmbeddingsByPage - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ์ƒ์„ฑ ์™„๋ฃŒ"); + return drugs; + } + + /** + * OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์„ EMBED ๋ชจ๋“œ๋กœ ์ดˆ๊ธฐํ™”ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return EMBED ๋ชจ๋“œ๋กœ ๊ตฌ์„ฑ๋œ OpenAiEmbeddingModel ์ธ์Šคํ„ด์Šค + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private OpenAiEmbeddingModel getOpenAiEmbeddingModel() { + OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel( + this.openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model(EMBEDDING_MODEL_NAME) + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE); + return openAiEmbeddingModel; + } + + /** + * Pageable ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ Pageable ๊ฐ์ฒด + * @return Object[] ๋ฐฐ์—ด ๋ฆฌ์ŠคํŠธ; [0]์—๋Š” DrugRawDataEntity, [1]์—๋Š” DrugGptEmbedEntity + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private List fetchRawAndEmbedPage(Pageable pageable) { + return govDrugGptEmbedJpaRepository.findRawAndEmbed(pageable); + } + + /** + * Object ๋ฐฐ์—ด๋กœ ์ „๋‹ฌ๋œ ์›์‹œ ๋ฐ์ดํ„ฐ ์—”ํ‹ฐํ‹ฐ์™€ ์ž„๋ฒ ๋”ฉ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ + * Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pair Object ๋ฐฐ์—ด; index 0์€ DrugRawDataEntity, index 1์€ DrugGptEmbedEntity + * @return ๊ฒฐํ•ฉ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private Drug combineRawAndEmbed(Object[] pair) { + DrugRawDataEntity raw = (DrugRawDataEntity) pair[0]; + DrugGptEmbedEntity embed = (DrugGptEmbedEntity) pair[1]; + return DrugEntityMapper.toDomainFromEntity(raw, embed); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java new file mode 100644 index 0000000..bd65dcf --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KmBertEmbedJpaRepository.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKmBertEmbedEntity; + +import java.util.List; + +@Repository +public interface KmBertEmbedJpaRepository extends JpaRepository { + String QUERY = """ + SELECT r, e + FROM DrugKmBertEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.kmBertVector IS NOT NULL + AND r.isHerbal is FALSE + """; + @Query(QUERY) + List findRawAndEmbed(Pageable pageable); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java new file mode 100644 index 0000000..1d405b6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/KrSBertEmbedJpaRepository.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugKrSBertEmbedEntity; + +@Repository +public interface KrSBertEmbedJpaRepository extends JpaRepository { + String QUERY = """ + SELECT r, e + FROM DrugKrSBertEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.krSBertVector IS NOT NULL + AND r.isHerbal is FALSE + """; + + @Query(QUERY) + List findRawAndEmbed(Pageable pageable); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java new file mode 100644 index 0000000..14069aa --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/infrastructure/persistence/jpa/repository/OpenAiEmbedJpaRepository.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.jpa.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.DrugGptEmbedEntity; + +import java.util.List; + +@Repository +public interface OpenAiEmbedJpaRepository extends JpaRepository { + String QUERY = """ + SELECT r, e + FROM DrugGptEmbedEntity e + JOIN DrugRawDataEntity r + ON r.drugId = e.drugId + WHERE e.gptVector IS NOT NULL + AND r.isHerbal is FALSE + """; + @Query(QUERY) + List findRawAndEmbed(Pageable pageable); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/DrugEmbedController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/DrugEmbedController.java new file mode 100644 index 0000000..5102901 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/DrugEmbedController.java @@ -0,0 +1,71 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation; + +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; + +import com.likelion.backendplus4.yakplus.drug.embed.presentation.docs.DrugEmbedControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.embed.application.port.in.DrugEmbedProcessorUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * ์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ์ž‘์—…์„ ์ œ์–ดํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค. + * ๋ฒกํ„ฐ ์ƒ์„ฑ ์‹œ์ž‘, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/job/embed") +@RequiredArgsConstructor +public class DrugEmbedController implements DrugEmbedControllerDocs { + private final DrugEmbedProcessorUseCase drugEmbedProcessorUseCase; + + /** + * ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @PostMapping("/start") + public ResponseEntity> start() { + return success(drugEmbedProcessorUseCase.startEmbedding()); + } + + /** + * ์ž„๋ฒ ๋”ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugEmbedProcessorUseCase.stopEmbedding()); + } + + /** + * ์ž„๋ฒ ๋”ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @GetMapping("/status") + public ResponseEntity> status() { + return success(drugEmbedProcessorUseCase.statusEmbedding()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java new file mode 100644 index 0000000..d1c7fc1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/EmbeddingRouterController.java @@ -0,0 +1,53 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.in.EmbeddingRoutingUseCase; +import com.likelion.backendplus4.yakplus.drug.embed.presentation.docs.EmbeddingRouterControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * Embedding ๋ผ์šฐํŒ… ์–ด๋Œ‘ํ„ฐ๋ฅผ ์ „ํ™˜ํ•˜๊ณ  ์กฐํšŒํ•˜๋Š” REST ์ปจํŠธ๋กค๋Ÿฌ + * + * ์š”์ฒญ์— ๋”ฐ๋ผ ํ™œ์„ฑํ™”๋œ Embedding adapter๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ํ˜„์žฌ adapter๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/switch/embeddings") +public class EmbeddingRouterController implements EmbeddingRouterControllerDocs { + private final EmbeddingRoutingUseCase routerUseCase; + + public EmbeddingRouterController(EmbeddingRoutingUseCase routerUseCase) { + this.routerUseCase = routerUseCase; + } + + /** + * ์ง€์ •๋œ adapterBeanName์— ํ•ด๋‹นํ•˜๋Š” embedding adapter๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param adapterBeanName ์ „ํ™˜ํ•  adapter Bean ์ด๋ฆ„ + * @return ์–ด๋Œ‘ํ„ฐ ๋ณ€๊ฒฝ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ด์€ ApiResponse + * @author ์ •์•ˆ์‹ + * @since 2025-05-02 + */ + @PostMapping("/switch/{adapterBeanName}") + public ResponseEntity> switchAdapter(@PathVariable String adapterBeanName) { + log("์Šค์œ„์น˜ ๋Œ€์ƒ ์ธ๋ฑ์Šค๋ช… : " + adapterBeanName); + routerUseCase.switchEmbedding(adapterBeanName); + return ApiResponse.success("์–ด๋Œ‘ํ„ฐ ๋ณ€๊ฒฝ๋จ - ์–ด๋Œ‘ํ„ฐ๋ช…: " + adapterBeanName); + } + + /** + * ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ embedding adapter Bean ์ด๋ฆ„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ˜„์žฌ adapter Bean ์ด๋ฆ„์„ ๋‹ด์€ ApiResponse + * @author ์ •์•ˆ์‹ + * @since 2025-05-02 + */ + @GetMapping("/current/adapter") + public ResponseEntity> checkCurrentAdapter() { + return ApiResponse.success(routerUseCase.getAdapterBeanName()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java new file mode 100644 index 0000000..f5518bc --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/DrugEmbedControllerDocs.java @@ -0,0 +1,26 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation.docs; + +import org.springframework.http.ResponseEntity; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * ์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ์ž‘์—… API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Embed", description = "์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ์ž‘์—… API") +public interface DrugEmbedControllerDocs { + + @Operation(summary = "์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ์ž‘์—… ์‹œ์ž‘", description = "์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> start(); + + @Operation(summary = "์ž„๋ฒ ๋”ฉ ์ž‘์—… ์ค‘์ง€", description = "์ง„ํ–‰ ์ค‘์ธ ์ž„๋ฒ ๋”ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> stop(); + + @Operation(summary = "์ž„๋ฒ ๋”ฉ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ", description = "ํ˜„์žฌ ์ž„๋ฒ ๋”ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> status(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java new file mode 100644 index 0000000..6b2156b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/embed/presentation/docs/EmbeddingRouterControllerDocs.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.embed.presentation.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * Embedding ๋ผ์šฐํŒ… ์–ด๋Œ‘ํ„ฐ API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Embedding Router", description = "Embedding ๋ผ์šฐํŒ… ์–ด๋Œ‘ํ„ฐ ์ „ํ™˜ ๋ฐ ์กฐํšŒ API") +public interface EmbeddingRouterControllerDocs { + + @Operation( + summary = "Embedding adapter ์ „ํ™˜", + description = "์ง€์ •๋œ adapterBeanName์— ํ•ด๋‹นํ•˜๋Š” embedding adapter๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> switchAdapter( + @Parameter( + name = "adapterBeanName", + description = "์ „ํ™˜ํ•  adapter Bean ์ด๋ฆ„", + in = ParameterIn.PATH, + required = true + ) + String adapterBeanName + ); + + @Operation( + summary = "ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ adapter ์กฐํšŒ", + description = "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ embedding adapter Bean ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> checkCurrentAdapter(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java new file mode 100644 index 0000000..bc30806 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/in/IndexUseCase.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.in; + +public interface IndexUseCase { + + /** + * ์š”์ฒญ์œผ๋กœ ์ „๋‹ฌ๋œ lastSeq, limit ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ RDB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  + * ES ์ธ๋ฑ์Šค์— ์ €์žฅํ•œ๋‹ค. + * + * @author ์ •์•ˆ์‹ + * @modified 2025-05-02 ์ดํ•ด์ฐฝ + * 25.05.02 - ์ €์žฅ๋œ ์•ฝ๋ฌผ ์ƒ์„ธ์ •๋ณด ๋ฐ์ดํ„ฐ ํฌ๊ธฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ES์— ์ƒ‰์ธํ•˜๋Š” loop๋ฅผ ๋งŒ๋“ค๋„๋ก ์ˆ˜์ • + * 25.04.28 - IndexRequest๋ฅผ ์ธ์ž๋กœ ๋” ์ด์ƒ ๋ฐ›์ง€ ์•Š๋„๋ก ์ˆ˜์ • + * 25.04.27 - esIndexname์„ ์ธ์ž๋กœ ๋ฐ›์•„ saveAll ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + void index(); + + /** + * DB์—์„œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง•์œผ๋กœ ๊ฐ€์ ธ์™€ Elasticsearch์— ์ผ๊ด„ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ ํŽ˜์ด์ง€๋Š” CHUNK_SIZE๋งŒํผ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-25 + * @since 2025-04-24 + */ + void indexKeyword(); + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java new file mode 100644 index 0000000..d9b195b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugIndexRepositoryPort.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import org.springframework.data.domain.Page; + +public interface DrugIndexRepositoryPort { + + /** + * ์ฃผ์–ด์ง„ Drug ๋ชฉ๋ก์„ Elasticsearch์— Bulk API๋กœ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param esIndexName Bulk ๋Œ€์ƒ ES ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @param drugs ์ €์žฅํ•  Drug ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws IndexException ์ƒ‰์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-27 + * 25.04.27 - esIndexname์„ ์ธ์ž๋กœ ๋ฐ›์•„ Bulk API๋กœ ์ผ๊ด„ ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + void saveAll(String esIndexName, List drugs); + + /** + * ์•ฝํ’ˆ ๋„๋ฉ”์ธ ๊ฐ์ฒด ํŽ˜์ด์ง€๋ฅผ Elasticsearch ๋ฌธ์„œ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * 1) Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด โ†’ DrugKeywordDocument๋กœ ๋ณ€ํ™˜ + * 2) Elasticsearch์— saveAll๋กœ ์ผ๊ด„ ์ƒ‰์ธ + * + * @param drugPage ์ƒ‰์ธํ•  ์•ฝํ’ˆ ๋„๋ฉ”์ธ ํŽ˜์ด์ง€ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + void saveAllKeyword(Page drugPage); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java new file mode 100644 index 0000000..9b42115 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/DrugRawDataPort.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.out; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface DrugRawDataPort { + List fetchRawData(int pageNo, int numOfRows); + + + /** + * ์ฃผ์–ด์ง„ Pageable ์ •๋ณด์— ๋”ฐ๋ผ DB์—์„œ ํ•œ ํŽ˜์ด์ง€ ๋ถ„๋Ÿ‰์˜ GovDrugEntity๋ฅผ ์กฐํšŒํ•˜๊ณ , + * ๊ฐ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ(GovDrug)๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ Page ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์™€ ํฌ๊ธฐ๋ฅผ ํฌํ•จํ•˜๋Š” Pageable ๊ฐ์ฒด + * @return ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋ณ€ํ™˜๋œ GovDrug ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ Page + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified 2025-04-25 + */ + Page findAllDrugs(Pageable pageable); + + /** + * JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ด์šฉํ•ด GovDrugJpaRepository์˜ ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return GovDrugJpaRepository์˜ ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜ + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-02 + * @modified + */ + long getDrugTotalSize(); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java new file mode 100644 index 0000000..eca2a94 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/port/out/EmbeddingPort.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.port.out; + +import java.util.List; + +import org.springframework.data.domain.Pageable; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; + +public interface EmbeddingPort { + /** + * ์ฃผ์–ด์ง„ ํŽ˜์ด์ง€ ์ •๋ณด(Pageable)์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ + * ์กฐ์ธํ•˜์—ฌ ํ•œ ํŽ˜์ด์ง€ ๋ถ„๋Ÿ‰์˜ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์™€ ํฌ๊ธฐ, ์ •๋ ฌ ์ •๋ณด ๋“ฑ์„ ํฌํ•จํ•˜๋Š” Pageable ๊ฐ์ฒด + * @return ์ง€์ •๋œ ํŽ˜์ด์ง€ ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค์˜ ๋ฆฌ์ŠคํŠธ + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + List loadEmbeddingsByPage(Pageable pageable); + + /** + * ์ฃผ์–ด์ง„ ํ…์ŠคํŠธ์— ๋Œ€ํ•ด OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์„ ํ˜ธ์ถœํ•˜์—ฌ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ž„๋ฒ ๋”ฉ์„ ์ƒ์„ฑํ•  ์›๋ณธ ํ…์ŠคํŠธ + * @return ์ƒ์„ฑ๋œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (float ๋ฐฐ์—ด) + * @author ์ดํ•ด์ฐฝ + * @since 2025-04-25 + * @modified + * 2025-04-30 - ์ž„๋ฒ ๋”ฉ ์ €์žฅ/๋กœ๋”ฉํฌํŠธ ํ†ตํ•ฉ์œผ๋กœ ์ธํ•œ ์œ„์น˜ ์ด๋™ + */ + float[] getEmbedding(String text); + + /** + * ์ „๋‹ฌ๋œ DrugVectorDto ๋ฆฌ์ŠคํŠธ์— ํฌํ•จ๋œ ์•ฝํ’ˆ ID์™€ ํ•ด๋‹น ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param dtos ์ €์žฅํ•  ์•ฝํ’ˆ ์ž„๋ฒ ๋”ฉ ์ •๋ณด ๋ฆฌ์ŠคํŠธ (DrugVectorDto ๊ฐ์ฒด) + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-25 + * @modified + * 2025-05-02 - ๋ฐฐ์น˜ ์ ์šฉ์œผ๋กœ ์ธํ•œ ์ž…๋ ฅ ํƒ€์ž… ๋ณ€๊ฒฝ + * 2025-04-30 - ์ž„๋ฒ ๋”ฉ ์ €์žฅ/๋กœ๋”ฉํฌํŠธ ํ†ตํ•ฉ์œผ๋กœ ์ธํ•œ ์œ„์น˜ ์ด๋™ + */ + void saveEmbedding(List dtos); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java new file mode 100644 index 0000000..660534c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/application/service/DrugIndexer.java @@ -0,0 +1,157 @@ +package com.likelion.backendplus4.yakplus.drug.index.application.service; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.index.application.port.in.IndexUseCase; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugRawDataPort; +import com.likelion.backendplus4.yakplus.drug.embed.application.port.out.EmbeddingSwitchPort; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * ์•ฝํ’ˆ ์ƒ‰์ธ(์ธ๋ฑ์‹ฑ) ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์„œ๋น„์Šค ๊ตฌํ˜„์ฒด + * + * @modified 2025-04-28 + * 25.04.27 - esIndexname์„ ์ธ์ž๋กœ ๋ฐ›์•„ saveAll ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ • + * - itemSeq -> drugId๋กœ ์ˆ˜์ • + * - ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ ๋กœ์ง ์ˆ˜์ • + * @since 2025-04-22 + */ +@Service +@RequiredArgsConstructor +public class DrugIndexer implements IndexUseCase { + private final DrugRawDataPort drugRawDataPort; + private final DrugIndexRepositoryPort drugIndexRepositoryPort; + private final EmbeddingSwitchPort embeddingSwitchPort; + + private static final String INDENT = " "; + private static final int CHUNK_SIZE = 100; + + /** + * ์š”์ฒญ์œผ๋กœ ์ „๋‹ฌ๋œ lastSeq, limit ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ RDB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  + * ES ์ธ๋ฑ์Šค์— ์ €์žฅํ•œ๋‹ค. + * + * @author ์ •์•ˆ์‹ + * @modified + * 25.05.02 - ์ดํ•ด์ฐฝ: ์ €์žฅ๋œ ์•ฝ๋ฌผ ์ƒ์„ธ์ •๋ณด ๋ฐ์ดํ„ฐ ํฌ๊ธฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ES์— ์ƒ‰์ธํ•˜๋Š” loop๋ฅผ ๋งŒ๋“ค๋„๋ก ์ˆ˜์ • + * 25.04.28 - IndexRequest๋ฅผ ์ธ์ž๋กœ ๋” ์ด์ƒ ๋ฐ›์ง€ ์•Š๋„๋ก ์ˆ˜์ • + * 25.04.27 - esIndexname์„ ์ธ์ž๋กœ ๋ฐ›์•„ saveAll ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + @Override + public void index() { + log("index ์„œ๋น„์Šค ์š”์ฒญ ์ˆ˜์‹ "); + + String esIndexName = getEsIndexName(); + long totalDataSize = drugRawDataPort.getDrugTotalSize(); + int totalPages = getTotalPages(totalDataSize); + + for(int currentPage = 0; currentPage < totalPages; currentPage++) { + log("์ƒ‰์ธ ์‹œ์ž‘: page=" + currentPage); + + List drugs = fetchRawData(currentPage, CHUNK_SIZE); + log(INDENT+"์กฐํšŒ ์™„๋ฃŒ: page=" + currentPage + ", ๊ฑด์ˆ˜=" + drugs.size()); + + saveDrugs(esIndexName, drugs); + log("์ƒ‰์ธ ์™„๋ฃŒ: page=" + currentPage + ", ๊ฑด์ˆ˜=" + drugs.size()); + } + } + + /** + * DB์—์„œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง•์œผ๋กœ ๊ฐ€์ ธ์™€ Elasticsearch์— ์ผ๊ด„ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ ํŽ˜์ด์ง€๋Š” CHUNK_SIZE๋งŒํผ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-25 + * @since 2025-04-24 + */ + @Override + public void indexKeyword() { + log("indexKeyword ์š”์ฒญ ์ˆ˜์‹ "); + int page = 0; + Page drugPage; + + do { + log("์ƒ‰์ธ ์‹œ์ž‘: page=" + page); + + // 1. ํŽ˜์ด์ง•์œผ๋กœ DB์—์„œ ํ•œ ์ฒญํฌ ๊ฐ€์ ธ์˜ค๊ธฐ + drugPage = drugRawDataPort.findAllDrugs(PageRequest.of(page, CHUNK_SIZE)); + log(INDENT+"์กฐํšŒ ์™„๋ฃŒ: page=" + page + ", ๊ฑด์ˆ˜=" + drugPage.getNumberOfElements()); + + // 2. ์ฒญํฌ๋ณ„ ES์— ์ƒ‰์ธ + drugIndexRepositoryPort.saveAllKeyword(drugPage); + log(INDENT+"์ƒ‰์ธ ์™„๋ฃŒ: page=" + page + ", ๊ฑด์ˆ˜=" + drugPage.getNumberOfElements()); + + // 3. ๋‹ค์Œ 1000๊ฐœ ๊ฐ’ ๋ฃจํ”„ + page++; + } while (drugPage.hasNext()); + log("indexSymptom ์ „์ฒด ์ฒ˜๋ฆฌ ์™„๋ฃŒ"); + } + + /** + * Elasticsearch ์ธ๋ฑ์Šค ์ด๋ฆ„์„ ์กฐํšŒํ•œ๋‹ค. + * + * @return Elasticsearch ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @author ์ •์•ˆ์‹ + * @since 2025-04-27 + * @modified + * 2025-05-02 - ์ดํ•ด์ฐฝ: ํ•˜๋“œ์ฝ”๋”ฉ ๋œ ๋ฌธ์ž๋ฅผ ๋ฐ›์–ด์˜ค๋˜ ๊ฒƒ์„ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ BeanName์„ ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ • + */ + private String getEsIndexName() { + log("ES ์ธ๋ฑ์Šค ์ด๋ฆ„ ์กฐํšŒ"); + return embeddingSwitchPort.getAdapterBeanName(); + } + + /** + * ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ CHUNK_SIZE ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„์–ด ํ•„์š”ํ•œ ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + *

์˜ˆ๋ฅผ ๋“ค์–ด, ์ „์ฒด ๋ฐ์ดํ„ฐ๊ฐ€ 25๊ฐœ์ด๊ณ  CHUNK_SIZE๊ฐ€ 10์ด๋ผ๋ฉด + * (25 + 10 - 1) / 10 = 3ํŽ˜์ด์ง€๊ฐ€ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.

+ * + * @param totalDataSize ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ + * @return ํ•„์š”ํ•œ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ (๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์— ๋‚จ๋Š” ๋ฐ์ดํ„ฐ๋„ ํ•œ ํŽ˜์ด์ง€๋กœ ์ฒ˜๋ฆฌ) + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + private static int getTotalPages(long totalDataSize) { + int totalPages = (int) ((totalDataSize + CHUNK_SIZE - 1) / CHUNK_SIZE); + return totalPages; + } + + /** + * RDB์—์„œ lastSeq ์ดํ›„์˜ ์›์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜์—ฌ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. + * + * @param pageNum ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @param numOfRows ํ•œ ํŽ˜์ด์ง€๋‹น ์กฐํšŒํ•  ๊ฑด์ˆ˜ + * @return ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ + * @author ์ •์•ˆ์‹ + * @modified + * 25.05.02 - ์ดํ•ด์ฐฝ: ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ ์‹œ ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ ๋ฐ›๋„๋ก ์ˆ˜์ •
+ * 25.04.28 - ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ ๋กœ์ง ์ˆ˜์ • + * @since 2025-04-22 + */ + private List fetchRawData(int pageNum, int numOfRows) { + log("RDB์—์„œ ์›์‹œ ๋ฐ์ดํ„ฐ ์กฐํšŒ"); + return drugRawDataPort.fetchRawData(pageNum, numOfRows); + } + + /** + * ์กฐํšŒ๋œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค์„ ES ์ธ๋ฑ์Šค์— ์ €์žฅ ์ฒ˜๋ฆฌํ•œ๋‹ค. + * + * @param drugs ์ €์žฅํ•  ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-27 + * 25.04.27 - esIndexName์„ ์ธ์ž๋กœ ๋ฐ›๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + private void saveDrugs(String esIndexName, List drugs) { + log("ES ์ธ๋ฑ์Šค์— ์ €์žฅ"); + drugIndexRepositoryPort.saveAll(esIndexName, drugs); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/IndexException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/IndexException.java new file mode 100644 index 0000000..e90072e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/IndexException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.index.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class IndexException extends CustomException { + private final ErrorCode errorCode; + + public IndexException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/error/IndexErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/error/IndexErrorCode.java new file mode 100644 index 0000000..dc51528 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/exception/error/IndexErrorCode.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.drug.index.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum IndexErrorCode implements ErrorCode { + RAW_DATA_FETCH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430001, "์›์‹œ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ"), + ES_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430002, "Elasticsearch ์ €์žฅ ์‹คํŒจ"), + EMBEDDING_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 440001, "์ž„๋ฒ ๋”ฉ API ํ˜ธ์ถœ ์‹คํŒจ"); + + private final HttpStatus status; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return status; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/adapter/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/adapter/ElasticsearchDrugAdapter.java new file mode 100644 index 0000000..12eff3a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/adapter/ElasticsearchDrugAdapter.java @@ -0,0 +1,155 @@ +package com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.adapter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugIndexRepositoryPort; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.index.exception.IndexException; +import com.likelion.backendplus4.yakplus.drug.index.exception.error.IndexErrorCode; + +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.repository.DrugKeywordRepository; +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document.DrugKeywordDocument; +import com.likelion.backendplus4.yakplus.drug.index.support.mapper.KeywordMapper; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Request; + +import org.elasticsearch.client.RestClient; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * Elasticsearch๋ฅผ ํ†ตํ•ด Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ƒ‰์ธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * DrugIndexRepositoryPort๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ + * Elasticsearch ์›๊ฒฉ ํ˜ธ์ถœ์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @modified 2025-04-27 + * 25.04.27 - saveAll()๋ฅผ Bulk ์š”์ฒญ์œผ๋กœ ์ „ํ™˜ + * - buildBulkRequestBody(), createBulkRequest() ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ + * @since 2025-04-22 + */ +@Component +public class ElasticsearchDrugAdapter implements DrugIndexRepositoryPort { + private final DrugKeywordRepository keywordRepository; + private final RestClient restClient; + private final ObjectMapper objectMapper; + + public ElasticsearchDrugAdapter(DrugKeywordRepository drugKeywordRepository, RestClient restClient, ObjectMapper objectMapper) { + this.keywordRepository = drugKeywordRepository; + this.restClient = restClient; + this.objectMapper = objectMapper; + } + + /** + * ์ฃผ์–ด์ง„ Drug ๋ชฉ๋ก์„ Elasticsearch์— Bulk API๋กœ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param esIndexName Bulk ๋Œ€์ƒ ES ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @param drugs ์ €์žฅํ•  Drug ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws IndexException ์ƒ‰์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-27 + * 25.04.27 - esIndexname์„ ์ธ์ž๋กœ ๋ฐ›์•„ Bulk API๋กœ ์ผ๊ด„ ์ €์žฅํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + @Override + public void saveAll(String esIndexName, List drugs) { + log("saveAll() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ์ธ๋ฑ์Šค ์ด๋ฆ„: " + esIndexName + ", Drug ๊ฐœ์ˆ˜: " + drugs.size()); + try { + String bulkBody = buildBulkRequestBody(esIndexName, drugs); + Request bulkRequest = createBulkRequest(esIndexName, bulkBody); + restClient.performRequest(bulkRequest); + log("saveAll() ๋ฉ”์„œ๋“œ ์™„๋ฃŒ, ์ธ๋ฑ์Šค ์ด๋ฆ„: " + esIndexName + ", Drug ๊ฐœ์ˆ˜: " + drugs.size()); + } catch (Exception e) { + log(LogLevel.ERROR, "Elasticsearch ์ƒ‰์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", e); + throw new IndexException(IndexErrorCode.ES_SAVE_ERROR); + } + } + + /** + * ์•ฝํ’ˆ ๋„๋ฉ”์ธ ๊ฐ์ฒด ํŽ˜์ด์ง€๋ฅผ Elasticsearch ๋ฌธ์„œ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * 1) Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด โ†’ DrugKeywordDocument๋กœ ๋ณ€ํ™˜ + * 2) Elasticsearch์— saveAll๋กœ ์ผ๊ด„ ์ƒ‰์ธ + * + * @param drugs ์ƒ‰์ธํ•  ์•ฝํ’ˆ ๋„๋ฉ”์ธ ํŽ˜์ด์ง€ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveAllKeyword(Page drugs) { + // ๋„๋ฉ”์ธ โ†’ ES Document ๋ณ€ํ™˜ + log("saveAllSymptom() ์š”์ฒญ ์ˆ˜์‹ "); + List docs = drugs.stream() + .map(drug -> KeywordMapper.toDocument( + drug, + drug.generateEfficacySuggestions(), + drug.generateIngredientSuggestions() + )) + .toList(); + + log(" ๋ฌธ์„œ ๋ณ€ํ™˜ ์™„๋ฃŒ: count=" + docs.size()); + + keywordRepository.saveAll(docs); + log(" ES ์ƒ‰์ธ ์™„๋ฃŒ: count=" + docs.size()); + } + + /** + * Bulk API ์š”์ฒญ์šฉ NDJSON ๋ฐ”๋””๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param esIndexName Bulk ๋Œ€์ƒ ES ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @param drugs ์ƒ‰์ธํ•  Drug ๋ฆฌ์ŠคํŠธ + * @return NDJSON ํ˜•์‹์˜ ๋ฌธ์ž์—ด + * @throws Exception JSON ์ง๋ ฌํ™” ์˜ค๋ฅ˜ ์‹œ + * @author ์ •์•ˆ์‹ + * @since 2025-04-27 + */ + private String buildBulkRequestBody(String esIndexName, List drugs) throws Exception { + log("buildBulkRequestBody() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ์ธ๋ฑ์Šค ์ด๋ฆ„: " + esIndexName + ", Drug ๊ฐœ์ˆ˜: " + drugs.size()); + StringBuilder sb = new StringBuilder(); + for (Drug drug : drugs) { + Map action = Map.of("index", Map.of("_index", esIndexName, "_id", drug.getDrugId().toString())); + sb.append(objectMapper.writeValueAsString(action)).append("\n"); + sb.append(objectMapper.writeValueAsString(createDrugDocument(drug))).append("\n"); + } + return sb.toString(); + } + + /** + * Bulk ์š”์ฒญ์„ ์œ„ํ•œ Request ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param esIndexName Bulk ์—”๋“œํฌ์ธํŠธ์— ์‚ฌ์šฉํ•  ES ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @param bulkBody NDJSON ํ˜•์‹์˜ Bulk ์š”์ฒญ ๋ฐ”๋”” + * @return Bulk ์šฉ Request + * @author ์ •์•ˆ์‹ + * @since 2025-04-27 + */ + private Request createBulkRequest(String esIndexName, String bulkBody) { + log("createBulkRequest() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ์ธ๋ฑ์Šค ์ด๋ฆ„: " + esIndexName); + Request request = new Request("POST", "/" + esIndexName + "/_bulk"); + request.setEntity(new NStringEntity(bulkBody, ContentType.APPLICATION_JSON)); + return request; + } + + /** + * Drug ๊ฐ์ฒด์™€ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Elasticsearch ์ƒ‰์ธ์šฉ ๋ฌธ์„œ ํ•„๋“œ ๋งต์„ ์ƒ์„ฑํ•œ๋‹ค. + * + * @param drug ์ƒ‰์ธํ•  Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @return Elasticsearch์— ์ €์žฅํ•  ๋ฌธ์„œ ํ•„๋“œ ๋งต + * @author ์ •์•ˆ์‹ + * @modified 2025-04-27 + * 25.04.27 - ๋ณ€๊ฒฝ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋‚ด๋ถ€ ํ•„๋“œ์— ๋งž์ถฐ ์ˆ˜์ • + * @since 2025-04-22 + */ + private Map createDrugDocument(Drug drug) { + return Map.of("drugId", drug.getDrugId(), "drugName", drug.getDrugName(), "company", drug.getCompany(), "efficacy", drug.getEfficacy(), "imageUrl", drug.getImageUrl() != null ? drug.getImageUrl() : "", "vector", drug.getVector()); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java new file mode 100644 index 0000000..17f1180 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/document/DrugKeywordDocument.java @@ -0,0 +1,56 @@ +package com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.CompletionField; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.List; + +@Document(indexName = "drug_keyword") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DrugKeywordDocument { + + @Id + @Field(type = FieldType.Keyword, name = "drugId") + private Long drugId; + + @Field(type = FieldType.Text, name = "drugName") + private String drugName; + + @Field(type = FieldType.Text, name = "company") + private String company; + + @Field(type = FieldType.Text, name = "imageUrl") + private String imageUrl; + + @Field(type = FieldType.Text, name = "efficacy") + private List efficacy; + + @Field(type = FieldType.Text, name = "efficacy_list") + private List efficacyList; + + @Field(type = FieldType.Text, name = "ingredientName") + private List ingredientName; + + @CompletionField( + analyzer = "drugName_autocomplete", + searchAnalyzer = "drugName_autocomplete" + ) + private List drugNameSuggester; + + @CompletionField( + analyzer = "drugName_autocomplete", + searchAnalyzer = "drugName_autocomplete" + ) + private List ingredientNameSuggester; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java new file mode 100644 index 0000000..a3f7a4e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/infrastructure/persistence/elasticsearch/repository/DrugKeywordRepository.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.repository; + +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document.DrugKeywordDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface DrugKeywordRepository extends ElasticsearchRepository { + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/DrugIndexingController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/DrugIndexingController.java new file mode 100644 index 0000000..f1fb017 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/DrugIndexingController.java @@ -0,0 +1,56 @@ +package com.likelion.backendplus4.yakplus.drug.index.presentation.controller; + +import com.likelion.backendplus4.yakplus.drug.index.application.port.in.IndexUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import com.likelion.backendplus4.yakplus.drug.index.presentation.controller.docs.DrugIndexingControllerDocs; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * ์•ฝํ’ˆ ์ธ๋ฑ์‹ฑ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค + * + * @modified 2025-04-28 + * @since 2025-04-22 + */ +@RestController +@RequestMapping("/drugs/index") +@RequiredArgsConstructor +public class DrugIndexingController implements DrugIndexingControllerDocs { + private final IndexUseCase indexUseCase; + + /** + * ์ƒ‰์ธ ์ƒ์„ฑ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. + * + * @author ์ •์•ˆ์‹ + * @modified 2025-04-28 + * 25.04.28 - IndexUseRequest๋ฅผ ์ธ์ž์—์„œ ์ œ๊ฑฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.(์ถ”ํ›„ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ์„ ํƒ ๋กœ์ง ์ถ”๊ฐ€์‹œ ๋ณ€๊ฒฝ์˜ˆ์ •) + * @since 2025-04-22 + */ + @Override + @PostMapping("/save") + public void index() { + log("์ปจํŠธ๋กค๋Ÿฌ indexAll ์š”์ฒญ ์ˆ˜์‹ "); + indexUseCase.index(); + } + + /** + * ์ƒ‰์ธ ๋ฐฐ์น˜ ์ž‘์—…์„ ์‹คํ–‰ํ•˜์—ฌ, DB๋กœ๋ถ€ํ„ฐ ์กฐํšŒํ•œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ Elasticsearch์— ์ผ๊ด„ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ‰์ธ ์ž‘์—… ์„ฑ๊ณต ์—ฌ๋ถ€ ์‘๋‹ต (Void) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-25 + * @since 2025-04-24 + */ + @Override + @PostMapping("/keyword") + public ResponseEntity> triggerIndex() { + log("indexSymptom ์š”์ฒญ ์ˆ˜์‹ "); + indexUseCase.indexKeyword(); + return ApiResponse.success(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java new file mode 100644 index 0000000..8dbe6e5 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/presentation/controller/docs/DrugIndexingControllerDocs.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.index.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * ์•ฝํ’ˆ ์ธ๋ฑ์‹ฑ API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Indexing", description = "์•ฝํ’ˆ ์ธ๋ฑ์‹ฑ API") +public interface DrugIndexingControllerDocs { + + @Operation( + summary = "์ž์—ฐ์–ด ์ธ๋ฑ์‹ฑ ์ž‘์—… ์‹คํ–‰", + description = "์•ฝํ’ˆ ์ธ๋ฑ์‹ฑ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + ) + void index(); + + @Operation( + summary = "ํ‚ค์›Œ๋“œ ์ธ๋ฑ์‹ฑ ์ž‘์—… ์‹คํ–‰", + description = "DB๋กœ๋ถ€ํ„ฐ ์กฐํšŒํ•œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ Elasticsearch์— ์ผ๊ด„ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> triggerIndex(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java new file mode 100644 index 0000000..6d46b70 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/DrugEntityMapper.java @@ -0,0 +1,39 @@ +package com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.persistence.entity.EmbeddingEntity; +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugFieldTypeMapper; + +public class DrugEntityMapper { + /** + * DrugRawDataEntity์™€ EmbeddingEntity๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param EmbeddingEntity๋ฅผ ์ƒ์†ํ•˜๋Š” ์ž„๋ฒ ๋”ฉ ์—”ํ‹ฐํ‹ฐ ํƒ€์ž… + * @param drugEntity DB์—์„œ ์กฐํšŒํ•œ ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ ์—”ํ‹ฐํ‹ฐ + * @param embedEntity ํ•ด๋‹น ์•ฝํ’ˆ์˜ ์ž„๋ฒ ๋”ฉ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ์—”ํ‹ฐํ‹ฐ + * @return ์›์‹œ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-03 + */ + public static Drug toDomainFromEntity(DrugRawDataEntity drugEntity, E embedEntity) { + return Drug.builder() + .drugId(drugEntity.getDrugId()) + .drugName(drugEntity.getDrugName()) + .company(drugEntity.getCompany()) + .permitDate(drugEntity.getPermitDate()) + .isGeneral(drugEntity.isGeneral()) + .materialInfo(DrugFieldTypeMapper.parseMaterials(drugEntity.getMaterialInfo())) + .storeMethod(drugEntity.getStoreMethod()) + .validTerm(drugEntity.getValidTerm()) + .efficacy(DrugFieldTypeMapper.parseStringToList(drugEntity.getEfficacy())) + .usage(DrugFieldTypeMapper.parseStringToList(drugEntity.getUsage())) + .precaution(DrugFieldTypeMapper.parsePrecaution(drugEntity.getPrecaution())) + .imageUrl(drugEntity.getImageUrl()) + .cancelDate(drugEntity.getCancelDate()) + .cancelName(drugEntity.getCancelName()) + .isHerbal(drugEntity.getIsHerbal()) + .vector(DrugFieldTypeMapper.parseJsonToFloatArray(embedEntity.getVector())) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/EmbedEntityBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/EmbedEntityBuilder.java new file mode 100644 index 0000000..98dce1a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/EmbeddingUtil/EmbedEntityBuilder.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.drug.index.support.EmbeddingUtil; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.embed.infrastructure.batch.step.dto.DrugVectorDto; + +public class EmbedEntityBuilder { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static T buildEmbedEntity(DrugVectorDto dto, Class clazz) { + try { + return clazz.getDeclaredConstructor(Long.class, String.class) + .newInstance(dto.getDrugId(), toStringFromFloatArray(dto.getVector())); + } catch (Exception e) { + //TODO: ์—”ํ„ฐํ‹ฐ ์ƒ์„ฑ ์‹คํŒจ + throw new RuntimeException(e); + } + } + + private static String toStringFromFloatArray(float[] vector) { + try { + return MAPPER.writeValueAsString(vector); + } catch (JsonProcessingException e) { + //TODO: ๋ณ€ํ™˜ ์‹คํŒจ ๋กœ๊ทธ + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java new file mode 100644 index 0000000..d51c2e2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/mapper/KeywordMapper.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.index.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.index.infrastructure.persistence.elasticsearch.document.DrugKeywordDocument; + +import java.util.List; + +public class KeywordMapper { + + public static DrugKeywordDocument toDocument( + Drug entity, + List efficacyTokens, + List ingredientTokens + ) { + return DrugKeywordDocument.builder() + .drugId(entity.getDrugId()) + .drugName(entity.getDrugName()) + .efficacy(entity.getEfficacy()) + .efficacyList(efficacyTokens) + .imageUrl(entity.getImageUrl()) + .company(entity.getCompany()) + .drugNameSuggester(List.of(entity.getDrugName())) + .ingredientName(ingredientTokens) + .ingredientNameSuggester(ingredientTokens) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/parser/SymptomTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/parser/SymptomTextParser.java new file mode 100644 index 0000000..e242d9e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/index/support/parser/SymptomTextParser.java @@ -0,0 +1,61 @@ +package com.likelion.backendplus4.yakplus.drug.index.support.parser; + + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * ์ฆ์ƒ ํ…์ŠคํŠธ ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * - ๋ฒˆํ˜ธ, ํ—ค๋”, ๊ธฐํ˜ธ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ๋‹จ์ผ ๋ฌธ์ž์—ด๋กœ ๊ฒฐํ•ฉํ•˜๋Š” ๊ธฐ๋Šฅ + * - ํ‚ค์›Œ๋“œ ์ž๋™์™„์„ฑ์„ ์œ„ํ•œ ํ† ํฐ ์ƒ์„ฑ ๊ธฐ๋Šฅ + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified 2025-04-25 + */ + +public class SymptomTextParser { + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด ๋ชฉ๋ก์—์„œ ๋ฒˆํ˜ธ(โ€œ1.โ€), ํ—ค๋”(โ€œํšจ๋Šฅํšจ๊ณผโ€), ๊ธฐํ˜ธ(โ€œโ—‹โ€ขโ–ถโ€)๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param raws ์›๋ณธ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ (๊ฐ ์ค„ ๋‹จ์œ„) + * @return ์ „์ฒ˜๋ฆฌ ํ›„ ๊ฒฐํ•ฉ๋œ ๋‹จ์ผ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-25 + * @since 2025-04-24 + */ + public static String flattenLines(List raws) { + // ๊ฐ ์ค„์—์„œ ๋ฒˆํ˜ธยทํ—ค๋”ยท๊ธฐํ˜ธ๋ฅผ ์ œ๊ฑฐ + return raws.stream() + .map(line -> line.replaceAll("^\\d+\\.\\s*|ํšจ๋Šฅํšจ๊ณผ|[โ—‹โ€ขโ–ถ]", " ")) + .collect(Collectors.joining(" ")); + } + + /** + * ์ „์ฒ˜๋ฆฌ๋œ ํ…์ŠคํŠธ๋ฅผ ํ† ํฐ์œผ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , ๋ถˆ์šฉ์–ด ๋ฐ ์กฐ์‚ฌ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ์ž๋™์™„์„ฑ์šฉ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ „์ฒ˜๋ฆฌ๋œ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ์šฉ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-25 + * @since 2025-04-24 + */ + public static List tokenizeForSuggestion(String text) { + // ๊ตฌ๋ถ„์ž(์‰ผํ‘œ, ๊ตฌ๋‘์ , ๊ณต๋ฐฑ ๋“ฑ)๋กœ ํ…์ŠคํŠธ ๋ถ„ํ•  + return Arrays.stream(text.split("[,ยท/;:\\s()\\[\\]]+")) + .map(String::trim) + // ์ตœ์†Œ 2์ž ์ด์ƒ์ธ ํ† ํฐ๋งŒ ์œ ์ง€ + .filter(tok -> tok.length() >= 2) + // ๋ถˆ์šฉ์–ด ํ•„ํ„ฐ๋ง + .filter(tok -> !Set.of("ํŠนํžˆ", "๋“ฑ์˜", "๋˜๋Š”", "๋ฐ", "์˜ํ•œ", "๋‹ค์Œ", "๋ณด๊ธ‰", "์—๋„ˆ์ง€") + .contains(tok)) + // ์กฐ์‚ฌ(์˜, ์—, ์œผ๋กœ ๋“ฑ) ์ œ๊ฑฐ + .map(tok -> tok.replaceAll("(?.+?)(?:์˜|์—|์œผ๋กœ|์—์„œ|์‹œ์˜|๋กœ|๊ฐ€)$", "${base}")) + // ์ค‘๋ณต ํ‚ค์›Œ๋“œ ์ œ๊ฑฐ + .distinct() + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java deleted file mode 100644 index 3b4cbdf..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/ApiDataDrugImgEntity.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.ToString; - -@Entity -@ToString -@Table(name="API_DATA_DRUG_IMG") -public class ApiDataDrugImgEntity { - @Id - @JsonProperty("ITEM_SEQ") - private Long seq; - - @JsonProperty("BIG_PRDT_IMG_URL") - private String imgUrl; -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java deleted file mode 100644 index 6b008bc..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/GovDrugJdbcRepository.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc; - -import java.util.List; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; - -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; - -@Repository -@RequiredArgsConstructor -public class GovDrugJdbcRepository { - private final JdbcTemplate jdbc; - - @Transactional - public void saveAll(List entities) { - String sql = "" - + "INSERT INTO gov_drug_detail " - + " (ITEM_SEQ, ITEM_NAME, ENTP_NAME, " - + " ITEM_PERMIT_DATE, ETC_OTC_CODE, MATERIAL_NAME, " - + " STORAGE_METHOD, VALID_TERM, EE_DOC_DATA, " - + " UD_DOC_DATA, NB_DOC_DATA) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " - + "ON DUPLICATE KEY UPDATE " - + " ITEM_NAME = VALUES(ITEM_NAME), " - + " ENTP_NAME = VALUES(ENTP_NAME), " - + " ITEM_PERMIT_DATE = VALUES(ITEM_PERMIT_DATE), " - + " ETC_OTC_CODE = VALUES(ETC_OTC_CODE), " - + " MATERIAL_NAME = VALUES(MATERIAL_NAME), " - + " STORAGE_METHOD = VALUES(STORAGE_METHOD), " - + " VALID_TERM = VALUES(VALID_TERM), " - + " EE_DOC_DATA = VALUES(EE_DOC_DATA), " - + " UD_DOC_DATA = VALUES(UD_DOC_DATA), " - + " NB_DOC_DATA = VALUES(NB_DOC_DATA)"; - jdbc.batchUpdate(sql, new JdbcBatchSetter(entities)); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/JdbcBatchSetter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/JdbcBatchSetter.java deleted file mode 100644 index 7310690..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jdbc/JdbcBatchSetter.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jdbc; - -import java.sql.Date; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Types; -import java.time.LocalDate; -import java.util.List; - -import org.springframework.jdbc.core.BatchPreparedStatementSetter; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class JdbcBatchSetter implements BatchPreparedStatementSetter { - - private final List entities; - - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - GovDrugDetailEntity e = entities.get(i); - ps.setLong (1, e.getDrugId()); - ps.setString (2, e.getDrugName()); - ps.setString (3, e.getCompany()); - - LocalDate permit = e.getPermitDate(); - if (permit != null) { - ps.setDate(4, Date.valueOf(permit)); - } else { - ps.setNull(4, Types.DATE); - } - - ps.setBoolean(5, e.isGeneral()); - ps.setString (6, e.getMaterialInfo()); - ps.setString (7, e.getStoreMethod()); - ps.setString (8, e.getValidTerm()); - ps.setString (9, e.getEfficacy()); - ps.setString (10, e.getUsage()); - ps.setString (11, e.getPrecaution()); - } - - @Override - public int getBatchSize() { - return entities.size(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java deleted file mode 100644 index 12a8800..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/ApiDataDrugImgRepo.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.ApiDataDrugImgEntity; - -@Repository -public interface ApiDataDrugImgRepo extends JpaRepository { -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java deleted file mode 100644 index 4ca4706..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugDetailJpaRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugDetailEntity; - -import jakarta.transaction.Transactional; - -@Repository -public interface GovDrugDetailJpaRepository extends JpaRepository { - - @Override - @Transactional - List saveAllAndFlush(Iterable entities); -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java deleted file mode 100644 index b6ab711..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/jpa/GovDrugJpaRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; - -public interface GovDrugJpaRepository extends JpaRepository { -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java deleted file mode 100644 index c48cf28..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiResponseMapper.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.api; - -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ApiResponseMapper { - - public static JsonNode getItemsFromResponse(String response) { - log.info("์‘๋‹ต์—์„œ items ๊ฐ’ ์ถ”์ถœ"); - try { - return new ObjectMapper().readTree(response) - .path("body") - .path("items"); - } catch (JsonProcessingException e) { - log.error("items ์ถ”์ถœ ์‹คํŒจ"); - //TODO: CustomException ๋งŒ๋“ค๊ณ , ControllerAdvice๋กœ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•„์š” - throw new RuntimeException(e); - } - } - - public static int getTotalCountFromResponse(String response) { - log.info("์‘๋‹ต์—์„œ ๋ฐ์ดํ„ฐ ์‚ฌ์ด์ฆˆ ์ถ”์ถœ"); - return 10_000; - // try { - // return new ObjectMapper().readTree(response) - // .path("body") - // .path("totalCount") - // .asInt(); - // } catch (JsonProcessingException e) { - // log.error("totalCount ์ถ”์ถœ ์‹คํŒจ"); - // //TODO: CustomException ๋งŒ๋“ค๊ณ , ControllerAdvice๋กœ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ•„์š” - // throw new RuntimeException(e); - // } - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java deleted file mode 100644 index c5e8b83..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/mapper/DrugDataMapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.mapper; - -import com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity.GovDrugEntity; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; - -public class DrugDataMapper { - public static GovDrug toDomainFromEntity(GovDrugEntity e){ - return GovDrug.builder() - .drugId(e.getId()) - .drugName(e.getDrugName()) - .company(e.getCompany()) - .permitDate(e.getPermitDate()) - .isGeneral(e.isGeneral()) - .materialInfo(e.getMaterialInfo()) - .storeMethod(e.getStoreMethod()) - .validTerm(e.getValidTerm()) - .efficacy(e.getEfficacy()) - .usage(e.getUsage()) - .precaution(e.getPrecaution()) - .imageUrl(e.getImageUrl()) - .build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java deleted file mode 100644 index ee4f867..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/MaterialParser.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.parser; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -public class MaterialParser { - public static String parseMaterial(String raw) throws Exception { - ObjectMapper result = new ObjectMapper(); - ArrayNode resultArray = result.createArrayNode(); - String[] blocks = splitBlock(raw); - parsingblocksAndPutArrayItem(blocks, resultArray); - return convertString(result, resultArray); - } - - private static void parsingblocksAndPutArrayItem(String[] blocks, ArrayNode resultArray) { - for (String block : blocks) { - block = block.trim(); - if (block.isEmpty()) { - continue; - } - String[] pairs = splitByPipe(block); - ObjectNode item = makeItem(pairs); - resultArray.add(item); - } - } - - private static String convertString(ObjectMapper objectMapper, ArrayNode arrayNode) { - try { - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode); - } catch (JsonProcessingException e) { - //TODO String ๋ณ€ํ™˜์‹คํŒจ - throw new RuntimeException(e); - } - } - - private static ObjectNode makeItem(String[] pairs) { - ObjectNode item = new ObjectMapper().createObjectNode(); - for (String pair : pairs) { - String[] kv = pair.split(":", 2); - String key = kv[0].trim(); - String value = ""; - if(kv.length == 2){ - value = kv[1].trim(); - } - item.put(key, value); - } - return item; - } - - private static String[] splitByPipe(String block) { - return block.split("\\|"); - } - - private static String[] splitBlock(String raw) { - return raw.split(";"); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/XMLParser.java deleted file mode 100644 index 8c7680b..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/parser/XMLParser.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.parser; - -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class XMLParser { - public static String toJson(String xml) { - - if(isXmlNull(xml)) { - return "{\"\": \"\"}"; - } - - Document doc = parseXmlString(xml); - Element root = doc.getDocumentElement(); - - List allSections = new ArrayList<>(); - List allArticles = new ArrayList<>(); - List allParagraphs = new ArrayList<>(); - - Map sectionMap = new HashMap<>(); - Map articleMap = new HashMap<>(); - - DocTag docTag = new DocTag(root, allSections); - parseSesctions(root, allSections, sectionMap); - parseArticles(root, allArticles, articleMap, sectionMap); - parseParagraph(root, allParagraphs, articleMap); - return convertJson(docTag); - } - private static final ObjectMapper mapper = new ObjectMapper(); - - private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - - private static String convertJson(DocTag docTag) { - try { - return mapper.writeValueAsString(docTag); - //TODO: ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ›„ ์‚ญ์ œ - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - private static void parseParagraph(Element root, List allParagraphs, Map articleMap) { - NodeList paraNodes = root.getElementsByTagName("PARAGRAPH"); - - if(paraNodes.getLength() != 0){ - for (int i = 0; i < paraNodes.getLength(); i++) { - Element paragraphElement = (Element) paraNodes.item(i); - ParagraphTag paragraphTag = new ParagraphTag(); - paragraphTag.tagName = paragraphElement.getAttribute("tagName"); - paragraphTag.textIndent = paragraphElement.getAttribute("textIndent"); - paragraphTag.marginLeft = paragraphElement.getAttribute("marginLeft"); - paragraphTag.text = paragraphElement.getTextContent().trim(); - - allParagraphs.add(paragraphTag); - mapSectionFromArticle(articleMap, paragraphTag, paragraphElement); - } - } - - } - - private static void parseArticles(Element root, List allArticles, - Map articleMap, - Map sectionMap) { - NodeList artNodes = root.getElementsByTagName("ARTICLE"); - if(artNodes.getLength() > 0) { - for (int i = 0; i < artNodes.getLength(); i++) { - Element artElement = (Element) artNodes.item(i); - ArticleTag articleTag = new ArticleTag(); - articleTag.title = artElement.getAttribute("title"); - articleTag.paragraphs = new ArrayList<>(); - - allArticles.add(articleTag); - articleMap.put(artElement, articleTag); - mapSectionFromArticle(sectionMap, articleTag, artElement); - } - } - - } - - private static void mapSectionFromArticle(Map map, Tags tags, Element element) { - Element parentElement = (Element) element.getParentNode(); - Tags parentTag = map.get(parentElement); - if (parentTag != null) { - parentTag.addTag(tags); - } - } - - private static void parseSesctions(Element root, List allSections, Map sectionMap) { - NodeList secNodes = root.getElementsByTagName("SECTION"); - - if(secNodes.getLength() > 0) { - for (int i = 0; i < secNodes.getLength(); i++) { - Element secEl = (Element) secNodes.item(i); - SectionTag secDto = new SectionTag(); - secDto.title = secEl.getAttribute("title"); - secDto.articles = new ArrayList<>(); - - allSections.add(secDto); - sectionMap.put(secEl, secDto); - } - } - } - - private static Document parseXmlString(String xml) { - //TODO: ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํ›„ ์‚ญ์ œ - try { - return documentBuilderFactory.newDocumentBuilder() - .parse(new InputSource(new StringReader(xml))); - } catch (SAXException e) { - // System.out.println(xml); - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (ParserConfigurationException e) { - //TODO DocumentBulider ์ƒ์„ฑ ์‹คํŒจ - throw new RuntimeException(e); - } - } - - private static boolean isXmlNull(String xml) { - if (xml == null || xml.trim().isEmpty() || xml == "null") { - return true; - } else { - return false; - } - } - - private static class DocTag implements Tags { - public String title; - public String type; - public List sections; - - DocTag(Element root, List sections) { - this.title = root.getAttribute("title"); - this.type = root.getAttribute("type"); - this.sections = sections; - } - - @Override - public void addTag(Tags tags) { - sections.add((SectionTag) tags); - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof DocTag; - } - } - - private static class SectionTag implements Tags { - public String title; - public List articles; - - @Override - public void addTag(Tags tags) { - articles.add((ArticleTag)tags); - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof SectionTag; - } - } - - private static class ArticleTag implements Tags { - public String title; - public List paragraphs; - - @Override - public void addTag(Tags tags) { - paragraphs.add((ParagraphTag) tags); - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof ArticleTag; - } - } - - public static class ParagraphTag implements Tags { - public String tagName; - public String textIndent; - public String marginLeft; - public String text; - - @Override - public void addTag(Tags tags) { - //TODO: addTag Exception ํ•˜์œ„ ์—†์Œ - } - - @Override - public boolean equalsClass(Tags tags) { - return tags instanceof ParagraphTag; - } - } - - public static interface Tags { - void addTag(Tags tags); - boolean equalsClass(Tags tags); - } -} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java deleted file mode 100644 index c36be75..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDataTestController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller; - -import java.util.List; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.likelion.backendplus4.yakplus.drug.application.service.DrugDataService; -import com.likelion.backendplus4.yakplus.drug.domain.model.GovDrug; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RestController -@Slf4j -@RequiredArgsConstructor -public class DrugDataTestController { - private final DrugDataService dragDataService; - - @GetMapping("/data/all") - public List getAllData(){ - log.info("getAllData"); - return dragDataService.findAllRawDrug(); - } - -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java deleted file mode 100644 index 7c87340..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugDetailController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; - -import com.likelion.backendplus4.yakplus.drug.application.service.DrugApprovalDetailScraper; - -import lombok.RequiredArgsConstructor; - -@Controller -@RequiredArgsConstructor -public class DrugDetailController { - private final DrugApprovalDetailScraper scraperUseCase; - - @GetMapping("/gov/api/parser/detail/start") - public ResponseEntity saveAPIData(){ - scraperUseCase.requestUpdateRawData(); - return ResponseEntity.ok().build(); - } - - @GetMapping("/gov/api/parser/detail/startAll") - public ResponseEntity saveAPIDataAll(){ - scraperUseCase.requestUpdateAllRawData(); - return ResponseEntity.ok().build(); - } - - @PostMapping("/gov/api/parser/detail/startAll") - public ResponseEntity saveAPIDataAllByJdbc(){ - scraperUseCase.requestUpdateAllRawDataByJdbc(); - return ResponseEntity.ok().build(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java deleted file mode 100644 index cf68610..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/presentation/controller/DrugImageController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.likelion.backendplus4.yakplus.drug.presentation.controller; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.likelion.backendplus4.yakplus.drug.application.service.DrugImageGovScraper; - -import lombok.RequiredArgsConstructor; - -@RestController -@RequiredArgsConstructor -public class DrugImageController { - private final DrugImageGovScraper imageScraper; - - @GetMapping("/gov/api/parser/image/start") - public void test(){ - imageScraper.getApiData(); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/ScraperException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/ScraperException.java similarity index 85% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/exception/ScraperException.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/ScraperException.java index 7f47c4d..f77daab 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/ScraperException.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/ScraperException.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.exception; +package com.likelion.backendplus4.yakplus.drug.scraper.application.exception; import com.likelion.backendplus4.yakplus.common.exception.CustomException; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/error/ScraperErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/error/ScraperErrorCode.java similarity index 66% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/exception/error/ScraperErrorCode.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/error/ScraperErrorCode.java index 017d57d..32ea7ae 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/exception/error/ScraperErrorCode.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/exception/error/ScraperErrorCode.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.exception.error; +package com.likelion.backendplus4.yakplus.drug.scraper.application.exception.error; import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; import lombok.RequiredArgsConstructor; @@ -11,6 +11,9 @@ public enum ScraperErrorCode implements ErrorCode { DB_ERROR_IMAGE_INFO(HttpStatus.INTERNAL_SERVER_ERROR, 300002, "์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), DB_ERROR_COMBINED_INFO(HttpStatus.INTERNAL_SERVER_ERROR, 300003, "๊ฒฐํ•ฉ๋œ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), API_CONNECT_FAIL(HttpStatus.BAD_GATEWAY, 400001, "์™ธ๋ถ€ API ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + API_DRUG_DETAIL_PARSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 300004, "ํ—ˆ๊ฐ€ ์ •๋ณด API์—์„œ ์ •๋ณด ํŒŒ์‹ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + MATERIAL_PARSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR,300006, "์„ฑ๋ถ„ ํŒŒ์‹ฑ์— ์‹คํŒจ ํ–ˆ์Šต๋‹ˆ๋‹ค"), + RESPONSE_TYPE_CHANGE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 300005, "API ์‘๋‹ต์„ ๊ฐ์ฒด ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), PARSING_ERROR(HttpStatus.BAD_REQUEST, 400001, "๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); private final HttpStatus httpStatus; @@ -19,16 +22,16 @@ public enum ScraperErrorCode implements ErrorCode { @Override public HttpStatus httpStatus() { - return null; + return httpStatus; } @Override public int codeNumber() { - return 0; + return code; } @Override public String message() { - return ""; + return message; } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperDetailUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperDetailUseCase.java new file mode 100644 index 0000000..a4c1dce --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperDetailUseCase.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ์œ ์Šค์ผ€์ด์Šค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ +public interface DrugScraperDetailUseCase { + /** + * ๋ชจ๋“  ์˜์•ฝํ’ˆ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ผ๊ด„์ ์œผ๋กœ ์š”์ฒญํ•˜์—ฌ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ + String requestAllData(); + + /** + * ์‹คํ–‰ ์ค‘์ธ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ๋ฐฐ์น˜๋ฅผ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + String stop(); + + /** + * ์‹คํ–‰ ์ค‘์ธ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + String getStatus(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperImageUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperImageUseCase.java new file mode 100644 index 0000000..42213f3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperImageUseCase.java @@ -0,0 +1,37 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; + +/** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ์œ ์Šค์ผ€์ด์Šค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ +public interface DrugScraperImageUseCase { + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ๋ฐฐ์น˜ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + * @modify 2025-05-02 + * - ์Šคํ”„๋ง ๋ฐฐ์น˜ ํ˜•ํƒœ๋กœ ์ˆ˜์ • + */ + String requestAllData(); + + /** + * ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ๋ฐฐ์น˜ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @since 2025-05-02 + */ + String stop(); + + /** + * ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @since 2025-05-02 + */ + String getStatus(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperTableCombineUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperTableCombineUseCase.java new file mode 100644 index 0000000..db8176a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperTableCombineUseCase.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; + +public interface DrugScraperTableCombineUseCase { + + /** + * API ์š”์ฒญ์œผ๋กœ ๋ฐ›์•„์˜จ RAW ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” 2๊ฐœ๋ฅผ ๋ณ‘ํ•ฉํ•ด + * 1๊ฐœ์˜ ํ…Œ์ด๋ธ”๋กœ ๋งŒ๋“œ๋Š” ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + String mergeTable(); + + /** + * ํ˜„์žฌ ์ž‘์—… ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String getStatus(); + + /** + * ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String stop(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperUseCase.java new file mode 100644 index 0000000..213db86 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/in/DrugScraperUseCase.java @@ -0,0 +1,46 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.in; + +public interface DrugScraperUseCase { + + /** + * ์˜์•ฝํ’ˆ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ž„๋ฒ ๋”ฉ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * 1. ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘ ํ›„ RDB ํ…Œ์ด๋ธ” ์ €์žฅ + * 2. ์ด๋ฏธ์ง€ ์ •๋ณด ์ˆ˜์ง‘ ํ›„ RDB ํ…Œ์ด๋ธ” ์ €์žฅ + * 3. ์ƒ์„ธ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ๋ณ‘ํ•ฉ ํ›„ ํ†ตํ•ฉ ํ…Œ์ด๋ธ” ์ €์žฅ + * 4. ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ๋ฐ ๊ฐ๊ฐ ๋ฒกํ„ฐ ํ…Œ์ด๋ธ” ์ €์žฅ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + String scraperStart(); + + /** + * ํ˜„์žฌ ์Šคํฌ๋ž˜ํ•‘ ๋ฐ ์ž„๋ฒ ๋”ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ˜„์žฌ ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String getStatus(); + + /** + * ์‹คํ–‰ ์ค‘์ธ ์Šคํฌ๋ž˜ํ•‘ ๋ฐ ์ž„๋ฒ ๋”ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String stop(); + + /** + * ์ค‘์ง€๋œ ๋ชจ๋“  ๋ฐฐ์น˜ ์ž‘์—…์„ ์กฐํšŒํ•˜๊ณ , ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String restart(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/out/BatchJobPort.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/out/BatchJobPort.java new file mode 100644 index 0000000..30f519d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/port/out/BatchJobPort.java @@ -0,0 +1,171 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.port.out; + +/** + * ์˜์•ฝํ’ˆ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ž„๋ฒ ๋”ฉ๊ณผ ๊ด€๋ จ๋œ ๋ฐฐ์น˜ ์ž‘์—…์„ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•œ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * ๊ฐ ๋ฐฐ์น˜ ์ž‘์—…์˜ ์‹œ์ž‘, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +public interface BatchJobPort { + + /** + * ์ „์ฒด ๋ฐฐ์น˜ ์ž‘์—…์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String allJobStart(); + + /** + * ์ „์ฒด ๋ฐฐ์น˜ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String allJobStop(); + + /** + * ์ค‘์ง€๋œ ์ „์ฒด ๋ฐฐ์น˜ ์ž‘์—…์„ ์žฌ๊ฐœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ์ž‘์—… ์žฌ๊ฐœ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String allJobResume(); + + + /** + * ์ „์ฒด ๋ฐฐ์น˜ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String allJobStatus(); + + /** + * ์ƒ์„ธ ์ •๋ณด ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String detailScrapJobStart(); + + /** + * ์ƒ์„ธ ์ •๋ณด ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String detailScrapJobStop(); + + /** + * ์ƒ์„ธ ์ •๋ณด ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String detailScrapJobStatus(); + + /** + * ์ด๋ฏธ์ง€ ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String imageScrapJobStart(); + + /** + * ์ด๋ฏธ์ง€ ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String imageScrapJobStop(); + + /** + * ์ด๋ฏธ์ง€ ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String imageScrapJobStatus(); + + /** + * ์ƒ์„ธ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ‘ํ•ฉํ•˜๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String tableCombineJobStart(); + + /** + * ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String tableCombineJobStop(); + + /** + * ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String tableCombineJobStatus(); + + /** + * ์ž„๋ฒ ๋”ฉ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String embedJobStart(); + + /** + * ์ž„๋ฒ ๋”ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String embedJobStop(); + + /** + * ์ž„๋ฒ ๋”ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + String embedjobStatus(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperDetailService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperDetailService.java new file mode 100644 index 0000000..7b9f3c3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperDetailService.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperDetailUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugScraperDetailService implements DrugScraperDetailUseCase { + private final BatchJobPort batchJobPort; + + @Override + public String requestAllData() { + return batchJobPort.detailScrapJobStart(); + } + + @Override + public String stop() { + return batchJobPort.detailScrapJobStop(); + } + + @Override + public String getStatus() { + return batchJobPort.detailScrapJobStatus(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperService.java new file mode 100644 index 0000000..5367992 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperService.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DrugScraperService implements DrugScraperUseCase { + private final BatchJobPort batchJobPort; + + @Override + public String scraperStart() { + return batchJobPort.allJobStart(); + } + + @Override + public String stop() { + return batchJobPort.allJobStop(); + } + + @Override + public String restart() { + return batchJobPort.allJobResume(); + } + + @Override + public String getStatus() { + return batchJobPort.allJobStatus(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperServiceImage.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperServiceImage.java new file mode 100644 index 0000000..59c108b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperServiceImage.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; + +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperImageUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class DrugScraperServiceImage implements DrugScraperImageUseCase { + private final BatchJobPort batchJobPort; + + @Override + public String requestAllData() { + return batchJobPort.imageScrapJobStart(); + } + + @Override + public String stop() { + return batchJobPort.imageScrapJobStop(); + } + + @Override + public String getStatus() { + return batchJobPort.imageScrapJobStatus(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperTableCombineService.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperTableCombineService.java new file mode 100644 index 0000000..694dcc1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/application/service/DrugScraperTableCombineService.java @@ -0,0 +1,29 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.application.service; + +import org.springframework.stereotype.Service; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperTableCombineUseCase; +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DrugScraperTableCombineService implements DrugScraperTableCombineUseCase { + private final BatchJobPort batchJobPort; + + @Override + public String mergeTable() { + return batchJobPort.tableCombineJobStart(); + } + + @Override + public String getStatus() { + return batchJobPort.tableCombineJobStatus(); + } + + @Override + public String stop() { + return batchJobPort.tableCombineJobStop(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java new file mode 100644 index 0000000..5430b5a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiError.java @@ -0,0 +1,36 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * API ํ˜ธ์ถœ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ์ฝ”๋“œ + * + * @since 2025-04-15 + */ +@RequiredArgsConstructor +public enum RestApiError implements ErrorCode { + PAGE_COUNT_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 550001, "API ์ „์ฒด ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."), + ITEM_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 550002, "API ์‘๋‹ต์—์„œ ์ ์ ˆํ•œ items๋ฅผ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); + + private final HttpStatus httpStatus; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiException.java new file mode 100644 index 0000000..2284f53 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/exception/RestApiException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class RestApiException extends CustomException { + private final ErrorCode errorCode; + + public RestApiException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/ApiRequestManager.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/ApiRequestManager.java new file mode 100644 index 0000000..81127b1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/ApiRequestManager.java @@ -0,0 +1,157 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception.RestApiError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.exception.RestApiException; + +/** + * ๊ณต๊ณต์˜์•ฝํ’ˆ API ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋งค๋‹ˆ์ € ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * ์ƒ์„ธ์ •๋ณด ๋ฐ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ์ด ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์‘๋‹ต ํŒŒ์‹ฑ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ +@Component +public class ApiRequestManager { + private final RestTemplate restTemplate; + private final UriCompBuilder uriCompBuilder; + private final ObjectMapper objectMapper; + private final int NUM_OF_ROWS; + + /** + * ์ƒ์„ฑ์ž ์ฃผ์ž…์„ ํ†ตํ•ด ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param restTemplate REST API ํ˜ธ์ถœ์„ ์œ„ํ•œ RestTemplate + * @param uriCompBuilder API URI ์กฐ๋ฆฝ ์œ ํ‹ธ๋ฆฌํ‹ฐ + * @param objectMapper JSON ํŒŒ์‹ฑ์šฉ ObjectMapper + * @param NUM_OF_ROWS ํŽ˜์ด์ง€๋‹น ๋ฐ์ดํ„ฐ ์ˆ˜ + */ + public ApiRequestManager(RestTemplate restTemplate, + UriCompBuilder uriCompBuilder, + ObjectMapper objectMapper, + @Value("${gov.numOfRows}") + int NUM_OF_ROWS) { + this.restTemplate = restTemplate; + this.uriCompBuilder = uriCompBuilder; + this.objectMapper = objectMapper; + this.NUM_OF_ROWS = NUM_OF_ROWS; + } + + /** + * ์ƒ์„ธ ์ •๋ณด API ์‘๋‹ต์œผ๋กœ๋ถ€ํ„ฐ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ + * + * @author ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + public int getDetailTotalPage() { + return getTotalPageCountFromResponse(fetchDetailData(1)); + } + + /** + * ์ด๋ฏธ์ง€ ์ •๋ณด API ์‘๋‹ต์œผ๋กœ๋ถ€ํ„ฐ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ + * + * @author ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + public int getImageTotalPage() { + return getTotalPageCountFromResponse(fetchImageData(1)); + } + + /** + * ์˜์•ฝํ’ˆ ID๋ฅผ ํ†ตํ•ด ์•ฝํ’ˆ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param drugId ์˜์•ฝํ’ˆ ๊ณ ์œ  ID + * @return ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ jpeg(base64) + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-01 + */ + public String getImage(Long drugId) { + return restTemplate.getForObject("https://nedrug.mfds.go.kr/pbp/ezdrug/" + drugId.toString(), JsonNode.class) + .get("item") + .get("extimgImageDocid") + .asText(); + } + + /** + * ํŠน์ • ํŽ˜์ด์ง€์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ API์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param pageNo ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @return ์ƒ์„ธ ์ •๋ณด JSON ๋ฌธ์ž์—ด + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + public String fetchDetailData(int pageNo) { + return restTemplate.getForObject(uriCompBuilder.getUriForDetailApi(pageNo), String.class); + } + + /** + * ํŠน์ • ํŽ˜์ด์ง€์˜ ๋‚ฑ์•Œ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ API์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param pageNo ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @return ๋‚ฑ์•Œ ์ด๋ฏธ์ง€ ์ •๋ณด JSON ๋ฌธ์ž์—ด + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + public String fetchImageData(int pageNo) { + return restTemplate.getForObject(uriCompBuilder.getUriForImgApi(pageNo), String.class); + } + + /** + * ์‘๋‹ต ๋ฌธ์ž์—ด์—์„œ items ๋…ธ๋“œ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param response API ์‘๋‹ต ๋ฌธ์ž์—ด + * @return items ๋…ธ๋“œ, ์‹คํŒจ ์‹œ null + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + public JsonNode getItemsFromResponse(String response) { + try { + return new ObjectMapper().readTree(response) + .path("body") + .path("items"); + } catch (JsonProcessingException e) { + log(LogLevel.ERROR, "items ์ถ”์ถœ ์‹คํŒจ"); + log(LogLevel.ERROR, "response: " + response); + throw new RestApiException(RestApiError.ITEM_NOT_FOUND); + } + } + + /** + * ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param response API ์‘๋‹ต ๋ฌธ์ž์—ด + * @return ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ + * + * @author ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private int getTotalPageCountFromResponse(String response) { + try { + int totalRows = objectMapper.readTree(response).path("body").path("totalCount").asInt(); + int pageCount = totalRows / NUM_OF_ROWS; + if (totalRows % NUM_OF_ROWS > 0) { + pageCount += 1; + } + return pageCount; + } catch (Exception e) { + log(LogLevel.ERROR, "์ „์ฒด ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜ ํ™•์ธ ์‹คํŒจ"); + throw new RestApiException(RestApiError.PAGE_COUNT_FAIL); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/UriCompBuilder.java similarity index 56% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/UriCompBuilder.java index f3312f6..a6e2dc7 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/support/api/ApiUriCompBuilder.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/api/util/UriCompBuilder.java @@ -1,11 +1,11 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.support.api; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util; + +import java.net.URI; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import java.net.URI; - /*** * API ์š”์ฒญ URI ๊ฐ์ฒด ์ƒ์„ฑ ๋นŒ๋” * @@ -13,26 +13,31 @@ * API HOST, PATH๋ฅผ ํ™•์ธํ•ด URI ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. * * @since 2025-04-15 - * @author ํ•จ์˜ˆ์ • */ @Component -public class ApiUriCompBuilder { +public class UriCompBuilder { private final String SERVICE_KEY; private final String HOST; private final String API_DETAIL_PATH; private final String API_IMG_PATH ; private final int NUM_OF_ROWS; + private final String API_KM_BERT; + private final String API_KR_SBERT; - public ApiUriCompBuilder(@Value("${gov.host}") String host, + public UriCompBuilder(@Value("${gov.host}") String host, @Value("${gov.serviceKey}") String serviceKey, @Value("${gov.path.detail}") String pathDetail, @Value("${gov.path.img}") String pathImg, - @Value("${gov.numOfRows}") int numOfRows) { + @Value("${gov.numOfRows}") int numOfRows, + @Value("${embed.kmbert}") String API_KM_BERT, + @Value("${embed.krsbert}") String API_KR_SBERT) { this.HOST = host; this.SERVICE_KEY = serviceKey; this.API_DETAIL_PATH = pathDetail; this.API_IMG_PATH = pathImg; this.NUM_OF_ROWS = numOfRows; + this.API_KM_BERT = API_KM_BERT; + this.API_KR_SBERT = API_KR_SBERT; } /*** @@ -41,10 +46,10 @@ public ApiUriCompBuilder(@Value("${gov.host}") String host, * @param path API ์š”์ฒญ ๊ฒฝ๋กœ * @return URI * - * @since 2025-04-15 * @author ํ•จ์˜ˆ์ • + * @since 2025-04-15 */ - private URI getUri(String path, int pageNo) { + private URI getUri(String path, int pageNo, int size) { return UriComponentsBuilder.newInstance() .scheme("https") .host(HOST) @@ -53,7 +58,7 @@ private URI getUri(String path, int pageNo) { .queryParam("serviceKey", SERVICE_KEY) .queryParam("type", "json") .queryParam("pageNo", pageNo) - .queryParam("numOfRows", NUM_OF_ROWS) + .queryParam("numOfRows", size) .build(true) .toUri(); } @@ -61,22 +66,59 @@ private URI getUri(String path, int pageNo) { /*** * ์‹ํ’ˆ์˜์•ฝํ’ˆ์•ˆ์ „์ฒ˜ ์˜์•ฝํ’ˆ ์ œํ’ˆ ํ—ˆ๊ฐ€ ์ƒ์„ธ ์ •๋ณด URI ๋ฐ˜ํ™˜ * @return URI ์ œํ’ˆ ํ—ˆ๊ฐ€ ์ƒ์„ธ ์ •๋ณด - * - * @since 2025-04-15 + * * @author ํ•จ์˜ˆ์ • + * @since 2025-04-15 */ public URI getUriForDetailApi(int pageNo) { - return getUri(API_DETAIL_PATH, pageNo); + return getUri(API_DETAIL_PATH, pageNo, NUM_OF_ROWS); } /*** * ์‹ํ’ˆ์˜์•ฝํ’ˆ์•ˆ์ „์ฒ˜ ์˜์•ฝํ’ˆ ์ œํ’ˆ ํ—ˆ๊ฐ€ ๋ชฉ๋ก URI ๋ฐ˜ํ™˜ * @return URI ์ œํ’ˆ ํ—ˆ๊ฐ€ ๋ชฉ๋ก * - * @since 2025-04-15 * @author ํ•จ์˜ˆ์ • + * @since 2025-04-15 */ public URI getUriForImgApi(int pageNo) { - return getUri(API_IMG_PATH, pageNo); + return getUri(API_IMG_PATH, pageNo, NUM_OF_ROWS); + } + + /** + * KmBERT ์ž„๋ฒ ๋”ฉ API ์š”์ฒญ์šฉ URI๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return KmBERT API URI + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + public URI getUriForKmbertEmbeding() { + return UriComponentsBuilder.newInstance() + .scheme("https") + .host(API_KM_BERT) + .port(443) + .path("/predict") + .build(true) + .toUri(); } + + /** + * KrSBERT ์ž„๋ฒ ๋”ฉ API ์š”์ฒญ์šฉ URI๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return KrSBERT API URI + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + public URI getUriForKrSbertEmbeding() { + return UriComponentsBuilder.newInstance() + .scheme("https") + .host(API_KR_SBERT) + .port(443) + .path("/predict") + .build(true) + .toUri(); + } + } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java new file mode 100644 index 0000000..1b5873d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/adapter/BatchJobAdapter.java @@ -0,0 +1,238 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.adapter; + +import com.likelion.backendplus4.yakplus.drug.support.JobManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import org.springframework.batch.core.Job; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.out.BatchJobPort; + +import lombok.RequiredArgsConstructor; + +/** + * BatchJobPort ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์ฒด๋กœ, + * Spring Batch Job ๊ฐ์ฒด๋“ค์„ ์ œ์–ดํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * JobManager๋ฅผ ํ†ตํ•ด ๋ฐฐ์น˜ ์ž‘์—…์˜ ์‹คํ–‰, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * JobManager: ์Šคํ”„๋ง ๋ฐฐ์น˜์˜ Job์„ ๊ด€๋ฆฌํ•ด์ฃผ๋Š” ์œ ํ‹ธ ํด๋ž˜์Šค + * Job: ์Šคํ”„๋ง ๋ฐฐ์น˜ ์ž‘์—…์„ ์ •์˜ํ•œ ์ž‘์—… ํด๋ž˜์Šค + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class BatchJobAdapter implements BatchJobPort { + + private final JobManager jobManager; + private final Job drugScrapJob; + private final Job drugDetailScrapJob; + private final Job drugImageScrapJob; + private final Job drugTableCombineJob; + private final Job embedJob; + + /** + * ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ž„๋ฒ ๋”ฉ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String allJobStart() { + return jobManager.startJob(drugScrapJob); + } + + /** + * ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @@author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String allJobStop() { + return jobManager.stopRunningBatch(drugScrapJob); + } + + /** + * ์ค‘๋‹จ๋œ ์ž‘์—…์„ ์กฐํšŒํ•˜๊ณ , ์žฌ๊ฐœํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์žฌ๊ฐœ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String allJobResume() { + return jobManager.restart(); + } + + /** + * ์ „์ฒด ์ž‘์—… ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String allJobStatus() { + return jobManager.getJobStatus(drugScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String detailScrapJobStart() { + return jobManager.startJob(drugDetailScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String detailScrapJobStop() { + return jobManager.stopRunningBatch(drugDetailScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String detailScrapJobStatus() { + return jobManager.getJobStatus(drugDetailScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String imageScrapJobStart() { + return jobManager.startJob(drugImageScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String imageScrapJobStop() { + return jobManager.stopRunningBatch(drugImageScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String imageScrapJobStatus() { + return jobManager.getJobStatus(drugImageScrapJob); + } + + /** + * ์˜์•ฝํ’ˆ ํ…Œ์ด๋ธ” ํ†ตํ•ฉ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String tableCombineJobStart() { + return jobManager.startJob(drugTableCombineJob); + } + + + /** + * ์˜์•ฝํ’ˆ ํ…Œ์ด๋ธ” ํ†ตํ•ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String tableCombineJobStop() { + return jobManager.stopRunningBatch(drugTableCombineJob); + } + + /** + * ์˜์•ฝํ’ˆ ํ…Œ์ด๋ธ” ํ†ตํ•ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String tableCombineJobStatus() { + return jobManager.getJobStatus(drugTableCombineJob); + } + + /** + * ์ฆ์ƒ ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์‹œ์ž‘ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String embedJobStart(){ + return jobManager.startJob(embedJob); + } + + + /** + * ์ฆ์ƒ ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String embedJobStop(){ + return jobManager.stopRunningBatch(embedJob); + } + + /** + * ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public String embedjobStatus(){ + return jobManager.getJobStatus(embedJob); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/ParserBatchException.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/ParserBatchException.java new file mode 100644 index 0000000..9578492 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/ParserBatchException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class ParserBatchException extends CustomException { + private final ErrorCode errorCode; + + public ParserBatchException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java new file mode 100644 index 0000000..f63a806 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/exception/error/ParserBatchError.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * ParserBatchError Enum + * ๋ฐฐ์น˜ ์ž‘์—…์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ ์—๋Ÿฌ ์ฝ”๋“œ๋Š” HTTP ์ƒํƒœ ์ฝ”๋“œ, ์—๋Ÿฌ ์ฝ”๋“œ, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @field httpStatus HTTP ์ƒํƒœ ์ฝ”๋“œ + * @field code ์—๋Ÿฌ ์ฝ”๋“œ + * @field message ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + * @since 2025-05-02 + */ +@RequiredArgsConstructor +public enum ParserBatchError implements ErrorCode { + ALREADY_RUN(HttpStatus.CONFLICT, 450001, "์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ ๋ฐฐ์น˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค."), + JOB_RUN_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 550001, "JOB ์‹คํ–‰ ์š”์ฒญ์€ ์ •์ƒ์ ์œผ๋กœ ๋„๋‹ฌํ–ˆ์œผ๋‚˜ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + JSON_TYPE_CHANGE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 550002, "JSON์„ ์ž๋ฐ” ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + private final HttpStatus httpStatus; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java new file mode 100644 index 0000000..bd1e48e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/CombineJobConfig.java @@ -0,0 +1,35 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.job.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * ์˜์•ฝํ’ˆ ํ…Œ์ด๋ธ” ํ†ตํ•ฉ ์ž‘์—…์„ ์ •์˜ํ•˜๋Š” Spring Batch Job ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * ๊ฐœ๋ณ„ ํ…Œ์ด๋ธ”๋กœ๋ถ€ํ„ฐ ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ ๋‹จ์ผ ํ…Œ์ด๋ธ”๋กœ ๋ณ‘ํ•ฉํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@Configuration +public class CombineJobConfig { + + /** + * ํ…Œ์ด๋ธ” ํ†ตํ•ฉ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” Job์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * ๋‹จ์ผ Step(tableCombineStep)์œผ๋กœ ๊ตฌ์„ฑ๋˜๋ฉฐ, ์˜์•ฝํ’ˆ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository Spring Batch JobRepository + * @param tableCombineStep ์‹ค์ œ ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” Step + * @return Job ๊ตฌ์„ฑ๋œ Job ์ธ์Šคํ„ด์Šค + */ + @Bean + public Job drugTableCombineJob(JobRepository jobRepository, + Step tableCombineStep) { + return new JobBuilder("drugTableCombineJob", jobRepository) + .start(tableCombineStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java new file mode 100644 index 0000000..61366cf --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/DetailJobConfig.java @@ -0,0 +1,43 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.job.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ๋ฐฐ์น˜ ์ž‘์—…์„ ๊ตฌ์„ฑํ•˜๋Š” ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * ์ž‘์—… ์ˆœ์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. + * 1. ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” Step: ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด์˜ ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * 2. ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” Step: ๊ณ„์‚ฐ๋œ ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์„ธ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@Configuration +@RequiredArgsConstructor +public class DetailJobConfig { + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘์„ ์œ„ํ•œ Job์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository Job ์‹คํ–‰ ์ •๋ณด ์ €์žฅ์†Œ + * @param totalPageCheckStep ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ ํ™•์ธ ์Šคํ… + * @param drugDetailStep ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์Šคํ… + * @return Job ๊ตฌ์„ฑ๋œ Job ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Job drugDetailScrapJob(JobRepository jobRepository, + Step totalPageCheckStep, + Step drugDetailStep) { + return new JobBuilder("drugDetailScrapJob", jobRepository) + .start(totalPageCheckStep) + .next(drugDetailStep) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java new file mode 100644 index 0000000..3ea12d2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/job/config/ImageJobConfig.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.job.config; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ํฌ๋กค๋ง ์ž‘์—…์„ ์ •์˜ํ•˜๋Š” Spring Batch Job ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * ์ด ํŽ˜์ด์ง€ ์ˆ˜ ํ™•์ธ ํ›„, ๋ณ‘๋ ฌ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” Step์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@Configuration +public class ImageJobConfig { + + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘์„ ์œ„ํ•œ Job์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + *

+ * Step ์ˆœ์„œ: + *

    + *
  1. imageTotalPageCheckStep: ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐ
  2. + *
  3. imageMasterStep: ๋ณ‘๋ ฌ๋กœ ์ด๋ฏธ์ง€ ํฌ๋กค๋ง ์ˆ˜ํ–‰
  4. + *
+ * + * @param jobRepository Spring Batch JobRepository + * @param imageTotalPageCheckStep ํŽ˜์ด์ง€ ์ˆ˜ ํ™•์ธ Step + * @param imageMasterStep ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ Master Step (partition ๊ธฐ๋ฐ˜) + * @return ๊ตฌ์„ฑ๋œ Job ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Job drugImageScrapJob(JobRepository jobRepository, + Step imageTotalPageCheckStep, + Step imageMasterStep) { + return new JobBuilder("drugImageScrapJob", jobRepository) + .start(imageTotalPageCheckStep) + .next(imageMasterStep) + .build(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java new file mode 100644 index 0000000..07b6f0d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/dto/TableCombineDto.java @@ -0,0 +1,51 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDate; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ‘ํ•ฉํ•œ DTO ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * DrugDetailEntity์™€ ApiDataDrugImgEntity๋ฅผ ์กฐ์ธํ•˜์—ฌ ๊ตฌ์„ฑ๋˜๋ฉฐ, + * ๋ณ‘ํ•ฉ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋˜๋Š” ์ €์žฅ์„ ์œ„ํ•œ ์ค‘๊ฐ„ ๊ตฌ์กฐ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @field drugId ์˜์•ฝํ’ˆ ID + * @field drugName ์˜์•ฝํ’ˆ ์ด๋ฆ„ + * @field company ์ œ์กฐ์‚ฌ + * @field permitDate ํ—ˆ๊ฐ€์ผ + * @field isGeneral ์ผ๋ฐ˜์˜์•ฝํ’ˆ ์—ฌ๋ถ€ + * @field materialInfo ์„ฑ๋ถ„ ์ •๋ณด + * @field storeMethod ๋ณด๊ด€ ๋ฐฉ๋ฒ• + * @field validTerm ์œ ํšจ๊ธฐ๊ฐ„ + * @field efficacy ํšจ๋Šฅ + * @field usage ์‚ฌ์šฉ๋ฒ• + * @field precaution ์ฃผ์˜์‚ฌํ•ญ + * @field cancelDate ์ทจ์†Œ์ผ + * @field cancelName ์ทจ์†Œ ์œ ํ˜•๋ช… + * @field isHerbal ํ•œ์•ฝ ์—ฌ๋ถ€ + * @field productImage ์ œํ’ˆ ์ด๋ฏธ์ง€ URL + * @field pillImage ๋‚ฑ์•Œ ์ด๋ฏธ์ง€ URL + * @since 2025-05-02 + */ +@Getter +@AllArgsConstructor +public class TableCombineDto { + private Long drugId; + private String drugName; + private String company; + private LocalDate permitDate; + private boolean isGeneral; + private String materialInfo; + private String storeMethod; + private String validTerm; + private String efficacy; + private String usage; + private String precaution; + private LocalDate cancelDate; + private String cancelName; + private Boolean isHerbal; + private String productImage; + private String pillImage; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java new file mode 100644 index 0000000..4677854 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/processor/TableCombineProcessor.java @@ -0,0 +1,264 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.processor; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto.TableCombineDto; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.StreamSupport; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * TableCombineDto๋ฅผ DrugRawDataEntity๋กœ ๋ณ€ํ™˜ํ•˜๋Š” Spring Batch ItemProcessor์ž…๋‹ˆ๋‹ค. + *

+ * ์ƒ์„ธ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ตœ์ข… ์ €์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ๊ฐ€๊ณตํ•˜๋ฉฐ, + * ํšจ๋Šฅ, ์‚ฌ์šฉ๋ฒ•, ์ฃผ์˜์‚ฌํ•ญ, ์„ฑ๋ถ„ ์ •๋ณด๋Š” JSON ํŒŒ์‹ฑ ๋ฐ ๊ตฌ์กฐ ๋ณ€ํ™˜์„ ํ†ตํ•ด ์ •์ œ๋ฉ๋‹ˆ๋‹ค. + * + * @modified 2025-05-02 ํ•จ์˜ˆ์ • + * - ์Šคํ”„๋ง ๋ฐฐ์น˜๋กœ ์ˆ˜์ • + * @since 2025-04-21 + */ +@Component +@RequiredArgsConstructor +public class TableCombineProcessor implements ItemProcessor { + + /** + * TableCombineDto๋ฅผ DrugRawDataEntity๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + *

+ * ์ด๋ฏธ์ง€ ์ •๋ณด๋Š” productImage๊ฐ€ ์œ ํšจํ•  ๊ฒฝ์šฐ ์šฐ์„  ์‚ฌ์šฉํ•˜๋ฉฐ, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด pillImage๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + * ํšจ๋Šฅ, ์‚ฌ์šฉ๋ฒ•, ์ฃผ์˜์‚ฌํ•ญ, ์„ฑ๋ถ„ ์ •๋ณด๋Š” JSON ๋ฌธ์ž์—ด์„ ๊ตฌ์กฐํ™”๋œ ํ˜•ํƒœ๋กœ ํŒŒ์‹ฑ ํ›„ ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param dto ๋ณ‘ํ•ฉ๋œ ์˜์•ฝํ’ˆ ์ƒ์„ธ + ์ด๋ฏธ์ง€ DTO + * @return DrugRawDataEntity ์ €์žฅ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ์˜ ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public DrugRawDataEntity process(TableCombineDto dto) { + String imgUrl = getCoverImageFromProductAndPill(dto); + + return DrugRawDataEntity.builder() + .drugId(dto.getDrugId()) + .drugName(dto.getDrugName()) + .company(dto.getCompany()) + .permitDate(dto.getPermitDate()) + .isGeneral(dto.isGeneral()) + .materialInfo(toStringFromObj(convertMaterialInfo(dto.getMaterialInfo()))) + .storeMethod(dto.getStoreMethod()) + .validTerm(dto.getValidTerm()) + .efficacy(toStringFromObj(convertEfficacy(dto.getEfficacy()))) + .usage(toStringFromObj(getUsage(dto.getUsage()))) + .precaution(toStringFromObj(getPrecaution(dto.getPrecaution()))) + .imageUrl(imgUrl) + .cancelDate(dto.getCancelDate()) + .cancelName(dto.getCancelName()) + .isHerbal(dto.getIsHerbal()) + .build(); + } + + /** + * ์ œํ’ˆ ์ด๋ฏธ์ง€๊ฐ€ ์œ ํšจํ•  ๊ฒฝ์šฐ ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์•Œ์•ฝ ์ด๋ฏธ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param dto TableCombineDto ๋ณ‘ํ•ฉ๋œ ์˜์•ฝํ’ˆ ์ƒ์„ธ + ์ด๋ฏธ์ง€ DTO + */ + private String getCoverImageFromProductAndPill(TableCombineDto dto) { + String imgUrl = (dto.getProductImage() != null && dto.getProductImage().length() > 10) + ? dto.getProductImage() + : dto.getPillImage(); + return imgUrl; + } + + /** + * ์‚ฌ์šฉ๋ฒ• ํ…์ŠคํŠธ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ๋ฌธ๋‹จ๋ณ„ ์„ค๋ช… ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param usage ์‚ฌ์šฉ๋ฒ• JSON ๋ฌธ์ž์—ด + * @return List ์‚ฌ์šฉ๋ฒ• ๋ฌธ์žฅ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private List getUsage(String usage) { + JsonNode json = toJsonNodeFromString(usage); + + if (json.isNull() || !json.has("sections")) { + return Collections.emptyList(); + } + + return StreamSupport.stream(json.get("sections").spliterator(), false) + .flatMap(section -> StreamSupport.stream(section.get("articles").spliterator(), false)) + .flatMap(article -> StreamSupport.stream(article.get("paragraphs").spliterator(), false)) + .map(paragraph -> paragraph.get("text").asText()) + .toList(); + } + + /** + * ์„ฑ๋ถ„ ์ •๋ณด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param material ์„ฑ๋ถ„ ์ •๋ณด JSON ๋ฌธ์ž์—ด + * @return List ์„ฑ๋ถ„ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private List convertMaterialInfo(String material) { + JsonNode json = toJsonNodeFromString(material); + if (json.isArray()) { + return mapFromMaterialJson(json); + } + return null; + } + + /** + * JSON ๋…ธ๋“œ๋ฅผ Material ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param json JSON ๋…ธ๋“œ + * @return List ์„ฑ๋ถ„ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private List mapFromMaterialJson(JsonNode json) { + List materials = new ArrayList<>(); + + ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try { + for (JsonNode node : json) { + Material ingredient = objectMapper.treeToValue(node, Material.class); + materials.add(ingredient); + } + return materials; + } catch (Exception e) { + log(LogLevel.ERROR, "๊ฐ์ฒด ๋งตํ•‘ ์‹คํŒจ", e); + return null; + } + } + + /** + * JSON ๋ฌธ์ž์—ด์„ JsonNode ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param json JSON ๋ฌธ์ž์—ด + * @return JsonNode ๋ณ€ํ™˜๋œ JSON ๋…ธ๋“œ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private JsonNode toJsonNodeFromString(String json) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readTree(json); + } catch (Exception e) { + log(LogLevel.ERROR, "json ๊ฐ์ฒด ์ƒ์„ฑ ์—๋Ÿฌ", e); + return null; + } + } + + /** + * ํšจ๋Šฅ ์ •๋ณด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param efficacyJsonString ํšจ๋Šฅ ์ •๋ณด JSON ๋ฌธ์ž์—ด + * @return List ํšจ๋Šฅ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private List convertEfficacy(String efficacyJsonString) { + JsonNode json = toJsonNodeFromString(efficacyJsonString); + List efficacy = new ArrayList<>(); + tryParseParagraphs(json, efficacy); + + if (efficacy.isEmpty()) { + tryParseTitle(json, efficacy); + } + return efficacy; + } + + /** + * JSON ๋…ธ๋“œ์—์„œ ์ œ๋ชฉ์„ ์ถ”์ถœํ•˜์—ฌ ํšจ๋Šฅ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param json JSON ๋…ธ๋“œ + * @param efficacy ํšจ๋Šฅ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private void tryParseTitle(JsonNode json, List efficacy) { + if (json.has("sections")) { + for (JsonNode section : json.get("sections")) { + for (JsonNode article : section.get("articles")) { + efficacy.add(article.get("title").asText()); + } + } + } + } + + /** + * JSON ๋…ธ๋“œ์—์„œ ๋ฌธ๋‹จ์„ ์ถ”์ถœํ•˜์—ฌ ํšจ๋Šฅ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param json JSON ๋…ธ๋“œ + * @param efficacy ํšจ๋Šฅ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private void tryParseParagraphs(JsonNode json, List efficacy) { + if (json.has("sections")) { + List parsed = StreamSupport.stream(json.get("sections").spliterator(), false) + .flatMap(section -> StreamSupport.stream(section.get("articles").spliterator(), false)) + .flatMap(article -> StreamSupport.stream(article.get("paragraphs").spliterator(), false)) + .map(paragraph -> paragraph.get("text").asText()) + .filter(text -> text != null && !text.isEmpty()) + .toList(); + + efficacy.addAll(parsed); + } + } + + /** + * ์ฃผ์˜์‚ฌํ•ญ ์ •๋ณด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param precaution ์ฃผ์˜์‚ฌํ•ญ JSON ๋ฌธ์ž์—ด + * @return Map> ์ฃผ์˜์‚ฌํ•ญ ๋งต + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private Map> getPrecaution(String precaution) { + Map> result = new LinkedHashMap<>(); + + JsonNode json = toJsonNodeFromString(precaution); + if (json.has("sections")) { + JsonNode articles = json.get("sections").get(0).get("articles"); + for (JsonNode article : articles) { + String title = article.get("title").asText(); + List texts = new ArrayList<>(); + for (JsonNode paragraph : article.get("paragraphs")) { + texts.add(paragraph.get("text").asText()); + } + result.put(title, texts); + } + } + + return result; + } + + /** + * ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param obj ๋ณ€ํ™˜ํ•  ๊ฐ์ฒด + * @return JSON ๋ฌธ์ž์—ด + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private String toStringFromObj(Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + log(LogLevel.ERROR, "JSON ๋ณ€ํ™˜ ์—๋Ÿฌ", e); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java new file mode 100644 index 0000000..124b335 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/combine/writer/TableCombineWriter.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.writer; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * DrugRawDataEntity ๋ฆฌ์ŠคํŠธ๋ฅผ JPA๋ฅผ ํ†ตํ•ด ์ €์žฅํ•˜๋Š” Spring Batch ItemWriter ๊ตฌํ˜„์ฒด์ž…๋‹ˆ๋‹ค. + *

+ * ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ํ›„ ์ƒ์„ฑ๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ DB์— ์ผ๊ด„ ์ €์žฅํ•˜๋ฉฐ, ์ฒ˜๋ฆฌ ๊ฑด์ˆ˜๋ฅผ ๋กœ๊ทธ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @fields drugRawDataRepository JPA๋ฅผ ํ†ตํ•ด DrugRawDataEntity๋ฅผ ์ €์žฅํ•˜๋Š” ๋ ˆํฌ์ง€ํ† ๋ฆฌ + * @fields count ์ €์žฅ๋œ ์—”ํ‹ฐํ‹ฐ์˜ ๊ฐœ์ˆ˜๋ฅผ ์„ธ๊ธฐ ์œ„ํ•œ AtomicInteger + * @since 2025-05-02 + */ +@Component +@StepScope +@RequiredArgsConstructor +public class TableCombineWriter implements ItemWriter { + private final DrugJpaRepository drugRawDataRepository; + private final AtomicInteger count = new AtomicInteger(); + + /** + * ๋ณ‘ํ•ฉ๋œ DrugRawDataEntity ๋ฆฌ์ŠคํŠธ๋ฅผ JPA๋ฅผ ํ†ตํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param entity Chunk ๋‹จ์œ„๋กœ ์ „๋‹ฌ๋œ ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก + * @since 2025-05-02 + * @author ํ•จ์˜ˆ์ • + */ + @Override + public void write(Chunk entity) { + List items = new ArrayList<>(entity.getItems()); + drugRawDataRepository.saveAll(items); + log("ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—… - ์“ฐ๊ธฐ ์™„๋ฃŒ: " + count.addAndGet(items.size())); + + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java new file mode 100644 index 0000000..3f533de --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/CombineStepConfig.java @@ -0,0 +1,141 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto.TableCombineDto; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import jakarta.persistence.EntityManagerFactory; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ DrugRawDataEntity๋กœ ์ €์žฅํ•˜๋Š” Step ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * DrugDetailEntity์™€ ApiDataDrugImgEntity๋ฅผ ์กฐ์ธํ•˜์—ฌ TableCombineDto๋กœ ์ฝ๊ณ , + * ์ด๋ฅผ DrugRawDataEntity๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @field combineStepName ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ Step์˜ ์ด๋ฆ„ + * @field drugDetailReaderName ItemReader์˜ ์ด๋ฆ„ + * @field tableCombineEntityFqn ๋ณ‘ํ•ฉ ํ…Œ์ด๋ธ” DTO์˜ ๊ฒฝ๋กœ Full Path + * @field drugDetailEntity ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด Entity ํด๋ž˜์Šค๋ช… + * @field drugImageEntity ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ Entity ํด๋ž˜์Šค๋ช… + * @field detailAlias ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด Entity์˜ ๋ณ„์นญ + * @field imageAlias ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ Entity์˜ ๋ณ„์นญ + * @field RETRY_LIMIT ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ œํ•œ ์„ค์ • ๊ฐ’ + * @field SKIP_LIMIT ์Šคํ‚ต ํšŸ์ˆ˜ ์ œํ•œ ์„ค์ • ๊ฐ’ + * @field PAGE_SIZE ํŽ˜์ด์ง€ ํฌ๊ธฐ + * @since 2025-05-02 + */ +@Configuration +public class CombineStepConfig { + private static final String COMBINE_STEP_NAME = "drugCombineStep"; + private static final String DRUG_DETAIL_READER_NAME = "drugDetailReader"; + + private static final String TABLE_COMBINE_ENTITY_FQN = + "com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.combine.dto.TableCombineDto"; + private static final String DRUG_DETAIL_ENTITY = "DrugDetailEntity"; + private static final String DRUG_IMAGE_ENTITY = "DrugImgEntity"; + private static final String DETAIL_ALIAS = "d"; + private static final String IMAGE_ALIAS = "i"; + private static final int RETRY_LIMIT = 3; + private static final int SKIP_LIMIT = 5_000; + private static final int PAGE_SIZE = 1_000; + + /** + * ๋ณ‘ํ•ฉ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” Step์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + *

+ * TableCombineDto โ†’ DrugRawDataEntity๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅํ•˜๋ฉฐ, ์˜ˆ์™ธ ํ—ˆ์šฉ ์ •์ฑ…๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. + * + * @param jobRepository Job ์ €์žฅ์†Œ + * @param transactionManager ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ์ž + * @param reader ๋ณ‘ํ•ฉ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” Reader + * @param processor ๋ณ‘ํ•ฉ ๋ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ๊ธฐ + * @param writer ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” Writer + * @return ๊ตฌ์„ฑ๋œ Step + */ + @Bean + public Step tableCombineStep( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + ItemReader reader, + ItemProcessor processor, + ItemWriter writer) { + return new StepBuilder(COMBINE_STEP_NAME, jobRepository) + .chunk(PAGE_SIZE, transactionManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(RETRY_LIMIT) + .skip(Exception.class) + .skipLimit(SKIP_LIMIT) + .build(); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ๋ณ‘ํ•ฉํ•˜์—ฌ TableCombineDto๋กœ ์ฝ์–ด์˜ค๋Š” Reader์ž…๋‹ˆ๋‹ค. + *

+ * DrugDetailEntity์™€ ApiDataDrugImgEntity๋ฅผ LEFT JOINํ•˜์—ฌ ํ•„์š”ํ•œ ํ•„๋“œ๋ฅผ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityManagerFactory JPA EntityManagerFactory + * @return JpaPagingItemReader JPA ํŽ˜์ด์ง• ItemReader + */ + @Bean + @StepScope + public JpaPagingItemReader drugDetailReader(EntityManagerFactory entityManagerFactory) { + JpaPagingItemReader reader = new JpaPagingItemReader<>(); + reader.setEntityManagerFactory(entityManagerFactory); + reader.setQueryString(getJoinTableSql()); + reader.setPageSize(PAGE_SIZE); + reader.setSaveState(true); + reader.setName(DRUG_DETAIL_READER_NAME); + return reader; + } + + /** + * DrugDetailEntity์™€ ApiDataDrugImgEntity๋ฅผ ์กฐ์ธํ•˜์—ฌ TableCombineDto๋ฅผ ์ƒ์„ฑํ•˜๋Š” JPQL ์ฟผ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ‘ํ•ฉ์„ ์œ„ํ•œ JPQL ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด + */ + private String getJoinTableSql() { + return String.format(""" + SELECT new %s( + %s.drugId, + %s.drugName, + %s.company, + %s.permitDate, + %s.isGeneral, + %s.materialInfo, + %s.storeMethod, + %s.validTerm, + %s.efficacy, + %s.usage, + %s.precaution, + %s.cancelDate, + %s.cancelName, + %s.isHerbal, + %s.productImage, + %s.pillImage + ) + FROM %s %s + LEFT JOIN %s %s ON %s.drugId = %s.drugId + """, + TABLE_COMBINE_ENTITY_FQN, + DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, + DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, + DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, DETAIL_ALIAS, + IMAGE_ALIAS, IMAGE_ALIAS, + DRUG_DETAIL_ENTITY, DETAIL_ALIAS, + DRUG_IMAGE_ENTITY, IMAGE_ALIAS, + DETAIL_ALIAS, IMAGE_ALIAS + ); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java new file mode 100644 index 0000000..0478d61 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/DetailStepConfig.java @@ -0,0 +1,120 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor.DetailTotalPageCalculator; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor.DrugDetailProcessor; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.reader.DetailPageNumberReader; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.writer.DrugDetailWriter; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.List; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘์„ ์œ„ํ•œ Spring Batch ์„ค์ • ํด๋ž˜์Šค + *

+ * DetailPageNumberReader: ์ƒ์„ธ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ฝ์–ด์˜ค๋Š” Reader + * ApiRequestManager: API ์š”์ฒญ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค + * ApiResponseMapper: API ์‘๋‹ต์„ ๋งคํ•‘ํ•˜๋Š” ํด๋ž˜์Šค + * TaskExecutor: ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ TaskExecutor + * + * @since 2025-05-02 + */ +@Configuration +public class DetailStepConfig { + private final String executorName = "normalExecutor"; + + private final DetailPageNumberReader detailPageNumberReader; + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + private final TaskExecutor taskExecutor; + + public DetailStepConfig(DetailPageNumberReader detailPageNumberReader, + ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper, + @Qualifier(executorName) + TaskExecutor taskExecutor) { + this.detailPageNumberReader = detailPageNumberReader; + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + this.taskExecutor = taskExecutor; + } + + /** + * ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” Tasklet ๊ธฐ๋ฐ˜ Step ์ •์˜ + * + * @param jobRepository JobRepository ์ธ์Šคํ„ด์Šค + * @param txManager ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param detailTotalPageCalculator ํŽ˜์ด์ง€ ๊ณ„์‚ฐ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” Tasklet + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-01 + */ + @Bean + Step totalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, + DetailTotalPageCalculator detailTotalPageCalculator) { + return new StepBuilder("totalPageCheck", jobRepository) + .tasklet(detailTotalPageCalculator, txManager) + .build(); + } + + /** + * ์ƒ์„ธ ํŽ˜์ด์ง€๋ณ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ์ฒ˜๋ฆฌ ๋ฐ ์ €์žฅํ•˜๋Š” Chunk ๊ธฐ๋ฐ˜ Step ์ •์˜ + * + * @param jobRepository JobRepository ์ธ์Šคํ„ด์Šค + * @param txManager ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param processor ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” Processor + * @param writer ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” Writer + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-01 + */ + @Bean + public Step drugDetailStep(JobRepository jobRepository, + PlatformTransactionManager txManager, + DrugDetailProcessor processor, + DrugDetailWriter writer) { + return new StepBuilder("drugDetailStep", jobRepository) + .>chunk(1, txManager) + .reader(detailPageNumberReader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .taskExecutor(taskExecutor) + .build(); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ฒ˜๋ฆฌ์šฉ Processor Bean ์ •์˜ + * + * @return CombineProcessor ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-01 + */ + @Bean + public DrugDetailProcessor processor() { + return new DrugDetailProcessor(apiRequestManager, apiResponseMapper); + } + + /** + * ์ฒ˜๋ฆฌ๋œ ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด๋ฅผ DB์— ์ €์žฅํ•˜๋Š” Writer Bean ์ •์˜ + * + * @param repository ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ €์žฅ์šฉ JPA Repository + * @return DrugDetailWriter ์ธ์Šคํ„ด์Šค + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-01 + */ +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java new file mode 100644 index 0000000..b5f52cd --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/config/ImageStepConfig.java @@ -0,0 +1,135 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.config; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor.ImageScrapProcessor; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor.ImageTotalPageCalculator; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader.PageRangePartitioner; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader.PartitionedPageReader; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.writer.DrugImageWriter; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugImgRepository; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.List; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘์„ ์œ„ํ•œ Spring Batch ์„ค์ • ํด๋ž˜์Šค + * + * @field imageTotalPageCheckStep ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” Step + * @field imageMasterStep ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘์„ ์œ„ํ•œ Master Step + * @field imageScrapStep ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘์„ ์œ„ํ•œ Step + * @since 2025-05-02 + */ +@Configuration +public class ImageStepConfig { + private static final String TASK_EXECUTOR = "singleItemExecutor"; + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + private final TaskExecutor taskExecutor; + + public ImageStepConfig(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper, + @Qualifier(TASK_EXECUTOR) + TaskExecutor taskExecutor) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + this.taskExecutor = taskExecutor; + } + + + /** + * ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” Tasklet ๊ธฐ๋ฐ˜ Step ์ •์˜ + * + * @param jobRepository JobRepository ์ธ์Šคํ„ด์Šค + * @param txManager ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param imageTotalPageCalculator ํŽ˜์ด์ง€ ๊ณ„์‚ฐ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” Tasklet + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + Step imageTotalPageCheckStep(JobRepository jobRepository, PlatformTransactionManager txManager, + ImageTotalPageCalculator imageTotalPageCalculator) { + return new StepBuilder("imageTotalPageCheck", jobRepository) + .tasklet(imageTotalPageCalculator, txManager) + .build(); + } + + @Bean + public Step imageMasterStep(JobRepository jobRepository, + PageRangePartitioner partitioner, + Step imageScrapStep) { + return new StepBuilder("imageMasterStep", jobRepository) + .partitioner(imageScrapStep.getName(), partitioner) + .step(imageScrapStep) + .gridSize(15) + .taskExecutor(taskExecutor) + .build(); + } + + /** + * ์ƒ์„ธ ํŽ˜์ด์ง€๋ณ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ์ฒ˜๋ฆฌ ๋ฐ ์ €์žฅํ•˜๋Š” Chunk ๊ธฐ๋ฐ˜ Step ์ •์˜ + * + * @param jobRepository JobRepository ์ธ์Šคํ„ด์Šค + * @param txManager ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € + * @param processor ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” Processor + * @param writer ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” Writer + * @return Step ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public Step imageScrapStep(JobRepository jobRepository, + PlatformTransactionManager txManager, + ImageScrapProcessor processor, + DrugImageWriter writer, + PartitionedPageReader reader) { + return new StepBuilder("imageScrapStep", jobRepository) + .>chunk(1, txManager) + .reader(reader) + .processor(processor) + .writer(writer) + .faultTolerant() + .retry(Exception.class) + .retryLimit(3) + .skip(Exception.class) + .skipLimit(Integer.MAX_VALUE) + .build(); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ฒ˜๋ฆฌ์šฉ Processor Bean ์ •์˜ + * + * @return CombineProcessor ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public ImageScrapProcessor imageScrapProcessor() { + return new ImageScrapProcessor(apiRequestManager, apiResponseMapper); + } + + /** + * ์ฒ˜๋ฆฌ๋œ ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด๋ฅผ DB์— ์ €์žฅํ•˜๋Š” Writer Bean ์ •์˜ + * + * @param repository ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ €์žฅ์šฉ JPA Repository + * @return DrugDetailWriter ์ธ์Šคํ„ด์Šค + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Bean + public DrugImageWriter imageScrapWriter(DrugImgRepository repository) { + return new DrugImageWriter(repository); + } + + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java new file mode 100644 index 0000000..b6377c9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/dto/DrugDetailRequest.java @@ -0,0 +1,106 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDate; + + +/** + * ObjectMapper๋ฅผ ํ†ตํ•ด JSON ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ๋งคํ•‘ํ•˜๊ธฐ ์œ„ํ•œ DTO ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด ์ˆ˜์ง‘ ๋ฐฐ์น˜ ์ž‘์—…์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + *

+ * ์ผ๋ถ€ ํ•„๋“œ๋Š” @JsonProperty๋ฅผ ํ†ตํ•ด ์™ธ๋ถ€ ํ•„๋“œ๋ช…๊ณผ ๋งคํ•‘๋˜๋ฉฐ, + * ์ƒ์„ฑ์ž์—์„œ๋Š” ์ผ๋ฐ˜์˜์•ฝํ’ˆ ์—ฌ๋ถ€(isGeneral)๋ฅผ ETC_OTC_CODE ๊ฐ’์œผ๋กœ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @field drugId ์˜์•ฝํ’ˆ ID + * @field drugName ์˜์•ฝํ’ˆ ์ด๋ฆ„ + * @field company ์ œ์กฐ์‚ฌ/ํŒ๋งค์‚ฌ ์ด๋ฆ„ + * @field permitDate ํ—ˆ๊ฐ€์ผ + * @field cancelDate ์ทจ์†Œ์ผ + * @field cancelName ์ทจ์†Œ ์œ ํ˜•๋ช… + * @field isGeneral ์ผ๋ฐ˜์˜์•ฝํ’ˆ ์—ฌ๋ถ€ + * @field isHerbal ํ•œ์•ฝ์žฌ ์—ฌ๋ถ€ + * @field materialInfo ์„ฑ๋ถ„ ์ •๋ณด + * @field storeMethod ๋ณด๊ด€ ๋ฐฉ๋ฒ• + * @field validTerm ์œ ํšจ๊ธฐ๊ฐ„ + * @field efficacy ํšจ๋Šฅ + * @field usage ์‚ฌ์šฉ๋ฒ• + * @field precaution ์ฃผ์˜์‚ฌํ•ญ + * @since 2025-04-21 + */ +@Getter +@ToString +public class DrugDetailRequest { + + @JsonProperty("ITEM_SEQ") + private Long drugId; + + @JsonProperty("ITEM_NAME") + private String drugName; + + @JsonProperty("ENTP_NAME") + private String company; + + @JsonProperty("ITEM_PERMIT_DATE") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") + private LocalDate permitDate; + + @JsonProperty("CANCEL_DATE") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") + private LocalDate cancelDate; + + @JsonProperty("CANCEL_NAME") + private String cancelName; + + private boolean isGeneral; + + private boolean isHerbal; + + private String materialInfo; + + @JsonProperty("STORAGE_METHOD") + private String storeMethod; + + @JsonProperty("VALID_TERM") + private String validTerm; + + private String efficacy; + private String usage; + private String precaution; + + /** + * ETC_OTC_CODE ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ๋ฐ˜์˜์•ฝํ’ˆ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugType "์ „๋ฌธ์˜์•ฝํ’ˆ"์ด๋ฉด false, ๊ทธ ์™ธ๋Š” true + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + @JsonCreator + public DrugDetailRequest(@JsonProperty("ETC_OTC_CODE") String drugType) { + this.isGeneral = !"์ „๋ฌธ์˜์•ฝํ’ˆ".equals(drugType); + } + + public void changeMaterialInfo(String materialInfo) { + this.materialInfo = materialInfo; + } + + public void changeUsage(String usage) { + this.usage = usage; + } + + public void changeEfficacy(String efficacy) { + this.efficacy = efficacy; + } + + public void changePrecaution(String precaution) { + this.precaution = precaution; + } + + public void changeIsHerbal(boolean isHerbal) { + this.isHerbal = isHerbal; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java new file mode 100644 index 0000000..a46702f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DetailTotalPageCalculator.java @@ -0,0 +1,44 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.reader.DetailPageNumberReader; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘์„ ์œ„ํ•œ ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” Tasklet์ž…๋‹ˆ๋‹ค. + * + * ApiRequestManager: API ์š”์ฒญ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class DetailTotalPageCalculator implements Tasklet { + private final ApiRequestManager apiRequestManager; + + /** + * ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ API์—์„œ ์กฐํšŒํ•œ ํ›„, DetailPageNumberReader์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * DetailPageNumberReader: ์ƒ์„ธ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ฝ์–ด์˜ค๋Š” Reader + * + * @param contribution ํ˜„์žฌ step์˜ ๊ธฐ์—ฌ๋„ + * @param chunkContext ํ˜„์žฌ chunk ์‹คํ–‰ ์ปจํ…์ŠคํŠธ + * @return RepeatStatus.FINISHED (์ž‘์—… ์™„๋ฃŒ ์‹ ํ˜ธ) + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int totalPage = apiRequestManager.getDetailTotalPage(); + DetailPageNumberReader.setTotalPage(totalPage); + + log("[CombineProcessor] ์ด ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ ์™„๋ฃŒ: " + totalPage); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java new file mode 100644 index 0000000..f5e2c37 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/processor/DrugDetailProcessor.java @@ -0,0 +1,109 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.processor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser.MaterialParser; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser.XMLParser; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.DrugDetailRequestMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +import org.springframework.batch.item.ItemProcessor; + +import java.util.List; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด API ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•˜๋Š” ItemProcessor ๊ตฌํ˜„์ฒด์ž…๋‹ˆ๋‹ค. + * ์ž…๋ ฅ์œผ๋กœ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ๋ฐ›์•„ ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ฅผ API๋กœ ์กฐํšŒํ•˜๊ณ , + * XML ๋ฐ ๊ธฐํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ์„ฑ๋ถ„, ํšจ๋Šฅ, ์‚ฌ์šฉ๋ฒ•, ์ฃผ์˜์‚ฌํ•ญ ๋“ฑ์˜ ์„ธ๋ถ€ ํ•ญ๋ชฉ์„ ํŒŒ์‹ฑํ•˜๋ฉฐ, + * ํ•œ์•ฝ ์—ฌ๋ถ€๋Š” ์ฃผ์˜์‚ฌํ•ญ ํ…์ŠคํŠธ์— ํŠน์ • ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + *

+ * ApiRequestManager: API ์š”์ฒญ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค + * ApiResponseMapper: API ์‘๋‹ต์„ ๋งคํ•‘ํ•˜๋Š” ํด๋ž˜์Šค + * + * @since 2025-05-02 + */ +public class DrugDetailProcessor implements ItemProcessor> { + private final String materialTagName = "MATERIAL_NAME"; + private final String efficacyTagName = "EE_DOC_DATA"; + private final String usageTagName = "UD_DOC_DATA"; + private final String precautionTagName = "NB_DOC_DATA"; + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + + public DrugDetailProcessor(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + } + + /** + * ์ž…๋ ฅ๋œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์— ํ•ด๋‹นํ•˜๋Š” ์ƒ์„ธ API ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , + * ํ•„์š”ํ•œ ํ•„๋“œ๋ฅผ ํŒŒ์‹ฑ ๋ฐ ๋ณ€ํ™˜ํ•˜์—ฌ ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + *

+ * ์ž‘์—… ์ˆœ์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค: + * 1) API ์š”์ฒญ์„ ํ†ตํ•ด JSON ์‘๋‹ต์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * 2) JSON ์‘๋‹ต์—์„œ ํ•„์š”ํ•œ ํ•ญ๋ชฉ์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * 3) ๊ฐ ํ•ญ๋ชฉ์— ๋Œ€ํ•ด XML ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. + * 4) ์„ฑ๋ถ„, ํšจ๋Šฅ, ์‚ฌ์šฉ๋ฒ•, ์ฃผ์˜์‚ฌํ•ญ์„ ํŒŒ์‹ฑํ•˜์—ฌ DTO์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * 5) ์ฃผ์˜์‚ฌํ•ญ์— ํ•œ์•ฝ ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์—ฌ ํ”Œ๋ž˜๊ทธ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * 6) DTO๋ฅผ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageNumber API ์กฐํšŒ์— ์‚ฌ์šฉํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @return ๋ณ€ํ™˜๋œ DrugDetailEntity ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public List process(Integer pageNumber) { + + String response = apiRequestManager.fetchDetailData(pageNumber); + JsonNode items = apiRequestManager.getItemsFromResponse(response); + List drugItems = apiResponseMapper.toListFromDrugDetails(items); + + for (int i = 0; i < drugItems.size(); i++) { + DrugDetailRequest drugDetail = drugItems.get(i); + JsonNode item = items.get(i); + + String materialRawData = item.get(materialTagName).asText(); + String materialInfo = MaterialParser.parseMaterial(materialRawData); + + drugDetail.changeMaterialInfo(materialInfo); + + String efficacyXmlText = item.get(efficacyTagName).asText(); + String efficacy = XMLParser.toJson(efficacyXmlText); + drugDetail.changeEfficacy(efficacy); + + String usageXmlText = item.get(usageTagName).asText(); + String usages = XMLParser.toJson(usageXmlText); + drugDetail.changeUsage(usages); + + String precautionXmlText = item.get(precautionTagName).asText(); + String precautions = XMLParser.toJson(precautionXmlText); + drugDetail.changePrecaution(precautions); + + String precaution = drugDetail.getPrecaution(); + if (isContainHerbalText(precaution)) { + drugDetail.changeIsHerbal(true); + } + } + return drugItems.stream() + .map(DrugDetailRequestMapper::toEntityFromRequest) + .toList(); + } + + /** + * ์ฃผ์˜์‚ฌํ•ญ ํ…์ŠคํŠธ์— ํ•œ์•ฝ ๊ด€๋ จ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param precaution ์ฃผ์˜์‚ฌํ•ญ ํ…์ŠคํŠธ + * @return boolean + * true: ํฌํ•จ๋จ, false: ํฌํ•จ๋˜์ง€ ์•Š์Œ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private static boolean isContainHerbalText(String precaution) { + return precaution != null && (precaution.contains("ํ•œ์˜์‚ฌ") || precaution.contains("ํ•œ์•ฝ์‚ฌ")); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java new file mode 100644 index 0000000..8bf8b5d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/reader/DetailPageNumberReader.java @@ -0,0 +1,47 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.reader; + +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import lombok.Getter; +import org.springframework.batch.item.ItemReader; +import org.springframework.stereotype.Component; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Spring Batch์—์„œ ๊ฐ Step ์‹คํ–‰ ์‹œ ์ฒ˜๋ฆฌํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” Reader ํด๋ž˜์Šค + *

+ * ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์—ฌ ์ฐจ๋ก€๋Œ€๋กœ page ๋ฒˆํ˜ธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, + * ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ๋ฐ˜ํ™˜๋˜๋ฉด null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋ฐ˜๋ณต์„ ์ข…๋ฃŒํ•œ๋‹ค. + * + * @field pageQueue ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ €์žฅํ•˜๋Š” ํ + * @since 2025-05-02 + */ +@Component +@Getter +public class DetailPageNumberReader implements ItemReader { + + private static final Queue pageQueue = new ConcurrentLinkedQueue<>(); + + public static void setTotalPage(int totalPage) { + pageQueue.clear(); + for (int i = 1; i <= totalPage; i++) { + pageQueue.add(i); + } + } + + /** + * ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๋‹ค์Œ ํ˜ธ์ถœ์„ ์œ„ํ•ด ๋‚ด๋ถ€ ์นด์šดํ„ฐ๋ฅผ ์ฆ๊ฐ€์‹œํ‚จ๋‹ค. + * ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ข…๋ฃŒ๋ฅผ ์•Œ๋ฆฐ๋‹ค. + * + * @return ํ˜„์žฌ ์ฒ˜๋ฆฌํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๋˜๋Š” null(๋ชจ๋“  ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ์‹œ) + */ + @Override + public Integer read() { + Integer page = pageQueue.poll(); + if (page != null) { + LogUtil.log(Thread.currentThread().getName() + " - Page ํ• ๋‹น: " + page); + } + return page; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java new file mode 100644 index 0000000..6382ef2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/detail/writer/DrugDetailWriter.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.writer; + +import java.util.List; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugDetailJpaRepository; + +import lombok.RequiredArgsConstructor; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด Entity ๋ฆฌ์ŠคํŠธ๋ฅผ JPA Repository๋ฅผ ํ†ตํ•ด ์ผ๊ด„ ์ €์žฅํ•˜๋Š” Writer์ž…๋‹ˆ๋‹ค. + *

+ * Chunk ๋‚ด๋ถ€์˜ ๊ฐ List ํ•ญ๋ชฉ์„ ๋ฐ˜๋ณต ์ฒ˜๋ฆฌํ•˜๋ฉฐ, + * ๊ฐ ๋ฆฌ์ŠคํŠธ๋Š” ํ•œ API ํŽ˜์ด์ง€์—์„œ ํŒŒ์‹ฑ๋œ ๋‹ค์ˆ˜์˜ ์˜์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + *

+ * DrugDetailJpaRepository: ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” JPA Repository + * + * @field repository ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” JPA Repository + * @since 2025-05-02 + */ + +@Component +@RequiredArgsConstructor +public class DrugDetailWriter implements ItemWriter> { + + private final DrugDetailJpaRepository repository; + /** + * Chunk ๋‹จ์œ„๋กœ ์ „๋‹ฌ๋œ Entity ๋ฆฌ์ŠคํŠธ๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ JPA๋ฅผ ํ†ตํ•ด ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param chunk ํŽ˜์ด์ง€๋ณ„๋กœ ์ˆ˜์ง‘๋œ ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public void write(Chunk> chunk) { + + for (List items : chunk.getItems()) { + repository.saveAll(items); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java new file mode 100644 index 0000000..7921da2 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageScrapProcessor.java @@ -0,0 +1,74 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor; + +import java.util.List; + +import org.springframework.batch.item.ItemProcessor; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper.ApiResponseMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.DrugImageRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugImageRequestMapper; + +/** + * pageNumber๋ฅผ ๋ฐ›์•„ + * ์™ธ๋ถ€ REST API ํ˜ธ์ถœ โ†’ JSON โ†’ DTO ๋ฆฌ์ŠคํŠธ ๋ณ€ํ™˜ โ†’ Entity ๋ฆฌ์ŠคํŠธ๋กœ ๋งคํ•‘ + * ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ”„๋กœ์„ธ์„œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @field apiRequestManager ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ์œ„ํ•œ ๋งค๋‹ˆ์ € + * @field apiResponseMapper API ์‘๋‹ต์„ DTO๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋งคํผ + * @since 2025-05-02 + */ +public class ImageScrapProcessor implements ItemProcessor> { + + private final ApiRequestManager apiRequestManager; + private final ApiResponseMapper apiResponseMapper; + + public ImageScrapProcessor(ApiRequestManager apiRequestManager, + ApiResponseMapper apiResponseMapper) { + this.apiRequestManager = apiRequestManager; + this.apiResponseMapper = apiResponseMapper; + } + + /** + * ์ฃผ์–ด์ง„ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์— ๋Œ€ํ•ด ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , + * ์‘๋‹ต์„ DTO ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ ํ›„, Entity ๋ฆฌ์ŠคํŠธ๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageNumber ์ฒ˜๋ฆฌํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @return DrugImgEntity ๋ฆฌ์ŠคํŠธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public List process(Integer pageNumber) { + LogUtil.log(Thread.currentThread().getName() + " - " + pageNumber + " page ์ฒ˜๋ฆฌ ์‹œ์ž‘"); + String response = apiRequestManager.fetchImageData(pageNumber); + JsonNode items = apiRequestManager.getItemsFromResponse(response); + List drugItems = apiResponseMapper.toListFromDrugImages(items); + + for (DrugImageRequest item : drugItems) { + String productImage = apiRequestManager.getImage(item.getDrugId()); + if (notEmptyProductImage(productImage)) { + item.changeProductImageUrl(productImage); + } + } + + return drugItems.stream() + .map(DrugImageRequestMapper::toEntityFromRequest) + .toList(); + } + + /** + * ์ฃผ์–ด์ง„ productImage๊ฐ€ null์ด ์•„๋‹ˆ๊ณ  ๊ธธ์ด๊ฐ€ 10๋ณด๋‹ค ํฐ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param productImage ๊ฒ€์‚ฌํ•  ์ด๋ฏธ์ง€ URL + * @return true: ์œ ํšจํ•œ ์ด๋ฏธ์ง€ URL, false: ์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฏธ์ง€ URL + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private boolean notEmptyProductImage(String productImage) { + return productImage != null && productImage.length() > 10; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java new file mode 100644 index 0000000..87f9cc1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/processor/ImageTotalPageCalculator.java @@ -0,0 +1,49 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.processor; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader.PageRangePartitioner; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.api.util.ApiRequestManager; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * ์ด๋ฏธ์ง€ ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” Tasklet ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + *

+ * ์ด ํด๋ž˜์Šค๋Š” Spring Batch์˜ Tasklet ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ, + * ์ด๋ฏธ์ง€ API์—์„œ ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ฐ€์ ธ์™€ PageRangePartitioner์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + *

+ * + * @field pageRangePartitioner ํŽ˜์ด์ง€ ๋ฒ”์œ„๋ฅผ ๋‚˜๋ˆ„๋Š” Partitioner + * @field apiRequestManager API ์š”์ฒญ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋งค๋‹ˆ์ € + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class ImageTotalPageCalculator implements Tasklet { + + private final PageRangePartitioner pageRangePartitioner; + private final ApiRequestManager apiRequestManager; + + /** + * ์ด๋ฏธ์ง€ API์—์„œ ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ ๊ฐ€์ ธ์™€ PageRangePartitioner์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param contribution ํ˜„์žฌ step์˜ ๊ธฐ์—ฌ๋„ + * @param chunkContext ํ˜„์žฌ chunk ์‹คํ–‰ ์ปจํ…์ŠคํŠธ + * @return RepeatStatus.FINISHED (์ž‘์—… ์™„๋ฃŒ ์‹ ํ˜ธ) + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int totalPage = apiRequestManager.getImageTotalPage(); + pageRangePartitioner.setTotalPages(totalPage); + + log("[Image-Total-Page-Calculator] ์ด ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ ์™„๋ฃŒ: " + totalPage); + return RepeatStatus.FINISHED; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PageRangePartitioner.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PageRangePartitioner.java new file mode 100644 index 0000000..3836617 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PageRangePartitioner.java @@ -0,0 +1,48 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Setter; +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.stereotype.Component; + +/** + * PageRangePartitioner๋Š” ํŽ˜์ด์ง€ ๋ฒ”์œ„๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒŒํ‹ฐ์…˜์„ ๋‚˜๋ˆ„๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * ์ฃผ์–ด์ง„ ์ด ํŽ˜์ด์ง€ ์ˆ˜๋ฅผ gridSize์— ๋”ฐ๋ผ ๋‚˜๋ˆ„์–ด ๊ฐ ํŒŒํ‹ฐ์…˜์˜ ์‹œ์ž‘๊ณผ ๋ ํŽ˜์ด์ง€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @field totalPages ์ด ํŽ˜์ด์ง€ ์ˆ˜ + * @since 2025-05-02 + */ +@Component +@Setter +public class PageRangePartitioner implements Partitioner { + private int totalPages = 0; + + /** + * partition ๋ฉ”์„œ๋“œ๋Š” ์ฃผ์–ด์ง„ gridSize์— ๋”ฐ๋ผ ํŽ˜์ด์ง€ ๋ฒ”์œ„๋ฅผ ๋‚˜๋ˆ„์–ด + * ๊ฐ ํŒŒํ‹ฐ์…˜์˜ ์‹œ์ž‘๊ณผ ๋ ํŽ˜์ด์ง€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param gridSize ํŒŒํ‹ฐ์…˜์˜ ๊ฐœ์ˆ˜ + * @return ๊ฐ ํŒŒํ‹ฐ์…˜์˜ ์‹œ์ž‘๊ณผ ๋ ํŽ˜์ด์ง€๋ฅผ ํฌํ•จํ•˜๋Š” ๋งต + */ + @Override + public Map partition(int gridSize) { + int range = totalPages / gridSize; + Map result = new HashMap<>(); + + for (int i = 0; i < gridSize; i++) { + int start = i * range + 1; + int end = (i == gridSize - 1) ? totalPages : start + range - 1; + + ExecutionContext context = new ExecutionContext(); + context.putInt("startPage", start); + context.putInt("endPage", end); + + result.put("partition" + i, context); + } + + return result; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java new file mode 100644 index 0000000..647128f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/reader/PartitionedPageReader.java @@ -0,0 +1,65 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.reader; + +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * PartitionedPageReader๋Š” Spring Batch์˜ ItemReader๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ + * ์ด๋ฏธ์ง€ ํŽ˜์ด์ง€๋ฅผ ์ฝ์–ด์˜ค๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. + * ์ด ํด๋ž˜์Šค๋Š” StepScope๋กœ ์„ค์ •๋˜์–ด, ๊ฐ ํŒŒํ‹ฐ์…˜์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @field currentPage ํ˜„์žฌ ์ฝ๊ณ  ์žˆ๋Š” ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @field endPage ์ฝ์„ ํŽ˜์ด์ง€์˜ ๋งˆ์ง€๋ง‰ ๋ฒˆํ˜ธ + * @since 2025-05-02 + * @author ํ•จ์˜ˆ์ • + */ +@Component +@StepScope +public class PartitionedPageReader implements ItemReader { + private int currentPage; + private final int endPage; + + /** + * PartitionedPageReader์˜ ์ƒ์„ฑ์ž. + *

+ * Spring Batch์˜ ํŒŒํ‹ฐ์…”๋‹์—์„œ ์‚ฌ์šฉ๋˜๋Š” {@code stepExecutionContext}๋กœ๋ถ€ํ„ฐ startPage์™€ endPage ๊ฐ’์„ ์ฃผ์ž…๋ฐ›์•„, + * ๊ฐ ์Šฌ๋ ˆ์ด๋ธŒ Step์ด ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ํŽ˜์ด์ง€ ๋ฒ”์œ„๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + *

+ * {@code @Value}๋Š” Spring์˜ Expression Language(SpEL)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ์—์„œ ๊ฐ’์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ๋ฅผ ๋“ค์–ด, ํŒŒํ‹ฐ์…”๋„ˆ๊ฐ€ {@code startPage=1, endPage=10}์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ์ด ๊ฐ’์ด ํ•ด๋‹น ์Šฌ๋ ˆ์ด๋ธŒ์— ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค. + * + * @param startPage ํŒŒํ‹ฐ์…˜์—์„œ ์ฒ˜๋ฆฌํ•  ์‹œ์ž‘ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @param endPage ํŒŒํ‹ฐ์…˜์—์„œ ์ฒ˜๋ฆฌํ•  ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + public PartitionedPageReader( + @Value("#{stepExecutionContext['startPage']}") int startPage, + @Value("#{stepExecutionContext['endPage']}") int endPage) { + log("[Reader bean ์ƒ์„ฑ] startPage=" + startPage + ", endPage=" + endPage); + this.currentPage = startPage; + this.endPage = endPage; + } + + /** + * ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. + *

+ * ์ง€์ •๋œ endPage๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด {@code null}์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๋” ์ด์ƒ ์ฝ์„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Œ์„ Spring Batch์— ์•Œ๋ฆฝ๋‹ˆ๋‹ค. + * + * @return ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ๋˜๋Š” endPage๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด null + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public Integer read() { + log("์ด๋ฏธ์ง€ ํŽ˜์ด์ง€ Read: " + currentPage); + if (currentPage > endPage) { + return null; + } + return currentPage++; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java new file mode 100644 index 0000000..ffbc774 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/step/image/writer/DrugImageWriter.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.image.writer; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import java.util.List; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugImgRepository; + +/** + * Entity ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ›์•„ JPA Repository๋กœ ํ•œ ๋ฒˆ์— ์ €์žฅํ•˜๋Š” Writer์ž…๋‹ˆ๋‹ค. + * + * @field repository ์ด๋ฏธ์ง€ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” JPA Repository + * @since 2025-05-02 + */ +public class DrugImageWriter implements ItemWriter> { + + private final DrugImgRepository repository; + + public DrugImageWriter(DrugImgRepository repository) { + this.repository = repository; + } + + /** + * Chunk ๋‹จ์œ„๋กœ ์ „๋‹ฌ๋œ DrugImgEntity ๋ฆฌ์ŠคํŠธ๋ฅผ JPA Repository์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param chunk ์ €์žฅํ•  DrugImgEntity ๋ฆฌ์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜๋Š” Chunk + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + public void write(Chunk> chunk) { + log(Thread.currentThread().getName() + " - Start Write"); + for (List items : chunk.getItems()) { + repository.saveAllAndFlush(items); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java new file mode 100644 index 0000000..17d9dfe --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/ApiResponseMapper.java @@ -0,0 +1,74 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.DrugImageRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * API ์‘๋‹ต(JsonNode)์„ DTO๋กœ ๋ณ€ํ™˜ํ•˜๋Š” Mapper ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * JsonNode๋ฅผ DTO ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @fields objectMapper Jackson ObjectMapper ์ธ์Šคํ„ด์Šค + * @modified 2025-05-02 + * @since 2025-04-22 + */ +@Component +@RequiredArgsConstructor +public class ApiResponseMapper { + private final ObjectMapper objectMapper; + + /** + * JsonNode๋ฅผ DrugDetailRequest ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param items JsonNode ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ + * @return DrugDetailRequest ๋ฆฌ์ŠคํŠธ + * @throws ParserBatchException JSON ํƒ€์ž… ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @author ํ•จ์˜ˆ์ • + * @modified 2025-05-02 + * @since 2025-04-22 + */ + public List toListFromDrugDetails(JsonNode items) { + try { + return objectMapper.readValue( + items.toString(), new TypeReference<>() { + } + ); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "ํƒ€์ž…๋ณ€ํ™˜ ์‹คํŒจ(JsonNode -> List"); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + + /** + * JsonNode๋ฅผ DrugImageRequest ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param items JsonNode ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ + * @return DrugImageRequest ๋ฆฌ์ŠคํŠธ + * @throws ParserBatchException JSON ํƒ€์ž… ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @author ํ•จ์˜ˆ์ • + * @modified 2025-05-02 + * @since 2025-04-22 + */ + public List toListFromDrugImages(JsonNode items) { + try { + return objectMapper.readValue( + items.toString(), new TypeReference<>() { + } + ); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "ํƒ€์ž…๋ณ€ํ™˜ ์‹คํŒจ(JsonNode -> List"); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java new file mode 100644 index 0000000..20aebec --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/mapper/DrugDetailRequestMapper.java @@ -0,0 +1,38 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.mapper; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.step.detail.dto.DrugDetailRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; +/** + * ์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด ์š”์ฒญ์„ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋งคํผ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +public class DrugDetailRequestMapper { + + /** + * DrugDetailRequest ๊ฐ์ฒด๋ฅผ DrugDetailEntity ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param r ๋ณ€ํ™˜ํ•  DrugDetailRequest ๊ฐ์ฒด + * @return ๋ณ€ํ™˜๋œ DrugDetailEntity ๊ฐ์ฒด + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + public static DrugDetailEntity toEntityFromRequest(DrugDetailRequest r) { + return DrugDetailEntity.builder() + .drugId(r.getDrugId()) + .drugName(r.getDrugName()) + .company(r.getCompany()) + .permitDate(r.getPermitDate()) + .isGeneral(r.isGeneral()) + .materialInfo(r.getMaterialInfo()) + .storeMethod(r.getStoreMethod()) + .validTerm(r.getValidTerm()) + .efficacy(r.getEfficacy()) + .usage(r.getUsage()) + .precaution(r.getPrecaution()) + .cancelDate(r.getCancelDate()) + .cancelName(r.getCancelName()) + .isHerbal(r.isHerbal()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java new file mode 100644 index 0000000..fa96107 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/MaterialParser.java @@ -0,0 +1,108 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.error.ScraperErrorCode; + +/** + * ์›์žฌ๋ฃŒ ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ JSON ๋ฐฐ์—ด ํ˜•์‹์˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +public class MaterialParser { + + /** + * ์›์žฌ๋ฃŒ ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•˜์—ฌ JSON ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param raw ์›์žฌ๋ฃŒ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๋ฌธ์ž์—ด + * (์„ธ๋ฏธ์ฝœ๋ก ์œผ๋กœ ๋ธ”๋ก ๊ตฌ๋ถ„, ํŒŒ์ดํ”„๋กœ ํ‚ค-๊ฐ’ ์Œ ๊ตฌ๋ถ„) + * @return JSON ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + public static String parseMaterial(String raw) { + ObjectMapper objectMapper = new ObjectMapper(); + ArrayNode resultArray = objectMapper.createArrayNode(); + String[] blocks = splitBlock(raw); + parsingBlocksAndPutArrayItem(blocks, resultArray); + return convertString(objectMapper, resultArray); + } + + /** + * ๋ธ”๋ก ๋ฐฐ์—ด์„ ํŒŒ์‹ฑํ•˜์—ฌ JSON ๋ฐฐ์—ด์— ํ•ญ๋ชฉ์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param blocks ์›์žฌ๋ฃŒ ๋ธ”๋ก ๋ฐฐ์—ด + * @param resultArray ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  JSON ๋ฐฐ์—ด + */ + private static void parsingBlocksAndPutArrayItem(String[] blocks, ArrayNode resultArray) { + for (String block : blocks) { + block = block.trim(); + if (block.isEmpty()) { + continue; + } + String[] pairs = splitByPipe(block); + ObjectNode item = makeItem(pairs); + resultArray.add(item); + } + } + + /** + * JSON ๋ฐฐ์—ด์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param objectMapper Jackson ObjectMapper ์ธ์Šคํ„ด์Šค + * @param arrayNode ๋ณ€ํ™˜ํ•  JSON ๋ฐฐ์—ด + * @return JSON ๋ฌธ์ž์—ด + * @throws ScraperException JSON ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + */ + private static String convertString(ObjectMapper objectMapper, ArrayNode arrayNode) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode); + } catch (JsonProcessingException e) { + LogUtil.log(LogLevel.ERROR, "JSON ๋ฌธ์ž์—ด์„ String์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: " + e.getMessage()); + throw new ScraperException(ScraperErrorCode.MATERIAL_PARSING_FAIL); + } + } + + /** + * ํ‚ค-๊ฐ’ ์Œ ๋ฐฐ์—ด๋กœ๋ถ€ํ„ฐ JSON ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param pairs ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ํ‚ค-๊ฐ’ ์Œ ๋ฐฐ์—ด + * @return ์ƒ์„ฑ๋œ JSON ๊ฐ์ฒด + */ + private static ObjectNode makeItem(String[] pairs) { + ObjectNode item = new ObjectMapper().createObjectNode(); + for (String pair : pairs) { + String[] kv = pair.split(":", 2); + String key = kv[0].trim(); + String value = ""; + if (kv.length == 2) { + value = kv[1].trim(); + } + item.put(key, value); + } + return item; + } + + /** + * ๋ธ”๋ก ๋‚ด ํ‚ค-๊ฐ’ ์Œ์„ ํŒŒ์ดํ”„(|) ๊ธฐํ˜ธ๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param block ๋ธ”๋ก ๋ฌธ์ž์—ด + * @return ํ‚ค-๊ฐ’ ์Œ ๋ฐฐ์—ด + */ + private static String[] splitByPipe(String block) { + return block.split("\\|"); + } + + /** + * ์›์žฌ๋ฃŒ ์ •๋ณด๋ฅผ ์„ธ๋ฏธ์ฝœ๋ก (;) ๊ธฐ์ค€์œผ๋กœ ๋ธ”๋ก์œผ๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param raw ์›์žฌ๋ฃŒ ๋ฌธ์ž์—ด + * @return ๋ธ”๋ก ๋ฐฐ์—ด + */ + private static String[] splitBlock(String raw) { + return raw.split(";"); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java new file mode 100644 index 0000000..9040788 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/batch/util/parser/XMLParser.java @@ -0,0 +1,426 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.util.parser; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.ScraperException; +import com.likelion.backendplus4.yakplus.drug.scraper.application.exception.error.ScraperErrorCode; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * XML ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•˜์—ฌ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ +public class XMLParser { + private static final Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); + private static final Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); + + private static final ObjectMapper mapper = new ObjectMapper(); + private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + /** + * XML ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•˜์—ฌ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param xml ๋ณ€ํ™˜ํ•  XML ๋ฌธ์ž์—ด + * @return ๋ณ€ํ™˜๋œ JSON ๋ฌธ์ž์—ด + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + public static String toJson(String xml) { + + if (isXmlNull(xml)) { + return "{\"\": \"\"}"; + } + + Document doc = parseXmlString(xml); + Element root = doc.getDocumentElement(); + + List allSections = new ArrayList<>(); + List allArticles = new ArrayList<>(); + List allParagraphs = new ArrayList<>(); + + Map sectionMap = new HashMap<>(); + Map articleMap = new HashMap<>(); + + DocTag docTag = new DocTag(root, allSections); + parseSections(root, allSections, sectionMap); + parseArticles(root, allArticles, articleMap, sectionMap); + parseParagraph(root, allParagraphs, articleMap); + return convertJson(docTag); + } + + /** + * DocTag ๊ฐ์ฒด๋ฅผ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param docTag ๋ณ€ํ™˜ํ•  DocTag ๊ฐ์ฒด + * @return JSON ๋ฌธ์ž์—ด + * @throws RuntimeException JSON ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static String convertJson(DocTag docTag) { + try { + return mapper.writeValueAsString(docTag); + } catch (JsonProcessingException e) { + log(LogLevel.ERROR, "JSON ๋ณ€ํ™˜ ์‹คํŒจ", e); + throw new ParserBatchException(ParserBatchError.JSON_TYPE_CHANGE_FAIL); + } + } + + /** + * XML์—์„œ PARAGRAPH ํƒœ๊ทธ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ParagraphTag ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root XML ๋ฃจํŠธ ์—˜๋ฆฌ๋จผํŠธ + * @param allParagraphs ํŒŒ์‹ฑ๋œ ParagraphTag ๋ฆฌ์ŠคํŠธ + * @param articleMap ARTICLE ์—˜๋ฆฌ๋จผํŠธ์™€ ArticleTag ๋งคํ•‘ ์ •๋ณด + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static void parseParagraph(Element root, List allParagraphs, Map articleMap) { + NodeList paraNodes = root.getElementsByTagName("PARAGRAPH"); + + if (paraNodes.getLength() != 0) { + for (int i = 0; i < paraNodes.getLength(); i++) { + Element paragraphElement = (Element) paraNodes.item(i); + ParagraphTag paragraphTag = new ParagraphTag(); + paragraphTag.tagName = cleanText(paragraphElement.getAttribute("tagName")); + paragraphTag.textIndent = cleanText(paragraphElement.getAttribute("textIndent")); + paragraphTag.marginLeft = cleanText(paragraphElement.getAttribute("marginLeft")); + paragraphTag.text = cleanText(paragraphElement.getTextContent().trim()); + + if (!isEmptyTagNameOrTagText(paragraphTag)) { + allParagraphs.add(paragraphTag); + } + + mapSectionFromArticle(articleMap, paragraphTag, paragraphElement); + } + } + + } + + private static boolean isEmptyTagNameOrTagText(ParagraphTag paragraphTag) { + return paragraphTag.tagName.isEmpty() || paragraphTag.text.isEmpty(); + } + + /** + * XML์—์„œ ARTICLE ํƒœ๊ทธ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ArticleTag ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root XML ๋ฃจํŠธ ์—˜๋ฆฌ๋จผํŠธ + * @param allArticles ํŒŒ์‹ฑ๋œ ArticleTag ๋ฆฌ์ŠคํŠธ + * @param articleMap ARTICLE ์—˜๋ฆฌ๋จผํŠธ์™€ ArticleTag ๋งคํ•‘ ์ •๋ณด + * @param sectionMap SECTION ์—˜๋ฆฌ๋จผํŠธ์™€ SectionTag ๋งคํ•‘ ์ •๋ณด + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static void parseArticles(Element root, List allArticles, + Map articleMap, + Map sectionMap) { + NodeList artNodes = root.getElementsByTagName("ARTICLE"); + if (artNodes.getLength() > 0) { + for (int i = 0; i < artNodes.getLength(); i++) { + Element artElement = (Element) artNodes.item(i); + ArticleTag articleTag = new ArticleTag(); + articleTag.title = cleanText(artElement.getAttribute("title")); + articleTag.paragraphs = new ArrayList<>(); + + allArticles.add(articleTag); + articleMap.put(artElement, articleTag); + mapSectionFromArticle(sectionMap, articleTag, artElement); + } + } + + } + + /** + * ์ƒ์œ„ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ด๋‹น ํƒœ๊ทธ๋ฅผ ๋ถ€๋ชจ ํƒœ๊ทธ์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param map ์ƒ์œ„ ์—˜๋ฆฌ๋จผํŠธ์™€ ํƒœ๊ทธ ๋งคํ•‘ ์ •๋ณด + * @param tags ํ˜„์žฌ ํƒœ๊ทธ + * @param element ํ˜„์žฌ ์—˜๋ฆฌ๋จผํŠธ + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static void mapSectionFromArticle(Map map, Tags tags, Element element) { + Element parentElement = (Element) element.getParentNode(); + Tags parentTag = map.get(parentElement); + if (parentTag != null) { + parentTag.addTag(tags); + } + } + + /** + * XML์—์„œ SECTION ํƒœ๊ทธ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ SectionTag ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root XML ๋ฃจํŠธ ์—˜๋ฆฌ๋จผํŠธ + * @param allSections ํŒŒ์‹ฑ๋œ SectionTag ๋ฆฌ์ŠคํŠธ + * @param sectionMap SECTION ์—˜๋ฆฌ๋จผํŠธ์™€ SectionTag ๋งคํ•‘ ์ •๋ณด + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static void parseSections(Element root, List allSections, Map sectionMap) { + NodeList secNodes = root.getElementsByTagName("SECTION"); + + if (secNodes.getLength() > 0) { + for (int i = 0; i < secNodes.getLength(); i++) { + Element secEl = (Element) secNodes.item(i); + SectionTag secDto = new SectionTag(); + secDto.title = cleanText(secEl.getAttribute("title")); + secDto.articles = new ArrayList<>(); + + allSections.add(secDto); + sectionMap.put(secEl, secDto); + } + } + } + + /** + * XML ๋ฌธ์ž์—ด์„ ํŒŒ์‹ฑํ•˜์—ฌ Document ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param xml ํŒŒ์‹ฑํ•  XML ๋ฌธ์ž์—ด + * @return ํŒŒ์‹ฑ๋œ Document ๊ฐ์ฒด + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static Document parseXmlString(String xml) { + try { + return documentBuilderFactory.newDocumentBuilder() + .parse(new InputSource(new StringReader(xml))); + } catch (Exception e) { + log(LogLevel.ERROR, "XML ํŒŒ์‹ฑ ์‹คํŒจ", e); + throw new ScraperException(ScraperErrorCode.PARSING_ERROR); + } + } + + /** + * XML ๋ฌธ์ž์—ด์ด null ์ด๊ฑฐ๋‚˜ ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param xml ํ™•์ธํ•  XML ๋ฌธ์ž์—ด + * @return null ๋˜๋Š” ๋น„์–ด์žˆ์œผ๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + * @author ํ•จ์˜ˆ์ •, ์ดํ•ด์ฐฝ + * @since 2025-04-21 + */ + private static boolean isXmlNull(String xml) { + if (xml == null || xml.trim().isEmpty() || xml == "null") { + return true; + } else { + return false; + } + } + + /** + * XML ๋ฃจํŠธ ํƒœ๊ทธ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * XML Parser ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ + private static class DocTag implements Tags { + public String title; + public String type; + public List sections; + + DocTag(Element root, List sections) { + this.title = cleanText(root.getAttribute("title")); + this.type = root.getAttribute("type"); + this.sections = sections; + } + + /** + * DocTag์— Section ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * Json ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param tags ์ถ”๊ฐ€ํ•  ํƒœ๊ทธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + sections.add((SectionTag) tags); + } + + } + + /** + * SECTION ํƒœ๊ทธ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ + private static class SectionTag implements Tags { + public String title; + public List articles; + + /** + * SectionTag์— Article ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * Json ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param tags + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + articles.add((ArticleTag) tags); + } + + } + + /** + * ARTICLE ํƒœ๊ทธ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ + private static class ArticleTag implements Tags { + public String title; + public List paragraphs; + + /** + * ArticleTag์— Paragraph ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * Json ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param tags + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + paragraphs.add((ParagraphTag) tags); + } + + } + + /** + * PARAGRAPH ํƒœ๊ทธ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ + private static class ParagraphTag implements Tags { + public String tagName; + public String textIndent; + public String marginLeft; + public String text; + + /** + * ParagraphTag๋Š” ํ•˜์œ„ ํƒœ๊ทธ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š์œผ๋ฏ€๋กœ addTag ๋ฉ”์„œ๋“œ๋Š” ๊ตฌํ˜„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + * + * @param tags + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + @Override + public void addTag(Tags tags) { + log(LogLevel.WARN, "ParagraphTag๋Š” ํ•˜์œ„ ํƒœ๊ทธ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + } + + /** + * ํƒœ๊ทธ ํด๋ž˜์Šค ๊ฐ„ ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ + private interface Tags { + /** + * ํ•ด๋‹น ํด๋ž˜์Šค์˜ ํ•˜์œ„ ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * Json ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param tags ์ถ”๊ฐ€ํ•  ํƒœ๊ทธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-21 + */ + void addTag(Tags tags); + } + + /** + * ๋ถˆํ•„์š”ํ•œ ๋ฌธ์ž๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param text + * @return String ์ •๋ฆฌ๋œ ํ…์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-21 + */ + private static String cleanText(String text) { + Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); + String tempText = TAG_REGEX.matcher(text) + .replaceAll("") + .replaceAll(" ", " ") + .replaceAll("โ— ", "") + .replaceAll("โ—‹ ", "") + .replaceAll("โˆŽ ", "") + .replaceAll("- ", ""); + return decodeHtml(tempText).trim(); + } + + /** + * HTML ์—”ํ‹ฐํ‹ฐ(10์ง„์ˆ˜ &#DDD; ๋ฐ 16์ง„์ˆ˜ &#xHHHH;)๋ฅผ ๋Œ€์‘ํ•˜๋Š” ๋ฌธ์ž๋กœ ๋””์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: "foo•bar" โ†’ "fooโ€ขbar", + * "foo•bar" โ†’ "fooโ€ขbar" + * + * @param input ์—”ํ‹ฐํ‹ฐ๋ฅผ ํฌํ•จํ•œ ๋ฌธ์ž์—ด + * @return ๋””์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modify 2025-05-03 ํ•จ์˜ˆ์ • + * - ๋ฉ”์†Œ๋“œ ๋ถ„๋ฆฌ + */ + private static String decodeHtml(String input) { + String tempText = decimalEntityDecode(input); + String result = hexEntityDecode(tempText); + return result; + } + + /** + * ๋ฌธ์ž์—ด ๋‚ด์˜ 16์ง„์ˆ˜ HTML ์—”ํ‹ฐํ‹ฐ(&#xHHHH;)๋ฅผ ํ•ด๋‹น ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param result 16์ง„์ˆ˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํฌํ•จํ•œ ๋ฌธ์ž์—ด + * @return ๋ณ€ํ™˜๋œ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modify 2025-05-03 ํ•จ์˜ˆ์ • + * - ๋ฉ”์†Œ๋“œ ๋ถ„๋ฆฌ + */ + private static String hexEntityDecode(String result) { + Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(result); + StringBuffer sb = new StringBuffer(); + while (hexMatcher.find()) { + int code = Integer.parseInt(hexMatcher.group(1), 16); + hexMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + hexMatcher.appendTail(sb); + return sb.toString(); + } + + /** + * ๋ฌธ์ž์—ด ๋‚ด์˜ 10์ง„์ˆ˜ HTML ์—”ํ‹ฐํ‹ฐ(&#DDD;)๋ฅผ ํ•ด๋‹น ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param result 10์ง„์ˆ˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํฌํ•จํ•œ ๋ฌธ์ž์—ด + * @return ๋ณ€ํ™˜๋œ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modify 2025-05-03 ํ•จ์˜ˆ์ • + * - ๋ฉ”์†Œ๋“œ ๋ถ„๋ฆฌ + */ + private static String decimalEntityDecode(String result) { + Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(result); + StringBuffer sb = new StringBuffer(); + while (decMatcher.find()) { + int code = Integer.parseInt(decMatcher.group(1)); + decMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + decMatcher.appendTail(sb); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/DrugImageRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/DrugImageRequest.java new file mode 100644 index 0000000..8bc4761 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/DrugImageRequest.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class DrugImageRequest { + + @JsonProperty("ITEM_SEQ") + private Long drugId; + + private String productImage; + + @JsonProperty("BIG_PRDT_IMG_URL") + private String pillImageUrl; + + public DrugImageRequest changeProductImageUrl(String productImage){ + this.productImage = productImage; + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/EmbeddingRequestText.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/EmbeddingRequestText.java new file mode 100644 index 0000000..de76814 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/dto/EmbeddingRequestText.java @@ -0,0 +1,12 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class EmbeddingRequestText { + private String text; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugDetailEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugDetailEntity.java similarity index 61% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugDetailEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugDetailEntity.java index 6c06157..fec641a 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugDetailEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugDetailEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonFormat; @@ -23,23 +23,18 @@ @AllArgsConstructor @ToString @Table(name = "gov_drug_detail") -public class GovDrugDetailEntity { +public class DrugDetailEntity { @Id - @JsonProperty("ITEM_SEQ") @Column( name= "ITEM_SEQ") private Long drugId; - @JsonProperty("ITEM_NAME") @Column( name= "ITEM_NAME", columnDefinition = "TEXT") private String drugName; - @JsonProperty("ENTP_NAME") @Column( name= "ENTP_NAME") private String company; - @JsonProperty("ITEM_PERMIT_DATE") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd") @Column( name= "ITEM_PERMIT_DATE") private LocalDate permitDate; @@ -49,11 +44,9 @@ public class GovDrugDetailEntity { @Column(name = "MATERIAL_NAME", columnDefinition = "JSON") private String materialInfo; - @JsonProperty("STORAGE_METHOD") @Column(name = "STORAGE_METHOD", columnDefinition = "TEXT") private String storeMethod; - @JsonProperty("VALID_TERM") @Column(name = "VALID_TERM") private String validTerm; @@ -66,24 +59,14 @@ public class GovDrugDetailEntity { @Column(name = "NB_DOC_DATA", columnDefinition = "JSON") private String precaution; - @JsonCreator - public GovDrugDetailEntity(@JsonProperty("ETC_OTC_CODE") String drugType) { - this.isGeneral = !"์ „๋ฌธ์˜์•ฝํ’ˆ".equals(drugType); - } + @Column(name="CANCEL_DATE") + private LocalDate cancelDate; - public void changeMaterialInfo(String materialInfo){ - this.materialInfo = materialInfo; - } + @Column(name="CANCEL_NAME") + private String cancelName; - public void changeUsage(String usage) { - this.usage = usage; - } + @Column(name="IS_HERBAL") + @Builder.Default + private Boolean isHerbal = false; - public void changeEfficacy(String efficacy) { - this.efficacy = efficacy; - } - - public void changePrecaution(String precaution) { - this.precaution = precaution; - } } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java new file mode 100644 index 0000000..e93548e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugImgEntity.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Table(name = "API_DATA_DRUG_IMG") +public class DrugImgEntity { + @Id + @Column(name = "ITEM_SEQ") + private Long drugId; + + @Column(name = "PRODUCT_IMAGE", columnDefinition = "LONGTEXT") + private String productImage; + + @Column(name = "PILL_IMAGE", columnDefinition = "LONGTEXT") + private String pillImage; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugRawDataEntity.java similarity index 72% rename from src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java rename to src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugRawDataEntity.java index c879ccf..b8a119e 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/drug/infrastructure/adapter/persistence/repository/entity/GovDrugEntity.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/entity/DrugRawDataEntity.java @@ -1,4 +1,4 @@ -package com.likelion.backendplus4.yakplus.drug.infrastructure.adapter.persistence.repository.entity; +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity; import java.time.LocalDate; @@ -19,10 +19,10 @@ @NoArgsConstructor @AllArgsConstructor @Table(name="GOV_DRUG_RAW_DATA") -public class GovDrugEntity { +public class DrugRawDataEntity { @Id @Column(name="ITEM_SEQ") - private Long id; + private Long drugId; @Column( name= "ITEM_NAME", columnDefinition = "TEXT") private String drugName; @@ -36,7 +36,7 @@ public class GovDrugEntity { @Column(name = "ETC_OTC_CODE") private boolean isGeneral; - @Column(name = "MATERIAL_NAME", columnDefinition = "JSON") + @Column(name = "MATERIAL_NAME", columnDefinition = "TEXT") private String materialInfo; @JsonProperty("STORAGE_METHOD") @@ -46,7 +46,7 @@ public class GovDrugEntity { @Column(name = "VALID_TERM") private String validTerm; - @Column(name = "EE_DOC_DATA", columnDefinition = "JSON") + @Column(name = "EE_DOC_DATA", columnDefinition = "JSON") private String efficacy; @Column(name = "UD_DOC_DATA", columnDefinition = "JSON") @@ -57,4 +57,13 @@ public class GovDrugEntity { @Column(name= "IMG_URL") private String imageUrl; + + @Column(name="CANCEL_DATE") + private LocalDate cancelDate; + + @Column(name="CANCEL_NAME") + private String cancelName; + + @Column(name="IS_HERBAL") + private Boolean isHerbal; } diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java new file mode 100644 index 0000000..1ad7b70 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/adapter/DrugRawDataAdapter.java @@ -0,0 +1,101 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.adapter; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository.DrugJpaRepository; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper.DrugRawDataMapper; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.DrugRawDataPort; +import com.likelion.backendplus4.yakplus.drug.index.application.port.out.EmbeddingPort; + +import lombok.RequiredArgsConstructor; + +/** + * ๊ณต๊ณต API๋กœ๋ถ€ํ„ฐ ์กฐํšŒํ•œ ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ JPA๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์™€ + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์ธ Drug๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-22 + * @modified 2025-04-24 + */ +@Component +@RequiredArgsConstructor +public class DrugRawDataAdapter implements DrugRawDataPort { + private final DrugJpaRepository drugJpaRepository; + private final EmbeddingPort embeddingPort; + + /** + * ์ฃผ์–ด์ง„ Pageable ์ •๋ณด์— ๋”ฐ๋ผ DB์—์„œ ํ•œ ํŽ˜์ด์ง€ ๋ถ„๋Ÿ‰์˜ GovDrugEntity๋ฅผ ์กฐํšŒํ•˜๊ณ , + * ๊ฐ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ(GovDrug)๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ Page ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageable ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์™€ ํฌ๊ธฐ๋ฅผ ํฌํ•จํ•˜๋Š” Pageable ๊ฐ์ฒด + * @return ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋ณ€ํ™˜๋œ GovDrug ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ Page + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified + * 2025-05-04 - ๋ฐ•์ฐฌ๋ณ‘: Mapper์— ๋„˜๊ฒจ์ฃผ๊ธฐ ์ „ ๋จผ์ € ํŒŒ์‹ฑํ•˜๋„๋ก ๋ณ€๊ฒฝ + */ + @Override + public Page findAllDrugs(Pageable pageable) { + log("findAllDrugs() ์š”์ฒญ ์ˆ˜์‹ "); + + return drugJpaRepository.findByIsGeneral(pageable) + .map(DrugRawDataMapper::toDomainFromEntity); + } + + /** + * ์ง€์ •๋œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์™€ ํŽ˜์ด์ง€ ํฌ๊ธฐ์— ๋”ฐ๋ผ Pageable ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ , + * ์›์‹œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ์™€ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ธํ•˜์—ฌ ํ•œ ํŽ˜์ด์ง€ ๋ถ„๋Ÿ‰์˜ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageNo ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @param numOfRows ํ•œ ํŽ˜์ด์ง€์— ํฌํ•จํ•  ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ + * @return ํŽ˜์ด์ง€ ๋ฒ”์œ„์— ํ•ด๋‹นํ•˜๋Š” Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค์˜ ๋ฆฌ์ŠคํŠธ + * @author ์ •์•ˆ์‹ + * @since 2025-04-24 + * @modified + * 2025-05-02 - ์ดํ•ด์ฐฝ: numOfRows ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + */ + @Override + public List fetchRawData(int pageNo, int numOfRows) { + log("index ์„œ๋น„์Šค ์š”์ฒญ ์ˆ˜์‹ "); + Pageable pageable = createPageable(pageNo, numOfRows); + return embeddingPort.loadEmbeddingsByPage(pageable); + } + + /** + * ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ์™€ ํŽ˜์ด์ง€ ํฌ๊ธฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ drugId ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ์ด ์ ์šฉ๋œ Pageable ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageNo ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @param numOfRows ํ•œ ํŽ˜์ด์ง€์— ํฌํ•จํ•  ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ + * @return ์ง€์ •๋œ ํŽ˜์ด์ง€ ์ •๋ณด์™€ ์ •๋ ฌ ์กฐ๊ฑด์„ ํฌํ•จํ•œ Pageable ๊ฐ์ฒด + * @author ์ •์•ˆ์‹ + * @since 2025-04-24 + * @modified + * 2025-04-24 + */ + private Pageable createPageable(int pageNo, int numOfRows) { + log("pageable ์ƒ์„ฑ"); + return PageRequest.of(pageNo, numOfRows, Sort.by(Sort.Direction.ASC, "drugId")); + } + + /** + * JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ด์šฉํ•ด GovDrugJpaRepository์˜ ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return GovDrugJpaRepository์˜ ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜ + * @author ์ดํ•ด์ฐฝ + * @since 2025-05-02 + * @modified + * 2025-05-02 - ์ดํ•ด์ฐฝ: numOfRows ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ + */ + @Override + public long getDrugTotalSize() { + return drugJpaRepository.count(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java new file mode 100644 index 0000000..fcbc333 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugDetailJpaRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugDetailEntity; + +@Repository +public interface DrugDetailJpaRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java new file mode 100644 index 0000000..5b6c46f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugImgRepository.java @@ -0,0 +1,10 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; + +@Repository +public interface DrugImgRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java new file mode 100644 index 0000000..a21d2ba --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/persistence/repository/jpa/repository/DrugJpaRepository.java @@ -0,0 +1,25 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.jpa.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; + +public interface DrugJpaRepository extends JpaRepository { + + @Query(""" + SELECT d + FROM DrugRawDataEntity d + WHERE d.isGeneral = true + AND d.isHerbal = false + """) + Page findByIsGeneral(Pageable pageable); + + @Query("SELECT MIN(d.drugId) FROM DrugRawDataEntity d") + Long findMinDrugId(); + + @Query("SELECT MAX(d.drugId) FROM DrugRawDataEntity d") + Long findMaxDrugId(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java new file mode 100644 index 0000000..9b6a355 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugFieldTypeMapper.java @@ -0,0 +1,62 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.domain.model.vo.Material; + +import java.util.List; +import java.util.Map; + +public class DrugFieldTypeMapper { + public static List parseMaterials(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("Material ํŒŒ์‹ฑ ์‹คํŒจ", e); + } + } + + public static List parseStringToList(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("String to list ํŒŒ์‹ฑ ์‹คํŒจ", e); + } + } + + public static Map> parsePrecaution(String json) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(json, new TypeReference>>() {}); + } catch (Exception e) { + throw new RuntimeException("precaution ํŒŒ์‹ฑ ์‹คํŒจ", e); + } + } + + public static float[] parseJsonToFloatArray(String json) { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(json, float[].class); + } catch (Exception e) { + throw new RuntimeException("Failed to parse vector JSON", e); + } + } + + public static String convertSingleStringForEfficacy(List stringList) { + log(LogLevel.DEBUG, "์•ฝํ’ˆ ํšจ๋Šฅ ์ •๋ณด ๋‹จ์ผ ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ ์‹œ์ž‘"); + StringBuilder stringBuilder = new StringBuilder(); + for (String s : stringList) { + stringBuilder.append(s); + stringBuilder.append(" "); + } + + String s = stringBuilder.toString(); + log(LogLevel.DEBUG, "์•ฝํ’ˆ ํšจ๋Šฅ ์ •๋ณด ๋‹จ์ผ ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ ์™„๋ฃŒ" + s); + return s; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java new file mode 100644 index 0000000..4f6403a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugImageRequestMapper.java @@ -0,0 +1,14 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.dto.DrugImageRequest; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugImgEntity; + +public class DrugImageRequestMapper { + public static DrugImgEntity toEntityFromRequest(DrugImageRequest r){ + return DrugImgEntity.builder() + .drugId(r.getDrugId()) + .productImage(r.getProductImage()) + .pillImage(r.getPillImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java new file mode 100644 index 0000000..3699ca7 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/mapper/DrugRawDataMapper.java @@ -0,0 +1,27 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.drug.domain.model.Drug; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.persistence.repository.entity.DrugRawDataEntity; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.parser.JsonArrayTextParser; + +public class DrugRawDataMapper { + public static Drug toDomainFromEntity(DrugRawDataEntity e) { + return Drug.builder() + .drugId(e.getDrugId()) + .drugName(e.getDrugName()) + .company(e.getCompany()) + .permitDate(e.getPermitDate()) + .isGeneral(e.isGeneral()) + .materialInfo(DrugFieldTypeMapper.parseMaterials(e.getMaterialInfo())) + .storeMethod(e.getStoreMethod()) + .validTerm(e.getValidTerm()) + .efficacy(JsonArrayTextParser.extractAndClean(e.getEfficacy())) + .usage(DrugFieldTypeMapper.parseStringToList(e.getUsage())) + .precaution(DrugFieldTypeMapper.parsePrecaution(e.getPrecaution())) + .imageUrl(e.getImageUrl()) + .cancelDate(e.getCancelDate()) + .cancelName(e.getCancelName()) + .isHerbal(e.getIsHerbal()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java new file mode 100644 index 0000000..9a11065 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/infrastructure/support/parser/JsonArrayTextParser.java @@ -0,0 +1,145 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.support.parser; + + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.common.logging.util.LogUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * ์ตœ์ƒ์œ„ JSON ๋ฐฐ์—ด์—์„œ ๊ฐ ์š”์†Œ(๋ฌธ์ž์—ด)์„ ์ถ”์ถœํ•˜๊ณ , + * ์ •๊ทœ์‹์„ ์ด์šฉํ•ด HTML ํƒœ๊ทธ ์ œ๊ฑฐ, HTML ์—”ํ‹ฐํ‹ฐ(์˜ˆ: •, •) ๋””์ฝ”๋”ฉ,   ๋“ฑ์„ ์ œ๊ฑฐํ•œ ๋’ค + * ๊นจ๋—ํ•œ ํ…์ŠคํŠธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + *

์˜ˆ์‹œ JSON: ["์ฒซ๋ฒˆ์งธ ํ…์ŠคํŠธ•", "ํ…์ŠคํŠธ ์˜ˆ์‹œ"]

+ *

โ†’ ๋ฆฌํ„ด: ["์ฒซ๋ฒˆ์งธ ํ…์ŠคํŠธโ€ข", "ํ…์ŠคํŠธ ์˜ˆ์‹œ"]

+ * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-27 + * @modified 2025-04-27 + */ +public class JsonArrayTextParser { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + // HTML ํƒœ๊ทธ ์ œ๊ฑฐ์šฉ ์ •๊ทœ์‹ + private static final Pattern TAG_REGEX = Pattern.compile("<[^>]+>"); + // 10์ง„์ˆ˜ HTML ์—”ํ‹ฐํ‹ฐ ๋””์ฝ”๋”ฉ์šฉ ์ •๊ทœ์‹ (์˜ˆ: •) + private static final Pattern DECIMAL_ENTITY_REGEX = Pattern.compile("&#(\\d+);"); + // 16์ง„์ˆ˜ HTML ์—”ํ‹ฐํ‹ฐ ๋””์ฝ”๋”ฉ์šฉ ์ •๊ทœ์‹ (์˜ˆ: •) + private static final Pattern HEX_ENTITY_REGEX = Pattern.compile("&#x([0-9A-Fa-f]+);"); + + /** + * JSON ๋ฌธ์ž์—ด ์ตœ์ƒ์œ„๊ฐ€ ๋ฐฐ์—ด์ผ ๋•Œ, ๊ฐ ์š”์†Œ๋ฅผ ํ…์ŠคํŠธ๋กœ ํŒŒ์‹ฑํ•˜๊ณ  HTML ํƒœ๊ทธ, HTML ์—”ํ‹ฐํ‹ฐ,   ๋“ฑ์„ ์ œ๊ฑฐํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param json JSON ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + * @return ์ •์ œ๋œ ํ…์ŠคํŠธ ๋ฆฌ์ŠคํŠธ + * @throws IOException JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + */ + public static List extractAndClean(String json) { + try{ + JsonNode root = objectMapper.readTree(json); + List texts = new ArrayList<>(); + + if (!root.isArray()) { + return texts; + } + + for (JsonNode element : root) { + if (element.isTextual()) { + String raw = element.asText().trim(); + if (raw.isEmpty()) { + continue; + } + + // 1) HTML ํƒœ๊ทธ ์ œ๊ฑฐ + String noHtml = TAG_REGEX.matcher(raw).replaceAll(""); + // 2)   ๋“ฑ์„ ์ผ๋ฐ˜ ๊ณต๋ฐฑ์œผ๋กœ ์น˜ํ™˜ + String withSpaces = noHtml.replaceAll(" ", " "); + // 3) HTML ์—”ํ‹ฐํ‹ฐ ๋””์ฝ”๋”ฉ (10์ง„์ˆ˜ ๋ฐ 16์ง„์ˆ˜) + String decoded = decodeHtmlEntities(withSpaces); + // 4) ์ตœ์ข… ํŠธ๋ฆฌ๋ฐ + String clean = decoded.trim(); + + if (!clean.isEmpty()) { + texts.add(clean); + } + } + } + return texts; + } catch (Exception e){ + LogUtil.log(LogLevel.ERROR, "ํ…์ŠคํŠธ ์ •์ œ ์‹คํŒจ", e); + return null; + } + + + } + + /** + * HTML ์—”ํ‹ฐํ‹ฐ(10์ง„์ˆ˜ &#DDD; ๋ฐ 16์ง„์ˆ˜ &#xHHHH;)๋ฅผ ๋Œ€์‘ํ•˜๋Š” ๋ฌธ์ž๋กœ ๋””์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: "foo•bar" โ†’ "fooโ€ขbar", "foo•bar" โ†’ "fooโ€ขbar" + * + * @param input ์—”ํ‹ฐํ‹ฐ๋ฅผ ํฌํ•จํ•œ ๋ฌธ์ž์—ด + * @return ๋””์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + private static String decodeHtmlEntities(String input) { + String result = decodeDecimalEntities(input); + return decodeHexEntities(result); + } + + /** + * 10์ง„์ˆ˜ HTML ์—”ํ‹ฐํ‹ฐ(์˜ˆ: •)๋ฅผ ๋ฌธ์ž๋กœ ๋””์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค. + * ๋‚ด๋ถ€์ ์œผ๋กœ ์ •๊ทœํ‘œํ˜„์‹์„ ์ด์šฉํ•ด &#์ˆซ์ž; ํŒจํ„ด์„ ์ฐพ์•„ ๋Œ€์‘ํ•˜๋Š” ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋””์ฝ”๋”ฉํ•  ๋ฌธ์ž์—ด + * @return 10์ง„์ˆ˜ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋””์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + private static String decodeDecimalEntities(String input) { + Matcher decMatcher = DECIMAL_ENTITY_REGEX.matcher(input); + StringBuffer sb = new StringBuffer(); + + while (decMatcher.find()) { + int code = Integer.parseInt(decMatcher.group(1)); + decMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + + decMatcher.appendTail(sb); + return sb.toString(); + } + + /** + * 16์ง„์ˆ˜ HTML ์—”ํ‹ฐํ‹ฐ(์˜ˆ: •)๋ฅผ ๋ฌธ์ž๋กœ ๋””์ฝ”๋”ฉํ•ฉ๋‹ˆ๋‹ค. + * ๋‚ด๋ถ€์ ์œผ๋กœ ์ •๊ทœํ‘œํ˜„์‹์„ ์ด์šฉํ•ด &#xํ—ฅ์‚ฌ๊ฐ’; ํŒจํ„ด์„ ์ฐพ์•„ ๋Œ€์‘ํ•˜๋Š” ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋””์ฝ”๋”ฉํ•  ๋ฌธ์ž์—ด + * @return 16์ง„์ˆ˜ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋””์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + private static String decodeHexEntities(String input) { + Matcher hexMatcher = HEX_ENTITY_REGEX.matcher(input); + StringBuffer sb = new StringBuffer(); + + while (hexMatcher.find()) { + int code = Integer.parseInt(hexMatcher.group(1), 16); + hexMatcher.appendReplacement(sb, + Matcher.quoteReplacement(Character.toString((char) code))); + } + + hexMatcher.appendTail(sb); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/DrugScraperController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/DrugScraperController.java new file mode 100644 index 0000000..28d8272 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/DrugScraperController.java @@ -0,0 +1,53 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller; + +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; + +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * ์˜์•ฝํ’ˆ ์ •๋ณด ์ˆ˜์ง‘ ์ „์ฒด ์ž‘์—…์„ ์ œ์–ดํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค. + * ์ž‘์—… ์‹œ์ž‘, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/job/scraper") +public class DrugScraperController implements DrugScraperControllerDocs { + private final DrugScraperUseCase drugScraperUsecase; + + @Override + @PostMapping("/start") + public ResponseEntity> start() { + return success(drugScraperUsecase.scraperStart()); + } + + @Override + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperUsecase.stop()); + } + + @Override + @PostMapping("/restart") + public ResponseEntity> restart() { + return success(drugScraperUsecase.restart()); + } + + @Override + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperUsecase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/combine/DrugScraperTableCombineController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/combine/DrugScraperTableCombineController.java new file mode 100644 index 0000000..364b88c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/combine/DrugScraperTableCombineController.java @@ -0,0 +1,70 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.combine; + +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; + +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperTableCombineControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperTableCombineUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—…์„ ์ œ์–ดํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค. + * ์ž‘์—… ์‹œ์ž‘, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/scraper/combine") +@RequiredArgsConstructor +public class DrugScraperTableCombineController implements DrugScraperTableCombineControllerDocs { + private final DrugScraperTableCombineUseCase drugScraperTableCombineUsecase; + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด ํ…Œ์ด๋ธ”์„ ๋ณ‘ํ•ฉํ•˜๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @PostMapping("/start") + public ResponseEntity> start(){ + return success(drugScraperTableCombineUsecase.mergeTable()); + } + + /** + * ์ง„ํ–‰ ์ค‘์ธ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperTableCombineUsecase.stop()); + } + + /** + * ์ž‘์—…์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperTableCombineUsecase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/details/DrugScraperDetailController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/details/DrugScraperDetailController.java new file mode 100644 index 0000000..ea1ef30 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/details/DrugScraperDetailController.java @@ -0,0 +1,70 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.details; + +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; + +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperDetailControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperDetailUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์ œ์–ดํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค. + * ์ž‘์—… ์‹œ์ž‘, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/scraper/details") +@RequiredArgsConstructor +public class DrugScraperDetailController implements DrugScraperDetailControllerDocs { + private final DrugScraperDetailUseCase drugScraperDetailUseCase; + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @PostMapping("/start") + public ResponseEntity> start() { + return success(drugScraperDetailUseCase.requestAllData()); + } + + /** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperDetailUseCase.stop()); + } + + /** + * ์ž‘์—…์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperDetailUseCase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java new file mode 100644 index 0000000..ba32d8c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperControllerDocs.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * ์˜์•ฝํ’ˆ ์ •๋ณด ์ˆ˜์ง‘ ์ „์ฒด ์ž‘์—… API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * ์ด API๋Š” ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘, ์ด๋ฏธ์ง€ ์ •๋ณด ์ˆ˜์ง‘, ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ, ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ ๋“ฑ + * ์ „์ฒด ์Šคํฌ๋ž˜ํ•‘ ํŒŒ์ดํ”„๋ผ์ธ์„ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper", description = "์˜์•ฝํ’ˆ ์ •๋ณด ์ˆ˜์ง‘ ์ „์ฒด ์ž‘์—…์„ ์ œ์–ดํ•˜๋Š” API") +public interface DrugScraperControllerDocs { + + @Operation( + summary = "์ „์ฒด ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—… ์‹œ์ž‘", + description = "์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘, ์ด๋ฏธ์ง€ ์ˆ˜์ง‘, ๋ณ‘ํ•ฉ, ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ์ƒ์„ฑ์„ ํฌํ•จํ•œ ์ „์ฒด ์Šคํฌ๋ž˜ํ•‘ ํŒŒ์ดํ”„๋ผ์ธ์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. " + + "์ž‘์—…์ด ์ •์ƒ์ ์œผ๋กœ ์‹œ์ž‘๋˜๋ฉด ์ž‘์—… ID ๋˜๋Š” ์‹œ์ž‘ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> start(); + + @Operation( + summary = "์ง„ํ–‰ ์ค‘์ธ ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—… ์ค‘์ง€", + description = "ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์ „์ฒด ์Šคํฌ๋ž˜ํ•‘ ํŒŒ์ดํ”„๋ผ์ธ ์ž‘์—…์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. " + + "์ค‘๋‹จ๋œ ์ƒํƒœ๋Š” ์œ ์ง€๋˜์–ด ์žฌ์‹œ์ž‘ ์‹œ ์ค‘๋‹จ๋œ ์ง€์ ๋ถ€ํ„ฐ ์žฌ๊ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค." + ) + ResponseEntity> stop(); + + @Operation( + summary = "์Šคํฌ๋ž˜ํ•‘ ์ž‘์—… ์žฌ์‹œ์ž‘", + description = "์ค‘๋‹จ๋œ ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์„ ์ค‘๋‹จ ์‹œ์ ๋ถ€ํ„ฐ ์žฌ๊ฐœํ•˜์—ฌ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. " + + "restart ์—”๋“œํฌ์ธํŠธ ํ˜ธ์ถœ ์‹œ ์ด์ „ ์ž‘์—… ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด์–ด์„œ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> restart(); + + @Operation( + summary = "์Šคํฌ๋ž˜ํ•‘ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ", + description = "ํ˜„์žฌ ์ „์ฒด ์Šคํฌ๋ž˜ํ•‘ ์ž‘์—…์˜ ์ง„ํ–‰ ์ƒํƒœ(๋‹จ๊ณ„, ์™„๋ฃŒ ๋น„์œจ, ์˜ค๋ฅ˜ ์—ฌ๋ถ€ ๋“ฑ)๋ฅผ ์กฐํšŒํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java new file mode 100644 index 0000000..9b3728d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperDetailControllerDocs.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—… API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper Detail", description = "์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—… API") +public interface DrugScraperDetailControllerDocs { + + @Operation( + summary = "์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—… ์‹œ์ž‘", + description = "์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> start(); + + @Operation( + summary = "์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—… ์ค‘์ง€", + description = "์ง„ํ–‰ ์ค‘์ธ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> stop(); + + @Operation( + summary = "์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ", + description = "ํ˜„์žฌ ์ƒ์„ธ์ •๋ณด ์ˆ˜์ง‘ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java new file mode 100644 index 0000000..a039151 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperImageControllerDocs.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—… API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper Image", description = "์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—… API") +public interface DrugScraperImageControllerDocs { + + @Operation( + summary = "์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—… ์‹œ์ž‘", + description = "์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> start(); + + @Operation( + summary = "์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—… ์ค‘์ง€", + description = "์ง„ํ–‰ ์ค‘์ธ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> stop(); + + @Operation( + summary = "์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ", + description = "ํ˜„์žฌ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java new file mode 100644 index 0000000..7e25e69 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/docs/DrugScraperTableCombineControllerDocs.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * ์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—… API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Drug Scraper Combine", description = "์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—… API") +public interface DrugScraperTableCombineControllerDocs { + + @Operation( + summary = "ํ…Œ์ด๋ธ” ๋ณ‘ํ•ฉ ์ž‘์—… ์‹œ์ž‘", + description = "์˜์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด์™€ ์ด๋ฏธ์ง€ ์ •๋ณด ํ…Œ์ด๋ธ”์„ ๋ณ‘ํ•ฉํ•˜๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> start(); + + @Operation( + summary = "๋ณ‘ํ•ฉ ์ž‘์—… ์ค‘์ง€", + description = "์ง„ํ–‰ ์ค‘์ธ ๋ณ‘ํ•ฉ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> stop(); + + @Operation( + summary = "๋ณ‘ํ•ฉ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ", + description = "ํ˜„์žฌ ๋ณ‘ํ•ฉ ์ž‘์—…์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> getBatchProgress(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/image/DrugScraperImageController.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/image/DrugScraperImageController.java new file mode 100644 index 0000000..8619848 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/scraper/presentation/controller/image/DrugScraperImageController.java @@ -0,0 +1,71 @@ +package com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.image; + +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; + +import com.likelion.backendplus4.yakplus.drug.scraper.presentation.controller.docs.DrugScraperImageControllerDocs; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.likelion.backendplus4.yakplus.drug.scraper.application.port.in.DrugScraperImageUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์ œ์–ดํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์ž…๋‹ˆ๋‹ค. + * ์ž‘์—… ์‹œ์ž‘, ์ค‘์ง€, ์ƒํƒœ ์กฐํšŒ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ + +@RestController +@RequestMapping("/scraper/images") +@RequiredArgsConstructor +public class DrugScraperImageController implements DrugScraperImageControllerDocs { + private final DrugScraperImageUseCase drugScraperImageUsecase; + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @PostMapping("/start") + public ResponseEntity> start(){ + return success(drugScraperImageUsecase.requestAllData()); + } + + /** + * ์˜์•ฝํ’ˆ ์ด๋ฏธ์ง€ ์ˆ˜์ง‘ ์ž‘์—…์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ง€ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @DeleteMapping("/stop") + public ResponseEntity> stop() { + return success(drugScraperImageUsecase.stop()); + } + + /** + * ์ž‘์—…์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž‘์—… ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + @Override + @GetMapping("/status") + public ResponseEntity> getBatchProgress() { + return success(drugScraperImageUsecase.getStatus()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/drug/support/JobManager.java b/src/main/java/com/likelion/backendplus4/yakplus/drug/support/JobManager.java new file mode 100644 index 0000000..6edc3e8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/drug/support/JobManager.java @@ -0,0 +1,192 @@ +package com.likelion.backendplus4.yakplus.drug.support; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.*; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.core.task.TaskExecutor; +import org.springframework.stereotype.Component; + +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.error.ParserBatchError; +import com.likelion.backendplus4.yakplus.drug.scraper.infrastructure.batch.exception.ParserBatchException; + +import lombok.RequiredArgsConstructor; + +/** + * Spring Batch Job ์‹คํ–‰ ๋ฐ ์ œ์–ด๋ฅผ ์œ„ํ•œ ๋งค๋‹ˆ์ € ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * Job ์‹คํ–‰, ์ค‘๋‹จ, ์ƒํƒœ ์กฐํšŒ, ์žฌ์‹œ์ž‘ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@Component +@RequiredArgsConstructor +public class JobManager { + private final String normalExecutorName = "normalExecutor"; + + private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final JobLauncher jobLauncher; + private final JobOperator jobOperator; + private final JobExplorer jobExplorer; + + private final Map taskExecutorMap; + + private final Set stoppedExecutionIds = new ConcurrentSkipListSet<>(); + + /** + * ์ง€์ •๋œ Job์„ ๋น„๋™๊ธฐ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * ๋งŒ์•ฝ ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ธ Job์ด ์žˆ๋‹ค๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param job ์‹คํ–‰ํ•  Job + * @return ์‹คํ–‰ ์š”์ฒญ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + public String startJob(Job job) { + IfAlreadyRunThrowException(); + TaskExecutor taskExecutor = taskExecutorMap.get(normalExecutorName); + log("๋ฐฐ์น˜ ์‹คํ–‰ ์‹œ์ž‘ - Job: " + job.getName()); + isRunning.set(true); + taskExecutor.execute(() -> { + try { + JobParameters params = new JobParametersBuilder() + .addLong("requestTime", System.currentTimeMillis()) + .toJobParameters(); + + jobLauncher.run(job, params); + } catch (Exception e) { + log(LogLevel.ERROR, "Job ์‹คํ–‰ ์š”์ฒญ์€ ์ •์ƒ์ ์œผ๋กœ ๋ฐ›์•˜์œผ๋‚˜ ์‹คํ–‰์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. " + job.getName()); + throw new ParserBatchException(ParserBatchError.JOB_RUN_FAIL); + } finally { + isRunning.set(false); + } + }); + + return "๋ฐฐ์น˜ ์‹คํ–‰ ์š”์ฒญ ์ˆ˜๋ฝ๋จ"; + } + + /** + * ์ค‘๋‹จ๋œ JobExecution์„ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์žฌ์‹œ์ž‘ ์š”์ฒญ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + public String restart() { + IfAlreadyRunThrowException(); + TaskExecutor taskExecutor = taskExecutorMap.get(normalExecutorName); + taskExecutor.execute(() -> { + for (Long id : new HashSet<>(stoppedExecutionIds)) { + try { + isRunning.set(true); + jobOperator.restart(id); + } catch (Exception e) { + log(LogLevel.ERROR, "Job ์žฌ ์‹คํ–‰ ์š”์ฒญ์€ ์ •์ƒ์ ์œผ๋กœ ๋ฐ›์•˜์œผ๋‚˜ ์‹คํ–‰์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค."); + throw new ParserBatchException(ParserBatchError.JOB_RUN_FAIL); + } finally { + isRunning.set(false); + } + } + }); + + stoppedExecutionIds.clear(); + return "์žฌ์‹œ์ž‘ ์š”์ฒญ ์„ฑ๊ณต"; + } + + /** + * ์ง€์ •๋œ Job์˜ ์ตœ๊ทผ ์‹คํ–‰ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param job ์กฐํšŒํ•  Job + * @return ์ƒํƒœ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + public String getJobStatus(Job job) { + List instances = jobExplorer.getJobInstances(job.getName(), 0, 30); + if (instances.isEmpty()) { + return "์‹คํ–‰๋œ Job์ด ์—†์Šต๋‹ˆ๋‹ค."; + } + + for (JobInstance instance : instances) { + List executions = jobExplorer.getJobExecutions(instance); + for (JobExecution execution : executions) { + if (execution.isRunning()) { + StringBuilder status = new StringBuilder("์‹คํ–‰ ์ค‘์ธ Step ์ƒํƒœ: {"); + for (StepExecution step : execution.getStepExecutions()) { + long read = step.getReadCount(); + status.append("Step: ").append(step.getStepName()) + .append(", Read: ").append(read) + .append(", Skip: ").append(step.getSkipCount()); + } + return status.toString().trim(); + } + } + } + + return "์‹คํ–‰ ์ค‘์ธ Job์ด ์—†์Šต๋‹ˆ๋‹ค."; + } + + /** + * ์ง€์ •๋œ Job์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param job ์ค‘์ง€ํ•  Job + * @return ์ค‘์ง€ ์š”์ฒญ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + public String stopRunningBatch(Job job) { + String jobName = job.getName(); + log("๋ฐฐ์น˜ ์ž‘์—… ์ค‘๋‹จ ์š”์ฒญ:" + jobName); + Collection instances = jobExplorer.getJobInstances(jobName, 0, 30); + if (!instances.isEmpty()) { + instances.stream() + .map(jobExplorer::getJobExecutions) + .flatMap(List::stream) + .filter(JobExecution::isRunning) + .forEach(exec -> { + try { + jobOperator.stop(exec.getId()); + } catch (Exception e) { + log("์ด๋ฏธ ์ค‘๋‹จ ์ค‘์ด๊ฑฐ๋‚˜ ์ค‘๋‹จ ๋ถˆ๊ฐ€ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. executionId=" + exec.getId()); + } finally { + stoppedExecutionIds.add(exec.getId()); + } + }); + } + isRunning.set(false); + return "์ค‘๋‹จ ์š”์ฒญ ์™„๋ฃŒ"; + } + + /** + * ์‹คํ–‰ ์ค‘์ธ ๋ฐฐ์น˜ ์ž‘์—…์ด ์žˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @throws ParserBatchException ์ค‘๋ณต ์‹คํ–‰ ์˜ˆ์™ธ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-05-02 + */ + private void IfAlreadyRunThrowException() { + if (isRunning.get()) { + throw new ParserBatchException(ParserBatchError.ALREADY_RUN); + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java b/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java deleted file mode 100644 index 580fc12..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.likelion.backendplus4.yakplus.logtest; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -/** - * ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค - * - * @modified 2025-04-18 - * @since 2025-04-16 - */ -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class MyController { - - private final MyService myService; - - /** - * ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ - * - * @return ResponseEntity ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ - * @author ์ •์•ˆ์‹ - * @modified 2025-04-18 - * @since 2025-04-16 - */ - @GetMapping("/process") - public ResponseEntity process() { - log(LogLevel.INFO, LogMessage.DATA_PROCESSING_START.getMessage()); - - try { - String result = myService.processData(); - log(LogLevel.INFO, LogMessage.DATA_PROCESSING_SUCCESS.getMessage()); - return ResponseEntity.ok(result); - } catch (Exception e) { - log(LogLevel.ERROR, LogMessage.DATA_PROCESSING_ERROR.getMessage(), e); - return ResponseEntity.internalServerError() - .body(LogMessage.DATA_PROCESSING_ERROR.getMessage()); - } - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java b/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java deleted file mode 100644 index 3480559..0000000 --- a/src/main/java/com/likelion/backendplus4/yakplus/logtest/MyService.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.likelion.backendplus4.yakplus.logtest; - -import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; -import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; -import org.springframework.stereotype.Service; - -import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; - -/** - * ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์„œ๋น„์Šค ํด๋ž˜์Šค - * - * @modified 2025-04-18 - * @since 2025-04-16 - */ -@Service -public class MyService { - - private static final long PROCESSING_DELAY = 1000L; - - /** - * ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ - * - * @return String ์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ ๊ฒฐ๊ณผ - * @throws RuntimeException ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ - * @author ์ •์•ˆ์‹ - * @modified 2025-04-18 - * @since 2025-04-16 - */ - public String processData() { - log(LogLevel.INFO, LogMessage.SERVICE_DATA_PROCESSING_START.getMessage()); - - try { - simulateProcessing(); - log(LogLevel.INFO, LogMessage.SERVICE_DATA_PROCESSING_SUCCESS.getMessage()); - return LogMessage.PROCESSED_DATA_RESULT.getMessage(); - } catch (InterruptedException e) { - log(LogLevel.ERROR, LogMessage.SERVICE_DATA_PROCESSING_ERROR.getMessage(), e); - Thread.currentThread().interrupt(); - throw new RuntimeException(LogMessage.SERVICE_DATA_PROCESSING_ERROR.getMessage(), e); - } - } - - /** - * ์ฒ˜๋ฆฌ ๊ณผ์ •์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” private ๋ฉ”์„œ๋“œ - * - * @throws InterruptedException ์ธํ„ฐ๋ŸฝํŠธ ๋ฐœ์ƒ ์‹œ - * @author ์ •์•ˆ์‹ - * @modified 2025-04-18 - * @since 2025-04-16 - */ - private void simulateProcessing() throws InterruptedException { - Thread.sleep(PROCESSING_DELAY); - } -} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java new file mode 100644 index 0000000..ac3bd75 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/in/DictionaryUseCase.java @@ -0,0 +1,20 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.in; + +public interface DictionaryUseCase { + + /** + * ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ ์„ค์ • ๋ฉ”์„œ๋“œ + *

+ * 1) JSON ํŒŒ์ผ์—์„œ ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ์–ด๋“ค์—ฌ, + * 2) ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ํ•ญ๋ชฉ์„ JPA DB์— ์ €์žฅํ•˜๊ณ , + * 3) Elasticsearch์—๋„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + *

+ * ๋‚ด๋ถ€ ๋กœ๊ทธ๋Š” ๊ฐ ๋‹จ๊ณ„๋ณ„ ์™„๋ฃŒ ์‹œ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค. + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-03 + * @since 2025-05-01 + */ + void setDictionary(); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java new file mode 100644 index 0000000..95b0442 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryElsRepositoryPort.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out; + +import java.util.Set; + +public interface SymptomDictionaryElsRepositoryPort { + + /** + * ์ฃผ์–ด์ง„ ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch์— ์‹ ๊ทœ ์ฆ์ƒ ์‚ฌ์ „ ๋ฌธ์„œ๋ฅผ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * 1) DictionaryMapper๋ฅผ ํ†ตํ•ด Document ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค, + * 2) repository.saveAll์„ + * ํ˜ธ์ถœํ•˜์—ฌ ์ผ๊ด„ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symptoms ์ €์žฅํ•  ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-01 + * @since 2025-04-30 + */ + void setDictionary(Set symptoms); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java new file mode 100644 index 0000000..0ba9a3c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryJpaRepositoryPort.java @@ -0,0 +1,23 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out; + +import java.util.Set; + +public interface SymptomDictionaryJpaRepositoryPort { + + + /** + * ์ฃผ์–ด์ง„ ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์‹ ๊ทœ ์ฆ์ƒ ๋‹จ์–ด๋งŒ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * 1) ๊ธฐ์กด์— ์ €์žฅ๋œ ์ฆ์ƒ๋ช…์„ ์กฐํšŒํ•˜๊ณ , + * 2) ์ „๋‹ฌ๋ฐ›์€ ๋ฆฌ์ŠคํŠธ์—์„œ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ๋‹จ์–ด๋งŒ ํ•„ํ„ฐ๋งํ•œ ํ›„, + * 3) ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param symptoms ์ €์žฅํ•  ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-01 + * @since 2025-04-30 + */ + void setDictionary(Set symptoms); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java new file mode 100644 index 0000000..27f7bd9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/port/out/SymptomDictionaryLoaderPort.java @@ -0,0 +1,21 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out; + +import com.likelion.backendplus4.yakplus.symptomdictionary.exception.DictionaryException; + +import java.util.Set; + +public interface SymptomDictionaryLoaderPort { + + /** + * ClassPathResource๋ฅผ ํ†ตํ•ด ์ง€์ •๋œ JSON ํŒŒ์ผ์„ ์ฝ๊ณ , + * Set ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return JSON์— ์ •์˜๋œ ์ฆ์ƒ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ + * @throws DictionaryException ํŒŒ์ผ ํ˜•์‹ ์˜ค๋ฅ˜ ๋˜๋Š” ํŒŒ์‹ฑ/IO ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @since 2025-04-30 + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-01 + */ + Set loadDictionary(); + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java new file mode 100644 index 0000000..640d41a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/application/service/DictionaryService.java @@ -0,0 +1,59 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.application.service; + +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.in.DictionaryUseCase; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryElsRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out.JsonSymptomDictionaryLoader; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Set; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +/** + * ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-01 + * @modified 2025-05-03 + */ +@Service +@RequiredArgsConstructor +public class DictionaryService implements DictionaryUseCase { + + private static final String INDENT = " "; + + private final JsonSymptomDictionaryLoader jsonSymptomDictionaryLoader; + private final SymptomDictionaryJpaRepositoryPort dictionaryRepositoryPort; + private final SymptomDictionaryElsRepositoryPort dictionaryElsRepositoryPort; + + /** + * ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ ์„ค์ • ๋ฉ”์„œ๋“œ + * 1) JSON ํŒŒ์ผ์—์„œ ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ์–ด๋“ค์—ฌ, + * 2) ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ํ•ญ๋ชฉ์„ JPA DB์— ์ €์žฅํ•˜๊ณ , + * 3) Elasticsearch์—๋„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * ๋‚ด๋ถ€ ๋กœ๊ทธ๋Š” ๊ฐ ๋‹จ๊ณ„๋ณ„ ์™„๋ฃŒ ์‹œ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค. + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-03 + * @since 2025-05-01 + */ + @Override + public void setDictionary() { + log("DictionaryService.setDictionary() ํ˜ธ์ถœ ์‹œ์ž‘"); + + Set symptomList = jsonSymptomDictionaryLoader.loadDictionary(); + log(INDENT+"loadDictionary() ์™„๋ฃŒ, ์ฆ์ƒ ์ˆ˜: " + symptomList.size()); + + dictionaryRepositoryPort.setDictionary(symptomList); + log(INDENT+"SymptomDictionaryJpaAdapter.setDictionary() ์™„๋ฃŒ"); + + dictionaryElsRepositoryPort.setDictionary(symptomList); + log(INDENT+"SymptomDictionaryElsAdapter.setDictionary() ์™„๋ฃŒ"); + + log("DictionaryService.setDictionary() ํ˜ธ์ถœ ์ข…๋ฃŒ"); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/DictionaryException.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/DictionaryException.java new file mode 100644 index 0000000..4e0e93e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/DictionaryException.java @@ -0,0 +1,19 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class DictionaryException extends CustomException { + + private final ErrorCode errorCode; + + public DictionaryException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/error/DictionaryErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/error/DictionaryErrorCode.java new file mode 100644 index 0000000..ed94e85 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/exception/error/DictionaryErrorCode.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum DictionaryErrorCode implements ErrorCode { + + INVALID_FILE_TYPE(HttpStatus.BAD_REQUEST, 11001, "์‚ฌ์ „ ํŒŒ์ผ ํ˜•์‹์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค: .json ํŒŒ์ผ๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค"), + DICTIONARY_LOAD_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, 54001, "์ฆ์ƒ ์‚ฌ์ „ ๋กœ๋”ฉ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"); + + private final HttpStatus status; + private final int code; + private final String message; + + @Override + public HttpStatus httpStatus() { + return status; + } + + @Override + public int codeNumber() { + return code; + } + + @Override + public String message() { + return message; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java new file mode 100644 index 0000000..851b416 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/JsonSymptomDictionaryLoader.java @@ -0,0 +1,66 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.logging.util.LogLevel; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryLoaderPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.exception.DictionaryException; +import com.likelion.backendplus4.yakplus.symptomdictionary.exception.error.DictionaryErrorCode; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +/** + * classpath์— ์œ„์น˜ํ•œ JSON ํŒŒ์ผ๋กœ๋ถ€ํ„ฐ ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * SymptomDictionaryLoaderPort๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ JSON ํŒŒ์‹ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-04-30 + * @modified 2025-05-03 + */ +@RequiredArgsConstructor +@Component +public class JsonSymptomDictionaryLoader implements SymptomDictionaryLoaderPort { + + private final ObjectMapper objectMapper; + private final ResourceLoader resourceLoader; + + + @Value("${dictionary.path}") + private String DICTIONARY_FILE_PATH; + + /** + * ClassLoader๋ฅผ ํ†ตํ•ด ์ง€์ •๋œ JSON ํŒŒ์ผ์„ ์ฝ๊ณ , + * Set ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return JSON์— ์ •์˜๋œ ์ฆ์ƒ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ + * @throws DictionaryException ํŒŒ์ผ ํ˜•์‹ ์˜ค๋ฅ˜ ๋˜๋Š” ํŒŒ์‹ฑ/IO ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @since 2025-04-30 + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + */ + @Override + public Set loadDictionary() { + log("loadDictionary() ํ˜ธ์ถœ - ๊ฒฝ๋กœ: " + DICTIONARY_FILE_PATH); + + if (!DICTIONARY_FILE_PATH.toLowerCase().endsWith(".json")) { + throw new DictionaryException(DictionaryErrorCode.INVALID_FILE_TYPE); + } + + try (InputStream in = resourceLoader.getResource(DICTIONARY_FILE_PATH).getInputStream()) { + Set result = objectMapper.readValue(in, new TypeReference<>() {}); + log("์‚ฌ์ „ ๋กœ๋”ฉ ์™„๋ฃŒ - ํฌ๊ธฐ: " + result.size()); + return result; + } catch (IOException e) { + log(LogLevel.ERROR, "์‚ฌ์ „ ํŒŒ์ผ ๋กœ๋”ฉ ์‹คํŒจ", e); + throw new DictionaryException(DictionaryErrorCode.DICTIONARY_LOAD_FAILURE); + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java new file mode 100644 index 0000000..9417c36 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryElsAdapter.java @@ -0,0 +1,53 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; + +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryElsRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.SymptomDictionaryDocRepository; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.support.mapper.DictionaryMapper; +import java.util.List; +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Elasticsearch๋ฅผ ์ด์šฉํ•ด ์ฆ์ƒ ์‚ฌ์ „ ๋ฌธ์„œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-04-30 + * @modified 2025-05-01 + */ +@Component +@RequiredArgsConstructor +public class SymptomDictionaryElsAdapter implements SymptomDictionaryElsRepositoryPort { + + private final SymptomDictionaryDocRepository repository; + + /** + * ์ฃผ์–ด์ง„ ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch์— ์‹ ๊ทœ ์ฆ์ƒ ์‚ฌ์ „ ๋ฌธ์„œ๋ฅผ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * 1) DictionaryMapper๋ฅผ ํ†ตํ•ด Document ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค, + * 2) repository.saveAll์„ + * ํ˜ธ์ถœํ•˜์—ฌ ์ผ๊ด„ ์ƒ‰์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symptoms ์ €์žฅํ•  ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ + * @since 2025-04-30 + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-01 + */ + @Override + @Transactional + public void setDictionary(Set symptoms) { + log("setDictionary() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ์ž…๋ ฅ ์ฆ์ƒ ์ˆ˜: " + symptoms.size()); + List docs = symptoms.stream() + .map(DictionaryMapper::toSymptomDocument) + .toList(); + + // ์ผ๊ด„ ์ €์žฅ + repository.saveAll(docs); + log("setDictionary() ์™„๋ฃŒ, ์ƒ‰์ธ๋œ ๋ฌธ์„œ ์ˆ˜: " + docs.size()); + + } + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java new file mode 100644 index 0000000..45bb24c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/adapter/out/SymptomDictionaryJpaAdapter.java @@ -0,0 +1,66 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.adapter.out; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.out.SymptomDictionaryJpaRepositoryPort; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.SymptomDictionaryRepository; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.support.mapper.DictionaryMapper; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * JPA๋ฅผ ์ด์šฉํ•ด ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * SymptomDictionaryJpaRepositoryPort๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-01 + * @modified 2025-05-01 + */ +@Component +@RequiredArgsConstructor +public class SymptomDictionaryJpaAdapter implements SymptomDictionaryJpaRepositoryPort { + + private final SymptomDictionaryRepository repository; + + /** + * ์ฃผ์–ด์ง„ ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์‹ ๊ทœ ์ฆ์ƒ ๋‹จ์–ด๋งŒ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * 1) ๊ธฐ์กด์— ์ €์žฅ๋œ ์ฆ์ƒ๋ช…์„ ์กฐํšŒํ•˜๊ณ , + * 2) ์ „๋‹ฌ๋ฐ›์€ ๋ฆฌ์ŠคํŠธ์—์„œ ์ค‘๋ณต๋˜์ง€ ์•Š๋Š” ๋‹จ์–ด๋งŒ ํ•„ํ„ฐ๋งํ•œ ํ›„, + * 3) ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param symptoms ์ €์žฅํ•  ์ฆ์ƒ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ + * @since 2025-04-30 + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-01 + */ + @Override + @Transactional + public void setDictionary(Set symptoms) { + log("setDictionary() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ์ž…๋ ฅ ์ฆ์ƒ ์ˆ˜: " + symptoms.size()); + + // 1) ๊ธฐ์กด์— ์กด์žฌํ•˜๋Š” ์ฆ์ƒ ๋‹จ์–ด ์กฐํšŒ + Set existing = repository.findAll().stream() + .map(SymptomDictionary::getName) + .collect(Collectors.toSet()); + log("setDictionary() ๊ธฐ์กด ์ €์žฅ๋œ ์ฆ์ƒ ์ˆ˜: " + existing.size()); + + // 2) ์‹ ๊ทœ ๋‹จ์–ด๋งŒ ํ•„ํ„ฐ๋ง + List toInsert = symptoms.stream() + .filter(s -> !existing.contains(s)) + .toList(); + log("setDictionary() ์‹ ๊ทœ ์ €์žฅ ๋Œ€์ƒ ์ฆ์ƒ ์ˆ˜: " + toInsert.size()); + + // 3) ์—”ํ‹ฐํ‹ฐ ๋ณ€ํ™˜ ๋ฐ ์ €์žฅ + List entities = toInsert.stream() + .map(DictionaryMapper::toSymptomEntity) + .toList(); + repository.saveAll(entities); + log("setDictionary() ์™„๋ฃŒ, ์ €์žฅ๋œ ์—”ํ‹ฐํ‹ฐ ์ˆ˜: " + entities.size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java new file mode 100644 index 0000000..00e6774 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryDocRepository.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository; + +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +public interface SymptomDictionaryDocRepository extends ElasticsearchRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java new file mode 100644 index 0000000..d8b56bb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/SymptomDictionaryRepository.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository; + +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SymptomDictionaryRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java new file mode 100644 index 0000000..0300808 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/document/SymptomDictionaryDocument.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.CompletionField; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Document(indexName = "symptom_dictionary") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SymptomDictionaryDocument { + + @Id + @Field(type = FieldType.Keyword, name = "symptom") + private String symptom; + + @CompletionField( + analyzer = "symptom_autocomplete", + searchAnalyzer = "symptom_search_autocomplete" + ) + private List symptomSuggester; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java new file mode 100644 index 0000000..ec637ac --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/persistence/repository/entity/SymptomDictionary.java @@ -0,0 +1,28 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +public class SymptomDictionary { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "symptom_dictionary_id") + private Long id; + + @Column(name = "symptom_name", unique = true) + private String name; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/support/mapper/DictionaryMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/support/mapper/DictionaryMapper.java new file mode 100644 index 0000000..8ecfb41 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/infrastructure/support/mapper/DictionaryMapper.java @@ -0,0 +1,45 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.support.mapper; + +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.document.SymptomDictionaryDocument; +import com.likelion.backendplus4.yakplus.symptomdictionary.infrastructure.persistence.repository.entity.SymptomDictionary; +import java.util.List; + +/** + * ์ฆ์ƒ ์‚ฌ์ „ ์—”ํ‹ฐํ‹ฐ ๋ฐ ๋ฌธ์„œ ๊ฐ์ฒด ๊ฐ„ ๋ณ€ํ™˜์„ ๋‹ด๋‹นํ•˜๋Š” ๋งคํผ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * + * @since 2025-05-01 + * @modified 2025-05-01 + */ +public class DictionaryMapper { + + /** + * ๋‹จ์ผ ์ฆ์ƒ๋ช…์„ JPA ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ฆ์ƒ๋ช… ๋ฌธ์ž์—ด + * @return SymptomDictionary JPA ์—”ํ‹ฐํ‹ฐ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-01 + * @modified 2025-05-01 + */ + public static SymptomDictionary toSymptomEntity(String name) { + return SymptomDictionary.builder() + .name(name) + .build(); + } + + /** + * ๋‹จ์ผ ์ฆ์ƒ๋ช…์„ Elasticsearch ์ƒ‰์ธ์šฉ ๋ฌธ์„œ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ฆ์ƒ๋ช… ๋ฌธ์ž์—ด * @return SymptomDictionaryDocument ES ๋ฌธ์„œ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-01 + * @modified 2025-05-01 + */ + public static SymptomDictionaryDocument toSymptomDocument(String name) { + return SymptomDictionaryDocument.builder() + .symptom(name) + .symptomSuggester(List.of(name)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/DictionaryController.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/DictionaryController.java new file mode 100644 index 0000000..d9dd9fb --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/DictionaryController.java @@ -0,0 +1,50 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.presentation.controller; + +import com.likelion.backendplus4.yakplus.symptomdictionary.application.port.in.DictionaryUseCase; +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import com.likelion.backendplus4.yakplus.symptomdictionary.presentation.controller.docs.DictionaryControllerDocs; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.likelion.backendplus4.yakplus.common.logging.util.LogUtil.log; +import static com.likelion.backendplus4.yakplus.common.response.ApiResponse.*; + +/** + * ์‚ฌ์ „ ๊ด€๋ฆฌ์šฉ REST API๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @modified 2025-05-01 + * @since 2025-05-01 + */ +@RestController +@RequestMapping("/dictionary") +@RequiredArgsConstructor +public class DictionaryController implements DictionaryControllerDocs { + + private final DictionaryUseCase dictionaryUseCase; + + /** + * ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ๋ฅผ JSON ํŒŒ์ผ๋กœ๋ถ€ํ„ฐ ๋กœ๋“œํ•˜์—ฌ + * DB ๋ฐ Elasticsearch์— ์ €์žฅํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + *

+ * 1) JSON ๋กœ๋”๋ฅผ ํ†ตํ•ด ์ฆ์ƒ ๋ฆฌ์ŠคํŠธ ๋กœ๋“œ + * 2) JPA ์–ด๋Œ‘ํ„ฐ๋กœ DB ์ €์žฅ + * 3) Elasticsearch ์–ด๋Œ‘ํ„ฐ๋กœ ์ƒ‰์ธ ์ €์žฅ + * + * @return ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ๋‹ด์€ ApiResponse + * @throws RuntimeException ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ „๋‹ฌ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-01 + * @since 2025-05-01 + */ + @PostMapping("/set") + public ResponseEntity> setDictionary() { + log("setDictionary() ํ˜ธ์ถœ"); + dictionaryUseCase.setDictionary(); + + return success(); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java new file mode 100644 index 0000000..4b6d181 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/symptomdictionary/presentation/controller/docs/DictionaryControllerDocs.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.symptomdictionary.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.common.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * ์ฆ์ƒ ์‚ฌ์ „ ๊ด€๋ฆฌ API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Dictionary", description = "์ฆ์ƒ ์‚ฌ์ „ ๊ด€๋ฆฌ API") +public interface DictionaryControllerDocs { + + @Operation( + summary = "์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ฐ ์ €์žฅ", + description = "JSON ํŒŒ์ผ๋กœ๋ถ€ํ„ฐ ์ฆ์ƒ ์‚ฌ์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜์—ฌ DB ๋ฐ Elasticsearch์— ์ €์žฅํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> setDictionary(); +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6b4b91a..031b79c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,7 @@ spring: + ai: + openai: + api-key: ${OPENAI_API_KEY} application: name: yakplus elasticsearch: @@ -8,18 +11,26 @@ spring: username: ${MYSQL_USERNAME} password: ${MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 30 jpa: database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: create + ddl-auto: none # ddl-auto: create # show-sql: true properties: hibernate: format_sql: true -#logging: -# level: -# root: DEBUG + batch: + job: + enabled: false + jdbc: + initialize-schema: embedded +logging: + level: + org.springframework.jdbc: DEBUG + org.springframework.data.jpa: DEBUG gov: host: apis.data.go.kr serviceKey: ${GOV_SERVICE_KEY} @@ -27,5 +38,20 @@ gov: path: detail: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnDtlInq05 img: /1471000/DrugPrdtPrmsnInfoService06/getDrugPrdtPrmsnInq06 +log: + rolling: + directory: logs + file-name: yakplus-batch.log + pattern: "%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n" + max-history: 30 + total-size-cap: 10MB +embed: + kmbert: embed.techlog.dev + krsbert: embedb.techlog.dev + switcher: + default-adapter: openAiEmbeddingAdapter + +dictionary: + path: classpath:unique_symptoms.json server: - port: 8084 + port: 8077