sync: update from workspace (latest ITSM/CICD/DR changes)
This commit is contained in:
parent
35c9f9c2a9
commit
efcc771263
@ -32,6 +32,7 @@ const AiPlatform = lazy(() => import('./pages/AiPlatform'))
|
|||||||
// ── GUARDiA 기능 개선 v4 ──
|
// ── GUARDiA 기능 개선 v4 ──
|
||||||
const AppDistribution = lazy(() => import('./pages/AppDistribution'))
|
const AppDistribution = lazy(() => import('./pages/AppDistribution'))
|
||||||
const NotificationRules = lazy(() => import('./pages/NotificationRules'))
|
const NotificationRules = lazy(() => import('./pages/NotificationRules'))
|
||||||
|
const InstallGuide = lazy(() => import('./pages/InstallGuide'))
|
||||||
|
|
||||||
function Loading() {
|
function Loading() {
|
||||||
return (
|
return (
|
||||||
@ -79,6 +80,7 @@ export default function App() {
|
|||||||
{/* GUARDiA 기능 개선 v4 */}
|
{/* GUARDiA 기능 개선 v4 */}
|
||||||
<Route path="app-distribution" element={<AppDistribution />} />
|
<Route path="app-distribution" element={<AppDistribution />} />
|
||||||
<Route path="notification-rules" element={<NotificationRules />} />
|
<Route path="notification-rules" element={<NotificationRules />} />
|
||||||
|
<Route path="install-guide" element={<InstallGuide />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -46,6 +46,7 @@ const NAV: NavItem[] = [
|
|||||||
// ── GUARDiA 기능 개선 v4 ──
|
// ── GUARDiA 기능 개선 v4 ──
|
||||||
{ label: '앱 배포', icon: '📱', path: '/app-distribution' },
|
{ label: '앱 배포', icon: '📱', path: '/app-distribution' },
|
||||||
{ label: '알림 규칙', icon: '🔔', path: '/notification-rules' },
|
{ label: '알림 규칙', icon: '🔔', path: '/notification-rules' },
|
||||||
|
{ label: '설치 가이드', icon: '📦', path: '/install-guide' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/* Variant 스타일 색상 상수 */
|
/* Variant 스타일 색상 상수 */
|
||||||
|
|||||||
789
frontend/src/pages/InstallGuide.tsx
Normal file
789
frontend/src/pages/InstallGuide.tsx
Normal file
@ -0,0 +1,789 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type EnvType = 'closed' | 'open'
|
||||||
|
type SystemType = 'itsm' | 'manager' | 'messenger' | 'all'
|
||||||
|
|
||||||
|
const S = {
|
||||||
|
page: { padding: '24px 28px', maxWidth: 1100 },
|
||||||
|
tabBar: { display: 'flex', gap: 4, marginBottom: 24, borderBottom: '2px solid #e2e8f0', paddingBottom: 0 },
|
||||||
|
tab: (active: boolean): React.CSSProperties => ({
|
||||||
|
padding: '10px 20px', border: 'none', background: 'none', cursor: 'pointer',
|
||||||
|
fontSize: 14, fontWeight: active ? 700 : 400,
|
||||||
|
color: active ? '#003366' : '#64748b',
|
||||||
|
borderBottom: active ? '2px solid #003366' : '2px solid transparent',
|
||||||
|
marginBottom: -2,
|
||||||
|
}),
|
||||||
|
card: { background: '#fff', border: '1px solid #e2e8f0', borderRadius: 10, padding: 20, marginBottom: 16 },
|
||||||
|
step: (color: string): React.CSSProperties => ({
|
||||||
|
display: 'flex', gap: 0, marginBottom: 20,
|
||||||
|
}),
|
||||||
|
stepNum: (color: string): React.CSSProperties => ({
|
||||||
|
width: 32, height: 32, borderRadius: '50%', background: color, color: '#fff',
|
||||||
|
fontWeight: 800, fontSize: 14, display: 'flex', alignItems: 'center',
|
||||||
|
justifyContent: 'center', flexShrink: 0, marginRight: 16, marginTop: 2,
|
||||||
|
}),
|
||||||
|
code: { background: '#0f172a', color: '#e2e8f0', borderRadius: 8, padding: '12px 16px',
|
||||||
|
fontSize: 12, fontFamily: 'monospace', lineHeight: 1.8,
|
||||||
|
overflowX: 'auto' as const, whiteSpace: 'pre' as const, margin: '8px 0 0' },
|
||||||
|
warn: { background: '#fef3c7', border: '1px solid #fcd34d', borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 12 },
|
||||||
|
info: { background: '#eff6ff', border: '1px solid #bfdbfe', borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 12 },
|
||||||
|
danger: { background: '#fef2f2', border: '1px solid #fca5a5', borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 12 },
|
||||||
|
success: { background: '#f0fdf4', border: '1px solid #bbf7d0', borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 12 },
|
||||||
|
badge: (color: string): React.CSSProperties => ({
|
||||||
|
display: 'inline-block', padding: '2px 8px', borderRadius: 8, fontSize: 11,
|
||||||
|
fontWeight: 700, background: color, color: '#fff', marginLeft: 8,
|
||||||
|
}),
|
||||||
|
h2: { fontSize: 18, fontWeight: 800, color: '#003366', margin: '0 0 4px' },
|
||||||
|
h3: { fontSize: 15, fontWeight: 700, color: '#1e293b', margin: '16px 0 8px' },
|
||||||
|
tag: (c: string, bg: string): React.CSSProperties => ({
|
||||||
|
padding: '3px 10px', borderRadius: 10, fontSize: 11, fontWeight: 700,
|
||||||
|
color: c, background: bg, display: 'inline-block',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 단계 컴포넌트 ──────────────────────────────────────────────────────────────
|
||||||
|
function Step({ num, title, color, children }: { num: number; title: string; color: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div style={S.step(color)}>
|
||||||
|
<div style={S.stepNum(color)}>{num}</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: 14, color: '#1e293b', marginBottom: 8 }}>{title}</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Code({ children }: { children: string }) {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard?.writeText(children).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000) })
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<pre style={S.code}>{children}</pre>
|
||||||
|
<button onClick={copy} style={{
|
||||||
|
position: 'absolute', top: 8, right: 8, padding: '3px 8px',
|
||||||
|
background: copied ? '#16a34a' : '#334155', color: '#fff', border: 'none',
|
||||||
|
borderRadius: 4, fontSize: 11, cursor: 'pointer',
|
||||||
|
}}>{copied ? '✓ 복사됨' : '복사'}</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Req() {
|
||||||
|
return (
|
||||||
|
<div style={{ ...S.card, borderLeft: '4px solid #003366' }}>
|
||||||
|
<div style={S.h3}>📋 시스템 요구사항</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 10, marginTop: 8 }}>
|
||||||
|
{[
|
||||||
|
{ k: 'OS', v: 'Ubuntu 22.04 LTS / RHEL 8+' },
|
||||||
|
{ k: 'CPU', v: '8코어 이상 (4코어 최소)' },
|
||||||
|
{ k: 'RAM', v: '16GB 이상 권장' },
|
||||||
|
{ k: 'GPU', v: '8GB VRAM (파인튜닝 선택)' },
|
||||||
|
{ k: 'Storage', v: '100GB SSD 이상' },
|
||||||
|
{ k: 'Python', v: '3.11 이상' },
|
||||||
|
{ k: 'Node.js', v: '20 LTS 이상' },
|
||||||
|
{ k: 'PostgreSQL', v: '16 이상' },
|
||||||
|
].map(r => (
|
||||||
|
<div key={r.k} style={{ background: '#f8fafc', borderRadius: 8, padding: '8px 12px', border: '1px solid #e2e8f0' }}>
|
||||||
|
<div style={{ fontWeight: 700, fontSize: 12, color: '#003366' }}>{r.k}</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#374151', marginTop: 2 }}>{r.v}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ ...S.info, marginTop: 12, marginBottom: 0 }}>
|
||||||
|
<strong>내부망 필수 포트:</strong> ITSM 8443(HTTPS), Manager 8090(HTTPS), Webmail 8025, API 9001, Ollama 11434, PostgreSQL 5432, Gitea 3000, Jenkins 8080
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 폐쇄망 설치 ───────────────────────────────────────────────────────────────
|
||||||
|
function ClosedNetwork({ system }: { system: SystemType }) {
|
||||||
|
const COLORS = ['#003366','#005A8C','#1d4ed8','#7c3aed','#059669','#dc2626','#d97706','#0891b2']
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={S.danger}>
|
||||||
|
<strong>🔒 폐쇄망 보안 주의사항</strong><br/>
|
||||||
|
• 설치 파일 SHA-256 무결성 검증 필수 — 변조 여부 반드시 확인<br/>
|
||||||
|
• USB·이동식 미디어 반입 시 보안 부서 승인 및 바이러스 검사 선행<br/>
|
||||||
|
• 설치 중 root 계정 사용 후 반드시 비활성화, opsagent 전용 계정으로 운영<br/>
|
||||||
|
• 설치 완료 즉시 기본 admin 비밀번호 변경 (초기값 사용 금지)<br/>
|
||||||
|
• 자격증명(IP·비밀번호·SSH 키)을 설치 로그·화면에 노출 금지
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Req />
|
||||||
|
|
||||||
|
{/* STEP 1: 오프라인 패키지 준비 */}
|
||||||
|
<Step num={1} title="오프라인 패키지 준비 (외부망 PC에서 수행)" color={COLORS[0]}>
|
||||||
|
<div style={S.info}>외부 인터넷이 가능한 PC에서 아래 작업 후 폐쇄망으로 이전합니다.</div>
|
||||||
|
<Code>{`# Python 패키지 다운로드
|
||||||
|
mkdir guardia-offline && cd guardia-offline
|
||||||
|
pip download fastapi sqlalchemy asyncpg pgvector httpx \\
|
||||||
|
paramiko cryptography pydantic uvicorn gunicorn \\
|
||||||
|
pillow qrcode --dest ./packages/
|
||||||
|
|
||||||
|
# Node.js 패키지 다운로드 (npm pack)
|
||||||
|
npm pack @vitejs/plugin-react react react-dom axios zustand --pack-destination ./npm-packages/
|
||||||
|
|
||||||
|
# Ollama 바이너리 다운로드
|
||||||
|
curl -Lo ollama-linux-amd64 https://ollama.ai/download/ollama-linux-amd64
|
||||||
|
chmod +x ollama-linux-amd64
|
||||||
|
|
||||||
|
# Ollama 모델 다운로드 (약 20GB)
|
||||||
|
ollama pull llama3 # 일반 NLP (4.7GB)
|
||||||
|
ollama pull llava:7b # 비전·이미지 분석 (4.7GB)
|
||||||
|
ollama pull nomic-embed-text # 임베딩 (0.27GB)
|
||||||
|
ollama pull codellama:7b # 코드 분석 (3.8GB)
|
||||||
|
|
||||||
|
# Ollama 모델 파일 압축
|
||||||
|
tar czf ollama-models.tar.gz ~/.ollama/models/
|
||||||
|
|
||||||
|
# GUARDiA 소스코드 압축
|
||||||
|
git clone https://github.com/zio/guardia-itsm.git
|
||||||
|
tar czf guardia-src.tar.gz guardia-itsm/
|
||||||
|
|
||||||
|
# 무결성 체크섬 생성 (필수)
|
||||||
|
sha256sum packages/* ollama-models.tar.gz guardia-src.tar.gz > SHA256SUMS.txt
|
||||||
|
cat SHA256SUMS.txt`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
{/* STEP 2: 보안 반입 */}
|
||||||
|
<Step num={2} title="폐쇄망 반입 및 무결성 검증" color={COLORS[1]}>
|
||||||
|
<div style={S.warn}>승인된 이동식 미디어 또는 내부망 파일 서버를 통해서만 반입합니다.</div>
|
||||||
|
<Code>{`# 폐쇄망 서버에서 수행
|
||||||
|
# 1. 파일 수신 후 무결성 검증 (필수!)
|
||||||
|
sha256sum -c SHA256SUMS.txt
|
||||||
|
# "OK" 출력 확인 — FAILED 시 즉시 중단
|
||||||
|
|
||||||
|
# 2. 작업 디렉토리 설정
|
||||||
|
mkdir -p /opt/guardia/{app,venv,logs,uploads,backups}
|
||||||
|
mkdir -p /var/www/{manager,zioinfo,mail}
|
||||||
|
chmod 750 /opt/guardia
|
||||||
|
|
||||||
|
# 3. 파일 압축 해제
|
||||||
|
tar xzf guardia-src.tar.gz -C /opt/guardia/
|
||||||
|
tar xzf ollama-models.tar.gz -C ~/`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
{/* STEP 3: 시스템 패키지 */}
|
||||||
|
<Step num={3} title="시스템 패키지 설치 (오프라인 deb/rpm)" color={COLORS[2]}>
|
||||||
|
<Code>{`# Ubuntu 22.04 — 오프라인 패키지 (USB에 미리 다운로드)
|
||||||
|
apt install --no-install-recommends \\
|
||||||
|
./postgresql-16_amd64.deb \\
|
||||||
|
./python3.11_amd64.deb \\
|
||||||
|
./nodejs_20_amd64.deb \\
|
||||||
|
./nginx_amd64.deb
|
||||||
|
|
||||||
|
# PostgreSQL 초기화
|
||||||
|
systemctl enable --now postgresql
|
||||||
|
su postgres -c "createuser guardia"
|
||||||
|
su postgres -c "createdb guardia_db -O guardia"
|
||||||
|
su postgres -c "psql -c \\"ALTER USER guardia PASSWORD 'G@urd1a_2026!'\\""
|
||||||
|
|
||||||
|
# pgvector 확장 설치 (오프라인 빌드 또는 패키지)
|
||||||
|
su postgres -c "psql -d guardia_db -c 'CREATE EXTENSION IF NOT EXISTS vector;'"
|
||||||
|
|
||||||
|
# Python 가상환경 + 오프라인 패키지 설치
|
||||||
|
python3 -m venv /opt/guardia/venv
|
||||||
|
/opt/guardia/venv/bin/pip install --no-index \\
|
||||||
|
--find-links=/opt/guardia-offline/packages/ \\
|
||||||
|
fastapi sqlalchemy asyncpg pgvector httpx \\
|
||||||
|
paramiko cryptography pydantic uvicorn`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
{/* STEP 4: Ollama */}
|
||||||
|
<Step num={4} title="Ollama AI 엔진 오프라인 설치" color={COLORS[3]}>
|
||||||
|
<div style={S.info}>Ollama는 외부 API 없이 온프레미스에서 완전히 동작합니다. 인터넷 연결 불필요.</div>
|
||||||
|
<Code>{`# Ollama 바이너리 설치
|
||||||
|
cp ./ollama-linux-amd64 /usr/local/bin/ollama
|
||||||
|
chmod +x /usr/local/bin/ollama
|
||||||
|
|
||||||
|
# systemd 서비스 등록
|
||||||
|
cat > /etc/systemd/system/ollama.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Ollama AI Engine
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/ollama serve
|
||||||
|
Restart=always
|
||||||
|
Environment=OLLAMA_HOST=0.0.0.0
|
||||||
|
Environment=OLLAMA_MODELS=/root/.ollama/models
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 오프라인 모델 복원 (USB에서 복사한 모델)
|
||||||
|
# 모델 파일은 이미 ~/.ollama/models/ 에 위치해야 함
|
||||||
|
systemctl enable --now ollama
|
||||||
|
|
||||||
|
# 모델 등록 확인 (자동 인식)
|
||||||
|
sleep 3 && ollama list
|
||||||
|
# 출력 예시:
|
||||||
|
# NAME SIZE
|
||||||
|
# llama3:8b 4.7 GB
|
||||||
|
# llava:7b 4.7 GB
|
||||||
|
# nomic-embed-text 0.27 GB`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
{/* STEP 5: ITSM 설정 */}
|
||||||
|
{(system === 'itsm' || system === 'all') && (
|
||||||
|
<Step num={5} title="GUARDiA ITSM 설치 및 설정" color={COLORS[4]}>
|
||||||
|
<Code>{`# 소스 복사
|
||||||
|
cp -r /opt/guardia/guardia-itsm/. /opt/guardia/app/
|
||||||
|
|
||||||
|
# 환경변수 설정 (폐쇄망 전용)
|
||||||
|
cat > /opt/guardia/app/.env << 'EOF'
|
||||||
|
# ── 네트워크 모드 ──
|
||||||
|
GUARDIA_NETWORK_MODE=closed # 폐쇄망 모드
|
||||||
|
|
||||||
|
# ── 데이터베이스 ──
|
||||||
|
DATABASE_URL=postgresql+asyncpg://guardia:G%40urd1a_2026%21@localhost:5432/guardia_db
|
||||||
|
|
||||||
|
# ── 보안 키 (반드시 랜덤 값으로 교체) ──
|
||||||
|
SECRET_KEY=$(openssl rand -hex 32)
|
||||||
|
AES_KEY=$(openssl rand -hex 16)
|
||||||
|
AES_IV=$(openssl rand -hex 8)
|
||||||
|
|
||||||
|
# ── Ollama (온프레미스, 외부 API 완전 차단) ──
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
OLLAMA_MODEL=llama3
|
||||||
|
OLLAMA_VISION_MODEL=llava:7b
|
||||||
|
|
||||||
|
# ── 서버 설정 ──
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=9001
|
||||||
|
WORKERS=4
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# systemd 서비스 등록
|
||||||
|
cat > /etc/systemd/system/guardia.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=GUARDiA ITSM Platform
|
||||||
|
After=network.target postgresql.service ollama.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/guardia/app
|
||||||
|
EnvironmentFile=/opt/guardia/app/.env
|
||||||
|
ExecStart=/opt/guardia/venv/bin/uvicorn main:app \\
|
||||||
|
--host 127.0.0.1 --port 9001 --workers 4
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl enable --now guardia
|
||||||
|
sleep 4 && systemctl is-active guardia`}</Code>
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 6: Manager */}
|
||||||
|
{(system === 'manager' || system === 'all') && (
|
||||||
|
<Step num={6} title="GUARDiA Manager 설치" color={COLORS[5]}>
|
||||||
|
<Code>{`# Manager 소스 복사 및 빌드 (오프라인 npm)
|
||||||
|
cp -r /opt/guardia/guardia-manager/. /opt/manager/src/
|
||||||
|
cd /opt/manager/src/frontend
|
||||||
|
|
||||||
|
# 오프라인 npm 패키지 설치
|
||||||
|
npm install --prefer-offline --legacy-peer-deps
|
||||||
|
|
||||||
|
# 빌드
|
||||||
|
npm run build
|
||||||
|
cp -r dist/. /var/www/manager/
|
||||||
|
|
||||||
|
# Manager 백엔드 서비스
|
||||||
|
cat > /etc/systemd/system/guardia-manager.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=GUARDiA Manager Backend
|
||||||
|
After=network.target guardia.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/manager/src/backend
|
||||||
|
ExecStart=/opt/guardia/venv/bin/uvicorn main:app \\
|
||||||
|
--host 127.0.0.1 --port 8002 --workers 2
|
||||||
|
Restart=on-failure
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl enable --now guardia-manager`}</Code>
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 7: Messenger */}
|
||||||
|
{(system === 'messenger' || system === 'all') && (
|
||||||
|
<Step num={7} title="GUARDiA Messenger 앱 배포 (APK QR 방식)" color={COLORS[6]}>
|
||||||
|
<div style={S.info}>
|
||||||
|
<strong>💡 앱스토어 없이 배포:</strong> Manager → 📱 앱 배포 → APK 업로드 → QR 생성 → 사용자 스캔 → 설치
|
||||||
|
</div>
|
||||||
|
<Code>{`# EAS 빌드 APK를 내부망 서버에 복사
|
||||||
|
cp GUARDiA_Messenger_v1.0.0.apk /opt/guardia/app/uploads/apk/
|
||||||
|
|
||||||
|
# Manager에서 APK 등록 (API)
|
||||||
|
curl -X POST https://localhost:8443/api/app/upload \\
|
||||||
|
-H "Authorization: Bearer <ADMIN_TOKEN>" \\
|
||||||
|
-F "file=@/opt/guardia/app/uploads/apk/GUARDiA_Messenger_v1.0.0.apk" \\
|
||||||
|
-F "version=1.0.0" \\
|
||||||
|
-F "release_notes=초기 배포"
|
||||||
|
|
||||||
|
# QR 코드 생성 확인
|
||||||
|
# Manager → 앱 배포 → QR 이미지 → 사용자에게 공유
|
||||||
|
|
||||||
|
# 사용자 설치 순서:
|
||||||
|
# 1. Android 기기에서 QR 스캔
|
||||||
|
# 2. 랜딩 페이지 → "Android 다운로드" 클릭
|
||||||
|
# 3. 설정 → 보안 → "알 수 없는 출처" 허용 (최초 1회)
|
||||||
|
# 4. APK 설치 → 서버 주소 입력 → 로그인`}</Code>
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STEP 8: nginx + SSL */}
|
||||||
|
<Step num={system === 'all' ? 8 : 5} title="nginx 리버스 프록시 + SSL 설정" color={COLORS[7]}>
|
||||||
|
<div style={S.warn}>폐쇄망에서는 내부 CA(Certificate Authority)가 발급한 인증서를 사용합니다.</div>
|
||||||
|
<Code>{`# 내부 CA로 인증서 생성 (이미 CA가 있는 경우 해당 CA 사용)
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/guardia/server.key \\
|
||||||
|
-out /etc/ssl/guardia/server.crt -days 3650 -nodes \\
|
||||||
|
-subj "/C=KR/ST=Seoul/O=Your-Org/CN=guardia.internal"
|
||||||
|
|
||||||
|
# nginx 설정
|
||||||
|
cat > /etc/nginx/sites-available/guardia << 'EOF'
|
||||||
|
# ITSM (8443)
|
||||||
|
server {
|
||||||
|
listen 8443 ssl;
|
||||||
|
ssl_certificate /etc/ssl/guardia/server.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/guardia/server.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
location / { root /opt/guardia/app/static; try_files $uri /index.html; }
|
||||||
|
location /api/ { proxy_pass http://127.0.0.1:9001; }
|
||||||
|
location /ws { proxy_pass http://127.0.0.1:9001; proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Manager (8090)
|
||||||
|
server {
|
||||||
|
listen 8090 ssl;
|
||||||
|
ssl_certificate /etc/ssl/guardia/server.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/guardia/server.key;
|
||||||
|
root /var/www/manager;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
location /api/ { proxy_pass http://127.0.0.1:8002; }
|
||||||
|
location /guardia-api/ { proxy_pass http://127.0.0.1:9001/; }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ln -sf /etc/nginx/sites-available/guardia /etc/nginx/sites-enabled/
|
||||||
|
nginx -t && systemctl reload nginx`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
{/* STEP 9: 방화벽 */}
|
||||||
|
<Step num={system === 'all' ? 9 : 6} title="방화벽 설정 (내부망 IP만 허용)" color="#dc2626">
|
||||||
|
<div style={S.danger}>
|
||||||
|
<strong>⚠️ 보안 필수:</strong> 관리 포트는 허가된 관리자 IP에서만 접근 허용. 외부 인터넷 차단 확인.
|
||||||
|
</div>
|
||||||
|
<Code>{`# UFW 방화벽 (Ubuntu)
|
||||||
|
ufw default deny incoming
|
||||||
|
ufw default allow outgoing
|
||||||
|
|
||||||
|
# SSH — 관리자 전용 (IP 제한)
|
||||||
|
ufw allow from <관리자-IP>/32 to any port 22
|
||||||
|
|
||||||
|
# ITSM — 내부망 허용
|
||||||
|
ufw allow from <내부망-CIDR> to any port 8443
|
||||||
|
|
||||||
|
# Manager — 내부망 허용
|
||||||
|
ufw allow from <내부망-CIDR> to any port 8090
|
||||||
|
|
||||||
|
# Webmail — 내부망 허용
|
||||||
|
ufw allow from <내부망-CIDR> to any port 8025
|
||||||
|
|
||||||
|
# Messenger 앱 API (선택)
|
||||||
|
ufw allow from <내부망-CIDR> to any port 8443
|
||||||
|
|
||||||
|
# 외부 인터넷 차단 확인 (폐쇄망 필수)
|
||||||
|
ufw deny out to any port 80 # HTTP 외부 차단
|
||||||
|
ufw deny out to any port 443 # HTTPS 외부 차단
|
||||||
|
|
||||||
|
ufw enable
|
||||||
|
ufw status verbose`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
{/* 완료 확인 */}
|
||||||
|
<div style={{ ...S.success, marginTop: 8 }}>
|
||||||
|
<strong>✅ 설치 완료 후 필수 점검 사항</strong><br/>
|
||||||
|
□ 기본 admin 비밀번호 변경 완료<br/>
|
||||||
|
□ SSL 인증서 적용 및 브라우저 경고 없음<br/>
|
||||||
|
□ 방화벽 규칙 검토 (ufw status verbose)<br/>
|
||||||
|
□ Ollama 모델 정상 로드 (ollama list)<br/>
|
||||||
|
□ ITSM 헬스체크: <code>curl -k https://localhost:8443/health</code><br/>
|
||||||
|
□ 감사 로그 경로 확인 (/opt/guardia/logs/)<br/>
|
||||||
|
□ PostgreSQL 자동 백업 설정 (crontab)<br/>
|
||||||
|
□ CSAP 보안 점검 실행 (Manager → CSAP 점검)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 개방망 설치 ───────────────────────────────────────────────────────────────
|
||||||
|
function OpenNetwork({ system }: { system: SystemType }) {
|
||||||
|
const COLORS = ['#059669','#0891b2','#7c3aed','#d97706','#dc2626','#003366','#16a34a','#9333ea']
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={S.warn}>
|
||||||
|
<strong>⚠️ 개방망 보안 주의사항</strong><br/>
|
||||||
|
• HTTPS 반드시 적용 — HTTP는 자격증명 노출 위험<br/>
|
||||||
|
• 관리 포트(8443, 8090) 방화벽으로 허가 IP만 허용<br/>
|
||||||
|
• Ollama는 localhost에서만 동작 (외부 노출 금지)<br/>
|
||||||
|
• 정기 보안 패치 적용 (Ubuntu: unattended-upgrades 권장)<br/>
|
||||||
|
• 기본 자격증명 설치 후 즉시 변경
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Req />
|
||||||
|
|
||||||
|
<Step num={1} title="시스템 초기 설정" color={COLORS[0]}>
|
||||||
|
<Code>{`# Ubuntu 22.04 기준
|
||||||
|
apt update && apt upgrade -y
|
||||||
|
apt install -y curl wget git unzip software-properties-common \\
|
||||||
|
build-essential libssl-dev libffi-dev python3-dev
|
||||||
|
|
||||||
|
# Python 3.11
|
||||||
|
add-apt-repository ppa:deadsnakes/ppa -y
|
||||||
|
apt install -y python3.11 python3.11-venv python3.11-dev
|
||||||
|
|
||||||
|
# Node.js 20 LTS
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||||
|
apt install -y nodejs
|
||||||
|
|
||||||
|
# PostgreSQL 16
|
||||||
|
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \\
|
||||||
|
> /etc/apt/sources.list.d/pgdg.list'
|
||||||
|
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||||
|
apt update && apt install -y postgresql-16 postgresql-16-pgvector
|
||||||
|
|
||||||
|
# PostgreSQL 초기화
|
||||||
|
systemctl enable --now postgresql
|
||||||
|
su postgres -c "createuser guardia -P" # 비밀번호 입력
|
||||||
|
su postgres -c "createdb guardia_db -O guardia"
|
||||||
|
su postgres -c "psql -d guardia_db -c 'CREATE EXTENSION IF NOT EXISTS vector;'"`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={2} title="Ollama 설치 및 AI 모델 다운로드" color={COLORS[1]}>
|
||||||
|
<div style={S.info}>Ollama는 온프레미스 전용 — 외부 API 호출 없이 서버 내부에서만 동작합니다.</div>
|
||||||
|
<Code>{`# Ollama 설치
|
||||||
|
curl -fsSL https://ollama.ai/install.sh | sh
|
||||||
|
systemctl enable --now ollama
|
||||||
|
|
||||||
|
# 필수 AI 모델 설치 (총 약 15GB, 시간 소요)
|
||||||
|
ollama pull llama3 # 일반 NLP (4.7GB)
|
||||||
|
ollama pull llava:7b # 비전·이미지·스크린샷 분석 (4.7GB)
|
||||||
|
ollama pull nomic-embed-text # 임베딩·RAG 검색 (0.27GB)
|
||||||
|
ollama pull codellama:7b # 코드·CSS 생성 (3.8GB)
|
||||||
|
|
||||||
|
# 서비스 확인
|
||||||
|
curl http://localhost:11434/api/tags | python3 -c \\
|
||||||
|
"import sys,json; [print(m['name']) for m in json.load(sys.stdin)['models']]"
|
||||||
|
|
||||||
|
# GPU 가속 설정 (NVIDIA GPU 있는 경우)
|
||||||
|
# CUDA toolkit 설치 후 Ollama 자동 감지`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={3} title="GUARDiA ITSM 설치" color={COLORS[2]}>
|
||||||
|
<Code>{`# Gitea에서 소스 클론 (개방망)
|
||||||
|
git clone https://zioinfo.co.kr:3000/zio/guardia-itsm.git /opt/guardia/app
|
||||||
|
# 또는
|
||||||
|
git clone <내부-Gitea-URL>/zio/guardia-itsm.git /opt/guardia/app
|
||||||
|
|
||||||
|
# Python 가상환경
|
||||||
|
python3.11 -m venv /opt/guardia/venv
|
||||||
|
/opt/guardia/venv/bin/pip install -r /opt/guardia/app/requirements.txt
|
||||||
|
|
||||||
|
# 환경변수 설정
|
||||||
|
cat > /opt/guardia/app/.env << 'ENVEOF'
|
||||||
|
GUARDIA_NETWORK_MODE=open
|
||||||
|
DATABASE_URL=postgresql+asyncpg://guardia:<비밀번호>@localhost:5432/guardia_db
|
||||||
|
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||||
|
AES_KEY=$(python3 -c "import secrets; print(secrets.token_hex(16))")
|
||||||
|
AES_IV=$(python3 -c "import secrets; print(secrets.token_hex(8))")
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
GUARDIA_ALLOWED_ORIGINS=https://<도메인>:8443,https://<도메인>:8090
|
||||||
|
ENVEOF
|
||||||
|
|
||||||
|
# DB 마이그레이션 및 초기 데이터
|
||||||
|
cd /opt/guardia/app
|
||||||
|
/opt/guardia/venv/bin/python3 -c "
|
||||||
|
from database import engine, Base
|
||||||
|
import asyncio
|
||||||
|
asyncio.run(engine.begin().__aenter__())
|
||||||
|
print('DB 초기화 완료')
|
||||||
|
"
|
||||||
|
|
||||||
|
# systemd 등록 및 시작
|
||||||
|
cp deploy/guardia.service /etc/systemd/system/
|
||||||
|
systemctl daemon-reload && systemctl enable --now guardia
|
||||||
|
sleep 5 && curl http://127.0.0.1:9001/health`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={4} title="GUARDiA Manager 설치" color={COLORS[3]}>
|
||||||
|
<Code>{`# Manager 소스
|
||||||
|
git clone <Gitea-URL>/zio/guardia-manager.git /opt/manager/src
|
||||||
|
|
||||||
|
# Frontend 빌드
|
||||||
|
cd /opt/manager/src/frontend
|
||||||
|
npm ci --legacy-peer-deps
|
||||||
|
npm run build
|
||||||
|
cp -r dist/. /var/www/manager/
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd /opt/manager/src/backend
|
||||||
|
/opt/guardia/venv/bin/pip install -r requirements.txt
|
||||||
|
cat > .env << 'EOF'
|
||||||
|
GUARDIA_ITSM_URL=http://127.0.0.1:9001
|
||||||
|
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 서비스 등록
|
||||||
|
cp deploy/guardia-manager.service /etc/systemd/system/
|
||||||
|
systemctl enable --now guardia-manager`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={5} title="HTTPS 인증서 설정" color={COLORS[4]}>
|
||||||
|
<div style={S.warn}>공인 도메인이 있는 경우 Let's Encrypt, 없으면 자체 서명 인증서를 사용합니다.</div>
|
||||||
|
<Code>{`# 방법 1: Let's Encrypt (공인 도메인 있는 경우)
|
||||||
|
apt install -y certbot
|
||||||
|
certbot certonly --standalone \\
|
||||||
|
-d guardia.your-domain.go.kr \\
|
||||||
|
--agree-tos --email admin@your-org.go.kr
|
||||||
|
|
||||||
|
# 인증서 경로
|
||||||
|
# /etc/letsencrypt/live/guardia.your-domain.go.kr/fullchain.pem
|
||||||
|
# /etc/letsencrypt/live/guardia.your-domain.go.kr/privkey.pem
|
||||||
|
|
||||||
|
# 방법 2: 자체 서명 인증서 (도메인 없는 경우)
|
||||||
|
mkdir -p /etc/ssl/guardia
|
||||||
|
openssl req -x509 -newkey rsa:4096 \\
|
||||||
|
-keyout /etc/ssl/guardia/server.key \\
|
||||||
|
-out /etc/ssl/guardia/server.crt \\
|
||||||
|
-days 3650 -nodes \\
|
||||||
|
-subj "/C=KR/O=Your-Org/CN=<서버-IP-또는-도메인>"
|
||||||
|
|
||||||
|
# 자동 갱신 (Let's Encrypt)
|
||||||
|
echo "0 3 * * * certbot renew --quiet && systemctl reload nginx" | crontab -`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={6} title="nginx 리버스 프록시 설정" color={COLORS[5]}>
|
||||||
|
<Code>{`apt install -y nginx
|
||||||
|
|
||||||
|
cat > /etc/nginx/sites-available/guardia << 'NGINXEOF'
|
||||||
|
# ── GUARDiA ITSM (8443) ──────────────────────────────
|
||||||
|
server {
|
||||||
|
listen 8443 ssl http2;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/guardia/server.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/guardia/server.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
|
||||||
|
# 정적 파일 (React SPA)
|
||||||
|
root /opt/guardia/app/static;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
|
||||||
|
# API 프록시
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:9001;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket (실시간 알림)
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:9001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── GUARDiA Manager (8090) ──────────────────────────
|
||||||
|
server {
|
||||||
|
listen 8090 ssl http2;
|
||||||
|
ssl_certificate /etc/ssl/guardia/server.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/guardia/server.key;
|
||||||
|
|
||||||
|
root /var/www/manager;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
location /api/ { proxy_pass http://127.0.0.1:8002; }
|
||||||
|
location /guardia-api/ { proxy_pass http://127.0.0.1:9001/; }
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Webmail (8025) ──────────────────────────────────
|
||||||
|
server {
|
||||||
|
listen 8025 ssl;
|
||||||
|
ssl_certificate /etc/ssl/guardia/server.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/guardia/server.key;
|
||||||
|
|
||||||
|
root /var/www/mail;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
location /api/ { proxy_pass http://127.0.0.1:8026; }
|
||||||
|
}
|
||||||
|
NGINXEOF
|
||||||
|
|
||||||
|
ln -sf /etc/nginx/sites-available/guardia /etc/nginx/sites-enabled/
|
||||||
|
nginx -t && systemctl enable --now nginx`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={7} title="방화벽 설정" color={COLORS[6]}>
|
||||||
|
<Code>{`# UFW 방화벽
|
||||||
|
ufw default deny incoming
|
||||||
|
ufw default allow outgoing
|
||||||
|
|
||||||
|
# SSH (관리자 IP만)
|
||||||
|
ufw allow from <관리자-IP> to any port 22
|
||||||
|
|
||||||
|
# ITSM/Manager/Webmail (허가 IP 대역만)
|
||||||
|
ufw allow from <허용-IP-대역>/24 to any port 8443
|
||||||
|
ufw allow from <허용-IP-대역>/24 to any port 8090
|
||||||
|
ufw allow from <허용-IP-대역>/24 to any port 8025
|
||||||
|
|
||||||
|
# Ollama는 localhost만 (외부 노출 금지!)
|
||||||
|
# PostgreSQL은 localhost만
|
||||||
|
# Gitea, Jenkins (선택)
|
||||||
|
ufw allow from <내부망-CIDR> to any port 3000
|
||||||
|
ufw allow from <내부망-CIDR> to any port 8080
|
||||||
|
|
||||||
|
ufw enable
|
||||||
|
ufw status numbered`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step num={8} title="서비스 시작 및 최종 확인" color={COLORS[7]}>
|
||||||
|
<Code>{`# 전체 서비스 상태 확인
|
||||||
|
systemctl is-active guardia guardia-manager ollama postgresql nginx
|
||||||
|
|
||||||
|
# API 헬스체크
|
||||||
|
curl -sk https://localhost:8443/health | python3 -m json.tool
|
||||||
|
curl -sk https://localhost:8090/api/health | python3 -m json.tool
|
||||||
|
|
||||||
|
# Ollama 모델 확인
|
||||||
|
ollama list
|
||||||
|
|
||||||
|
# 초기 admin 로그인 (즉시 비밀번호 변경!)
|
||||||
|
# URL: https://<서버IP>:8443
|
||||||
|
# ID: admin PW: 1111 ← 즉시 변경 필수!
|
||||||
|
|
||||||
|
# 보안 점검 실행
|
||||||
|
curl -sk https://localhost:8443/api/n2sf/checklist \\
|
||||||
|
-H "Authorization: Bearer <TOKEN>" | python3 -m json.tool`}</Code>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<div style={{ ...S.success, marginTop: 8 }}>
|
||||||
|
<strong>✅ 개방망 설치 완료 체크리스트</strong><br/>
|
||||||
|
□ admin 기본 비밀번호 변경 완료<br/>
|
||||||
|
□ HTTPS 인증서 적용 (브라우저 자물쇠 확인)<br/>
|
||||||
|
□ 방화벽 규칙 검토 (ufw status numbered)<br/>
|
||||||
|
□ Ollama 외부 노출 없음 (curl http://외부IP:11434 실패 확인)<br/>
|
||||||
|
□ PostgreSQL 외부 노출 없음<br/>
|
||||||
|
□ Let's Encrypt 자동 갱신 설정<br/>
|
||||||
|
□ 보안 패치 자동화 (unattended-upgrades)<br/>
|
||||||
|
□ 백업 스케줄 설정 (PostgreSQL + 파일)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 메인 페이지 ───────────────────────────────────────────────────────────────
|
||||||
|
export default function InstallGuide() {
|
||||||
|
const [env, setEnv] = useState<EnvType>('closed')
|
||||||
|
const [sys, setSys] = useState<SystemType>('all')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={S.page}>
|
||||||
|
<h1 style={{ fontSize: 22, fontWeight: 800, margin: '0 0 4px', color: '#003366' }}>
|
||||||
|
📦 GUARDiA 설치 가이드
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: '#64748b', marginBottom: 24, fontSize: 13 }}>
|
||||||
|
폐쇄망(공공기관 내부망)과 개방망 환경별 단계별 설치 방법
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 환경 선택 */}
|
||||||
|
<div style={{ display: 'flex', gap: 16, marginBottom: 20 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151', marginBottom: 6 }}>설치 환경</div>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
{([['closed','🔒 폐쇄망','공공기관 내부망·인터넷 차단'],['open','🌐 개방망','인터넷 연결 가능']] as const).map(([v,label,desc])=>(
|
||||||
|
<button key={v} onClick={() => setEnv(v as EnvType)} style={{
|
||||||
|
padding: '10px 16px', border: `2px solid ${env===v?'#003366':'#e2e8f0'}`,
|
||||||
|
borderRadius: 10, background: env===v?'#003366':'#fff',
|
||||||
|
color: env===v?'#fff':'#374151', cursor: 'pointer',
|
||||||
|
fontWeight: env===v?700:400, fontSize: 13, textAlign: 'left' as const,
|
||||||
|
}}>
|
||||||
|
<div>{label}</div>
|
||||||
|
<div style={{ fontSize: 11, opacity: .7 }}>{desc}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151', marginBottom: 6 }}>설치 대상</div>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
{([['all','전체','ITSM+Manager+Messenger'],['itsm','ITSM','API 서버'],['manager','Manager','관제 포털'],['messenger','Messenger','모바일 앱']] as const).map(([v,label,desc])=>(
|
||||||
|
<button key={v} onClick={() => setSys(v as SystemType)} style={{
|
||||||
|
padding: '10px 16px', border: `2px solid ${sys===v?'#003366':'#e2e8f0'}`,
|
||||||
|
borderRadius: 10, background: sys===v?'#003366':'#fff',
|
||||||
|
color: sys===v?'#fff':'#374151', cursor: 'pointer',
|
||||||
|
fontWeight: sys===v?700:400, fontSize: 13, textAlign: 'left' as const,
|
||||||
|
}}>
|
||||||
|
<div>{label}</div>
|
||||||
|
<div style={{ fontSize: 11, opacity: .7 }}>{desc}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 선택 배지 */}
|
||||||
|
<div style={{ ...S.info, marginBottom: 20 }}>
|
||||||
|
현재 선택: <strong>{env === 'closed' ? '🔒 폐쇄망' : '🌐 개방망'}</strong>
|
||||||
|
+
|
||||||
|
<strong>{sys === 'all' ? '전체 시스템' : `GUARDiA ${sys.toUpperCase()}`}</strong>
|
||||||
|
설치 가이드를 표시합니다.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 설치 가이드 렌더링 */}
|
||||||
|
{env === 'closed'
|
||||||
|
? <ClosedNetwork system={sys} />
|
||||||
|
: <OpenNetwork system={sys} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* 지원 문의 */}
|
||||||
|
<div style={{ ...S.card, borderTop: '3px solid #003366', textAlign: 'center', padding: 24 }}>
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 700, color: '#003366', marginBottom: 8 }}>
|
||||||
|
설치 지원이 필요하신가요?
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 13, color: '#64748b', margin: '0 0 12px' }}>
|
||||||
|
지오정보기술 전문 엔지니어가 기관 환경에 맞는 설치를 지원합니다.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' as const }}>
|
||||||
|
{[
|
||||||
|
{ label: '📞 기술 지원 전화', val: '02-0000-0000' },
|
||||||
|
{ label: '📧 이메일 문의', val: 'support@zioinfo.co.kr' },
|
||||||
|
{ label: '🌐 홈페이지', val: 'www.zioinfo.co.kr' },
|
||||||
|
].map(c => (
|
||||||
|
<div key={c.label} style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 8, padding: '8px 16px', fontSize: 13 }}>
|
||||||
|
<span style={{ color: '#64748b' }}>{c.label}: </span>
|
||||||
|
<strong style={{ color: '#003366' }}>{c.val}</strong>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/api/clients.ts","./src/api/types.ts","./src/components/common/btn.tsx","./src/components/common/datatable.tsx","./src/components/common/protectedroute.tsx","./src/components/common/slidepanel.tsx","./src/components/common/statcard.tsx","./src/components/common/statusbadge.tsx","./src/components/layout/applayout.tsx","./src/components/layout/gnb.tsx","./src/components/layout/sidebar.tsx","./src/config/env.ts","./src/hooks/useapi.ts","./src/hooks/useauth.ts","./src/pages/aiplatform.tsx","./src/pages/apikeys.tsx","./src/pages/appdistribution.tsx","./src/pages/auditlog.tsx","./src/pages/bianalytics.tsx","./src/pages/billingmanage.tsx","./src/pages/cmdb.tsx","./src/pages/configenv.tsx","./src/pages/confignginx.tsx","./src/pages/csapconsole.tsx","./src/pages/dashboard.tsx","./src/pages/deployments.tsx","./src/pages/drconsole.tsx","./src/pages/exportimport.tsx","./src/pages/institutions.tsx","./src/pages/integrationhub.tsx","./src/pages/kpidashboard.tsx","./src/pages/llmmanager.tsx","./src/pages/licenses.tsx","./src/pages/login.tsx","./src/pages/networkconsole.tsx","./src/pages/notificationrules.tsx","./src/pages/notifications.tsx","./src/pages/repos.tsx","./src/pages/scrapingmanager.tsx","./src/pages/servers.tsx","./src/pages/users.tsx"],"version":"5.9.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/api/clients.ts","./src/api/types.ts","./src/components/common/btn.tsx","./src/components/common/datatable.tsx","./src/components/common/protectedroute.tsx","./src/components/common/slidepanel.tsx","./src/components/common/statcard.tsx","./src/components/common/statusbadge.tsx","./src/components/layout/applayout.tsx","./src/components/layout/gnb.tsx","./src/components/layout/sidebar.tsx","./src/config/env.ts","./src/hooks/useapi.ts","./src/hooks/useauth.ts","./src/pages/aiplatform.tsx","./src/pages/apikeys.tsx","./src/pages/appdistribution.tsx","./src/pages/auditlog.tsx","./src/pages/bianalytics.tsx","./src/pages/billingmanage.tsx","./src/pages/cmdb.tsx","./src/pages/configenv.tsx","./src/pages/confignginx.tsx","./src/pages/csapconsole.tsx","./src/pages/dashboard.tsx","./src/pages/deployments.tsx","./src/pages/drconsole.tsx","./src/pages/exportimport.tsx","./src/pages/installguide.tsx","./src/pages/institutions.tsx","./src/pages/integrationhub.tsx","./src/pages/kpidashboard.tsx","./src/pages/llmmanager.tsx","./src/pages/licenses.tsx","./src/pages/login.tsx","./src/pages/networkconsole.tsx","./src/pages/notificationrules.tsx","./src/pages/notifications.tsx","./src/pages/repos.tsx","./src/pages/scrapingmanager.tsx","./src/pages/servers.tsx","./src/pages/users.tsx"],"version":"5.9.3"}
|
||||||
Loading…
Reference in New Issue
Block a user