sync: update from workspace (latest ITSM/CICD/DR changes)

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-03 09:48:15 +09:00
parent 35c9f9c2a9
commit efcc771263
4 changed files with 793 additions and 1 deletions

View File

@ -32,6 +32,7 @@ const AiPlatform = lazy(() => import('./pages/AiPlatform'))
// ── GUARDiA 기능 개선 v4 ──
const AppDistribution = lazy(() => import('./pages/AppDistribution'))
const NotificationRules = lazy(() => import('./pages/NotificationRules'))
const InstallGuide = lazy(() => import('./pages/InstallGuide'))
function Loading() {
return (
@ -79,6 +80,7 @@ export default function App() {
{/* GUARDiA 기능 개선 v4 */}
<Route path="app-distribution" element={<AppDistribution />} />
<Route path="notification-rules" element={<NotificationRules />} />
<Route path="install-guide" element={<InstallGuide />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>

View File

@ -46,6 +46,7 @@ const NAV: NavItem[] = [
// ── GUARDiA 기능 개선 v4 ──
{ label: '앱 배포', icon: '📱', path: '/app-distribution' },
{ label: '알림 규칙', icon: '🔔', path: '/notification-rules' },
{ label: '설치 가이드', icon: '📦', path: '/install-guide' },
]
/* Variant 스타일 색상 상수 */

View 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>
&nbsp;+&nbsp;
<strong>{sys === 'all' ? '전체 시스템' : `GUARDiA ${sys.toUpperCase()}`}</strong>
&nbsp; .
</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>
)
}

View File

@ -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"}