Skip to content

Commit c4dbe6c

Browse files
authored
Merge pull request #136 from prgrms-web-devcourse-final-project/node/12
[FIX]: AI에러 수정 및 속도 최적화(정확도 떨어짐)
2 parents d0bf078 + b618291 commit c4dbe6c

28 files changed

+1606
-86
lines changed

back/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ out/
3737
.vscode/
3838

3939
### Environment Variables ###
40-
.env
40+
../.env
4141

4242
# 테스트 이미지 경로 (LocalStorageServiceTest)
4343
test-uploads/

back/build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ dependencies {
4747
testImplementation("org.springframework.boot:spring-boot-starter-test")
4848
testImplementation("org.springframework.security:spring-security-test")
4949
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
50+
testImplementation("org.testcontainers:junit-jupiter")
51+
testImplementation("org.testcontainers:postgresql")
52+
testImplementation("org.springframework.boot:spring-boot-testcontainers")
53+
implementation("org.testcontainers:testcontainers")
54+
implementation("org.testcontainers:jdbc")
55+
implementation("org.testcontainers:postgresql")
5056

5157
// Swagger
5258
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
@@ -63,6 +69,7 @@ dependencies {
6369
// Database
6470
runtimeOnly("com.h2database:h2")
6571
runtimeOnly("org.postgresql:postgresql")
72+
implementation("org.postgresql:postgresql")
6673

6774
// Migration
6875
implementation("org.flywaydb:flyway-core:11.11.2")
@@ -78,6 +85,8 @@ dependencies {
7885
// AI Services - WebFlux for non-blocking HTTP clients
7986
implementation("org.springframework.boot:spring-boot-starter-webflux")
8087
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
88+
implementation("io.netty:netty-tcnative-boringssl-static:2.0.65.Final")
89+
8190

8291
// AWS SDK for S3
8392
implementation("software.amazon.awssdk:s3:2.20.+")

back/src/main/java/com/back/domain/scenario/service/ScenarioService.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.transaction.annotation.Transactional;
3131

3232
import java.time.LocalDateTime;
33+
import java.util.Comparator;
3334
import java.util.List;
3435
import java.util.Map;
3536
import java.util.Optional;
@@ -163,6 +164,39 @@ protected Long createScenarioInTransaction(
163164
// lastDecision 처리 (필요 시)
164165
if (lastDecision != null) {
165166
decisionFlowService.createDecisionNodeNext(lastDecision);
167+
List<DecisionNode> ordered = decisionNodeRepository.findByDecisionLine_IdOrderByAgeYearAscIdAsc(decisionLine.getId());
168+
DecisionNode parent = ordered.isEmpty() ? null : ordered.get(ordered.size() - 1);
169+
170+
// 베이스 라인의 tail BaseNode 해석(“결말” 우선, 없으면 최대 age)
171+
BaseLine baseLine = decisionLine.getBaseLine();
172+
List<BaseNode> baseNodes = baseLine.getBaseNodes();
173+
BaseNode tailBase = baseNodes.stream()
174+
.filter(b -> {
175+
String s = b.getSituation() == null ? "" : b.getSituation();
176+
String d = b.getDecision() == null ? "" : b.getDecision();
177+
return s.contains("결말") || d.contains("결말");
178+
})
179+
.max(Comparator.comparingInt(BaseNode::getAgeYear).thenComparingLong(BaseNode::getId))
180+
.orElseGet(() -> baseNodes.stream()
181+
.max(Comparator.comparingInt(BaseNode::getAgeYear).thenComparingLong(BaseNode::getId))
182+
.orElseThrow(() -> new ApiException(ErrorCode.INVALID_INPUT_VALUE, "tail base not found"))
183+
);
184+
185+
// 엔티티 빌더로 ‘결말’ 결정노드 저장(테일과 동일 age)
186+
DecisionNode ending = DecisionNode.builder()
187+
.user(decisionLine.getUser())
188+
.nodeKind(NodeType.DECISION)
189+
.decisionLine(decisionLine)
190+
.baseNode(tailBase)
191+
.parent(parent)
192+
.category(tailBase.getCategory())
193+
.situation("결말")
194+
.decision("결말")
195+
.ageYear(tailBase.getAgeYear())
196+
.background(tailBase.getSituation() == null ? "" : tailBase.getSituation())
197+
.build();
198+
199+
decisionNodeRepository.save(ending);
166200
}
167201

168202
// DecisionLine 완료 처리
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* 이 파일은 RAG 검색용 스니펫 엔티티를 정의한다.
3+
* 라인/나이/카테고리/텍스트/임베딩을 저장하며 pgvector 컬럼을 float[]로 매핑한다.
4+
*/
5+
package com.back.domain.search.entity;
6+
7+
import com.back.infra.pgvector.PgVectorConverter;
8+
import jakarta.persistence.*;
9+
import lombok.*;
10+
import org.hibernate.annotations.JdbcTypeCode;
11+
import org.hibernate.type.SqlTypes;
12+
13+
@Entity
14+
@Table(name = "node_snippet")
15+
@Getter @Setter
16+
@NoArgsConstructor @AllArgsConstructor @Builder
17+
public class NodeSnippet {
18+
19+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
20+
private Long id;
21+
22+
@Column(name = "line_id", nullable = false)
23+
private Long lineId;
24+
25+
@Column(name = "age_year", nullable = false)
26+
private Integer ageYear;
27+
28+
private String category;
29+
30+
@Column(name = "text", nullable = false, columnDefinition = "text")
31+
private String text;
32+
33+
@JdbcTypeCode(SqlTypes.OTHER)
34+
@Convert(converter = PgVectorConverter.class)
35+
@Column(name = "embedding", nullable = false, columnDefinition = "vector(768)")
36+
private float[] embedding;
37+
38+
39+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* 이 파일은 라인/나이 윈도우로 후보를 좁힌 뒤 pgvector 유사도로 정렬해 topK를 반환하는 네이티브 쿼리를 제공한다.
3+
*/
4+
package com.back.domain.search.repository;
5+
6+
import com.back.domain.search.entity.NodeSnippet;
7+
import org.springframework.data.jpa.repository.*;
8+
import org.springframework.data.repository.query.Param;
9+
10+
import java.util.List;
11+
12+
public interface NodeSnippetRepository extends JpaRepository<NodeSnippet, Long> {
13+
14+
// 라인/나이 윈도우 필터 + pgvector 유사도(<=>) 정렬로 상위 K를 조회한다.
15+
@Query(value = """
16+
SELECT * FROM node_snippet
17+
WHERE line_id = :lineId
18+
AND age_year BETWEEN :minAge AND :maxAge
19+
ORDER BY embedding <=> CAST(:q AS vector)
20+
LIMIT :k
21+
""", nativeQuery = true)
22+
List<NodeSnippet> searchTopKByLineAndAgeWindow(
23+
@Param("lineId") Long lineId,
24+
@Param("minAge") Integer minAge,
25+
@Param("maxAge") Integer maxAge,
26+
@Param("q") String vectorLiteral,
27+
@Param("k") int k
28+
);
29+
30+
// 텍스트만(가볍게) 가져오기 — 네트워크·파싱 비용 급감
31+
@Query(value = """
32+
SELECT text FROM node_snippet
33+
WHERE line_id = :lineId
34+
AND age_year BETWEEN :minAge AND :maxAge
35+
ORDER BY embedding <=> CAST(:q AS vector)
36+
LIMIT :k
37+
""", nativeQuery = true)
38+
List<String> searchTopKTextByLineAndAgeWindow(
39+
@Param("lineId") Long lineId,
40+
@Param("minAge") Integer minAge,
41+
@Param("maxAge") Integer maxAge,
42+
@Param("q") String vectorLiteral,
43+
@Param("k") int k
44+
);
45+
}

0 commit comments

Comments
 (0)