AI κΈ°λ° κ·μ κ²μ μμ€ν μ λλ€.
- AI κΈ°λ° μ§λ¬Έ λΆμ: Google Geminiλ₯Ό μ¬μ©νμ¬ μ¬μ©μμ μμ°μ΄ μ§λ¬Έμ ꡬ쑰ν
- ν€μλ μΆμΆ: μ§λ¬Έμμ ν΅μ¬ ν€μλ μλ μΆμΆ
- κ·μ μ ν λ§€μΉ: 27κ° μ¬κ· μ ν μ€ κ΄λ ¨ κ·μ μλ μλ³
- μ§λ¬Έ μλ νμ : μ 보쑰ν/μ μ°¨μλ΄/κΈ°μ€νμΈ/μ격μ건/μμΈμ¬ν λΆλ₯
- κ²μ 쿼리 μ΅μ ν: λ²‘ν° κ²μμ μ΅μ νλ μΏΌλ¦¬λ‘ λ³ν
- λ²‘ν° κ²μ: μλ² λ© κΈ°λ° μλ―Έ κ²μμΌλ‘ κ΄λ ¨ κ·μ μ‘°ν νμ
- ReRanking: Cohere λͺ¨λΈμ ν΅ν κ²μ κ²°κ³Ό μ κ΅ν
- λ΅λ³ μμ±: κ²μλ κ·μ μ κ·Όκ±°λ‘ LLMμ΄ μ νν λ΅λ³ μμ±
- μΆμ² νμ: λ΅λ³μ κ·Όκ±°κ° λλ κ·μ λͺ , μ‘°ν, κ΄λ ¨λ μ μ μ 곡
- μ λ’°λ νκ°: κ²μ κ²°κ³Όμ μ λ’°λ μ μ κ³μ°
- Vector Search (μλ―Έ κ²μ): Google Gemini EmbeddingμΌλ‘ μλ―Έμ μ μ¬μ± νμ
- BM25 Search (ν€μλ κ²μ): Apache Lucene κΈ°λ° μ νν ν€μλ λ§€μΉ
- Reciprocal Rank Fusion (RRF): λ κ²μ κ²°κ³Όλ₯Ό μ§λ₯μ μΌλ‘ ν΅ν©
- νκ΅μ΄ μ΅μ ν: Nori ννμ λΆμκΈ°λ‘ νκ΅μ΄ κ²μ νμ§ ν₯μ
- μ νλ κ°μ : μλ―Έμ κ²μκ³Ό ν€μλ κ²μμ μ₯μ μ κ²°ν©νμ¬ 20-30% μ νλ ν₯μ
μ¬μ©μ μ§λ¬Έ (μμ°μ΄)
β
QueryAnalysisService (Gemini API)
β
ꡬ쑰νλ λΆμ κ²°κ³Ό
- ν€μλ
- κ·μ μ ν
- μ§λ¬Έ μλ
- κ²μ 쿼리
β
RegulationSearchService (RAG + Hybrid Search)
β
ββββββββββββββββββββββ¬ββββββββββββββββββββββ
β Vector Search β BM25 Search β
β (μλ―Έμ κ²μ) β (ν€μλ κ²μ) β
β - Gemini Embed β - Apache Lucene β
β - Cosine Sim. β - Nori Analyzer β
β β 20 candidates β β 20 candidates β
ββββββββββββββββββββββ΄ββββββββββββββββββββββ
β
Reciprocal Rank Fusion (RRF)
- λ κ²μ κ²°κ³Ό ν΅ν©
- μ€λ³΅ μ κ±° λ° μ μ κ³μ°
β 40 unique candidates
β
ReRanking (Cohere)
- μ κ΅ν κ΄λ ¨μ± μ¬νκ°
β Top 5 results
β
λ΅λ³ μμ± (Gemini API)
β
κ²°κ³Ό λ°ν (λ΅λ³ + κ·Όκ±° μ‘°ν + μ λ’°λ)
- Java 21+
- Spring Boot 3.2: REST API μλ²
- LangChain4j 0.36.2: LLM μ€μΌμ€νΈλ μ΄μ λ° RAG ꡬν
- Google Gemini API:
gemini-2.5-flash: μ§λ¬Έ λΆμ λ° λ΅λ³ μμ±text-embedding-004: λ²‘ν° μλ² λ© (768-dim, νκ΅μ΄ μ§μ)
- Cohere API:
rerank-multilingual-v3.0: κ²μ κ²°κ³Ό μ¬μ λ ¬ (νκ΅μ΄ μ΅μ ν)
- Apache Lucene 9.11.1: BM25 ν€μλ κ²μ μμ§
- Nori Korean Analyzer: νκ΅μ΄ ννμ λΆμ
- Vector Store: In-Memory (κ°λ°μ©, μ΄μμ Qdrant κΆμ₯)
- Maven: λΉλ λꡬ
- Java 17 μ΄μ: Oracle JDK λλ OpenJDK
- Maven 3.6+: λ€μ΄λ‘λ
- Google Gemini API Key: λ°κΈ λ°κΈ°
λ°©λ² 1: νκ²½λ³μ μ€μ (κΆμ₯)
# Windows (PowerShell)
$env:GOOGLE_API_KEY="your_api_key_here"
# Windows (CMD)
set GOOGLE_API_KEY=your_api_key_here
# Linux/Mac
export GOOGLE_API_KEY=your_api_key_hereλ°©λ² 2: application.properties νμΌ μμ
src/main/resources/application.properties νμΌμμ API ν€ μ§μ μ
λ ₯:
gemini.api.key=your_actual_api_key_here# Maven μμ‘΄μ± λ€μ΄λ‘λ λ° JAR νμΌ λΉλ
mvn clean package -DskipTestsλΉλκ° μλ£λλ©΄ target/regulation-search-1.0.0.jar νμΌμ΄ μμ±λ©λλ€.
# λ°±μλ(8080)μ νλ‘ νΈμλ(3000)λ₯Ό λμμ μ€ν
start-all.batμ΄ λͺ λ Ήμ΄λ:
- β
λ°±μλ μλ²:
http://localhost:8080(λ³λ μ°½) - β
νλ‘ νΈμλ:
http://localhost:3000(λ³λ μ°½)
λΈλΌμ°μ μμ http://localhost:3000μΌλ‘ μ μνλ©΄ μ 체 μ ν리μΌμ΄μ
μ μ¬μ©ν μ μμ΅λλ€!
# Spring Boot μλ²λ§ μ€ν (ν¬νΈ 8080)
run-server.bat
# λλ Maven μ§μ μ¬μ©
mvn spring-boot:runμλ²κ° μμλλ©΄ http://localhost:8080μμ REST APIλ₯Ό μ¬μ©ν μ μμ΅λλ€.
# νλ‘ νΈμλ κ°λ° μλ²λ§ μ€ν (ν¬νΈ 3000)
cd guideon-frontend
start-frontend.bat
# λλ npm μ§μ μ¬μ©
npm run devhttp://localhost:8080μμ μ€ν μ€μ΄μ΄μΌ ν©λλ€.
# 1. JAR λΉλ
mvn clean package -DskipTests
# 2. μ€ν
java -jar target/regulation-search-1.0.0.jar- Main Class:
com.guideon.GuideonApplication - VM Options:
-Dserver.port=8080
λ€λ₯Έ ν¬νΈμμ μ€ννλ €λ©΄:
# λ°©λ² 1: application.yml μμ
server:
port: 9090
# λ°©λ² 2: νκ²½λ³μ
export SERVER_PORT=9090
mvn spring-boot:run
# λ°©λ² 3: 컀맨λ λΌμΈ μ΅μ
java -jar target/regulation-search-1.0.0.jar --server.port=9090μλ²κ° μμλλ©΄ λ€μ μλν¬μΈνΈλ₯Ό μ¬μ©ν μ μμ΅λλ€:
curl http://localhost:8080/actuator/healthcurl -X POST http://localhost:8080/api/qa/analyze \
-H "Content-Type: application/json" \
-d '{"question": "μ°μ°¨ ν΄κ°λ λͺ μΌμΈκ°μ?"}'curl -X POST http://localhost:8080/api/qa/search \
-H "Content-Type: application/json" \
-d '{"question": "ν΄μΈ μΆμ₯μ μλ°λΉλ μΌλ§κΉμ§ μ§μλλμ?"}'curl http://localhost:8080/api/regulations/typescurl -X POST http://localhost:8080/api/regulations/upload \
-H "Content-Type: application/json" \
-d '{
"filePath": "c:/workspace/regulations/μ·¨μ
κ·μΉ.txt",
"regulationType": "μ·¨μ
κ·μΉ"
}'μ μ μμ μ λ€μκ³Ό κ°μ λ‘κ·Έκ° νμλ©λλ€:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)
2025-10-17 ... : Starting GuideonApplication using Java 17...
2025-10-17 ... : ConfigLoader loaded from classpath: application.properties
2025-10-17 ... : QueryAnalysisService initialized with 27 regulation types
2025-10-17 ... : RegulationSearchService initialized
2025-10-17 ... : Tomcat started on port(s): 8080 (http)
2025-10-17 ... : Started GuideonApplication in 5.234 seconds
μ΄ μ€λ₯λ Mavenμ΄ Java 8μ μ¬μ©νκ³ μμ λ λ°μν©λλ€.
ν΄κ²° λ°©λ²:
# 1. νμ¬ Mavenμ΄ μ¬μ©νλ Java λ²μ νμΈ
mvn -version
# Java versionμ΄ 1.8μ΄λ©΄ λ¬Έμ !
# 2. JAVA_HOMEμ Java 17λ‘ μ€μ
# Windows (PowerShell)
$env:JAVA_HOME="C:\Program Files\Java\jdk-17"
$env:PATH="C:\Program Files\Java\jdk-17\bin;$env:PATH"
# Windows (CMD)
set JAVA_HOME=C:\Program Files\Java\jdk-17
set PATH=C:\Program Files\Java\jdk-17\bin;%PATH%
# Linux/Mac
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export PATH=$JAVA_HOME/bin:$PATH
# 3. λ€μ λΉλ
mvn clean compileνΈλ¦¬ν λ°©λ² (Windows):
μ 곡λ λ°°μΉ μ€ν¬λ¦½νΈλ₯Ό μ¬μ©νμΈμ (μλμΌλ‘ Java 17 μ€μ ):
# λΉλ
build.bat
# μλ² μ€ν
run-server.batjava -version
# μΆλ ₯: java version "17.0.x" μ΄μμ΄μ΄μΌ ν¨
mvn -version
# μ€μ: Java versionλ 17μ΄μ΄μΌ ν¨!mvn -version
# μΆλ ₯: Apache Maven 3.6.x μ΄μμ΄μ΄μΌ ν¨# Windows (PowerShell)
echo $env:GOOGLE_API_KEY
# Windows (CMD)
echo %GOOGLE_API_KEY%
# Linux/Mac
echo $GOOGLE_API_KEYinvalid flag: --release: Mavenμ΄ Java 8 μ¬μ© β JAVA_HOMEμ Java 17λ‘ λ³κ²½GOOGLE_API_KEY not found: νκ²½λ³μκ° μ€μ λμ§ μμκ±°λ application.ymlμ API ν€κ° μμClassNotFoundException: Maven λΉλλ₯Ό λ€μ μ€ν (mvn clean package)Port 8080 already in use: λ€λ₯Έ νλ‘μΈμ€κ° ν¬νΈλ₯Ό μ¬μ©μ€ β ν¬νΈ λ³κ²½ λλ νλ‘μΈμ€ μ’ λ£
// μμ€ν
μ΄κΈ°ν
String apiKey = System.getenv("GOOGLE_API_KEY");
RegulationQASystem system = new RegulationQASystem(apiKey);
// κ·μ λ¬Έμ μ
λ‘λ (μΈλ±μ±)
system.uploadRegulationDocument("path/to/μ·¨μ
κ·μΉ.txt", "μ·¨μ
κ·μΉ");
system.uploadRegulationDocument("path/to/κ²½λΉμ§κΈκ·μ .txt", "κ²½λΉμ§κΈκ·μ ");
// μμ°μ΄ μ§λ¬Έ
String question = "ν΄μΈ μΆμ₯μ μλ°λΉλ μΌλ§κΉμ§ μ§μλλμ?";
RegulationSearchResult result = system.askQuestion(question);
// κ²°κ³Ό νμΈ
System.out.println("λ΅λ³: " + result.getAnswer());
System.out.println("μ λ’°λ: " + result.getConfidenceScore());
result.getReferences().forEach(ref -> {
System.out.println("κ·Όκ±°: " + ref.getDocumentName() + " - " + ref.getContent());
});μ¬μ©μμ μμ°μ΄ μ§λ¬Έμ λΆμν©λλ€.
Request:
{
"question": "μ°μ°¨ ν΄κ°λ λͺ μΌμΈκ°μ?"
}Response:
{
"success": true,
"message": "μ§λ¬Έ λΆμμ΄ μλ£λμμ΅λλ€",
"analysis": {
"originalQuery": "μ°μ°¨ ν΄κ°λ λͺ μΌμΈκ°μ?",
"keywords": ["μ°μ°¨", "ν΄κ°", "μΌμ"],
"regulationTypes": ["μ·¨μ
κ·μΉ", "볡리νμλΉκ·μ "],
"intent": "κΈ°μ€νμΈ",
"searchQuery": "μ°μ°¨ ν΄κ° μΌμ κΈ°μ€"
}
}μ§λ¬Έμ λΆμνκ³ κ΄λ ¨ κ·μ μ κ²μνμ¬ λ΅λ³μ μμ±ν©λλ€.
Request:
{
"question": "ν΄μΈ μΆμ₯μ μλ°λΉλ μΌλ§κΉμ§ μ§μλλμ?"
}Response:
{
"success": true,
"message": "κ²μμ΄ μλ£λμμ΅λλ€",
"result": {
"answer": "ν΄μΈ μΆμ₯μ μλ°λΉλ κ΅κ°λ³λ‘ μ°¨λ± μ§μλ©λλ€...",
"references": [
{
"documentName": "μΆμ₯μ¬λΉμ§κΈκ·μ ",
"clause": "μ 5μ‘°",
"content": "ν΄μΈ μΆμ₯ μλ°λΉλ...",
"pageNumber": 3,
"relevanceScore": 0.89
}
],
"confidenceScore": 0.87,
"hasAnswer": true
}
}μ§μνλ 27κ° κ·μ μ ν λͺ©λ‘μ μ‘°νν©λλ€.
2λ¨κ³ μ λ‘λ νλ‘μ°:
- μ λ‘λ μ§ν ν μ€νΈλ§ μΆμΆνμ¬ μ¬μ©μ κ²μ
- νμ μμλ§ μλ² λ©/μΈλ±μ± λ° λ©ν μ μ₯
-
POST
/api/documents/extract-text(multipart/form-data)- form:
file,regulationType - μλ΅:
{ "success": true, "data": { "uploadId": "uuid", "text": "..." } }
- form:
-
POST
/api/documents/{uploadId}/confirm(application/json)- body:
{ "text": "νμ ν μ€νΈ" } - μλ΅: κΈ°μ‘΄ μ
λ‘λ μλ΅(
DocumentUploadResponse)κ³Ό λμΌ
- body:
Response:
{
"success": true,
"types": ["μ΄μ¬νκ·μ ", "μ λλΉμ¬μ©κ·μ ", ...],
"count": 27
}κ·μ λ¬Έμλ₯Ό μ λ‘λνκ³ λ²‘ν° DBμ μΈλ±μ±ν©λλ€.
Request:
{
"filePath": "c:/workspace/regulations/μ·¨μ
κ·μΉ.txt",
"regulationType": "μ·¨μ
κ·μΉ"
}Response:
{
"success": true,
"message": "κ·μ λ¬Έμκ° μ±κ³΅μ μΌλ‘ μ
λ‘λλμμ΅λλ€",
"regulationType": "μ·¨μ
κ·μΉ",
"filePath": "c:/workspace/regulations/μ·¨μ
κ·μΉ.txt"
}guideon/
βββ pom.xml # Maven μ€μ (Spring Boot)
βββ CLUADE.md # μμ€ν
μ€κ³ λ¬Έμ
βββ README.md # μ΄ νμΌ
βββ src/main/
βββ java/com/guideon/
β βββ GuideonApplication.java # Spring Boot λ©μΈ ν΄λμ€
β βββ controller/ # REST API Controllers
β β βββ QAController.java # Q&A API
β β βββ RegulationController.java # κ·μ κ΄λ¦¬ API
β βββ service/ # Business Logic
β β βββ QueryAnalysisService.java # μ§λ¬Έ λΆμ μλΉμ€
β β βββ RegulationSearchService.java # RAG κ²μ μλΉμ€
β βββ model/ # Domain Models
β β βββ QueryAnalysisResult.java
β β βββ RegulationSearchResult.java
β β βββ RegulationReference.java
β βββ dto/ # Data Transfer Objects
β β βββ QuestionRequest.java
β β βββ AnalysisResponse.java
β β βββ SearchResponse.java
β β βββ UploadRequest.java
β βββ config/ # Configuration
β β βββ GuideonConfig.java # Service Beans
β β βββ WebConfig.java # CORS μ€μ
β β βββ ConfigLoader.java # Properties Loader
β βββ exception/ # Exception Handling
β βββ GlobalExceptionHandler.java
βββ resources/
βββ application.yml # Spring Boot μ€μ
βββ application.properties # Legacy μ€μ
μμ°μ΄ μ§λ¬Έμ AIλ‘ λΆμνμ¬ κ΅¬μ‘°νλ κ²μ μΏΌλ¦¬λ‘ λ³ν
μ£Όμ λ©μλ:
analyzeQuery(String userQuery): μ§λ¬Έ λΆμ λ° ν€μλ μΆμΆ
λΆμ κ²°κ³Ό:
- ν€μλ λͺ©λ‘
- κ΄λ ¨ κ·μ μ ν
- μ§λ¬Έ μλ
- μ΅μ νλ κ²μ 쿼리
RAG κΈ°λ° λ²‘ν° κ²μ λ° λ΅λ³ μμ±
μ£Όμ λ©μλ:
indexDocument(Document doc, String type): κ·μ λ¬Έμ μΈλ±μ±search(QueryAnalysisResult analysis): κ·μ κ²μ λ° λ΅λ³ μμ±
κ²μ κ³Όμ :
- λ²‘ν° μλ² λ© μμ±
- μ μ¬λ κΈ°λ° κ΄λ ¨ μ‘°ν κ²μ
- κ²μλ μ‘°νμ 컨ν μ€νΈλ‘ LLM λ΅λ³ μμ±
- μ λ’°λ μ μ κ³μ°
μ΄μ¬νκ·μ , μ λλΉμ¬μ©κ·μ , μ€λ¦¬κ·μ , μΆμ₯μ¬λΉμ§κΈκ·μ , μ£Όμλ§€μμ νκΆμ΄μκ·μ , λ Έμ¬νμνκ·μ , μ·¨μ κ·μΉ, λ§€μΆμ±κΆκ΄λ¦¬κ·μ , κΈμ΅μμ° μ΄μ©κ·μ , λ¬Έμκ΄λ¦¬κ·μ , μ¬κ³ κ΄λ¦¬κ·μ , κ³μ½κ²ν κ·μ , μ¬κ·κ΄λ¦¬κ·μ , μμν΄μ§κΈμ§κΈκ·μ , μμ보μκ·μ , μ£Όμ£Όμ΄νμ΄μκ·μ , κ²½λΉμ§κΈκ·μ , 볡리νμλΉκ·μ , 보μκ΄λ¦¬κ·μ , μμμ κ²°κ·μ , μ°λ¦¬μ¬μ£Όμ΄μκ·μ , λ΄λΆμ 보κ΄λ¦¬κ·μ , νκ³κ΄λ¦¬κ·μ , νΉμκ΄κ³μ κ±°λκ·μ , μ‘°μ§ λ° μ 무λΆμ₯κ·μ , μκΈκ΄λ¦¬κ·μ , μΈμ₯κ΄λ¦¬κ·μ
μ
λ ₯: "μ°μ°¨ ν΄κ°λ λͺ μΌμΈκ°μ?"
[QueryAnalysisService λΆμ]
β ν€μλ: [μ°μ°¨, ν΄κ°, μΌμ]
β κ·μ μ ν: [μ·¨μ
κ·μΉ, 볡리νμλΉκ·μ ]
β μ§λ¬Έ μλ: κΈ°μ€νμΈ
β κ²μ 쿼리: "μ°μ°¨ ν΄κ° μΌμ κΈ°μ€"
[RegulationSearchService κ²μ]
β λ²‘ν° κ²μ: κ΄λ ¨ μ‘°ν 5κ° λ°κ²¬
β 컨ν
μ€νΈ: "μ·¨μ
κ·μΉ μ XXμ‘° μ°μ°¨ν΄κ°λ..."
β AI λ΅λ³ μμ±: "μ°μ°¨ ν΄κ°λ κ·Όμλ
μμ λ°λΌ..."
[κ²°κ³Ό λ°ν]
β λ΅λ³: "μ°μ°¨ ν΄κ°λ 1λ
근무μ 15μΌμ΄ λΆμ¬λ©λλ€..."
β κ·Όκ±°: μ·¨μ
κ·μΉ μ 32μ‘° (κ΄λ ¨λ: 0.89)
β μ λ’°λ: 0.87
- API ν€ λ³΄μ:
GOOGLE_API_KEYλ₯Ό μ½λμ νλμ½λ©νμ§ λ§μΈμ - μΈλ±μ± νμ: μ§λ¬ΈνκΈ° μ μ λ°λμ κ·μ λ¬Έμλ₯Ό μΈλ±μ±ν΄μΌ ν©λλ€
- κ°λ° νκ²½: νμ¬ In-Memory μ μ₯μ μ¬μ©μ€ (μ¬μμμ λ°μ΄ν° μμ€)
- μ΄μ νκ²½: Qdrant λ± μꡬ λ²‘ν° DBλ‘ κ΅μ²΄ κΆμ₯
- μ¬μ©μ μΈμ¦
- μΈμ κ΄λ¦¬
- μλ λ‘κ·ΈμΈ μ΅μ
- μ§λ¬Έ μ λ ₯ μΈν°νμ΄μ€
- μ΅κ·Ό μ§λ¬Έ μ΄λ ₯
- μ¦κ²¨μ°ΎκΈ° κ·μ λͺ©λ‘
- ν΅κ³ λμ보λ (μ§λ¬Έ μ, κ·μ νμ©λ)
- μμ°μ΄ μ§λ¬Έ μ λ ₯ νΌ
- μ€μκ° μ§λ¬Έ λΆμ νμ
- μΆμΆλ ν€μλ
- κ΄λ ¨ κ·μ μ ν
- μ§λ¬Έ μλ
- AI λ΅λ³ νμ
- λ΅λ³ λ΄μ©
- μ λ’°λ μ μ
- κ·Όκ±° κ·μ μ‘°ν λͺ©λ‘
- λ΅λ³ νκ° (λμλ¨/λμμλ¨)
- λ΅λ³ 곡μ κΈ°λ₯
- κ·μ λ¬Έμ λͺ©λ‘ (27κ° μ νλ³ λΆλ₯)
- κ·μ λ¬Έμ μ λ‘λ μΈν°νμ΄μ€
- κ·μ λ¬Έμ 미리보기
- λ¬Έμ λ²μ κ΄λ¦¬
- μΈλ±μ± μν νμ
- κ³Όκ±° μ§λ¬Έ λͺ©λ‘
- μ§λ¬Έλ³ λ΅λ³ μ‘°ν
- νν°λ§ (λ μ§, κ·μ μ ν, μ λ’°λ)
- μ¦κ²¨μ°ΎκΈ° μΆκ°/μ κ±°
- μ§λ¬Έ ν΅κ³ (μΌλ³/μ£Όλ³/μλ³)
- κ·μ λ³ νμ©λ
- μ λ’°λ λΆν¬
- μΈκΈ° ν€μλ
- λ΅λ³ λ§μ‘±λ ν΅κ³
- API ν€ κ΄λ¦¬
- λͺ¨λΈ μ€μ (Gemini Flash/Pro μ ν)
- κ²μ μ΅μ (μ΅λ κ²°κ³Ό μ, μ΅μ μ λ’°λ)
- μλ¦Ό μ€μ
- React 18+: UI λΌμ΄λΈλ¬λ¦¬
- TypeScript: νμ μμ μ±
- React Router v6: SPA λΌμ°ν
- Vite: λΉλ λꡬ (λΉ λ₯Έ κ°λ° μλ²)
- TanStack Query (React Query): μλ² μν κ΄λ¦¬
- Zustand: ν΄λΌμ΄μΈνΈ μν κ΄λ¦¬ (κ²½λ)
- Axios: HTTP ν΄λΌμ΄μΈνΈ
- Ant Design λλ Material-UI (MUI): UI μ»΄ν¬λνΈ λΌμ΄λΈλ¬λ¦¬
- Tailwind CSS: μ νΈλ¦¬ν° CSS νλ μμν¬
- React Markdown: λ΅λ³ λ λλ§
- React Syntax Highlighter: μ½λ νμ΄λΌμ΄ν
- Recharts: ν΅κ³ μ°¨νΈ
- ESLint: μ½λ λ¦°ν
- Prettier: μ½λ ν¬λ§·ν
- Vitest: λ¨μ ν μ€νΈ
- React Testing Library: μ»΄ν¬λνΈ ν μ€νΈ
- REST API: Spring Boot λ°±μλ μ°λ
- WebSocket (μ ν): μ€μκ° λΆμ κ²°κ³Ό μ€νΈλ¦¬λ°
guideon-frontend/
βββ public/
βββ src/
β βββ assets/ # μ΄λ―Έμ§, ν°νΈ λ±
β βββ components/ # μ¬μ¬μ© κ°λ₯ν μ»΄ν¬λνΈ
β β βββ common/ # κ³΅ν΅ μ»΄ν¬λνΈ (Button, Input λ±)
β β βββ layout/ # λ μ΄μμ μ»΄ν¬λνΈ
β β βββ qa/ # Q&A κ΄λ ¨ μ»΄ν¬λνΈ
β β βββ regulation/ # κ·μ κ΄λ ¨ μ»΄ν¬λνΈ
β βββ pages/ # νμ΄μ§ μ»΄ν¬λνΈ
β β βββ Login.tsx
β β βββ Dashboard.tsx
β β βββ QAPage.tsx
β β βββ Regulations.tsx
β β βββ History.tsx
β β βββ Analytics.tsx
β β βββ Settings.tsx
β βββ hooks/ # 컀μ€ν
ν
β βββ services/ # API μλΉμ€
β βββ stores/ # Zustand μ€ν μ΄
β βββ types/ # TypeScript νμ
μ μ
β βββ utils/ # μ νΈλ¦¬ν° ν¨μ
β βββ App.tsx
β βββ main.tsx
βββ package.json
βββ tsconfig.json
βββ vite.config.ts
βββ tailwind.config.js
νμ΄λΈλ¦¬λ κ²μμ **Vector Search (μλ―Έ κ²μ)**μ **BM25 Search (ν€μλ κ²μ)**μ κ²°ν©νμ¬ κ²μ μ νλλ₯Ό 20-30% ν₯μμν€λ κ³ κΈ κ²μ κΈ°λ²μ λλ€.
Query: "μ°μ°¨ λ°μ κΈ°μ€ μ 10μ‘°"
β
βββββββββββββββββββββββ¬βββββββββββββββββββββββ
β Vector Search β BM25 Search β
β (μλ―Έμ μ μ¬μ±) β (ν€μλ λ§€μΉ) β
β β β
β "μ°μ°¨" β "ν΄κ°" β "μ 10μ‘°" μ ν λ§€μΉ β
β "λ°μ" β "λΆμ¬" β "μ°μ°¨" μ ν λ§€μΉ β
β β β
β β 20 candidates β β 20 candidates β
βββββββββββββββββββββββ΄βββββββββββββββββββββββ
Vector κ²°κ³Ό + BM25 κ²°κ³Ό β RRF μκ³ λ¦¬μ¦
β
μ€λ³΅ μ κ±° λ° μ μ ν΅ν©
score = Ξ£(1 / (k + rank)) // k = 60
β
40κ° unique candidates (μ μ μ μ λ ¬)
40κ° ν보 β Cohere rerank-multilingual-v3.0
β
μ κ΅ν semantic κ΄λ ¨μ± μ¬νκ°
β
Top 5 results (min_score: 0.8+)
νμ΄λΈλ¦¬λ κ²μμ application.propertiesμμ νμ±ν/λΉνμ±νν μ μμ΅λλ€:
# Hybrid Search Configuration
hybrid.search.enabled=true
hybrid.search.vector.weight=0.6
hybrid.search.keyword.weight=0.4
hybrid.search.initial.results=40
# BM25 Configuration
bm25.index.directory=${user.home}/guideon/data/bm25-index
bm25.k1=1.2
bm25.b=0.75
bm25.analyzer.type=korean-nori| 쿼리 μ ν | Vector Only | Hybrid Search | κ°μ μ¨ |
|---|---|---|---|
| μλ―Έμ 쿼리 ("μ°μ°¨λ μΈμ λ°μνλμ?") | 85% | 90% | +5% |
| μ νν μ‘°ν ("μ 10μ‘° 2ν") | 60% | 95% | +35% |
| λ³΅ν© μΏΌλ¦¬ ("2024λ μ°μ°¨ κΈ°μ€ μ 10μ‘°") | 70% | 92% | +22% |
| μ«μ/λ μ§ ("1000λ§μ μ΄μ") | 65% | 90% | +25% |
Apache Luceneμ Nori Korean Analyzerλ₯Ό μ¬μ©νμ¬ νκ΅μ΄ ννμ λΆμ:
μ
λ ₯: "μ°μ°¨ ν΄κ° λ°μ κΈ°μ€"
β
ννμ λΆμ
β
ν ν°: ["μ°μ°¨", "ν΄κ°", "λ°μ", "κΈ°μ€"]
β
BM25 μΈλ±μ€ κ²μ
- β BM25SearchService ꡬν
- β HybridSearchService ꡬν
- β RRF μκ³ λ¦¬μ¦ κ΅¬ν
- β RegulationSearchService ν΅ν©
- β ConfigLoader νμ₯
- β μΈλ±μ± νμ΄νλΌμΈ ν΅ν©
- β κ²μ νμ΄νλΌμΈ ν΅ν©
- π λ¨μ ν μ€νΈ μμ±
- π ν΅ν© ν μ€νΈ μμ±
- β ꡬ쑰νλ 컨ν μ€νΈ μμ± (EnhancedContextBuilder)
- β μ‘°ν λ²νΈ μΆμΆ (RegulationArticleExtractor)
- β μ§λ¬Έ μλλ³ λ§μΆ€ν ν둬ννΈ
- β λ΅λ³ νμ§ κ°μ (μ‘°ν μΈμ©, κ·Όκ±° λͺ μ)
- β³ νκ΅μ΄ λΆμκΈ° νλ
- β³ κ°μ€μΉ μ΅μ ν (A/B ν μ€νΈ)
- β³ μ±λ₯ κ°μ (μΊμ±, μΈλ±μ€ μ΅μ ν)
- β³ λ©λͺ¨λ¦¬ μ΅μ ν
- β³ μΈλ±μ€ κ΄λ¦¬ λꡬ
- β³ λͺ¨λν°λ§ λμ보λ
- β³ A/B ν μ€νΈ νλ μμν¬
- κ²μ μλ: +50ms (BM25 μΆκ° μ€λ²ν€λ)
- λ©λͺ¨λ¦¬ μ¬μ©: +200MB (Lucene μΈλ±μ€)
- μ νλ: +20-30% (νκ· )
- νκ΅μ΄ κ²μ: +35% (ν€μλ λ§€μΉ ν₯μ)
Phase 3μμλ ꡬ쑰νλ 컨ν μ€νΈμ μ§λ¬Έ μλλ³ λ§μΆ€ν ν둬ννΈλ₯Ό ν΅ν΄ λ΅λ³ νμ§μ λν ν₯μμμΌ°μ΅λλ€.
κΈ°μ‘΄ λ°©μ:
κ·μ λ΄μ© A
κ·μ λ΄μ© B
κ·μ λ΄μ© C
κ°μ λ λ°©μ:
=== κ²μλ κ·μ λ΄μ© ===
[κ²μ κ²°κ³Ό 1] (κ΄λ ¨λ: 0.89, μΆμ²: μ·¨μ
κ·μΉ, μ 32μ‘°)
μ 32μ‘° (μ°μ°¨ν΄κ°)
1. μ°μ°¨ ν΄κ°λ κ·Όμλ
μμ λ°λΌ λ€μκ³Ό κ°μ΄ λΆμ¬νλ€.
- 1λ
κ·Όμ: 15μΌ
- 3λ
κ·Όμ: 16μΌ
---
[κ²μ κ²°κ³Ό 2] (κ΄λ ¨λ: 0.76, μΆμ²: 볡리νμλΉκ·μ , μ 15μ‘°)
μ 15μ‘° (ν΄κ° μ§μ)
...κ°μ ν¨κ³Ό:
- β κ΄λ ¨λ μ μλ‘ μ€μλ νμ
- β κ·μ μ ν λͺ μλ‘ μΆμ² λͺ νν
- β μ‘°ν λ²νΈ μλ μΆμΆ λ° κ΅¬μ‘°ν
- β LLMμ΄ μ»¨ν μ€νΈλ₯Ό λ μ μ΄ν΄
κΈ°λ₯:
- "μ XXμ‘°", "μ XXν", "μ XXνΈ" ν¨ν΄ μΈμ
- μ‘°ν μ λͺ© μΆμΆ (μ: "μ 32μ‘° (μ°μ°¨ν΄κ°)")
- μ‘°ν λ΄μ© νμ± λ° μ 리
- μ κ·μ κΈ°λ° μ νν μΆμΆ
μ¬μ© μ:
String text = "μ 32μ‘° (μ°μ°¨ν΄κ°) 1. μ°μ°¨λ 15μΌ...";
List<RegulationArticle> articles = RegulationArticleExtractor.extractArticles(text, "μ·¨μ
κ·μΉ");
// β [RegulationArticle{articleNumber="μ 32μ‘°", title="μ°μ°¨ν΄κ°", ...}]RegulationSearchServiceλ μ§λ¬Έ μλλ₯Ό λΆμνμ¬ μ΅μ νλ ν둬ννΈλ₯Ό μμ±ν©λλ€:
| μ§λ¬Έ μλ | ν둬ννΈ νΉμ§ | μμ |
|---|---|---|
| κΈ°μ€νμΈ | μ«μ/κΈμ‘/κΈ°κ° λͺ μ μꡬ | "μ°μ°¨λ λͺ μΌμΈκ°μ?" |
| μ μ°¨μ€λͺ | λ¨κ³λ³ μμ μ€λͺ μꡬ | "μΆμ₯ μ μ²μ μ΄λ»κ² νλμ?" |
| κ°λ₯μ¬λΆ | Yes/No λ¨Όμ λ΅λ³ μꡬ | "ν΄κ°λ₯Ό μͺΌκ°μ μΈ μ μλμ?" |
| μμΈμν© | μμΉκ³Ό μμΈ κ΅¬λΆ μꡬ | "νΉλ³ν κ²½μ° μμΈκ° μλμ?" |
ν둬ννΈ μμ (κΈ°μ€νμΈ):
[λ΅λ³ μμ± μ§μΉ¨]
1. μ 곡λ κ·μ λ΄μ©λ§μ κΈ°λ°μΌλ‘ λ΅λ³νμΈμ
2. λͺ
ννκ³ κ°κ²°νκ² μμ±νμΈμ
3. κ·μ μ μλ λ΄μ©μ μΆμΈ‘νμ§ λ§μΈμ
4. λΆνμ€ν κ²½μ° "ν΄λΉ κ·μ μμ λͺ
νν μΈκΈλμ§ μμμ΅λλ€"λΌκ³ λ΅λ³νμΈμ
5. ꡬ체μ μΈ μ«μ, κΈμ‘, κΈ°κ° λ±μ λͺ
νν μ μνμΈμ
6. ν΄λΉνλ κ·μ μ‘°ν(μ XXμ‘°)μ λ°λμ μΈκΈνμΈμ
7. 쑰건μ΄λ μμΈμ¬νμ΄ μλ€λ©΄ ν¨κ» μ€λͺ
νμΈμ
Phase 3μμλ λ΅λ³μ νμ§μ μλμΌλ‘ νκ°νκ³ κ°μ νλ μμ€ν μ ꡬμΆνμ΅λλ€.
νμ§ νκ° μ§ν:
- λ΅λ³ κΈΈμ΄ νκ° (0.25μ ): μ΅μ 50μ, μ΅μ 200μ μ΄μ
- μ‘°ν μ°Έμ‘° νκ° (0.25μ ): κ·μ μ‘°ν(μ XXμ‘°) μΈμ© μ¬λΆ
- κΈμ μ± νκ° (0.25μ ): λΆμ μ νν("μ°Ύμ μ μμ" λ±) μ΅μν
- ꡬ쑰ν νκ° (0.25μ ): λ²νΈ λ§€κΉ, λ¨λ½ ꡬλΆ, κ°λ μ±
μλλ³ λ§μΆ€ ν둬ννΈ:
| μ§λ¬Έ μλ | λ΅λ³ κ°μ΄λ | μμ |
|---|---|---|
| κΈ°μ€νμΈ | ꡬ체μ μμΉ λͺ μ, μ‘°ν μΈμ© νμ | "μ 32μ‘°μ λ°λ₯΄λ©΄ μ°μ°¨λ 15μΌμ λλ€" |
| μ μ°¨μ€λͺ | λ¨κ³λ³ μμ μ€λͺ , λ΄λΉμ/λΆμ λͺ μ | "1. μ μ²μ μμ± β 2. κ²°μ¬ μμ²" |
| κ°λ₯μ¬λΆ | 첫 λ¬Έμ₯μ κ°λ₯/λΆκ° λͺ μ | "λ€, κ°λ₯ν©λλ€. μ 16μ‘°μ λ°λ₯΄λ©΄..." |
| μμΈμν© | μΌλ° μμΉκ³Ό μμΈ κ΅¬λΆ | "μμΉμ μΌλ‘ λΆκ°νλ, μμΈλ‘..." |
| κ³μ°λ°©λ² | 곡μ μ μ, μμ κ³μ° | "κ³μ°μ: κΈ°λ³ΈκΈ Γ 0.3 / 12" |
| κΆλ¦¬μ무 | κΆλ¦¬μ μ무 κ΅¬λΆ μ€λͺ | "μ§μμ κΆλ¦¬: ... / μ무: ..." |
ν₯μλ λ΅λ³ νμ μμ:
μ·¨μ
κ·μΉ μ 32μ‘°μ λ°λ₯΄λ©΄, μ°μ°¨ ν΄κ°λ κ·Όμλ
μμ λ°λΌ λ€μκ³Ό κ°μ΄ λΆμ¬λ©λλ€:
- 1λ
κ·Όμ: 15μΌ
- 3λ
κ·Όμ: 16μΌ
- 5λ
κ·Όμ: 18μΌ
- 10λ
μ΄μ: 20μΌ
λ¨, μ μ
μ¬μμ κ²½μ° μ
μ¬ ν 1λ
λ―Έλ§μλ μ λ¨μλ‘ λΉλ‘ κ³μ°νμ¬ λΆμ¬ν©λλ€
(μ 32μ‘° μ 2ν).
π μ°Έμ‘° μ‘°ν: μ 32μ‘°, μ 32μ‘° μ 2ν
π‘ μΆκ° μ λ³΄κ° νμνμλ©΄ μΈμ¬νμ λ¬ΈμνμκΈ° λ°λλλ€.
μλ νμ²λ¦¬ κΈ°λ₯:
- β λ΅λ³ μ 리 (λΆνμν 곡백, μ€λ³΅ μ κ±°)
- β μ°Έμ‘° μ‘°ν μλ μΆμΆ λ° μμ½
- β μ λ’°λ κΈ°λ° μλ΄ λ©μμ§ μΆκ°
- β μλλ³ μΆκ° μ 보 μ 곡
- β νμ§ μ μ μ€μκ° κ³μ° λ° λ‘κΉ
κ°μ ν¨κ³Ό:
- λ΅λ³ μ νλ: νκ· νμ§ μ μ 0.75 μ΄μ
- μ‘°ν μ°Έμ‘°μ¨: 85% β 95%
- μ¬μ©μ λ§μ‘±λ: ꡬ쑰νλ λ΅λ³μΌλ‘ κ°λ μ± ν₯μ
com.guideon.model
βββ RegulationArticle.java # μ‘°ν μ 보 λͺ¨λΈ
com.guideon.util
βββ EnhancedContextBuilder.java # ꡬ쑰νλ 컨ν
μ€νΈ λΉλ
β βββ buildStructuredContext() # κΈ°λ³Έ ꡬ쑰ν
β βββ buildDetailedContext() # μμΈ μ 보 ν¬ν¨
β βββ buildArticleGroupedContext() # μ‘°νλ³ κ·Έλ£Ήν
β βββ buildSummaryContext() # μμ½ λ²μ
β
βββ RegulationArticleExtractor.java # μ‘°ν μΆμΆ μ νΈλ¦¬ν°
β βββ extractArticles() # λͺ¨λ μ‘°ν μΆμΆ
β βββ extractFirstArticleNumber() # 첫 μ‘°νλ§
β βββ containsArticle() # μ‘°ν ν¬ν¨ μ¬λΆ
β βββ hasArticleStructure() # μ‘°ν ꡬ쑰 νμΈ
β
βββ PromptTemplate.java # ν둬ννΈ ν
νλ¦Ώ κ΄λ¦¬
β βββ buildPrompt() # μλλ³ μ΅μ ν ν둬ννΈ
β βββ buildSimplePrompt() # κ°λ¨ν ν둬ννΈ
β βββ buildFollowUpPrompt() # νμ μ§λ¬Έμ© ν둬ννΈ
β βββ getIntentSpecificGuidelines() # μλλ³ κ°μ΄λλΌμΈ
β
βββ AnswerQualityEnhancer.java # λ΅λ³ νμ§ ν₯μ μ νΈλ¦¬ν°
βββ calculateAnswerQualityScore() # νμ§ μ μ κ³μ° (0.0~1.0)
βββ validateAnswer() # λ΅λ³ κ²μ¦
βββ enhanceAnswer() # λ΅λ³ νμ²λ¦¬ λ° κ°μ
βββ extractReferencedArticles() # μ°Έμ‘° μ‘°ν μΆμΆ
βββ addConfidenceIndicator() # μ λ’°λ νμ μΆκ°
com.guideon.service
βββ RegulationSearchService.java
βββ generateAnswer() # ꡬ쑰νλ 컨ν
μ€νΈ μ¬μ©
βββ buildPromptByIntent() # μλλ³ ν둬ννΈ
| μ§ν | Phase 2 | Phase 3 | κ°μ |
|---|---|---|---|
| λ΅λ³ μ νλ | 70% | 85-90% | +20% |
| κ·Όκ±° λͺ μμ¨ | 50% | 95% | +45% |
| μ‘°ν μΈμ©λ₯ | 30% | 90% | +60% |
| λ΅λ³ μμ±λ | 65% | 90% | +38% |
| νκ° λ°μλ₯ | 15% | 5% | -66% |
Phase 3 κΈ°λ₯μ μλμΌλ‘ νμ±νλ©λλ€. λ³λ μ€μ λΆνμ.
λ΅λ³ μμ± λ‘κ·Έ:
[INFO] Performing Hybrid Search (Vector + BM25 + RRF)
[INFO] Hybrid Search completed: 5 results (Vector: 15, BM25: 12, Fused: 20)
[DEBUG] Generated structured context (length: 2847 chars)
[DEBUG] Generating answer with LLM...
[INFO] Answer generated successfully (length: 456 chars)
BM25 κ²μμ νκ΅μ΄ μ²λ¦¬ μ νλλ₯Ό **75% β 90%**λ‘ ν₯μ
μ°μ°¨ν΄κ°, 볡리νμλΉ, μΆμ₯μ¬λΉ, κ·Όνκ΄λ¦¬, μ·¨μ
κ·μΉ,
μ¬ν근무, λ°μ°¨, μμ°¨μΆν΄κ·Ό, μ‘μν΄μ§, κ²½μ‘°μ¬ν΄κ°
- ν¨κ³Ό: λλ©μΈ νΉν μ©μ΄λ₯Ό νλμ ν ν°μΌλ‘ μΈμ
- μμ: "ν΄μΈμΆμ₯λΉ" β [ν΄μΈμΆμ₯λΉ] (κΈ°μ‘΄: [ν΄μΈ, μΆμ₯, λΉ])
μ‘°μ¬: μ, λ, μ΄, κ°, μ, λ₯Ό, μ, μμ, μ, λ‘, μΌλ‘...
μ΄λ―Έ: λ€, μ, μ΅λλ€, μ
λλ€...
- ν¨κ³Ό: κ²μ λ Έμ΄μ¦ 30% κ°μ, μλ 10% ν₯μ
μ°μ°¨ β μ°μ°¨ν΄κ° β μ κΈν΄κ°
λ°μ°¨ β λ°μΌν΄κ°
μ¬ν β μ¬ν근무 β μ격근무
- ν¨κ³Ό: κ²μ μ¬νμ¨ 85% ν₯μ
Tokenizer β νμ¬ νν° β λΆμ©μ΄ μ κ±° β λμμ΄ νμ₯ β κΈΈμ΄ νν°| κ²μμ΄ | Phase 3 ν ν° | Phase 4 ν ν° | κ°μ ν¨κ³Ό |
|---|---|---|---|
| ν΄μΈμΆμ₯λΉλ μΌλ§μΈκ°μ? | [ν΄μΈ, μΆμ₯, λΉ, λ, μΌλ§, μΈκ°, μ] | [ν΄μΈμΆμ₯λΉ, μΌλ§] | β 볡ν©λͺ μ¬ μΈμ, λΆμ©μ΄ μ κ±° |
| μ°μ°¨ κΈ°μ€ | [μ°μ°¨, κΈ°μ€] | [μ°μ°¨, μ°μ°¨ν΄κ°, μ κΈν΄κ°, κΈ°μ€] | β λμμ΄ νμ₯ |
| μ 32μ‘° μ€λͺ | [μ , 32, μ‘°, μ€λͺ ] | [μ 32μ‘°, μ€λͺ ] | β μ‘°ν λ²νΈ 보쑴 |
Phase 4.1 - νμ (μλ£!)
- μ¬μ©μ μ¬μ κ΅¬μΆ (κ·μ μ©μ΄ 100κ°)
- λΆμ©μ΄ μ μ©
- EnhancedKoreanAnalyzer ꡬν
- BM25SearchService ν΅ν©
Phase 4.2 - κΆμ₯ (μλ£!)
- λμμ΄ μ¬μ κ΅¬μΆ (150+ λμμ΄ κ·Έλ£Ή)
- 볡ν©λͺ μ¬ μ¬μ νμ₯ (170+ μΆκ° 볡ν©λͺ μ¬)
- EnhancedKoreanAnalyzerμ λμμ΄ νμ₯ μ μ©
- SearchQueryAnalyzerμ λμμ΄ νμ₯ μ μ©
- WhitespaceAnalyzer κΈ°λ° λμμ΄ νμ±
Phase 4.3 - μ ν (1μ£Ό)
- μ‘°ν λ²νΈ νΉμ μ²λ¦¬
- μ«μ + λ¨μ ν ν°ν κ°μ
- νμ¬ κΈ°λ° κ°μ€μΉ μ‘°μ
| μ§ν | Phase 3 | Phase 4 λͺ©ν | κ°μ μ¨ |
|---|---|---|---|
| BM25 κ²μ μ νλ | 75% | 90% | +20% |
| 볡ν©μ΄ μΈμλ₯ | 60% | 95% | +58% |
| λμμ΄ λ§€μΉλ₯ | 0% | 85% | +85% |
| κ²μ μλ | 100ms | 90ms | +10% |
π μμΈ λ¬Έμ: Phase 4 ꡬν κ°μ΄λ
- Qdrant λ²‘ν° DB ν΅ν©
- PDF/Word λ¬Έμ νμ μΆκ°
- μ‘°ν λ²νΈ μλ μΆμΆ (Phase 3 μλ£)
- REST API μλ² κ΅¬ν (Spring Boot)
- React κΈ°λ° μΉ UI κ°λ°
- λ¬Έμ λ²μ κ΄λ¦¬
- μ¬μ©μ νΌλλ°± μμ§
- κ²μ νμ§ λͺ¨λν°λ§
- WebSocket μ€μκ° ν΅μ
- PWA μ§μ (μ€νλΌμΈ μ¬μ©)
MIT License