From caa70a608b97694624ea47873f4a8de86e963f76 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Tue, 2 Jun 2026 06:24:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=EB=82=98=EB=A8=B8=EC=A7=80?= =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C=20=E2=80=94=20ITSM/Manager/Messenger=20?= =?UTF-8?q?=EC=8B=A0=EA=B7=9C=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ITSM static (app.js + index.html): - 사이드바: AI플랫폼·분석KPI·클라우드·외부연동·SaaS 5개 그룹 추가 - 23개 신규 뷰 핸들러 (rag_search, kpi_dashboard, bi_dashboard, jira_sync 등) - 액션 헬퍼 함수 20개+ (재계산, 파인튜닝, 보고서 생성, 데이터 기여 등) Manager 5개 신규 페이지: - KpiDashboard.tsx: KPI 신호등 대시보드 + 재계산 - BiAnalytics.tsx: SR트렌드·카테고리파이·MTTR·엔지니어워크로드 - BillingManage.tsx: 구독플랜·사용량·청구서 이력 - IntegrationHub.tsx: Jira/Slack/ServiceNow/ERP/Kakao/SSO 탭 - AiPlatform.tsx: AI인사이트·LearningLoop·예측·벤치마킹 Messenger 신규 탭: - insights.tsx: AI 주간 인사이트 + SLA 예측 + 이상 감지 Co-Authored-By: Claude Sonnet 4.6 --- workspace/guardia-itsm/static/app.js | 1058 +++++++++++++++++ workspace/guardia-itsm/static/index.html | 66 + .../guardia-manager/frontend/src/App.tsx | 12 + .../src/components/layout/AppLayout.tsx | 6 + .../src/components/layout/Sidebar.tsx | 8 + .../frontend/src/pages/AiPlatform.tsx | 199 ++++ .../frontend/src/pages/BiAnalytics.tsx | 150 +++ .../frontend/src/pages/BillingManage.tsx | 118 ++ .../frontend/src/pages/IntegrationHub.tsx | 213 ++++ .../frontend/src/pages/KpiDashboard.tsx | 119 ++ .../guardia-messenger/app/(tabs)/_layout.tsx | 7 + .../guardia-messenger/app/(tabs)/insights.tsx | 131 ++ 12 files changed, 2087 insertions(+) create mode 100644 workspace/guardia-manager/frontend/src/pages/AiPlatform.tsx create mode 100644 workspace/guardia-manager/frontend/src/pages/BiAnalytics.tsx create mode 100644 workspace/guardia-manager/frontend/src/pages/BillingManage.tsx create mode 100644 workspace/guardia-manager/frontend/src/pages/IntegrationHub.tsx create mode 100644 workspace/guardia-manager/frontend/src/pages/KpiDashboard.tsx create mode 100644 workspace/guardia-messenger/app/(tabs)/insights.tsx diff --git a/workspace/guardia-itsm/static/app.js b/workspace/guardia-itsm/static/app.js index d3e73e33..92155843 100644 --- a/workspace/guardia-itsm/static/app.js +++ b/workspace/guardia-itsm/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}건)

+ + + + ${mappings.slice(0,10).map(m=>` + + + + + `).join("")} + ${mappings.length===0?``:""} + +
SR IDJira Key프로젝트동기화 시간
SR-${m.sr_id}${esc(m.jira_key)}${esc(m.project)}${fmtDate(m.synced_at)}
매핑 없음
+
+
`; + 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=>` + + + + `).join("")} + +
이름이메일역할상태
${esc(u.name)}${esc(u.email)}${esc(u.role)}${u.is_active?'활성':'비활성'}
+
`; + 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=>` + + + + `).join("")} + ${!invoices.length ? `` : ""} + +
기간플랜금액상태
${esc(i.period)}${esc(i.plan||"-")}${i.amount ? i.amount.toLocaleString()+"원" : "무료"}${esc(i.status)}
청구서 없음
+
`; + 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=>` + + + + + `).join("") || ``} + +
유형컨테이너심각도시간
${esc(l.type)}${esc(l.container)}${esc(l.severity)}${fmtDate(l.detected_at)}
알림 없음
+
+
`; + 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("")} +
+
+

서버 목록

