diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml new file mode 100644 index 0000000..e7b999b --- /dev/null +++ b/.github/workflows/dev-deploy.yml @@ -0,0 +1,44 @@ +name: Deploy + +on: + push: + branches: + - dev + paths-ignore: + - '.github/workflows/**' + +jobs: + deploy: + runs-on: self-hosted + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '21' + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Build Spring Boot (JAR) + run: ./gradlew bootJar -Penv=test + + - name: Copy JAR to shared volume + run: cp build/libs/*.jar /deploy/app.jar + + - name: Restart Docker with New JAR + run: | + cd /deploy + docker-compose down + docker-compose up -d --build + + - name: Wait for test server to be healthy + run: | + echo "๐Ÿ” Checking https://yakplus-test.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 diff --git a/.gitignore b/.gitignore index 8b64324..a2ba527 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ build/ *.iml *.iws *.ipr -out/ +/out/ .idea/**/ .idea_modules/ .idea/httpRequests diff --git a/build.gradle b/build.gradle index 00703ef..f806f2d 100644 --- a/build.gradle +++ b/build.gradle @@ -39,10 +39,31 @@ dependencies { // RDB runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' // Elastic search implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.17.10' + + //ai + implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M5' + + // Swagger ์„ค์น˜ + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + + // build.gradle + if (project.hasProperty('env') && project.env == 'test') { + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + } + } +} + +dependencyManagement { + imports { + mavenBom "org.springframework.ai:spring-ai-bom:1.0.0-M5" + } } tasks.named('test') { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9bbc975 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java b/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java index 91ce47c..46dc920 100644 --- a/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java +++ b/src/main/java/com/likelion/backendplus4/yakplus/YakplusApplication.java @@ -5,8 +5,7 @@ @SpringBootApplication public class YakplusApplication { - public static void main(String[] args) { - 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/LogbackConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/LogbackConfig.java new file mode 100644 index 0000000..492bd32 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/LogbackConfig.java @@ -0,0 +1,188 @@ +package com.likelion.backendplus4.yakplus.common.configuration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.FileAppender; +import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; +import ch.qos.logback.core.util.FileSize; +import jakarta.annotation.PostConstruct; + +/** + * ๋กœ๊น… ์„ค์ •์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +@Configuration +public class LogbackConfig { + @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; + + /** + * ๋กœ๊น… ์„ค์ •์„ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + @PostConstruct + public void configure() { + LoggerContext context = initializeLoggerContext(); + createLogDirectory(); + + ConsoleAppender consoleAppender = createConsoleAppender(context); + FileAppender fileAppender = createFileAppender(context); + + configureRootLogger(context, consoleAppender, fileAppender); + } + + /** + * LoggerContext๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @return LoggerContext ์ดˆ๊ธฐํ™”๋œ ๋กœ๊ฑฐ ์ปจํ…์ŠคํŠธ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private LoggerContext initializeLoggerContext() { + LoggerContext context = (LoggerContext)LoggerFactory.getILoggerFactory(); + context.reset(); + return context; + } + + /** + * ๋กœ๊ทธ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private void createLogDirectory() { + Path logPath = Paths.get(LOG_DIRECTORY); + try { + if (!Files.exists(logPath)) { + Files.createDirectories(logPath); + } + } catch (Exception e) { + throw new RuntimeException("๋กœ๊ทธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ ์‹คํŒจ", e); + } + } + + /** + * ์ฝ˜์†” ์–ดํŽœ๋”๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param context LoggerContext ๋กœ๊ฑฐ ์ปจํ…์ŠคํŠธ + * @return ConsoleAppender ์ƒ์„ฑ๋œ ์ฝ˜์†” ์–ดํŽœ๋” + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private ConsoleAppender createConsoleAppender(LoggerContext context) { + ConsoleAppender appender = new ConsoleAppender<>(); + appender.setContext(context); + appender.setEncoder(createEncoder(context)); + appender.start(); + return appender; + } + + /** + * ํŒŒ์ผ ์–ดํŽœ๋”๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param context LoggerContext ๋กœ๊ฑฐ ์ปจํ…์ŠคํŠธ + * @return FileAppender ์ƒ์„ฑ๋œ ํŒŒ์ผ ์–ดํŽœ๋” + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private FileAppender createFileAppender(LoggerContext context) { + FileAppender appender = new FileAppender<>(); + appender.setContext(context); + appender.setFile(LOG_DIRECTORY + "/" + LOG_FILE_NAME); + appender.setAppend(true); + appender.setEncoder(createEncoder(context)); + + TimeBasedRollingPolicy rollingPolicy = createRollingPolicy(context, appender); + rollingPolicy.start(); + + appender.start(); + return appender; + } + + /** + * ํŒจํ„ด ๋ ˆ์ด์•„์›ƒ ์ธ์ฝ”๋”๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param context LoggerContext ๋กœ๊ฑฐ ์ปจํ…์ŠคํŠธ + * @return PatternLayoutEncoder ์ƒ์„ฑ๋œ ํŒจํ„ด ๋ ˆ์ด์•„์›ƒ ์ธ์ฝ”๋” + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private PatternLayoutEncoder createEncoder(LoggerContext context) { + PatternLayoutEncoder encoder = new PatternLayoutEncoder(); + encoder.setContext(context); + encoder.setPattern(LOG_PATTERN); + encoder.start(); + return encoder; + } + + /** + * ๋กค๋ง ์ •์ฑ…์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param context LoggerContext ๋กœ๊ฑฐ ์ปจํ…์ŠคํŠธ + * @param parent FileAppender ๋ถ€๋ชจ ํŒŒ์ผ ์–ดํŽœ๋” + * @return TimeBasedRollingPolicy ์ƒ์„ฑ๋œ ๋กค๋ง ์ •์ฑ… + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private TimeBasedRollingPolicy createRollingPolicy(LoggerContext context, + FileAppender parent) { + TimeBasedRollingPolicy policy = new TimeBasedRollingPolicy<>(); + policy.setContext(context); + policy.setParent(parent); + policy.setFileNamePattern(LOG_DIRECTORY + "/" + LOG_FILE_NAME.replace(".log", ".%d{yyyy-MM-dd}.log")); + policy.setMaxHistory(MAX_HISTORY); + policy.setTotalSizeCap(FileSize.valueOf(TOTAL_SIZE_CAP)); + return policy; + } + + /** + * ๋ฃจํŠธ ๋กœ๊ฑฐ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param context LoggerContext ๋กœ๊ฑฐ ์ปจํ…์ŠคํŠธ + * @param consoleAppender ConsoleAppender ์ฝ˜์†” ์–ดํŽœ๋” + * @param fileAppender FileAppender ํŒŒ์ผ ์–ดํŽœ๋” + * @author ์ •์•ˆ์‹ + * @since 2025-04-16 + */ + private void configureRootLogger(LoggerContext context, ConsoleAppender consoleAppender, + FileAppender fileAppender) { + Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + if (logger instanceof ch.qos.logback.classic.Logger) { + ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)logger; + rootLogger.setLevel(Level.INFO); + rootLogger.addAppender(consoleAppender); + rootLogger.addAppender(fileAppender); + } + } +} \ No newline at end of file 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..b66f003 --- /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 new file mode 100644 index 0000000..e2d1ff8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/configuration/WebConfig.java @@ -0,0 +1,38 @@ +package com.likelion.backendplus4.yakplus.common.configuration; + +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; + +/** + * ์›น ์„ค์ •์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private static final String ALL_PATTERN = "/**"; + + private final LogInterceptor logInterceptor; + + /** + * ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param registry InterceptorRegistry ์ธํ„ฐ์…‰ํ„ฐ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด LogInterceptor๋ฅผ ์ ์šฉ + registry.addInterceptor(logInterceptor) + .addPathPatterns(ALL_PATTERN); // ๋ชจ๋“  URL ํŒจํ„ด์— ์ ์šฉ + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/exception/CustomException.java b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/CustomException.java new file mode 100644 index 0000000..bd8a7a5 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/CustomException.java @@ -0,0 +1,35 @@ +package com.likelion.backendplus4.yakplus.common.exception; + + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +/** + * ์‚ฌ์šฉ์ž ์ •์˜ ์˜ˆ์™ธ์˜ ์ถ”์ƒ ํด๋ž˜์Šค ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ณตํ†ต ์˜ˆ์™ธ ์ƒ์œ„ ํƒ€์ž…์ด๋‹ค. + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +public abstract class CustomException extends RuntimeException { + + /** + * ์ƒ์„ฑ์ž - ErrorCode์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋ฅผ ์„ค์ •ํ•œ๋‹ค. + * + * @param errorCode ErrorCode ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + public CustomException(ErrorCode errorCode) { + super(errorCode.message()); + } + + /** + * ErrorCode ๋ฐ˜ํ™˜ + * + * @return ErrorCode ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + public abstract ErrorCode getErrorCode(); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/exception/error/ErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/error/ErrorCode.java new file mode 100644 index 0000000..d516915 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/error/ErrorCode.java @@ -0,0 +1,61 @@ +package com.likelion.backendplus4.yakplus.common.exception.error; + +import org.springframework.http.HttpStatus; + +/** + * ์—๋Ÿฌ ์ฝ”๋“œ ์ธํ„ฐํŽ˜์ด์Šค ๊ฐ ์—๋Ÿฌ ํ•ญ๋ชฉ์— ๋Œ€ํ•œ HTTP ์ƒํƒœ, ์—๋Ÿฌ ๋ฒˆํ˜ธ, ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•œ๋‹ค. + * A[BB][CCC] + * A (1์ž๋ฆฌ) : ์—๋Ÿฌ ์‹ฌ๊ฐ๋„ (1~5) + * 1: ํด๋ผ์ด์–ธํŠธ ์˜ค๋ฅ˜ + * 2: ์ธ์ฆ ๊ด€๋ จ ์˜ค๋ฅ˜ + * 3: ์‚ฌ์šฉ์ž ๊ด€๋ จ ์˜ค๋ฅ˜ + * 4: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + * 5: ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ + * + * BB (2์ž๋ฆฌ) : ๋„๋ฉ”์ธ ์ฝ”๋“œ + * 10: ์‚ฌ์šฉ์ž ๊ด€๋ จ (ex: USER_NOT_FOUND) + * 20: ์ธ์ฆ ๊ด€๋ จ (ex: AUTHORIZATION_FAILED) + * 30: DB ๊ด€๋ จ ์˜ค๋ฅ˜ (ex: DB_CONNECTION_FAILED) + * 40: API ๊ด€๋ จ ์˜ค๋ฅ˜ (ex: API_TIMEOUT) + * 50: ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ (ex: INTERNAL_SERVER_ERROR) + * + * CCC (3์ž๋ฆฌ) : ์„ธ๋ถ€ ์˜ค๋ฅ˜ ์ˆœ๋ฒˆ + * 001: ์ฒซ ๋ฒˆ์งธ ์˜ค๋ฅ˜ + * 002: ๋‘ ๋ฒˆ์งธ ์˜ค๋ฅ˜ + * 003: ์„ธ ๋ฒˆ์งธ ์˜ค๋ฅ˜, ๋“ฑ๋“ฑ + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +public interface ErrorCode { + + /** + * HTTP ์ƒํƒœ ๋ฐ˜ํ™˜ + * + * @return HTTP ์ƒํƒœ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + HttpStatus httpStatus(); + + /** + * ์—๋Ÿฌ ์ฝ”๋“œ ๋ฒˆํ˜ธ ๋ฐ˜ํ™˜ + * + * @return ์—๋Ÿฌ ์ฝ”๋“œ ๋ฒˆํ˜ธ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + int codeNumber(); + + /** + * ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ + * + * @return ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + String message(); +} 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 new file mode 100644 index 0000000..d19d62e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,173 @@ +package com.likelion.backendplus4.yakplus.common.exception.handler; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.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; + +/** + * ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํด๋ž˜์Šค + * ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ ๊ณตํ†ต์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. + * + * @modified 2025-05-03 + * @since 2025-04-16 + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + // ์—๋Ÿฌ ์ฝ”๋“œ ์ƒ์ˆ˜ ์ •์˜ (์ •์ˆ˜ํ˜• ์ฝ”๋“œ ์‚ฌ์šฉ) + 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 ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜์œผ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. + * + * @param ex CustomException ๊ฐ์ฒด + * @return ์—๋Ÿฌ ์‘๋‹ต + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + @ExceptionHandler(CustomException.class) + public ResponseEntity> handleCustomException(CustomException ex) { + ErrorCode errorCode = ex.getErrorCode(); + return buildErrorResponse( + errorCode.httpStatus(), + errorCode.codeNumber(), + errorCode.message(), + ex + ); + } + + /** + * IllegalArgumentException ์ฒ˜๋ฆฌ + * ์ž˜๋ชป๋œ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์‘๋‹ต ์ฒ˜๋ฆฌ + * + * @param ex ์˜ˆ์™ธ ๊ฐ์ฒด + * @return ์—๋Ÿฌ ์‘๋‹ต + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + ILLEGAL_ARGUMENT_CODE, + ex.getMessage(), + ex + ); + } + + /** + * MethodArgumentNotValidException ์ฒ˜๋ฆฌ + * ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ์— ๋Œ€ํ•œ ์‘๋‹ต ์ฒ˜๋ฆฌ + * + * @param ex ์˜ˆ์™ธ ๊ฐ์ฒด + * @return ์—๋Ÿฌ ์‘๋‹ต + * @author ์ •์•ˆ์‹ + * @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, + METHOD_ARGUMENT_NOT_VALID_CODE, + errorMessage, + ex + ); + } + + /** + * BindException ์ฒ˜๋ฆฌ + * GET ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋‚˜ ํผ ๋ฐ”์ธ๋”ฉ ์œ ํšจ์„ฑ ์‹คํŒจ ์‹œ ์ฒ˜๋ฆฌ + * + * @param ex BindException ์˜ค๋ฅ˜ + * @return ์—๋Ÿฌ ์‘๋‹ต + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @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, + BIND_EXCEPTION_CODE, + errorMessage, + ex + ); + } + + /** + * ๊ธฐํƒ€ ๋ชจ๋“  ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + * ์ •์˜๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ๋Š” ๋‚ด๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜๋กœ ์‘๋‹ต + * + * @param ex ์˜ˆ์™ธ ๊ฐ์ฒด + * @return ์—๋Ÿฌ ์‘๋‹ต + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity> handleAllExceptions(Exception ex) { + return buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + INTERNAL_SERVER_ERROR_CODE, + "๋‚ด๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜", + ex + ); + } + + /** + * ๊ณตํ†ต ์—๋Ÿฌ ์‘๋‹ต ์ƒ์„ฑ ๋ฉ”์„œ๋“œ + * ์˜ˆ์™ธ ๋กœ๊น… ํ›„ ApiResponse.error๋ฅผ ํ†ตํ•ด ํ‘œ์ค€ํ™”๋œ ์—๋Ÿฌ ์‘๋‹ต์„ ์ƒ์„ฑํ•œ๋‹ค. + * + * @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 + */ + 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 new file mode 100644 index 0000000..34592ab --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/interceptor/LogInterceptor.java @@ -0,0 +1,94 @@ +package com.likelion.backendplus4.yakplus.common.interceptor; + +import com.likelion.backendplus4.yakplus.common.util.log.LogMessage; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.UUID; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * ๋กœ๊น…์„ ์œ„ํ•œ ์ธํ„ฐ์…‰ํ„ฐ ํด๋ž˜์Šค + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +@Component +public class LogInterceptor implements HandlerInterceptor { + + /** + * ์š”์ฒญ ์ฒ˜๋ฆฌ ์ „์— ์‹คํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param request HttpServletRequest ์š”์ฒญ ๊ฐ์ฒด + * @param response HttpServletResponse ์‘๋‹ต ๊ฐ์ฒด + * @param handler Object ํ•ธ๋“ค๋Ÿฌ ๊ฐ์ฒด + * @return boolean ์ฒ˜๋ฆฌ ๊ณ„์† ์—ฌ๋ถ€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, Object handler) { + String traceId = generateTraceId(); + setTraceId(traceId); + log("TraceId ์ƒ์„ฑ ์„ฑ๊ณต - " + traceId); + return true; + } + + /** + * ์š”์ฒญ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ์‹คํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param request HttpServletRequest ์š”์ฒญ ๊ฐ์ฒด + * @param response HttpServletResponse ์‘๋‹ต ๊ฐ์ฒด + * @param handler Object ํ•ธ๋“ค๋Ÿฌ ๊ฐ์ฒด + * @param ex Exception ์˜ˆ์™ธ ๊ฐ์ฒด + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + clearTraceId(); + } + + /** + * TraceId๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @return String ์ƒ์„ฑ๋œ TraceId + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private String generateTraceId() { + return UUID.randomUUID().toString(); + } + + /** + * TraceId๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param traceId String ์„ค์ •ํ•  TraceId + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private void setTraceId(String traceId) { + MDC.put(LogMessage.TRACE_ID.getMessage(), traceId); + } + + /** + * TraceId๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private void clearTraceId() { + MDC.clear(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogLevel.java b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogLevel.java new file mode 100644 index 0000000..982e143 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogLevel.java @@ -0,0 +1,126 @@ +package com.likelion.backendplus4.yakplus.common.util.log; + +import org.slf4j.Logger; + +/** + * ๋กœ๊ทธ ๋ ˆ๋ฒจ์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜• ํด๋ž˜์Šค + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +public enum LogLevel { + /** + * TRACE ๋ ˆ๋ฒจ ๋กœ๊ทธ + */ + TRACE { + @Override + public void log(Logger logger, String traceId, String message) { + logMessage(logger::trace, traceId, message); + } + }, + /** + * DEBUG ๋ ˆ๋ฒจ ๋กœ๊ทธ + */ + DEBUG { + @Override + 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 ๋ ˆ๋ฒจ ๋กœ๊ทธ + */ + ERROR { + @Override + public void log(Logger logger, String traceId, String message) { + logMessage(logger::error, traceId, message); + } + + @Override + public void log(Logger logger, String traceId, String message, Throwable t) { + logger.error(formatMessage(traceId, message), t); + } + }; + + /** + * ๋กœ๊ฑฐ ํ•จ์ˆ˜ ์ธํ„ฐํŽ˜์ด์Šค + */ + @FunctionalInterface + private interface LoggerFunction { + void log(String message); + } + + /** + * ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param loggerFunction LoggerFunction ๋กœ๊ฑฐ ํ•จ์ˆ˜ + * @param traceId String ์ถ”์  ID + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private static void logMessage(LoggerFunction loggerFunction, String traceId, String message) { + loggerFunction.log(formatMessage(traceId, message)); + } + + /** + * ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ ํฌ๋งทํŒ…ํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param traceId String ์ถ”์  ID + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @return String ํฌ๋งทํŒ…๋œ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private static String formatMessage(String traceId, String message) { + return String.format("TraceId: %s - %s", traceId, message); + } + + /** + * ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ์ถ”์ƒ ๋ฉ”์„œ๋“œ + * + * @param logger Logger ๋กœ๊ฑฐ ๊ฐ์ฒด + * @param traceId String ์ถ”์  ID + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + public abstract void log(Logger logger, String traceId, String message); + + /** + * ์˜ˆ์™ธ์™€ ํ•จ๊ป˜ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param logger Logger ๋กœ๊ฑฐ ๊ฐ์ฒด + * @param traceId String ์ถ”์  ID + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @param t Throwable ์˜ˆ์™ธ ๊ฐ์ฒด + * @throws UnsupportedOperationException ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋กœ๊ทธ ๋ ˆ๋ฒจ์ผ ๊ฒฝ์šฐ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + public void log(Logger logger, String traceId, String message, Throwable t) { + throw new UnsupportedOperationException("์ด ๋กœ๊ทธ ๋ ˆ๋ฒจ์€ ์˜ˆ์™ธ ๋กœ๊น…์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogMessage.java b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogMessage.java new file mode 100644 index 0000000..ac1b7a4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogMessage.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.common.util.log; + +import lombok.Getter; + +@Getter +public enum LogMessage { + DATA_PROCESSING_START("๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค"), + DATA_PROCESSING_SUCCESS("๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + DATA_PROCESSING_ERROR("๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + SERVICE_DATA_PROCESSING_START("Service: ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค"), + SERVICE_DATA_PROCESSING_SUCCESS("Service: ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + SERVICE_DATA_PROCESSING_ERROR("Service: ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + PROCESSED_DATA_RESULT("์ฒ˜๋ฆฌ๋œ ๋ฐ์ดํ„ฐ"), + TRACE_ID("traceId"); + + private final String message; + + LogMessage(String message) { + this.message = message; + } + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogUtil.java b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogUtil.java new file mode 100644 index 0000000..92c6880 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LogUtil.java @@ -0,0 +1,65 @@ +package com.likelion.backendplus4.yakplus.common.util.log; + +/** + * ๋กœ๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +public class LogUtil { + + /** + * INFO ๋ ˆ๋ฒจ๋กœ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + public static void log(String message) { + logWithLevel(LogLevel.INFO, message); + } + + /** + * ์ง€์ •๋œ ๋ ˆ๋ฒจ๋กœ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param level LogLevel ๋กœ๊ทธ ๋ ˆ๋ฒจ + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + public static void log(LogLevel level, String message) { + logWithLevel(level, message); + } + + /** + * ์˜ˆ์™ธ์™€ ํ•จ๊ป˜ ์ง€์ •๋œ ๋ ˆ๋ฒจ๋กœ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @param level LogLevel ๋กœ๊ทธ ๋ ˆ๋ฒจ + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @param t Throwable ์˜ˆ์™ธ ๊ฐ์ฒด + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + public static void log(LogLevel level, String message, Throwable t) { + LoggerWithTraceId loggerWithTraceId = LoggerWithTraceId.create(); + level.log(loggerWithTraceId.getLogger(), loggerWithTraceId.getTraceId(), message, t); + } + + /** + * ๋‚ด๋ถ€์ ์œผ๋กœ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๋Š” private ๋ฉ”์„œ๋“œ + * + * @param level LogLevel ๋กœ๊ทธ ๋ ˆ๋ฒจ + * @param message String ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private static void logWithLevel(LogLevel level, String message) { + LoggerWithTraceId loggerWithTraceId = LoggerWithTraceId.create(); + level.log(loggerWithTraceId.getLogger(), loggerWithTraceId.getTraceId(), message); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java new file mode 100644 index 0000000..36ae238 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/common/util/log/LoggerWithTraceId.java @@ -0,0 +1,109 @@ +package com.likelion.backendplus4.yakplus.common.util.log; + +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * TraceId๋ฅผ ํฌํ•จํ•œ ๋กœ๊ฑฐ ํด๋ž˜์Šค + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +@Getter +public class LoggerWithTraceId { + private final Logger logger; + private final String traceId; + + /** + * LoggerWithTraceId ์ƒ์„ฑ์ž + * + * @param logger Logger ๋กœ๊ฑฐ ๊ฐ์ฒด + * @param traceId String ์ถ”์  ID + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private LoggerWithTraceId(Logger logger, String traceId) { + this.logger = logger; + this.traceId = traceId; + } + + /** + * LoggerWithTraceId ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ + * + * @return LoggerWithTraceId ์ƒ์„ฑ๋œ ๋กœ๊ฑฐ ์ธ์Šคํ„ด์Šค + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + public static LoggerWithTraceId create() { + String traceId = makeTraceId(); + Logger logger = makeLogger(); + return new LoggerWithTraceId(logger, traceId); + } + + /** + * ๋กœ๊ฑฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @return Logger ์ƒ์„ฑ๋œ ๋กœ๊ฑฐ + * @throws IllegalStateException ๋กœ๊ฑฐ ์ƒ์„ฑ ์‹คํŒจ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private static Logger makeLogger() { + String callingClassName = getCallingClassName(); + if (callingClassName.trim().isEmpty()) { + throw new IllegalStateException("ํ˜ธ์ถœ ํด๋ž˜์Šค๋ช…์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + + Logger logger = LoggerFactory.getLogger(callingClassName); + if (logger == null) { + throw new IllegalStateException(String.format("ํด๋ž˜์Šค '%s'์— ๋Œ€ํ•œ Logger ์ƒ์„ฑ ์‹คํŒจ", callingClassName)); + } + return logger; + } + + /** + * TraceId๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ + * + * @return String ์ƒ์„ฑ๋œ TraceId + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private static String makeTraceId() { + String traceId = MDC.get("traceId"); + if (traceId == null || traceId.trim().isEmpty()) { + return "no-trace"; + } + return traceId; + } + + /** + * ํ˜ธ์ถœํ•œ ํด๋ž˜์Šค์˜ ์ด๋ฆ„์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ + * + * @return String ํ˜ธ์ถœํ•œ ํด๋ž˜์Šค๋ช… + * @throws IllegalStateException ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ๊ด€๋ จ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-18 + * @since 2025-04-16 + */ + private static String getCallingClassName() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + if (stackTraceElements.length == 0) { + throw new IllegalStateException("์Šคํƒ ํŠธ๋ ˆ์ด์Šค๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค."); + } + if (stackTraceElements.length < 5) { + throw new IllegalStateException("์Šคํƒ ํŠธ๋ ˆ์ด์Šค๊ฐ€ ์˜ˆ์ƒ๋ณด๋‹ค ์งง์Šต๋‹ˆ๋‹ค."); + } + + String className = stackTraceElements[6].getClassName(); + if (className.trim().isEmpty()) { + return "UnknownClass"; + } + return className; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/response/ApiResponse.java b/src/main/java/com/likelion/backendplus4/yakplus/response/ApiResponse.java new file mode 100644 index 0000000..7f7102a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/response/ApiResponse.java @@ -0,0 +1,89 @@ +package com.likelion.backendplus4.yakplus.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import lombok.Getter; +import lombok.Builder; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.AccessLevel; + +/** + * API ์‘๋‹ต ํฌ๋งท ํด๋ž˜์Šค ์ •์ƒ ๋ฐ ์—๋Ÿฌ ์‘๋‹ต์„ ํ†ตํ•ฉ๋œ ํ˜•์‹์œผ๋กœ ์ œ๊ณตํ•œ๋‹ค. + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ApiResponse { + + private static final String SUCCESS_MESSAGE = "์š”์ฒญ ์„ฑ๊ณต"; + + private String errorCode; + private String message; + private T data; + + /** + * ์ •์ƒ ์‘๋‹ต ์ƒ์„ฑ (๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) + * + * @return 200 OK ์‘๋‹ต (body๋Š” message๋งŒ ํฌํ•จ) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-17 + */ + public static ResponseEntity> success() { + ApiResponse body = ApiResponse.builder() + .errorCode(null) + .message(SUCCESS_MESSAGE) + .data(null) + .build(); + return ResponseEntity.ok(body); + } + + /** + * ์ •์ƒ ์‘๋‹ต ์ƒ์„ฑ (๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) + * + * @param ์‘๋‹ต ๋ฐ์ดํ„ฐ ํƒ€์ž… + * @param data ์‘๋‹ต ๋ฐ์ดํ„ฐ + * @return 200 OK ์‘๋‹ต (body์— message์™€ data ํฌํ•จ) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-17 + */ + public static ResponseEntity> success(T data) { + ApiResponse body = ApiResponse.builder() + .errorCode(null) + .message(SUCCESS_MESSAGE) + .data(data) + .build(); + return ResponseEntity.ok(body); + } + + /** + * ์—๋Ÿฌ ์‘๋‹ต ์ƒ์„ฑ + * + * @param ๋ฐ์ดํ„ฐ ํƒ€์ž… + * @param status HTTP ์ƒํƒœ ์ฝ”๋“œ + * @param errorCode ์—๋Ÿฌ ์ฝ”๋“œ + * @param message ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + * @return ์—๋Ÿฌ ์‘๋‹ต ResponseEntity + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-18 ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-16 + */ + public static ResponseEntity> error(HttpStatus status, String errorCode, String message) { + ApiResponse body = ApiResponse.builder() + .errorCode(errorCode) + .message(message) + .data(null) + .build(); + return ResponseEntity.status(status).body(body); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/in/SearchDrugUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/in/SearchDrugUseCase.java new file mode 100644 index 0000000..7fb0bb4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/in/SearchDrugUseCase.java @@ -0,0 +1,114 @@ +package com.likelion.backendplus4.yakplus.search.application.port.in; + + +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.AutoCompleteStringList; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.DetailSearchResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponseList; + +/** + * ์˜์•ฝํ’ˆ ๊ฒ€์ƒ‰ ๊ด€๋ จ ์œ ์Šค์ผ€์ด์Šค๋ฅผ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์— ๋”ฐ๋ผ ์˜์•ฝํ’ˆ์„ ๊ฒ€์ƒ‰ํ•˜๊ฑฐ๋‚˜ ์ž๋™์™„์„ฑ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-04-21 + */ +public interface SearchDrugUseCase { + + /** + * ์˜์•ฝํ’ˆ ID๋ฅผ ํ†ตํ•ด ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugId ์กฐํšŒํ•  ์˜์•ฝํ’ˆ์˜ ๊ณ ์œ  ID + * @return ์ƒ์„ธ ๊ฒ€์ƒ‰ ์‘๋‹ต ๊ฐ์ฒด + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + DetailSearchResponse searchByDrugId(Long drugId); + + /** + * ๊ฒ€์ƒ‰์–ด ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ, ES ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰ ํ›„ + * ๋ฆฌ์ŠคํŠธ๋กœ ๋งคํ•‘ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * + * @param drugSearchNatural ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ๋‹ด์€ ๊ฐ์ฒด + * @return ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰์–ด๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋ฐœ์ƒ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-02 + * - 25.05.02 - SearchResponseList๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + SearchResponseList searchDrugByNatural(DrugSearchNatural drugSearchNatural); + + /** + * ์ฃผ์–ด์ง„ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ ์ฆ์ƒ ์ž๋™์™„์„ฑ ํ‚ค์›Œ๋“œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * Elasticsearch์—์„œ Suggest API ๋“ฑ์„ ํ™œ์šฉํ•˜์—ฌ ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-25 + * @since 2025-04-24 + */ + AutoCompleteStringList getSymptomAutoComplete(String q); + + /** + * ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ ์•ฝํ’ˆ๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * Elasticsearch Suggest API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์•ฝํ’ˆ๋ช…๊ณผ ๊ด€๋ จ๋œ ์ž๋™์™„์„ฑ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ DTO (AutoCompleteStringList) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-30 + * @since 2025-04-29 + */ + AutoCompleteStringList getDrugNameAutoComplete(String q); + + /** + * ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ ์„ฑ๋ถ„๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * Elasticsearch Suggest API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋ถ„๋ช…๊ณผ ๊ด€๋ จ๋œ ์ž๋™์™„์„ฑ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ DTO (AutoCompleteStringList) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-03 + * @since 2025-05-03 + */ + AutoCompleteStringList getIngredientAutoComplete(String q); + + /** + * ์ฃผ์–ด์ง„ ์ฆ์ƒ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์•ฝํ’ˆ๋ช… ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSearch ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @return ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-04-27 + * @since 2025-04-25 + */ + SearchResponseList searchDrugBySymptom(DrugSearchNatural drugSearch); + + + /** + * ์ฃผ์–ด์ง„ ์•ฝํ’ˆ๋ช… ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSearch ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @return ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-03 + * @since 2025-04-29 + */ + SearchResponseList searchDrugByDrugName(DrugSearchNatural drugSearch); + + /** + * ์ฃผ์–ด์ง„ ์„ฑ๋ถ„๋ช… ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSearch ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @return ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-03 + * @since 2025-05-03 + */ + SearchResponseList searchDrugByIngredient(DrugSearchNatural drugSearch); + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/DrugSearchRdbRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/DrugSearchRdbRepositoryPort.java new file mode 100644 index 0000000..184101c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/DrugSearchRdbRepositoryPort.java @@ -0,0 +1,21 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out; + +import com.likelion.backendplus4.yakplus.search.domain.model.Drug; + +/** + * RDB ๊ธฐ๋ฐ˜ ์˜์•ฝํ’ˆ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ๋ถ€ํ„ฐ ์˜์•ฝํ’ˆ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-04-30 + */ +public interface DrugSearchRdbRepositoryPort { + /** + * ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹จ์ผ ์˜์•ฝํ’ˆ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์กฐํšŒํ•  ์˜์•ฝํ’ˆ์˜ ๊ณ ์œ  ID + * @return ์กฐํšŒ๋œ ์˜์•ฝํ’ˆ ๊ฐ์ฒด + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + Drug findById(Long id); +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/DrugSearchRepositoryPort.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/DrugSearchRepositoryPort.java new file mode 100644 index 0000000..f98bfc0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/DrugSearchRepositoryPort.java @@ -0,0 +1,105 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out; + +import java.util.List; + +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomainNatural; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; +import org.springframework.data.domain.Page; + +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomain; + +public interface DrugSearchRepositoryPort { + + /** + * ์ฃผ์–ด์ง„ ์ฟผ๋ฆฌ ๋ฐ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด Elasticsearch์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜๊ณ , + * ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์‹คํŒจ ์‹œ SearchException์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. + * + * @param drugSearchNatural ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ๋‹ด์€ ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @param embeddings ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ๋ฐฐ์—ด + * @return ๊ฒ€์ƒ‰๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 + * 25.04.27 - ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง + * 25.05.02 - ํ—ฅ์‚ฌ๊ณ ๋‚  ๊ตฌ์กฐ์— ๋งž์ถ”์–ด ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง + * 25.05.03 - EsIndexName๋ฅผ ์™ธ๋ถ€์— ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + List searchByNatural(DrugSearchNatural drugSearchNatural, float[] embeddings,String EsIndexName); + + /** + * ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch Completion Suggest API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ + * symptomSuggester ํ•„๋“œ์—์„œ ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ + * @throws SearchException ์ž๋™์™„์„ฑ API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified 2025-04-28 + */ + List getSymptomAutoCompleteResponse(String q); + + /** + * ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch Completion Suggest API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ + * drugNameSuggester ํ•„๋“œ์—์„œ ์•ฝํ’ˆ๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ + * @throws SearchException ์ž๋™์™„์„ฑ API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-28 + * @modified 2025-04-29 + */ + List getDrugNameAutoCompleteResponse(String q); + + /** + * ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch Completion Suggest API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ + * ingredientNameSuggester ํ•„๋“œ์—์„œ ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๋‹จ์–ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ์„ฑ๋ถ„๋ช… ๋ฆฌ์ŠคํŠธ + * @throws SearchException ์ž๋™์™„์„ฑ API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-01 + * @modified 2025-05-01 + */ + List getIngredientAutoCompleteResponse(String q); + + /** + * ๊ฒ€์ƒ‰์–ด์— ๋งค์นญ๋˜๋Š” ์ฆ์ƒ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ Elasticsearch์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ DTO + * @return ์ฆ์ƒ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified 2025-05-03 + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + Page searchDocsBySymptom(DrugSearchNatural request); + + /** + * ๊ฒ€์ƒ‰์–ด์— ๋งค์นญ๋˜๋Š” ์•ฝํ’ˆ๋ช… ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ Elasticsearch์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ DTO + * @return ์•ฝํ’ˆ๋ช… ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-28 + * @modified 2025-05-03 + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + Page searchDocsByDrugName(DrugSearchNatural request); + + /** + * ๊ฒ€์ƒ‰์–ด์— ๋งค์นญ๋˜๋Š” ์„ฑ๋ถ„๋ช…์„ ํฌํ•จํ•œ ์•ฝํ’ˆ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ Elasticsearch์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ DTO + * @return ์„ฑ๋ถ„ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-01 + * @modified 2025-05-03 + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + Page searchDocsByIngredient(DrugSearchNatural request); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/EmbeddingPort.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/EmbeddingPort.java new file mode 100644 index 0000000..9e20924 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/EmbeddingPort.java @@ -0,0 +1,15 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out; + +public interface EmbeddingPort { + + /** + * ํ˜„์žฌ ์„ ํƒ๋œ ์–ด๋Œ‘ํ„ฐ๋กœ๋ถ€ํ„ฐ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ž„๋ฒ ๋”ฉ์„ ์ƒ์„ฑํ•  ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž…๋ ฅ ๋ฌธ์ž์—ด์— ๋Œ€ํ•œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ๋ฐฐ์—ด + * @throws IllegalStateException ์–ด๋Œ‘ํ„ฐ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + * @author ์ •์•ˆ์‹ + * @since 2025-05-02 + */ + float[] getEmbedding(String text); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/dto/SearchByKeywordParams.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/dto/SearchByKeywordParams.java new file mode 100644 index 0000000..93c4b36 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/dto/SearchByKeywordParams.java @@ -0,0 +1,14 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class SearchByKeywordParams { + private final String query; + private final int from; + private final int size; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/dto/SearchByNaturalParams.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/dto/SearchByNaturalParams.java new file mode 100644 index 0000000..bcdf02b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/dto/SearchByNaturalParams.java @@ -0,0 +1,39 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out.dto; + +import lombok.Getter; + +/** + * ์ž์—ฐ์–ด ๊ฒ€์ƒ‰์„ ์œ„ํ•œ Elasticsearch ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋‹ด๋Š” DTO ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * query, vector, from, size ํ•„๋“œ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-03 + */ +@Getter +public class SearchByNaturalParams { + private final String query; + private final float[] vector; + private final int from; + private final int size; + + /** + * ์ฃผ์–ด์ง„ ๊ฒ€์ƒ‰์–ด, ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ, ์˜คํ”„์…‹, ํŽ˜์ด์ง€ ํฌ๊ธฐ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param query ๊ฒ€์ƒ‰์–ด ๋ฌธ์ž์—ด + * @param vector ๊ฒ€์ƒ‰์–ด ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (null์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค) + * @param from ์กฐํšŒ ์‹œ์ž‘ ์˜คํ”„์…‹ (0 ์ด์ƒ) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ (1 ์ด์ƒ) + * @throws IllegalArgumentException vector๊ฐ€ null์ธ ๊ฒฝ์šฐ ๋ฐœ์ƒ + * @author ์ •์•ˆ์‹ + * @since 2025-05-03 + */ + public SearchByNaturalParams(String query, float[] vector, int from, int size) { + if (vector == null) { + throw new IllegalArgumentException("Vector๋Š” null์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + this.query = query; + this.vector = vector; + this.from = from; + this.size = size; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/mapper/SearchByKeywordParamsMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/mapper/SearchByKeywordParamsMapper.java new file mode 100644 index 0000000..425eb6c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/mapper/SearchByKeywordParamsMapper.java @@ -0,0 +1,15 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out.mapper; + +import com.likelion.backendplus4.yakplus.search.application.port.out.dto.SearchByKeywordParams; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; + +public class SearchByKeywordParamsMapper { + + public static SearchByKeywordParams toParams(DrugSearchNatural drugSearchNatural) { + return SearchByKeywordParams.builder() + .query(drugSearchNatural.getQuery()) + .size(drugSearchNatural.getSize()) + .from(drugSearchNatural.getPage()) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/mapper/SearchByNaturalParamsMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/mapper/SearchByNaturalParamsMapper.java new file mode 100644 index 0000000..6c92db8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/port/out/mapper/SearchByNaturalParamsMapper.java @@ -0,0 +1,30 @@ +package com.likelion.backendplus4.yakplus.search.application.port.out.mapper; + +import com.likelion.backendplus4.yakplus.search.application.port.out.dto.SearchByNaturalParams; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; + +/** + * ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋งคํผ ํด๋ž˜์Šค + * + * DrugSearchNatural ๋„๋ฉ”์ธ ๋ชจ๋ธ๊ณผ ์ƒ์„ฑ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐฐ์—ด์„ + * SearchByNaturalParams DTO๋กœ ์กฐ๋ฆฝํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +public class SearchByNaturalParamsMapper { + + /** + * DrugSearchNatural๊ณผ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ + * SearchByNaturalParams ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param natural ๊ฒ€์ƒ‰์–ด, ํŽ˜์ด์ง€ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๋„๋ฉ”์ธ ๋ชจ๋ธ + * @param embeddings ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ๋ฐฐ์—ด + * @return ์กฐ๋ฆฝ๋œ SearchByNaturalParams DTO + * @author ์ •์•ˆ์‹ + * @since 2025-05-02 + */ + public static SearchByNaturalParams toParams(DrugSearchNatural natural, float[] embeddings) { + int from = natural.getPage() * natural.getSize(); + return new SearchByNaturalParams(natural.getQuery(), embeddings, from, natural.getSize()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/application/service/DrugSearcher.java b/src/main/java/com/likelion/backendplus4/yakplus/search/application/service/DrugSearcher.java new file mode 100644 index 0000000..039cff3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/application/service/DrugSearcher.java @@ -0,0 +1,225 @@ +package com.likelion.backendplus4.yakplus.search.application.service; + +import com.likelion.backendplus4.yakplus.search.application.port.out.DrugSearchRdbRepositoryPort; +import com.likelion.backendplus4.yakplus.search.application.port.out.mapper.SearchByNaturalParamsMapper; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomain; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomainNatural; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.DrugMapper; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.AutoCompleteStringList; +import com.likelion.backendplus4.yakplus.search.application.port.in.SearchDrugUseCase; +import com.likelion.backendplus4.yakplus.search.application.port.out.DrugSearchRepositoryPort; +import com.likelion.backendplus4.yakplus.search.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.domain.model.Drug; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.DetailSearchResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponseList; + +import com.likelion.backendplus4.yakplus.search.presentation.mapper.SearchResponseMapper; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * ์‚ฌ์šฉ์ž ๊ฒ€์ƒ‰ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋ฒกํ„ฐ ์œ ์‚ฌ๋„ ๋ฐ ํ…์ŠคํŠธ ๊ฒ€์ƒ‰์„ ํ†ตํ•ด + * ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์„œ๋น„์Šค ๊ตฌํ˜„์ฒด + * + * @modified 2025-05-02 + * @since 2025-04-22 + */ +@Service +@RequiredArgsConstructor +public class DrugSearcher implements SearchDrugUseCase { + private final DrugSearchRepositoryPort drugSearchRepositoryPort; + private final EmbeddingPort embeddingPort; + private final EmbeddingSwitchPort embeddingSwitchPort; + private final DrugSearchRdbRepositoryPort drugSearchRdbRepositoryPort; + + /** + * ๊ฒ€์ƒ‰์–ด ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ, ES ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰ ํ›„ + * ๋ฆฌ์ŠคํŠธ๋กœ ๋งคํ•‘ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * + * @param drugSearchNatural ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ๋‹ด์€ ๊ฐ์ฒด + * @return ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰์–ด๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋ฐœ์ƒ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-02 + * - 25.05.02 - SearchResponseList๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + @Override + public SearchResponseList searchDrugByNatural(DrugSearchNatural drugSearchNatural) { + log("search() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + drugSearchNatural.getQuery()); + return searchDrugs(drugSearchNatural, generateEmbeddings(drugSearchNatural.getQuery())); + } + + /** + * OpenAI API๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰์–ด์˜ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. + * + * @param query ๊ฒ€์ƒ‰์–ด ๋ฌธ์ž์—ด + * @return ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ๋ฐฐ์—ด + * @author ์ •์•ˆ์‹ + * @modified 2025-04-24 + * @since 2025-04-22 + */ + private float[] generateEmbeddings(String query) { + log("generateEmbeddings() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + query); + return embeddingPort.getEmbedding(query); + } + + /** + * ์ƒ์„ฑ๋œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ์™€ ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด๋ฅผ ์ด์šฉํ•ด Elasticsearch์—์„œ ์กฐํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ , + * ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ๋ฅผ SearchResponse DTO ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ด ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * + * @param drugSearchNatural ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @param embeddings ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ๋ฐฐ์—ด + * @return SearchResponse ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-02 + * - 25.05.02 - SearchResponseList๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + private SearchResponseList searchDrugs(DrugSearchNatural drugSearchNatural, float[] embeddings) { + log("searchDrugs() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ"); + List drugs = drugSearchRepositoryPort.searchByNatural(drugSearchNatural, embeddings, getEsIndexName()); + log("searchDrugs() ๋ฉ”์„œ๋“œ ์™„๋ฃŒ, ๊ฒ€์ƒ‰์–ด: " + drugSearchNatural.getQuery() + ", ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜: " + drugs.size()); + return SearchResponseMapper.toResponseListWithCount(drugs); + } + + /** + * Elasticsearch ์ธ๋ฑ์Šค ์ด๋ฆ„์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @return Elasticsearch ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @author ์ •์•ˆ์‹ + * @since 2025-05-03 + */ + private String getEsIndexName() { + return embeddingSwitchPort.getAdapterBeanName(); + } + + /** + * ์˜์•ฝํ’ˆ ID๋ฅผ ํ†ตํ•ด ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugId ์กฐํšŒํ•  ์˜์•ฝํ’ˆ์˜ ๊ณ ์œ  ID + * @return ๋ณ€ํ™˜๋œ ์ƒ์„ธ ๊ฒ€์ƒ‰ ์‘๋‹ต ๊ฐ์ฒด + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + @Override + public DetailSearchResponse searchByDrugId(Long drugId){ + Drug drug = drugSearchRdbRepositoryPort.findById(drugId); + return DrugMapper.toDetailResponse(drug); + } + + /** + * ์ฃผ์–ด์ง„ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ ์ฆ์ƒ ์ž๋™์™„์„ฑ ํ‚ค์›Œ๋“œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * Elasticsearch์—์„œ Suggest API ๋“ฑ์„ ํ™œ์šฉํ•˜์—ฌ ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified 2025-04-25 + */ + @Override + public AutoCompleteStringList getSymptomAutoComplete(String q) { + log("getSymptomAutoComplete() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + q); + return new AutoCompleteStringList(drugSearchRepositoryPort.getSymptomAutoCompleteResponse(q)); + } + + /** + * ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ ์•ฝํ’ˆ๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * Elasticsearch Suggest API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์•ฝํ’ˆ๋ช…๊ณผ ๊ด€๋ จ๋œ ์ž๋™์™„์„ฑ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ DTO (AutoCompleteStringList) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-29 + * @modified 2025-04-30 + */ + @Override + public AutoCompleteStringList getDrugNameAutoComplete(String q) { + log("getDrugNameAutoComplete() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + q); + return new AutoCompleteStringList(drugSearchRepositoryPort.getDrugNameAutoCompleteResponse(q)); + } + + + + /** + * ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ ์„ฑ๋ถ„๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * Elasticsearch Suggest API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์„ฑ๋ถ„๋ช…๊ณผ ๊ด€๋ จ๋œ ์ž๋™์™„์„ฑ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ DTO (AutoCompleteStringList) + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + @Override + public AutoCompleteStringList getIngredientAutoComplete(String q) { + log("getIngredientAutoComplete() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + q); + return new AutoCompleteStringList(drugSearchRepositoryPort.getIngredientAutoCompleteResponse(q)); + } + + + /** + * ์ฃผ์–ด์ง„ ์ฆ์ƒ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์•ฝํ’ˆ๋ช… ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSearch ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @return ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-25 + * @modified 2025-04-27 + */ + @Override + public SearchResponseList searchDrugBySymptom(DrugSearchNatural drugSearch) { + log("searchDrugBySymptom() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + drugSearch.getQuery()); + Page drugPage = drugSearchRepositoryPort.searchDocsBySymptom(drugSearch); + return SearchResponseMapper.toResponseByKeywordDomain(drugPage); + } + + + /** + * ์ฃผ์–ด์ง„ ์•ฝํ’ˆ๋ช… ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSearch ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @return ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-29 + * @modified 2025-05-03 + */ + @Override + public SearchResponseList searchDrugByDrugName(DrugSearchNatural drugSearch) { + log("searchDrugByDrugName() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + drugSearch.getQuery()); + Page drugPage = drugSearchRepositoryPort.searchDocsByDrugName(drugSearch); + + return SearchResponseMapper.toResponseByKeywordDomain(drugPage); + } + + /** + * ์ฃผ์–ด์ง„ ์„ฑ๋ถ„๋ช… ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSearch ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ๊ฐ์ฒด + * @return ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ ํฌํ•จํ•˜๋Š” SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + * @modified 2025-05-03 + */ + @Override + public SearchResponseList searchDrugByIngredient(DrugSearchNatural drugSearch) { + log("searchDrugByIngredient() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + drugSearch.getQuery()); + Page drugPage = drugSearchRepositoryPort.searchDocsByIngredient(drugSearch); + + return SearchResponseMapper.toResponseByKeywordDomain(drugPage); + } + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/common/exception/SearchException.java b/src/main/java/com/likelion/backendplus4/yakplus/search/common/exception/SearchException.java new file mode 100644 index 0000000..dc96606 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/common/exception/SearchException.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.search.common.exception; + +import com.likelion.backendplus4.yakplus.common.exception.CustomException; +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; + +public class SearchException extends CustomException { + private final ErrorCode errorCode; + + public SearchException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } + + @Override + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/common/exception/error/SearchErrorCode.java b/src/main/java/com/likelion/backendplus4/yakplus/search/common/exception/error/SearchErrorCode.java new file mode 100644 index 0000000..d0bdba4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/common/exception/error/SearchErrorCode.java @@ -0,0 +1,61 @@ +package com.likelion.backendplus4.yakplus.search.common.exception.error; + +import com.likelion.backendplus4.yakplus.common.exception.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +/** + * ์—๋Ÿฌ ์ฝ”๋“œ ์ธํ„ฐํŽ˜์ด์Šค ๊ฐ ์—๋Ÿฌ ํ•ญ๋ชฉ์— ๋Œ€ํ•œ HTTP ์ƒํƒœ, ์—๋Ÿฌ ๋ฒˆํ˜ธ, ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•œ๋‹ค. + * A[BB][CCC] + * A (1์ž๋ฆฌ) : ์—๋Ÿฌ ์‹ฌ๊ฐ๋„ (1~5) + * 1: ํด๋ผ์ด์–ธํŠธ ์˜ค๋ฅ˜ + * 2: ์ธ์ฆ ๊ด€๋ จ ์˜ค๋ฅ˜ + * 3: ์‚ฌ์šฉ์ž ๊ด€๋ จ ์˜ค๋ฅ˜ + * 4: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + * 5: ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ + * + * BB (2์ž๋ฆฌ) : ๋„๋ฉ”์ธ ์ฝ”๋“œ + * 10: ์‚ฌ์šฉ์ž ๊ด€๋ จ (ex: USER_NOT_FOUND) + * 20: ์ธ์ฆ ๊ด€๋ จ (ex: AUTHORIZATION_FAILED) + * 30: DB ๊ด€๋ จ ์˜ค๋ฅ˜ (ex: DB_CONNECTION_FAILED) + * 40: API ๊ด€๋ จ ์˜ค๋ฅ˜ (ex: API_TIMEOUT) + * 50: ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ (ex: INTERNAL_SERVER_ERROR) + * + * CCC (3์ž๋ฆฌ) : ์„ธ๋ถ€ ์˜ค๋ฅ˜ ์ˆœ๋ฒˆ + * 001: ์ฒซ ๋ฒˆ์งธ ์˜ค๋ฅ˜ + * 002: ๋‘ ๋ฒˆ์งธ ์˜ค๋ฅ˜ + * 003: ์„ธ ๋ฒˆ์งธ ์˜ค๋ฅ˜, ๋“ฑ๋“ฑ + * + * @modified 2025-04-18 + * @since 2025-04-16 + */ +@RequiredArgsConstructor +public enum SearchErrorCode implements ErrorCode { + INVALID_QUERY(HttpStatus.BAD_REQUEST, 140001, "๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"), + INVALID_PAGE(HttpStatus.BAD_REQUEST, 140002, "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + INVALID_SIZE(HttpStatus.BAD_REQUEST, 140003, "ํŽ˜์ด์ง€ ํฌ๊ธฐ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."), + INVALID_SEARCH_TYPE(HttpStatus.BAD_REQUEST, 140004, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒ€์ƒ‰ ํƒ€์ž…์ž…๋‹ˆ๋‹ค."), + ES_SEARCH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 430001, "Elasticsearch ๊ฒ€์ƒ‰ ์‹คํŒจ"), + RDB_SEARCH_ERROR(HttpStatus.NO_CONTENT, 430002, "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"), + EMBEDDING_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 440001, "์ž„๋ฒ ๋”ฉ API ํ˜ธ์ถœ ์‹คํŒจ"), + ES_SUGGEST_SEARCH_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 440002, "๊ฒ€์ƒ‰์–ด ์ž๋™์™„์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + ES_SEARCH_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, 440003, "์ฆ์ƒ ๊ฒ€์ƒ‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + + 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/search/config/ElasticsearchConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/search/config/ElasticsearchConfig.java new file mode 100644 index 0000000..85f5040 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/config/ElasticsearchConfig.java @@ -0,0 +1,7 @@ +package com.likelion.backendplus4.yakplus.search.config; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ElasticsearchConfig { +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/config/OpenAiConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/search/config/OpenAiConfig.java new file mode 100644 index 0000000..6d5ec09 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/config/OpenAiConfig.java @@ -0,0 +1,18 @@ +package com.likelion.backendplus4.yakplus.search.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ai.openai.api.OpenAiApi; + +@Configuration +public class OpenAiConfig { + + @Value("${spring.ai.openai.api-key}") + private String apiKey; + + @Bean + public OpenAiApi openaiApi() { + return new OpenAiApi(apiKey); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/config/RestTemplateConfig.java b/src/main/java/com/likelion/backendplus4/yakplus/search/config/RestTemplateConfig.java new file mode 100644 index 0000000..b9e8e2b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package com.likelion.backendplus4.yakplus.search.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/Drug.java b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/Drug.java new file mode 100644 index 0000000..d5129d1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/Drug.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.search.domain.model; + +import lombok.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Drug { + private Long drugId; + private String drugName; + private String company; + private List efficacy; + private float[] vector; + private LocalDate permitDate; + private boolean isGeneral; + private List materialInfo; + private String storeMethod; + private String validTerm; + private List usage; + private Map> precaution; + private String imageUrl; + private LocalDate cancelDate; + private String cancelName; + private boolean isHerbal; +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchDomain.java b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchDomain.java new file mode 100644 index 0000000..82cb94c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchDomain.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.search.domain.model; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Elasticsearch ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด๋Š” ๋„๋ฉ”์ธ ๋ชจ๋ธ ํด๋ž˜์Šค + * + * @since 2025-05-03 + */ +@Getter +@AllArgsConstructor +@Builder +public class DrugSearchDomain { + private Long drugId; + private String drugName; + private List efficacy; + private String company; + private String imageUrl; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchDomainNatural.java b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchDomainNatural.java new file mode 100644 index 0000000..f433003 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchDomainNatural.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.search.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋„๋ฉ”์ธ ๋ชจ๋ธ ํด๋ž˜์Šค + * + * @since 2025-05-03 + */ +@Getter +@AllArgsConstructor +@Builder +public class DrugSearchDomainNatural { + private Long drugId; + private String drugName; + private List efficacy; + private String company; + private String imageUrl; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchNatural.java b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchNatural.java new file mode 100644 index 0000000..ebb7eb4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/DrugSearchNatural.java @@ -0,0 +1,54 @@ +package com.likelion.backendplus4.yakplus.search.domain.model; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.common.exception.error.SearchErrorCode; +import lombok.Builder; +import lombok.Getter; +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋„๋ฉ”์ธ ๋ชจ๋ธ ํด๋ž˜์Šค + * + * query, page, size ํ•„๋“œ๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ ์ •์˜ํ•˜๊ณ , + * ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@Getter +@Builder +public class DrugSearchNatural { + private final String query; + private final int page; + private final int size; + + /** + * ์ฃผ์–ด์ง„ ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ž…๋ ฅ๊ฐ’์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜์—ฌ ๋ถ€์ ์ ˆํ•œ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋˜์ง‘๋‹ˆ๋‹ค. + * + * @param query ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด (null ๋˜๋Š” ๋นˆ ๋ฌธ์ž์—ด ๋ถˆ๊ฐ€) + * @param page ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0 ์ด์ƒ) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ (1 ์ด์ƒ) + * @throws SearchException ์ฟผ๋ฆฌ๊ฐ€ ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ํŽ˜์ด์ง€/์‚ฌ์ด์ฆˆ ๊ฐ’์ด ๋ถ€์ ์ ˆํ•œ ๊ฒฝ์šฐ + * @author ์ •์•ˆ์‹ + * @since 2025-05-02 + */ + public DrugSearchNatural(String query, int page, int size) { + + if (query == null || query.isBlank()) { + log(LogLevel.ERROR, "์ฟผ๋ฆฌ๊ฐ€ null์ด๊ฑฐ๋‚˜ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); + throw new SearchException(SearchErrorCode.INVALID_QUERY); + } + if (page < 0) { + log(LogLevel.ERROR, "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ์Œ์ˆ˜์ž…๋‹ˆ๋‹ค."); + throw new SearchException(SearchErrorCode.INVALID_PAGE); + } + if (size <= 0) { + log(LogLevel.ERROR, "์‚ฌ์ด์ฆˆ๊ฐ€ 0 ์ดํ•˜์ž…๋‹ˆ๋‹ค."); + throw new SearchException(SearchErrorCode.INVALID_SIZE); + } + this.query = query; + this.page = page; + this.size = size; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/Material.java b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/Material.java new file mode 100644 index 0000000..13915d3 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/domain/model/Material.java @@ -0,0 +1,31 @@ +package com.likelion.backendplus4.yakplus.search.domain.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class Material { + @JsonProperty("์„ฑ๋ถ„๋ช…") + private String name; + + @JsonProperty("๋ถ„๋Ÿ‰") + private String amount; + + @JsonProperty("๋‹จ์œ„") + private String unit; + + @JsonProperty("์ด๋Ÿ‰") + private String totalAmount; + + @JsonProperty("๊ทœ๊ฒฉ") + private String standard; + + @JsonProperty("๋น„๊ณ ") + private String note; + + @JsonProperty("์„ฑ๋ถ„์ •๋ณด") + private String info; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/DrugJpaAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/DrugJpaAdapter.java new file mode 100644 index 0000000..feb95a8 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/DrugJpaAdapter.java @@ -0,0 +1,95 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence; + + +import com.likelion.backendplus4.yakplus.search.application.port.out.DrugSearchRdbRepositoryPort; +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.common.exception.error.SearchErrorCode; +import com.likelion.backendplus4.yakplus.search.domain.model.Drug; +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.jpa.DrugJpaRepository; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.DrugMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * DB์—์„œ ์•ฝํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ์–ด๋Œ‘ํ„ฐ์ž…๋‹ˆ๋‹ค. + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @fields LEADING_NUMBER_PATTERN ์•ฝํ’ˆ ์ฃผ์˜์‚ฌํ•ญ์˜ ํ‚ค์—์„œ ์„ ํ–‰ ์ˆซ์ž๋ฅผ ์ถ”์ถœํ•˜๊ธฐ ์œ„ํ•œ ์ •๊ทœ ํ‘œํ˜„์‹ + * @fields drugJpaRepository ์•ฝํ’ˆ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ + * @modified 2025-05-03 ํ•จ์˜ˆ์ • + * - RDB ์ €์žฅ๋œ ์ฃผ์˜์‚ฌํ•ญ์„ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌํ•˜๋Š” ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + * @modified 2025-04-25 + * @since 2025-04-24 + */ +@Component +@RequiredArgsConstructor +public class DrugJpaAdapter implements DrugSearchRdbRepositoryPort { + private static final Pattern LEADING_NUMBER_PATTERN = Pattern.compile("^(\\d{1,2})"); + + private final DrugJpaRepository drugJpaRepository; + + /** + * ์•ฝํ’ˆ ์ •๋ณด๋ฅผ DB์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์•ฝํ’ˆ ID + * @return ์•ฝํ’ˆ ์ •๋ณด + * @throws SearchException ์•ฝํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + @Override + public Drug findById(Long id) { + Drug drug = DrugMapper.toDomainFromEntity( + drugJpaRepository.findById(id).orElseThrow( + () -> new SearchException(SearchErrorCode.RDB_SEARCH_ERROR))); + Map> precaution = drug.getPrecaution(); + if (precaution != null) { + drug.setPrecaution(sortByLeadingNumberIfPresent(precaution)); + } + return drug; + } + + /** + * ์•ฝํ’ˆ ์ฃผ์˜์‚ฌํ•ญ์„ ์„ ํ–‰ ์ˆซ์ž์— ๋”ฐ๋ผ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค. + * @param input ์•ฝํ’ˆ ์ฃผ์˜์‚ฌํ•ญ Map + * @return Map ์ •๋ ฌ๋œ ์•ฝํ’ˆ ์ฃผ์˜์‚ฌํ•ญ Map + * @since 2025-05-03 + * @author ํ•จ์˜ˆ์ • + */ + private Map> sortByLeadingNumberIfPresent(Map> input) { + return input.entrySet().stream() + .sorted(Comparator.comparingInt(entry -> extractLeadingNumberOrMax(entry.getKey()))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new + )); + } + + /** + * ์•ฝํ’ˆ ์ฃผ์˜์‚ฌํ•ญ์˜ ํ‚ค์—์„œ ์„ ํ–‰ ์ˆซ์ž๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * ๋งŒ์•ฝ ์ˆซ์ž๊ฐ€ ์—†์œผ๋ฉด Integer.MAX_VALUE๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ์ˆซ์ž ํฌ๋งท์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  Integer.MAX_VALUE๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์•ฝํ’ˆ ์ฃผ์˜์‚ฌํ•ญ์˜ ํ‚ค + * @return ์„ ํ–‰ ์ˆซ์ž ๋˜๋Š” Integer.MAX_VALUE + */ + private int extractLeadingNumberOrMax(String key) { + Matcher matcher = LEADING_NUMBER_PATTERN.matcher(key); + if (matcher.find()) { + try { + return Integer.parseInt(matcher.group(1)); + } catch (NumberFormatException ignored) {} + } + return Integer.MAX_VALUE; + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java new file mode 100644 index 0000000..0c1cf8c --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/ElasticsearchDrugAdapter.java @@ -0,0 +1,440 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.CompletionSuggestOption; +import co.elastic.clients.elasticsearch.core.search.Hit; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.search.application.port.out.DrugSearchRepositoryPort; +import com.likelion.backendplus4.yakplus.search.application.port.out.dto.SearchByKeywordParams; +import com.likelion.backendplus4.yakplus.search.application.port.out.dto.SearchByNaturalParams; +import com.likelion.backendplus4.yakplus.search.application.port.out.mapper.SearchByKeywordParamsMapper; +import com.likelion.backendplus4.yakplus.search.application.port.out.mapper.SearchByNaturalParamsMapper; +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.common.exception.error.SearchErrorCode; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomain; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomainNatural; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.document.DrugKeywordDocument; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.DrugMapper; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.natural.DrugSearchNaturalMapper; +import lombok.RequiredArgsConstructor; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * Elasticsearch๋ฅผ ํ†ตํ•ด Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * DrugSearchRepositoryPort๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ Elasticsearch ์›๊ฒฉ ํ˜ธ์ถœ์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @modified 2025-05-06 + * 25.04.27 - searchBySymptoms() ๋ฉ”์„œ๋“œ ๋ฆฌํŒฉํ† ๋ง + * 25.04.29 - ์•ฝํ’ˆ๋ช… ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + * @since 2025-04-22 + */ +@Component +@RequiredArgsConstructor +public class ElasticsearchDrugAdapter implements DrugSearchRepositoryPort { + + private final RestClient restClient; + private final ObjectMapper objectMapper; + private final ElasticsearchClient esClient; + + /** + * ์ฃผ์–ด์ง„ ์ฟผ๋ฆฌ ๋ฐ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด Elasticsearch์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•˜๊ณ , + * ๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์‹คํŒจ ์‹œ SearchException์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. + * + * @param drugSearchNatural ๊ฒ€์ƒ‰์–ด ๋ฐ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ๋‹ด์€ ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @param embeddings ๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ ๋ฐฐ์—ด + * @return ๊ฒ€์ƒ‰๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 + * 25.04.27 - ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง + * 25.05.02 - ํ—ฅ์‚ฌ๊ณ ๋‚  ๊ตฌ์กฐ์— ๋งž์ถ”์–ด ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง + * 25.05.03 - EsIndexName๋ฅผ ์™ธ๋ถ€์— ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ • + * @since 2025-04-22 + */ + @Override + public List searchByNatural(DrugSearchNatural drugSearchNatural, float[] embeddings, String EsIndexName) { + SearchByNaturalParams params = SearchByNaturalParamsMapper.toParams(drugSearchNatural, embeddings); + log("searchBySymptoms() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ๊ฒ€์ƒ‰์–ด: " + params.getQuery()); + try { + String esQuery = buildSearchQuery( + params.getVector(), + params.getSize(), + params.getFrom() + ); + Response response = executeSearch(esQuery, EsIndexName); + return parseSearchResults(response); + + } catch (Exception e) { + log(LogLevel.ERROR, "Elasticsearch ๊ฒ€์ƒ‰ ์‹คํŒจ: query = " + params.getQuery(), e); + throw new SearchException(SearchErrorCode.ES_SEARCH_ERROR); + } + } + + /** + * ์ฆ์ƒ ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * Elasticsearch์˜ Completion Suggest API๋ฅผ ํ™œ์šฉํ•˜์—ฌ symptomSuggester ํ•„๋“œ์—์„œ + * ์ฆ์ƒ ํ‚ค์›Œ๋“œ ์ž๋™์™„์„ฑ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ถ”์ฒœ๋œ ์ฆ์ƒ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-24 + */ + @Override + public List getSymptomAutoCompleteResponse(String q) { + return getAutoCompleteResponse("symptom_dictionary", "symp_sugg", "symptomSuggester", "symptom_autocomplete", q); + } + + /** + * ์•ฝํ’ˆ๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * drugNameSuggester ํ•„๋“œ์—์„œ Completion Suggest API๋ฅผ ํ†ตํ•ด ์•ฝํ’ˆ๋ช… ํ”„๋ฆฌํ”ฝ์Šค ๊ธฐ๋ฐ˜ + * ์ถ”์ฒœ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ์‚ฌ์šฉ์ž ์ž…๋ ฅ ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ถ”์ฒœ๋œ ์•ฝํ’ˆ๋ช… ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-28 + */ + @Override + public List getDrugNameAutoCompleteResponse(String q) { + return getAutoCompleteResponse("drug_keyword", "name_sugg", "drugNameSuggester", "drugName_autocomplete", q); + } + + /** + * ์„ฑ๋ถ„๋ช… ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * ingredientNameSuggester ํ•„๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ๊ฐ’์— ๋Œ€ํ•œ ์ž๋™์™„์„ฑ + * ์ถ”์ฒœ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ถ”์ฒœ๋œ ์„ฑ๋ถ„๋ช… ๋ฆฌ์ŠคํŠธ + * @throws SearchException ๊ฒ€์ƒ‰ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-05-01 + */ + @Override + public List getIngredientAutoCompleteResponse(String q) { + return getAutoCompleteResponse("drug_keyword", "ingr_sugg", "ingredientNameSuggester", "drugName_autocomplete", q); + } + + /** + * ๊ฒ€์ƒ‰์–ด์— ๋งค์นญ๋˜๋Š” ์ฆ์ƒ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ Elasticsearch์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * match ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ efficacy_list ํ•„๋“œ์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ DTO + * @return ์ฆ์ƒ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-24 + */ + @Override + public Page searchDocsBySymptom(DrugSearchNatural request) { + SearchByKeywordParams params = SearchByKeywordParamsMapper.toParams(request); + return searchWithMatch(params, "efficacy_list"); + } + + /** + * ๊ฒ€์ƒ‰์–ด์— ๋งค์นญ๋˜๋Š” ์•ฝํ’ˆ๋ช… ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ Elasticsearch์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * match ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ drugName ํ•„๋“œ์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ DTO + * @return ์•ฝํ’ˆ๋ช… ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-28 + */ + @Override + public Page searchDocsByDrugName(DrugSearchNatural request) { + SearchByKeywordParams params = SearchByKeywordParamsMapper.toParams(request); + return searchWithMatchPrefix(params, "drugName"); + } + + /** + * ๊ฒ€์ƒ‰์–ด์— ๋งค์นญ๋˜๋Š” ์„ฑ๋ถ„๋ช… ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ Elasticsearch์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * match ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ingredientName ํ•„๋“œ์—์„œ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ DTO + * @return ์„ฑ๋ถ„ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-05-01 + */ + @Override + public Page searchDocsByIngredient(DrugSearchNatural request) { + SearchByKeywordParams params = SearchByKeywordParamsMapper.toParams(request); + return searchWithMatch(params, "ingredientName"); + } + + + /** + * ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch Query DSL์„ JSON ๋ฌธ์ž์—ด๋กœ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param vector ๊ฒ€์ƒ‰์–ด ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ + * @param size ํ•œ ํŽ˜์ด์ง€์— ์กฐํšŒํ•  ๋ฌธ์„œ ์ˆ˜ + * @param from ์กฐํšŒ ์‹œ์ž‘ ์˜คํ”„์…‹ + * @return Elasticsearch์— ์ „๋‹ฌํ•  ์ฟผ๋ฆฌ JSON ๋ฌธ์ž์—ด + * @throws IOException JSON ์ง๋ ฌํ™” ๊ณผ์ •์—์„œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-06 + * 25.04.27 - ์Šคํฌ๋ฆฝํŠธ ํ•„๋“œ๋ช…์„ ๋ณ€๊ฒฝ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด์— ๋งž์ถ”์–ด ์ˆ˜์ • + * - ํ…์ŠคํŠธ ๋งค์นญ ํ•„๋“œ searchAll โ†’ efficacy ๋กœ ๋ณ€๊ฒฝ(ํ˜„์žฌ๋Š” ์•ฝ์˜ ํšจ๊ณผ๋กœ๋งŒ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์žˆ์Œ) + * 25.05.06 - ๋ฒกํ„ฐ ์œ ์‚ฌ๋„๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•„ํ„ฐ๋ง ๋ฐ ๋ณด๋„ˆ์Šค ๊ณ„์‚ฐ ๋กœ์ง ์ ์šฉ + * - ํ…์ŠคํŠธ ๋งค์นญ ํ•„๋“œ์— ๋Œ€ํ•œ ๋ถ€์ŠคํŠธ ์ œ๊ฑฐ ๋ฐ ๋ฒกํ„ฐ ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜์œผ๋กœ๋งŒ ์ˆœ์œ„ ์ •๋ ฌ + * - ์œ ์‚ฌ๋„ ๊ธฐ์ค€์— ๋”ฐ๋ผ ๋ฌธ์„œ์˜ ์ ์ˆ˜ ๊ณ„์‚ฐ (cutoff ๋ฐ bonus ์ ์šฉ) + * @since 2025-04-22 + */ + private String buildSearchQuery(float[] vector, int size, int from) throws IOException { + String vectorJson = objectMapper.writeValueAsString(vector); + + String painless = + "double v = cosineSimilarity(params.q, 'vector') + 1.0; " + + "if (v < params.cutoff) return 0.0; " + + "double bonus = v >= params.cutoff + 0.15 ? v * params.boostFactor : 0.0; " + + "return v + bonus;"; + + return String.format(""" + { + "from": %d, + "size": %d, + "sort": [ + { "_score": { "order": "desc" } }, + { "drugId": { "order": "asc" } } + ], + "query": { + "script_score": { + "query": { "match_all": {} }, + "script": { + "source": "%s", + "lang": "painless", + "params": { + "q": %s, + "cutoff": 1.15, + "boostFactor": 0.5 + } + } + } + } + } + """, from, size, painless.replace("\"", "\\\\\""), vectorJson + ); + } + + /** + * Elasticsearch์— ๋นŒ๋“œ๋œ ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•˜์—ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ Response๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + * + * @param esQuery Elasticsearch Query DSL JSON ๋ฌธ์ž์—ด + * @return Elasticsearch ์‘๋‹ต ๊ฐ์ฒด + * @throws IOException ์š”์ฒญ ์ „์†ก ๋˜๋Š” ์‘๋‹ต ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-24 + * @since 2025-04-22 + */ + private Response executeSearch(String esQuery, String EsIndexName) throws IOException { + log("executeSearch() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ"); + Request request = new Request("GET", "/" + EsIndexName + "/_search"); + request.setEntity(new NStringEntity(esQuery, ContentType.APPLICATION_JSON)); + return restClient.performRequest(request); + } + + /** + * Elasticsearch ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ Response์—์„œ hits ๋ฐฐ์—ด์„ ํŒŒ์‹ฑํ•ด + * Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. + * + * @param response Elasticsearch ๊ฒ€์ƒ‰ ์‘๋‹ต ๊ฐ์ฒด + * @return ํŒŒ์‹ฑ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @throws IOException ์‘๋‹ต ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-02 + * 25.04.27 - ์Šคํฌ๋ฆฝํŠธ ํ•„๋“œ๋ช…์„ ๋ณ€๊ฒฝ๋œ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด์— ๋งž์ถ”์–ด ์ˆ˜์ • + * 25.05.02 - ๊ธฐ์กด ๋กœ์ง(๋‹จ์ˆœ ์ˆœํ™˜ ๋ฐ List ๋งค์นญ) StreamSupport์„ ์‚ฌ์šฉํ•˜๋„๋ก ๊ฐœ์„  + * @since 2025-05-03 + */ + private List parseSearchResults(Response response) throws IOException { + log("parseSearchResults() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ"); + try (InputStream is = response.getEntity().getContent()) { + JsonNode hits = objectMapper.readTree(is) + .path("hits") + .path("hits"); + return StreamSupport.stream(hits.spliterator(), false) + .map(hit -> hit.path("_source")) + .map(DrugSearchNaturalMapper::toDomainNatural) + .collect(Collectors.toList()); + } + } + + /** + * Completion Suggest API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + *

+ * ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ํ”„๋ฆฌํ”ฝ์Šค(query)๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Elasticsearch Suggest API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , + * ์ง€์ •๋œ ํ•„๋“œ์—์„œ ์ž๋™์™„์„ฑ ํ›„๋ณด ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param indexName ๊ฒ€์ƒ‰ ๋Œ€์ƒ ์ธ๋ฑ์Šค ์ด๋ฆ„ + * @param suggesterKey suggest ์‘๋‹ต์—์„œ ์‚ฌ์šฉํ•  key ์ด๋ฆ„ + * @param fieldName ์ž๋™์™„์„ฑ ํ•„๋“œ๋ช… + * @param analyzer ์‚ฌ์šฉํ•  ๋ถ„์„๊ธฐ(analyzer) ์ด๋ฆ„ + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค (์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ฐ’) + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ + * @throws SearchException Elasticsearch ํ†ต์‹  ๋˜๋Š” ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-05-04 + */ + private List getAutoCompleteResponse(String indexName, String suggesterKey, String fieldName, String analyzer, String q) { + log("getAutoCompleteResponse() ํ˜ธ์ถœ - index: " + indexName + ", field: " + fieldName + ", query: " + q); + + try { + SearchResponse resp = esClient.search(s -> s + .index(indexName) + .suggest(su -> su + .suggesters(suggesterKey, sg -> sg + .prefix(q) + .completion(c -> c + .field(fieldName) + .analyzer(analyzer) + .size(10) + ) + ) + ), + Void.class + ); + + return resp.suggest() + .get(suggesterKey) + .getFirst() + .completion() + .options() + .stream() + .map(CompletionSuggestOption::text) + .distinct() + .toList(); + + } catch (IOException e) { + log(LogLevel.ERROR, "Elasticsearch ์ž๋™์™„์„ฑ ์‹คํŒจ: query = " + q, e); + throw new SearchException(SearchErrorCode.ES_SUGGEST_SEARCH_FAIL); + } + } + + /** + * match ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Elasticsearch ๊ฒ€์ƒ‰ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * @param request ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด (์ฟผ๋ฆฌ, ํŽ˜์ด์ง€, ์‚ฌ์ด์ฆˆ) + * @param fieldName ๊ฒ€์ƒ‰ ๋Œ€์ƒ ํ•„๋“œ๋ช… + * @return ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ Elasticsearch ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-24 + */ + private Page searchWithMatch(SearchByKeywordParams request, String fieldName) { + log("searchWithMatch() ํ˜ธ์ถœ - field: " + fieldName + ", query: " + request.getQuery()); + try { + SearchResponse resp = esClient.search(s -> s + .index("drug_keyword") + .from(request.getFrom() * request.getSize()) + .size(request.getSize()) + .query(qb -> qb + .match(m -> m + .field(fieldName) + .query(request.getQuery()) + ) + ), + DrugKeywordDocument.class); + + return toPageResponse(resp, request); + + } catch (IOException e) { + log(LogLevel.ERROR, "Elasticsearch ๊ฒ€์ƒ‰ ์‹คํŒจ: query = " + request.getQuery(), e); + throw new SearchException(SearchErrorCode.ES_SEARCH_FAIL); + } + } + + /** + * match_phrase_prefix ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Elasticsearch ๊ฒ€์ƒ‰ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * @param request ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด (์ฟผ๋ฆฌ, ํŽ˜์ด์ง€, ์‚ฌ์ด์ฆˆ) + * @param fieldName ๊ฒ€์ƒ‰ ๋Œ€์ƒ ํ•„๋“œ๋ช… + * @return ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @throws SearchException ๊ฒ€์ƒ‰ ์ค‘ Elasticsearch ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-24 + */ + private Page searchWithMatchPrefix(SearchByKeywordParams request, String fieldName) { + log("searchWithMatchPrefix() ํ˜ธ์ถœ - field: " + fieldName + ", query: " + request.getQuery()); + try { + SearchResponse resp = esClient.search(s -> s + .index("drug_keyword") + .from(request.getFrom() * request.getSize()) + .size(request.getSize()) + .query(qb -> qb + .matchPhrasePrefix(mpp -> mpp + .field(fieldName) + .query(request.getQuery()) + ) + ), + DrugKeywordDocument.class); + + return toPageResponse(resp, request); + + } catch (IOException e) { + log(LogLevel.ERROR, "Elasticsearch ๊ฒ€์ƒ‰ ์‹คํŒจ: query = " + request.getQuery(), e); + throw new SearchException(SearchErrorCode.ES_SEARCH_FAIL); + } + } + + /** + * Elasticsearch ์‘๋‹ต์„ Page ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param resp Elasticsearch ์‘๋‹ต ๊ฐ์ฒด + * @param request ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด + * @return ๋ณ€ํ™˜๋œ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @modified 2025-05-04 + * @since 2025-04-24 + */ + private Page toPageResponse(SearchResponse resp, SearchByKeywordParams request) { + List results = resp.hits().hits().stream() + .map(Hit::source) + .filter(Objects::nonNull) + .map(DrugMapper::toDomainByDocument) + .toList(); + + long totalHits = resp.hits().total().value(); + return new PageImpl<>(results, PageRequest.of(request.getFrom(), request.getSize()), totalHits); + } + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/KmBertEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/KmBertEmbeddingAdapter.java new file mode 100644 index 0000000..971039e --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/KmBertEmbeddingAdapter.java @@ -0,0 +1,41 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.dto.EmbeddingRequestText; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import lombok.RequiredArgsConstructor; +import com.likelion.backendplus4.yakplus.search.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.UriCompBuilder; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; + + +@Component +@RequiredArgsConstructor +public class KmBertEmbeddingAdapter implements EmbeddingPort { + private final UriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKmbertEmbeding(); + } + + private float[] getEmbeddingVector(URI embedUri, String text) { + EmbeddingRequestText embeddingRequestText = new EmbeddingRequestText(); + embeddingRequestText.setText(text); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity request = new HttpEntity<>(embeddingRequestText, headers); + return restTemplate.postForObject(embedUri, request, float[].class); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/KrSBertEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/KrSBertEmbeddingAdapter.java new file mode 100644 index 0000000..7f16753 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/KrSBertEmbeddingAdapter.java @@ -0,0 +1,40 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.search.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.dto.EmbeddingRequestText; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.UriCompBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; + +@Component +@RequiredArgsConstructor +public class KrSBertEmbeddingAdapter implements EmbeddingPort { + private final UriCompBuilder apiUriCompBuilder; + private final RestTemplate restTemplate; + + @Override + public float[] getEmbedding(String text) { + URI embeddingURI = getEmbeddingURI(); + return getEmbeddingVector(embeddingURI, text); + } + + private URI getEmbeddingURI() { + return apiUriCompBuilder.getUriForKrSbertEmbeding(); + } + + 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); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java new file mode 100644 index 0000000..d0086f6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/OpenAIEmbeddingAdapter.java @@ -0,0 +1,78 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.search.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.search.common.exception.SearchException; +import com.likelion.backendplus4.yakplus.search.common.exception.error.SearchErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * OpenAI ์ž„๋ฒ ๋”ฉ API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ…์ŠคํŠธ์— ๋Œ€ํ•œ ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ์„ ์ƒ์„ฑํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * EmbeddingPort ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ, ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ SearchException์œผ๋กœ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @modified 2025-04-24 + * @since 2025-04-22 + */ +@Component +@RequiredArgsConstructor +public class OpenAIEmbeddingAdapter implements EmbeddingPort { + private final OpenAiApi openAiApi; + private static final String EMBEDDING_MODEL = "text-embedding-3-small"; + + /** + * ์ฃผ์–ด์ง„ ํ…์ŠคํŠธ๋ฅผ OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์— ์ „๋‹ฌํ•˜์—ฌ ๋ฒกํ„ฐ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ๋‚ด๋ถ€์—์„œ OpenAiEmbeddingModel์„ ์ƒ์„ฑํ•˜๊ณ  retry ํ…œํ”Œ๋ฆฟ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * API ํ˜ธ์ถœ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด SearchException(EMBEDDING_API_ERROR)์„ ๋˜์ง‘๋‹ˆ๋‹ค. + * + * @param text ๋ฒกํ„ฐํ™”ํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @return float ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ + * @throws SearchException EMBEDDING_API_ERROR ์ฝ”๋“œ๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ ๋ฐœ์ƒ + * @author ์ •์•ˆ์‹ + * @modified 2025-04-24 + * @since 2025-04-22 + */ + @Override + public float[] getEmbedding(String text) { + try { + OpenAiEmbeddingModel embeddingModel = createEmbeddingModel(); + EmbeddingResponse response = embeddingModel.embedForResponse(List.of(text)); + return response.getResults().getFirst().getOutput(); + } catch (Exception e) { + log(LogLevel.ERROR, "์ž„๋ฒ ๋”ฉ API์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.", e); + throw new SearchException(SearchErrorCode.EMBEDDING_API_ERROR); + } + } + + /** + * OpenAiEmbeddingModel ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * MetadataMode์™€ ๋ชจ๋ธ ์ด๋ฆ„, RetryUtils ์„ค์ •์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๊ธฐํ™”๋œ OpenAiEmbeddingModel ๊ฐ์ฒด + * @author ์ •์•ˆ์‹ + * @modified 2025-04-24 + * @since 2025-04-22 + */ + private OpenAiEmbeddingModel createEmbeddingModel() { + log("createEmbeddingModel() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ, ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ: " + EMBEDDING_MODEL); + return new OpenAiEmbeddingModel( + openAiApi, + MetadataMode.EMBED, + OpenAiEmbeddingOptions.builder() + .model(EMBEDDING_MODEL) + .build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/document/DrugKeywordDocument.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/document/DrugKeywordDocument.java new file mode 100644 index 0000000..c8aba05 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/document/DrugKeywordDocument.java @@ -0,0 +1,60 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.document; + +import java.util.List; + +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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Document(indexName = "drug_keyword") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class DrugKeywordDocument { + + @Id + @Field(type = FieldType.Keyword, name = "drugId") + private Long drugId; + + @Field(type = FieldType.Text, name = "drugName") + private String drugName; + + @Field(type = FieldType.Text, name = "company") + private String company; + + @Field(type = FieldType.Keyword, name = "imageUrl") + private String imageUrl; + + @Field(type = FieldType.Text, name = "efficacy") + private List efficacy; + + @Field(type = FieldType.Text, name = "efficacy_list") + private List efficacyList; + + @Field(type = FieldType.Text, name = "ingredientName") + private List ingredientName; + + @CompletionField( + analyzer = "drugName_autocomplete", + searchAnalyzer = "drugName_autocomplete" + ) + private List drugNameSuggester; + + @CompletionField( + analyzer = "drugName_autocomplete", + searchAnalyzer = "drugName_autocomplete" + ) + private List ingredientNameSuggester; + +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/dto/EmbeddingRequestText.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/dto/EmbeddingRequestText.java new file mode 100644 index 0000000..0dad207 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/dto/EmbeddingRequestText.java @@ -0,0 +1,12 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.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/search/infrastructure/adapter/persistence/entity/DrugEntity.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/entity/DrugEntity.java new file mode 100644 index 0000000..ad05f15 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/entity/DrugEntity.java @@ -0,0 +1,69 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.entity; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonProperty; + +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 +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name="GOV_DRUG_RAW_DATA") +public class DrugEntity { + @Id + @Column(name="ITEM_SEQ") + private Long id; + + @Column( name= "ITEM_NAME", columnDefinition = "TEXT") + private String drugName; + + @Column( name= "ENTP_NAME") + private String company; + + @Column( name= "ITEM_PERMIT_DATE") + private LocalDate permitDate; + + @Column(name = "ETC_OTC_CODE") + private boolean isGeneral; + + @Column(name = "MATERIAL_NAME", columnDefinition = "JSON") + private String materialInfo; + + @JsonProperty("STORAGE_METHOD") + @Column(name = "STORAGE_METHOD", columnDefinition = "TEXT") + private String storeMethod; + + @Column(name = "VALID_TERM") + private String validTerm; + + @Column(name = "EE_DOC_DATA", columnDefinition = "JSON") + private String efficacy; + + @Column(name = "UD_DOC_DATA", columnDefinition = "JSON") + private String usage; + + @Column(name = "NB_DOC_DATA", columnDefinition = "JSON") + private String precaution; + + @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/search/infrastructure/adapter/persistence/jpa/DrugJpaRepository.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/jpa/DrugJpaRepository.java new file mode 100644 index 0000000..346cc09 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/adapter/persistence/jpa/DrugJpaRepository.java @@ -0,0 +1,12 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.entity.DrugEntity; +/** + * ์˜์•ฝํ’ˆ ์ •๋ณด(GovDrugEntity)์— ๋Œ€ํ•œ JPA ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค. + * + * @since 2025-04-30 + */ +public interface DrugJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/DrugMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/DrugMapper.java new file mode 100644 index 0000000..1d33270 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/DrugMapper.java @@ -0,0 +1,189 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.support; + + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.likelion.backendplus4.yakplus.search.application.port.out.dto.SearchByKeywordParams; +import com.likelion.backendplus4.yakplus.search.domain.model.Drug; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomain; +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.document.DrugKeywordDocument; +import com.likelion.backendplus4.yakplus.search.infrastructure.adapter.persistence.entity.DrugEntity; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.DetailSearchResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponse; + +import co.elastic.clients.elasticsearch.core.search.Hit; + +/** + * ์ฆ์ƒ ๊ด€๋ จ ๊ฐ์ฒด๋ฅผ ๋‹ค๋ฃจ๋Š” ๋งคํผ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-25 + * @modified 2025-05-03 + */ +public class DrugMapper { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * ES ์ƒ‰์ธ์šฉ Document๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ(DrugSymptom)๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symptomDocument ๋ณ€ํ™˜ ๋Œ€์ƒ ES Document ๊ฐ์ฒด + * @return DrugSymptom ๋„๋ฉ”์ธ ๋ชจ๋ธ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-25 + * @modified 2025-05-01 + */ + public static DrugSearchDomain toDomainByDocument(DrugKeywordDocument symptomDocument) { + return DrugSearchDomain.builder() + .drugId(symptomDocument.getDrugId()) + .drugName(symptomDocument.getDrugName()) + .efficacy(symptomDocument.getEfficacy()) + .company(symptomDocument.getCompany()) + .imageUrl(symptomDocument.getImageUrl()) + .build(); + } + + + /** + * Elasticsearch ์‘๋‹ต์„ Page ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param resp Elasticsearch ์‘๋‹ต ๊ฐ์ฒด + * @param request ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด + * @return ๋ณ€ํ™˜๋œ ํŽ˜์ด์ง€ ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-24 + * @modified 2025-05-04 + */ + public static Page toPageResponse( + co.elastic.clients.elasticsearch.core.SearchResponse resp, SearchByKeywordParams request) { + List results = resp.hits().hits().stream() + .map(Hit::source) + .filter(Objects::nonNull) + .map(DrugMapper::toDomainByDocument) + .toList(); + + long totalHits = Objects.requireNonNull(resp.hits().total()).value(); + return new PageImpl<>(results, PageRequest.of(request.getFrom(), request.getSize()), totalHits); + } + + + + /** + * ๋„๋ฉ”์ธ ๋ชจ๋ธ(DrugSymptom)์„ HTTP ์‘๋‹ต์šฉ DTO(DrugSymptomResponse)๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugSymptom ๋ณ€ํ™˜ ๋Œ€์ƒ ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @return DrugSymptomResponse ์‘๋‹ต DTO ๊ฐ์ฒด + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-04-25 + * @modified 2025-04-25 + */ + public static SearchResponse toResponse(DrugSearchDomain drugSymptom) { + return SearchResponse.builder() + .drugId(drugSymptom.getDrugId()) + .drugName(drugSymptom.getDrugName()) + .efficacy(drugSymptom.getEfficacy()) + .company(drugSymptom.getCompany()) + .imageUrl(drugSymptom.getImageUrl()) + .build(); + } + + /** + * Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ DetailSearchResponse DTO๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param d ๋ณ€ํ™˜ํ•  Drug ๊ฐ์ฒด + * @return DetailSearchResponse ์‘๋‹ต ๊ฐ์ฒด + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + public static DetailSearchResponse toDetailResponse(Drug d) { + return DetailSearchResponse.builder() + .drugId(d.getDrugId()) + .drugName(d.getDrugName()) + .company(d.getCompany()) + .efficacy(d.getEfficacy()) + .permitDate(d.getPermitDate()) + .isGeneral(d.isGeneral()) + .materialInfo(d.getMaterialInfo()) + .storeMethod(d.getStoreMethod()) + .validTerm(d.getValidTerm()) + .usage(d.getUsage()) + .precaution(d.getPrecaution()) + .imageUrl(d.getImageUrl()) + .cancelDate(d.getCancelDate()) + .cancelName(d.getCancelName()) + .isGeneral(d.isGeneral()) + .build(); + } + + /** + * GovDrugEntity ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋ฅผ Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param d ๋ณ€ํ™˜ํ•  GovDrugEntity ๊ฐ์ฒด + * @return Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + public static Drug toDomainFromEntity(DrugEntity d) { + return Drug.builder() + .drugId(d.getId()) + .drugName(d.getDrugName()) + .company(d.getCompany()) + .efficacy(toListFromString(d.getEfficacy())) + .permitDate(d.getPermitDate()) + .isGeneral(d.isGeneral()) + .materialInfo(toListFromString(d.getMaterialInfo())) + .storeMethod(d.getStoreMethod()) + .validTerm(d.getValidTerm()) + .usage(toListFromString(d.getUsage())) + .precaution(toMapFromString(d.getPrecaution())) + .imageUrl(d.getImageUrl()) + .cancelDate(d.getCancelDate()) + .cancelName(d.getCancelName()) + .isGeneral(d.isGeneral()) + .build(); + } + + /** + * JSON ๋ฌธ์ž์—ด์„ List ๊ฐ์ฒด๋กœ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param str JSON ํ˜•์‹์˜ ๋ฌธ์ž์—ด + * @return ํŒŒ์‹ฑ๋œ List ๊ฐ์ฒด, ์‹คํŒจ ์‹œ null ๋ฐ˜ํ™˜ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + private static List toListFromString(String str){ + try { + return objectMapper.readValue(str, List.class); + } catch (Exception e){ + e.printStackTrace(); + return null; + } + } + + /** + * JSON ๋ฌธ์ž์—ด์„ Map ๊ฐ์ฒด๋กœ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param str JSON ํ˜•์‹์˜ ๋ฌธ์ž์—ด + * @return ํŒŒ์‹ฑ๋œ Map ๊ฐ์ฒด, ์‹คํŒจ ์‹œ null ๋ฐ˜ํ™˜ + * + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + private static Map toMapFromString(String str){ + try { + return objectMapper.readValue(str, Map.class); + } catch (Exception e){ + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/UriCompBuilder.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/UriCompBuilder.java new file mode 100644 index 0000000..bf6da0f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/UriCompBuilder.java @@ -0,0 +1,65 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.support; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +/*** + * API ์š”์ฒญ URI ๊ฐ์ฒด ์ƒ์„ฑ ๋นŒ๋” + * + * application.yml์—์„œ ์ฃผ์ž…๋˜๋Š” ์†์„ฑ ๊ฐ’์œผ๋กœ, + * API HOST, PATH๋ฅผ ํ™•์ธํ•ด URI ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. + * + * @since 2025-04-15 + */ +@Component +public class UriCompBuilder { + private final String API_KM_BERT; + private final String API_KR_SBERT; + + public UriCompBuilder( + @Value("${embed.kmbert}") String API_KM_BERT, + @Value("${embed.krsbert}") String API_KR_SBERT) { + this.API_KM_BERT = API_KM_BERT; + this.API_KR_SBERT = API_KR_SBERT; + } + + + /** + * 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/search/infrastructure/support/natural/DrugSearchNaturalMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/natural/DrugSearchNaturalMapper.java new file mode 100644 index 0000000..5f4d06f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/natural/DrugSearchNaturalMapper.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.support.natural; + +import com.fasterxml.jackson.databind.JsonNode; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomainNatural; + +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Elasticsearch ์‘๋‹ต์˜ JsonNode๋ฅผ DrugSearchDomainNatural ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋งคํผ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +public class DrugSearchNaturalMapper { + /** + * JsonNode์˜ ํ•„๋“œ ๊ฐ’์„ ์ฝ์–ด๋“ค์—ฌ DrugSearchDomainNatural ๊ฐ์ฒด๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @param src JsonNode ํ˜•ํƒœ์˜ Elasticsearch ์‘๋‹ต _source ๋ฐ์ดํ„ฐ + * @return ๋งคํ•‘๋œ DrugSearchDomainNatural ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @author ์ •์•ˆ์‹ + * @since 2025-05-03 + */ + public static DrugSearchDomainNatural toDomainNatural(JsonNode src) { + return DrugSearchDomainNatural.builder() + .drugId(src.path("drugId").asLong()) + .drugName(src.path("drugName").asText()) + .company(src.path("company").asText()) + .efficacy(StreamSupport.stream(src.path("efficacy").spliterator(), false) + .map(JsonNode::asText) + .collect(Collectors.toList())) + .imageUrl(src.path("imageUrl").asText(null)) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/natural/SearchNaturalMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/natural/SearchNaturalMapper.java new file mode 100644 index 0000000..af42ce4 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/infrastructure/support/natural/SearchNaturalMapper.java @@ -0,0 +1,33 @@ +package com.likelion.backendplus4.yakplus.search.infrastructure.support.natural; + +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; + +/** + * ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋งคํผ ํด๋ž˜์Šค + * + * ์ด ๋งคํผ๋Š” ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ „๋‹ฌ๋œ ์ฟผ๋ฆฌ, ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ ํฌ๊ธฐ ์ •๋ณด๋ฅผ + * DrugSearchNatural ๊ฐ์ฒด๋กœ ์กฐ๋ฆฝํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-03 + */ +public class SearchNaturalMapper { + + /** + * ์ปจํŠธ๋กค๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌ๋œ ์ฟผ๋ฆฌ, ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ, ํŽ˜์ด์ง€ ํฌ๊ธฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ + * DrugSearchNatural ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param query ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด + * @param page ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ + * @return ๊ตฌ์„ฑ๋œ DrugSearchNatural ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @author ์ •์•ˆ์‹ + * @since 2025-05-02 + */ + public static DrugSearchNatural toDrugSearchNatural(String query, int page, int size) { + return DrugSearchNatural.builder() + .query(query) + .page(page) + .size(size) + .build(); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/DrugController.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/DrugController.java new file mode 100644 index 0000000..9aaa21a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/DrugController.java @@ -0,0 +1,172 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller; + +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.search.application.port.in.SearchDrugUseCase; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchNatural; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.natural.SearchNaturalMapper; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.AutoCompleteStringList; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.DetailSearchResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponseList; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * ์•ฝํ’ˆ ๊ฒ€์ƒ‰ API ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค + * + * @modified 2025-05-02 + * @since 2025-04-22 + */ +@RestController +@RequestMapping("/drugs") +@RequiredArgsConstructor +public class DrugController { + private final SearchDrugUseCase searchDrugUseCase; + + /** + * ์ž์—ฐ์–ด ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param query ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด + * @param page ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ๊ฐ’ 0) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’ 10) + * @return ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด์€ ํ‘œ์ค€ API ์‘๋‹ต + * @author ์ •์•ˆ์‹ + * @modified 2025-05-02 + * - GET ๋ฉ”์†Œ๋“œ๋กœ ๋ณ€๊ฒฝ, ์ด์ œ Service์— ๋„˜๊ธธ ๋•Œ SearchRequest๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ + * @since 2025-04-22 + */ + @GetMapping("/search/natural") + public ResponseEntity> searchNatural( + @RequestParam("q") String query, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "10") int size) { + + log("drugController ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ์š”์ฒญ ์ˆ˜์‹  - ์ž์—ฐ์–ด: " + query); + DrugSearchNatural natural = SearchNaturalMapper.toDrugSearchNatural(query, page, size); + return ApiResponse.success(searchDrugUseCase.searchDrugByNatural(natural)); + } + + /** + * ์ฆ์ƒ ํ‚ค์›Œ๋“œ ์ž๋™์™„์„ฑ API + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ์‹ผ ApiResponse + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + @GetMapping("/autocomplete/symptom") + public ResponseEntity> autocompleteSymptom(@RequestParam String q) { + log("drugController ์š”์ฒญ ์ˆ˜์‹  - symptom autocomplete, query: " + q); + AutoCompleteStringList results = searchDrugUseCase.getSymptomAutoComplete(q); + return ApiResponse.success(results); + } + + /** + * ์•ฝํ’ˆ๋ช… ์ž๋™์™„์„ฑ API + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ์‹ผ ApiResponse + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + @GetMapping("/autocomplete/name") + public ResponseEntity> autocompleteDrugName(@RequestParam String q) { + log("drugController ์š”์ฒญ ์ˆ˜์‹  - drug name autocomplete, query: " + q); + AutoCompleteStringList results = searchDrugUseCase.getDrugNameAutoComplete(q); + return ApiResponse.success(results); + } + + /** + * ์„ฑ๋ถ„๋ช… ์ž๋™์™„์„ฑ API + * + * @param q ๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค + * @return ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ‚ค์›Œ๋“œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ์‹ผ ApiResponse + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + @GetMapping("/autocomplete/ingredient") + public ResponseEntity> autocompleteIngredient(@RequestParam String q) { + log("drugController ์š”์ฒญ ์ˆ˜์‹  - ingredient autocomplete, query: " + q); + AutoCompleteStringList results = searchDrugUseCase.getIngredientAutoComplete(q); + return ApiResponse.success(results); + } + + /** + * ์ฆ์ƒ ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ API + * + * @param query ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ + * @param page ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ๊ฐ’ 0) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’ 10) + * @return ์ฆ์ƒ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + @GetMapping("/search/symptom") + public ResponseEntity> searchBySymptom( + @RequestParam("q") String query, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "10") int size) { + + log("drugController ์ฆ์ƒ ๊ฒ€์ƒ‰ ์š”์ฒญ ์ˆ˜์‹  - query: " + query); + DrugSearchNatural natural = SearchNaturalMapper.toDrugSearchNatural(query, page, size); + return ApiResponse.success(searchDrugUseCase.searchDrugBySymptom(natural)); + } + + /** + * ์•ฝํ’ˆ๋ช… ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ API + * + * @param query ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ + * @param page ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ๊ฐ’ 0) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’ 10) + * @return ์•ฝํ’ˆ๋ช… ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + @GetMapping("/search/name") + public ResponseEntity> searchByName( + @RequestParam("q") String query, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "10") int size) { + + log("drugController ์•ฝํ’ˆ๋ช… ๊ฒ€์ƒ‰ ์š”์ฒญ ์ˆ˜์‹  - query: " + query); + DrugSearchNatural natural = SearchNaturalMapper.toDrugSearchNatural(query, page, size); + return ApiResponse.success(searchDrugUseCase.searchDrugByDrugName(natural)); + } + + /** + * ์„ฑ๋ถ„๋ช… ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ API + * + * @param query ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ + * @param page ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (๊ธฐ๋ณธ๊ฐ’ 0) + * @param size ํŽ˜์ด์ง€๋‹น ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’ 10) + * @return ์„ฑ๋ถ„๋ช… ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + @GetMapping("/search/ingredient") + public ResponseEntity> searchByIngredient( + @RequestParam("q") String query, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "10") int size) { + + log("drugController ์„ฑ๋ถ„ ๊ฒ€์ƒ‰ ์š”์ฒญ ์ˆ˜์‹  - query: " + query); + DrugSearchNatural natural = SearchNaturalMapper.toDrugSearchNatural(query, page, size); + return ApiResponse.success(searchDrugUseCase.searchDrugByIngredient(natural)); + } + + /** + * ์˜์•ฝํ’ˆ ID๋ฅผ ํ†ตํ•ด ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” API์ž…๋‹ˆ๋‹ค. + * + * @param id ์กฐํšŒํ•  ์˜์•ฝํ’ˆ์˜ ๊ณ ์œ  ID + * @return ์˜์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด ApiResponse + * @author ํ•จ์˜ˆ์ • + * @since 2025-04-30 + */ + @GetMapping("/search/detail/{id}") + public ResponseEntity> searchDetail(@PathVariable Long id) { + return ApiResponse.success(searchDrugUseCase.searchByDrugId(id)); + } + +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/docs/DrugControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/docs/DrugControllerDocs.java new file mode 100644 index 0000000..769c929 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/docs/DrugControllerDocs.java @@ -0,0 +1,68 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.AutoCompleteStringList; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.DetailSearchResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponseList; +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; + +/** + * ์•ฝํ’ˆ ๊ฒ€์ƒ‰ API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-04 + */ +@Tag(name = "Drug", description = "์•ฝํ’ˆ ๊ฒ€์ƒ‰ ๋ฐ ์ž๋™์™„์„ฑ API") +public interface DrugControllerDocs { + + @Operation(summary = "์ž์—ฐ์–ด ๊ฒ€์ƒ‰", description = "์ž์—ฐ์–ด ๋ฌธ์žฅ์œผ๋กœ ์•ฝํ’ˆ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> searchNatural( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ", example = "๋จธ๋ฆฌ๊ฐ€ ์•„ํŒŒ์š”") String q, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ", example = "0") int page, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "10") int size + ); + + @Operation(summary = "์ฆ์ƒ ์ž๋™์™„์„ฑ", description = "์ฆ์ƒ ํ‚ค์›Œ๋“œ ์ž๋™์™„์„ฑ ์ถ”์ฒœ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> autocompleteSymptom( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค", example = "๋‘ํ†ต") String q + ); + + @Operation(summary = "์•ฝํ’ˆ๋ช… ์ž๋™์™„์„ฑ", description = "์•ฝํ’ˆ๋ช… ํ‚ค์›Œ๋“œ ์ž๋™์™„์„ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> autocompleteDrugName( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค", example = "ํƒ€์ด๋ ˆ") String q + ); + + @Operation(summary = "์„ฑ๋ถ„๋ช… ์ž๋™์™„์„ฑ", description = "์„ฑ๋ถ„๋ช… ํ‚ค์›Œ๋“œ ์ž๋™์™„์„ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> autocompleteIngredient( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰์–ด ํ”„๋ฆฌํ”ฝ์Šค", example = "์•„์„ธํŠธ") String q + ); + + @Operation(summary = "์ฆ์ƒ ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰", description = "์ฆ์ƒ ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> searchBySymptom( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ", example = "๋‘ํ†ต") String q, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ", example = "0") int page, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "10") int size + ); + + @Operation(summary = "์•ฝํ’ˆ๋ช… ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰", description = "์•ฝํ’ˆ๋ช… ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> searchByName( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ", example = "ํƒ€์ด๋ ˆ๋†€") String q, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ", example = "0") int page, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "10") int size + ); + + @Operation(summary = "์„ฑ๋ถ„๋ช… ๊ธฐ๋ฐ˜ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰", description = "์„ฑ๋ถ„๋ช… ํ‚ค์›Œ๋“œ๋กœ ์•ฝํ’ˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> searchByIngredient( + @Parameter(in = ParameterIn.QUERY, description = "๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ", example = "์•„์„ธํŠธ์•„๋ฏธ๋…ธํŽœ") String q, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ", example = "0") int page, + @Parameter(in = ParameterIn.QUERY, description = "ํŽ˜์ด์ง€ ํฌ๊ธฐ", example = "10") int size + ); + + @Operation(summary = "์˜์•ฝํ’ˆ ์ƒ์„ธ ์กฐํšŒ", description = "์˜์•ฝํ’ˆ ID๋กœ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.") + ResponseEntity> searchDetail( + @Parameter(in = ParameterIn.PATH, description = "์˜์•ฝํ’ˆ ๊ณ ์œ  ID", example = "12345") Long id + ); +} \ No newline at end of file diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/request/SearchRequest.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/request/SearchRequest.java new file mode 100644 index 0000000..29d0f90 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/request/SearchRequest.java @@ -0,0 +1,15 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.dto.request; + +import org.springframework.web.bind.annotation.RequestParam; + +/** + * ๊ฒ€์ƒ‰ ์š”์ฒญ ์ •๋ณด DTO + * + * @since 2025-04-22 + * @modified 2025-04-24 + */ +public record SearchRequest( + String query, + int page, + int size) { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/request/SearchRequestById.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/request/SearchRequestById.java new file mode 100644 index 0000000..5ff644a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/request/SearchRequestById.java @@ -0,0 +1,8 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.dto.request; + +import lombok.Getter; + +@Getter +public class SearchRequestById { + Long durgId; +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/AutoCompleteStringList.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/AutoCompleteStringList.java new file mode 100644 index 0000000..eb23c0f --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/AutoCompleteStringList.java @@ -0,0 +1,13 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response; + +import java.util.List; + +/** + * ์ž๋™์™„์„ฑ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ DTO + * @since 2025-04-28 + * @modified 2025-04-28 + */ +public record AutoCompleteStringList( + List autoCompleteList +) { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/DetailSearchResponse.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/DetailSearchResponse.java new file mode 100644 index 0000000..d7973c1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/DetailSearchResponse.java @@ -0,0 +1,34 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response; + +import com.likelion.backendplus4.yakplus.search.domain.model.Material; +import lombok.Builder; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ณด DTO + * + * @modified 2025-04-27 + * 25.04.27 - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ํ•„๋“œ ์ˆ˜์ • + * @since 2025-04-22 + */ +@Builder +public record DetailSearchResponse( + Long drugId, + String drugName, + String company, + List efficacy, + LocalDate permitDate, + boolean isGeneral, + List materialInfo, + String storeMethod, + String validTerm, + List usage, + Map> precaution, + String imageUrl, + LocalDate cancelDate, + String cancelName, + boolean isHerbal) { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/SearchResponse.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/SearchResponse.java new file mode 100644 index 0000000..1dd92c1 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/SearchResponse.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response; + +import java.util.List; + + +import lombok.Builder; + +/** + * ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ณด DTO + * + * @modified 2025-04-27 + * 25.04.27 - Drug ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ํ•„๋“œ ์ˆ˜์ • + * @since 2025-04-22 + */ +@Builder +public record SearchResponse( + Long drugId, + String drugName, + String company, + List efficacy, + String imageUrl) { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/SearchResponseList.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/SearchResponseList.java new file mode 100644 index 0000000..69dafda --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/controller/dto/response/SearchResponseList.java @@ -0,0 +1,14 @@ +package com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response; + +import java.util.List; + +/** + * ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ณด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋‹ด๋Š” DTO + * @since 2025-04-28 + * @modified 2025-04-28 + */ +public record SearchResponseList( + List searchResponseList, + long totalResponseCount +) { +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/mapper/SearchRequestMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/mapper/SearchRequestMapper.java new file mode 100644 index 0000000..c315850 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/mapper/SearchRequestMapper.java @@ -0,0 +1,24 @@ +package com.likelion.backendplus4.yakplus.search.presentation.mapper; + +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.request.SearchRequest; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * Presentation ๊ณ„์ธต์˜ Request ๋งคํ•‘ ๋กœ์ง์„ ๋‹ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * HTTP ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ SearchRequest ๋„๋ฉ”์ธ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +public class SearchRequestMapper { + /** + * HTTP GET/POST ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ SearchRequest DTO๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @param query ๊ฒ€์ƒ‰์–ด + * @param page ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ) + * @param size ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ + * @return SearchRequest ๊ฐ์ฒด + */ + public static SearchRequest toSearchRequest(String query, int page, int size) { + return new SearchRequest(query, page, size); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/mapper/SearchResponseMapper.java b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/mapper/SearchResponseMapper.java new file mode 100644 index 0000000..155bef9 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/search/presentation/mapper/SearchResponseMapper.java @@ -0,0 +1,88 @@ +package com.likelion.backendplus4.yakplus.search.presentation.mapper; + +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomain; +import com.likelion.backendplus4.yakplus.search.domain.model.DrugSearchDomainNatural; +import com.likelion.backendplus4.yakplus.search.infrastructure.support.DrugMapper; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponse; +import com.likelion.backendplus4.yakplus.search.presentation.controller.dto.response.SearchResponseList; +import org.springframework.data.domain.Page; + +import java.util.List; + +/** + * DrugSearchDomainNatural ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ API ์‘๋‹ต DTO(SearchResponse, SearchResponseList)๋กœ ๋งคํ•‘ํ•˜๋Š” ์œ ํ‹ธ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @modified 2025-05-04 + * @since 2025-04-22 + */ +public class SearchResponseMapper { + + /** + * ๋‹จ์ผ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ SearchResponse DTO๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param dsn ๋ณ€ํ™˜ํ•  DrugSearchDomainNatural ๋„๋ฉ”์ธ ๊ฐ์ฒด + * @return ๋ณ€ํ™˜๋œ SearchResponse DTO + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 + * @since 2025-04-22 + */ + public static SearchResponse toResponse(DrugSearchDomainNatural dsn) { + return SearchResponse.builder() + .drugId(dsn.getDrugId()) + .drugName(dsn.getDrugName()) + .company(dsn.getCompany()) + .efficacy(dsn.getEfficacy()) + .imageUrl(dsn.getImageUrl()) + .build(); + } + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋ฅผ SearchResponse DTO ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugs ๋ณ€ํ™˜ํ•  DrugSearchDomainNatural ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @return ๋ณ€ํ™˜๋œ SearchResponse DTO ๋ฆฌ์ŠคํŠธ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 + * @since 2025-04-22 + */ + public static List toResponseList(List drugs) { + return drugs.stream() + .map(SearchResponseMapper::toResponse) + .toList(); + } + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋ฅผ SearchResponseList DTO๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ์ „์ฒด ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @param drugs ๋ณ€ํ™˜ํ•  DrugSearchDomainNatural ๋„๋ฉ”์ธ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ + * @return ์‘๋‹ต DTO(SearchResponseList) โ€” ๋ฆฌ์ŠคํŠธ์™€ ์ด ๊ฐœ์ˆ˜๋ฅผ ํฌํ•จ + * @author ์ •์•ˆ์‹ + * @modified 2025-05-03 + * @since 2025-04-22 + */ + public static SearchResponseList toResponseListWithCount(List drugs) { + List responses = toResponseList(drugs); + return new SearchResponseList(responses, responses.size()); + } + + + /** + * DrugSearchDomain ํŽ˜์ด์ง€ ๊ฐ์ฒด๋ฅผ SearchResponseList DTO๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + *

+ * 1) ํŽ˜์ด์ง€์—์„œ ์ฝ˜ํ…์ธ  ๋ฆฌ์ŠคํŠธ๋ฅผ ๊บผ๋‚ด๊ณ , + * 2) ๊ฐ๊ฐ์„ DTO๋กœ ๋ณ€ํ™˜ํ•œ ํ›„, + * 3) ์ด ๊ฐœ์ˆ˜์™€ ํ•จ๊ป˜ SearchResponseList๋กœ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @param page Elasticsearch ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ (DrugSearchDomain ๊ธฐ์ค€) + * @return SearchResponseList DTO + * @author ๋ฐ•์ฐฌ๋ณ‘ + * @since 2025-05-03 + */ + public static SearchResponseList toResponseByKeywordDomain(Page page) { + List content = page.getContent().stream() + .map(DrugMapper::toResponse) + .toList(); + + return new SearchResponseList(content, page.getTotalElements()); + } +} diff --git a/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java new file mode 100644 index 0000000..5b3a5aa --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/EmbeddingRouter.java @@ -0,0 +1,50 @@ +package com.likelion.backendplus4.yakplus.switcher.application; + +import com.likelion.backendplus4.yakplus.switcher.application.port.in.EmbeddingRoutingUseCase; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import org.springframework.stereotype.Service; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * Embedding ์–ด๋Œ‘ํ„ฐ๋ฅผ ์ „ํ™˜ํ•˜๊ณ  ์กฐํšŒํ•˜๋Š” ์„œ๋น„์Šค ๊ตฌํ˜„์ฒด + * + * ์ฃผ์–ด์ง„ adapterBeanName์„ ์ด์šฉํ•ด ํ™œ์„ฑํ™”๋œ Embedding adapter๋ฅผ ์ „ํ™˜ํ•˜๊ฑฐ๋‚˜, + * ํ˜„์žฌ ์„ ํƒ๋œ adapter Bean ์ด๋ฆ„์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@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/switcher/application/port/in/EmbeddingRoutingUseCase.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java new file mode 100644 index 0000000..3ac7bb0 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/in/EmbeddingRoutingUseCase.java @@ -0,0 +1,22 @@ +package com.likelion.backendplus4.yakplus.switcher.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/switcher/application/port/out/EmbeddingSwitchPort.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java new file mode 100644 index 0000000..dd99b2a --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/application/port/out/EmbeddingSwitchPort.java @@ -0,0 +1,23 @@ +package com.likelion.backendplus4.yakplus.switcher.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/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java new file mode 100644 index 0000000..fc426c6 --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/infrastructure/route/adapter/EmbeddingRouterAdapter.java @@ -0,0 +1,105 @@ +package com.likelion.backendplus4.yakplus.switcher.infrastructure.route.adapter; + +import com.likelion.backendplus4.yakplus.common.util.log.LogLevel; +import com.likelion.backendplus4.yakplus.search.application.port.out.EmbeddingPort; +import com.likelion.backendplus4.yakplus.switcher.application.port.out.EmbeddingSwitchPort; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import java.util.Map; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * EmbeddingPort ๋ฐ EmbeddingSwitchPort๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ + * ์–ด๋Œ‘ํ„ฐ ๊ฐ„ ์ „ํ™˜๊ณผ ํ˜„์žฌ ์–ด๋Œ‘ํ„ฐ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ + * + * @since 2025-05-02 + */ +@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); + } + + /** + * ํ˜„์žฌ ์„ ํƒ๋œ ์–ด๋Œ‘ํ„ฐ๋กœ๋ถ€ํ„ฐ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @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); + } + + /** + * ์ง€์ •๋œ 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/switcher/presentation/controller/EmbeddingRouterController.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java new file mode 100644 index 0000000..6f8602b --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/EmbeddingRouterController.java @@ -0,0 +1,52 @@ +package com.likelion.backendplus4.yakplus.switcher.presentation.controller; + +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import com.likelion.backendplus4.yakplus.switcher.application.port.in.EmbeddingRoutingUseCase; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.likelion.backendplus4.yakplus.common.util.log.LogUtil.log; + +/** + * Embedding ๋ผ์šฐํŒ… ์–ด๋Œ‘ํ„ฐ๋ฅผ ์ „ํ™˜ํ•˜๊ณ  ์กฐํšŒํ•˜๋Š” REST ์ปจํŠธ๋กค๋Ÿฌ + * + * ์š”์ฒญ์— ๋”ฐ๋ผ ํ™œ์„ฑํ™”๋œ Embedding adapter๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ํ˜„์žฌ adapter๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @since 2025-05-02 + */ +@RestController +@RequestMapping("/switch/embeddings") +public class EmbeddingRouterController { + 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/switcher/presentation/controller/docs/EmbeddingRouterControllerDocs.java b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/docs/EmbeddingRouterControllerDocs.java new file mode 100644 index 0000000..985b86d --- /dev/null +++ b/src/main/java/com/likelion/backendplus4/yakplus/switcher/presentation/controller/docs/EmbeddingRouterControllerDocs.java @@ -0,0 +1,40 @@ +package com.likelion.backendplus4.yakplus.switcher.presentation.controller.docs; + +import com.likelion.backendplus4.yakplus.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +/** + * Embedding ๋ผ์šฐํŒ… ์–ด๋Œ‘ํ„ฐ ์ „ํ™˜/์กฐํšŒ API ๋ฌธ์„œ ์ •์˜ ์ธํ„ฐํŽ˜์ด์Šค + * + * @since 2025-05-02 + */ +@Tag( + name = "Embedding Router", + description = "์–ด๋–ค Embedding adapter๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ์กฐํšŒํ•˜๊ณ  ์ „ํ™˜ํ•˜๋Š” API" +) +public interface EmbeddingRouterControllerDocs { + + @Operation( + summary = "Embedding ์–ด๋Œ‘ํ„ฐ ์ „ํ™˜", + description = "์ง€์ •๋œ adapterBeanName ์œผ๋กœ ํ™œ์„ฑํ™”๋œ Embedding adapter๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> switchAdapter( + @Parameter( + in = ParameterIn.PATH, + description = "์ „ํ™˜ํ•  adapter Bean ์ด๋ฆ„", + example = "KmBertEmbeddingAdapter" + ) + String adapterBeanName + ); + + @Operation( + summary = "ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ Embedding ์–ด๋Œ‘ํ„ฐ ์กฐํšŒ", + description = "ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ Embedding adapter Bean ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค." + ) + ResponseEntity> checkCurrentAdapter(); +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c0bd10d..adb90b4 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: @@ -13,9 +16,32 @@ spring: hibernate: ddl-auto: none # ddl-auto: create - show-sql: true +# show-sql: true properties: hibernate: format_sql: true +#logging: +# level: +# root: DEBUG +gov: + host: apis.data.go.kr + serviceKey: ${GOV_SERVICE_KEY} + numOfRows: 100 + 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 server: - port: 8084 + port: 8084 \ No newline at end of file