diff --git a/backend/src/main/java/com/zioinfo/hrm/wise/RagClient.java b/backend/src/main/java/com/zioinfo/hrm/wise/RagClient.java
new file mode 100644
index 0000000..e43fc42
--- /dev/null
+++ b/backend/src/main/java/com/zioinfo/hrm/wise/RagClient.java
@@ -0,0 +1,116 @@
+package com.zioinfo.hrm.wise;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * WISE AI — 얇은 중앙 guardia-rag REST 클라이언트 (HRM 전용). [GUARDiA-HRM]
+ *
+ *
핵심 설계 : HRM 은 LangChain·벡터 로직을 Java 에 재구현하지 않는다. 중앙 Python
+ * guardia-rag 의 검색·근거생성·인용·guardrail 을 {@code POST /rag/answer} 로 호출하기만 한다.
+ * 미가용/타임아웃/RAM 부족 시 예외를 전파하지 않고 {@code degraded:true} 폴백을 돌려준다.
+ *
+ *
보안 불변 : base-url 은 온프레미스(중앙 guardia-rag) 전용. 외부 LLM 직접 호출 없음.
+ * 응답에서 자격증명/PII/스택트레이스 미노출. 오류는 1줄 요약만 로깅.
+ */
+@Slf4j
+@Component
+public class RagClient {
+
+ /** 솔루션 격리 키(컬렉션 rag_hrm·피드백·설정). */
+ private static final String SOLUTION = "hrm";
+
+ /** /rag/answer 콜드로드 감안 타임아웃(WISE_APPLY_SPEC: 240s). */
+ private static final Duration ANSWER_TIMEOUT = Duration.ofSeconds(240);
+
+ /** 중앙 guardia-rag 베이스 URL(서버 내부 루프백 기본). 외부 URL 설정 금지. */
+ @Value("${guardia.rag.base-url:http://127.0.0.1:8020}")
+ private String baseUrl;
+
+ /** RAG 경유 마스터 스위치. false 면 항상 degraded(HRM 화면은 안내 메시지). */
+ @Value("${guardia.rag.enabled:true}")
+ private boolean enabled;
+
+ private final HttpClient http = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(5))
+ .build();
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ * POST /rag/answer — 검색+근거생성+인용+guardrail(근거 부족 시 abstained 보류).
+ *
+ * @return {@code {answer, sources[], grounded, faithfulness, abstained, degraded, degraded_reason, trace_id, answer_id}}.
+ * 미가용 시 {@code {degraded:true, degraded_reason:...}}.
+ */
+ @SuppressWarnings("unchecked")
+ public Map answer(String query) {
+ if (!enabled) {
+ return degraded("rag_disabled");
+ }
+ try {
+ Map options = new LinkedHashMap<>();
+ options.put("stream", false);
+ options.put("verify", true);
+
+ Map body = new LinkedHashMap<>();
+ body.put("solution", SOLUTION);
+ body.put("query", query == null ? "" : query);
+ body.put("retrieval_mode", "vector");
+ body.put("top_k", 5);
+ body.put("options", options);
+
+ String json = mapper.writeValueAsString(body);
+ HttpRequest req = HttpRequest.newBuilder()
+ .uri(URI.create(trimTrailingSlash(baseUrl) + "/rag/answer"))
+ .timeout(ANSWER_TIMEOUT)
+ .header("Content-Type", "application/json")
+ .header("X-Solution-Key", SOLUTION)
+ .POST(HttpRequest.BodyPublishers.ofString(json))
+ .build();
+
+ HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString());
+ if (resp.statusCode() / 100 != 2 || resp.body() == null || resp.body().isBlank()) {
+ log.warn("중앙 rag /answer 비2xx 또는 빈 응답({}) — HRM 폴백", resp.statusCode());
+ return degraded("rag_unavailable");
+ }
+ Map out = mapper.readValue(resp.body(), Map.class);
+ return out != null ? out : degraded("rag_no_response");
+ } catch (Exception e) {
+ log.warn("중앙 rag /answer 일시 불가 — HRM 폴백: {}", summarize(e));
+ return degraded("rag_unavailable");
+ }
+ }
+
+ private static Map degraded(String reason) {
+ Map out = new LinkedHashMap<>();
+ out.put("degraded", true);
+ out.put("degraded_reason", reason);
+ return out;
+ }
+
+ private static String trimTrailingSlash(String s) {
+ if (s == null || s.isBlank()) {
+ return "http://127.0.0.1:8020";
+ }
+ return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
+ }
+
+ /** 스택트레이스 미노출: 메시지 1줄만 요약. */
+ private static String summarize(Exception e) {
+ String m = e.getMessage();
+ if (m == null) {
+ return e.getClass().getSimpleName();
+ }
+ return m.length() > 160 ? m.substring(0, 160) : m;
+ }
+}
diff --git a/backend/src/main/java/com/zioinfo/hrm/wise/WiseAskController.java b/backend/src/main/java/com/zioinfo/hrm/wise/WiseAskController.java
new file mode 100644
index 0000000..23d2359
--- /dev/null
+++ b/backend/src/main/java/com/zioinfo/hrm/wise/WiseAskController.java
@@ -0,0 +1,35 @@
+package com.zioinfo.hrm.wise;
+
+import com.zioinfo.hrm.common.ApiResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * WISE AI — 중앙 guardia-rag {@code /rag/answer} 프록시(HRM). [GUARDiA-HRM]
+ *
+ * {@code POST /api/wise/ask {query}} → 근거·인용·환각차단(abstained) 답변. 인증 사용자 전용
+ * (Spring Security {@code anyRequest().authenticated()} — 로그인 JWT 필요). 브라우저는 이 백엔드만
+ * 호출하고 rag 는 서버 내부 루프백으로만 접근한다(외부 노출 금지). 실패는 degraded 요약으로 반환.
+ *
+ *
기존 {@code /api/hrm/ai/*}(Ollama 직접)는 불변. 본 컨트롤러는 중앙 경유 신규 경로만 추가한다.
+ */
+@RestController
+@RequestMapping("/api/wise")
+@RequiredArgsConstructor
+public class WiseAskController {
+
+ private final RagClient ragClient;
+
+ @PostMapping("/ask")
+ public ApiResponse> ask(@RequestBody Map req, Authentication auth) {
+ Object q = req != null ? req.get("query") : null;
+ String query = q != null ? String.valueOf(q) : "";
+ return ApiResponse.ok(ragClient.answer(query));
+ }
+}
diff --git a/doc/guardia-hrm_개발자지침서_v1.1.pptx b/doc/guardia-hrm_개발자지침서_v1.1.pptx
new file mode 100644
index 0000000..d29076c
Binary files /dev/null and b/doc/guardia-hrm_개발자지침서_v1.1.pptx differ
diff --git a/doc/guardia-hrm_사용자지침서_v1.1.pptx b/doc/guardia-hrm_사용자지침서_v1.1.pptx
new file mode 100644
index 0000000..3971085
Binary files /dev/null and b/doc/guardia-hrm_사용자지침서_v1.1.pptx differ
diff --git a/doc/guardia-hrm_아키텍처설계서_v1.1.pptx b/doc/guardia-hrm_아키텍처설계서_v1.1.pptx
new file mode 100644
index 0000000..0426598
Binary files /dev/null and b/doc/guardia-hrm_아키텍처설계서_v1.1.pptx differ
diff --git a/doc/guardia-hrm_운영자지침서_v1.1.pptx b/doc/guardia-hrm_운영자지침서_v1.1.pptx
new file mode 100644
index 0000000..c0cb865
Binary files /dev/null and b/doc/guardia-hrm_운영자지침서_v1.1.pptx differ
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 6b72a11..9e98f89 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -10,6 +10,7 @@ import PerformancePage from './pages/PerformancePage'
import RecruitmentPage from './pages/RecruitmentPage'
import TrainingPage from './pages/TrainingPage'
import AiPage from './pages/AiPage'
+import WiseAiPage from './pages/WiseAiPage'
import AiPlatformSettings from './pages/AiPlatformSettings'
import AdminPage from './pages/AdminPage'
import MyPage from './pages/MyPage'
@@ -40,6 +41,7 @@ const MENU = [
{ path: '/messages', label: '쪽지함', icon: '✉️' },
{ path: '/work-stats', label: '업무통계', icon: '📈' },
{ path: '/ai', label: 'AI 인사분석', icon: '🤖' },
+ { path: '/wise-ai', label: 'WISE AI', icon: '✨' },
{ path: '/ai-platform', label: 'AI 플랫폼 설정', icon: '🧠', adminOnly: true },
{ path: '/admin', label: '시스템관리', icon: '⚙️' },
// UIWS system 이식 — 권한·코드·메뉴·부서·거래처 (관리자 전용)
@@ -143,6 +145,8 @@ export default function App() {
} />
} />
} />
+ {/* WISE AI — 중앙 guardia-rag 근거·인용 질의응답(인증 사용자) */}
+ } />
{/* AI 플랫폼 설정(provider/모델·연결테스트·피드백 학습) — 관리자 전용. 백엔드 /api/hrm/admin/** RBAC 강제 */}
} />
} />
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
index 6690016..c35df8e 100644
--- a/frontend/src/api/client.ts
+++ b/frontend/src/api/client.ts
@@ -21,6 +21,17 @@ api.interceptors.response.use(
export default api
+// ── WISE AI (중앙 guardia-rag 프록시) ──────────────────────────────────
+// 별도 baseURL(/api/wise) — 공용 api(/api/hrm)와 경로가 다르므로 전용 인스턴스.
+// 인증 사용자 전용(로그인 JWT). 응답은 ApiResponse 봉투(r.data.data 에 rag 결과).
+const wiseApi = axios.create({ baseURL: '/api/wise' })
+wiseApi.interceptors.request.use(cfg => {
+ const token = localStorage.getItem('hrm_token')
+ if (token) cfg.headers.Authorization = `Bearer ${token}`
+ return cfg
+})
+export const wiseAsk = (query: string) => wiseApi.post('/ask', { query })
+
// ── Auth / Me ─────────────────────────────────────────────────────────
// client baseURL='/api/hrm' → 경로는 상대(/auth/...·/admin/...).
export const getMe = () => api.get('/auth/me')
diff --git a/frontend/src/pages/WiseAiPage.tsx b/frontend/src/pages/WiseAiPage.tsx
new file mode 100644
index 0000000..26d8725
--- /dev/null
+++ b/frontend/src/pages/WiseAiPage.tsx
@@ -0,0 +1,130 @@
+import React, { useState } from 'react'
+import { wiseAsk } from '../api/client'
+
+/**
+ * WISE AI — Enterprise AI for Trusted Knowledge. [GUARDiA-HRM]
+ *
+ * 중앙 guardia-rag(/rag/answer) 경유 근거·인용 기반 질의응답 화면.
+ * - 질문 입력 → 스피너 → 답변(plain text)
+ * - sources[] 인용 카드(없으면 "근거 문서 없음" 표기)
+ * - abstained=true → 경고 톤 안내 배지(오류 아님)
+ * - degraded → 회색 배지(사유 코드)
+ * 외부 API 미사용(온프레미스 rag 프록시). 기존 AI 인사분석(/ai) 화면과 별개 레이어.
+ */
+export default function WiseAiPage() {
+ const [query, setQuery] = useState('')
+ const [busy, setBusy] = useState(false)
+ const [res, setRes] = useState(null)
+ const [err, setErr] = useState('')
+
+ const ask = async () => {
+ if (!query.trim()) return
+ setBusy(true); setErr(''); setRes(null)
+ try {
+ const r = await wiseAsk(query.trim())
+ setRes(r.data?.data ?? r.data)
+ } catch (e: any) {
+ setErr(e.response?.data?.message || 'AI 서비스 일시 불가 — 잠시 후 다시 시도해주세요.')
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ const sources: any[] = Array.isArray(res?.sources) ? res.sources
+ : Array.isArray(res?.citations) ? res.citations : []
+ const abstained = res?.abstained === true
+ const degraded = res?.degraded === true
+ const answer: string = res?.answer ?? ''
+
+ return (
+
+
+
+
WISE AI
+
Enterprise AI for Trusted Knowledge
+
+
중앙 guardia-rag · 외부 API 미사용
+
+
+
+ 질문
+
+
+ {busy && (
+
+
+
+
근거 문서 검색 및 답변 생성 중…
+
+
+ )}
+
+ {err &&
{err}
}
+
+ {res && !busy && (
+
+ {/* 배지 */}
+
+ {abstained && (
+
+ 근거가 부족해 답변을 보류했습니다
+
+ )}
+ {degraded && (
+
+ degraded{res.degraded_reason ? ` · ${res.degraded_reason}` : ''}
+
+ )}
+ {!abstained && !degraded && res.faithfulness != null && (
+
+ 신뢰도(faithfulness) {Math.round(Number(res.faithfulness) * 100)}%
+
+ )}
+
+
+ {/* 답변 */}
+ {answer
+ ?
{answer}
+ : abstained
+ ?
확인된 근거가 없어 답변을 제시하지 않았습니다. 질문을 더 구체화하거나 관련 문서를 확인해주세요.
+ :
응답이 비어 있습니다.
}
+
+ {/* 인용 카드 */}
+
+
근거 인용
+ {sources.length > 0 ? (
+
+ {sources.map((s, i) => (
+
+
+ {s.source || s.document || s.title || s.chunk_id || `근거 ${i + 1}`}
+
+ {(s.page != null || s.location) && (
+
위치: {s.page ?? s.location}
+ )}
+ {s.support != null && (
+
관련도 {Math.round(Number(s.support) * 100)}%
+ )}
+
+ ))}
+
+ ) : (
+
근거 문서 없음
+ )}
+
+
+ )}
+
+ )
+}