diff --git a/CHATBOT_GUIDE.md b/CHATBOT_GUIDE.md index f62b58d..5b5aac2 100644 --- a/CHATBOT_GUIDE.md +++ b/CHATBOT_GUIDE.md @@ -2,11 +2,60 @@ ## ๐ŸŽฏ API ๊ตฌ์กฐ - **์—”๋“œํฌ์ธํŠธ**: `POST /api/chat/query` -- **์š”์ฒญ**: `ChatRequest` (query + context) +- **์š”์ฒญ**: `ChatRequest` (query + context + language) - **์‘๋‹ต**: `CommonResponse` +- **๋‹ค๊ตญ์–ด ์ง€์›**: ํ•œ๊ตญ์–ด(ko), ์˜์–ด(en), ์ผ๋ณธ์–ด(ja), ์ค‘๊ตญ์–ด(zh) ## ๐Ÿ“‹ ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์งˆ๋‹ต ํ๋ฆ„ +### ๐ŸŒ ๋‹ค๊ตญ์–ด ์ง€์› ์˜ˆ์‹œ + +#### ์˜์–ด ์‚ฌ์šฉ์ž ์˜ˆ์‹œ +```json +// ์š”์ฒญ +{ + "query": "recommend me a kpop route", + "context": null, + "language": "en" +} + +// ์‘๋‹ต โ“ +{ + "responseType": "QUESTION", + "message": "Which region would you like to visit? (e.g., Seoul, Busan)", + "context": { + "theme": "K_POP", + "conversationState": "AWAITING_REGION", + "lastBotQuestion": "Which region would you like to visit? (e.g., Seoul, Busan)", + "sessionId": "...", + "userLanguage": "en" + } +} +``` + +#### ์ผ๋ณธ์–ด ์‚ฌ์šฉ์ž ์˜ˆ์‹œ +```json +// ์š”์ฒญ +{ + "query": "Kใƒใƒƒใƒ—ใƒซใƒผใƒˆใ‚’ๆŽจ่–ฆใ—ใฆใใ ใ•ใ„", + "context": null, + "language": "ja" +} + +// ์‘๋‹ต โ“ +{ + "responseType": "QUESTION", + "message": "ใฉใกใ‚‰ใฎๅœฐๅŸŸใ‚’ใ”ๅธŒๆœ›ใงใ™ใ‹๏ผŸ๏ผˆไพ‹๏ผšใ‚ฝใ‚ฆใƒซใ€้‡œๅฑฑ๏ผ‰", + "context": { + "theme": "K_POP", + "conversationState": "AWAITING_REGION", + "lastBotQuestion": "ใฉใกใ‚‰ใฎๅœฐๅŸŸใ‚’ใ”ๅธŒๆœ›ใงใ™ใ‹๏ผŸ๏ผˆไพ‹๏ผšใ‚ฝใ‚ฆใƒซใ€้‡œๅฑฑ๏ผ‰", + "sessionId": "...", + "userLanguage": "ja" + } +} +``` + ### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ƒˆ ๋ฃจํŠธ ์ƒ์„ฑ (CREATE_ROUTE) ๐Ÿ†• #### 1-1. ์™„์ „ํ•œ ์ •๋ณด ์ œ๊ณต ์‹œ @@ -15,7 +64,8 @@ POST /api/chat/query { "query": "2์ผ ์„œ์šธ K-POP ๋ฃจํŠธ ์ถ”์ฒœํ•ด์ค˜", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต 1 โœ… @@ -38,7 +88,8 @@ POST /api/chat/query // ์š”์ฒญ 1 { "query": "๋ฃจํŠธ ์ถ”์ฒœํ•ด์ค˜", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต 1 โ“ @@ -51,7 +102,10 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": null + "days": null, + "conversationState": "AWAITING_THEME", + "sessionId": "...", + "userLanguage": "ko" } } @@ -64,8 +118,12 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": null - } + "days": null, + "conversationState": "AWAITING_THEME", + "sessionId": "...", + "userLanguage": "ko" + }, + "language": "ko" } // ์‘๋‹ต 2 โ“ @@ -78,7 +136,10 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": null + "days": null, + "conversationState": "AWAITING_REGION", + "sessionId": "...", + "userLanguage": "ko" } } @@ -91,8 +152,12 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": null - } + "days": null, + "conversationState": "AWAITING_REGION", + "sessionId": "...", + "userLanguage": "ko" + }, + "language": "ko" } // ์‘๋‹ต 3 โ“ @@ -105,7 +170,10 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": null + "days": null, + "conversationState": "AWAITING_DAYS", + "sessionId": "...", + "userLanguage": "ko" } } @@ -118,8 +186,12 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": null - } + "days": null, + "conversationState": "AWAITING_DAYS", + "sessionId": "...", + "userLanguage": "ko" + }, + "language": "ko" } // ์‘๋‹ต 4 โœ… @@ -133,7 +205,10 @@ POST /api/chat/query "budget": null, "preferences": null, "durationMinutes": null, - "days": 2 + "days": 2, + "conversationState": "READY_FOR_ROUTE", + "sessionId": "...", + "userLanguage": "ko" } } ``` @@ -143,7 +218,8 @@ POST /api/chat/query // ์š”์ฒญ { "query": "4์ผ ์„œ์šธ K-POP ๋ฃจํŠธ ๋งŒ๋“ค์–ด์ค˜", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต โš ๏ธ @@ -162,7 +238,8 @@ POST /api/chat/query // ์š”์ฒญ { "query": "๊ธฐ์กด์— ๋งŒ๋“ค์–ด์ง„ ๋ถ€์‚ฐ ๋“œ๋ผ๋งˆ ๋ฃจํŠธ ์žˆ์–ด?", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต โœ… @@ -190,7 +267,8 @@ POST /api/chat/query // ์š”์ฒญ { "query": "ํ™๋Œ€ ๊ทผ์ฒ˜ K-POP ์žฅ์†Œ ์–ด๋”” ์žˆ์–ด?", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต โœ… @@ -218,7 +296,8 @@ POST /api/chat/query // ์š”์ฒญ { "query": "BTS๊ฐ€ ๋ญ์•ผ?", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต โœ… @@ -237,7 +316,8 @@ POST /api/chat/query // ์š”์ฒญ { "query": "๋ฃจํŠธ ์ถ”์ฒœํ•ด์ค˜", - "context": {"theme": "KPOP", "region": "์„œ์šธ", "days": 2} + "context": {"theme": "KPOP", "region": "์„œ์šธ", "days": 2}, + "language": "ko" } // ์‘๋‹ต โŒ @@ -252,7 +332,8 @@ POST /api/chat/query // ์š”์ฒญ { "query": "์ œ์ฃผ๋„ K-POP ์žฅ์†Œ ์ฐพ์•„์ค˜", - "context": null + "context": null, + "language": "ko" } // ์‘๋‹ต โŒ @@ -269,7 +350,8 @@ POST /api/chat/query "query": "๋ฃจํŠธ ์ถ”์ฒœ", "context": { "theme": "INVALID_THEME" // ์ž˜๋ชป๋œ Theme enum ๊ฐ’ - } + }, + "language": "ko" } // ์‘๋‹ต โŒ (400 Bad Request) @@ -303,11 +385,13 @@ interface ChatContext { lastBotQuestion?: string | null; sessionId?: string | null; // ๐Ÿ”’ ์‚ฌ์šฉ์ž๋ณ„ ์„ธ์…˜ ๋ณด์žฅ conversationStartTime?: number | null; + userLanguage?: "ko" | "en" | "ja" | "zh" | null; // ๐ŸŒ ์‚ฌ์šฉ์ž ์–ธ์–ด } interface ChatRequest { query: string; context: ChatContext | null; + language: "ko" | "en" | "ja" | "zh"; // ๐ŸŒ ๋‹ค๊ตญ์–ด ์ง€์› } interface ChatResponse { @@ -318,31 +402,40 @@ interface ChatResponse { } ``` -### ๐Ÿ”„ **Context ์ƒํƒœ ๊ด€๋ฆฌ ํ”Œ๋กœ์šฐ (๊ฐœ์„ ๋œ ์„ธ์…˜ ๋ณด์žฅ)** +### ๐Ÿ”„ **Context ์ƒํƒœ ๊ด€๋ฆฌ ํ”Œ๋กœ์šฐ (๋‹ค๊ตญ์–ด ์ง€์› ํฌํ•จ)** ```javascript class ChatManager { - constructor() { + constructor(userLanguage = 'ko') { this.currentContext = null; // ํ˜„์žฌ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ + this.userLanguage = userLanguage; // ๐ŸŒ ์‚ฌ์šฉ์ž ์–ธ์–ด ์„ค์ • } generateSessionId() { return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } + setLanguage(language) { + this.userLanguage = language; + // ์–ธ์–ด ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ์„ธ์…˜ ์‹œ์ž‘ + this.resetContext(); + } + async sendMessage(query) { // ๐Ÿ†• ์ฒซ ๋ฒˆ์งธ ๋ฉ”์‹œ์ง€์—์„œ ์„ธ์…˜ ID ์ƒ์„ฑ (์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ) if (!this.currentContext?.sessionId) { this.currentContext = { ...this.currentContext, sessionId: this.generateSessionId(), - conversationStartTime: Date.now() + conversationStartTime: Date.now(), + userLanguage: this.userLanguage // ๐ŸŒ ์–ธ์–ด ์ •๋ณด ํฌํ•จ }; } const request = { query: query, - context: this.currentContext // โš ๏ธ sessionId๋ฅผ ํฌํ•จํ•œ ์ „์ฒด context ์ „์†ก + context: this.currentContext, // โš ๏ธ sessionId๋ฅผ ํฌํ•จํ•œ ์ „์ฒด context ์ „์†ก + language: this.userLanguage // ๐ŸŒ ์–ธ์–ด ๋ช…์‹œ์  ์ „์†ก }; const response = await fetch('/api/chat/query', { @@ -369,62 +462,56 @@ class ChatManager { getConversationState() { return this.currentContext?.conversationState; } + + getUserLanguage() { + return this.userLanguage; + } } ``` -### ๐Ÿ“ **์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์‚ฌ์šฉ ์˜ˆ์‹œ** +### ๐Ÿ“ **๋‹ค๊ตญ์–ด ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์‚ฌ์šฉ ์˜ˆ์‹œ** ```javascript -const chatManager = new ChatManager(); +// ์˜์–ด ์‚ฌ์šฉ์ž +const chatManager = new ChatManager('en'); // 1. ์ฒซ ๋ฒˆ์งธ ์š”์ฒญ (์„ธ์…˜ ์‹œ์ž‘) -let response1 = await chatManager.sendMessage("๋ฃจํŠธ ์ถ”์ฒœํ•ด์ค˜"); +let response1 = await chatManager.sendMessage("recommend me a kpop route"); console.log(response1.result.context); // ์ถœ๋ ฅ: { // sessionId: "session_1693920000_abc123", -// conversationState: "AWAITING_THEME", -// lastBotQuestion: "์–ด๋–ค ํ…Œ๋งˆ์˜ ๋ฃจํŠธ๋ฅผ ์ฐพ๊ณ  ๊ณ„์‹ ๊ฐ€์š”?", +// conversationState: "AWAITING_REGION", // ํ…Œ๋งˆ๋Š” ์ด๋ฏธ ์ถ”์ถœ๋จ +// lastBotQuestion: "Which region would you like to visit? (e.g., Seoul, Busan)", // conversationStartTime: 1693920000000, -// theme: null, region: null, days: null, ... -// } - -// 2. ๋‘ ๋ฒˆ์งธ ์š”์ฒญ (ํ…Œ๋งˆ ์ œ๊ณต - ์˜๋„๋ถ„๋ฅ˜ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์ง์ ‘ ์ฒ˜๋ฆฌ!) -let response2 = await chatManager.sendMessage("K-POP์ด์š”"); -console.log(response2.result.context); -// ์ถœ๋ ฅ: { -// sessionId: "session_1693920000_abc123", // ๋™์ผ ์„ธ์…˜ ์œ ์ง€ -// conversationState: "AWAITING_REGION", // ๋‹ค์Œ ์ƒํƒœ๋กœ ์ „ํ™˜ -// lastBotQuestion: "์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”?", -// theme: "KPOP", // ํ…Œ๋งˆ ์ •๋ณด ์ถ”๊ฐ€ -// region: null, days: null, ... +// theme: "K_POP", region: null, days: null, +// userLanguage: "en" // } -// 3. ์„ธ ๋ฒˆ์งธ ์š”์ฒญ (์ง€์—ญ ์ œ๊ณต) -let response3 = await chatManager.sendMessage("์„œ์šธ๋กœ"); -// conversationState: "AWAITING_DAYS", theme: "KPOP", region: "์„œ์šธ" +// 2. ๋‘ ๋ฒˆ์งธ ์š”์ฒญ (์ง€์—ญ ์ œ๊ณต) +let response2 = await chatManager.sendMessage("Seoul"); +// conversationState: "AWAITING_DAYS", theme: "K_POP", region: "Seoul" -// 4. ๋„ค ๋ฒˆ์งธ ์š”์ฒญ (์ผ์ˆ˜ ์ œ๊ณต) -let response4 = await chatManager.sendMessage("2์ผ"); +// 3. ์„ธ ๋ฒˆ์งธ ์š”์ฒญ (์ผ์ˆ˜ ์ œ๊ณต) +let response3 = await chatManager.sendMessage("2 days"); // responseType: "ROUTE_RECOMMENDATION" - ๋ฃจํŠธ ์ƒ์„ฑ ์™„๋ฃŒ! - -// ๐Ÿ” ์„ธ์…˜ ์ƒํƒœ ํ™•์ธ -console.log(`ํ˜„์žฌ ์„ธ์…˜ ID: ${chatManager.getCurrentSessionId()}`); -console.log(`๋Œ€ํ™” ์ƒํƒœ: ${chatManager.getConversationState()}`); ``` ### โš ๏ธ **์ฃผ์˜์‚ฌํ•ญ** 1. **Context ๋ˆ„์ **: ๊ฐ ์‘๋‹ต์˜ `context` ํ•„๋“œ๋ฅผ ๋‹ค์Œ ์š”์ฒญ์˜ `context`๋กœ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ 2. **์„ธ์…˜ ๋ณด์žฅ**: `sessionId`๋Š” ๋ฐฑ์—”๋“œ์—์„œ ์ž๋™ ์ƒ์„ฑ/๊ด€๋ฆฌ๋˜๋ฏ€๋กœ ํ”„๋ก ํŠธ์—”๋“œ๋Š” ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ๋งŒ -3. **์ƒํƒœ ์ดˆ๊ธฐํ™”**: ์ƒˆ๋กœ์šด ๋Œ€ํ™” ์‹œ์ž‘ ์‹œ `resetContext()` ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ์„ธ์…˜ ์ƒ์„ฑ -4. **์—๋Ÿฌ ์ฒ˜๋ฆฌ**: API ์—๋Ÿฌ ์‹œ์—๋„ context ์ƒํƒœ๋ฅผ ์ ์ ˆํžˆ ๊ด€๋ฆฌ (์„ธ์…˜ ID ๋ณด์กด) -5. **ํƒ€์ž… ์•ˆ์ •์„ฑ**: TypeScript ์‚ฌ์šฉ ์‹œ ํ™•์žฅ๋œ Context ํƒ€์ž… ์ •์˜ ์ค€์ˆ˜ -6. **๐Ÿ”ฅ ํ•ต์‹ฌ ๊ฐœ์„ **: ์ด์ œ "k-pop" ๊ฐ™์€ ๋‹ต๋ณ€์ด ์˜๋„ ๋ถ„๋ฅ˜ ์˜ค๋ฅ˜ ์—†์ด ์ •ํ™•ํžˆ ์ฒ˜๋ฆฌ๋จ +3. **๋‹ค๊ตญ์–ด ์ง€์›**: `language` ํ•„๋“œ๋ฅผ ํ•ญ์ƒ ๋ช…์‹œํ•˜๊ณ , context์˜ `userLanguage`์™€ ์ผ์น˜์‹œํ‚ด +4. **์–ธ์–ด ๋ณ€๊ฒฝ**: ์–ธ์–ด ๋ณ€๊ฒฝ ์‹œ ์ƒˆ ์„ธ์…˜ ์‹œ์ž‘์œผ๋กœ ์ด์ „ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ ์ดˆ๊ธฐํ™” +5. **์ƒํƒœ ์ดˆ๊ธฐํ™”**: ์ƒˆ๋กœ์šด ๋Œ€ํ™” ์‹œ์ž‘ ์‹œ `resetContext()` ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ์„ธ์…˜ ์ƒ์„ฑ +6. **์—๋Ÿฌ ์ฒ˜๋ฆฌ**: API ์—๋Ÿฌ ์‹œ์—๋„ context ์ƒํƒœ๋ฅผ ์ ์ ˆํžˆ ๊ด€๋ฆฌ (์„ธ์…˜ ID ๋ณด์กด) +7. **ํƒ€์ž… ์•ˆ์ •์„ฑ**: TypeScript ์‚ฌ์šฉ ์‹œ ํ™•์žฅ๋œ Context ํƒ€์ž… ์ •์˜ ์ค€์ˆ˜ +8. **๐Ÿ”ฅ ํ•ต์‹ฌ**: ์–ธ์–ด๋ณ„ ๋งž์ถคํ˜• ๋ฉ”์‹œ์ง€ ์ œ๊ณต ๋ฐ fallback ์ „๋žต (ja/zh โ†’ en, ๊ธฐํƒ€ โ†’ ko) --- -## ๐ŸŽ›๏ธ ์˜๋„ ๋ถ„๋ฅ˜ ํ‚ค์›Œ๋“œ +## ๐ŸŽ›๏ธ ์˜๋„ ๋ถ„๋ฅ˜ ํ‚ค์›Œ๋“œ (๋‹ค๊ตญ์–ด) +### ํ•œ๊ตญ์–ด (ko) | **Intent** | **ํ‚ค์›Œ๋“œ** | **์˜ˆ์‹œ** | |------------|------------|----------| | **CREATE_ROUTE** | "์ถ”์ฒœํ•ด์ค˜", "๊ณ„ํšํ•ด์ค˜", "๋ฃจํŠธ ๋งŒ๋“ค", "์—ฌํ–‰ ๊ณ„ํš" | "2์ผ ์„œ์šธ ์—ฌํ–‰ ๊ณ„ํšํ•ด์ค˜" | @@ -432,53 +519,50 @@ console.log(`๋Œ€ํ™” ์ƒํƒœ: ${chatManager.getConversationState()}`); | **SEARCH_PLACES** | "์žฅ์†Œ", "๋ช…์†Œ", "์–ด๋””", "์œ„์น˜", "๊ณณ" | "๋ช…๋™ ๊ทผ์ฒ˜ ๋ทฐํ‹ฐ์ƒต ์–ด๋”” ์žˆ์–ด?" | | **GENERAL_QUESTION** | ๊ธฐํƒ€ ๋ชจ๋“  ์งˆ๋ฌธ | "ํ•œ๋ฅ˜๊ฐ€ ๋ญ์•ผ?", "K-POP ์—ญ์‚ฌ ์•Œ๋ ค์ค˜" | +### ์˜์–ด (en) +| **Intent** | **ํ‚ค์›Œ๋“œ** | **์˜ˆ์‹œ** | +|------------|------------|----------| +| **CREATE_ROUTE** | "recommend", "suggest", "plan", "create route", "make itinerary" | "Recommend a 2-day Seoul K-POP route" | +| **SEARCH_EXISTING_ROUTES** | "existing routes", "available routes", "find route", "search route" | "Are there existing Busan drama routes?" | +| **SEARCH_PLACES** | "place", "location", "where", "spot", "find", "near", "around" | "Where are K-POP places near Hongdae?" | +| **GENERAL_QUESTION** | ๊ธฐํƒ€ ๋ชจ๋“  ์งˆ๋ฌธ | "What is BTS?", "Tell me about K-POP history" | + +### ์ผ๋ณธ์–ด (ja) +| **Intent** | **ํ‚ค์›Œ๋“œ** | **์˜ˆ์‹œ** | +|------------|------------|----------| +| **CREATE_ROUTE** | "ๆŽจ่–ฆ", "่จˆ็”ป", "ใƒซใƒผใƒˆไฝœๆˆ", "ๆ—…่กŒใƒ—ใƒฉใƒณ" | "2ๆ—ฅ้–“ใฎใ‚ฝใ‚ฆใƒซK-POPใƒซใƒผใƒˆใ‚’ๆŽจ่–ฆใ—ใฆใใ ใ•ใ„" | +| **SEARCH_EXISTING_ROUTES** | "ๆ—ขๅญ˜ใƒซใƒผใƒˆ", "ไฝœๆˆๆธˆใฟใƒซใƒผใƒˆ", "ใƒซใƒผใƒˆๆคœ็ดข" | "้‡œๅฑฑใฎใƒ‰ใƒฉใƒžใƒซใƒผใƒˆใฏใ‚ใ‚Šใพใ™ใ‹๏ผŸ" | +| **SEARCH_PLACES** | "ๅ ดๆ‰€", "ใ‚นใƒใƒƒใƒˆ", "ใฉใ“", "ไฝ็ฝฎ", "่ฟ‘ใ" | "ๅผ˜ๅคง่ฟ‘ใใฎK-POPๅ ดๆ‰€ใฏใฉใ“ใงใ™ใ‹๏ผŸ" | +| **GENERAL_QUESTION** | ๊ธฐํƒ€ ๋ชจ๋“  ์งˆ๋ฌธ | "BTSใจใฏไฝ•ใงใ™ใ‹๏ผŸ", "K-POPๆญดๅฒใ‚’ๆ•™ใˆใฆ" | + +### ์ค‘๊ตญ์–ด (zh) +| **Intent** | **ํ‚ค์›Œ๋“œ** | **์˜ˆ์‹œ** | +|------------|------------|----------| +| **CREATE_ROUTE** | "ๆŽจ่", "่ฎกๅˆ’", "่ทฏ็บฟๅˆถไฝœ", "ๆ—…่กŒ่ง„ๅˆ’" | "่ฏทๆŽจ่2ๅคฉ้ฆ–ๅฐ”K-POP่ทฏ็บฟ" | +| **SEARCH_EXISTING_ROUTES** | "็Žฐๆœ‰่ทฏ็บฟ", "ๅทฒๅˆถไฝœ่ทฏ็บฟ", "่ทฏ็บฟๆœ็ดข" | "ๆœ‰้‡œๅฑฑๆˆๅ‰ง่ทฏ็บฟๅ—๏ผŸ" | +| **SEARCH_PLACES** | "ๅœฐ็‚น", "ๆ™ฏ็‚น", "ๅ“ช้‡Œ", "ไฝ็ฝฎ", "้™„่ฟ‘" | "ๅผ˜ๅคง้™„่ฟ‘็š„K-POPๅœฐ็‚นๅœจๅ“ช้‡Œ๏ผŸ" | +| **GENERAL_QUESTION** | ๊ธฐํƒ€ ๋ชจ๋“  ์งˆ๋ฌธ | "BTSๆ˜ฏไป€ไนˆ๏ผŸ", "ๅ‘Š่ฏ‰ๆˆ‘K-POPๅކๅฒ" | + --- ## ๐Ÿ“Š ์‘๋‹ต ํƒ€์ž…๋ณ„ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ | **ResponseType** | **ํฌํ•จ ํ•„๋“œ** | **์šฉ๋„** | |------------------|---------------|----------| -| **QUESTION** | `message` | ์ถ”๊ฐ€ ์ •๋ณด ์š”์ฒญ | +| **QUESTION** | `message` | ์ถ”๊ฐ€ ์ •๋ณด ์š”์ฒญ (๋‹ค๊ตญ์–ด ์ง€์›) | | **ROUTE_RECOMMENDATION** | `message`, `routeRecommendation` | ์ƒˆ ๋ฃจํŠธ ์ƒ์„ฑ ์™„๋ฃŒ | | **EXISTING_ROUTES** | `message`, `existingRoutes[]` | ๊ธฐ์กด ๋ฃจํŠธ ๋ชฉ๋ก | | **PLACE_INFO** | `message`, `places[]` | ์žฅ์†Œ ์ •๋ณด ๋ชฉ๋ก | -| **GENERAL_INFO** | `message` | ์ผ๋ฐ˜ ์ •๋ณด/๋‹ต๋ณ€ | - -## ๐Ÿ”„ ์ฃผ์š” ์ฒ˜๋ฆฌ ํ๋ฆ„ - -1. **ChatController.sendMessage()** - - ์š”์ฒญ ๋กœ๊น… - - ChatService.processUserQuery() ํ˜ธ์ถœ - - ์˜ˆ์™ธ ์ฒ˜๋ฆฌ (LLMException ๋ฐœ์ƒ) - -2. **ChatService.processUserQuery()** - - ์„ธ์…˜ ๋ณด์žฅ ๋ฐ ์ปจํ…์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ (ConversationManager.ensureSessionAndGetContext) - - ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์ฒ˜๋ฆฌ ํ™•์ธ (requiresStatefulHandling) - - ์ƒํƒœ๊ฐ€ ์žˆ์œผ๋ฉด: handleStatefulConversation() - - ์ƒํƒœ๊ฐ€ ์—†์œผ๋ฉด: ์˜๋„ ๋ถ„๋ฅ˜ ํ›„ ์˜๋„๋ณ„ ํ•ธ๋“ค๋Ÿฌ ๋ถ„๊ธฐ - - ์˜๋„๋ณ„ ํ•ธ๋“ค๋Ÿฌ ๋ถ„๊ธฐ - - CREATE_ROUTE โ†’ handleCreateRouteIntent() - - SEARCH_EXISTING_ROUTES โ†’ handleSearchExistingRoutesIntent() - - SEARCH_PLACES โ†’ handleSearchPlacesIntent() - - GENERAL_QUESTION โ†’ handleGeneralQuestionIntent() - -3. **๋ฃจํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ** - - ์ปจํ…์ŠคํŠธ ์ถ”์ถœ (ContextExtractor.extractContextFromQuery) - - ํ•„์ˆ˜ ์ •๋ณด ํ™•์ธ (ContextExtractor.checkMissingRequiredInfo) - - RAG ๊ธฐ๋ฐ˜ ๋ฃจํŠธ ์ถ”์ฒœ (recommendRouteWithRag) - - ์ผ์ˆ˜ ์กฐ์ • (adjustDaysIfNeeded) - **๐Ÿ”ง ์ˆ˜์ •๋จ: K_POP โ†’ "K_POP" ์˜ฌ๋ฐ”๋ฅธ ํ…Œ๋งˆ ๋ณ€ํ™˜** - - ์žฅ์†Œ ๊ฒ€์ƒ‰ (searchAndExtractPlaceIds) - - RouteService ํ˜ธ์ถœ (createRouteByAI) - - ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ (RagService.generateRouteRecommendationAnswer) - - ChatResponseBuilder๋ฅผ ํ†ตํ•œ ์ผ๊ด€๋œ ์‘๋‹ต ์ƒ์„ฑ - **๐Ÿ†• ์ถ”๊ฐ€๋จ** - -4. **์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์ฒ˜๋ฆฌ** - **๐Ÿ†• ์ถ”๊ฐ€๋จ** - - AWAITING_THEME โ†’ handleThemeInput() - - AWAITING_REGION โ†’ handleRegionInput() - - AWAITING_DAYS โ†’ handleDaysInput() - -5. **์‘๋‹ต ์ƒ์„ฑ ์•„ํ‚คํ…์ฒ˜** - **๐Ÿ†• ์ถ”๊ฐ€๋จ** - - ๋ชจ๋“  ์‘๋‹ต ์ƒ์„ฑ์€ ChatResponseBuilder๋ฅผ ํ†ตํ•ด ์ผ๊ด€์„ฑ ๋ณด์žฅ - - @JsonInclude(JsonInclude.Include.NON_NULL)๋กœ null ํ•„๋“œ ์‘๋‹ต์—์„œ ์ œ์™ธ - - ๋ฃจํŠธ ์ œ๋ชฉ ๊ฐœ์„ : "์„œ์šธ K-POP 2์ผ ๋ฃจํŠธ" ํ˜•ํƒœ๋กœ ๊ตฌ์ฒด์  ์ •๋ณด ํฌํ•จ +| **GENERAL_INFO** | `message` | ์ผ๋ฐ˜ ์ •๋ณด/๋‹ต๋ณ€ (๋‹ค๊ตญ์–ด ์ง€์›) | + +--- + +## ๐ŸŒ ์–ธ์–ด๋ณ„ Fallback ์ „๋žต +- **์ง€์› ์–ธ์–ด**: ko (ํ•œ๊ตญ์–ด), en (์˜์–ด), ja (์ผ๋ณธ์–ด), zh (์ค‘๊ตญ์–ด) +- **Fallback ๊ทœ์น™**: + - ja/zh โ†’ en (์ผ๋ณธ์–ด/์ค‘๊ตญ์–ด๋Š” ์˜์–ด๋กœ fallback) + - ๊ธฐํƒ€ ๋ชจ๋“  ์–ธ์–ด โ†’ ko (ํ•œ๊ตญ์–ด ๊ธฐ๋ณธ๊ฐ’) +- **์–ธ์–ด ์ฝ”๋“œ ๋ณ€ํ™˜**: + - chatbot ๋„๋ฉ”์ธ: "ja", "zh" ์‚ฌ์šฉ + - Tour API ํ˜ธ์ถœ ์‹œ: "J", "C"๋กœ ์ž๋™ ๋ณ€ํ™˜ diff --git a/src/main/java/com/mey/backend/domain/chatbot/dto/ChatContext.java b/src/main/java/com/mey/backend/domain/chatbot/dto/ChatContext.java index 890e696..3198d5a 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/dto/ChatContext.java +++ b/src/main/java/com/mey/backend/domain/chatbot/dto/ChatContext.java @@ -46,4 +46,7 @@ public class ChatContext { @Schema(description = "๋Œ€ํ™” ์‹œ์ž‘ ์‹œ๊ฐ„ (ํƒ€์ž„์Šคํƒฌํ”„)", example = "1693920000000") private Long conversationStartTime; + + @Schema(description = "์‚ฌ์šฉ์ž ์–ธ์–ด", example = "ko", allowableValues = {"ko", "en", "ja", "zh"}) + private String userLanguage; } \ No newline at end of file diff --git a/src/main/java/com/mey/backend/domain/chatbot/dto/ChatRequest.java b/src/main/java/com/mey/backend/domain/chatbot/dto/ChatRequest.java index ae8b71a..c4f3c41 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/dto/ChatRequest.java +++ b/src/main/java/com/mey/backend/domain/chatbot/dto/ChatRequest.java @@ -19,4 +19,8 @@ public class ChatRequest { @Schema(description = "์ด์ „ ๋Œ€ํ™”์—์„œ ์ถ”์ถœ๋œ ์ปจํ…์ŠคํŠธ ์ •๋ณด") private ChatContext context; + + @Schema(description = "์‚ฌ์šฉ์ž ์–ธ์–ด", example = "ko", allowableValues = {"ko", "en", "ja", "zh"}) + @Builder.Default + private String language = "ko"; } diff --git a/src/main/java/com/mey/backend/domain/chatbot/dto/IntentClassificationResult.java b/src/main/java/com/mey/backend/domain/chatbot/dto/IntentClassificationResult.java index 28e792e..122eccb 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/dto/IntentClassificationResult.java +++ b/src/main/java/com/mey/backend/domain/chatbot/dto/IntentClassificationResult.java @@ -3,14 +3,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @Schema(description = "LLM ๊ธฐ๋ฐ˜ ์˜๋„ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ") @Getter @Builder -@AllArgsConstructor public class IntentClassificationResult { @Schema(description = "๋ถ„๋ฅ˜๋œ ์˜๋„", example = "CREATE_ROUTE") @@ -32,6 +30,12 @@ public IntentClassificationResult( this.reasoning = reasoning; } + public IntentClassificationResult(UserIntent intent, double confidence, String reasoning) { + this.intent = intent; + this.confidence = confidence; + this.reasoning = reasoning; + } + public enum UserIntent { CREATE_ROUTE("CREATE_ROUTE", "์ƒˆ๋กœ์šด ๋ฃจํŠธ ์ƒ์„ฑ/์ถ”์ฒœ ์š”์ฒญ"), SEARCH_EXISTING_ROUTES("SEARCH_EXISTING_ROUTES", "๊ธฐ์กด์— ๋งŒ๋“ค์–ด์ง„ ๋ฃจํŠธ ๊ฒ€์ƒ‰ ์š”์ฒญ"), diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/ChatResponseBuilder.java b/src/main/java/com/mey/backend/domain/chatbot/service/ChatResponseBuilder.java index 88b5352..7586ff9 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/service/ChatResponseBuilder.java +++ b/src/main/java/com/mey/backend/domain/chatbot/service/ChatResponseBuilder.java @@ -29,6 +29,8 @@ public class ChatResponseBuilder { private final ConversationManager conversationManager; + private final MessageTemplateService messageTemplateService; + private final LanguageService languageService; /** * ๊ธฐ๋ณธ ์งˆ๋ฌธ ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. @@ -64,9 +66,10 @@ public ChatResponse createQuestionResponse(String message, ChatContext context, * ์—๋Ÿฌ ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. */ public ChatResponse createErrorResponse(String message, ChatContext context) { + String language = context.getUserLanguage() != null ? context.getUserLanguage() : "ko"; return ChatResponse.builder() .responseType(ChatResponse.ResponseType.QUESTION) - .message(message + " ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์‹œ๊ฒ ์–ด์š”?") + .message(message + " " + messageTemplateService.getErrorSuffix(language)) .context(context) .build(); } @@ -86,12 +89,13 @@ public ChatResponse createGeneralInfoResponse(String message, ChatContext contex * ์žฅ์†Œ ์ •๋ณด ์‘๋‹ต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. */ public ChatResponse createPlaceInfoResponse(String message, List places, ChatContext context) { + String language = context.getUserLanguage() != null ? context.getUserLanguage() : "ko"; List placeInfos = places.stream() .map(place -> ChatResponse.PlaceInfo.builder() .placeId(place.getPlaceId()) - .name(place.getNameKo()) - .description(place.getDescriptionKo()) - .address(place.getAddressKo()) + .name(languageService.getPlaceName(place, language)) + .description(languageService.getPlaceDescription(place, language)) + .address(languageService.getPlaceAddress(place, language)) .themes(place.getThemes()) .costInfo(place.getCostInfo()) .build()) @@ -111,9 +115,10 @@ public ChatResponse createPlaceInfoResponse(String message, List places, public ChatResponse createExistingRoutesResponse(String message, List routes, ChatContext context) { + String language = context.getUserLanguage() != null ? context.getUserLanguage() : "ko"; List existingRoutes = routes.stream() .limit(5) // ์ตœ๋Œ€ 5๊ฐœ ๋ฃจํŠธ๋งŒ ๋ฐ˜ํ™˜ - .map(this::convertRouteToExistingRoute) + .map(route -> convertRouteToExistingRoute(route, language)) .toList(); return ChatResponse.builder() @@ -163,7 +168,7 @@ public ChatResponse createAIRouteRecommendationResponse(String message, /** * Route ์—”ํ‹ฐํ‹ฐ๋ฅผ ExistingRoute DTO๋กœ ๋ณ€ํ™˜ */ - private ChatResponse.ExistingRoute convertRouteToExistingRoute(com.mey.backend.domain.route.entity.Route route) { + private ChatResponse.ExistingRoute convertRouteToExistingRoute(com.mey.backend.domain.route.entity.Route route, String language) { // Theme enum์„ String์œผ๋กœ ๋ณ€ํ™˜ List themeStrings = route.getThemes().stream() .map(theme -> theme.getRouteTheme()) @@ -171,8 +176,8 @@ private ChatResponse.ExistingRoute convertRouteToExistingRoute(com.mey.backend.d return ChatResponse.ExistingRoute.builder() .routeId(route.getId()) - .title(route.getTitleKo()) - .description(route.getDescriptionKo()) + .title(languageService.getRouteTitle(route, language)) + .description(languageService.getRouteDescription(route, language)) .estimatedCost(route.getTotalCost()) .durationMinutes(route.getTotalDurationMinutes()) .themes(themeStrings) @@ -183,12 +188,7 @@ private ChatResponse.ExistingRoute convertRouteToExistingRoute(com.mey.backend.d * ๋‹จ๊ณ„๋ณ„ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ */ public String generateStepMessage(ConversationState currentState, ChatContext context) { - return switch (currentState) { - case AWAITING_THEME -> "ํ…Œ๋งˆ ์„ ํƒ์ด ํ•„์š”ํ•ด์š”. K-POP, K-๋“œ๋ผ๋งˆ, K-ํ‘ธ๋“œ, K-ํŒจ์…˜ ์ค‘ ์–ด๋–ค ํ…Œ๋งˆ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”?"; - case AWAITING_REGION -> "์ข‹์Šต๋‹ˆ๋‹ค! " + (context.getTheme() != null ? context.getTheme().name() : "") + " ํ…Œ๋งˆ๋ฅผ ์„ ํƒํ•˜์…จ๋„ค์š”. ์–ด๋А ์ง€์—ญ์„ ์—ฌํ–‰ํ•˜๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”?"; - case AWAITING_DAYS -> (context.getRegion() != null ? context.getRegion() : "") + " ์ง€์—ญ์„ ์„ ํƒํ•˜์…จ๋„ค์š”! ๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”?"; - case READY_FOR_ROUTE -> "๋ชจ๋“  ์ •๋ณด๊ฐ€ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๋งž์ถค ๋ฃจํŠธ๋ฅผ ์ƒ์„ฑํ•ด๋“œ๋ฆด๊ฒŒ์š”."; - default -> "์•ˆ๋‚ด์— ๋”ฐ๋ผ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."; - }; + String language = context.getUserLanguage() != null ? context.getUserLanguage() : "ko"; + return messageTemplateService.getStateMessage(currentState, language); } } diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/ChatService.java b/src/main/java/com/mey/backend/domain/chatbot/service/ChatService.java index c7fd835..73c8776 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/service/ChatService.java +++ b/src/main/java/com/mey/backend/domain/chatbot/service/ChatService.java @@ -34,6 +34,8 @@ public class ChatService { private final IntentClassifier intentClassifier; private final ContextExtractor contextExtractor; private final ChatResponseBuilder responseBuilder; + private final LanguageService languageService; + private final MessageTemplateService messageTemplateService; @EventListener(ApplicationReadyEvent.class) @Transactional(readOnly = true) @@ -86,26 +88,31 @@ public void initializeVectorStore() { * ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์ฒ˜๋ฆฌ๋ฅผ ์ง€์›ํ•˜๋ฉฐ, ์„ธ์…˜ ์—ฐ์†์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. */ public ChatResponse processUserQuery(ChatRequest request) { - log.info("Processing user query: {}", request.getQuery()); + log.info("Processing user query: {} (language: {})", request.getQuery(), request.getLanguage()); - // 1. ์„ธ์…˜ ๋ณด์žฅ ๋ฐ ์ปจํ…์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ + // 1. ์–ธ์–ด ๊ฒ€์ฆ ๋ฐ ์„ค์ • + String validatedLanguage = languageService.validateAndGetLanguage(request.getLanguage()); + log.info("Validated language: {}", validatedLanguage); + + // 2. ์„ธ์…˜ ๋ณด์žฅ ๋ฐ ์ปจํ…์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ (์–ธ์–ด ์ •๋ณด ํฌํ•จ) ChatContext context = conversationManager.ensureSessionAndGetContext(request); - log.info("Current conversation state: {}, SessionId: {}", - context.getConversationState(), context.getSessionId()); + context = context.toBuilder().userLanguage(validatedLanguage).build(); + log.info("Current conversation state: {}, SessionId: {}, Language: {}", + context.getConversationState(), context.getSessionId(), context.getUserLanguage()); - // 2. ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์ฒ˜๋ฆฌ + // 3. ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์ฒ˜๋ฆฌ if (conversationManager.requiresStatefulHandling(context)) { return handleStatefulConversation(request, context); } - // 3. ์ดˆ๊ธฐ ์ƒํƒœ ๋˜๋Š” ์ƒํƒœ ์—†์Œ - ์˜๋„ ๋ถ„๋ฅ˜ ์ˆ˜ํ–‰ - IntentClassificationResult classificationResult = intentClassifier.classifyUserIntent(request.getQuery()); + // 4. ์ดˆ๊ธฐ ์ƒํƒœ ๋˜๋Š” ์ƒํƒœ ์—†์Œ - ์˜๋„ ๋ถ„๋ฅ˜ ์ˆ˜ํ–‰ (์–ธ์–ด ๊ณ ๋ ค) + IntentClassificationResult classificationResult = intentClassifier.classifyUserIntent(request.getQuery(), validatedLanguage); log.info("LLM ์˜๋„ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ: {} (์‹ ๋ขฐ๋„: {}, ๊ทผ๊ฑฐ: {})", classificationResult.getIntent(), classificationResult.getConfidence(), classificationResult.getReasoning()); - // 4. ์˜๋„๋ณ„ ์ฒ˜๋ฆฌ + // 5. ์˜๋„๋ณ„ ์ฒ˜๋ฆฌ return switch (classificationResult.getIntent()) { case CREATE_ROUTE -> handleCreateRouteIntent(request.toBuilder().context(context).build()); case SEARCH_EXISTING_ROUTES -> handleSearchExistingRoutesIntent(request.toBuilder().context(context).build()); @@ -140,11 +147,12 @@ private ChatResponse handleThemeInput(ChatRequest request, ChatContext context) ChatContext updatedContext = contextExtractor.extractThemeFromQuery(request.getQuery(), context); if (updatedContext.getTheme() == null) { + String language = context.getUserLanguage(); return responseBuilder.createQuestionResponse( - "ํ…Œ๋งˆ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. K-POP, K-๋“œ๋ผ๋งˆ, K-ํ‘ธ๋“œ, K-ํŒจ์…˜ ์ค‘์—์„œ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + messageTemplateService.getRecognitionFailureMessage("theme", language), context.toBuilder().build(), // ์ƒํƒœ ์œ ์ง€ ConversationState.AWAITING_THEME, - "์–ด๋–ค ํ…Œ๋งˆ์˜ ๋ฃจํŠธ๋ฅผ ์ฐพ๊ณ  ๊ณ„์‹ ๊ฐ€์š”?" + messageTemplateService.getMissingInfoMessage("theme", language) ); } @@ -156,11 +164,12 @@ private ChatResponse handleThemeInput(ChatRequest request, ChatContext context) conversationManager.saveSessionContext(nextContext.getSessionId(), nextContext); + String language = context.getUserLanguage(); return responseBuilder.createQuestionResponse( - "์ข‹์Šต๋‹ˆ๋‹ค! " + updatedContext.getTheme().name() + " ํ…Œ๋งˆ๋ฅผ ์„ ํƒํ•˜์…จ๋„ค์š”. ์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”? (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ)", + messageTemplateService.getThemeConfirmationMessage(updatedContext.getTheme().name(), language), nextContext, ConversationState.AWAITING_REGION, - "์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”?" + messageTemplateService.getMissingInfoMessage("region", language) ); } @@ -174,11 +183,12 @@ private ChatResponse handleRegionInput(ChatRequest request, ChatContext context) ChatContext updatedContext = contextExtractor.extractRegionFromQuery(request.getQuery(), context); if (updatedContext.getRegion() == null || updatedContext.getRegion().trim().isEmpty()) { + String language = context.getUserLanguage(); return responseBuilder.createQuestionResponse( - "์ง€์—ญ์„ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ์ฒด์ ์ธ ์ง€์—ญ๋ช…์„ ๋ง์”€ํ•ด์ฃผ์„ธ์š”. (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ, ์ œ์ฃผ๋„)", + messageTemplateService.getRecognitionFailureMessage("region", language), context.toBuilder().build(), // ์ƒํƒœ ์œ ์ง€ ConversationState.AWAITING_REGION, - "์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”?" + messageTemplateService.getMissingInfoMessage("region", language) ); } @@ -190,11 +200,12 @@ private ChatResponse handleRegionInput(ChatRequest request, ChatContext context) conversationManager.saveSessionContext(nextContext.getSessionId(), nextContext); + String language = context.getUserLanguage(); return responseBuilder.createQuestionResponse( - updatedContext.getRegion() + " ์ง€์—ญ์„ ์„ ํƒํ•˜์…จ๋„ค์š”! ๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”? (์˜ˆ: 1์ผ, 2์ผ, 3์ผ)", + messageTemplateService.getRegionConfirmationMessage(updatedContext.getRegion(), language), nextContext, ConversationState.AWAITING_DAYS, - "๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”?" + messageTemplateService.getMissingInfoMessage("days", language) ); } @@ -208,11 +219,12 @@ private ChatResponse handleDaysInput(ChatRequest request, ChatContext context) { ChatContext updatedContext = contextExtractor.extractDaysFromQuery(request.getQuery(), context); if (updatedContext.getDays() == null || updatedContext.getDays() <= 0) { + String language = context.getUserLanguage(); return responseBuilder.createQuestionResponse( - "์ผ์ˆ˜๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆซ์ž๋กœ ๋ง์”€ํ•ด์ฃผ์„ธ์š”. (์˜ˆ: 1์ผ, 2์ผ, 3์ผ)", + messageTemplateService.getRecognitionFailureMessage("days", language), context.toBuilder().build(), // ์ƒํƒœ ์œ ์ง€ ConversationState.AWAITING_DAYS, - "๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”?" + messageTemplateService.getMissingInfoMessage("days", language) ); } @@ -237,7 +249,8 @@ private ChatResponse recommendRouteWithRag(ChatContext context, String originalQ List placeIds = ragService.searchPlaceIds(searchQuery, placesNeeded); if (placeIds.isEmpty()) { - return responseBuilder.createErrorResponse("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ์žฅ์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ๋งˆ๋‚˜ ์ง€์—ญ์„ ์‹œ๋„ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”?", context); + String language = context.getUserLanguage(); + return responseBuilder.createErrorResponse(messageTemplateService.getNoResultsMessage(language), context); } // 2. ์‹ค์ œ ๊ฒ€์ƒ‰๋œ ์žฅ์†Œ ์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ์ˆ˜ ์กฐ์ • @@ -272,10 +285,20 @@ private String buildSearchQuery(ChatContext context, String originalQuery) { } private String createDocumentFromPlace(Place place) { + return createDocumentFromPlace(place, "ko"); // ๊ธฐ๋ณธ ํ•œ๊ตญ์–ด๋กœ ๋ฒกํ„ฐ ์Šคํ† ์–ด ๊ตฌ์„ฑ + } + + private String createDocumentFromPlace(Place place, String language) { StringBuilder document = new StringBuilder(); - document.append("์žฅ์†Œ๋ช…: ").append(place.getNameKo()).append("\n"); - document.append("์„ค๋ช…: ").append(place.getDescriptionKo()).append("\n"); - document.append("์ฃผ์†Œ: ").append(place.getAddressKo()).append("\n"); + + // ์–ธ์–ด๋ณ„ ํ•„๋“œ ํ™œ์šฉ + String placeName = languageService.getPlaceName(place, language); + String placeDescription = languageService.getPlaceDescription(place, language); + String placeAddress = languageService.getPlaceAddress(place, language); + + document.append("์žฅ์†Œ๋ช…: ").append(placeName).append("\n"); + document.append("์„ค๋ช…: ").append(placeDescription).append("\n"); + document.append("์ฃผ์†Œ: ").append(placeAddress).append("\n"); document.append("์ง€์—ญ: ").append(place.getRegion().getNameKo()).append("\n"); document.append("ํ…Œ๋งˆ: ").append(String.join(", ", place.getThemes())).append("\n"); document.append("๋น„์šฉ์ •๋ณด: ").append(place.getCostInfo()).append("\n"); @@ -363,10 +386,12 @@ private ChatResponse createRouteAndResponse(DaysAdjustmentResult adjustmentResul .filter(java.util.Objects::nonNull) .collect(java.util.stream.Collectors.toList()); + String language = adjustmentResult.adjustedContext().getUserLanguage(); String aiGeneratedMessage = ragService.generateRouteRecommendationAnswerWithPlaces( adjustmentResult.adjustedContext().getDays() + "์ผ " + adjustmentResult.adjustedContext().getTheme().name() + " ํ…Œ๋งˆ ๋ฃจํŠธ", - routePlaces + routePlaces, + language ); String finalMessage = adjustmentResult.adjustmentMessage() + aiGeneratedMessage; @@ -390,7 +415,8 @@ private ChatResponse createRouteAndResponse(DaysAdjustmentResult adjustmentResul } catch (Exception e) { log.error("๋ฃจํŠธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", e); - return responseBuilder.createErrorResponse("๋ฃจํŠธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์‹œ๊ฒ ์–ด์š”?", adjustmentResult.adjustedContext()); + String language = adjustmentResult.adjustedContext().getUserLanguage(); + return responseBuilder.createErrorResponse(messageTemplateService.getNoResultsMessage(language), adjustmentResult.adjustedContext()); } } @@ -405,21 +431,22 @@ private ChatResponse handleCreateRouteIntent(ChatRequest request) { String missingInfo = contextExtractor.checkMissingRequiredInfo(extractedContext); if (missingInfo != null) { // ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋Œ€ํ™” ์‹œ์ž‘ - ์ฒซ ๋ฒˆ์งธ ๋ˆ„๋ฝ ํ•ญ๋ชฉ์— ๋”ฐ๋ผ ์ƒํƒœ ์„ค์ • + String language = request.getLanguage(); ConversationState nextState; String question; if (extractedContext.getTheme() == null) { nextState = ConversationState.AWAITING_THEME; - question = "์–ด๋–ค ํ…Œ๋งˆ์˜ ๋ฃจํŠธ๋ฅผ ์ฐพ๊ณ  ๊ณ„์‹ ๊ฐ€์š”? (K-POP, K-๋“œ๋ผ๋งˆ, K-ํ‘ธ๋“œ, K-ํŒจ์…˜ ์ค‘ ์„ ํƒํ•ด์ฃผ์„ธ์š”)"; + question = messageTemplateService.getMissingInfoMessage("theme", language); } else if (extractedContext.getRegion() == null || extractedContext.getRegion().trim().isEmpty()) { nextState = ConversationState.AWAITING_REGION; - question = "์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”? (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ)"; + question = messageTemplateService.getMissingInfoMessage("region", language); } else { nextState = ConversationState.AWAITING_DAYS; - question = "๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”? (์˜ˆ: 1์ผ, 2์ผ, 3์ผ)"; + question = messageTemplateService.getMissingInfoMessage("days", language); } - return responseBuilder.createQuestionResponse(missingInfo, extractedContext, nextState, question); + return responseBuilder.createQuestionResponse(question, extractedContext, nextState, question); } // 3. RAG๋ฅผ ํ†ตํ•œ ๋ฃจํŠธ ์ƒ์„ฑ @@ -438,12 +465,14 @@ private ChatResponse handleSearchExistingRoutesIntent(ChatRequest request) { List routes = searchExistingRoutes(extractedContext, request.getQuery()); if (routes.isEmpty()) { - return responseBuilder.createQuestionResponse("์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ๊ธฐ์กด ๋ฃจํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๋ฃจํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋“œ๋ฆด๊นŒ์š”?", extractedContext); + String language = extractedContext.getUserLanguage(); + return responseBuilder.createQuestionResponse(messageTemplateService.getNoResultsMessage(language), extractedContext); } - // 3. RAG๋ฅผ ํ†ตํ•œ ์ž์—ฐ์Šค๋Ÿฌ์šด ์ถ”์ฒœ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + // 3. RAG๋ฅผ ํ†ตํ•œ ์ž์—ฐ์Šค๋Ÿฌ์šด ์ถ”์ฒœ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ (์–ธ์–ด ๊ณ ๋ ค) List relevantDocs = ragService.retrieve(request.getQuery(), 3); - String recommendationMessage = ragService.generateRouteRecommendationAnswer(request.getQuery(), relevantDocs); + String language = extractedContext.getUserLanguage(); + String recommendationMessage = ragService.generateRouteRecommendationAnswer(request.getQuery(), relevantDocs, language); // 4. Route ์—”ํ‹ฐํ‹ฐ๋ฅผ ExistingRoute DTO๋กœ ๋ณ€ํ™˜ return responseBuilder.createExistingRoutesResponse(recommendationMessage, routes, extractedContext); @@ -459,11 +488,13 @@ private ChatResponse handleSearchPlacesIntent(ChatRequest request) { List placeIds = ragService.searchPlaceIds(request.getQuery(), 5); if (placeIds.isEmpty()) { - return responseBuilder.createQuestionResponse("์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ์žฅ์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”?", extractedContext); + String language = extractedContext.getUserLanguage(); + return responseBuilder.createQuestionResponse(messageTemplateService.getNoResultsMessage(language), extractedContext); } List places = placeRepository.findAllById(placeIds); - return responseBuilder.createPlaceInfoResponse("๊ฒ€์ƒ‰ํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ์žฅ์†Œ๋“ค์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:", places, extractedContext); + String language = extractedContext.getUserLanguage(); + return responseBuilder.createPlaceInfoResponse(messageTemplateService.getPlaceInfoHeader(language), places, extractedContext); } /** diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/ContextExtractor.java b/src/main/java/com/mey/backend/domain/chatbot/service/ContextExtractor.java index e0cdf33..34bcd23 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/service/ContextExtractor.java +++ b/src/main/java/com/mey/backend/domain/chatbot/service/ContextExtractor.java @@ -44,35 +44,15 @@ public class ContextExtractor { * ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์—์„œ ์ „์ฒด ์ปจํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. */ public ChatContext extractContextFromQuery(String query, ChatContext existingContext) { - String systemPrompt = """ - ๋‹น์‹ ์€ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์—์„œ ํ•œ๋ฅ˜ ๋ฃจํŠธ ์ถ”์ฒœ์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. - ๋‹ค์Œ ์ •๋ณด๋ฅผ JSON ํ˜•ํƒœ๋กœ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”: - - theme: ํ…Œ๋งˆ ("KDRAMA", "KPOP", "KFOOD", "KFASHION" ์ค‘ ํ•˜๋‚˜, ์ •ํ™•ํžˆ ์ด ๊ฐ’๋“ค ์‚ฌ์šฉ) - - region: ์ง€์—ญ๋ช… (์„œ์šธ, ๋ถ€์‚ฐ ๋“ฑ) - - budget: ์˜ˆ์‚ฐ (์ˆซ์ž๋งŒ, ์› ๋‹จ์œ„) - - preferences: ํŠน๋ณ„ ์„ ํ˜ธ์‚ฌํ•ญ - - durationMinutes: ์†Œ์š” ์‹œ๊ฐ„ (๋ถ„ ๋‹จ์œ„) - - days: ์—ฌํ–‰ ์ผ์ˆ˜ (1, 2, 3 ๋“ฑ์˜ ์ˆซ์ž) - - ๊ธฐ์กด ์ปจํ…์ŠคํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ์ •๋ณด๋งŒ ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”. - ์ •๋ณด๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ถ”์ถœํ•  ์ˆ˜ ์—†์œผ๋ฉด null๋กœ ์„ค์ •ํ•˜์„ธ์š”. - - ํ…Œ๋งˆ ๋ณ€ํ™˜ ๊ทœ์น™: - - "K-POP", "์ผ€์ดํŒ", "kpop" โ†’ "KPOP" - - "K-๋“œ๋ผ๋งˆ", "์ผ€์ด๋“œ๋ผ๋งˆ", "kdrama" โ†’ "KDRAMA" - - "K-ํ‘ธ๋“œ", "์ผ€์ดํ‘ธ๋“œ", "kfood" โ†’ "KFOOD" - - "K-ํŒจ์…˜", "์ผ€์ดํŒจ์…˜", "kfashion" โ†’ "KFASHION" - - ์‘๋‹ต ํ˜•์‹ (๋ฐ˜๋“œ์‹œ ์œ ํšจํ•œ JSON): - { - "theme": "KPOP", - "region": "์„œ์šธ", - "budget": 50000, - "preferences": null, - "durationMinutes": null, - "days": 2 - } - """; + String language = existingContext != null ? existingContext.getUserLanguage() : "ko"; + return extractContextFromQuery(query, existingContext, language); + } + + /** + * ์–ธ์–ด๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์—์„œ ์ „์ฒด ์ปจํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + */ + public ChatContext extractContextFromQuery(String query, ChatContext existingContext, String language) { + String systemPrompt = getSystemPromptByLanguage(language); String contextInfo = existingContext != null ? "๊ธฐ์กด ์ปจํ…์ŠคํŠธ: " + convertContextToString(existingContext) : "๊ธฐ์กด ์ปจํ…์ŠคํŠธ ์—†์Œ"; @@ -334,4 +314,148 @@ private org.springframework.ai.chat.model.ChatResponse callOpenAi(String userInp return chatModel.call(prompt); } + + /** + * ์–ธ์–ด๋ณ„ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private String getSystemPromptByLanguage(String language) { + if (language == null) { + language = "ko"; + } + return switch (language) { + case "ko" -> getKoreanSystemPrompt(); + case "en" -> getEnglishSystemPrompt(); + case "ja" -> getJapaneseSystemPrompt(); + case "zh" -> getChineseSystemPrompt(); + default -> getKoreanSystemPrompt(); + }; + } + + private String getKoreanSystemPrompt() { + return """ + ๋‹น์‹ ์€ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์—์„œ ํ•œ๋ฅ˜ ๋ฃจํŠธ ์ถ”์ฒœ์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. + ๋‹ค์Œ ์ •๋ณด๋ฅผ JSON ํ˜•ํƒœ๋กœ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”: + - theme: ํ…Œ๋งˆ ("KDRAMA", "KPOP", "KFOOD", "KFASHION" ์ค‘ ํ•˜๋‚˜, ์ •ํ™•ํžˆ ์ด ๊ฐ’๋“ค ์‚ฌ์šฉ) + - region: ์ง€์—ญ๋ช… (์„œ์šธ, ๋ถ€์‚ฐ ๋“ฑ) + - budget: ์˜ˆ์‚ฐ (์ˆซ์ž๋งŒ, ์› ๋‹จ์œ„) + - preferences: ํŠน๋ณ„ ์„ ํ˜ธ์‚ฌํ•ญ + - durationMinutes: ์†Œ์š” ์‹œ๊ฐ„ (๋ถ„ ๋‹จ์œ„) + - days: ์—ฌํ–‰ ์ผ์ˆ˜ (1, 2, 3 ๋“ฑ์˜ ์ˆซ์ž) + + ๊ธฐ์กด ์ปจํ…์ŠคํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ์ •๋ณด๋งŒ ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”. + ์ •๋ณด๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ถ”์ถœํ•  ์ˆ˜ ์—†์œผ๋ฉด null๋กœ ์„ค์ •ํ•˜์„ธ์š”. + + ํ…Œ๋งˆ ๋ณ€ํ™˜ ๊ทœ์น™: + - "K-POP", "์ผ€์ดํŒ", "kpop" โ†’ "KPOP" + - "K-๋“œ๋ผ๋งˆ", "์ผ€์ด๋“œ๋ผ๋งˆ", "kdrama" โ†’ "KDRAMA" + - "K-ํ‘ธ๋“œ", "์ผ€์ดํ‘ธ๋“œ", "kfood" โ†’ "KFOOD" + - "K-ํŒจ์…˜", "์ผ€์ดํŒจ์…˜", "kfashion" โ†’ "KFASHION" + + ์‘๋‹ต ํ˜•์‹ (๋ฐ˜๋“œ์‹œ ์œ ํšจํ•œ JSON): + { + "theme": "KPOP", + "region": "์„œ์šธ", + "budget": 50000, + "preferences": null, + "durationMinutes": null, + "days": 2 + } + """; + } + + private String getEnglishSystemPrompt() { + return """ + You are an expert at extracting information needed for Korean Wave route recommendations from user questions. + Please extract the following information in JSON format: + - theme: Theme ("KDRAMA", "KPOP", "KFOOD", "KFASHION" - use exactly these values) + - region: Region name (Seoul, Busan, etc.) + - budget: Budget (numbers only, in KRW) + - preferences: Special preferences + - durationMinutes: Duration in minutes + - days: Number of travel days (numbers like 1, 2, 3) + + If there is existing context, update only new information based on it. + If information is not available or cannot be extracted, set it to null. + + Theme conversion rules: + - "K-POP", "kpop", "k-pop" โ†’ "KPOP" + - "K-Drama", "kdrama", "k-drama" โ†’ "KDRAMA" + - "K-Food", "kfood", "k-food" โ†’ "KFOOD" + - "K-Fashion", "kfashion", "k-fashion" โ†’ "KFASHION" + + Response format (must be valid JSON): + { + "theme": "KPOP", + "region": "Seoul", + "budget": 50000, + "preferences": null, + "durationMinutes": null, + "days": 2 + } + """; + } + + private String getJapaneseSystemPrompt() { + return """ + ใ‚ใชใŸใฏ้Ÿ“ๆตใƒซใƒผใƒˆๆŽจ่–ฆใซๅฟ…่ฆใชๆƒ…ๅ ฑใ‚’ใƒฆใƒผใ‚ถใƒผใฎ่ณชๅ•ใ‹ใ‚‰ๆŠฝๅ‡บใ™ใ‚‹ๅฐ‚้–€ๅฎถใงใ™ใ€‚ + ไปฅไธ‹ใฎๆƒ…ๅ ฑใ‚’JSONๅฝขๅผใงๆŠฝๅ‡บใ—ใฆใใ ใ•ใ„๏ผš + - theme: ใƒ†ใƒผใƒž ("KDRAMA", "KPOP", "KFOOD", "KFASHION" ใฎใ„ใšใ‚Œใ‹ใ€ๆญฃ็ขบใซใ“ใ‚Œใ‚‰ใฎๅ€คใ‚’ไฝฟ็”จ) + - region: ๅœฐๅŸŸๅ (ใ‚ฝใ‚ฆใƒซใ€้‡œๅฑฑใชใฉ) + - budget: ไบˆ็ฎ— (ๆ•ฐๅญ—ใฎใฟใ€ใ‚ฆใ‚ฉใƒณๅ˜ไฝ) + - preferences: ็‰นๅˆฅใชๅฅฝใฟ + - durationMinutes: ๆ‰€่ฆๆ™‚้–“ (ๅˆ†ๅ˜ไฝ) + - days: ๆ—…่กŒๆ—ฅๆ•ฐ (1, 2, 3ใชใฉใฎๆ•ฐๅญ—) + + ๆ—ขๅญ˜ใฎใ‚ณใƒณใƒ†ใ‚ญใ‚นใƒˆใŒใ‚ใ‚‹ๅ ดๅˆใฏใ€ใใ‚Œใ‚’ๅŸบใซๆ–ฐใ—ใ„ๆƒ…ๅ ฑใฎใฟใ‚’ๆ›ดๆ–ฐใ—ใฆใใ ใ•ใ„ใ€‚ + ๆƒ…ๅ ฑใŒใชใ„ใ‹ๆŠฝๅ‡บใงใใชใ„ๅ ดๅˆใฏใ€nullใซ่จญๅฎšใ—ใฆใใ ใ•ใ„ใ€‚ + + ใƒ†ใƒผใƒžๅค‰ๆ›ใƒซใƒผใƒซ: + - "K-POP", "ใ‚ฑใ‚คใƒใƒƒใƒ—", "kpop" โ†’ "KPOP" + - "K-ใƒ‰ใƒฉใƒž", "ใ‚ฑใ‚คใƒ‰ใƒฉใƒž", "kdrama" โ†’ "KDRAMA" + - "K-ใƒ•ใƒผใƒ‰", "ใ‚ฑใ‚คใƒ•ใƒผใƒ‰", "kfood" โ†’ "KFOOD" + - "K-ใƒ•ใ‚กใƒƒใ‚ทใƒงใƒณ", "ใ‚ฑใ‚คใƒ•ใ‚กใƒƒใ‚ทใƒงใƒณ", "kfashion" โ†’ "KFASHION" + + ๅฟœ็ญ”ๅฝขๅผ (ๆœ‰ๅŠนใชJSONใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚Šใพใ™): + { + "theme": "KPOP", + "region": "ใ‚ฝใ‚ฆใƒซ", + "budget": 50000, + "preferences": null, + "durationMinutes": null, + "days": 2 + } + """; + } + + private String getChineseSystemPrompt() { + return """ + ๆ‚จๆ˜ฏไปŽ็”จๆˆท้—ฎ้ข˜ไธญๆๅ–้Ÿฉๆต่ทฏ็บฟๆŽจ่ๆ‰€้œ€ไฟกๆฏ็š„ไธ“ๅฎถใ€‚ + ่ฏทไปฅJSONๆ ผๅผๆๅ–ไปฅไธ‹ไฟกๆฏ๏ผš + - theme: ไธป้ข˜ ("KDRAMA", "KPOP", "KFOOD", "KFASHION" ไธญ็š„ไธ€ไธช๏ผŒ่ฏทๅ‡†็กฎไฝฟ็”จ่ฟ™ไบ›ๅ€ผ) + - region: ๅœฐๅŒบๅ็งฐ (้ฆ–ๅฐ”ใ€้‡œๅฑฑ็ญ‰) + - budget: ้ข„็ฎ— (ไป…ๆ•ฐๅญ—๏ผŒ้Ÿฉๅ…ƒๅ•ไฝ) + - preferences: ็‰นๆฎŠๅๅฅฝ + - durationMinutes: ๆŒ็ปญๆ—ถ้—ด (ๅˆ†้’Ÿ) + - days: ๆ—…่กŒๅคฉๆ•ฐ (1, 2, 3็ญ‰ๆ•ฐๅญ—) + + ๅฆ‚ๆžœๆœ‰็Žฐๆœ‰ไธŠไธ‹ๆ–‡๏ผŒ่ฏทๅŸบไบŽๅฎƒไป…ๆ›ดๆ–ฐๆ–ฐไฟกๆฏใ€‚ + ๅฆ‚ๆžœไฟกๆฏไธๅฏ็”จๆˆ–ๆ— ๆณ•ๆๅ–๏ผŒ่ฏท่ฎพ็ฝฎไธบnullใ€‚ + + ไธป้ข˜่ฝฌๆข่ง„ๅˆ™: + - "K-POP", "้Ÿฉๆต้Ÿณไน", "kpop" โ†’ "KPOP" + - "K-Drama", "้Ÿฉๅ‰ง", "kdrama" โ†’ "KDRAMA" + - "K-Food", "้Ÿฉ้ฃŸ", "kfood" โ†’ "KFOOD" + - "K-Fashion", "้Ÿฉๆตๆ—ถๅฐš", "kfashion" โ†’ "KFASHION" + + ๅ“ๅบ”ๆ ผๅผ (ๅฟ…้กปๆ˜ฏๆœ‰ๆ•ˆ็š„JSON): + { + "theme": "KPOP", + "region": "้ฆ–ๅฐ”", + "budget": 50000, + "preferences": null, + "durationMinutes": null, + "days": 2 + } + """; + } } diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/IntentClassifier.java b/src/main/java/com/mey/backend/domain/chatbot/service/IntentClassifier.java index f8f3acb..d0e83f5 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/service/IntentClassifier.java +++ b/src/main/java/com/mey/backend/domain/chatbot/service/IntentClassifier.java @@ -39,45 +39,26 @@ public class IntentClassifier { * ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์˜ ์˜๋„๋ฅผ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. */ public IntentClassificationResult classifyUserIntent(String query) { + return classifyUserIntent(query, "ko"); // ๊ธฐ๋ณธ ํ•œ๊ตญ์–ด + } + + /** + * ์–ธ์–ด๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์˜ ์˜๋„๋ฅผ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public IntentClassificationResult classifyUserIntent(String query, String language) { try { - return classifyWithLLM(query); + return classifyWithLLM(query, language); } catch (Exception e) { log.error("LLM ์˜๋„ ๋ถ„๋ฅ˜ ์‹คํŒจ, fallback ์‚ฌ์šฉ: {}", e.getMessage()); - return fallbackIntentClassification(query); + return fallbackIntentClassification(query, language); } } /** * LLM์„ ์‚ฌ์šฉํ•œ ์˜๋„ ๋ถ„๋ฅ˜ */ - private IntentClassificationResult classifyWithLLM(String query) { - String systemPrompt = """ - ๋‹น์‹ ์€ ํ•œ๋ฅ˜ ์—ฌํ–‰ ์ฑ—๋ด‡์˜ ์˜๋„ ๋ถ„๋ฅ˜ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. - ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์„ ๋‹ค์Œ 4๊ฐ€์ง€ ์˜๋„ ์ค‘ ํ•˜๋‚˜๋กœ ๋ถ„๋ฅ˜ํ•ด์ฃผ์„ธ์š”: - - 1. CREATE_ROUTE: ์ƒˆ๋กœ์šด ์—ฌํ–‰ ๋ฃจํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋‹ฌ๋ผ๋Š” ์š”์ฒญ - - ํ‚ค์›Œ๋“œ: "์ถ”์ฒœํ•ด์ค˜", "๊ณ„ํšํ•ด์ค˜", "๋ฃจํŠธ ๋งŒ๋“ค", "์—ฌํ–‰ ๊ณ„ํš", "์ผ์ • ์งœ์ค˜" - - ์˜ˆ์‹œ: "2์ผ ์„œ์šธ K-POP ๋ฃจํŠธ ์ถ”์ฒœํ•ด์ค˜", "๋ถ€์‚ฐ ์—ฌํ–‰ ๊ณ„ํšํ•ด์ค˜" - - 2. SEARCH_EXISTING_ROUTES: ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ๋ฃจํŠธ๋ฅผ ์ฐพ์•„๋‹ฌ๋ผ๋Š” ์š”์ฒญ - - ํ‚ค์›Œ๋“œ: "๊ธฐ์กด ๋ฃจํŠธ", "๋งŒ๋“ค์–ด์ง„ ๋ฃจํŠธ", "๋ฃจํŠธ ์ฐพ์•„", "๋ฃจํŠธ ๊ฒ€์ƒ‰", "์žˆ๋Š” ๋ฃจํŠธ" - - ์˜ˆ์‹œ: "๊ธฐ์กด์— ๋งŒ๋“ค์–ด์ง„ ๋ถ€์‚ฐ ๋“œ๋ผ๋งˆ ๋ฃจํŠธ ์žˆ์–ด?", "๋งŒ๋“ค์–ด์ง„ K-POP ๋ฃจํŠธ ๋ณด์—ฌ์ค˜" - - 3. SEARCH_PLACES: ํŠน์ • ์žฅ์†Œ๋‚˜ ๋ช…์†Œ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ฐพ๋Š” ์š”์ฒญ - - ํ‚ค์›Œ๋“œ: "์žฅ์†Œ", "๋ช…์†Œ", "์–ด๋””", "์œ„์น˜", "๊ณณ", "์ฐพ์•„์ค˜", "๊ทผ์ฒ˜", "~์— ์žˆ๋Š”", "์ถ”์ฒœํ•ด์ค˜" (์žฅ์†Œ ์ฐพ๊ธฐ ๋งฅ๋ฝ์—์„œ) - - ์˜ˆ์‹œ: "ํ™๋Œ€ ๊ทผ์ฒ˜ K-POP ์žฅ์†Œ ์–ด๋”” ์žˆ์–ด?", "๋ช…๋™ ๋ง›์ง‘ ์ฐพ์•„์ค˜", "ํ™๋Œ€ ๊ทผ์ฒ˜ K-POP ์žฅ์†Œ ์ถ”์ฒœํ•ด์ค˜" - - 4. GENERAL_QUESTION: ํ•œ๋ฅ˜๋‚˜ ์—ฌํ–‰์— ๋Œ€ํ•œ ์ผ๋ฐ˜์ ์ธ ์งˆ๋ฌธ - - ํ‚ค์›Œ๋“œ: ์„ค๋ช… ์š”์ฒญ, ์ •๋ณด ์งˆ๋ฌธ - - ์˜ˆ์‹œ: "BTS๊ฐ€ ๋ญ์•ผ?", "K-POP์ด๋ž€?", "ํ•œ๋ฅ˜ ์—ญ์‚ฌ ์•Œ๋ ค์ค˜" - - JSON ํ˜•์‹์œผ๋กœ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”: - { - "intent": "CREATE_ROUTE", - "confidence": 0.95, - "reasoning": "์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ๋กœ์šด ์—ฌํ–‰ ๋ฃจํŠธ๋ฅผ ์š”์ฒญํ•จ" - } - """; + private IntentClassificationResult classifyWithLLM(String query, String language) { + String systemPrompt = getSystemPromptByLanguage(language); org.springframework.ai.chat.model.ChatResponse aiResponse = callOpenAi(query, systemPrompt); String responseText = aiResponse.getResult().getOutput().getText().trim(); @@ -91,45 +72,131 @@ private IntentClassificationResult classifyWithLLM(String query) { // ์‹ ๋ขฐ๋„ ๊ฒ€์ฆ (๋„ˆ๋ฌด ๋‚ฎ์œผ๋ฉด fallback) if (result.getConfidence() < 0.6) { log.warn("LLM ์˜๋„ ๋ถ„๋ฅ˜ ์‹ ๋ขฐ๋„๊ฐ€ ๋‚ฎ์Œ ({})... fallback ์‚ฌ์šฉ", result.getConfidence()); - return fallbackIntentClassification(query); + return fallbackIntentClassification(query, language); } log.info("LLM ์˜๋„ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ: {} (์‹ ๋ขฐ๋„: {})", result.getIntent(), result.getConfidence()); return result; } catch (JsonProcessingException e) { log.error("JSON ํŒŒ์‹ฑ ์‹คํŒจ, fallback ์‚ฌ์šฉ: {}", e.getMessage()); - return fallbackIntentClassification(query); + return fallbackIntentClassification(query, language); } } /** * ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ fallback ์˜๋„ ๋ถ„๋ฅ˜ */ - private IntentClassificationResult fallbackIntentClassification(String query) { + private IntentClassificationResult fallbackIntentClassification(String query, String language) { String lowerQuery = query.toLowerCase(); + // ์–ธ์–ด๋ณ„ ํ‚ค์›Œ๋“œ๋กœ ์˜๋„ ๋ถ„๋ฅ˜ + if (language == null) { + language = "ko"; + } + return switch (language) { + case "ko" -> fallbackClassificationKorean(lowerQuery); + case "en" -> fallbackClassificationEnglish(lowerQuery); + case "ja" -> fallbackClassificationJapanese(lowerQuery); + case "zh" -> fallbackClassificationChinese(lowerQuery); + default -> fallbackClassificationKorean(lowerQuery); // ๊ธฐ๋ณธ๊ฐ’ + }; + } + + private IntentClassificationResult fallbackClassificationKorean(String lowerQuery) { // CREATE_ROUTE ํ‚ค์›Œ๋“œ if (containsAnyKeyword(lowerQuery, "์ถ”์ฒœ", "๊ณ„ํš", "๋ฃจํŠธ ๋งŒ๋“ค", "์—ฌํ–‰ ๊ณ„ํš", "์ผ์ •", "๋งŒ๋“ค์–ด์ค˜")) { - return new IntentClassificationResult("CREATE_ROUTE", 0.8, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ๋ฃจํŠธ ์ƒ์„ฑ ์š”์ฒญ"); + return new IntentClassificationResult(IntentClassificationResult.UserIntent.CREATE_ROUTE, 0.8, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ๋ฃจํŠธ ์ƒ์„ฑ ์š”์ฒญ"); } // SEARCH_EXISTING_ROUTES ํ‚ค์›Œ๋“œ if (containsAnyKeyword(lowerQuery, "๊ธฐ์กด", "๋งŒ๋“ค์–ด์ง„", "๋ฃจํŠธ ์ฐพ", "๋ฃจํŠธ ๊ฒ€์ƒ‰", "์žˆ๋Š” ๋ฃจํŠธ", "๋งŒ๋“  ๋ฃจํŠธ")) { - return new IntentClassificationResult("SEARCH_EXISTING_ROUTES", 0.8, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ๊ธฐ์กด ๋ฃจํŠธ ๊ฒ€์ƒ‰"); + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_EXISTING_ROUTES, 0.8, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ๊ธฐ์กด ๋ฃจํŠธ ๊ฒ€์ƒ‰"); } // SEARCH_PLACES ํ‚ค์›Œ๋“œ (๊ทผ์ฒ˜ ํ‚ค์›Œ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์ฒ˜๋ฆฌ) if (containsAnyKeyword(lowerQuery, "๊ทผ์ฒ˜")) { - return new IntentClassificationResult("SEARCH_PLACES", 0.9, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ๊ทผ์ฒ˜ ์žฅ์†Œ ๊ฒ€์ƒ‰"); + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.9, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ๊ทผ์ฒ˜ ์žฅ์†Œ ๊ฒ€์ƒ‰"); } // SEARCH_PLACES ๊ธฐํƒ€ ํ‚ค์›Œ๋“œ if (containsAnyKeyword(lowerQuery, "์žฅ์†Œ", "๋ช…์†Œ", "์–ด๋””", "์œ„์น˜", "๊ณณ", "์ฐพ์•„")) { - return new IntentClassificationResult("SEARCH_PLACES", 0.8, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ์žฅ์†Œ ๊ฒ€์ƒ‰"); + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.8, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ์žฅ์†Œ ๊ฒ€์ƒ‰"); } // ๊ธฐ๋ณธ๊ฐ’: GENERAL_QUESTION - return new IntentClassificationResult("GENERAL_QUESTION", 0.7, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ์ผ๋ฐ˜ ์งˆ๋ฌธ"); + return new IntentClassificationResult(IntentClassificationResult.UserIntent.GENERAL_QUESTION, 0.7, "ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜: ์ผ๋ฐ˜ ์งˆ๋ฌธ"); + } + + private IntentClassificationResult fallbackClassificationEnglish(String lowerQuery) { + // CREATE_ROUTE keywords + if (containsAnyKeyword(lowerQuery, "recommend", "suggest", "plan", "create route", "make itinerary", "trip planning")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.CREATE_ROUTE, 0.8, "Keyword-based classification: Route creation request"); + } + + // SEARCH_EXISTING_ROUTES keywords + if (containsAnyKeyword(lowerQuery, "existing", "available routes", "find route", "search route", "show routes")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_EXISTING_ROUTES, 0.8, "Keyword-based classification: Existing route search"); + } + + // SEARCH_PLACES keywords + if (containsAnyKeyword(lowerQuery, "near", "nearby", "around")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.9, "Keyword-based classification: Nearby place search"); + } + + if (containsAnyKeyword(lowerQuery, "place", "location", "where", "find", "spot", "attraction")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.8, "Keyword-based classification: Place search"); + } + + // Default: GENERAL_QUESTION + return new IntentClassificationResult(IntentClassificationResult.UserIntent.GENERAL_QUESTION, 0.7, "Keyword-based classification: General question"); + } + + private IntentClassificationResult fallbackClassificationJapanese(String lowerQuery) { + // CREATE_ROUTE keywords + if (containsAnyKeyword(lowerQuery, "ใŠใ™ใ™ใ‚", "่จˆ็”ป", "ใƒซใƒผใƒˆไฝœ", "ๆ—…่กŒ่จˆ็”ป", "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ", "ไฝœใฃใฆ")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.CREATE_ROUTE, 0.8, "ใ‚ญใƒผใƒฏใƒผใƒ‰ใƒ™ใƒผใ‚นๅˆ†้กž: ใƒซใƒผใƒˆไฝœๆˆ่ฆๆฑ‚"); + } + + // SEARCH_EXISTING_ROUTES keywords + if (containsAnyKeyword(lowerQuery, "ๆ—ขๅญ˜", "ไฝœใ‚‰ใ‚ŒใŸ", "ใƒซใƒผใƒˆๆŽข", "ใƒซใƒผใƒˆๆคœ็ดข", "ใ‚ใ‚‹ใƒซใƒผใƒˆ")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_EXISTING_ROUTES, 0.8, "ใ‚ญใƒผใƒฏใƒผใƒ‰ใƒ™ใƒผใ‚นๅˆ†้กž: ๆ—ขๅญ˜ใƒซใƒผใƒˆๆคœ็ดข"); + } + + // SEARCH_PLACES keywords + if (containsAnyKeyword(lowerQuery, "่ฟ‘ใ", "ไป˜่ฟ‘")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.9, "ใ‚ญใƒผใƒฏใƒผใƒ‰ใƒ™ใƒผใ‚นๅˆ†้กž: ่ฟ‘ใใฎๅ ดๆ‰€ๆคœ็ดข"); + } + + if (containsAnyKeyword(lowerQuery, "ๅ ดๆ‰€", "ๅๆ‰€", "ใฉใ“", "ไฝ็ฝฎ", "ๆŽขใ—ใฆ", "ใ‚นใƒใƒƒใƒˆ")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.8, "ใ‚ญใƒผใƒฏใƒผใƒ‰ใƒ™ใƒผใ‚นๅˆ†้กž: ๅ ดๆ‰€ๆคœ็ดข"); + } + + // Default: GENERAL_QUESTION + return new IntentClassificationResult(IntentClassificationResult.UserIntent.GENERAL_QUESTION, 0.7, "ใ‚ญใƒผใƒฏใƒผใƒ‰ใƒ™ใƒผใ‚นๅˆ†้กž: ไธ€่ˆฌ็š„ใช่ณชๅ•"); + } + + private IntentClassificationResult fallbackClassificationChinese(String lowerQuery) { + // CREATE_ROUTE keywords + if (containsAnyKeyword(lowerQuery, "ๆŽจ่", "่ฎกๅˆ’", "่ทฏ็บฟๅˆถ", "ๆ—…่กŒ่ฎกๅˆ’", "่กŒ็จ‹", "ๅˆถไฝœ")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.CREATE_ROUTE, 0.8, "ๅŸบไบŽๅ…ณ้”ฎ่ฏ็š„ๅˆ†็ฑป: ่ทฏ็บฟๅˆ›ๅปบ่ฏทๆฑ‚"); + } + + // SEARCH_EXISTING_ROUTES keywords + if (containsAnyKeyword(lowerQuery, "็Žฐๆœ‰", "ๅทฒๅˆถไฝœ", "่ทฏ็บฟๆŸฅๆ‰พ", "่ทฏ็บฟๆœ็ดข", "็Žฐๆœ‰่ทฏ็บฟ")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_EXISTING_ROUTES, 0.8, "ๅŸบไบŽๅ…ณ้”ฎ่ฏ็š„ๅˆ†็ฑป: ็Žฐๆœ‰่ทฏ็บฟๆœ็ดข"); + } + + // SEARCH_PLACES keywords + if (containsAnyKeyword(lowerQuery, "้™„่ฟ‘", "ๅ‘จๅ›ด")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.9, "ๅŸบไบŽๅ…ณ้”ฎ่ฏ็š„ๅˆ†็ฑป: ้™„่ฟ‘ๅœฐ็‚นๆœ็ดข"); + } + + if (containsAnyKeyword(lowerQuery, "ๅœฐ็‚น", "ๆ™ฏ็‚น", "ๅ“ช้‡Œ", "ไฝ็ฝฎ", "ๆ‰พ", "ๅœฐๆ–น")) { + return new IntentClassificationResult(IntentClassificationResult.UserIntent.SEARCH_PLACES, 0.8, "ๅŸบไบŽๅ…ณ้”ฎ่ฏ็š„ๅˆ†็ฑป: ๅœฐ็‚นๆœ็ดข"); + } + + // Default: GENERAL_QUESTION + return new IntentClassificationResult(IntentClassificationResult.UserIntent.GENERAL_QUESTION, 0.7, "ๅŸบไบŽๅ…ณ้”ฎ่ฏ็š„ๅˆ†็ฑป: ไธ€่ˆฌ้—ฎ้ข˜"); } /** @@ -159,4 +226,140 @@ private org.springframework.ai.chat.model.ChatResponse callOpenAi(String userInp return chatModel.call(prompt); } + + /** + * ์–ธ์–ด๋ณ„ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private String getSystemPromptByLanguage(String language) { + if (language == null) { + language = "ko"; + } + return switch (language) { + case "ko" -> getKoreanSystemPrompt(); + case "en" -> getEnglishSystemPrompt(); + case "ja" -> getJapaneseSystemPrompt(); + case "zh" -> getChineseSystemPrompt(); + default -> getKoreanSystemPrompt(); // ๊ธฐ๋ณธ๊ฐ’ + }; + } + + private String getKoreanSystemPrompt() { + return """ + ๋‹น์‹ ์€ ํ•œ๋ฅ˜ ์—ฌํ–‰ ์ฑ—๋ด‡์˜ ์˜๋„ ๋ถ„๋ฅ˜ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. + ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์„ ๋‹ค์Œ 4๊ฐ€์ง€ ์˜๋„ ์ค‘ ํ•˜๋‚˜๋กœ ๋ถ„๋ฅ˜ํ•ด์ฃผ์„ธ์š”: + + 1. CREATE_ROUTE: ์ƒˆ๋กœ์šด ์—ฌํ–‰ ๋ฃจํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋‹ฌ๋ผ๋Š” ์š”์ฒญ + - ํ‚ค์›Œ๋“œ: "์ถ”์ฒœํ•ด์ค˜", "๊ณ„ํšํ•ด์ค˜", "๋ฃจํŠธ ๋งŒ๋“ค", "์—ฌํ–‰ ๊ณ„ํš", "์ผ์ • ์งœ์ค˜" + - ์˜ˆ์‹œ: "2์ผ ์„œ์šธ K-POP ๋ฃจํŠธ ์ถ”์ฒœํ•ด์ค˜", "๋ถ€์‚ฐ ์—ฌํ–‰ ๊ณ„ํšํ•ด์ค˜" + + 2. SEARCH_EXISTING_ROUTES: ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ๋ฃจํŠธ๋ฅผ ์ฐพ์•„๋‹ฌ๋ผ๋Š” ์š”์ฒญ + - ํ‚ค์›Œ๋“œ: "๊ธฐ์กด ๋ฃจํŠธ", "๋งŒ๋“ค์–ด์ง„ ๋ฃจํŠธ", "๋ฃจํŠธ ์ฐพ์•„", "๋ฃจํŠธ ๊ฒ€์ƒ‰", "์žˆ๋Š” ๋ฃจํŠธ" + - ์˜ˆ์‹œ: "๊ธฐ์กด์— ๋งŒ๋“ค์–ด์ง„ ๋ถ€์‚ฐ ๋“œ๋ผ๋งˆ ๋ฃจํŠธ ์žˆ์–ด?", "๋งŒ๋“ค์–ด์ง„ K-POP ๋ฃจํŠธ ๋ณด์—ฌ์ค˜" + + 3. SEARCH_PLACES: ํŠน์ • ์žฅ์†Œ๋‚˜ ๋ช…์†Œ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ฐพ๋Š” ์š”์ฒญ + - ํ‚ค์›Œ๋“œ: "์žฅ์†Œ", "๋ช…์†Œ", "์–ด๋””", "์œ„์น˜", "๊ณณ", "์ฐพ์•„์ค˜", "๊ทผ์ฒ˜" + - ์˜ˆ์‹œ: "ํ™๋Œ€ ๊ทผ์ฒ˜ K-POP ์žฅ์†Œ ์–ด๋”” ์žˆ์–ด?", "๋ช…๋™ ๋ง›์ง‘ ์ฐพ์•„์ค˜" + + 4. GENERAL_QUESTION: ํ•œ๋ฅ˜๋‚˜ ์—ฌํ–‰์— ๋Œ€ํ•œ ์ผ๋ฐ˜์ ์ธ ์งˆ๋ฌธ + - ํ‚ค์›Œ๋“œ: ์„ค๋ช… ์š”์ฒญ, ์ •๋ณด ์งˆ๋ฌธ + - ์˜ˆ์‹œ: "BTS๊ฐ€ ๋ญ์•ผ?", "K-POP์ด๋ž€?", "ํ•œ๋ฅ˜ ์—ญ์‚ฌ ์•Œ๋ ค์ค˜" + + JSON ํ˜•์‹์œผ๋กœ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”: + { + "intent": "CREATE_ROUTE", + "confidence": 0.95, + "reasoning": "์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ๋กœ์šด ์—ฌํ–‰ ๋ฃจํŠธ๋ฅผ ์š”์ฒญํ•จ" + } + """; + } + + private String getEnglishSystemPrompt() { + return """ + You are an intent classification expert for a Korean Wave (Hallyu) travel chatbot. + Please classify the user's question into one of the following 4 intents: + + 1. CREATE_ROUTE: Request to create a new travel route + - Keywords: "recommend", "suggest", "plan", "create route", "make itinerary" + - Examples: "Recommend a 2-day Seoul K-POP route", "Plan a Busan trip" + + 2. SEARCH_EXISTING_ROUTES: Request to find existing routes + - Keywords: "existing routes", "available routes", "find route", "search route" + - Examples: "Are there existing Busan drama routes?", "Show me K-POP routes" + + 3. SEARCH_PLACES: Request for information about specific places or attractions + - Keywords: "place", "location", "where", "spot", "find", "near", "around" + - Examples: "Where are K-POP places near Hongdae?", "Find restaurants in Myeongdong" + + 4. GENERAL_QUESTION: General questions about Korean Wave or travel + - Keywords: explanation requests, information questions + - Examples: "What is BTS?", "What is K-POP?", "Tell me about Hallyu history" + + Please respond in JSON format: + { + "intent": "CREATE_ROUTE", + "confidence": 0.95, + "reasoning": "User is requesting a new travel route" + } + """; + } + + private String getJapaneseSystemPrompt() { + return """ + ใ‚ใชใŸใฏ้Ÿ“ๆตๆ—…่กŒใƒใƒฃใƒƒใƒˆใƒœใƒƒใƒˆใฎๆ„ๅ›ณๅˆ†้กžๅฐ‚้–€ๅฎถใงใ™ใ€‚ + ใƒฆใƒผใ‚ถใƒผใฎ่ณชๅ•ใ‚’ไปฅไธ‹ใฎ4ใคใฎๆ„ๅ›ณใฎใ„ใšใ‚Œใ‹ใซๅˆ†้กžใ—ใฆใใ ใ•ใ„๏ผš + + 1. CREATE_ROUTE: ๆ–ฐใ—ใ„ๆ—…่กŒใƒซใƒผใƒˆใ‚’ไฝœใฃใฆใปใ—ใ„ใจใ„ใ†่ฆๆฑ‚ + - ใ‚ญใƒผใƒฏใƒผใƒ‰: "ใŠใ™ใ™ใ‚", "่จˆ็”ป", "ใƒซใƒผใƒˆไฝœๆˆ", "ๆ—…่กŒ่จˆ็”ป", "ใ‚นใ‚ฑใ‚ธใƒฅใƒผใƒซ" + - ไพ‹: "2ๆ—ฅ้–“ใฎใ‚ฝใ‚ฆใƒซK-POPใƒซใƒผใƒˆใ‚’ใŠใ™ใ™ใ‚ใ—ใฆ", "้‡œๅฑฑๆ—…่กŒใ‚’่จˆ็”ปใ—ใฆ" + + 2. SEARCH_EXISTING_ROUTES: ๆ—ขๅญ˜ใฎใƒซใƒผใƒˆใ‚’ๆŽขใ—ใฆใปใ—ใ„ใจใ„ใ†่ฆๆฑ‚ + - ใ‚ญใƒผใƒฏใƒผใƒ‰: "ๆ—ขๅญ˜ใƒซใƒผใƒˆ", "ไฝœใ‚‰ใ‚ŒใŸใƒซใƒผใƒˆ", "ใƒซใƒผใƒˆๆคœ็ดข", "ใ‚ใ‚‹ใƒซใƒผใƒˆ" + - ไพ‹: "้‡œๅฑฑใƒ‰ใƒฉใƒžใƒซใƒผใƒˆใฏใ‚ใ‚Šใพใ™ใ‹๏ผŸ", "K-POPใƒซใƒผใƒˆใ‚’่ฆ‹ใ›ใฆ" + + 3. SEARCH_PLACES: ็‰นๅฎšใฎๅ ดๆ‰€ใ‚„่ฆณๅ…‰ๅœฐใซใคใ„ใฆใฎๆƒ…ๅ ฑใ‚’ๆŽขใ™่ฆๆฑ‚ + - ใ‚ญใƒผใƒฏใƒผใƒ‰: "ๅ ดๆ‰€", "่ฆณๅ…‰ๅœฐ", "ใฉใ“", "ไฝ็ฝฎ", "ๆŽขใ—ใฆ", "่ฟ‘ใ", "ไป˜่ฟ‘" + - ไพ‹: "ๅผ˜ๅคง่ฟ‘ใใฎK-POPๅ ดๆ‰€ใฏใฉใ“๏ผŸ", "ๆ˜Žๆดžใฎใƒฌใ‚นใƒˆใƒฉใƒณใ‚’ๆŽขใ—ใฆ" + + 4. GENERAL_QUESTION: ้Ÿ“ๆตใ‚„ๆ—…่กŒใซ้–ขใ™ใ‚‹ไธ€่ˆฌ็š„ใช่ณชๅ• + - ใ‚ญใƒผใƒฏใƒผใƒ‰: ่ชฌๆ˜Ž่ฆๆฑ‚, ๆƒ…ๅ ฑ่ณชๅ• + - ไพ‹: "BTSใฃใฆไฝ•๏ผŸ", "K-POPใจใฏ๏ผŸ", "้Ÿ“ๆตใฎๆญดๅฒใ‚’ๆ•™ใˆใฆ" + + JSONๅฝขๅผใงๅฟœ็ญ”ใ—ใฆใใ ใ•ใ„๏ผš + { + "intent": "CREATE_ROUTE", + "confidence": 0.95, + "reasoning": "ใƒฆใƒผใ‚ถใƒผใŒๆ–ฐใ—ใ„ๆ—…่กŒใƒซใƒผใƒˆใ‚’่ฆๆฑ‚ใ—ใฆใ„ใ‚‹" + } + """; + } + + private String getChineseSystemPrompt() { + return """ + ๆ‚จๆ˜ฏ้Ÿฉๆตๆ—…่กŒ่Šๅคฉๆœบๅ™จไบบ็š„ๆ„ๅ›พๅˆ†็ฑปไธ“ๅฎถใ€‚ + ่ฏทๅฐ†็”จๆˆท็š„้—ฎ้ข˜ๅˆ†็ฑปไธบไปฅไธ‹4ไธชๆ„ๅ›พไน‹ไธ€๏ผš + + 1. CREATE_ROUTE: ่ฏทๆฑ‚ๅˆ›ๅปบๆ–ฐ็š„ๆ—…่กŒ่ทฏ็บฟ + - ๅ…ณ้”ฎ่ฏ: "ๆŽจ่", "ๅปบ่ฎฎ", "่ฎกๅˆ’", "ๅˆ›ๅปบ่ทฏ็บฟ", "ๅˆถไฝœ่กŒ็จ‹" + - ็คบไพ‹: "ๆŽจ่2ๅคฉ้ฆ–ๅฐ”K-POP่ทฏ็บฟ", "่ฎกๅˆ’้‡œๅฑฑๆ—…่กŒ" + + 2. SEARCH_EXISTING_ROUTES: ่ฏทๆฑ‚ๆŸฅๆ‰พ็Žฐๆœ‰่ทฏ็บฟ + - ๅ…ณ้”ฎ่ฏ: "็Žฐๆœ‰่ทฏ็บฟ", "ๅทฒๆœ‰่ทฏ็บฟ", "ๆŸฅๆ‰พ่ทฏ็บฟ", "ๆœ็ดข่ทฏ็บฟ" + - ็คบไพ‹: "ๆœ‰้‡œๅฑฑ้Ÿฉๅ‰ง่ทฏ็บฟๅ—๏ผŸ", "ๆ˜พ็คบK-POP่ทฏ็บฟ" + + 3. SEARCH_PLACES: ่ฏทๆฑ‚็‰นๅฎšๅœฐ็‚นๆˆ–ๆ™ฏ็‚นไฟกๆฏ + - ๅ…ณ้”ฎ่ฏ: "ๅœฐ็‚น", "ๆ™ฏ็‚น", "ๅ“ช้‡Œ", "ไฝ็ฝฎ", "ๆŸฅๆ‰พ", "้™„่ฟ‘", "ๅ‘จๅ›ด" + - ็คบไพ‹: "ๅผ˜ๅคง้™„่ฟ‘็š„K-POPๅœฐ็‚นๅœจๅ“ช๏ผŸ", "ๆ‰พๆ˜Žๆดž้คๅŽ…" + + 4. GENERAL_QUESTION: ๅ…ณไบŽ้Ÿฉๆตๆˆ–ๆ—…่กŒ็š„ไธ€่ˆฌ้—ฎ้ข˜ + - ๅ…ณ้”ฎ่ฏ: ่งฃ้‡Š่ฏทๆฑ‚, ไฟกๆฏ่ฏข้—ฎ + - ็คบไพ‹: "BTSๆ˜ฏไป€ไนˆ๏ผŸ", "ไป€ไนˆๆ˜ฏK-POP๏ผŸ", "ๅ‘Š่ฏ‰ๆˆ‘้Ÿฉๆตๅކๅฒ" + + ่ฏทไปฅJSONๆ ผๅผๅ›ž็ญ”๏ผš + { + "intent": "CREATE_ROUTE", + "confidence": 0.95, + "reasoning": "็”จๆˆท่ฏทๆฑ‚ๅˆ›ๅปบๆ–ฐ็š„ๆ—…่กŒ่ทฏ็บฟ" + } + """; + } } diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/LanguageService.java b/src/main/java/com/mey/backend/domain/chatbot/service/LanguageService.java new file mode 100644 index 0000000..d6cb341 --- /dev/null +++ b/src/main/java/com/mey/backend/domain/chatbot/service/LanguageService.java @@ -0,0 +1,131 @@ +package com.mey.backend.domain.chatbot.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Set; + +/** + * ๋‹ค๊ตญ์–ด ์ง€์›์„ ์œ„ํ•œ ์–ธ์–ด ์ฒ˜๋ฆฌ ์„œ๋น„์Šค + * + * ์ฃผ์š” ์ฑ…์ž„: + * - ์ง€์› ์–ธ์–ด ๊ฒ€์ฆ ๋ฐ fallback ์ฒ˜๋ฆฌ + * - ์–ธ์–ด๋ณ„ ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋งคํ•‘ + * - ์–ธ์–ด ์„ค์ • ๊ด€๋ฆฌ + */ +@Slf4j +@Service +public class LanguageService { + + private static final Set SUPPORTED_LANGUAGES = Set.of("ko", "en", "ja", "zh"); + private static final String DEFAULT_FALLBACK_LANGUAGE = "en"; + + @Value("${chatbot.language.fallback:en}") + private String fallbackLanguage; + + /** + * ์–ธ์–ด ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ์ง€์›๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ fallback ์–ธ์–ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String validateAndGetLanguage(String requestedLanguage) { + if (requestedLanguage == null || requestedLanguage.trim().isEmpty()) { + return "ko"; // ๊ธฐ๋ณธ ์–ธ์–ด + } + + String normalizedLanguage = requestedLanguage.toLowerCase().trim(); + + if (SUPPORTED_LANGUAGES.contains(normalizedLanguage)) { + return normalizedLanguage; + } + + log.info("์ง€์›๋˜์ง€ ์•Š๋Š” ์–ธ์–ด '{}' ์š”์ฒญ, fallback ์–ธ์–ด '{}'๋กœ ์ฒ˜๋ฆฌ", requestedLanguage, fallbackLanguage); + return fallbackLanguage; + } + + /** + * Place ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์–ธ์–ด์˜ ์ด๋ฆ„์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + */ + public String getPlaceName(com.mey.backend.domain.place.entity.Place place, String language) { + return switch (language) { + case "ko" -> place.getNameKo(); + case "en" -> place.getNameEn() != null ? place.getNameEn() : place.getNameKo(); + case "ja", "zh" -> { + // ์ผ๋ณธ์–ด/์ค‘๊ตญ์–ด๋Š” ์•„์ง ์ง€์›๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์˜์–ด๋กœ fallback + String englishName = place.getNameEn(); + yield englishName != null ? englishName : place.getNameKo(); + } + default -> place.getNameKo(); + }; + } + + /** + * Place ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์–ธ์–ด์˜ ์„ค๋ช…์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + */ + public String getPlaceDescription(com.mey.backend.domain.place.entity.Place place, String language) { + return switch (language) { + case "ko" -> place.getDescriptionKo(); + case "en" -> place.getDescriptionEn() != null ? place.getDescriptionEn() : place.getDescriptionKo(); + case "ja", "zh" -> { + // ์ผ๋ณธ์–ด/์ค‘๊ตญ์–ด๋Š” ์•„์ง ์ง€์›๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์˜์–ด๋กœ fallback + String englishDesc = place.getDescriptionEn(); + yield englishDesc != null ? englishDesc : place.getDescriptionKo(); + } + default -> place.getDescriptionKo(); + }; + } + + /** + * Place ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์–ธ์–ด์˜ ์ฃผ์†Œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * ํ˜„์žฌ๋Š” ํ•œ๊ตญ์–ด ์ฃผ์†Œ๋งŒ ์ง€์›ํ•˜๋ฏ€๋กœ ๋ชจ๋“  ์–ธ์–ด์— ๋Œ€ํ•ด ํ•œ๊ตญ์–ด ์ฃผ์†Œ๋ฅผ ๋ฐ˜ํ™˜ + */ + public String getPlaceAddress(com.mey.backend.domain.place.entity.Place place, String language) { + return place.getAddressKo(); // ํ˜„์žฌ๋Š” ํ•œ๊ตญ์–ด ์ฃผ์†Œ๋งŒ ์ง€์› + } + + /** + * Route ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์–ธ์–ด์˜ ์ œ๋ชฉ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + */ + public String getRouteTitle(com.mey.backend.domain.route.entity.Route route, String language) { + return switch (language) { + case "ko" -> route.getTitleKo(); + case "en" -> route.getTitleEn() != null ? route.getTitleEn() : route.getTitleKo(); + case "ja", "zh" -> { + // ์ผ๋ณธ์–ด/์ค‘๊ตญ์–ด๋Š” ์•„์ง ์ง€์›๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์˜์–ด๋กœ fallback + String englishTitle = route.getTitleEn(); + yield englishTitle != null ? englishTitle : route.getTitleKo(); + } + default -> route.getTitleKo(); + }; + } + + /** + * Route ๋ฐ์ดํ„ฐ์—์„œ ํ•ด๋‹น ์–ธ์–ด์˜ ์„ค๋ช…์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + */ + public String getRouteDescription(com.mey.backend.domain.route.entity.Route route, String language) { + return switch (language) { + case "ko" -> route.getDescriptionKo(); + case "en" -> route.getDescriptionEn() != null ? route.getDescriptionEn() : route.getDescriptionKo(); + case "ja", "zh" -> { + // ์ผ๋ณธ์–ด/์ค‘๊ตญ์–ด๋Š” ์•„์ง ์ง€์›๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์˜์–ด๋กœ fallback + String englishDesc = route.getDescriptionEn(); + yield englishDesc != null ? englishDesc : route.getDescriptionKo(); + } + default -> route.getDescriptionKo(); + }; + } + + /** + * ์ง€์›๋˜๋Š” ์–ธ์–ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + public boolean isLanguageSupported(String language) { + return SUPPORTED_LANGUAGES.contains(language); + } + + /** + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์™„์ „ํžˆ ์ง€์›๋˜๋Š” ์–ธ์–ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * (ํ˜„์žฌ๋Š” ํ•œ๊ตญ์–ด์™€ ์˜์–ด๋งŒ ์™„์ „ ์ง€์›) + */ + public boolean isLanguageFullySupported(String language) { + return "ko".equals(language) || "en".equals(language); + } +} \ No newline at end of file diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/MessageTemplateService.java b/src/main/java/com/mey/backend/domain/chatbot/service/MessageTemplateService.java new file mode 100644 index 0000000..e1a3192 --- /dev/null +++ b/src/main/java/com/mey/backend/domain/chatbot/service/MessageTemplateService.java @@ -0,0 +1,212 @@ +package com.mey.backend.domain.chatbot.service; + +import com.mey.backend.domain.chatbot.dto.ConversationState; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * ๋‹ค๊ตญ์–ด ๋ฉ”์‹œ์ง€ ํ…œํ”Œ๋ฆฟ ๊ด€๋ฆฌ ์„œ๋น„์Šค + * + * ์ฃผ์š” ์ฑ…์ž„: + * - ์–ธ์–ด๋ณ„ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ๋ฐ ์‚ฌ์šฉ์ž ์‘๋‹ต ํ…œํ”Œ๋ฆฟ ๊ด€๋ฆฌ + * - ๋Œ€ํ™” ์ƒํƒœ๋ณ„ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ์ œ๊ณต + * - ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ ํ™•์ธ ๋ฉ”์‹œ์ง€ ๋‹ค๊ตญ์–ด ์ง€์› + */ +@Slf4j +@Service +public class MessageTemplateService { + + // ํ•„์ˆ˜ ์ •๋ณด ๋ถ€์กฑ ์‹œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ + private static final Map> MISSING_INFO_MESSAGES = Map.of( + "theme", Map.of( + "ko", "์–ด๋–ค ํ…Œ๋งˆ์˜ ๋ฃจํŠธ๋ฅผ ์ฐพ๊ณ  ๊ณ„์‹ ๊ฐ€์š”? (K-POP, K-๋“œ๋ผ๋งˆ, K-ํ‘ธ๋“œ, K-ํŒจ์…˜ ์ค‘ ์„ ํƒํ•ด์ฃผ์„ธ์š”)", + "en", "What theme are you looking for? (Please choose from K-POP, K-Drama, K-Food, K-Fashion)", + "ja", "ใฉใฎใ‚ˆใ†ใชใƒ†ใƒผใƒžใ‚’ใŠๆŽขใ—ใงใ™ใ‹๏ผŸ๏ผˆK-POPใ€K-ใƒ‰ใƒฉใƒžใ€K-ใƒ•ใƒผใƒ‰ใ€K-ใƒ•ใ‚กใƒƒใ‚ทใƒงใƒณใ‹ใ‚‰ใŠ้ธใณใใ ใ•ใ„๏ผ‰", + "zh", "ๆ‚จๅœจๅฏปๆ‰พไป€ไนˆไธป้ข˜๏ผŸ๏ผˆ่ฏทไปŽK-POPใ€K-Dramaใ€K-Foodใ€K-Fashionไธญ้€‰ๆ‹ฉ๏ผ‰" + ), + "region", Map.of( + "ko", "์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”? (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ)", + "en", "Which region would you like to visit? (e.g., Seoul, Busan)", + "ja", "ใฉใกใ‚‰ใฎๅœฐๅŸŸใ‚’ใ”ๅธŒๆœ›ใงใ™ใ‹๏ผŸ๏ผˆไพ‹๏ผšใ‚ฝใ‚ฆใƒซใ€้‡œๅฑฑ๏ผ‰", + "zh", "ๆ‚จๅธŒๆœ›ๅŽปๅ“ชไธชๅœฐๅŒบ๏ผŸ๏ผˆไพ‹ๅฆ‚๏ผš้ฆ–ๅฐ”ใ€้‡œๅฑฑ๏ผ‰" + ), + "days", Map.of( + "ko", "๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”? (์˜ˆ: 1์ผ, 2์ผ, 3์ผ)", + "en", "How many days are you planning to travel? (e.g., 1 day, 2 days, 3 days)", + "ja", "ไฝ•ๆ—ฅ้–“ใฎๆ—…่กŒใ‚’่จˆ็”ปใ—ใฆใ„ใพใ™ใ‹๏ผŸ๏ผˆไพ‹๏ผš1ๆ—ฅใ€2ๆ—ฅใ€3ๆ—ฅ๏ผ‰", + "zh", "ๆ‚จ่ฎกๅˆ’ๆ—…่กŒๅ‡ ๅคฉ๏ผŸ๏ผˆไพ‹ๅฆ‚๏ผš1ๅคฉใ€2ๅคฉใ€3ๅคฉ๏ผ‰" + ) + ); + + // ๋Œ€ํ™” ์ƒํƒœ๋ณ„ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ + private static final Map> STATE_MESSAGES = Map.of( + ConversationState.AWAITING_THEME, Map.of( + "ko", "ํ…Œ๋งˆ ์„ ํƒ์ด ํ•„์š”ํ•ด์š”. K-POP, K-๋“œ๋ผ๋งˆ, K-ํ‘ธ๋“œ, K-ํŒจ์…˜ ์ค‘ ์–ด๋–ค ํ…Œ๋งˆ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”?", + "en", "Theme selection is needed. Which theme would you like: K-POP, K-Drama, K-Food, or K-Fashion?", + "ja", "ใƒ†ใƒผใƒžใฎ้ธๆŠžใŒๅฟ…่ฆใงใ™ใ€‚K-POPใ€K-ใƒ‰ใƒฉใƒžใ€K-ใƒ•ใƒผใƒ‰ใ€K-ใƒ•ใ‚กใƒƒใ‚ทใƒงใƒณใฎใฉใฎใƒ†ใƒผใƒžใ‚’ใ”ๅธŒๆœ›ใงใ™ใ‹๏ผŸ", + "zh", "้œ€่ฆ้€‰ๆ‹ฉไธป้ข˜ใ€‚ๆ‚จๅธŒๆœ›้€‰ๆ‹ฉๅ“ชไธชไธป้ข˜๏ผšK-POPใ€K-Dramaใ€K-Food่ฟ˜ๆ˜ฏK-Fashion๏ผŸ" + ), + ConversationState.AWAITING_REGION, Map.of( + "ko", "์–ด๋А ์ง€์—ญ์„ ์—ฌํ–‰ํ•˜๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”?", + "en", "Which region would you like to travel to?", + "ja", "ใฉใกใ‚‰ใฎๅœฐๅŸŸใ‚’ๆ—…่กŒใ—ใŸใ„ใงใ™ใ‹๏ผŸ", + "zh", "ๆ‚จๆƒณๅŽปๅ“ชไธชๅœฐๅŒบๆ—…่กŒ๏ผŸ" + ), + ConversationState.AWAITING_DAYS, Map.of( + "ko", "๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”?", + "en", "How many days are you planning to travel?", + "ja", "ไฝ•ๆ—ฅ้–“ใฎๆ—…่กŒใ‚’่จˆ็”ปใ—ใฆใ„ใพใ™ใ‹๏ผŸ", + "zh", "ๆ‚จ่ฎกๅˆ’ๆ—…่กŒๅ‡ ๅคฉ๏ผŸ" + ), + ConversationState.READY_FOR_ROUTE, Map.of( + "ko", "๋ชจ๋“  ์ •๋ณด๊ฐ€ ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๋งž์ถค ๋ฃจํŠธ๋ฅผ ์ƒ์„ฑํ•ด๋“œ๋ฆด๊ฒŒ์š”.", + "en", "All information is ready! I'll create a customized route for you.", + "ja", "ใ™ในใฆใฎๆƒ…ๅ ฑใŒๆบ–ๅ‚™ใงใใพใ—ใŸ๏ผใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บใ•ใ‚ŒใŸใƒซใƒผใƒˆใ‚’ไฝœๆˆใ„ใŸใ—ใพใ™ใ€‚", + "zh", "ๆ‰€ๆœ‰ไฟกๆฏ้ƒฝๅทฒๅ‡†ๅค‡ๅฐฑ็ปช๏ผๆˆ‘ๅฐ†ไธบๆ‚จๅˆ›ๅปบๅฎšๅˆถ่ทฏ็บฟใ€‚" + ) + ); + + // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + private static final Map ERROR_MESSAGES = Map.of( + "ko", "๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์‹œ๊ฒ ์–ด์š”?", + "en", "Would you like to try again?", + "ja", "ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใ„ใŸใ ใ‘ใพใ™ใ‹๏ผŸ", + "zh", "ๆ‚จๆ„ฟๆ„ๅ†่ฏ•ไธ€ๆฌกๅ—๏ผŸ" + ); + + // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ ๋ฉ”์‹œ์ง€ + private static final Map NO_RESULTS_MESSAGES = Map.of( + "ko", "์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์กฐ๊ฑด์œผ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”?", + "en", "No information found matching your criteria. Would you like to try different conditions?", + "ja", "ใŠๅฎขๆง˜ใฎๆกไปถใซๅˆใ†ๆƒ…ๅ ฑใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใงใ—ใŸใ€‚ไป–ใฎๆกไปถใงๆคœ็ดขใ—ใฆใฟใพใ›ใ‚“ใ‹๏ผŸ", + "zh", "ๆœชๆ‰พๅˆฐ็ฌฆๅˆๆ‚จๆกไปถ็š„ไฟกๆฏใ€‚ๆ‚จๆƒณๅฐ่ฏ•ๅ…ถไป–ๆกไปถๅ—๏ผŸ" + ); + + // ํ…Œ๋งˆ ํ™•์ธ ๋ฉ”์‹œ์ง€ + private static final Map THEME_CONFIRMATION_TEMPLATES = Map.of( + "ko", "์ข‹์Šต๋‹ˆ๋‹ค! {theme} ํ…Œ๋งˆ๋ฅผ ์„ ํƒํ•˜์…จ๋„ค์š”. ์–ด๋А ์ง€์—ญ์˜ ๋ฃจํŠธ๋ฅผ ์›ํ•˜์‹œ๋‚˜์š”? (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ)", + "en", "Great! You've selected the {theme} theme. Which region would you like to visit? (e.g., Seoul, Busan)", + "ja", "็ด ๆ™ดใ‚‰ใ—ใ„๏ผ{theme}ใƒ†ใƒผใƒžใ‚’้ธๆŠžใ•ใ‚Œใพใ—ใŸใญใ€‚ใฉใกใ‚‰ใฎๅœฐๅŸŸใ‚’ใ”ๅธŒๆœ›ใงใ™ใ‹๏ผŸ๏ผˆไพ‹๏ผšใ‚ฝใ‚ฆใƒซใ€้‡œๅฑฑ๏ผ‰", + "zh", "ๅคชๅฅฝไบ†๏ผๆ‚จ้€‰ๆ‹ฉไบ†{theme}ไธป้ข˜ใ€‚ๆ‚จๅธŒๆœ›ๅŽปๅ“ชไธชๅœฐๅŒบ๏ผŸ๏ผˆไพ‹ๅฆ‚๏ผš้ฆ–ๅฐ”ใ€้‡œๅฑฑ๏ผ‰" + ); + + // ์ง€์—ญ ํ™•์ธ ๋ฉ”์‹œ์ง€ + private static final Map REGION_CONFIRMATION_TEMPLATES = Map.of( + "ko", "{region} ์ง€์—ญ์„ ์„ ํƒํ•˜์…จ๋„ค์š”! ๋ช‡ ์ผ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”? (์˜ˆ: 1์ผ, 2์ผ, 3์ผ)", + "en", "You've selected {region}! How many days are you planning to travel? (e.g., 1 day, 2 days, 3 days)", + "ja", "{region}ใ‚’้ธๆŠžใ•ใ‚Œใพใ—ใŸใญ๏ผไฝ•ๆ—ฅ้–“ใฎๆ—…่กŒใ‚’่จˆ็”ปใ—ใฆใ„ใพใ™ใ‹๏ผŸ๏ผˆไพ‹๏ผš1ๆ—ฅใ€2ๆ—ฅใ€3ๆ—ฅ๏ผ‰", + "zh", "ๆ‚จ้€‰ๆ‹ฉไบ†{region}๏ผๆ‚จ่ฎกๅˆ’ๆ—…่กŒๅ‡ ๅคฉ๏ผŸ๏ผˆไพ‹ๅฆ‚๏ผš1ๅคฉใ€2ๅคฉใ€3ๅคฉ๏ผ‰" + ); + + // ์ธ์‹ ์‹คํŒจ ๋ฉ”์‹œ์ง€ + private static final Map> RECOGNITION_FAILURE_MESSAGES = Map.of( + "theme", Map.of( + "ko", "ํ…Œ๋งˆ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. K-POP, K-๋“œ๋ผ๋งˆ, K-ํ‘ธ๋“œ, K-ํŒจ์…˜ ์ค‘์—์„œ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + "en", "I couldn't recognize the theme. Please choose from K-POP, K-Drama, K-Food, K-Fashion.", + "ja", "ใƒ†ใƒผใƒžใ‚’่ช่ญ˜ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚K-POPใ€K-ใƒ‰ใƒฉใƒžใ€K-ใƒ•ใƒผใƒ‰ใ€K-ใƒ•ใ‚กใƒƒใ‚ทใƒงใƒณใ‹ใ‚‰ใŠ้ธใณใใ ใ•ใ„ใ€‚", + "zh", "ๆ— ๆณ•่ฏ†ๅˆซไธป้ข˜ใ€‚่ฏทไปŽK-POPใ€K-Dramaใ€K-Foodใ€K-Fashionไธญ้€‰ๆ‹ฉใ€‚" + ), + "region", Map.of( + "ko", "์ง€์—ญ์„ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ์ฒด์ ์ธ ์ง€์—ญ๋ช…์„ ๋ง์”€ํ•ด์ฃผ์„ธ์š”. (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ, ์ œ์ฃผ๋„)", + "en", "I couldn't recognize the region. Please provide a specific region name. (e.g., Seoul, Busan, Jeju)", + "ja", "ๅœฐๅŸŸใ‚’่ช่ญ˜ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ๅ…ทไฝ“็š„ใชๅœฐๅŸŸๅใ‚’ๆ•™ใˆใฆใใ ใ•ใ„ใ€‚๏ผˆไพ‹๏ผšใ‚ฝใ‚ฆใƒซใ€้‡œๅฑฑใ€ๆธˆๅทžๅณถ๏ผ‰", + "zh", "ๆ— ๆณ•่ฏ†ๅˆซๅœฐๅŒบใ€‚่ฏทๆไพ›ๅ…ทไฝ“็š„ๅœฐๅŒบๅ็งฐใ€‚๏ผˆไพ‹ๅฆ‚๏ผš้ฆ–ๅฐ”ใ€้‡œๅฑฑใ€ๆตŽๅทžๅฒ›๏ผ‰" + ), + "days", Map.of( + "ko", "์ผ์ˆ˜๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆซ์ž๋กœ ๋ง์”€ํ•ด์ฃผ์„ธ์š”. (์˜ˆ: 1์ผ, 2์ผ, 3์ผ)", + "en", "I couldn't recognize the number of days. Please provide a number. (e.g., 1 day, 2 days, 3 days)", + "ja", "ๆ—ฅๆ•ฐใ‚’่ช่ญ˜ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚ๆ•ฐๅญ—ใงๆ•™ใˆใฆใใ ใ•ใ„ใ€‚๏ผˆไพ‹๏ผš1ๆ—ฅใ€2ๆ—ฅใ€3ๆ—ฅ๏ผ‰", + "zh", "ๆ— ๆณ•่ฏ†ๅˆซๅคฉๆ•ฐใ€‚่ฏท็”จๆ•ฐๅญ—ๅ‘Š่ฏ‰ๆˆ‘ใ€‚๏ผˆไพ‹ๅฆ‚๏ผš1ๅคฉใ€2ๅคฉใ€3ๅคฉ๏ผ‰" + ) + ); + + // ์ผ๋ฐ˜ ์ •๋ณด ์‘๋‹ต ์‹œ์ž‘ ๋ฌธ๊ตฌ + private static final Map PLACE_INFO_HEADERS = Map.of( + "ko", "๊ฒ€์ƒ‰ํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ์žฅ์†Œ๋“ค์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค:", + "en", "I found places matching your search criteria:", + "ja", "ใŠๅฎขๆง˜ใฎๆคœ็ดขๆกไปถใซๅˆใ†ๅ ดๆ‰€ใ‚’่ฆ‹ใคใ‘ใพใ—ใŸ๏ผš", + "zh", "ๆˆ‘ๆ‰พๅˆฐไบ†็ฌฆๅˆๆ‚จๆœ็ดขๆกไปถ็š„ๅœฐ็‚น๏ผš" + ); + + /** + * ํ•„์ˆ˜ ์ •๋ณด ๋ถ€์กฑ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getMissingInfoMessage(String infoType, String language) { + // null ์ฒดํฌ + if (infoType == null || language == null) { + return MISSING_INFO_MESSAGES.get("theme").get("ko"); // ๊ธฐ๋ณธ๊ฐ’ + } + + Map messages = MISSING_INFO_MESSAGES.get(infoType); + if (messages == null) { + return MISSING_INFO_MESSAGES.get("theme").get("ko"); // ๊ธฐ๋ณธ๊ฐ’ + } + + return messages.getOrDefault(language, messages.get("ko")); + } + + /** + * ๋Œ€ํ™” ์ƒํƒœ๋ณ„ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getStateMessage(ConversationState state, String language) { + Map messages = STATE_MESSAGES.get(state); + if (messages == null) { + return ""; + } + + return messages.getOrDefault(language, messages.get("ko")); + } + + /** + * ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getErrorSuffix(String language) { + return ERROR_MESSAGES.getOrDefault(language, ERROR_MESSAGES.get("ko")); + } + + /** + * ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getNoResultsMessage(String language) { + return NO_RESULTS_MESSAGES.getOrDefault(language, NO_RESULTS_MESSAGES.get("ko")); + } + + /** + * ํ…Œ๋งˆ ํ™•์ธ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getThemeConfirmationMessage(String theme, String language) { + String template = THEME_CONFIRMATION_TEMPLATES.getOrDefault(language, + THEME_CONFIRMATION_TEMPLATES.get("ko")); + return template.replace("{theme}", theme); + } + + /** + * ์ง€์—ญ ํ™•์ธ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getRegionConfirmationMessage(String region, String language) { + String template = REGION_CONFIRMATION_TEMPLATES.getOrDefault(language, + REGION_CONFIRMATION_TEMPLATES.get("ko")); + return template.replace("{region}", region); + } + + /** + * ์ธ์‹ ์‹คํŒจ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getRecognitionFailureMessage(String infoType, String language) { + Map messages = RECOGNITION_FAILURE_MESSAGES.get(infoType); + if (messages == null) { + return RECOGNITION_FAILURE_MESSAGES.get("theme").get("ko"); // ๊ธฐ๋ณธ๊ฐ’ + } + + return messages.getOrDefault(language, messages.get("ko")); + } + + /** + * ์žฅ์†Œ ์ •๋ณด ํ—ค๋” ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + public String getPlaceInfoHeader(String language) { + return PLACE_INFO_HEADERS.getOrDefault(language, PLACE_INFO_HEADERS.get("ko")); + } +} \ No newline at end of file diff --git a/src/main/java/com/mey/backend/domain/chatbot/service/RagService.java b/src/main/java/com/mey/backend/domain/chatbot/service/RagService.java index 94857dc..d85f393 100644 --- a/src/main/java/com/mey/backend/domain/chatbot/service/RagService.java +++ b/src/main/java/com/mey/backend/domain/chatbot/service/RagService.java @@ -50,12 +50,19 @@ public List retrieve(String question, int maxResults) { * @return ์ถœ์ฒ˜ ์ •๋ณด๊ฐ€ ์ œ๊ฑฐ๋œ ๋ฃจํŠธ ์ถ”์ฒœ ์‘๋‹ต */ public String generateRouteRecommendationAnswer(String question, List relevantDocs) { - log.debug("๋ฃจํŠธ ์ถ”์ฒœ ์‘๋‹ต ์ƒ์„ฑ ์‹œ์ž‘: '{}'", question); + return generateRouteRecommendationAnswer(question, relevantDocs, "ko"); // ๊ธฐ๋ณธ ํ•œ๊ตญ์–ด + } + + /** + * ์–ธ์–ด๋ฅผ ๊ณ ๋ คํ•œ ํ•œ๋ฅ˜ ๋ฃจํŠธ ์ถ”์ฒœ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + public String generateRouteRecommendationAnswer(String question, List relevantDocs, String language) { + log.debug("๋ฃจํŠธ ์ถ”์ฒœ ์‘๋‹ต ์ƒ์„ฑ ์‹œ์ž‘: '{}' (์–ธ์–ด: {})", question, language); // ๊ด€๋ จ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋˜๋Š” ์‚ฌ์šฉ if (relevantDocs.isEmpty()) { log.info("๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: '{}'", question); - return "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ๋ฃจํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ๋งˆ๋‚˜ ์ง€์—ญ์„ ์‹œ๋„ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”?"; + return getNoResultsMessage(language); } // ๊ด€๋ จ ๋ฌธ์„œ์˜ ๋‚ด์šฉ์„ ์ปจํ…์ŠคํŠธ๋กœ ๊ฒฐํ•ฉ (๋ฒˆํ˜ธ ์—†์ด) @@ -65,21 +72,8 @@ public String generateRouteRecommendationAnswer(String question, List searchPlaceIds(String searchQuery, int maxResults) { * @return ๋ชจ๋“  ์žฅ์†Œ๋ฅผ ๋ฐ˜์˜ํ•œ ๋ฃจํŠธ ์ถ”์ฒœ ์‘๋‹ต */ public String generateRouteRecommendationAnswerWithPlaces(String question, java.util.List places) { - log.debug("๋ฃจํŠธ ์ถ”์ฒœ ์‘๋‹ต ์ƒ์„ฑ ์‹œ์ž‘ (์žฅ์†Œ ๊ธฐ๋ฐ˜): '{}', ์žฅ์†Œ ์ˆ˜: {}", question, places.size()); + return generateRouteRecommendationAnswerWithPlaces(question, places, "ko"); // ๊ธฐ๋ณธ ํ•œ๊ตญ์–ด + } + + /** + * ์–ธ์–ด๋ฅผ ๊ณ ๋ คํ•œ ์‹ค์ œ ๋ฃจํŠธ์˜ ์žฅ์†Œ๋“ค ๊ธฐ๋ฐ˜ ๋ฃจํŠธ ์ถ”์ฒœ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + public String generateRouteRecommendationAnswerWithPlaces(String question, java.util.List places, String language) { + log.debug("๋ฃจํŠธ ์ถ”์ฒœ ์‘๋‹ต ์ƒ์„ฑ ์‹œ์ž‘ (์žฅ์†Œ ๊ธฐ๋ฐ˜): '{}', ์žฅ์†Œ ์ˆ˜: {}, ์–ธ์–ด: {}", question, places.size(), language); if (places.isEmpty()) { log.info("์žฅ์†Œ ์ •๋ณด ์—†์Œ: '{}'", question); - return "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ๋ฃจํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ๋งˆ๋‚˜ ์ง€์—ญ์„ ์‹œ๋„ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”?"; + return getNoResultsMessage(language); } - // ์žฅ์†Œ ์ •๋ณด๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌํ•˜๊ณ  ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ + // ์žฅ์†Œ ์ •๋ณด๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌํ•˜๊ณ  ์–ธ์–ด๋ณ„ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ String context = places.stream() - .map(place -> String.format(""" - ์žฅ์†Œ๋ช…: %s - ์„ค๋ช…: %s - ์ฃผ์†Œ: %s - ์ง€์—ญ: %s - ํ…Œ๋งˆ: %s - ๋น„์šฉ์ •๋ณด: %s - ์—ฐ๋ฝ์ฒ˜: %s - """, - place.getNameKo(), - place.getDescriptionKo(), - place.getAddressKo(), - place.getRegion().getNameKo(), - String.join(", ", place.getThemes()), - place.getCostInfo(), - place.getContactInfo() != null ? place.getContactInfo() : "์ •๋ณด ์—†์Œ" - )) + .map(place -> createPlaceContext(place, language)) .collect(java.util.stream.Collectors.joining("\n")); log.debug("์ปจํ…์ŠคํŠธ ํฌ๊ธฐ: {} ๋ฌธ์ž, ์žฅ์†Œ ์ˆ˜: {}", context.length(), places.size()); - // ๋ฃจํŠธ ์ถ”์ฒœ์— ํŠนํ™”๋œ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ - String systemPromptText = String.format(""" - ๋‹น์‹ ์€ ์นœ๊ทผํ•˜๊ณ  ์ „๋ฌธ์ ์ธ ํ•œ๋ฅ˜ ์—ฌํ–‰ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. - ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋Œ€ํ•ด ์ฃผ์–ด์ง„ ๋ฃจํŠธ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋ ฅ์ ์ธ ์ถ”์ฒœ์„ ํ•ด์ฃผ์„ธ์š”. - - ๋‹ค์Œ ์›์น™์„ ๋”ฐ๋ผ์ฃผ์„ธ์š”: - 1. ์žฅ์†Œ๋“ค์€ ์ œ๊ณต๋œ ์ˆœ์„œ๋Œ€๋กœ ๋ฐฉ๋ฌธํ•˜๋Š” ๊ฒƒ์ด ์ตœ์ ํ™”๋œ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค - 2. ์—ฌํ–‰ ์ผ์ˆ˜์— ๋งž๊ฒŒ ์ผ์ฐจ๋ณ„๋กœ ๊ตฌ์„ฑํ•˜๋˜, ์ ์ ˆํ•œ ๊ฐœ์ˆ˜์˜ ๋Œ€ํ‘œ ์žฅ์†Œ๋“ค์„ ์„ ๋ณ„ํ•˜์—ฌ ์†Œ๊ฐœํ•ด์ฃผ์„ธ์š” - 3. ๊ฐ ์žฅ์†Œ์˜ ํŠน์ง•๊ณผ ๋งค๋ ฅ์„ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š” - 4. ์ •๋ณด ์ถœ์ฒ˜๋‚˜ ์ฐธ๊ณ  ๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ์–ธ๊ธ‰ํ•˜์ง€ ๋งˆ์„ธ์š” - 5. ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์นœ๊ทผํ•œ ํ†ค์œผ๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š” - 6. 2-3๋ฌธ๋‹จ ์ •๋„์˜ ์ ์ ˆํ•œ ๊ธธ์ด๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š” - - ๋ฃจํŠธ ์ •๋ณด: - %s - """, context); + // ์–ธ์–ด๋ณ„ ๋ฃจํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ + String systemPromptText = String.format(getPlaceBasedRouteRecommendationSystemPrompt(language), context); // LLM์„ ํ†ตํ•œ ์‘๋‹ต ์ƒ์„ฑ try { @@ -303,8 +274,262 @@ public String generateRouteRecommendationAnswerWithPlaces(String question, java. } catch (Exception e) { log.error("AI ์‘๋‹ต ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", e); - return "๋ฃจํŠธ ์ถ”์ฒœ ์ •๋ณด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ค‘์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์‹œ๊ฒ ์–ด์š”?"; + return getSystemErrorMessage(language); + } + } + + /** + * ์–ธ์–ด๋ณ„ ์žฅ์†Œ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private String createPlaceContext(com.mey.backend.domain.place.entity.Place place, String language) { + // null ์ฒดํฌ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + if (language == null) { + language = "ko"; + } + + return switch (language) { + case "ko" -> String.format(""" + ์žฅ์†Œ๋ช…: %s + ์„ค๋ช…: %s + ์ฃผ์†Œ: %s + ์ง€์—ญ: %s + ํ…Œ๋งˆ: %s + ๋น„์šฉ์ •๋ณด: %s + ์—ฐ๋ฝ์ฒ˜: %s + """, + place.getNameKo(), + place.getDescriptionKo(), + place.getAddressKo(), + place.getRegion().getNameKo(), + String.join(", ", place.getThemes()), + place.getCostInfo(), + place.getContactInfo() != null ? place.getContactInfo() : "์ •๋ณด ์—†์Œ" + ); + case "en" -> String.format(""" + Place Name: %s + Description: %s + Address: %s + Region: %s + Theme: %s + Cost Info: %s + Contact: %s + """, + place.getNameEn() != null ? place.getNameEn() : place.getNameKo(), + place.getDescriptionEn() != null ? place.getDescriptionEn() : place.getDescriptionKo(), + place.getAddressKo(), // Address is only available in Korean + place.getRegion().getNameKo(), + String.join(", ", place.getThemes()), + place.getCostInfo(), + place.getContactInfo() != null ? place.getContactInfo() : "No information" + ); + case "ja" -> String.format(""" + ๅ ดๆ‰€ๅ: %s + ่ชฌๆ˜Ž: %s + ไฝๆ‰€: %s + ๅœฐๅŸŸ: %s + ใƒ†ใƒผใƒž: %s + ่ฒป็”จๆƒ…ๅ ฑ: %s + ้€ฃ็ตกๅ…ˆ: %s + """, + place.getNameEn() != null ? place.getNameEn() : place.getNameKo(), // Fallback to English + place.getDescriptionEn() != null ? place.getDescriptionEn() : place.getDescriptionKo(), + place.getAddressKo(), + place.getRegion().getNameKo(), + String.join(", ", place.getThemes()), + place.getCostInfo(), + place.getContactInfo() != null ? place.getContactInfo() : "ๆƒ…ๅ ฑใชใ—" + ); + case "zh" -> String.format(""" + ๅœฐ็‚นๅ็งฐ: %s + ๆ่ฟฐ: %s + ๅœฐๅ€: %s + ๅœฐๅŒบ: %s + ไธป้ข˜: %s + ่ดน็”จไฟกๆฏ: %s + ่”็ณปๆ–นๅผ: %s + """, + place.getNameEn() != null ? place.getNameEn() : place.getNameKo(), // Fallback to English + place.getDescriptionEn() != null ? place.getDescriptionEn() : place.getDescriptionKo(), + place.getAddressKo(), + place.getRegion().getNameKo(), + String.join(", ", place.getThemes()), + place.getCostInfo(), + place.getContactInfo() != null ? place.getContactInfo() : "ๆ— ไฟกๆฏ" + ); + default -> createPlaceContext(place, "ko"); // ๊ธฐ๋ณธ๊ฐ’ + }; + } + + /** + * ์–ธ์–ด๋ณ„ "๊ฒฐ๊ณผ ์—†์Œ" ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private String getNoResultsMessage(String language) { + if (language == null) { + language = "ko"; + } + return switch (language) { + case "ko" -> "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญํ•˜์‹  ์กฐ๊ฑด์— ๋งž๋Š” ๋ฃจํŠธ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ๋งˆ๋‚˜ ์ง€์—ญ์„ ์‹œ๋„ํ•ด๋ณด์‹œ๊ฒ ์–ด์š”?"; + case "en" -> "Sorry, I couldn't find route information matching your criteria. Would you like to try different themes or regions?"; + case "ja" -> "็”ณใ—่จณใ”ใ–ใ„ใพใ›ใ‚“ใ€‚ใŠๅฎขๆง˜ใฎๆกไปถใซๅˆใ†ใƒซใƒผใƒˆๆƒ…ๅ ฑใŒ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใงใ—ใŸใ€‚ไป–ใฎใƒ†ใƒผใƒžใ‚„ๅœฐๅŸŸใ‚’ใŠ่ฉฆใ—ใ„ใŸใ ใ‘ใพใ™ใ‹๏ผŸ"; + case "zh" -> "ๆŠฑๆญ‰๏ผŒๆ‰พไธๅˆฐ็ฌฆๅˆๆ‚จๆกไปถ็š„่ทฏ็บฟไฟกๆฏใ€‚ๆ‚จๆƒณๅฐ่ฏ•ๅ…ถไป–ไธป้ข˜ๆˆ–ๅœฐๅŒบๅ—๏ผŸ"; + default -> getNoResultsMessage("ko"); + }; + } + + /** + * ์–ธ์–ด๋ณ„ ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private String getSystemErrorMessage(String language) { + if (language == null) { + language = "ko"; + } + return switch (language) { + case "ko" -> "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ถ”์ฒœ ์‹œ์Šคํ…œ์— ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."; + case "en" -> "Sorry, there's currently an issue with the recommendation system. Please try again later."; + case "ja" -> "็”ณใ—่จณใ”ใ–ใ„ใพใ›ใ‚“ใ€‚็พๅœจใ€ๆŽจ่–ฆใ‚ทใ‚นใƒ†ใƒ ใซๅ•้กŒใŒใ‚ใ‚Šใพใ™ใ€‚ใ—ใฐใ‚‰ใใ—ใฆใ‹ใ‚‰ใ‚‚ใ†ไธ€ๅบฆใŠ่ฉฆใ—ใใ ใ•ใ„ใ€‚"; + case "zh" -> "ๆŠฑๆญ‰๏ผŒๆŽจ่็ณป็ปŸ็›ฎๅ‰ๆœ‰้—ฎ้ข˜ใ€‚่ฏท็จๅŽๅ†่ฏ•ใ€‚"; + default -> getSystemErrorMessage("ko"); + }; + } + + /** + * ์–ธ์–ด๋ณ„ ๋ฃจํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private String getRouteRecommendationSystemPrompt(String language) { + if (language == null) { + language = "ko"; + } + return switch (language) { + case "ko" -> """ + ๋‹น์‹ ์€ ์นœ๊ทผํ•˜๊ณ  ์ „๋ฌธ์ ์ธ ํ•œ๋ฅ˜ ์—ฌํ–‰ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. + ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋Œ€ํ•ด ์ฃผ์–ด์ง„ ๋ฃจํŠธ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋ ฅ์ ์ธ ์ถ”์ฒœ์„ ํ•ด์ฃผ์„ธ์š”. + + ๋‹ค์Œ ์›์น™์„ ๋”ฐ๋ผ์ฃผ์„ธ์š”: + 1. ์ •๋ณด ์ถœ์ฒ˜๋‚˜ ์ฐธ๊ณ  ๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ์–ธ๊ธ‰ํ•˜์ง€ ๋งˆ์„ธ์š” + 2. ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์นœ๊ทผํ•œ ํ†ค์œผ๋กœ ์‘๋‹ตํ•˜์„ธ์š” + 3. ๋ฃจํŠธ์˜ ํŠน์ง•๊ณผ ๋งค๋ ฅ์„ ๊ฐ•์กฐํ•˜์„ธ์š” + 4. ๊ตฌ์ฒด์ ์ธ ์ถ”์ฒœ ์ด์œ ๋ฅผ ํฌํ•จํ•˜์„ธ์š” + 5. 2-3๋ฌธ์žฅ์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š” + + ๋ฃจํŠธ ์ •๋ณด: + %s + """; + case "en" -> """ + You are a friendly and professional Korean Wave travel guide. + Please provide natural and attractive recommendations based on the given route information in response to user questions. + + Please follow these principles: + 1. Never mention information sources or reference numbers + 2. Respond in a natural and friendly tone + 3. Emphasize the features and attractions of the route + 4. Include specific reasons for recommendations + 5. Write concisely in 2-3 sentences + + Route Information: + %s + """; + case "ja" -> """ + ใ‚ใชใŸใฏ่ฆชใ—ใฟใ‚„ใ™ใๅฐ‚้–€็š„ใช้Ÿ“ๆตๆ—…่กŒใ‚ฌใ‚คใƒ‰ใงใ™ใ€‚ + ไธŽใˆใ‚‰ใ‚ŒใŸใƒซใƒผใƒˆๆƒ…ๅ ฑใซๅŸบใฅใ„ใฆใ€ใƒฆใƒผใ‚ถใƒผใฎ่ณชๅ•ใซๅฏพใ—ใฆ่‡ช็„ถใง้ญ…ๅŠ›็š„ใชๆŽจ่–ฆใ‚’ใ—ใฆใใ ใ•ใ„ใ€‚ + + ไปฅไธ‹ใฎๅŽŸๅ‰‡ใซๅพ“ใฃใฆใใ ใ•ใ„๏ผš + 1. ๆƒ…ๅ ฑๆบใ‚„ๅ‚่€ƒ็•ชๅทใฏ็ตถๅฏพใซ่จ€ๅŠใ—ใชใ„ใงใใ ใ•ใ„ + 2. ่‡ช็„ถใง่ฆชใ—ใฟใ‚„ใ™ใ„ใƒˆใƒผใƒณใงๅฟœ็ญ”ใ—ใฆใใ ใ•ใ„ + 3. ใƒซใƒผใƒˆใฎ็‰นๅพดใจ้ญ…ๅŠ›ใ‚’ๅผท่ชฟใ—ใฆใใ ใ•ใ„ + 4. ๅ…ทไฝ“็š„ใชๆŽจ่–ฆ็†็”ฑใ‚’ๅซใ‚ใฆใใ ใ•ใ„ + 5. 2-3ๆ–‡ใง็ฐกๆฝ”ใซๆ›ธใ„ใฆใใ ใ•ใ„ + + ใƒซใƒผใƒˆๆƒ…ๅ ฑ๏ผš + %s + """; + case "zh" -> """ + ๆ‚จๆ˜ฏไธ€ไฝๅ‹ๅ–„ไธ”ไธ“ไธš็š„้Ÿฉๆตๆ—…่กŒๅ‘ๅฏผใ€‚ + ่ฏทๅŸบไบŽๆไพ›็š„่ทฏ็บฟไฟกๆฏ๏ผŒๅฏน็”จๆˆท็š„้—ฎ้ข˜็ป™ๅ‡บ่‡ช็„ถไธ”ๆœ‰ๅธๅผ•ๅŠ›็š„ๆŽจ่ใ€‚ + + ่ฏท้ตๅพชไปฅไธ‹ๅŽŸๅˆ™๏ผš + 1. ็ปไธๆๅŠไฟกๆฏๆฅๆบๆˆ–ๅ‚่€ƒ็ผ–ๅท + 2. ไปฅ่‡ช็„ถๅ‹ๅฅฝ็š„่ฏญ่ฐƒๅ›žๅบ” + 3. ๅผบ่ฐƒ่ทฏ็บฟ็š„็‰น่‰ฒๅ’Œ้ญ…ๅŠ› + 4. ๅŒ…ๅซๅ…ทไฝ“็š„ๆŽจ่็†็”ฑ + 5. ็”จ2-3ๅฅ่ฏ็ฎ€ๆดๅœฐๅ†™ๅ‡บ + + ่ทฏ็บฟไฟกๆฏ๏ผš + %s + """; + default -> getRouteRecommendationSystemPrompt("ko"); + }; + } + + /** + * ์–ธ์–ด๋ณ„ ์žฅ์†Œ ๊ธฐ๋ฐ˜ ๋ฃจํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private String getPlaceBasedRouteRecommendationSystemPrompt(String language) { + if (language == null) { + language = "ko"; } + return switch (language) { + case "ko" -> """ + ๋‹น์‹ ์€ ์นœ๊ทผํ•˜๊ณ  ์ „๋ฌธ์ ์ธ ํ•œ๋ฅ˜ ์—ฌํ–‰ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค. + ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋Œ€ํ•ด ์ฃผ์–ด์ง„ ๋ฃจํŠธ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋ ฅ์ ์ธ ์ถ”์ฒœ์„ ํ•ด์ฃผ์„ธ์š”. + + ๋‹ค์Œ ์›์น™์„ ๋”ฐ๋ผ์ฃผ์„ธ์š”: + 1. ์žฅ์†Œ๋“ค์€ ์ œ๊ณต๋œ ์ˆœ์„œ๋Œ€๋กœ ๋ฐฉ๋ฌธํ•˜๋Š” ๊ฒƒ์ด ์ตœ์ ํ™”๋œ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค + 2. ์—ฌํ–‰ ์ผ์ˆ˜์— ๋งž๊ฒŒ ์ผ์ฐจ๋ณ„๋กœ ๊ตฌ์„ฑํ•˜๋˜, ์ ์ ˆํ•œ ๊ฐœ์ˆ˜์˜ ๋Œ€ํ‘œ ์žฅ์†Œ๋“ค์„ ์„ ๋ณ„ํ•˜์—ฌ ์†Œ๊ฐœํ•ด์ฃผ์„ธ์š” + 3. ๊ฐ ์žฅ์†Œ์˜ ํŠน์ง•๊ณผ ๋งค๋ ฅ์„ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š” + 4. ์ •๋ณด ์ถœ์ฒ˜๋‚˜ ์ฐธ๊ณ  ๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ์–ธ๊ธ‰ํ•˜์ง€ ๋งˆ์„ธ์š” + 5. ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์นœ๊ทผํ•œ ํ†ค์œผ๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š” + 6. 2-3๋ฌธ๋‹จ ์ •๋„์˜ ์ ์ ˆํ•œ ๊ธธ์ด๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š” + + ๋ฃจํŠธ ์ •๋ณด: + %s + """; + case "en" -> """ + You are a friendly and professional Korean Wave travel guide. + Please provide natural and attractive recommendations based on the given route information in response to user questions. + + Please follow these principles: + 1. The places should be visited in the provided order as it's an optimized route + 2. Organize by daily itinerary according to travel days, selecting appropriate representative places to introduce + 3. Briefly describe the features and attractions of each place + 4. Never mention information sources or reference numbers + 5. Write in a natural and friendly tone + 6. Write in an appropriate length of 2-3 paragraphs + + Route Information: + %s + """; + case "ja" -> """ + ใ‚ใชใŸใฏ่ฆชใ—ใฟใ‚„ใ™ใๅฐ‚้–€็š„ใช้Ÿ“ๆตๆ—…่กŒใ‚ฌใ‚คใƒ‰ใงใ™ใ€‚ + ไธŽใˆใ‚‰ใ‚ŒใŸใƒซใƒผใƒˆๆƒ…ๅ ฑใซๅŸบใฅใ„ใฆใ€ใƒฆใƒผใ‚ถใƒผใฎ่ณชๅ•ใซๅฏพใ—ใฆ่‡ช็„ถใง้ญ…ๅŠ›็š„ใชๆŽจ่–ฆใ‚’ใ—ใฆใใ ใ•ใ„ใ€‚ + + ไปฅไธ‹ใฎๅŽŸๅ‰‡ใซๅพ“ใฃใฆใใ ใ•ใ„๏ผš + 1. ๅ ดๆ‰€ใฏๆไพ›ใ•ใ‚ŒใŸ้ †ๅบใง่จชๅ•ใ™ใ‚‹ใฎใŒๆœ€้ฉๅŒ–ใ•ใ‚ŒใŸใƒซใƒผใƒˆใงใ™ + 2. ๆ—…่กŒๆ—ฅๆ•ฐใซๅˆใ‚ใ›ใฆๆ—ฅๅˆฅใซๆง‹ๆˆใ—ใ€้ฉๅˆ‡ใชๆ•ฐใฎไปฃ่กจ็š„ใชๅ ดๆ‰€ใ‚’้ธใ‚“ใง็ดนไป‹ใ—ใฆใใ ใ•ใ„ + 3. ๅ„ๅ ดๆ‰€ใฎ็‰นๅพดใจ้ญ…ๅŠ›ใ‚’็ฐกๆฝ”ใซ่ชฌๆ˜Žใ—ใฆใใ ใ•ใ„ + 4. ๆƒ…ๅ ฑๆบใ‚„ๅ‚่€ƒ็•ชๅทใฏ็ตถๅฏพใซ่จ€ๅŠใ—ใชใ„ใงใใ ใ•ใ„ + 5. ่‡ช็„ถใง่ฆชใ—ใฟใ‚„ใ™ใ„ใƒˆใƒผใƒณใงๆ›ธใ„ใฆใใ ใ•ใ„ + 6. 2-3ๆฎต่ฝ็จ‹ๅบฆใฎ้ฉๅˆ‡ใช้•ทใ•ใงๆ›ธใ„ใฆใใ ใ•ใ„ + + ใƒซใƒผใƒˆๆƒ…ๅ ฑ๏ผš + %s + """; + case "zh" -> """ + ๆ‚จๆ˜ฏไธ€ไฝๅ‹ๅ–„ไธ”ไธ“ไธš็š„้Ÿฉๆตๆ—…่กŒๅ‘ๅฏผใ€‚ + ่ฏทๅŸบไบŽๆไพ›็š„่ทฏ็บฟไฟกๆฏ๏ผŒๅฏน็”จๆˆท็š„้—ฎ้ข˜็ป™ๅ‡บ่‡ช็„ถไธ”ๆœ‰ๅธๅผ•ๅŠ›็š„ๆŽจ่ใ€‚ + + ่ฏท้ตๅพชไปฅไธ‹ๅŽŸๅˆ™๏ผš + 1. ๅœฐ็‚นๅบ”ๆŒ‰ๆไพ›็š„้กบๅบๅ‚่ง‚๏ผŒ่ฟ™ๆ˜ฏไผ˜ๅŒ–็š„่ทฏ็บฟ + 2. ๆ นๆฎๆ—…่กŒๅคฉๆ•ฐๆŒ‰ๆ—ฅๅฎ‰ๆŽ’๏ผŒ้€‰ๆ‹ฉ้€‚ๅฝ“ๆ•ฐ้‡็š„ไปฃ่กจๆ€งๅœฐ็‚นไป‹็ป + 3. ็ฎ€่ฆๆ่ฟฐๆฏไธชๅœฐ็‚น็š„็‰น่‰ฒๅ’Œ้ญ…ๅŠ› + 4. ็ปไธๆๅŠไฟกๆฏๆฅๆบๆˆ–ๅ‚่€ƒ็ผ–ๅท + 5. ไปฅ่‡ช็„ถๅ‹ๅฅฝ็š„่ฏญ่ฐƒไนฆๅ†™ + 6. ไปฅ2-3ๆฎต็š„้€‚ๅฝ“็ฏ‡ๅน…ไนฆๅ†™ + + ่ทฏ็บฟไฟกๆฏ๏ผš + %s + """; + default -> getPlaceBasedRouteRecommendationSystemPrompt("ko"); + }; } } diff --git a/src/main/java/com/mey/backend/domain/place/service/PlaceService.java b/src/main/java/com/mey/backend/domain/place/service/PlaceService.java index e15b167..14d2a8d 100644 --- a/src/main/java/com/mey/backend/domain/place/service/PlaceService.java +++ b/src/main/java/com/mey/backend/domain/place/service/PlaceService.java @@ -44,7 +44,43 @@ public List getRelatedPlaces(Long placeId, String language) Place place = placeRepository.findPlaceByPlaceId(placeId); - return tourApiClient.fetchRelatedPlaces(place.getLatitude(), place.getLongitude(), language); + // chatbot ์–ธ์–ด ์ฝ”๋“œ๋ฅผ Tour API ์–ธ์–ด ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ + String tourApiLanguage = convertToTourApiLanguage(language); + return tourApiClient.fetchRelatedPlaces(place.getLatitude(), place.getLongitude(), tourApiLanguage); + } + + /** + * ์–ธ์–ด ์ฝ”๋“œ ๋ณ€ํ™˜์šฉ enum + */ + public enum LanguageCode { + KOREAN("ko", "K"), + ENGLISH("en", "E"), + JAPANESE("ja", "J"), + CHINESE("zh", "C"); + + private final String chatbotCode; + private final String tourApiCode; + + LanguageCode(String chatbotCode, String tourApiCode) { + this.chatbotCode = chatbotCode; + this.tourApiCode = tourApiCode; + } + + public static String toTourApiCode(String chatbotCode) { + for (LanguageCode lang : values()) { + if (lang.chatbotCode.equals(chatbotCode)) { + return lang.tourApiCode; + } + } + return KOREAN.tourApiCode; // ๊ธฐ๋ณธ๊ฐ’ + } + } + + /** + * chatbot ์–ธ์–ด ์ฝ”๋“œ๋ฅผ Tour API ์–ธ์–ด ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ + */ + private String convertToTourApiLanguage(String language) { + return LanguageCode.toTourApiCode(language); } @Transactional(readOnly = true) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3d93c69..f329909 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,63 +1,70 @@ -spring: - application: - name: backend - - config: - import: optional:file:.env[.properties] - - profiles: - active: dev - - ai: - openai: - api-key: ${OPENAI_API_KEY} - embedding: - options: - model: text-embedding-3-small - -springdoc: - swagger-ui: - path: /swagger - -jwt: - secret: ${JWT_SECRET} - access-token-validity: 3600000 # 1์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) - refresh-token-validity: 604800000 # 7์ผ (๋ฐ€๋ฆฌ์ดˆ) - -openai: - api: - key: ${OPENAI_API_KEY:dummy} - url: https://api.openai.com/v1/chat/completions - -tmap: - transit: - base-url: https://apis.openapi.sk.com/transit - app-key: ${TMAP_APP_KEY:dummy} - lang: 0 # 0=Korean - count: 1 - -tourapi: - service-key: ${TOURAPI_SERVICE_KEY} - mobile-os: "ETC" - mobile-app: "KRoute" - base: - related: http://apis.data.go.kr/B551011/TarRlteTarService1 - kor: http://apis.data.go.kr/B551011/KorService2 - eng: http://apis.data.go.kr/B551011/EngService2 - jpn: http://apis.data.go.kr/B551011/JpnService2 - chs: http://apis.data.go.kr/B551011/ChsService2 - -cloud: - aws: - s3: - bucket: mey-cd-bucket - path: - profile: profile - place: place - region: - static: ap-northeast-2 - stack: - auto: false - credentials: - access-key: ${S3_ACCESS_KEY} - secret-key: ${S3_SECRET_KEY} \ No newline at end of file +spring: + application: + name: backend + + config: + import: optional:file:.env[.properties] + + profiles: + active: dev + + ai: + openai: + api-key: ${OPENAI_API_KEY} + embedding: + options: + model: text-embedding-3-small + +springdoc: + swagger-ui: + path: /swagger + +jwt: + secret: ${JWT_SECRET} + access-token-validity: 3600000 # 1์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + refresh-token-validity: 604800000 # 7์ผ (๋ฐ€๋ฆฌ์ดˆ) + +openai: + api: + key: ${OPENAI_API_KEY:dummy} + url: https://api.openai.com/v1/chat/completions + +tmap: + transit: + base-url: https://apis.openapi.sk.com/transit + app-key: ${TMAP_APP_KEY:dummy} + lang: 0 # 0=Korean + count: 1 + +tourapi: + service-key: ${TOURAPI_SERVICE_KEY} + mobile-os: "ETC" + mobile-app: "KRoute" + base: + related: http://apis.data.go.kr/B551011/TarRlteTarService1 + kor: http://apis.data.go.kr/B551011/KorService2 + eng: http://apis.data.go.kr/B551011/EngService2 + jpn: http://apis.data.go.kr/B551011/JpnService2 + chs: http://apis.data.go.kr/B551011/ChsService2 + +cloud: + aws: + s3: + bucket: mey-cd-bucket + path: + profile: profile + place: place + region: + static: ap-northeast-2 + stack: + auto: false + credentials: + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + +# Chatbot ๋‹ค๊ตญ์–ด ์„ค์ • +chatbot: + language: + supported: ko,en,ja,zh # ์ง€์›ํ•˜๋Š” ์–ธ์–ด ๋ชฉ๋ก + fallback: en # ์ง€์›๋˜์ง€ ์•Š๋Š” ์–ธ์–ด์˜ fallback ์–ธ์–ด (์ผ๋ณธ์–ด/์ค‘๊ตญ์–ด โ†’ ์˜์–ด) + default: ko # ๊ธฐ๋ณธ ์–ธ์–ด \ No newline at end of file