diff --git a/backend/src/main/java/com/zioinfo/mall/config/SecurityConfig.java b/backend/src/main/java/com/zioinfo/mall/config/SecurityConfig.java
index 4fff1e2..dd2aacb 100644
--- a/backend/src/main/java/com/zioinfo/mall/config/SecurityConfig.java
+++ b/backend/src/main/java/com/zioinfo/mall/config/SecurityConfig.java
@@ -81,7 +81,10 @@ public class SecurityConfig {
"/api/mall/promotion/**", "/api/mall/inventory/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/mall/product/**", "/api/mall/category/**",
"/api/mall/promotion/**", "/api/mall/inventory/**").hasAnyRole("ADMIN", "MANAGER")
- // 그 외 mall API (장바구니·주문·결제·배송·찜·CS·회원·AI) — 인증 사용자
+ // 최신 AI 기법(중앙 guardia-rag) — 운영 의사결정·토글 변경은 MANAGER+ (추천/피드백/토글조회는 인증 사용자)
+ .requestMatchers(HttpMethod.POST, "/api/mall/rag/demand-plan").hasAnyRole("ADMIN", "MANAGER")
+ .requestMatchers(HttpMethod.PUT, "/api/mall/rag/toggles/**").hasAnyRole("ADMIN", "MANAGER")
+ // 그 외 mall API (장바구니·주문·결제·배송·찜·CS·회원·AI·RAG 추천/피드백/토글조회) — 인증 사용자
.requestMatchers("/api/mall/**").authenticated()
// 나머지 모든 API/WS/Actuator는 인증 (아래 SPA permit 보다 먼저 — API 노출 방지)
.requestMatchers("/api/**", "/ws/**", "/actuator/**").authenticated()
diff --git a/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java b/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java
new file mode 100644
index 0000000..a4709f8
--- /dev/null
+++ b/backend/src/main/java/com/zioinfo/mall/rag/MallRagAiService.java
@@ -0,0 +1,344 @@
+package com.zioinfo.mall.rag;
+
+import com.zioinfo.mall.ai.MallAiService;
+import com.zioinfo.mall.analytics.mapper.AnalyticsMapper;
+import com.zioinfo.mall.product.MallProduct;
+import com.zioinfo.mall.rag.RagToggleService.RagToggles;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Mall 대표 AI 기능 RAG 배선 레이어(순증·비파괴).
+ *
+ *
기존 {@link MallAiService}(Ollama 우선 + Java 폴백)는 불변. 본 서비스는 중앙 guardia-rag 를
+ * 경유하는 별개 레이어로, 두 대표 기능을 전환한다:
+ *
+ * - 상품 추천·자연어 상품검색 → {@code /rag/answer}(retrieval_mode=hybrid 근거) +
+ * {@code /rag/structured}(productId·score·reason 결정론 JSON). 매장 재고 ON/OFF·상태 필터 적용.
+ * 토글 off/미가용 시 기존 {@link MallAiService#recommend}/{@link MallAiService#nlSearch} 폴백(degraded:true).
+ * - 수요예측·재고이양 추천 → {@code /rag/agent}(tool-use, 사람 승인 게이트, 제안만).
+ * tool_use 토글 off/미가용 시 기존 {@link MallAiService#demandForecast}/{@link MallAiService#transferRecommendations} 통계 폴백.
+ *
+ *
+ * 모든 응답에 적용 metadata(retrieval_mode·rerank·toolUse·structured·degraded·abstained)를 표기해
+ * 토글 effect 가 관측 가능하게 한다. 자격증명/카드/회원 PII/내부IP/매장 SSH/스택트레이스는 입력·출력 모두 마스킹.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MallRagAiService {
+
+ private final RagClient rag;
+ private final RagToggleService toggles;
+ private final MallAiService mallAi; // 기존 Ollama/Java 폴백 재사용(불변)
+ private final AnalyticsMapper analyticsMapper; // 수요예측 신호(매장별 판매)
+
+ /** 추천/검색 결정론 결과 JSON 스키마(중앙 /structured 강제). */
+ private static final Map RECOMMEND_SCHEMA = Map.of(
+ "type", "object",
+ "properties", Map.of(
+ "items", Map.of(
+ "type", "array",
+ "items", Map.of(
+ "type", "object",
+ "properties", Map.of(
+ "productId", Map.of("type", "integer"),
+ "name", Map.of("type", "string"),
+ "score", Map.of("type", "number"),
+ "reason", Map.of("type", "string")
+ ),
+ "required", List.of("productId")
+ )
+ ),
+ "summary", Map.of("type", "string"),
+ "noResults", Map.of("type", "boolean")
+ ),
+ "required", List.of("items")
+ );
+
+ /** 재고이양/수요 결정 JSON 스키마(중앙 /structured·/agent 최종 정리 강제). */
+ private static final Map DECISION_SCHEMA = Map.of(
+ "type", "object",
+ "properties", Map.of(
+ "decisions", Map.of(
+ "type", "array",
+ "items", Map.of(
+ "type", "object",
+ "properties", Map.of(
+ "action", Map.of("type", "string"), // forecast|transfer|clearance
+ "storeFrom", Map.of("type", "integer"),
+ "storeTo", Map.of("type", "integer"),
+ "productId", Map.of("type", "integer"),
+ "qty", Map.of("type", "integer"),
+ "expectedImpact", Map.of("type", "string"),
+ "confidence", Map.of("type", "number")
+ ),
+ "required", List.of("action")
+ )
+ ),
+ "rationale", Map.of("type", "string")
+ ),
+ "required", List.of("decisions")
+ );
+
+ /** 입력/근거 내 민감정보 마스킹 패턴(내부IP·자격증명·토큰·카드번호). */
+ private static final Pattern SENSITIVE = Pattern.compile(
+ "(?i)(pass(word)?\\s*[:=]\\s*\\S+|token\\s*[:=]\\s*\\S+|api[-_]?key\\s*[:=]\\s*\\S+"
+ + "|\\b(?:10|172|192)\\.(?:\\d{1,3})\\.(?:\\d{1,3})\\.(?:\\d{1,3})\\b"
+ + "|\\b(?:\\d[ -]?){13,16}\\b)");
+
+ // ── ① 상품 추천·자연어 검색 (중앙 /answer hybrid + /structured) ────────────
+ @SuppressWarnings("unchecked")
+ public Map recommend(Map req, String actor) {
+ String occasion = str(req.get("occasion"));
+ String keyword = str(req.get("keyword"));
+ if (keyword.isBlank()) keyword = str(req.get("query")); // nl-search 진입 호환
+ int limit = clamp(intOf(req.get("limit"), 6), 1, 20);
+ RagToggles t = toggles.current();
+ Map out = new LinkedHashMap<>();
+
+ // RAG 미가용 → 기존 추천(판매순/평점 폴백)
+ if (!t.ragEnabled || !rag.available()) {
+ return fallbackRecommend(req, actor, t, occasion, keyword, limit,
+ t.ragEnabled ? "rag_unavailable" : "rag_disabled");
+ }
+
+ // hybrid(BM25+벡터) 검색 근거 — 매칭 사유 동반. graphrag 토글 on 이면 graph.
+ String mode = toggles.effectiveMode(t);
+ String query = "GUARDiA Mall 꽃집 카탈로그에서 다음 조건에 맞는 부케/상품을 추천하라. "
+ + "occasion='" + (occasion.isBlank() ? "any" : occasion) + "' "
+ + "요청='" + keyword + "'. 근거(매칭 사유)를 동반하고, 조건에 맞는 상품이 없으면 추천을 만들지 말라.";
+ Map ares = rag.answer(query, mode, t.rerank, t.topK, t.generationModel, true);
+ boolean degraded = bool(ares.get("degraded"));
+
+ if (degraded || ares.get("answer") == null) {
+ return fallbackRecommend(req, actor, t, occasion, keyword, limit,
+ str(ares.getOrDefault("degraded_reason", "answer_empty")));
+ }
+
+ // /structured 결정론 결과(productId·score·reason 고정) — 토글 on 시
+ List