diff --git a/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java b/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java index a4709f8..0225050 100644 --- a/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java +++ b/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java @@ -170,6 +170,71 @@ public class MallRagAiService { return out; } + // ── ①-b WISE AI 일반 지식 질의 (중앙 /answer 근거·인용·보류) ────────────── + /** + * WISE AI 일반 Q&A. 상품 프레이밍 없이 중앙 {@code /rag/answer}(rag_mall 컬렉션) 근거 답변을 + * 그대로 전달한다. 근거 미달이면 {@code abstained:true}(환각 차단), 중앙 미가용이면 + * {@code degraded:true}. 기존 recommend 로직·필드는 불변(순증 메서드). + */ + @SuppressWarnings("unchecked") + public Map ask(Map req, String actor) { + String query = str(req.get("query")); + if (query.isBlank()) query = str(req.get("q")); + RagToggles t = toggles.current(); + Map out = new LinkedHashMap<>(); + + if (query.isBlank()) { + out.put("answer", null); + out.put("sources", List.of()); + out.put("citations", List.of()); + out.put("abstained", false); + out.put("degraded", true); + out.put("degraded_reason", "empty_query"); + out.putAll(meta(t, "answer", false)); + return out; + } + + // RAG 미가용/비활성 → degraded (AI 서비스 일시 불가). 온프레미스 폴백 없음(일반 지식 검색). + if (!t.ragEnabled || !rag.available()) { + out.put("answer", null); + out.put("sources", List.of()); + out.put("citations", List.of()); + out.put("abstained", false); + out.put("degraded", true); + out.put("degraded_reason", t.ragEnabled ? "rag_unavailable" : "rag_disabled"); + out.put("engine", "NONE"); + out.putAll(meta(t, "answer", false)); + return out; + } + + String mode = toggles.effectiveMode(t); + Map ares = rag.answer(mask(query), mode, t.rerank, t.topK, t.generationModel, true); + if (bool(ares.get("degraded")) || ares.get("answer") == null) { + out.put("answer", null); + out.put("sources", List.of()); + out.put("citations", List.of()); + out.put("abstained", ares.getOrDefault("abstained", Boolean.FALSE)); + out.put("degraded", true); + out.put("degraded_reason", str(ares.getOrDefault("degraded_reason", "answer_empty"))); + out.put("engine", "RAG_ANSWER"); + out.putAll(meta(t, "answer", false)); + return out; + } + + out.put("answer", mask(str(ares.get("answer")))); + out.put("grounded", ares.get("grounded")); + out.put("faithfulness", ares.get("faithfulness")); + out.put("citations", ares.get("citations")); + out.put("sources", ares.get("sources")); + out.put("guardrail", ares.get("guardrail")); + out.put("abstained", ares.getOrDefault("abstained", Boolean.FALSE.equals(ares.get("grounded")))); + out.put("answerId", ares.get("answer_id")); + out.put("engine", "RAG_ANSWER"); + out.put("degraded", false); + out.putAll(meta(t, "answer", false)); + return out; + } + // ── ② 수요예측·재고이양 추천 (중앙 /agent tool-use, 제안만) ──────────────── @SuppressWarnings("unchecked") public Map demandPlan(Map req, String actor) { diff --git a/backend/src/main/java/com/zioinfo/mall/rag/RagController.java b/backend/src/main/java/com/zioinfo/mall/rag/RagController.java index ebb30d6..a30f33e 100644 --- a/backend/src/main/java/com/zioinfo/mall/rag/RagController.java +++ b/backend/src/main/java/com/zioinfo/mall/rag/RagController.java @@ -37,6 +37,15 @@ public class RagController { return ApiResponse.ok(service.recommend(req, auth.getName())); } + /** + * WISE AI 일반 지식 질의(Q&A) — 중앙 {@code /rag/answer}(근거·인용·보류) 프록시. 인증 사용자. + * 순증 엔드포인트로, 기존 recommend/demand-plan 필드는 불변. 요청 {@code {query}}. + */ + @PostMapping("/ask") + public ApiResponse> ask(@RequestBody Map req, Authentication auth) { + return ApiResponse.ok(service.ask(req, auth == null ? "anon" : auth.getName())); + } + /** 수요예측·재고이양 추천 — MANAGER+ (운영 의사결정). */ @PostMapping("/demand-plan") @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") diff --git a/doc/guardia-mall_개발자지침서_v1.1.pptx b/doc/guardia-mall_개발자지침서_v1.1.pptx new file mode 100644 index 0000000..2f126ef Binary files /dev/null and b/doc/guardia-mall_개발자지침서_v1.1.pptx differ diff --git a/doc/guardia-mall_사용자지침서_v1.1.pptx b/doc/guardia-mall_사용자지침서_v1.1.pptx new file mode 100644 index 0000000..3abce6f Binary files /dev/null and b/doc/guardia-mall_사용자지침서_v1.1.pptx differ diff --git a/doc/guardia-mall_아키텍처설계서_v1.1.pptx b/doc/guardia-mall_아키텍처설계서_v1.1.pptx new file mode 100644 index 0000000..e31625e Binary files /dev/null and b/doc/guardia-mall_아키텍처설계서_v1.1.pptx differ diff --git a/doc/guardia-mall_운영자지침서_v1.1.pptx b/doc/guardia-mall_운영자지침서_v1.1.pptx new file mode 100644 index 0000000..1100c4d Binary files /dev/null and b/doc/guardia-mall_운영자지침서_v1.1.pptx differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0f22dbd..371b215 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -44,6 +44,7 @@ import AdminApp from './admin/AdminApp' import AiTechniques from './admin/AiTechniques' import AiPlatformSettings from './admin/AiPlatformSettings' import MyPage from './admin/MyPage' +import WiseAiPage from './pages/WiseAiPage' // UIWS 이식 — 업무 모듈(관리자 영역 병합, 고객 쇼핑 화면 무영향) import UiwsLayout from './pages/uiws/UiwsLayout' @@ -112,6 +113,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/admin/AdminLayout.tsx b/frontend/src/admin/AdminLayout.tsx index 31018db..04e7a4d 100644 --- a/frontend/src/admin/AdminLayout.tsx +++ b/frontend/src/admin/AdminLayout.tsx @@ -4,7 +4,7 @@ import { LayoutDashboard, Store, Flower2, Boxes, ShoppingBag, Users, Crown, Megaphone, Repeat, CalendarClock, BarChart3, UserCog, ScrollText, Settings, LogOut, UserCircle, ArrowLeftRight, Smartphone, ClipboardList, CalendarDays, Mail, PieChart, Sparkles, Cpu, - ShieldCheck, KeySquare, ListTree, Menu as MenuIcon, Building2, Briefcase, + ShieldCheck, KeySquare, ListTree, Menu as MenuIcon, Building2, Briefcase, BrainCircuit, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { getMe } from '../api/client' @@ -32,6 +32,7 @@ const adminLinks = [ ] // 최신 AI 기법(중앙 guardia-rag) 토글 — 라벨 i18n 미의존(고정 표기), 변경은 MANAGER+(USER 차단) const aiLinks = [ + { to: '/admin/wise-ai', label: 'WISE AI', icon: BrainCircuit, roles: ['ADMIN', 'MANAGER'] }, { to: '/admin/ai-techniques', label: 'AI Techniques', icon: Sparkles, roles: ['ADMIN', 'MANAGER'] }, { to: '/admin/ai-platform', label: 'AI 플랫폼 설정', icon: Cpu, roles: ['ADMIN'] }, ] diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index d3e8b02..688d589 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -263,6 +263,8 @@ export const updateRagToggle = (key: string, value: string | number | boolean) = u(api.put(`/api/mall/rag/toggles/${key}`, { value })) // 추천·자연어 검색(/answer hybrid + /structured, 근거+보류) — 인증 사용자 export const ragRecommend = (req: object) => u(api.post('/api/mall/rag/recommend', req)) +// WISE AI 일반 지식 질의(Q&A) — /rag/answer 근거·인용·보류. 인증 사용자 +export const ragAsk = (query: string) => u(api.post('/api/mall/rag/ask', { query })) // 수요예측·재고이양 추천(/agent tool-use, 승인 게이트) — MANAGER+ export const ragDemandPlan = (req: object) => u(api.post('/api/mall/rag/demand-plan', req)) export const ragFeedback = (req: object) => u(api.post('/api/mall/rag/feedback', req)) diff --git a/frontend/src/pages/WiseAiPage.tsx b/frontend/src/pages/WiseAiPage.tsx new file mode 100644 index 0000000..7e138bd --- /dev/null +++ b/frontend/src/pages/WiseAiPage.tsx @@ -0,0 +1,133 @@ +import { useState } from 'react' +import { BrainCircuit, Send, Quote, ShieldAlert, Cpu, FileText } from 'lucide-react' +import { ragAsk } from '../api/client' + +/** + * WISE AI — Enterprise AI for Trusted Knowledge. + * + * 중앙 guardia-rag(/rag/answer, rag_mall 컬렉션) 경유 지식 질의 화면. + * - 질문 입력 → 스피너 → 답변(plain text) + * - 인용 카드(sources/citations) — 없으면 "근거 문서 없음" + * - abstained=true → 경고 배지 "근거가 부족해 답변을 보류했습니다"(오류 아님·환각 차단) + * - degraded=true → 회색 배지(사유 코드) "AI 서비스 일시 불가" + * + * 기존 Mall AI(/api/mall/ai/*)·RAG 배선(recommend/demand-plan)은 불변. 본 화면은 기존 + * RagController /ask 엔드포인트만 소비한다. 외부 API 없음(전부 백엔드→중앙 rag 경유). + */ +export default function WiseAiPage() { + const [q, setQ] = useState('') + const [busy, setBusy] = useState(false) + const [res, setRes] = useState(null) + const [err, setErr] = useState('') + + const ask = async () => { + if (!q.trim()) return + setBusy(true); setErr(''); setRes(null) + try { + setRes(await ragAsk(q.trim())) + } catch { + setErr('AI 서비스 일시 불가 — 잠시 후 다시 시도해 주세요.') + } finally { + setBusy(false) + } + } + + const onKey = (e: React.KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') ask() + } + + const abstained = res?.abstained === true + const degraded = res?.degraded === true + const cites: any[] = res?.citations || res?.sources || [] + const answer = res?.answer + + return ( +
+ {/* 헤더 + 브랜딩 */} +
+
+

+ WISE AI +

+

Enterprise AI for Trusted Knowledge

+
+ + 중앙 guardia-rag · 외부 API 미사용 + +
+ + {/* 질문 입력 */} +
+