diff --git a/build.gradle b/build.gradle index b7cbe52..e662301 100644 --- a/build.gradle +++ b/build.gradle @@ -1,50 +1,59 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.3' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '3.3.3' + id 'io.spring.dependency-management' version '1.1.6' } group = 'com.numberone' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } repositories { - mavenCentral() + mavenCentral() +} + +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.4") + } } dependencies { - // ====================================== prod ====================================== - // lombok - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + // ====================================== prod ====================================== + // lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + // db + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation "com.mysql:mysql-connector-j" - // db - implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation "com.mysql:mysql-connector-j" + // web + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' - // web - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-validation' + // open feign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - // swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - // ====================================== test ====================================== - // spring - testImplementation 'org.springframework.boot:spring-boot-starter-test' + // ====================================== test ====================================== + // spring + testImplementation 'org.springframework.boot:spring-boot-starter-test' - // junit - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // junit + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/com/numberone/daepiro/config/SwaggerConfig.java b/src/main/java/com/numberone/daepiro/config/SwaggerConfig.java index 5aa5267..201411f 100644 --- a/src/main/java/com/numberone/daepiro/config/SwaggerConfig.java +++ b/src/main/java/com/numberone/daepiro/config/SwaggerConfig.java @@ -30,7 +30,7 @@ public OpenAPI openAPI() { public GroupedOpenApi readyToUseGroup() { return GroupedOpenApi.builder() .group("1) ready to use") - .pathsToMatch("") + .pathsToMatch("/api/v1/bbiyoung/**") .build(); } diff --git a/src/main/java/com/numberone/daepiro/config/WebConfig.java b/src/main/java/com/numberone/daepiro/config/WebConfig.java new file mode 100644 index 0000000..4c61f0c --- /dev/null +++ b/src/main/java/com/numberone/daepiro/config/WebConfig.java @@ -0,0 +1,25 @@ +package com.numberone.daepiro.config; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +// TODO: Spring Security 넣을 때 방법 변경하기 +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Value("${cors.allowed-origins}") + private List allowedOrigins; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns(allowedOrigins.toArray(String[]::new)) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/numberone/daepiro/config/feign/FeignConfig.java b/src/main/java/com/numberone/daepiro/config/feign/FeignConfig.java new file mode 100644 index 0000000..2578179 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/config/feign/FeignConfig.java @@ -0,0 +1,9 @@ +package com.numberone.daepiro.config.feign; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients(basePackages = "com.numberone.daepiro") +class FeignConfig { +} diff --git a/src/main/java/com/numberone/daepiro/config/feign/UpstageFeignConfig.java b/src/main/java/com/numberone/daepiro/config/feign/UpstageFeignConfig.java new file mode 100644 index 0000000..95eb953 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/config/feign/UpstageFeignConfig.java @@ -0,0 +1,22 @@ +package com.numberone.daepiro.config.feign; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import feign.RequestInterceptor; + +@Configuration +public class UpstageFeignConfig { + + @Value("${upstage.api-key}") + private String apiKey; + + @Bean + public RequestInterceptor authInterceptor() { + return template -> { + template.header("Authorization", "Bearer " + apiKey); + template.header("Content-Type", "application/json"); + }; + } +} diff --git a/src/main/java/com/numberone/daepiro/domain/.gitkeep b/src/main/java/com/numberone/daepiro/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/dto/response/EvaluationResponse.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/dto/response/EvaluationResponse.java new file mode 100644 index 0000000..1fcc714 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/dto/response/EvaluationResponse.java @@ -0,0 +1,17 @@ +package com.numberone.daepiro.domain.bbiyong.ai.dto.response; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.numberone.daepiro.domain.bbiyong.dto.response.EvaluationItemResultResponse; + +public record EvaluationResponse( + + @JsonProperty("total-score") + double totalScore, + + @JsonProperty("item-scores") + List itemScores, + String comment +) { +} \ No newline at end of file diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/factory/EvaluationPromptFactory.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/factory/EvaluationPromptFactory.java new file mode 100644 index 0000000..bc67697 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/factory/EvaluationPromptFactory.java @@ -0,0 +1,29 @@ +package com.numberone.daepiro.domain.bbiyong.ai.factory; + +import java.util.List; + +import com.numberone.daepiro.domain.bbiyong.ai.util.PromptBuilder; +import com.numberone.daepiro.domain.bbiyong.dto.request.QAndARequest; +import com.numberone.daepiro.domain.bbiyong.vo.EvaluationItemInfo; + +public final class EvaluationPromptFactory { + + private EvaluationPromptFactory() { + } + + public static String createSystemPrompt( + String situationTitle, + List evaluationItems + ) { + return PromptBuilder.buildSystemPrompt( + situationTitle, + evaluationItems + ); + } + + public static String createUserPrompt( + List script + ) { + return PromptBuilder.buildUserPrompt(script); + } +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/factory/JsonSchemaFactory.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/factory/JsonSchemaFactory.java new file mode 100644 index 0000000..1d322a8 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/factory/JsonSchemaFactory.java @@ -0,0 +1,39 @@ +package com.numberone.daepiro.domain.bbiyong.ai.factory; + +import java.util.List; +import java.util.Map; + +import com.numberone.daepiro.global.feign.dto.request.UpstageChatRequest; + +public final class JsonSchemaFactory { + private JsonSchemaFactory() { + } + + public static UpstageChatRequest.JsonSchema evaluationScript() { + + Map properties = Map.of( + "total-score", Map.of("type", "number"), + "item-scores", Map.of( + "type", "array", + "items", Map.of( + "type", "object", + "properties", Map.of( + "title", Map.of("type", "string"), + "score", Map.of("type", "number") + ), + "required", List.of("title", "score") + ) + ), + "comment", Map.of("type", "string") + ); + + return new UpstageChatRequest.JsonSchema( + "evaluation_result", + new UpstageChatRequest.Schema( + "object", + properties, + List.of("total-score", "item-scores", "comment") + ) + ); + } +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/mapper/EvaluationMapper.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/mapper/EvaluationMapper.java new file mode 100644 index 0000000..4737dd6 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/mapper/EvaluationMapper.java @@ -0,0 +1,34 @@ +package com.numberone.daepiro.domain.bbiyong.ai.mapper; + +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.numberone.daepiro.domain.bbiyong.ai.dto.response.EvaluationResponse; +import com.numberone.daepiro.global.exception.CustomErrorContext; +import com.numberone.daepiro.global.exception.CustomException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class EvaluationMapper { + + private final ObjectMapper objectMapper; + + public EvaluationResponse fromJson(String content) { + try { + return objectMapper.readValue(content, EvaluationResponse.class); + } catch (JsonProcessingException e) { + log.error( + "[UPSTAGE JSON PARSING ERROR] content={}", + content, + e + ); + throw new CustomException(CustomErrorContext.UNCAUGHT_ERROR); + } + } + +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/util/PromptBuilder.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/util/PromptBuilder.java new file mode 100644 index 0000000..9663f69 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/ai/util/PromptBuilder.java @@ -0,0 +1,116 @@ +package com.numberone.daepiro.domain.bbiyong.ai.util; + +import java.util.List; + +import com.numberone.daepiro.domain.bbiyong.dto.request.QAndARequest; +import com.numberone.daepiro.domain.bbiyong.vo.EvaluationItemInfo; + +public final class PromptBuilder { + private PromptBuilder() { + } + + public static String buildSystemPrompt( + String situationTitle, + List evaluationItems + ) { + return buildSystemRole() + + buildSituation(situationTitle) + + buildEvaluationCriteria(evaluationItems); + } + + public static String buildUserPrompt(List script) { + StringBuilder sb = new StringBuilder(); + + sb.append(""" + User Response: + """ + ); + sb.append("\n\"\"\"\n"); + + for (QAndARequest qandA : script) { + sb.append(qandA.getQuestion()) + .append("\n") + .append(qandA.getAnswer()) + .append("\n\n"); + } + + sb.append("\"\"\""); + + return sb.toString(); + } + + private static String buildSystemRole() { + return """ + You are an AI evaluator responsible for assessing emergency call responses. + + Your role is to objectively evaluate how appropriate and effective the user's response is, + based strictly on the provided situation and evaluation criteria. + + You must follow these rules: + + Evaluation rules: + - Evaluate each evaluation item independently. + - Do not invent new evaluation items. + - Do not omit any evaluation item. + + - Each item score must be a real number between 0.0 and 1.0. + - An item score around 0.7 means the child mostly understood the concept. + - An item score around 0.9 means the child did well. + - An item score of 1.0 means the child did an excellent job. + - Avoid giving item scores below 0.4 unless the item was almost entirely missing. + + - The totalScore must reflect the overall effort positively. + - Minor weaknesses in one or two items should not significantly reduce the totalScore. + - When most item scores are 0.6 or higher, + the totalScore should generally fall in the 70–95 range. + + Comment rules: + - Respond in Korean. + - The final comment must be a single warm, and encouraging sentence written for children. + - The comment should praise what the user did well and gently suggest one simple improvement. + - The tone must be kind, supportive, and reassuring, as if speaking to a child who tried their best. + - The comment must be 40 characters or fewer. + --- + """; + } + + private static String buildSituation(String situationTitle) { + return """ + Situation: + - Title: %s + + This situation assumes that the user is making an emergency report to a dispatcher. + + --- + """.formatted(situationTitle); + } + + private static String buildEvaluationCriteria( + List evaluationItems + ) { + StringBuilder sb = new StringBuilder(""" + Evaluation Criteria: + The following items define what a good response means in this situation. + Each item includes a title and a description explaining how it should be evaluated. + + """); + + int index = 1; + for (EvaluationItemInfo item : evaluationItems) { + sb.append(""" + %d. %s + Description: + %s + + """.formatted( + index++, + item.title(), + item.description() + )); + } + + sb.append("---\n"); + return sb.toString(); + } + +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/api/BbiyoungApiV1.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/api/BbiyoungApiV1.java new file mode 100644 index 0000000..bac5cb3 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/api/BbiyoungApiV1.java @@ -0,0 +1,53 @@ +package com.numberone.daepiro.domain.bbiyong.api; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.numberone.daepiro.domain.bbiyong.dto.request.ScriptEvaluationRequest; +import com.numberone.daepiro.domain.bbiyong.dto.response.ScriptEvaluationResponse; +import com.numberone.daepiro.global.dto.ApiResult; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "bbiyoung", description = "119 삐용 서비스 관련 API") +@RequestMapping("/api/v1/bbiyoung") +public interface BbiyoungApiV1 { + @PostMapping + @Operation(summary = "전화 시뮬레이션 결과지 받기", description = """ + 대화 내용을 body에 담아 전달해주세요. + + 어떤 상황에 대한 대화인지 상황 id 와 함께 전달해주세요 + + - id 값: 화재(1,2) / 의식 소실(3,4) / 부상(5,6) / 익수(7,8) + - ex 멀리 난 화재: 1 + + 응답에는 평가 항목(item scores)별 점수 및 피드백이 포함됩니다. + + ■ 평가 항목 목록 + - WHAT : 무엇이 + - WHERE : 어디에서 + - SELF_INFO : 본인 정보 + - WHO : 누가 + - RISK_FACTOR : 위험 요소 + - ACTION_GUIDE : 행동 지침 + - CONSCIOUSNESS_BREATHING : 의식 및 호흡 확인 + - PATIENT_IDENTIFICATION : 환자 파악 + - EXTERNAL_INJURY : 외상 여부 확인 + - CONSCIOUSNESS_PAIN : 의식 및 통증 확인 + - DANGER_SIGNAL : 위험 징후 확인 + - CONSCIOUSNESS_RESPONSE : 의식 및 반응 확인 + - STATUS_CHECK : 상태 확인 + - SUPPORT_ACTION : 보조 행동 안내 + """) + ApiResult createScriptEvaluation(@RequestBody @Valid ScriptEvaluationRequest request); + + @GetMapping("/{resultId}") + @Operation(summary = "결과지 조회하기", description = " 조회할 결과지 id를 파라미터로 전달해주세요.") + ApiResult getScriptEvaluation(@PathVariable Long resultId); + +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/controller/BbiyoungController.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/controller/BbiyoungController.java new file mode 100644 index 0000000..4a5841f --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/controller/BbiyoungController.java @@ -0,0 +1,28 @@ +package com.numberone.daepiro.domain.bbiyong.controller; + +import org.springframework.web.bind.annotation.RestController; + +import com.numberone.daepiro.domain.bbiyong.api.BbiyoungApiV1; +import com.numberone.daepiro.domain.bbiyong.dto.request.ScriptEvaluationRequest; +import com.numberone.daepiro.domain.bbiyong.dto.response.ScriptEvaluationResponse; +import com.numberone.daepiro.domain.bbiyong.service.BbiyoungService; +import com.numberone.daepiro.global.dto.ApiResult; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class BbiyoungController implements BbiyoungApiV1 { + private final BbiyoungService bbiyoungService; + + public ApiResult createScriptEvaluation(ScriptEvaluationRequest request) { + ScriptEvaluationResponse response = bbiyoungService.createScriptEvaluation(request); + return ApiResult.ok(response); + } + + public ApiResult getScriptEvaluation(Long resultId) { + ScriptEvaluationResponse response = bbiyoungService.getScriptEvaluation(resultId); + return ApiResult.ok(response); + } + +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/request/QAndARequest.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/request/QAndARequest.java new file mode 100644 index 0000000..dfea8c6 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/request/QAndARequest.java @@ -0,0 +1,19 @@ +package com.numberone.daepiro.domain.bbiyong.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class QAndARequest { + @NotNull(message = "시스템 질문은 null일 수 없습니다.") + @Schema(defaultValue = "그 위치가 어디인가요?") + private String question; + + @NotNull(message = "사용자 답변 null일 수 없습니다.") + @Schema(defaultValue = "강남역 근처입니다.") + private String answer; +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/request/ScriptEvaluationRequest.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/request/ScriptEvaluationRequest.java new file mode 100644 index 0000000..74d6caa --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/request/ScriptEvaluationRequest.java @@ -0,0 +1,31 @@ +package com.numberone.daepiro.domain.bbiyong.dto.request; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ScriptEvaluationRequest { + @NotNull(message = "ip는 null일 수 없습니다.") + @Schema(defaultValue = "192.168.1.1") + private String ip; + + @NotNull(message = "세부 상황 Id는 null일 수 없습니다.") + @Schema(defaultValue = "1") + private Long id; + + @NotEmpty(message = "script는 비어 있을 수 없습니다.") + @Size(min = 5, message = "script는 최소 5개 이상이어야 합니다.") + @Valid + @ArraySchema(minItems = 5) + private List script; +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/response/EvaluationItemResultResponse.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/response/EvaluationItemResultResponse.java new file mode 100644 index 0000000..50f7b3e --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/response/EvaluationItemResultResponse.java @@ -0,0 +1,11 @@ +package com.numberone.daepiro.domain.bbiyong.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record EvaluationItemResultResponse( + @Schema(description = "평가 요소 이름", example = "who") + String title, + @Schema(description = "평가 점수", example = "0.3") + double score +) { +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/response/ScriptEvaluationResponse.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/response/ScriptEvaluationResponse.java new file mode 100644 index 0000000..0672486 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/dto/response/ScriptEvaluationResponse.java @@ -0,0 +1,19 @@ +package com.numberone.daepiro.domain.bbiyong.dto.response; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record ScriptEvaluationResponse( + @Schema(description = "결과지 ID", example = "1") + long resultId, + @Schema(description = "결과지 레벨 이름", example = "불 지킴이") + String title, + @Schema(description = "최종 점수", example = "80.0") + double totalScore, + @Schema(description = "각 평가 요소 별 점수") + List itemScores, + @Schema(description = "한 줄 코멘트", example = "설명을 잘했어요!~~") + String comment +) { +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationItem.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationItem.java new file mode 100644 index 0000000..0c87f21 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationItem.java @@ -0,0 +1,31 @@ +package com.numberone.daepiro.domain.bbiyong.entity; + +import com.numberone.daepiro.domain.baseentity.PrimaryKeyEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EvaluationItem extends PrimaryKeyEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "situation_id", nullable = false) + private Situation situation; + + @Column(nullable = false) + private String title; + + @Column(nullable = false, length = 1000) + private String description; + + @Column(nullable = false) + private Integer displayOrder; +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationItemResult.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationItemResult.java new file mode 100644 index 0000000..79241b3 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationItemResult.java @@ -0,0 +1,39 @@ +package com.numberone.daepiro.domain.bbiyong.entity; + +import com.numberone.daepiro.domain.baseentity.PrimaryKeyEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EvaluationItemResult extends PrimaryKeyEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "evaluation_result_id", nullable = false) + private EvaluationResult evaluationResult; + + @Column(nullable = false, length = 30) + private String title; + + @Column(nullable = false) + private double score; + + public static EvaluationItemResult of(String title, double score) { + EvaluationItemResult result = new EvaluationItemResult(); + result.title = title; + result.score = score; + return result; + } + + public void assignTo(EvaluationResult evaluationResult) { + this.evaluationResult = evaluationResult; + } +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationResult.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationResult.java new file mode 100644 index 0000000..cce6005 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/EvaluationResult.java @@ -0,0 +1,71 @@ +package com.numberone.daepiro.domain.bbiyong.entity; + +import java.util.ArrayList; +import java.util.List; + +import com.numberone.daepiro.domain.baseentity.PrimaryKeyEntity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EvaluationResult extends PrimaryKeyEntity { + + private String ip; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "situation_id") + private Situation situation; + + @Column(nullable = false, length = 50) + private String title; + + @Column(nullable = false, length = 300) + private String comment; + + @Column(nullable = false) + private double totalScore; + + @OneToMany( + mappedBy = "evaluationResult", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private List itemScores = new ArrayList<>(); + + public static EvaluationResult of( + Situation situation, + String ip, + String title, + String comment, + double totalScore, + List itemScores + ) { + EvaluationResult result = new EvaluationResult(); + result.situation = situation; + result.ip = ip; + result.title = title; + result.comment = comment; + result.totalScore = totalScore; + + itemScores.forEach(result::addItemResult); + + return result; + } + + public void addItemResult(EvaluationItemResult itemScore) { + itemScores.add(itemScore); + itemScore.assignTo(this); + } + +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/Situation.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/Situation.java new file mode 100644 index 0000000..59714c2 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/entity/Situation.java @@ -0,0 +1,35 @@ +package com.numberone.daepiro.domain.bbiyong.entity; + +import java.util.ArrayList; +import java.util.List; + +import com.numberone.daepiro.domain.baseentity.PrimaryKeyEntity; +import com.numberone.daepiro.domain.bbiyong.enums.SituationType; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Situation extends PrimaryKeyEntity { + + @Column(nullable = false) + private String title; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SituationType type; + + @OneToMany(mappedBy = "situation", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("displayOrder ASC") + private List evaluationItems = new ArrayList<>(); +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/enums/LevelType.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/enums/LevelType.java new file mode 100644 index 0000000..edc068d --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/enums/LevelType.java @@ -0,0 +1,53 @@ +package com.numberone.daepiro.domain.bbiyong.enums; + +import java.util.Arrays; + +import com.numberone.daepiro.global.exception.CustomErrorContext; +import com.numberone.daepiro.global.exception.CustomException; + +import lombok.Getter; + +@Getter +public enum LevelType { + + CHALLENGER(0.0, 60.0, "신고 도전자"), + EXPLORER(61.0, 70.0, "신고 탐험가"), + EXPERT(71.0, 80.0, "안전 전문가"), + BRAVE(81.0, 90.0, "용감한 신고자"), + + FIRE_GUARDIAN(91.0, 100.0, "불 지킴이", SituationType.FIRE), + BREATH_GUARDIAN(91.0, 100.0, "숨 지킴이", SituationType.UNCONSCIOUS), + SAFETY_GUARDIAN(91.0, 100.0, "안전 지킴이", SituationType.INJURY), + WATER_GUARDIAN(91.0, 100.0, "물 지킴이", SituationType.DROWNING); + + private final double minScore; + private final double maxScore; + private final String title; + private final SituationType type; + + LevelType(double min, double max, String title) { + this(min, max, title, null); + } + + LevelType(double min, double max, String title, SituationType type) { + this.minScore = min; + this.maxScore = max; + this.title = title; + this.type = type; + } + + public static LevelType from(double score, SituationType type) { + return Arrays.stream(values()) + .filter(v -> v.matches(score, type)) + .findFirst() + .orElseThrow(() -> new CustomException(CustomErrorContext.INVALID_VALUE)); + } + + private boolean matches(double score, SituationType type) { + if (score < minScore || score > maxScore) + return false; + if (this.type == null) + return true; + return this.type == type; + } +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/enums/SituationType.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/enums/SituationType.java new file mode 100644 index 0000000..3331ef0 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/enums/SituationType.java @@ -0,0 +1,26 @@ +package com.numberone.daepiro.domain.bbiyong.enums; + +import java.util.Arrays; +import java.util.Optional; + +import lombok.Getter; + +@Getter +public enum SituationType { + FIRE("화재"), + UNCONSCIOUS("의식 소실/심정지"), + INJURY("부상"), + DROWNING("익수"); + + private final String description; + + SituationType(String description) { + this.description = description; + } + + public static Optional of(String description) { + return Arrays.stream(values()) + .filter(v -> v.getDescription().equals(description)) + .findFirst(); + } +} \ No newline at end of file diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationItemRepository.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationItemRepository.java new file mode 100644 index 0000000..1aeecde --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationItemRepository.java @@ -0,0 +1,10 @@ +package com.numberone.daepiro.domain.bbiyong.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.numberone.daepiro.domain.bbiyong.entity.EvaluationItem; + +@Repository +public interface EvaluationItemRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationItemResultRepository.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationItemResultRepository.java new file mode 100644 index 0000000..530d6be --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationItemResultRepository.java @@ -0,0 +1,10 @@ +package com.numberone.daepiro.domain.bbiyong.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.numberone.daepiro.domain.bbiyong.entity.EvaluationItemResult; + +@Repository +public interface EvaluationItemResultRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationResultRepository.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationResultRepository.java new file mode 100644 index 0000000..5c28533 --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/EvaluationResultRepository.java @@ -0,0 +1,10 @@ +package com.numberone.daepiro.domain.bbiyong.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.numberone.daepiro.domain.bbiyong.entity.EvaluationResult; + +@Repository +public interface EvaluationResultRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/SituationRepository.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/SituationRepository.java new file mode 100644 index 0000000..5f4b4ac --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/repository/SituationRepository.java @@ -0,0 +1,10 @@ +package com.numberone.daepiro.domain.bbiyong.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.numberone.daepiro.domain.bbiyong.entity.Situation; + +@Repository +public interface SituationRepository extends JpaRepository { +} diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/service/BbiyoungService.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/service/BbiyoungService.java new file mode 100644 index 0000000..8e24b0e --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/service/BbiyoungService.java @@ -0,0 +1,139 @@ +package com.numberone.daepiro.domain.bbiyong.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.numberone.daepiro.domain.bbiyong.ai.dto.response.EvaluationResponse; +import com.numberone.daepiro.domain.bbiyong.ai.factory.EvaluationPromptFactory; +import com.numberone.daepiro.domain.bbiyong.ai.factory.JsonSchemaFactory; +import com.numberone.daepiro.domain.bbiyong.ai.mapper.EvaluationMapper; +import com.numberone.daepiro.domain.bbiyong.dto.request.ScriptEvaluationRequest; +import com.numberone.daepiro.domain.bbiyong.dto.response.EvaluationItemResultResponse; +import com.numberone.daepiro.domain.bbiyong.dto.response.ScriptEvaluationResponse; +import com.numberone.daepiro.domain.bbiyong.entity.EvaluationItemResult; +import com.numberone.daepiro.domain.bbiyong.entity.EvaluationResult; +import com.numberone.daepiro.domain.bbiyong.entity.Situation; +import com.numberone.daepiro.domain.bbiyong.enums.LevelType; +import com.numberone.daepiro.domain.bbiyong.repository.EvaluationResultRepository; +import com.numberone.daepiro.domain.bbiyong.repository.SituationRepository; +import com.numberone.daepiro.domain.bbiyong.vo.EvaluationItemInfo; +import com.numberone.daepiro.global.exception.CustomErrorContext; +import com.numberone.daepiro.global.exception.CustomException; +import com.numberone.daepiro.global.feign.client.UpstageClient; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Slf4j +public class BbiyoungService { + private final SituationRepository situationRepository; + private final EvaluationResultRepository evaluationResultRepository; + private final UpstageClient upstageClient; + private final EvaluationMapper evaluationMapper; + + @Transactional + public ScriptEvaluationResponse createScriptEvaluation(ScriptEvaluationRequest request) { + + // 프롬프트 생성을 위한 상황 제목 및 상황 별 평가항목 준비 + Situation situation = findSituation(request.getId()); + List evaluationItems = extractEvaluationItems(situation); + + // 프롬프트 생성 + String systemPrompt = + EvaluationPromptFactory.createSystemPrompt( + situation.getTitle(), + evaluationItems + ); + String userPrompt = + EvaluationPromptFactory.createUserPrompt(request.getScript()); + + // upstage 요청 + String rawAiResponse = + upstageClient.requestChatCompletion(systemPrompt, userPrompt, JsonSchemaFactory.evaluationScript()); + + // 응답 json 매핑 + EvaluationResponse aiResult = evaluationMapper.fromJson(rawAiResponse); + + // db 저장 + EvaluationResult evaluationResult = saveEvaluationResult(request, situation, + aiResult); + + // id 값과 함께 반환 + return buildResultResponse(evaluationResult); + } + + public ScriptEvaluationResponse getScriptEvaluation(Long resultId) { + EvaluationResult evaluationResult = evaluationResultRepository.findById(resultId) + .orElseThrow(() -> new CustomException(CustomErrorContext.NOT_FOUND_SCRIPT_RESULT)); + + return buildResultResponse(evaluationResult); + } + + private Situation findSituation(Long situationId) { + return situationRepository.findById(situationId) + .orElseThrow(() -> new CustomException(CustomErrorContext.INVALID_VALUE)); + } + + private List extractEvaluationItems(Situation situation) { + return situation.getEvaluationItems().stream() + .map(item -> new EvaluationItemInfo( + item.getTitle(), + item.getDescription() + )) + .toList(); + } + + private EvaluationResult saveEvaluationResult(ScriptEvaluationRequest request, Situation situation, + EvaluationResponse aiResult) { + return evaluationResultRepository.save( + EvaluationResult.of( + situation, + request.getIp(), + LevelType.from(aiResult.totalScore(), situation.getType()).getTitle(), + aiResult.comment(), + aiResult.totalScore(), + mapDtoToItemResults(aiResult.itemScores()) + ) + ); + } + + private List mapDtoToItemResults( + List responses + ) { + return responses.stream() + .map(r -> EvaluationItemResult.of( + r.title(), + r.score() + )) + .toList(); + } + + private ScriptEvaluationResponse buildResultResponse( + EvaluationResult evaluationResult + ) { + return new ScriptEvaluationResponse( + evaluationResult.getId(), + evaluationResult.getTitle(), + evaluationResult.getTotalScore(), + mapItemResultsToDto(evaluationResult.getItemScores()), + evaluationResult.getComment() + ); + } + + private List mapItemResultsToDto( + List itemResults + ) { + return itemResults.stream() + .map(item -> new EvaluationItemResultResponse( + item.getTitle(), + item.getScore() + )) + .toList(); + } +} + diff --git a/src/main/java/com/numberone/daepiro/domain/bbiyong/vo/EvaluationItemInfo.java b/src/main/java/com/numberone/daepiro/domain/bbiyong/vo/EvaluationItemInfo.java new file mode 100644 index 0000000..27fb0db --- /dev/null +++ b/src/main/java/com/numberone/daepiro/domain/bbiyong/vo/EvaluationItemInfo.java @@ -0,0 +1,7 @@ +package com.numberone.daepiro.domain.bbiyong.vo; + +public record EvaluationItemInfo( + String title, + String description +) { +} diff --git a/src/main/java/com/numberone/daepiro/global/exception/CustomErrorContext.java b/src/main/java/com/numberone/daepiro/global/exception/CustomErrorContext.java index 5dac5fa..e182601 100644 --- a/src/main/java/com/numberone/daepiro/global/exception/CustomErrorContext.java +++ b/src/main/java/com/numberone/daepiro/global/exception/CustomErrorContext.java @@ -7,6 +7,9 @@ @Getter public enum CustomErrorContext { + //30xx: WEB 삐용 관련 오류 + NOT_FOUND_SCRIPT_RESULT(HttpStatus.NOT_FOUND, 3000, "스크립트 결과지를 찾을 수 없습니다."), + // 99xx: 공통 오류 INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST, 9001, "JSON 형식이 잘못되었습니다."), INVALID_VALUE(HttpStatus.INTERNAL_SERVER_ERROR, 9002, "유효하지 않은 값이 발견되었습니다."), diff --git a/src/main/java/com/numberone/daepiro/global/feign/UpstageFeign.java b/src/main/java/com/numberone/daepiro/global/feign/UpstageFeign.java new file mode 100644 index 0000000..f3523fa --- /dev/null +++ b/src/main/java/com/numberone/daepiro/global/feign/UpstageFeign.java @@ -0,0 +1,22 @@ +package com.numberone.daepiro.global.feign; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import com.numberone.daepiro.config.feign.UpstageFeignConfig; +import com.numberone.daepiro.global.feign.dto.request.UpstageChatRequest; +import com.numberone.daepiro.global.feign.dto.response.UpstageChatResponse; + +@FeignClient( + name = "upstageFeign", + url = "${upstage.base-url}", + configuration = UpstageFeignConfig.class +) +public interface UpstageFeign { + + @PostMapping("/chat/completions") + UpstageChatResponse chatCompletion( + @RequestBody UpstageChatRequest request + ); +} diff --git a/src/main/java/com/numberone/daepiro/global/feign/client/UpstageClient.java b/src/main/java/com/numberone/daepiro/global/feign/client/UpstageClient.java new file mode 100644 index 0000000..38e522e --- /dev/null +++ b/src/main/java/com/numberone/daepiro/global/feign/client/UpstageClient.java @@ -0,0 +1,73 @@ +package com.numberone.daepiro.global.feign.client; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.numberone.daepiro.global.exception.CustomErrorContext; +import com.numberone.daepiro.global.exception.CustomException; +import com.numberone.daepiro.global.feign.UpstageFeign; +import com.numberone.daepiro.global.feign.dto.request.UpstageChatRequest; +import com.numberone.daepiro.global.feign.dto.response.UpstageChatResponse; + +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class UpstageClient { + @Value("${upstage.model}") + private String model; + + private final UpstageFeign upstageFeign; + + public String requestChatCompletion( + String systemPrompt, + String userPrompt, + UpstageChatRequest.JsonSchema jsonSchema + ) { + UpstageChatRequest request = + buildRequest(systemPrompt, userPrompt, jsonSchema); + + try { + UpstageChatResponse response = + upstageFeign.chatCompletion(request); + + return response.getChoices() + .getFirst() + .getMessage() + .getContent(); + + } catch (FeignException e) { + log.error( + "[UPSTAGE CONNECTION ERROR] status={}, body={}", + e.status(), + e.contentUTF8() + ); + + throw new CustomException(CustomErrorContext.UNCAUGHT_ERROR); + } + } + + private UpstageChatRequest buildRequest( + String systemPrompt, + String userPrompt, + UpstageChatRequest.JsonSchema jsonSchema + ) { + return new UpstageChatRequest( + model, + List.of( + new UpstageChatRequest.Message("system", systemPrompt), + new UpstageChatRequest.Message("user", userPrompt) + ), + false, + new UpstageChatRequest.ResponseFormat( + "json_schema", + jsonSchema + ) + ); + } +} diff --git a/src/main/java/com/numberone/daepiro/global/feign/dto/request/UpstageChatRequest.java b/src/main/java/com/numberone/daepiro/global/feign/dto/request/UpstageChatRequest.java new file mode 100644 index 0000000..d4c3b9a --- /dev/null +++ b/src/main/java/com/numberone/daepiro/global/feign/dto/request/UpstageChatRequest.java @@ -0,0 +1,53 @@ +package com.numberone.daepiro.global.feign.dto.request; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class UpstageChatRequest { + private String model; + private List messages; + private Boolean stream; + private ResponseFormat response_format; + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Message { + private String role; + private String content; + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class ResponseFormat { + private String type; + @JsonProperty("json_schema") + private JsonSchema jsonSchema; + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class JsonSchema { + private String name; + private Schema schema; + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Schema { + private String type; + private java.util.Map properties; + private java.util.List required; + } +} \ No newline at end of file diff --git a/src/main/java/com/numberone/daepiro/global/feign/dto/response/UpstageChatResponse.java b/src/main/java/com/numberone/daepiro/global/feign/dto/response/UpstageChatResponse.java new file mode 100644 index 0000000..68dae2d --- /dev/null +++ b/src/main/java/com/numberone/daepiro/global/feign/dto/response/UpstageChatResponse.java @@ -0,0 +1,41 @@ +package com.numberone.daepiro.global.feign.dto.response; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UpstageChatResponse { + private String id; + private String object; + private Long created; + private String model; + private List choices; + private Usage usage; + + @Getter + @NoArgsConstructor + public static class Choice { + private int index; + private Message message; + private String finish_reason; + + @Getter + @NoArgsConstructor + public static class Message { + private String role; + private String content; + } + } + + @Getter + @NoArgsConstructor + public static class Usage { + private int prompt_tokens; + private int completion_tokens; + private int total_tokens; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f2e4cb4..b18aacd 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,6 +3,14 @@ spring: hibernate: ddl-auto: update show-sql: false + cloud: + openfeign: + client: + config: + upstageFeign: + connect-timeout: 3000 + read-timeout: 20000 + logger-level: basic datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -13,3 +21,8 @@ spring: springdoc: swagger-ui: enabled: true + +upstage: + base-url: https://api.upstage.ai/v1 + model: solar-pro2 + api-key: ${UPSTAGE_API_KEY} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 725dd29..2b320d3 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -3,6 +3,15 @@ spring: hibernate: ddl-auto: update show-sql: true + cloud: + openfeign: + client: + config: + upstageFeign: + connect-timeout: 1000 + read-timeout: 10000 + logger-level: full + datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -13,3 +22,9 @@ spring: springdoc: swagger-ui: enabled: true + + +upstage: + base-url: https://api.upstage.ai/v1 + model: solar-pro2 + api-key: ${UPSTAGE_API_KEY} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index d83689b..effe3dc 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -3,6 +3,14 @@ spring: hibernate: ddl-auto: none show-sql: false + cloud: + openfeign: + client: + config: + upstageFeign: + connect-timeout: 2000 + read-timeout: 15000 + logger-level: none datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -13,3 +21,8 @@ spring: springdoc: swagger-ui: enabled: false + +upstage: + base-url: https://api.upstage.ai/v1 + model: solar-pro2 + api-key: ${UPSTAGE_API_KEY} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b007f00..ca1e293 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,7 +6,12 @@ spring: hibernate: format_sql: true defer-datasource-initialization: true - + cloud: + openfeign: + client: + config: + default: + logger-level: basic springdoc: api-docs: path: /api-docs # API 문서의 기본 경로 설정 @@ -14,4 +19,6 @@ springdoc: path: /swagger-ui.html # Swagger UI 경로 설정 operationsSorter: method # 메서드 정렬 (get, post 순) +cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:}