{{ analysisLabel(card.type) }}
+{{ card.title || analysisTitle(card.type) }}
+{{ card.summary }}
+ +-
+
- {{ metric.label }} +
- {{ metric.value }} +
diff --git a/frontend/package.json b/frontend/package.json index c63bf16..fb8a706 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,10 +22,5 @@ "postcss": "^8.4.0", "tailwindcss": "^3.4.0", "vite": "^6.4.3" - }, - "pnpm": { - "overrides": { - "form-data": "4.0.6" - } } } diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index d9534f8..2f13654 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,6 +1,9 @@ packages: - . +overrides: + form-data: 4.0.6 + allowBuilds: esbuild: true vue-demi: true diff --git a/frontend/src/api/chat-normalizer.js b/frontend/src/api/chat-normalizer.js index 248c6b0..35414b6 100644 --- a/frontend/src/api/chat-normalizer.js +++ b/frontend/src/api/chat-normalizer.js @@ -8,6 +8,16 @@ const normalizeLegalCard = (card = {}) => ({ score: typeof card.score === 'number' ? card.score : null }) +const asObject = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {}) + +const normalizeAnalysisCard = (card = {}) => ({ + type: card.type || '', + title: card.title || '', + summary: card.summary || '', + score: typeof card.score === 'number' ? card.score : null, + metrics: { ...asObject(card.metrics) } +}) + export const normalizeChatResponse = (body = {}) => { const data = body.data || {} return { @@ -15,6 +25,7 @@ export const normalizeChatResponse = (body = {}) => { message: data.message || '', sessionId: data.sessionId || null, properties: asArray(data.properties), - legalCards: asArray(data.legalCards).map(normalizeLegalCard) + legalCards: asArray(data.legalCards).map(normalizeLegalCard), + analysisCards: asArray(data.analysisCards).map(normalizeAnalysisCard) } } diff --git a/frontend/src/api/chat.js b/frontend/src/api/chat.js index f2bdd61..6a26b1f 100644 --- a/frontend/src/api/chat.js +++ b/frontend/src/api/chat.js @@ -3,15 +3,20 @@ import { normalizeChatResponse } from './chat-normalizer.js' export { normalizeChatResponse } -export const sendChatMessage = async ({ message, sessionId = null } = {}, client = http) => { +export const sendChatMessage = async ({ message, sessionId = null, selectedPropertyId = null } = {}, client = http) => { const trimmedMessage = String(message || '').trim() if (!trimmedMessage) { throw new Error('message must not be blank') } - const response = await client.post('/api/v1/chat', { + const payload = { message: trimmedMessage, sessionId - }) + } + if (selectedPropertyId !== null && selectedPropertyId !== undefined && selectedPropertyId !== '') { + payload.selectedPropertyId = selectedPropertyId + } + + const response = await client.post('/api/v1/chat', payload) return normalizeChatResponse(response.data) } diff --git a/frontend/src/api/chat.test.mjs b/frontend/src/api/chat.test.mjs index 484cb8e..7f4221d 100644 --- a/frontend/src/api/chat.test.mjs +++ b/frontend/src/api/chat.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict' import { test } from 'node:test' import { normalizeChatResponse } from './chat-normalizer.js' +import { sendChatMessage } from './chat.js' test('normalizeChatResponse maps legal RAG response into chat payload', () => { const result = normalizeChatResponse({ @@ -49,4 +50,132 @@ test('normalizeChatResponse tolerates missing optional arrays', () => { assert.deepEqual(result.properties, []) assert.deepEqual(result.legalCards, []) + assert.deepEqual(result.analysisCards, []) +}) + +test('sendChatMessage includes selected property id in request payload', async () => { + let capturedUrl = '' + let capturedPayload = null + const client = { + async post(url, payload) { + capturedUrl = url + capturedPayload = payload + return { + data: { + data: { + intent: 'PRICE_ANALYSIS', + message: '분석했습니다.', + sessionId: 'session-1', + properties: [], + legalCards: [], + analysisCards: [] + } + } + } + } + } + + await sendChatMessage( + { + message: '이 매물 시세 어때?', + sessionId: 'session-1', + selectedPropertyId: 17 + }, + client + ) + + assert.equal(capturedUrl, '/api/v1/chat') + assert.deepEqual(capturedPayload, { + message: '이 매물 시세 어때?', + sessionId: 'session-1', + selectedPropertyId: 17 + }) +}) + +test('sendChatMessage omits empty selected property id', async () => { + let capturedPayload = null + const client = { + async post(_url, payload) { + capturedPayload = payload + return { + data: { + data: { + message: 'OK' + } + } + } + } + } + + await sendChatMessage( + { + message: '안전 분석해줘', + selectedPropertyId: null + }, + client + ) + + assert.deepEqual(capturedPayload, { + message: '안전 분석해줘', + sessionId: null + }) +}) + +test('normalizeChatResponse maps price and safety analysis cards', () => { + const result = normalizeChatResponse({ + data: { + intent: 'SAFETY_ANALYSIS', + message: '주변 안전 데이터는 안전 점수 78점으로 확인됩니다.', + sessionId: 'session-2', + analysisCards: [ + { + type: 'PRICE', + title: '시세 분석', + summary: '최근 실거래 2건을 기준으로 확인했습니다.', + score: null, + metrics: { + comparableTransactionCount: 2, + avgDeposit: 10500000, + avgMonthlyRent: 520000 + } + }, + { + type: 'SAFETY', + title: '안전 분석', + summary: '반경 500m 기준 안전 점수는 78점입니다.', + score: 78, + metrics: { + radius: 500, + cctvCount300m: 8, + bellCount300m: 0 + } + } + ] + } + }) + + assert.deepEqual(result.analysisCards, [ + { + type: 'PRICE', + title: '시세 분석', + summary: '최근 실거래 2건을 기준으로 확인했습니다.', + score: null, + metrics: { + comparableTransactionCount: 2, + avgDeposit: 10500000, + avgMonthlyRent: 520000 + } + }, + { + type: 'SAFETY', + title: '안전 분석', + summary: '반경 500m 기준 안전 점수는 78점입니다.', + score: 78, + metrics: { + radius: 500, + cctvCount300m: 8, + bellCount300m: 0 + } + } + ]) }) diff --git a/frontend/src/store/mapStore.js b/frontend/src/store/mapStore.js index 1d4d57d..eac20d4 100644 --- a/frontend/src/store/mapStore.js +++ b/frontend/src/store/mapStore.js @@ -50,7 +50,8 @@ export default defineStore('map', { { role: 'bot', text: '계약서, 보증금 회수, 확정일자처럼 헷갈리는 전월세 법률 질문을 물어보세요.', - legalCards: [] + legalCards: [], + analysisCards: [] } ], lastFetchedAt: null, @@ -197,7 +198,8 @@ export default defineStore('map', { try { const response = await sendChatMessage({ message: text, - sessionId: this.chatSessionId + sessionId: this.chatSessionId, + selectedPropertyId: this.selectedPropertyId }) if (seq !== this.chatRequestSeq) return @@ -208,6 +210,7 @@ export default defineStore('map', { text: response.message, intent: response.intent, legalCards: response.legalCards, + analysisCards: response.analysisCards, properties: response.properties }) } catch (error) { @@ -217,7 +220,8 @@ export default defineStore('map', { role: 'bot', text: this.chatError, isError: true, - legalCards: [] + legalCards: [], + analysisCards: [] }) } finally { if (seq === this.chatRequestSeq) { diff --git a/frontend/src/views/Chatbot.vue b/frontend/src/views/Chatbot.vue index a273fa5..d8b7aa2 100644 --- a/frontend/src/views/Chatbot.vue +++ b/frontend/src/views/Chatbot.vue @@ -51,6 +51,43 @@
{{ card.content }}
+ +{{ analysisLabel(card.type) }}
+{{ card.summary }}
+ +