diff --git a/static/app.js b/static/app.js
index d3e73e3..9215584 100644
--- a/static/app.js
+++ b/static/app.js
@@ -327,6 +327,20 @@ function switchView(view) {
kb: "기술 문서 KB",
institutions: "기관 관리", scripts: "스크립트 관리",
timetable: "작업 타임테이블",
+ // ── GUARDiA 확장 v3 ──
+ rag_search: "RAG 하이브리드 검색", ai_insights: "AI 운영 인사이트",
+ ai_workflow: "자율 워크플로우", learning_loop: "Learning Loop",
+ multimodal: "멀티모달 AI 분석",
+ kpi_dashboard: "KPI 대시보드", bi_dashboard: "BI 대시보드",
+ predictive: "예측 분석", benchmark: "벤치마킹",
+ auto_report: "자동 보고서", cohort: "코호트 분석",
+ kubernetes: "Kubernetes 관리", container_alerts: "컨테이너 알림",
+ ncloud: "NCloud 관리",
+ jira_sync: "Jira 동기화", servicenow: "ServiceNow",
+ slack_config: "Slack 설정", sso_config: "SSO 인증",
+ erp_config: "ERP 연동", kakao_config: "카카오 알림톡",
+ tenant_portal: "테넌트 포털", billing: "구독 · 과금",
+ white_label: "브랜딩 설정",
};
document.getElementById("page-title").textContent = titles[view] || view;
renderCurrentView();
@@ -342,6 +356,8 @@ function renderCurrentView() {
else if (currentView === "institutions") loadInstitutions();
else if (currentView === "scripts") loadScripts();
else if (currentView === "timetable") loadTimetable();
+ // ── GUARDiA 확장 v3 뷰 ──
+ else loadExpansionView(currentView);
}
/* ─── Data loading ──────────────────────────────── */
@@ -2225,3 +2241,1045 @@ function fmtDate(iso) {
});
} catch { return iso; }
}
+
+/* ══════════════════════════════════════════════════
+ GUARDiA 확장 v3 — 뷰 렌더러 (P1~P3 신규 기능)
+══════════════════════════════════════════════════ */
+
+function getEl(id) { return document.getElementById(id); }
+
+function renderApiView(containerId, html) {
+ const el = getEl(containerId) || getEl("main-content") || document.querySelector(".main-content");
+ if (el) el.innerHTML = html;
+}
+
+async function loadExpansionView(view) {
+ const container = document.getElementById("main-content") || document.querySelector(".main-scroll") || document.body;
+ const token = localStorage.getItem("token") || "";
+ const H = { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" };
+
+ try {
+ switch (view) {
+
+ // ── RAG 검색 ─────────────────────────────────────
+ case "rag_search":
+ container.innerHTML = `
+
+
🔍 RAG 하이브리드 검색
+
KB + SR 이력을 BM25 + pgvector로 검색합니다
+
+
+
+
+
+
`;
+ break;
+
+ // ── AI 인사이트 ───────────────────────────────────
+ case "ai_insights": {
+ const r = await fetch("/api/insights/weekly", {headers: H});
+ const d = await r.json();
+ container.innerHTML = `
+
+ ${[
+ {label:"신규 SR", val: d.stats?.total || 0, color:"#003366"},
+ {label:"완료율", val: (d.stats?.completion_rate || 0) + "%", color:"#10B981"},
+ {label:"미처리", val: d.stats?.open || 0, color:"#EF4444"},
+ ].map(s=>`
+
${s.val}
+
${s.label}
+
`).join("")}
+
+
+
🤖 AI 주간 인사이트
+
${esc(d.ai_insight || "데이터 수집 중...")}
+
+
+
📊 상위 SR 카테고리
+ ${(d.top_categories||[]).map(c=>`
+
+
${esc(c.category)}
+
+
${c.count}건
+
`).join("")}
+
`;
+ break;
+ }
+
+ // ── KPI 대시보드 ──────────────────────────────────
+ case "kpi_dashboard": {
+ const r = await fetch("/api/kpi/dashboard", {headers: H});
+ const d = await r.json();
+ const statusColor = {"GREEN":"#10B981","YELLOW":"#F59E0B","RED":"#EF4444","NO_DATA":"#6B7280"};
+ container.innerHTML = `
+
+
${{"GREEN":"✅","YELLOW":"⚠️","RED":"❌","NO_DATA":"❔"}[d.overall_status]||"❔"}
+
+
전체 KPI 상태: ${d.overall_status||"N/A"}
+
GREEN:${d.summary?.GREEN||0} · YELLOW:${d.summary?.YELLOW||0} · RED:${d.summary?.RED||0}
+
+
+
+
+ ${(d.kpis||[]).map(k=>`
+
+
+ ${esc(k.display_name)}
+ ${k.status}
+
+
+ ${k.current_value !== null ? k.current_value : "—"}${esc(k.unit)}
+
+
+ 목표: ${k.target}${k.unit} · 달성: ${k.achievement_pct !== null ? k.achievement_pct+"%" : "—"}
+
+
+
`).join("")}
+ ${(d.kpis||[]).length === 0 ? `
+
KPI가 없습니다. 템플릿을 적용하세요.
+
+
` : ""}
+
`;
+ break;
+ }
+
+ // ── BI 대시보드 ───────────────────────────────────
+ case "bi_dashboard": {
+ const [ovr, pie] = await Promise.all([
+ fetch("/api/bi/overview", {headers: H}).then(r=>r.json()),
+ fetch("/api/bi/category-pie?days=30", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+ ${(ovr.cards||[]).map(c=>`
+
+
${c.value}
+
${esc(c.label)} ${esc(c.unit)}
+ ${c.change !== undefined ? `
${c.change>=0?"+":""}${c.change} ${esc(c.change_label||"")}
` : ""}
+
`).join("")}
+
+
+
+
📊 SR 카테고리 분포 (30일)
+ ${(pie.data||[]).slice(0,6).map(d=>`
+
+
${esc(d.category)}
+
+
${d.pct}%
+
`).join("")}
+
+
+
🔗 빠른 분석
+ ${[
+ ["SR 트렌드", "bi_sr_trend"], ["SLA 히트맵", "bi_sla"],
+ ["엔지니어 워크로드", "bi_eng"], ["MTTR 트렌드", "bi_mttr"],
+ ].map(([label, v])=>`
+ `).join("")}
+
+
`;
+ break;
+ }
+
+ // ── 예측 분석 ─────────────────────────────────────
+ case "predictive": {
+ const r = await fetch("/api/predict/summary", {headers: H});
+ const d = await r.json();
+ const [sla, surge] = await Promise.all([
+ fetch("/api/predict/sla-breach", {headers: H}).then(r=>r.json()),
+ fetch("/api/predict/sr-surge", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
📉 SLA 위반 예측 (7일)
+
+ ${Math.round((sla.breach_probability_7d||0)*100)}%
+
+
현재 SLA: ${sla.current_rate||0}% · 목표: ${sla.target||95}%
+ ${sla.insight ? `
${esc(sla.insight)}
` : ""}
+
+
+
📈 SR 급증 감지
+
+ ${surge.surge_ratio||1}x
+
+
오늘 ${surge.today_count||0}건 · 7일 평균 ${surge.avg_7d||0}건
+ ${surge.insight ? `
${esc(surge.insight)}
` : ""}
+
+
+ ${(d.alerts||[]).length ? `
+
+
⚠️ 알림 (${d.alerts.length}건)
+ ${d.alerts.map(a=>`
${esc(a.type)}: ${esc(a.message)}
`).join("")}
+
` : ""}`;
+ break;
+ }
+
+ // ── Jira 동기화 ───────────────────────────────────
+ case "jira_sync": {
+ const [cfg, mappings] = await Promise.all([
+ fetch("/api/jira/config", {headers: H}).then(r=>r.json()),
+ fetch("/api/jira/mappings", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
⚙️ Jira 연동 설정
+ ${cfg ? `
+
+
URL: ${esc(cfg.base_url||"")}
+
프로젝트: ${esc(cfg.project_key||"")}
+
자동 동기화: ${cfg.auto_sync?"켜짐":"꺼짐"}
+
+
` :
+ `
설정 없음
+
`}
+
+
+
🔄 SR-Issue 매핑 현황 (${mappings.length}건)
+
+ | SR ID | Jira Key | 프로젝트 | 동기화 시간 |
+
+ ${mappings.slice(0,10).map(m=>`
+ | SR-${m.sr_id} |
+ ${esc(m.jira_key)} |
+ ${esc(m.project)} |
+ ${fmtDate(m.synced_at)} |
+
`).join("")}
+ ${mappings.length===0?`| 매핑 없음 |
`:""}
+
+
+
+
`;
+ break;
+ }
+
+ // ── 테넌트 포털 ───────────────────────────────────
+ case "tenant_portal": {
+ const [me, quota, users] = await Promise.all([
+ fetch("/api/portal/me", {headers: H}).then(r=>r.json()),
+ fetch("/api/portal/quota", {headers: H}).then(r=>r.json()),
+ fetch("/api/portal/users", {headers: H}).then(r=>r.json()),
+ ]);
+ const pct = (used, limit) => limit > 0 ? Math.min(100, Math.round(used/limit*100)) : 0;
+ container.innerHTML = `
+
+
+
🏢 기관 현황
+
+
플랜: ${esc(me.plan||"")}
+
역할: ${esc(me.my_role||"")}
+
SR (이번 달): ${me.stats?.sr_this_month||0}건
+
미처리 SR: ${me.stats?.open_sr||0}건
+
+
+
+
📊 쿼터 사용량
+ ${[
+ ["서버", quota.servers_used, quota.servers_limit],
+ ["사용자", quota.users_used, quota.users_limit],
+ ].map(([name, used, limit])=>`
+
+
+ ${name}${used} / ${limit < 0 ? "∞" : limit}
+
+
+
`).join("")}
+
+
+
+
+
👥 사용자 관리 (${users.length}명)
+
+
+
+ | 이름 | 이메일 | 역할 | 상태 |
+
+ ${users.slice(0,10).map(u=>`
+ | ${esc(u.name)} | ${esc(u.email)} |
+ ${esc(u.role)} |
+ ${u.is_active?'활성':'비활성'} |
+
`).join("")}
+
+
+
`;
+ break;
+ }
+
+ // ── 구독·과금 ─────────────────────────────────────
+ case "billing": {
+ const [sub, usage, invoices] = await Promise.all([
+ fetch("/api/billing/subscription", {headers: H}).then(r=>r.json()),
+ fetch("/api/billing/usage", {headers: H}).then(r=>r.json()),
+ fetch("/api/billing/invoices", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
📋 현재 구독
+
${esc(sub.plan||"COMMUNITY")}
+
+ ${sub.price ? `월 ${sub.price.toLocaleString()}원` : "무료"} · ${esc(sub.billing_cycle||"MONTHLY")}
+
+
+
+
+
+
+
📊 이번 달 사용량
+
+
서버: ${usage.servers?.used||0} / ${usage.servers?.limit < 0 ? "∞" : (usage.servers?.limit||"-")}
+
사용자: ${usage.users?.used||0} / ${usage.users?.limit < 0 ? "∞" : (usage.users?.limit||"-")}
+
SR (이번 달): ${usage.sr_this_month||0}건
+
+
+
+
+
+
🧾 청구서 이력
+
+
+
+ | 기간 | 플랜 | 금액 | 상태 |
+
+ ${invoices.slice(0,10).map(i=>`
+ | ${esc(i.period)} | ${esc(i.plan||"-")} |
+ ${i.amount ? i.amount.toLocaleString()+"원" : "무료"} |
+ ${esc(i.status)} |
+
`).join("")}
+ ${!invoices.length ? `| 청구서 없음 |
` : ""}
+
+
+
`;
+ break;
+ }
+
+ // ── Kubernetes ────────────────────────────────────
+ case "kubernetes": {
+ const r = await fetch("/api/k8s/clusters", {headers: H});
+ const clusters = await r.json();
+ container.innerHTML = `
+
+
☸️ Kubernetes 클러스터 관리
+
+
+ ${clusters.length ? clusters.map(cl=>`
+
+
+
☸️
+
+
${esc(cl.name)}
+
${esc(cl.description||"")} · 네임스페이스: ${esc(cl.namespace)}
+
+
+
+
+
+
+
+
+
`).join("") :
+ `
+
등록된 클러스터가 없습니다.
+
+
`}`;
+ break;
+ }
+
+ // ── 컨테이너 알림 ─────────────────────────────────
+ case "container_alerts": {
+ const [rules, logs] = await Promise.all([
+ fetch("/api/container-alerts/rules", {headers: H}).then(r=>r.json()),
+ fetch("/api/container-alerts/list", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
+
📋 알림 규칙 (${rules.length})
+
+
+ ${rules.map(r=>`
+ ${esc(r.name)}
+ 서버ID: ${r.server_id} · ${r.auto_sr?"SR자동":"수동"}
+
`).join("") || `
규칙 없음
`}
+
+
+
🔔 최근 알림 로그
+
+ | 유형 | 컨테이너 | 심각도 | 시간 |
+
+ ${logs.slice(0,15).map(l=>`
+ | ${esc(l.type)} |
+ ${esc(l.container)} |
+ ${esc(l.severity)} |
+ ${fmtDate(l.detected_at)} |
+
`).join("") || `| 알림 없음 |
`}
+
+
+
+
`;
+ break;
+ }
+
+ // ── NCloud ────────────────────────────────────────
+ case "ncloud": {
+ const r = await fetch("/api/ncloud/summary", {headers: H});
+ if (r.status === 404) {
+ container.innerHTML = `
+
☁️ NCloud 연동 설정
+
NCloud API Key를 등록하세요.
+
+
`; break;
+ }
+ const d = await r.json();
+ const servers = await fetch("/api/ncloud/servers", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+ ${[
+ {label:"전체 서버", val: d.server_count||0, icon:"🖥️"},
+ {label:"실행 중", val: d.running_servers||0, icon:"▶️"},
+ {label:"LB", val: d.lb_count||0, icon:"⚖️"},
+ ].map(s=>`
+
${s.icon} ${s.val}
+
${s.label}
+
`).join("")}
+
+
+
서버 목록
+
+ | 이름 | 상태 | 공인 IP | 사설 IP |
+
+ ${(servers.servers||[]).map(s=>`
+ | ${esc(s.name)} |
+ ${esc(s.status)} |
+ ${esc(s.public_ip||"-")} |
+ ${esc(s.private_ip||"-")} |
+
`).join("") || `| 서버 없음 또는 API 응답 대기 |
`}
+
+
+
`;
+ break;
+ }
+
+ // ── 자율 워크플로우 ───────────────────────────────
+ case "ai_workflow": {
+ const rules = await fetch("/api/workflow/rules", {headers: H}).then(r=>r.json());
+ const history = await fetch("/api/workflow/history?limit=10", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+
⚙️ 자율 워크플로우 엔진
+
+
+
+
+ ${rules.map(r=>`
+
+
⚙️
+
+
${esc(r.name)}
+
트리거: ${esc(r.trigger_type)} · 오늘: ${r.run_count_today}회
+
+
${r.is_active?"활성":"비활성"}
+
+
+
`).join("") || `
`}
+
+
+
최근 실행 이력
+ ${history.slice(0,8).map(h=>`
+
${esc(h.rule_name||"")}
+
${h.status} · ${fmtDate(h.started_at)}
+
`).join("") || `
이력 없음
`}
+
+
`;
+ break;
+ }
+
+ // ── Learning Loop ─────────────────────────────────
+ case "learning_loop": {
+ const [status, history, quality] = await Promise.all([
+ fetch("/api/learn/status", {headers: H}).then(r=>r.json()),
+ fetch("/api/learn/history?limit=10", {headers: H}).then(r=>r.json()),
+ fetch("/api/learn/quality", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
🧠 학습 데이터 현황
+
${status.available_samples||0}
+
수집 가능 샘플
+
+ RAG 피드백: ${status.high_quality_rag||0} · SR 이력: ${status.sr_samples||0}
+
+
+
+ ${status.ready_to_train?"준비됨":"샘플 부족"}
+
+
+
+
📊 모델 품질
+
${quality.quality_grade||"N/A"}
+
품질 등급
+
+ 평균 평점: ${quality.avg_rating||0} · 긍정 비율: ${quality.positive_rate||0}%
+
+
+
+
+
📋 학습 이력
+
+ | 모델 | 상태 | 샘플 | 시작 |
+
+ ${history.slice(0,8).map(h=>`
+ | ${esc(h.model_name||"-")} |
+ ${h.status} |
+ ${h.samples_used||0} |
+ ${fmtDate(h.started_at)} |
+
`).join("") || `| 이력 없음 |
`}
+
+
+
`;
+ break;
+ }
+
+ // ── 멀티모달 AI ───────────────────────────────────
+ case "multimodal":
+ container.innerHTML = `
+
+
🖼️ 멀티모달 AI 분석
+
스크린샷·에러 화면을 업로드하면 AI가 자동 분석합니다
+
+
+
📎 파일 드래그 또는 클릭
+
+
이미지(PNG/JPG) · 로그 파일(.log/.txt) 지원
+
+
+
`;
+ break;
+
+ // ── 벤치마킹 ─────────────────────────────────────
+ case "benchmark": {
+ const comp = await fetch("/api/benchmark/comparison", {headers: H}).then(r=>r.json());
+ const rank = await fetch("/api/benchmark/my-rank", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+
📊 업계 평균 대비 비교
+
+ ${(comp.comparison||[]).map(m=>`
+
${esc(m.metric)}
+
${m.mine}${esc(m.unit)}
+
업계 평균: ${m.industry}${esc(m.unit)}
+
${m.status==="ABOVE"?"▲ 평균 이상":"▼ 평균 이하"}
+
`).join("")}
+
+
+
+
🏆 업계 백분위 순위
+
+ ${[
+ {label:"SR 완료율", val: rank.completion_rate_percentile||0},
+ {label:"MTTR", val: rank.mttr_percentile||0},
+ {label:"SLA 준수율", val: rank.sla_percentile||0},
+ ].map(r=>`
+
${r.val}%ile
+
${r.label}
+
`).join("")}
+
+
+
+ 기여 시 더 정확한 벤치마킹 가능
+
+
`;
+ break;
+ }
+
+ // ── 자동 보고서 ───────────────────────────────────
+ case "auto_report": {
+ const [templates, reports] = await Promise.all([
+ fetch("/api/auto-report/templates", {headers: H}).then(r=>r.json()),
+ fetch("/api/auto-report/list", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
📄 보고서 템플릿
+ ${templates.map(t=>`
+
${esc(t.name)}
+
${esc(t.period)} · ${(t.format||[]).join("/")}
+
+
`).join("")}
+
+
+
📋 생성된 보고서 (${reports.length}개)
+
+ | 템플릿 | 기간 | 포맷 | 상태 | |
+
+ ${reports.slice(0,15).map(r=>`
+ | ${esc(r.template)} | ${esc(r.period)} |
+ ${esc(r.format)} |
+ ${esc(r.status)} |
+ ⬇ |
+
`).join("") || `| 보고서 없음 |
`}
+
+
+
+
`;
+ break;
+ }
+
+ // ── 브랜딩 설정 ───────────────────────────────────
+ case "white_label": {
+ const brand = await fetch("/api/brand/", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+
🎨 화이트라벨 브랜딩 설정
+
+
+
+
미리보기
+
+ ${esc(brand.company_name||"GUARDiA ITSM")}
+
+
+
+
+
+
+
`;
+ break;
+ }
+
+ // ── SSO 설정 ──────────────────────────────────────
+ case "sso_config": {
+ const configs = await fetch("/api/sso/config", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+
🔐 SSO 통합 인증
+
+
+ ${configs.length ? configs.map(c=>`
+
+
${c.provider_type==="SAML"?"🏛️":c.provider_type==="OIDC"?"🔑":"🔗"}
+
+
${esc(c.name)}
+
${esc(c.provider_type)} · ${c.is_active?"활성":"비활성"}
+
+
테스트 로그인
+
+
`).join("") :
+ `
+
SSO IdP 미등록
+
행안부 GPKI, Google, Microsoft 등 SSO를 설정하세요.
+
+
`}
+ `;
+ break;
+ }
+
+ // ── Slack 설정 ────────────────────────────────────
+ case "slack_config": {
+ const cfg = await fetch("/api/slack/config", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+
💬 Slack 연동 설정
+ ${cfg ? `
+
발신번호: ${esc(cfg.sender||"")}
+
기본 채널: ${esc(cfg.default_channel||"")}
+
상태: 연동됨
+
+
` :
+ `
설정되지 않음
`}
+
+
+
+
+
+
+
+
`;
+ break;
+ }
+
+ // ── ERP 연동 ──────────────────────────────────────
+ case "erp_config": {
+ const cfgs = await fetch("/api/erp/config", {headers: H}).then(r=>r.json());
+ container.innerHTML = `
+
+
🏢 ERP / 그룹웨어 연동
+
+
+ ${cfgs.map(c=>`
+
🏢
+
+
${esc(c.name)}
+
${esc(c.erp_type)} · ${esc(c.base_url)}
+
+
+
`).join("") || ``}`;
+ break;
+ }
+
+ // ── 카카오 알림톡 ─────────────────────────────────
+ case "kakao_config": {
+ const [cfg, history] = await Promise.all([
+ fetch("/api/kakao/config", {headers: H}).then(r=>r.json()),
+ fetch("/api/kakao/history?limit=10", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
+
💬 카카오 알림톡 설정
+ ${cfg ? `
+
발신번호: ${esc(cfg.sender||"")}
+
ID: ${esc(cfg.userid||"")}
+
연동됨
+
` : `
설정 없음
`}
+
+
+
+
+
+
+
+
📋 발송 이력
+
+ | 템플릿 | 수신자 | 결과 | 시간 |
+
+ ${history.map(h=>`
+ | ${esc(h.template)} | ${h.receivers}명 |
+ ${h.success?"성공":"실패"} |
+ ${fmtDate(h.sent_at)} |
+
`).join("") || `| 이력 없음 |
`}
+
+
+
+
`;
+ break;
+ }
+
+ // ── 코호트 분석 ───────────────────────────────────
+ case "cohort": {
+ const [growth, resolution] = await Promise.all([
+ fetch("/api/cohort/tenant-growth?cohort_months=6", {headers: H}).then(r=>r.json()),
+ fetch("/api/cohort/sr-resolution", {headers: H}).then(r=>r.json()),
+ ]);
+ container.innerHTML = `
+
+
📈 SR 처리 속도 코호트
+
+ | 코호트 | SR 수 | 평균 처리 시간 | 평가 |
+
+ ${(resolution.data||[]).map(r=>`
+ | ${esc(r.cohort)} | ${r.sr_count} |
+ ${r.avg_resolution_hours}시간 |
+ ${esc(r.benchmark)} |
+
`).join("") || `| 데이터 없음 |
`}
+
+
+
+ `;
+ break;
+ }
+
+ // ── ServiceNow ────────────────────────────────────
+ case "servicenow": {
+ const cfg = await fetch("/api/servicenow/config", {headers: H}).then(r=>r.json()).catch(()=>null);
+ container.innerHTML = `
+
+
🔗 ServiceNow 연동
+ ${cfg ? `
+
인스턴스: ${esc(cfg.instance_url||"")}
+
연동됨
+
+
+
` :
+ `
설정 없음
`}
+
+
+
`;
+ break;
+ }
+
+ default:
+ container.innerHTML = `
+
🚧 준비 중
+
${esc(view)} 화면을 구현 중입니다.
+
`;
+ }
+ } catch(e) {
+ container.innerHTML = `
+
오류 발생
${esc(e.message||String(e))}
+
`;
+ }
+}
+
+/* ── 확장 뷰 헬퍼 함수들 ──────────────────────────── */
+
+async function doRagSearch() {
+ const q = document.getElementById("rag-q")?.value;
+ if (!q) return;
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/rag/search", {
+ method: "POST", headers: {"Authorization":`Bearer ${token}`,"Content-Type":"application/json"},
+ body: JSON.stringify({query: q, top_k: 5, include_sr: true})
+ });
+ const results = await r.json();
+ const el = document.getElementById("rag-results");
+ if (el) el.innerHTML = results.map(item=>`
+
+
${esc(item.title)}
+
${esc(item.excerpt)}
+
관련도: ${item.score} · 출처: ${item.source}
+
`).join("") || '결과 없음
';
+}
+
+async function applyKpiTemplates() {
+ const token = localStorage.getItem("token")||"";
+ await fetch("/api/kpi/apply-template", {
+ method:"POST", headers:{"Authorization":`Bearer ${token}`,"Content-Type":"application/json"},
+ body: JSON.stringify({template_names:["MTTR","FCR","SLA_COMPLIANCE","SR_BACKLOG","DEPLOY_SUCCESS_RATE"]})
+ });
+ showPage("kpi_dashboard");
+ showToast("KPI 템플릿 5개 적용됨", "success");
+}
+
+async function recalcKpi(id) {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch(`/api/kpi/${id}/calculate`, {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(`KPI 재계산: ${d.name} = ${d.value} ${d.unit}`, "success");
+ showPage("kpi_dashboard");
+}
+
+async function startLearning() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/learn/train", {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(`파인튜닝 시작 (Run #${d.run_id}, ${d.samples}개 샘플)`, "success");
+}
+
+async function checkContainers() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/container-alerts/check", {headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(`컨테이너 체크 완료 — 알림 ${d.total}건`, d.total > 0 ? "warning" : "success");
+ showPage("container_alerts");
+}
+
+async function runWorkflow(id) {
+ const token = localStorage.getItem("token")||"";
+ await fetch(`/api/workflow/rules/${id}/run`, {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ showToast("워크플로우 실행 완료", "success");
+}
+
+async function generateReport(template) {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/auto-report/generate", {
+ method:"POST", headers:{"Authorization":`Bearer ${token}`,"Content-Type":"application/json"},
+ body: JSON.stringify({template, format:"excel"})
+ });
+ const d = await r.json();
+ showToast(`보고서 생성 완료 (ID: ${d.report_id})`, "success");
+ showPage("auto_report");
+}
+
+async function generateInvoice() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/billing/invoices/generate", {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(`청구서 생성 완료 (ID: ${d.invoice_id})`, "success");
+ showPage("billing");
+}
+
+async function contributeBenchmark() {
+ const token = localStorage.getItem("token")||"";
+ await fetch("/api/benchmark/contribute", {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ showToast("익명 데이터 기여 완료", "success");
+}
+
+async function testSlack() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/slack/test", {headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(d.ok ? "Slack 테스트 메시지 발송 성공" : "Slack 발송 실패", d.ok?"success":"error");
+}
+
+async function loadFeatureAdoption() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/cohort/feature-adoption", {headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ const el = document.getElementById("feature-adoption-result");
+ if (el) el.innerHTML = (d.feature_adoption||[]).map(f=>`
+
+ ${f.adopted?"✅":"❌"}
+ ${esc(f.feature)}
+ ${f.usage_count}회
+
`).join("");
+}
+
+async function loadK8sNodes(clusterId) {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch(`/api/k8s/clusters/${clusterId}/nodes`, {headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ const el = document.getElementById(`k8s-detail-${clusterId}`);
+ if (el) el.innerHTML = `
+ | 노드 | 상태 | 역할 | 버전 |
+ ${(d.nodes||[]).map(n=>`
+ | ${esc(n.name)} |
+ ${n.status} |
+ ${esc(n.roles)} |
+ ${esc(n.version)} |
+
`).join("")}
+
`;
+}
+
+async function analyzeFile() {
+ const file = document.getElementById("mm-file")?.files[0];
+ if (!file) return;
+ const token = localStorage.getItem("token")||"";
+ const form = new FormData();
+ form.append("file", file);
+ const el = document.getElementById("mm-result");
+ if (el) el.innerHTML = '분석 중...
';
+ try {
+ const r = await fetch("/api/multimodal/upload-and-analyze", {
+ method:"POST", headers:{"Authorization":`Bearer ${token}`}, body: form
+ });
+ const d = await r.json();
+ if (el) el.innerHTML = `
+
분석 결과
+
${esc(JSON.stringify(d.analysis||d, null, 2))}
+
`;
+ } catch(e) {
+ if (el) el.innerHTML = `분석 실패: ${esc(e.message)}
`;
+ }
+}
+
+function showPage(view) { currentView = view; renderCurrentView(); }
+
+function showAddClusterModal() { showToast("K8s 클러스터 등록 모달 준비 중", "info"); }
+function showCreateWorkflowModal() { showToast("워크플로우 규칙 생성 모달 준비 중", "info"); }
+function showAddSSOModal() { showToast("SSO IdP 등록 모달 준비 중", "info"); }
+function showAddERPModal() { showToast("ERP 연동 추가 모달 준비 중", "info"); }
+function showNcloudConfig() { showToast("NCloud API 설정 모달 준비 중", "info"); }
+function showJiraConfig() { showToast("Jira 설정 모달 준비 중", "info"); }
+function showInviteModal() { showToast("사용자 초대 모달 준비 중", "info"); }
+function showAddBrandModal() { showToast("브랜딩 설정 저장 중...", "info"); }
+
+async function saveBranding() {
+ const token = localStorage.getItem("token")||"";
+ const body = {
+ company_name: document.getElementById("brand-name")?.value,
+ primary_color: document.getElementById("brand-primary")?.value,
+ };
+ await fetch("/api/brand/", {method:"PUT", headers:{"Authorization":`Bearer ${token}`,"Content-Type":"application/json"}, body:JSON.stringify(body)});
+ showToast("브랜딩 저장됨", "success");
+}
+
+async function saveSlack() {
+ const token = localStorage.getItem("token")||"";
+ const body = {
+ name: "Slack 알림", webhook_url: document.getElementById("slack-webhook")?.value||"",
+ default_channel: document.getElementById("slack-channel")?.value||"#guardia-ops",
+ };
+ await fetch("/api/slack/config", {method:"POST", headers:{"Authorization":`Bearer ${token}`,"Content-Type":"application/json"}, body:JSON.stringify(body)});
+ showToast("Slack 설정 저장됨", "success");
+}
+
+async function saveKakao() {
+ const token = localStorage.getItem("token")||"";
+ const body = {
+ apikey: document.getElementById("kakao-apikey")?.value||"",
+ userid: document.getElementById("kakao-userid")?.value||"",
+ senderkey: document.getElementById("kakao-senderkey")?.value||"",
+ sender: document.getElementById("kakao-sender")?.value||"",
+ };
+ await fetch("/api/kakao/config", {method:"POST", headers:{"Authorization":`Bearer ${token}`,"Content-Type":"application/json"}, body:JSON.stringify(body)});
+ showToast("카카오 설정 저장됨", "success");
+}
+
+async function saveServiceNow() {
+ const token = localStorage.getItem("token")||"";
+ const body = {
+ instance_url: document.getElementById("snow-url")?.value||"",
+ username: document.getElementById("snow-user")?.value||"",
+ password: document.getElementById("snow-pw")?.value||"",
+ };
+ await fetch("/api/servicenow/config", {method:"POST", headers:{"Authorization":`Bearer ${token}`,"Content-Type":"application/json"}, body:JSON.stringify(body)});
+ showToast("ServiceNow 설정 저장됨", "success");
+ showPage("servicenow");
+}
+
+async function testServiceNow() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/servicenow/test", {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(d.ok ? "ServiceNow 연결 성공" : "연결 실패", d.ok?"success":"error");
+}
+
+async function loadSNowIncidents() {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch("/api/servicenow/incidents", {headers:{"Authorization":`Bearer ${token}`}});
+ const incidents = await r.json();
+ const el = document.getElementById("snow-incidents");
+ if (el) el.innerHTML = `
+ | 번호 | 제목 | 우선순위 |
+ ${(Array.isArray(incidents)?incidents:[]).slice(0,5).map(i=>`
+ | ${esc(i.number)} | ${esc(i.title)} | ${esc(i.priority)} |
+
`).join("")||`| 없음 |
`}
+
`;
+}
+
+async function testERP(id) {
+ const token = localStorage.getItem("token")||"";
+ const r = await fetch(`/api/erp/test/${id}`, {method:"POST", headers:{"Authorization":`Bearer ${token}`}});
+ const d = await r.json();
+ showToast(d.ok ? "ERP 연결 성공" : `연결 실패: ${d.error||""}`, d.ok?"success":"error");
+}
diff --git a/static/index.html b/static/index.html
index a81e5d5..b45b543 100644
--- a/static/index.html
+++ b/static/index.html
@@ -128,6 +128,72 @@
📅 작업 타임테이블
+
+
+
+
+
+
+
RAG 하이브리드 검색
+
AI 운영 인사이트
+
자율 워크플로우
+
Learning Loop
+
멀티모달 분석
+
+
+
+
+
+
KPI 대시보드
+
BI 대시보드
+
예측 분석
+
벤치마킹
+
자동 보고서
+
코호트 분석
+
+
+
+
+
+
Kubernetes
+
컨테이너 알림
+
NCloud 관리
+
+
+
+
+
+
Jira 동기화
+
ServiceNow
+
Slack 설정
+
SSO 인증
+
ERP 연동
+
카카오 알림톡
+
+
+
+
+
+
테넌트 포털
+
구독 · 과금
+
브랜딩 설정
+
+
🔏 라이선스 관리