+ + + + ${(servers.servers||[]).map(s=>` + + + + + `).join("") || ``} + +
이름상태공인 IP사설 IP
${esc(s.name)}${esc(s.status)}${esc(s.public_ip||"-")}${esc(s.private_ip||"-")}
서버 없음 또는 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=>` + + + + + `).join("") || ``} + +
모델상태샘플시작
${esc(h.model_name||"-")}${h.status}${h.samples_used||0}${fmtDate(h.started_at)}
이력 없음
+
`; + 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=>` + + + + + `).join("") || ``} + +
템플릿기간포맷상태
${esc(r.template)}${esc(r.period)}${esc(r.format)}${esc(r.status)}
보고서 없음
+
+
`; + 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=>` + + + + `).join("") || ``} + +
템플릿수신자결과시간
${esc(h.template)}${h.receivers}명${h.success?"성공":"실패"}${fmtDate(h.sent_at)}
이력 없음
+
+
`; + 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 처리 속도 코호트

+ + + + ${(resolution.data||[]).map(r=>` + + + + `).join("") || ``} + +
코호트SR 수평균 처리 시간평가
${esc(r.cohort)}${r.sr_count}${r.avg_resolution_hours}시간${esc(r.benchmark)}
데이터 없음
+
+
+

🎯 기능 도입률

+ +
+
`; + 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=>` + + + + + `).join("")} +
노드상태역할버전
${esc(n.name)}${n.status}${esc(n.roles)}${esc(n.version)}
`; +} + +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=>` + + `).join("")||``} +
번호제목우선순위
${esc(i.number)}${esc(i.title)}${esc(i.priority)}
없음
`; +} + +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/workspace/guardia-itsm/static/index.html b/workspace/guardia-itsm/static/index.html index a81e5d5e..b45b5435 100644 --- a/workspace/guardia-itsm/static/index.html +++ b/workspace/guardia-itsm/static/index.html @@ -128,6 +128,72 @@ + + + + + + + + + + + + + + + + + + + + + + + 🔏 라이선스 관리 diff --git a/workspace/guardia-manager/frontend/src/App.tsx b/workspace/guardia-manager/frontend/src/App.tsx index c12ee24e..f0c30a5a 100644 --- a/workspace/guardia-manager/frontend/src/App.tsx +++ b/workspace/guardia-manager/frontend/src/App.tsx @@ -23,6 +23,12 @@ const DrConsole = lazy(() => import('./pages/DrConsole')) const NetworkConsole = lazy(() => import('./pages/NetworkConsole')) const CsapConsole = lazy(() => import('./pages/CsapConsole')) const ScrapingManager = lazy(() => import('./pages/ScrapingManager')) +// ── GUARDiA 확장 v3 ── +const KpiDashboard = lazy(() => import('./pages/KpiDashboard')) +const BiAnalytics = lazy(() => import('./pages/BiAnalytics')) +const BillingManage = lazy(() => import('./pages/BillingManage')) +const IntegrationHub = lazy(() => import('./pages/IntegrationHub')) +const AiPlatform = lazy(() => import('./pages/AiPlatform')) function Loading() { return ( @@ -61,6 +67,12 @@ export default function App() { } /> } /> } /> + {/* GUARDiA 확장 v3 */} + } /> + } /> + } /> + } /> + } /> } /> diff --git a/workspace/guardia-manager/frontend/src/components/layout/AppLayout.tsx b/workspace/guardia-manager/frontend/src/components/layout/AppLayout.tsx index d9a3346b..3ce3f9d1 100644 --- a/workspace/guardia-manager/frontend/src/components/layout/AppLayout.tsx +++ b/workspace/guardia-manager/frontend/src/components/layout/AppLayout.tsx @@ -21,6 +21,12 @@ const PAGE_TITLES: Record = { '/dr': 'DR 재해복구 관제', '/network': '네트워크 장비 관제', '/csap': 'CSAP 보안 점검', + // GUARDiA 확장 v3 + '/kpi': 'KPI 대시보드', + '/bi': 'BI 대시보드', + '/billing': '구독 · 과금 관리', + '/integrations': '외부 연동 허브', + '/ai-platform': 'AI 플랫폼', } export function AppLayout() { diff --git a/workspace/guardia-manager/frontend/src/components/layout/Sidebar.tsx b/workspace/guardia-manager/frontend/src/components/layout/Sidebar.tsx index 1f11bd48..77302877 100644 --- a/workspace/guardia-manager/frontend/src/components/layout/Sidebar.tsx +++ b/workspace/guardia-manager/frontend/src/components/layout/Sidebar.tsx @@ -35,6 +35,14 @@ const NAV: NavItem[] = [ { label: '네트워크 장비', icon: '', path: '/network' }, { label: 'CSAP 점검', icon: '', path: '/csap' }, ]}, + // ── GUARDiA 확장 v3 ── + { label: '분석 · KPI', icon: '📈', children: [ + { label: 'KPI 대시보드', icon: '', path: '/kpi' }, + { label: 'BI 대시보드', icon: '', path: '/bi' }, + ]}, + { label: 'AI 플랫폼', icon: '🧠', path: '/ai-platform' }, + { label: '외부 연동', icon: '🔗', path: '/integrations' }, + { label: '구독 · 과금', icon: '💰', path: '/billing' }, ] /* Variant 스타일 색상 상수 */ diff --git a/workspace/guardia-manager/frontend/src/pages/AiPlatform.tsx b/workspace/guardia-manager/frontend/src/pages/AiPlatform.tsx new file mode 100644 index 00000000..d0fd7996 --- /dev/null +++ b/workspace/guardia-manager/frontend/src/pages/AiPlatform.tsx @@ -0,0 +1,199 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +type Tab = 'insights' | 'learning' | 'predict' | 'benchmark' + +export default function AiPlatform() { + const [tab, setTab] = useState('insights') + const tabs: { id: Tab; label: string; icon: string }[] = [ + { id: 'insights', label: 'AI 인사이트', icon: '🧠' }, + { id: 'learning', label: 'Learning Loop', icon: '🔄' }, + { id: 'predict', label: '예측 분석', icon: '🔮' }, + { id: 'benchmark', label: '벤치마킹', icon: '📊' }, + ] + return ( +
+

