Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 1 addition & 1 deletion back/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ out/
.vscode/

### Environment Variables ###
.env
../.env

# 테스트 이미지 경로 (LocalStorageServiceTest)
test-uploads/
Expand Down
9 changes: 9 additions & 0 deletions back/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
implementation("org.testcontainers:testcontainers")
implementation("org.testcontainers:jdbc")
implementation("org.testcontainers:postgresql")

// Swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
Expand All @@ -63,6 +69,7 @@ dependencies {
// Database
runtimeOnly("com.h2database:h2")
runtimeOnly("org.postgresql:postgresql")
implementation("org.postgresql:postgresql")

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


// AWS SDK for S3
implementation("software.amazon.awssdk:s3:2.20.+")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import org.springframework.stereotype.Component;

@Component
@Profile({"local","dev"})
@Profile({"local","dev","prod"})
@ConditionalOnProperty(name = "dvcs.backfill.enabled", havingValue = "true", matchIfMissing = false)
@RequiredArgsConstructor
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -163,6 +164,39 @@ protected Long createScenarioInTransaction(
// lastDecision 처리 (필요 시)
if (lastDecision != null) {
decisionFlowService.createDecisionNodeNext(lastDecision);
List<DecisionNode> ordered = decisionNodeRepository.findByDecisionLine_IdOrderByAgeYearAscIdAsc(decisionLine.getId());
DecisionNode parent = ordered.isEmpty() ? null : ordered.get(ordered.size() - 1);

// 베이스 라인의 tail BaseNode 해석(“결말” 우선, 없으면 최대 age)
BaseLine baseLine = decisionLine.getBaseLine();
List<BaseNode> baseNodes = baseLine.getBaseNodes();
BaseNode tailBase = baseNodes.stream()
.filter(b -> {
String s = b.getSituation() == null ? "" : b.getSituation();
String d = b.getDecision() == null ? "" : b.getDecision();
return s.contains("결말") || d.contains("결말");
})
.max(Comparator.comparingInt(BaseNode::getAgeYear).thenComparingLong(BaseNode::getId))
.orElseGet(() -> baseNodes.stream()
.max(Comparator.comparingInt(BaseNode::getAgeYear).thenComparingLong(BaseNode::getId))
.orElseThrow(() -> new ApiException(ErrorCode.INVALID_INPUT_VALUE, "tail base not found"))
);

// 엔티티 빌더로 ‘결말’ 결정노드 저장(테일과 동일 age)
DecisionNode ending = DecisionNode.builder()
.user(decisionLine.getUser())
.nodeKind(NodeType.DECISION)
.decisionLine(decisionLine)
.baseNode(tailBase)
.parent(parent)
.category(tailBase.getCategory())
.situation("결말")
.decision("결말")
.ageYear(tailBase.getAgeYear())
.background(tailBase.getSituation() == null ? "" : tailBase.getSituation())
.build();

decisionNodeRepository.save(ending);
}

// DecisionLine 완료 처리
Expand Down
39 changes: 39 additions & 0 deletions back/src/main/java/com/back/domain/search/entity/NodeSnippet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 이 파일은 RAG 검색용 스니펫 엔티티를 정의한다.
* 라인/나이/카테고리/텍스트/임베딩을 저장하며 pgvector 컬럼을 float[]로 매핑한다.
*/
package com.back.domain.search.entity;

import com.back.infra.pgvector.PgVectorConverter;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@Entity
@Table(name = "node_snippet")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
public class NodeSnippet {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "line_id", nullable = false)
private Long lineId;

@Column(name = "age_year", nullable = false)
private Integer ageYear;

private String category;

@Column(name = "text", nullable = false, columnDefinition = "text")
private String text;

@JdbcTypeCode(SqlTypes.OTHER)
@Convert(converter = PgVectorConverter.class)
@Column(name = "embedding", nullable = false, columnDefinition = "vector(768)")
private float[] embedding;


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 이 파일은 라인/나이 윈도우로 후보를 좁힌 뒤 pgvector 유사도로 정렬해 topK를 반환하는 네이티브 쿼리를 제공한다.
*/
package com.back.domain.search.repository;

import com.back.domain.search.entity.NodeSnippet;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface NodeSnippetRepository extends JpaRepository<NodeSnippet, Long> {

// 라인/나이 윈도우 필터 + pgvector 유사도(<=>) 정렬로 상위 K를 조회한다.
@Query(value = """
SELECT * FROM node_snippet
WHERE line_id = :lineId
AND age_year BETWEEN :minAge AND :maxAge
ORDER BY embedding <=> CAST(:q AS vector)
LIMIT :k
""", nativeQuery = true)
List<NodeSnippet> searchTopKByLineAndAgeWindow(
@Param("lineId") Long lineId,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge,
@Param("q") String vectorLiteral,
@Param("k") int k
);

// 텍스트만(가볍게) 가져오기 — 네트워크·파싱 비용 급감
@Query(value = """
SELECT text FROM node_snippet
WHERE line_id = :lineId
AND age_year BETWEEN :minAge AND :maxAge
ORDER BY embedding <=> CAST(:q AS vector)
LIMIT :k
""", nativeQuery = true)
List<String> searchTopKTextByLineAndAgeWindow(
@Param("lineId") Long lineId,
@Param("minAge") Integer minAge,
@Param("maxAge") Integer maxAge,
@Param("q") String vectorLiteral,
@Param("k") int k
);
}
Loading