Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
57a75dc
config: feign 의존성 추가 (#4)
Jan 18, 2026
e0bc0b5
config: feign 설정 추가 (#4)
Jan 18, 2026
f838508
config: upstage feign 연결 위한 기본 설정 추가 (#4)
Jan 18, 2026
6c67294
feat: upstage feign client 추가 (#4)
Jan 18, 2026
6302dc5
feat: upstage feign 요청 단순화를 위한 upstage client 추가 (#4)
Jan 18, 2026
6851dd1
feat: upstage 기본 요청 처리를 위한 res,req dto 추가 (#4)
Jan 18, 2026
55c9689
feat: 시뮬레이션 상황 Entity 및 Repository 추가 (#4)
Jan 18, 2026
dd29a5c
feat: 시뮬레이션 상황 별 평가 요소 Entity 및 Repository 추가 (#4)
Jan 18, 2026
5a46413
feat: 레벨 별 제목 내보내기를 위한 LevelType Enum 추가 (#4)
Jan 18, 2026
dda7a46
feat: 프롬프트 생성을 위한 builder 와 factory 추가 (#4)
Jan 18, 2026
c74d4b4
feat: 응답 구조 설정을 위해 json schema factory 추가 (#4)
Jan 18, 2026
37295fb
feat: AI 평가 결과 매핑을 위한 dto와 mapper 추가 (#4)
Jan 18, 2026
e91f467
feat: AI 평가 결과지 저장을 위한 Entity 및 Repository 추가 (#4)
Jan 18, 2026
2d3c107
feat: 상황 별 평가 요소 전달을 위한 vo 추가 (#4)
Jan 18, 2026
f67a292
feat: 결과지 생성 API 처리를 위한 res, req dto 추가 (#4)
Jan 18, 2026
db1a357
feat: 결과지 생성 및 조회 로직 추가 (#4)
Jan 18, 2026
85ef08b
feat: 결과지 생성 및 조회 에러 context 설정 추가 (#4)
Jan 18, 2026
fecc423
feat: 결과지 생성 및 조회 api 추가 (#4)
Jan 18, 2026
87387e2
feat: api 문서 확인을 위하 그룹화 설정 (#4)
Jan 18, 2026
02c7f2d
remove: gitkeep 파일 삭제 (#4)
Jan 18, 2026
a7a836e
config: upstage, feign 설정 추가 (#4)
Jan 18, 2026
7329624
docs: 전화 시뮬레이션 결과지 받기 api 문서 설명 변경 (#4)
Jan 21, 2026
6ed6f0e
fix: 변수명 규칙에 맞게 다시 설정 (#4)
Jan 21, 2026
14f3664
config: CORS 허용 origin 환경변수 설정 추가 (#4)
Jan 21, 2026
7bbe532
config: CORS 에러 방지 위한 WebConfig 추가 (#4)
Jan 21, 2026
1514582
Merge pull request #5 from Team-NumberOne/feat/119-script-evaluation-…
yerim123456 Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 36 additions & 27 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public OpenAPI openAPI() {
public GroupedOpenApi readyToUseGroup() {
return GroupedOpenApi.builder()
.group("1) ready to use")
.pathsToMatch("")
.pathsToMatch("/api/v1/bbiyoung/**")
.build();
}

Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/numberone/daepiro/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -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<String> allowedOrigins;

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns(allowedOrigins.toArray(String[]::new))
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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");
};
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -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<EvaluationItemResultResponse> itemScores,
String comment
) {
}
Original file line number Diff line number Diff line change
@@ -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<EvaluationItemInfo> evaluationItems
) {
return PromptBuilder.buildSystemPrompt(
situationTitle,
evaluationItems
);
}

public static String createUserPrompt(
List<QAndARequest> script
) {
return PromptBuilder.buildUserPrompt(script);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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")
)
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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<EvaluationItemInfo> evaluationItems
) {
return buildSystemRole()
+ buildSituation(situationTitle)
+ buildEvaluationCriteria(evaluationItems);
}

public static String buildUserPrompt(List<QAndARequest> 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<EvaluationItemInfo> 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();
}

}
Loading