🧠 AI 플랫폼

+
+ {tabs.map(t => ( + + ))} +
+ {tab === 'insights' && } + {tab === 'learning' && } + {tab === 'predict' && } + {tab === 'benchmark' && } +
+ ) +} + +function Card({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ) +} + +function InsightsTab() { + const [weekly, setWeekly] = useState(null) + const [anomalies, setAnomalies] = useState(null) + useEffect(() => { + guardiaApi.get('/api/insights/weekly').then((r: any) => setWeekly(r.data)).catch(() => {}) + guardiaApi.get('/api/insights/anomalies').then((r: any) => setAnomalies(r.data)).catch(() => {}) + }, []) + return ( + <> + {anomalies?.anomalies?.length > 0 && ( +
+ ⚠️ 이상 감지 ({anomalies.anomalies.length}건) + {anomalies.anomalies.map((a: any, i: number) => ( +
{a.message}
+ ))} +
+ )} +
+ {weekly && [ + { label: '신규 SR', val: weekly.stats?.total || 0, color: '#003366' }, + { label: '완료율', val: `${weekly.stats?.completion_rate || 0}%`, color: '#10B981' }, + { label: '미처리', val: weekly.stats?.open || 0, color: '#EF4444' }, + ].map(s => ( +
+
{s.val}
+
{s.label}
+
+ ))} +
+ +

+ {weekly?.ai_insight || '데이터 수집 중...'} +

+
+ + ) +} + +function LearningTab() { + const [status, setStatus] = useState(null) + const [quality, setQuality] = useState(null) + const [history, setHistory] = useState([]) + useEffect(() => { + guardiaApi.get('/api/learn/status').then((r: any) => setStatus(r.data)).catch(() => {}) + guardiaApi.get('/api/learn/quality').then((r: any) => setQuality(r.data)).catch(() => {}) + guardiaApi.get('/api/learn/history?limit=10').then((r: any) => setHistory(r.data || [])).catch(() => {}) + }, []) + async function startTrain() { + await guardiaApi.post('/api/learn/train') + guardiaApi.get('/api/learn/history?limit=10').then((r: any) => setHistory(r.data || [])).catch(() => {}) + } + return ( + <> +
+ +
{status?.available_samples || 0}
+
수집 가능 샘플
+
+ RAG 피드백: {status?.high_quality_rag || 0} · SR 이력: {status?.sr_samples || 0} +
+ +
+ +
{quality?.quality_grade || 'N/A'}
+
+ 평균 {quality?.avg_rating || 0}점 · 긍정 {quality?.positive_rate || 0}% +
+
+
+ + + + {['모델', '상태', '샘플', '시작'].map(h => )} + + + {history.slice(0, 8).map((h: any) => ( + + + + + + + ))} + {!history.length && } + +
{h}
{h.model_name || '-'}{h.status}{h.samples_used || 0}{h.started_at ? new Date(h.started_at).toLocaleDateString('ko-KR') : '-'}
이력 없음
+
+ + ) +} + +function PredictTab() { + const [sla, setSla] = useState(null) + const [surge, setSurge] = useState(null) + useEffect(() => { + guardiaApi.get('/api/predict/sla-breach').then((r: any) => setSla(r.data)).catch(() => {}) + guardiaApi.get('/api/predict/sr-surge').then((r: any) => setSurge(r.data)).catch(() => {}) + }, []) + return ( +
+ +
+ {Math.round((sla?.breach_probability_7d || 0) * 100)}% +
+
현재 SLA: {sla?.current_rate || 0}% · 목표: {sla?.target || 95}%
+ {sla?.insight &&

{sla.insight}

} +
+ +
+ {surge?.surge_ratio || 1}x +
+
오늘 {surge?.today_count || 0}건 · 7일 평균 {surge?.avg_7d || 0}건
+ {surge?.insight &&

{surge.insight}

} +
+
+ ) +} + +function BenchmarkTab() { + const [comp, setComp] = useState(null) + const [rank, setRank] = useState(null) + useEffect(() => { + guardiaApi.get('/api/benchmark/comparison').then((r: any) => setComp(r.data)).catch(() => {}) + guardiaApi.get('/api/benchmark/my-rank').then((r: any) => setRank(r.data)).catch(() => {}) + }, []) + async function contribute() { await guardiaApi.post('/api/benchmark/contribute') } + return ( + <> + +
+ {(comp?.comparison || []).map((m: any) => ( +
+
{m.metric}
+
+ {m.mine}{m.unit} +
+
업계 평균: {m.industry}{m.unit}
+
+ {m.status === 'ABOVE' ? '▲ 평균 이상' : '▼ 평균 이하'} +
+
+ ))} +
+
+
+ + 기여 시 더 정확한 벤치마킹 가능 (완전 익명화) +
+ + ) +} diff --git a/workspace/guardia-manager/frontend/src/pages/BiAnalytics.tsx b/workspace/guardia-manager/frontend/src/pages/BiAnalytics.tsx new file mode 100644 index 00000000..d1e2e2e6 --- /dev/null +++ b/workspace/guardia-manager/frontend/src/pages/BiAnalytics.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from 'react' +import { + LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, + XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, +} from 'recharts' +import { guardiaApi } from '../api/clients' + +const COLORS = ['#003366', '#00A0C8', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'] + +export default function BiAnalytics() { + const [overview, setOverview] = useState(null) + const [srTrend, setSrTrend] = useState(null) + const [pie, setPie] = useState(null) + const [mttr, setMttr] = useState(null) + const [tab, setTab] = useState<'overview' | 'trend' | 'engineer' | 'mttr'>('overview') + + useEffect(() => { loadData() }, []) + + async function loadData() { + const [o, t, p, m] = await Promise.all([ + guardiaApi.get('/api/bi/overview').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/bi/sr-trend?days=30').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/bi/category-pie?days=30').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/bi/mttr-trend?months=6').then((r: any) => r.data).catch(() => null), + ]) + setOverview(o); setSrTrend(t); setPie(p); setMttr(m) + } + + const tabs = [ + { id: 'overview', label: '개요' }, + { id: 'trend', label: 'SR 트렌드' }, + { id: 'engineer', label: '엔지니어' }, + { id: 'mttr', label: 'MTTR' }, + ] as const + + return ( +
+
+

📊 BI 대시보드

+ +
+ + {/* 탭 */} +
+ {tabs.map(t => ( + + ))} +
+ + {/* 개요 탭 */} + {tab === 'overview' && ( + <> +
+ {(overview?.cards || []).map((c: any) => ( +
+
{c.value}
+
{c.label} ({c.unit})
+
+ ))} +
+ {pie && ( +
+

SR 카테고리 분포 (30일)

+ + + `${category} ${pct}%`}> + {(pie.data || []).map((_: any, i: number) => )} + + + + +
+ )} + + )} + + {/* SR 트렌드 탭 */} + {tab === 'trend' && srTrend && ( +
+

SR 생성/완료 추이 (30일)

+ + ({ + date: d.slice(5), + 신규: srTrend.datasets[0]?.data[i] || 0, + 완료: srTrend.datasets[1]?.data[i] || 0, + }))}> + + + + + + + + + +
+ )} + + {/* MTTR 탭 */} + {tab === 'mttr' && mttr && ( +
+

MTTR 월별 추이 (시간)

+ + ({ month: r.month, mttr: r.mttr_hours }))}> + + + + `${v}시간`} /> + + + +
+ )} + + {tab === 'engineer' && } +
+ ) +} + +function EngineerLoad() { + const [data, setData] = useState(null) + useEffect(() => { + guardiaApi.get('/api/bi/engineer-load?days=30').then((r: any) => setData(r.data)).catch(() => {}) + }, []) + if (!data) return
+ const combined = (data.labels || []).map((name: string, i: number) => ({ + name, 완료: data.datasets[0]?.data[i] || 0, 진행중: data.datasets[1]?.data[i] || 0, + })) + return ( +
+

엔지니어별 SR 워크로드

+ + + + + + + + + + + +
+ ) +} diff --git a/workspace/guardia-manager/frontend/src/pages/BillingManage.tsx b/workspace/guardia-manager/frontend/src/pages/BillingManage.tsx new file mode 100644 index 00000000..e788de19 --- /dev/null +++ b/workspace/guardia-manager/frontend/src/pages/BillingManage.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +export default function BillingManage() { + const [sub, setSub] = useState(null) + const [usage, setUsage] = useState(null) + const [invoices, setInvoices] = useState([]) + const [plans, setPlans] = useState([]) + + useEffect(() => { load() }, []) + + async function load() { + const [s, u, i, p] = await Promise.all([ + guardiaApi.get('/api/billing/subscription').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/billing/usage').then((r: any) => r.data).catch(() => null), + guardiaApi.get('/api/billing/invoices').then((r: any) => r.data).catch(() => []), + guardiaApi.get('/api/billing/plans').then((r: any) => r.data).catch(() => []), + ]) + setSub(s); setUsage(u); setInvoices(i || []); setPlans(p || []) + } + + async function generateInvoice() { + await guardiaApi.post('/api/billing/invoices/generate') + load() + } + + const pctColor = (pct: number) => pct > 90 ? '#ef4444' : pct > 70 ? '#f59e0b' : '#003366' + + return ( +
+

💰 구독 · 과금 관리

+ +
+ {/* 현재 구독 */} +
+

📋 현재 구독

+
{sub?.plan || 'COMMUNITY'}
+
+ {sub?.price ? `월 ${sub.price.toLocaleString()}원` : '무료'} · {sub?.billing_cycle || 'MONTHLY'} +
+
+ {plans.slice(0, 3).map((p: any) => ( + + ))} +
+
+ + {/* 사용량 */} +
+

📊 이번 달 사용량

+ {usage && [ + { label: '서버', used: usage.servers?.used || 0, limit: usage.servers?.limit || 20 }, + { label: '사용자', used: usage.users?.used || 0, limit: usage.users?.limit || 10 }, + ].map(item => { + const pct = item.limit > 0 ? Math.min(100, Math.round(item.used / item.limit * 100)) : 0 + return ( +
+
+ {item.label} + {item.used} / {item.limit < 0 ? '∞' : item.limit} +
+
+
+
+
+ ) + })} +
SR (이번 달): {usage?.sr_this_month || 0}건
+
+
+ + {/* 청구서 */} +
+
+

🧾 청구서 이력 ({invoices.length}건)

+ +
+ + + + {['기간', '플랜', '금액', '상태', '생성일'].map(h => ( + + ))} + + + + {invoices.length === 0 ? ( + + ) : invoices.slice(0, 12).map((inv: any) => ( + + + + + + + + ))} + +
{h}
청구서 없음
{inv.period}{inv.plan || '-'}{inv.amount ? `${inv.amount.toLocaleString()}원` : '무료'} + + {inv.status} + + + {inv.created_at ? new Date(inv.created_at).toLocaleDateString('ko-KR') : '-'} +
+
+
+ ) +} diff --git a/workspace/guardia-manager/frontend/src/pages/IntegrationHub.tsx b/workspace/guardia-manager/frontend/src/pages/IntegrationHub.tsx new file mode 100644 index 00000000..61077839 --- /dev/null +++ b/workspace/guardia-manager/frontend/src/pages/IntegrationHub.tsx @@ -0,0 +1,213 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +type Tab = 'jira' | 'slack' | 'servicenow' | 'erp' | 'kakao' | 'sso' + +export default function IntegrationHub() { + const [tab, setTab] = useState('jira') + const tabs: { id: Tab; label: string; icon: string }[] = [ + { id: 'jira', label: 'Jira', icon: '🔵' }, + { id: 'slack', label: 'Slack', icon: '💬' }, + { id: 'servicenow', label: 'ServiceNow', icon: '🔗' }, + { id: 'erp', label: 'ERP', icon: '🏢' }, + { id: 'kakao', label: '카카오', icon: '💛' }, + { id: 'sso', label: 'SSO', icon: '🔐' }, + ] + + return ( +
+

🔗 외부 연동 허브

+
+ {tabs.map(t => ( + + ))} +
+ {tab === 'jira' && } + {tab === 'slack' && } + {tab === 'servicenow' && } + {tab === 'erp' && } + {tab === 'kakao' && } + {tab === 'sso' && } +
+ ) +} + +function Card({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { + return
{children}
+} + +function JiraTab() { + const [cfg, setCfg] = useState(null) + const [mappings, setMappings] = useState([]) + useEffect(() => { + guardiaApi.get('/api/jira/config').then((r: any) => setCfg(r.data)).catch(() => {}) + guardiaApi.get('/api/jira/mappings').then((r: any) => setMappings(r.data || [])).catch(() => {}) + }, []) + return ( +
+ +

Jira 연동 설정

+ {cfg ? ( +
+
URL: {cfg.base_url}
+
프로젝트: {cfg.project_key}
+
✅ 연동됨
+
+ ) :

설정 없음

} +
+ +

SR-Issue 매핑 ({mappings.length}건)

+ + + {['SR ID', 'Jira Key', '동기화 시간'].map(h => )} + + + {mappings.slice(0, 8).map((m: any) => ( + + + + + + ))} + {!mappings.length && } + +
{h}
SR-{m.sr_id}{m.jira_key}{m.synced_at ? new Date(m.synced_at).toLocaleDateString('ko-KR') : '-'}
매핑 없음
+
+
+ ) +} + +function SlackTab() { + const [cfg, setCfg] = useState(null) + const [webhook, setWebhook] = useState('') + const [channel, setChannel] = useState('#guardia-ops') + useEffect(() => { guardiaApi.get('/api/slack/config').then((r: any) => setCfg(r.data)).catch(() => {}) }, []) + async function save() { + await guardiaApi.post('/api/slack/config', { name: 'Slack', webhook_url: webhook, default_channel: channel }) + const r: any = await guardiaApi.get('/api/slack/config') + setCfg(r.data) + } + async function testMsg() { await guardiaApi.get('/api/slack/test') } + return ( + +

Slack 연동

+ {cfg &&
✅ 연동됨 ({cfg.default_channel})
} +
+ + setWebhook(e.target.value)} placeholder="https://hooks.slack.com/..." style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> +
+
+ + setChannel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> +
+
+ + {cfg && } +
+
+ ) +} + +function ServiceNowTab() { + const [url, setUrl] = useState(''); const [user, setUser] = useState(''); const [pw, setPw] = useState('') + async function save() { await guardiaApi.post('/api/servicenow/config', { instance_url: url, username: user, password: pw }) } + return ( + +

ServiceNow 연동

+ {[['인스턴스 URL', url, setUrl, 'https://company.service-now.com'], ['사용자명', user, setUser, ''], ['비밀번호', pw, setPw, '']].map(([label, val, set, ph]: any) => ( +
+ + (set as Function)(e.target.value)} placeholder={ph as string} + style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} /> +
+ ))} + +
+ ) +} + +function ERPTab() { + const [cfgs, setCfgs] = useState([]) + useEffect(() => { guardiaApi.get('/api/erp/config').then((r: any) => setCfgs(r.data || [])).catch(() => {}) }, []) + return ( +
+

등록된 ERP 연동 ({cfgs.length}개)

+ {cfgs.map((c: any) => ( + + 🏢 +
+
{c.name}
+
{c.erp_type} · {c.base_url}
+
+
+ ))} + {!cfgs.length &&

등록된 연동 없음

} +
+ ) +} + +function KakaoTab() { + const [cfg, setCfg] = useState(null) + const [history, setHistory] = useState([]) + useEffect(() => { + guardiaApi.get('/api/kakao/config').then((r: any) => setCfg(r.data)).catch(() => {}) + guardiaApi.get('/api/kakao/history').then((r: any) => setHistory(r.data || [])).catch(() => {}) + }, []) + return ( +
+ +

카카오 알림톡

+ {cfg ?
✅ 연동됨
발신: {cfg.sender}
+ :

미설정

} +
+ +

발송 이력

+ + + {['템플릿', '수신', '결과', '시간'].map(h => )} + + + {history.slice(0, 8).map((h: any) => ( + + + + + + + ))} + {!history.length && } + +
{h}
{h.template}{h.receivers}명{h.success ? '성공' : '실패'}{h.sent_at ? new Date(h.sent_at).toLocaleDateString('ko-KR') : '-'}
이력 없음
+
+
+ ) +} + +function SSOTab() { + const [configs, setConfigs] = useState([]) + useEffect(() => { guardiaApi.get('/api/sso/config').then((r: any) => setConfigs(r.data || [])).catch(() => {}) }, []) + return ( +
+

등록된 SSO IdP ({configs.length}개)

+ {configs.map((c: any) => ( + + {c.provider_type === 'SAML' ? '🏛️' : '🔑'} +
+
{c.name}
+
{c.provider_type} · {c.is_active ? '활성' : '비활성'}
+
+
테스트 → + + ))} + {!configs.length &&

등록된 IdP 없음

} +
+ 📄 SP Metadata XML +
+
+ ) +} diff --git a/workspace/guardia-manager/frontend/src/pages/KpiDashboard.tsx b/workspace/guardia-manager/frontend/src/pages/KpiDashboard.tsx new file mode 100644 index 00000000..f68e35df --- /dev/null +++ b/workspace/guardia-manager/frontend/src/pages/KpiDashboard.tsx @@ -0,0 +1,119 @@ +import { useEffect, useState } from 'react' +import { guardiaApi } from '../api/clients' + +const STATUS_COLOR: Record = { + GREEN: '#22c55e', YELLOW: '#f59e0b', RED: '#ef4444', NO_DATA: '#94a3b8', +} +const STATUS_ICON: Record = { + GREEN: '✅', YELLOW: '⚠️', RED: '❌', NO_DATA: '❔', +} + +interface KPI { + id: number; name: string; display_name: string + unit: string; direction: string; target: number + current_value: number | null; status: string + achievement_pct: number | null; last_calculated_at: string | null +} + +interface Dashboard { kpis: KPI[]; summary: Record; overall_status: string } + +function GaugeBar({ pct }: { pct: number | null }) { + const p = Math.min(100, Math.max(0, pct ?? 0)) + const color = p >= 95 ? '#22c55e' : p >= 80 ? '#f59e0b' : '#ef4444' + return ( +
+
+
+ ) +} + +export default function KpiDashboard() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { load() }, []) + + async function load() { + setLoading(true) + try { + const d = await guardiaApi.get('/api/kpi/dashboard').then((r: any) => r.data) + setData(d) + } catch { /* 오류 무시 */ } + setLoading(false) + } + + async function applyTemplates() { + await guardiaApi.post('/api/kpi/apply-template', { + template_names: ['MTTR', 'FCR', 'SLA_COMPLIANCE', 'SR_BACKLOG', 'DEPLOY_SUCCESS_RATE'], + }) + load() + } + + async function recalc(id: number) { + await guardiaApi.post(`/api/kpi/${id}/calculate`) + load() + } + + if (loading) return
⏳ 로딩 중...
+ + const overall = data?.overall_status || 'NO_DATA' + const summary = data?.summary || {} + + return ( +
+ {/* 헤더 */} +
+
+

+ {STATUS_ICON[overall]} KPI 대시보드 +

+

+ GREEN:{summary.GREEN||0} · YELLOW:{summary.YELLOW||0} · RED:{summary.RED||0} +

+
+
+ + {(!data?.kpis.length) && ( + + )} +
+
+ + {/* KPI 카드 그리드 */} + {data?.kpis.length === 0 ? ( +
+

KPI가 없습니다.

+ +
+ ) : ( +
+ {data?.kpis.map(k => ( +
+
+ {k.display_name} + {k.status} +
+
+ {k.current_value !== null ? k.current_value : '—'} + {k.unit} +
+
+ 목표: {k.target}{k.unit} · 달성: {k.achievement_pct !== null ? `${k.achievement_pct}%` : '—'} +
+ + +
+ ))} +
+ )} +
+ ) +} diff --git a/workspace/guardia-messenger/app/(tabs)/_layout.tsx b/workspace/guardia-messenger/app/(tabs)/_layout.tsx index 4088f3bb..c272dc86 100644 --- a/workspace/guardia-messenger/app/(tabs)/_layout.tsx +++ b/workspace/guardia-messenger/app/(tabs)/_layout.tsx @@ -84,6 +84,13 @@ export default function TabLayout() { tabBarIcon: ({ focused }) => , }} /> + , + }} + /> = { + CRITICAL: '#ef4444', WARNING: '#f59e0b', NORMAL: '#22c55e', NO_DATA: '#94a3b8', +} + +export default function InsightsScreen() { + const { token } = useAuth() + const [weekly, setWeekly] = useState(null) + const [anomaly, setAnomaly] = useState(null) + const [predict, setPredict] = useState(null) + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + + useEffect(() => { load() }, []) + + async function load() { + setLoading(true) + try { + const [w, a, p] = await Promise.all([ + apiClient.get('/api/insights/weekly', token), + apiClient.get('/api/insights/anomalies', token), + apiClient.get('/api/predict/sla-breach', token), + ]) + setWeekly(w); setAnomaly(a); setPredict(p) + } catch { /* 오류 무시 */ } + setLoading(false); setRefreshing(false) + } + + function onRefresh() { setRefreshing(true); load() } + + if (loading) return ( + + + + ) + + return ( + }> + {/* 이상 감지 알림 */} + {(anomaly?.anomalies?.length || 0) > 0 && ( + + ⚠️ 이상 감지 {anomaly!.anomalies.length}건 + {anomaly!.anomalies.map((a: any, i: number) => ( + {a.message} + ))} + + )} + + {/* 통계 카드 */} + + {[ + { label: '신규 SR', val: weekly?.stats?.total || 0, color: COLORS.primary }, + { label: '완료율', val: `${weekly?.stats?.completion_rate || 0}%`, color: '#22c55e' }, + { label: '미처리', val: weekly?.stats?.open || 0, color: '#ef4444' }, + ].map(item => ( + + {item.val} + {item.label} + + ))} + + + {/* SLA 예측 */} + {predict && ( + + 📉 SLA 위반 예측 (7일) + + {Math.round((predict.breach_probability_7d || 0) * 100)}% + + 현재 SLA: {predict.current_rate || 0}% + {predict.insight ? {predict.insight} : null} + + )} + + {/* AI 주간 인사이트 */} + {weekly?.ai_insight && ( + + 🤖 AI 주간 인사이트 + {weekly.ai_insight} + + )} + + {/* 상위 카테고리 */} + {(weekly?.top_categories?.length || 0) > 0 && ( + + 📊 SR 상위 카테고리 + {weekly!.top_categories.map((c: any, i: number) => ( + + {c.category} + + + + {c.count}건 + + ))} + + )} + + ) +} + +const s = StyleSheet.create({ + scroll: { flex: 1, backgroundColor: '#f8fafc' }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + alertCard: { margin: 12, padding: 14, backgroundColor: '#fef2f2', borderRadius: 12, borderLeftWidth: 4, borderLeftColor: '#ef4444' }, + alertTitle: { fontWeight: '700', fontSize: 14, color: '#7f1d1d', marginBottom: 4 }, + alertMsg: { fontSize: 12, color: '#991b1b', marginTop: 3 }, + statsRow: { flexDirection: 'row', padding: 12, gap: 8 }, + statCard: { flex: 1, backgroundColor: '#fff', borderRadius: 12, padding: 14, alignItems: 'center', borderTopWidth: 3, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 4, elevation: 2 }, + statVal: { fontSize: 24, fontWeight: '800' }, + statLabel: { fontSize: 11, color: '#64748b', marginTop: 2 }, + card: { margin: 12, marginTop: 0, backgroundColor: '#fff', borderRadius: 12, padding: 16, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 4, elevation: 2 }, + cardTitle: { fontSize: 14, fontWeight: '700', color: '#1e293b', marginBottom: 10 }, + bigNum: { fontSize: 36, fontWeight: '800' }, + subText: { fontSize: 12, color: '#64748b', marginTop: 2 }, + insightText: { fontSize: 13, lineHeight: 20, color: '#374151', marginTop: 8 }, + categoryRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 }, + categoryName: { fontSize: 12, width: 80, color: '#374151' }, + barBg: { flex: 1, backgroundColor: '#f1f5f9', borderRadius: 3, height: 6 }, + barFill: { height: 6, backgroundColor: COLORS.primary, borderRadius: 3 }, + categoryCount:{ fontSize: 11, color: '#64748b', width: 30, textAlign: 'right' }, +})