zioinfo-mail/workspace/guardia-manager/frontend/src/pages/IntegrationHub.tsx
DESKTOP-TKLFCPR\ython caa70a608b feat(frontend): 나머지 개발 — ITSM/Manager/Messenger 신규 UI
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 <noreply@anthropic.com>
2026-06-02 06:24:54 +09:00

214 lines
11 KiB
TypeScript

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<Tab>('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 (
<div style={{ padding: '24px 28px' }}>
<h2 style={{ margin: '0 0 20px', fontSize: 20, fontWeight: 700 }}>🔗 </h2>
<div style={{ display: 'flex', gap: 4, marginBottom: 20, borderBottom: '1px solid #e2e8f0' }}>
{tabs.map(t => (
<button key={t.id} onClick={() => setTab(t.id)} style={{
padding: '8px 16px', border: 'none', background: 'none', cursor: 'pointer',
fontSize: 13, fontWeight: tab === t.id ? 700 : 400,
color: tab === t.id ? '#003366' : '#64748b',
borderBottom: tab === t.id ? '2px solid #003366' : '2px solid transparent',
}}>{t.icon} {t.label}</button>
))}
</div>
{tab === 'jira' && <JiraTab />}
{tab === 'slack' && <SlackTab />}
{tab === 'servicenow' && <ServiceNowTab />}
{tab === 'erp' && <ERPTab />}
{tab === 'kakao' && <KakaoTab />}
{tab === 'sso' && <SSOTab />}
</div>
)
}
function Card({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) {
return <div style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: 10, padding: 20, ...style }}>{children}</div>
}
function JiraTab() {
const [cfg, setCfg] = useState<any>(null)
const [mappings, setMappings] = useState<any[]>([])
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 (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 16 }}>
<Card>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}>Jira </h3>
{cfg ? (
<div style={{ fontSize: 13, lineHeight: 2 }}>
<div>URL: {cfg.base_url}</div>
<div>: {cfg.project_key}</div>
<div style={{ color: '#10B981' }}> </div>
</div>
) : <p style={{ color: '#94a3b8', fontSize: 13 }}> </p>}
</Card>
<Card>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}>SR-Issue ({mappings.length})</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
<thead><tr style={{ borderBottom: '1px solid #f1f5f9' }}>
{['SR ID', 'Jira Key', '동기화 시간'].map(h => <th key={h} style={{ textAlign: 'left', padding: '6px 8px', color: '#64748b', fontSize: 11 }}>{h}</th>)}
</tr></thead>
<tbody>
{mappings.slice(0, 8).map((m: any) => (
<tr key={m.id} style={{ borderBottom: '1px solid #f8fafc' }}>
<td style={{ padding: '8px' }}>SR-{m.sr_id}</td>
<td style={{ padding: '8px', color: '#003366' }}>{m.jira_key}</td>
<td style={{ padding: '8px', color: '#94a3b8', fontSize: 11 }}>{m.synced_at ? new Date(m.synced_at).toLocaleDateString('ko-KR') : '-'}</td>
</tr>
))}
{!mappings.length && <tr><td colSpan={3} style={{ textAlign: 'center', padding: 16, color: '#94a3b8' }}> </td></tr>}
</tbody>
</table>
</Card>
</div>
)
}
function SlackTab() {
const [cfg, setCfg] = useState<any>(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 (
<Card style={{ maxWidth: 520 }}>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}>Slack </h3>
{cfg && <div style={{ fontSize: 13, marginBottom: 12, color: '#10B981' }}> ({cfg.default_channel})</div>}
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 }}>Webhook URL</label>
<input value={webhook} onChange={e => 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' }} />
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 }}> </label>
<input value={channel} onChange={e => setChannel(e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, boxSizing: 'border-box' }} />
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={save} style={{ padding: '7px 16px', background: '#003366', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 12 }}></button>
{cfg && <button onClick={testMsg} style={{ padding: '7px 16px', border: '1px solid #e2e8f0', borderRadius: 6, cursor: 'pointer', fontSize: 12 }}></button>}
</div>
</Card>
)
}
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 (
<Card style={{ maxWidth: 520 }}>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}>ServiceNow </h3>
{[['인스턴스 URL', url, setUrl, 'https://company.service-now.com'], ['사용자명', user, setUser, ''], ['비밀번호', pw, setPw, '']].map(([label, val, set, ph]: any) => (
<div key={label as string} style={{ marginBottom: 12 }}>
<label style={{ fontSize: 12, fontWeight: 600, display: 'block', marginBottom: 4 }}>{label as string}</label>
<input type={label === '비밀번호' ? 'password' : 'text'} value={val as string} onChange={(e: 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' }} />
</div>
))}
<button onClick={save} style={{ padding: '7px 16px', background: '#003366', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 12 }}></button>
</Card>
)
}
function ERPTab() {
const [cfgs, setCfgs] = useState<any[]>([])
useEffect(() => { guardiaApi.get('/api/erp/config').then((r: any) => setCfgs(r.data || [])).catch(() => {}) }, [])
return (
<div>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}> ERP ({cfgs.length})</h3>
{cfgs.map((c: any) => (
<Card key={c.id} style={{ marginBottom: 10, display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 24 }}>🏢</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 13 }}>{c.name}</div>
<div style={{ fontSize: 11, color: '#64748b' }}>{c.erp_type} · {c.base_url}</div>
</div>
</Card>
))}
{!cfgs.length && <p style={{ color: '#94a3b8' }}> </p>}
</div>
)
}
function KakaoTab() {
const [cfg, setCfg] = useState<any>(null)
const [history, setHistory] = useState<any[]>([])
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 (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 16 }}>
<Card>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}> </h3>
{cfg ? <div style={{ fontSize: 13, color: '#10B981' }}> <br/><span style={{ color: '#64748b' }}>: {cfg.sender}</span></div>
: <p style={{ color: '#94a3b8', fontSize: 13 }}></p>}
</Card>
<Card>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}> </h3>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
<thead><tr style={{ borderBottom: '1px solid #f1f5f9' }}>
{['템플릿', '수신', '결과', '시간'].map(h => <th key={h} style={{ textAlign: 'left', padding: '4px 8px', color: '#64748b', fontSize: 11 }}>{h}</th>)}
</tr></thead>
<tbody>
{history.slice(0, 8).map((h: any) => (
<tr key={h.id} style={{ borderBottom: '1px solid #f8fafc' }}>
<td style={{ padding: '6px 8px' }}>{h.template}</td>
<td style={{ padding: '6px 8px' }}>{h.receivers}</td>
<td style={{ padding: '6px 8px', color: h.success ? '#10B981' : '#EF4444' }}>{h.success ? '성공' : '실패'}</td>
<td style={{ padding: '6px 8px', color: '#94a3b8', fontSize: 11 }}>{h.sent_at ? new Date(h.sent_at).toLocaleDateString('ko-KR') : '-'}</td>
</tr>
))}
{!history.length && <tr><td colSpan={4} style={{ textAlign: 'center', padding: 12, color: '#94a3b8' }}> </td></tr>}
</tbody>
</table>
</Card>
</div>
)
}
function SSOTab() {
const [configs, setConfigs] = useState<any[]>([])
useEffect(() => { guardiaApi.get('/api/sso/config').then((r: any) => setConfigs(r.data || [])).catch(() => {}) }, [])
return (
<div>
<h3 style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}> SSO IdP ({configs.length})</h3>
{configs.map((c: any) => (
<Card key={c.id} style={{ marginBottom: 10, display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 24 }}>{c.provider_type === 'SAML' ? '🏛️' : '🔑'}</span>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 13 }}>{c.name}</div>
<div style={{ fontSize: 11, color: '#64748b' }}>{c.provider_type} · {c.is_active ? '활성' : '비활성'}</div>
</div>
<a href={`/api/sso/login/${c.id}`} style={{ fontSize: 12, color: '#003366', textDecoration: 'none' }}> </a>
</Card>
))}
{!configs.length && <p style={{ color: '#94a3b8' }}> IdP </p>}
<div style={{ marginTop: 16 }}>
<a href="/api/sso/metadata" target="_blank" style={{ fontSize: 12, color: '#64748b' }}>📄 SP Metadata XML</a>
</div>
</div>
)
}