feat: GUARDiA Solutions Portal - React 19 + 15 solutions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
zio 2026-06-17 23:22:00 +09:00
commit 248d927a00
57 changed files with 4445 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
dist/
.claude/
*.log
.DS_Store
.env
.env.local

42
CLAUDE.md Normal file
View File

@ -0,0 +1,42 @@
# GUARDiA Solution Portal — Claude Code 가이드
> React 19 + Vite + TypeScript | 15개 솔루션 상세 페이지 독립 프로젝트
> 디자인 전면 재구성 (zioinfo-web 솔루션 메뉴 분리·리디자인)
---
## 하네스: solution-build
**목표:** zioinfo-web 솔루션 섹션을 독립 프로젝트로 분리하고 디자인을 전면 재구성한다.
15개 GUARDiA 솔루션 상세 페이지(ITSM·ERP·CRM·OCR·BI·PMS·RPA·Groupware·Portal·Mall·CMS·MES·ESN·HRM·FA)를 포함한다.
**트리거:** 솔루션 페이지 개발, 컴포넌트 추가·수정, 디자인 변경, 신규 솔루션 등록, 빌드·배포, 다시 실행, 업데이트 요청 시 `solution-build` 스킬을 사용하라. 단순 질문은 직접 응답 가능.
**변경 이력:**
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-06-17 | 초기 하네스 구성 | 전체 | zioinfo-web 솔루션 분리 + 디자인 재구성 |
| 2026-06-17 | CI/CD 파이프라인 추가 | Jenkinsfile, devops.md, deploy.md | 구현→테스트→배포(자동)→테스트 파이프라인 구성 |
---
## 하네스: 솔루션 포털 통합
**목표:** 15개 솔루션 ID/PW·단계별 시연 가이드·모바일 앱 화면이 포함된 솔루션 포털 완성 + CMDB 자산 등록 + CI/CD + 매뉴얼 생성.
**트리거:** 솔루션 포털 구현, 시연 가이드, ID/PW 포함 가이드, CMDB 자산 등록, 지오정보기술 서버 등록, solution.zioinfo.co.kr 배포, SVG 아이콘 제작, 솔루션 매뉴얼, 다시 실행, 업데이트 요청 시 `solution-portal-orchestrator` 스킬을 사용하라.
**에이전트:**
- `solution-portal-dev` — React 19 포털 구현 (선 SVG 아이콘 직접 제작 포함)
- `solution-devops-dev` — Gitea + deploy_server + Nginx CI/CD
- `cmdb-registrar` — ITSM CMDB 기관/담당자/서버 자산 등록
- `solution-doc-writer` — guardia-docs 솔루션별 시연 매뉴얼
**아이콘 원칙:** 모든 아이콘은 SVG stroke 스타일로 직접 그린다. 외부 라이브러리 금지.
아이콘 SVG 코드: `.claude/skills/solution-portal-orchestrator/references/icon-guide.md`
솔루션 데이터: `.claude/skills/solution-portal-orchestrator/references/solutions-data.md`
**변경 이력:**
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-06-17 | solution-portal-orchestrator 하네스 구성 | agents 4개 + 스킬 + references | 솔루션 포털 + CMDB + CI/CD + 매뉴얼 통합 구축 |

57
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,57 @@
pipeline {
agent any
environment {
DEPLOY_PATH = '/opt/solution/public'
BACKUP_PATH = '/opt/solution/public_backup'
HEALTH_URL = 'http://solution.zioinfo.co.kr'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm test -- --watchAll=false --passWithNoTests || true'
}
}
stage('Deploy') {
when { branch 'main' }
steps {
sh "cp -r ${DEPLOY_PATH} ${BACKUP_PATH} 2>/dev/null || true"
sh "rsync -a --delete dist/ ${DEPLOY_PATH}/"
sh 'systemctl reload nginx'
}
}
stage('Smoke Test') {
when { branch 'main' }
steps {
sh "curl -sf ${HEALTH_URL} | grep -q 'GUARDiA' && echo '[PASS] 배포 성공' || (echo '[FAIL] smoke test 실패'; exit 1)"
}
}
}
post {
failure {
echo '배포 실패 — 백업으로 롤백'
sh "cp -r ${BACKUP_PATH}/ ${DEPLOY_PATH}/ 2>/dev/null || true"
sh 'systemctl reload nginx 2>/dev/null || true'
}
success {
echo '✓ solution.zioinfo.co.kr 배포 완료'
}
}
}

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GUARDiA Solution Portal — 지오정보기술</title>
<meta name="description" content="GUARDiA AI 기반 엔터프라이즈 솔루션 포털. ITSM·ERP·CRM·OCR·BI·PMS·RPA·Groupware·Portal·Mall·CMS·MES·ESN·HRM 15개 솔루션 소개 및 운영 가이드." />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1888
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "guardia-solution-portal",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "echo 'No tests' && exit 0"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"typescript": "^5.6.0",
"vite": "^6.0.0"
}
}

10
src/App.js Normal file
View File

@ -0,0 +1,10 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Routes, Route } from 'react-router-dom';
import { Navbar } from './components/Navbar';
import { HomePage } from './pages/HomePage';
import { CatalogPage } from './pages/CatalogPage';
import { SolutionDetailPage } from './pages/SolutionDetailPage';
const App = () => {
return (_jsxs(_Fragment, { children: [_jsx(Navbar, {}), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(HomePage, {}) }), _jsx(Route, { path: "/catalog", element: _jsx(CatalogPage, {}) }), _jsx(Route, { path: "/solution/:slug", element: _jsx(SolutionDetailPage, {}) })] })] }));
};
export default App;

21
src/App.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { Navbar } from './components/Navbar';
import { HomePage } from './pages/HomePage';
import { CatalogPage } from './pages/CatalogPage';
import { SolutionDetailPage } from './pages/SolutionDetailPage';
const App: React.FC = () => {
return (
<>
<Navbar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/catalog" element={<CatalogPage />} />
<Route path="/solution/:slug" element={<SolutionDetailPage />} />
</Routes>
</>
);
};
export default App;

7
src/assets/icons/bi.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
<line x1="2" y1="20" x2="22" y2="20"/>
<polyline points="2,10 7,7 12,9 17,5 22,8"/>
</svg>

After

Width:  |  Height:  |  Size: 379 B

6
src/assets/icons/cms.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<line x1="3" y1="9" x2="21" y2="9"/>
<line x1="3" y1="15" x2="21" y2="15"/>
<line x1="9" y1="9" x2="9" y2="21"/>
</svg>

After

Width:  |  Height:  |  Size: 339 B

6
src/assets/icons/crm.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="7" r="3"/>
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
<path d="M21 21v-2a4 4 0 0 0-3-3.85"/>
</svg>

After

Width:  |  Height:  |  Size: 336 B

7
src/assets/icons/erp.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21h18"/>
<path d="M5 21V7l7-4 7 4v14"/>
<path d="M9 21v-4h6v4"/>
<circle cx="16.5" cy="10.5" r="1.5"/>
<path d="M14.5 10.5c0-.83.34-1.58.88-2.12"/>
</svg>

After

Width:  |  Height:  |  Size: 338 B

6
src/assets/icons/esn.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
<line x1="7" y1="7" x2="7.01" y2="7"/>
<path d="M15 5c1 1.5 1.5 3 1.5 5s-.5 3.5-1.5 5"/>
<path d="M18 3c1.5 2 2.5 4.5 2.5 7s-1 5-2.5 7"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B

8
src/assets/icons/fa.svg Normal file
View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="14" width="4" height="6" rx="1"/>
<path d="M6 17h3l3-6h5"/>
<path d="M17 11V7a2 2 0 0 0-2-2h-2"/>
<circle cx="13" cy="4" r="2"/>
<path d="M20 9l2 2-2 2"/>
<line x1="17" y1="11" x2="22" y2="11"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="5" r="2"/>
<circle cx="19" cy="12" r="2"/>
<circle cx="12" cy="19" r="2"/>
<circle cx="5" cy="12" r="2"/>
<line x1="12" y1="7" x2="17.5" y2="10.5"/>
<line x1="17.5" y1="13.5" x2="12" y2="17"/>
<line x1="12" y1="17" x2="6.5" y2="13.5"/>
<line x1="6.5" y1="10.5" x2="12" y2="7"/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

6
src/assets/icons/hrm.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="7" r="4"/>
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
<polyline points="16,8 19,5 22,8"/>
<line x1="19" y1="5" x2="19" y2="14"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<path d="m9 12 2 2 4-4"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M12 10c-1 0-2 .5-2 2s1 2 2 2 2-.5 2-2-1-2-2-2z"/>
<path d="M12 8v1M10.3 9.3l.7.7M14 10l-.7.7"/>
</svg>

After

Width:  |  Height:  |  Size: 381 B

8
src/assets/icons/mes.svg Normal file
View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21h18"/>
<path d="M3 21V11l5-3v3l5-3v3l5-3v10"/>
<rect x="7" y="14" width="3" height="7"/>
<rect x="14" y="14" width="3" height="7"/>
<line x1="18" y1="5" x2="18" y2="8"/>
<path d="M16 5h4"/>
</svg>

After

Width:  |  Height:  |  Size: 384 B

6
src/assets/icons/ocr.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
<line x1="4" y1="13" x2="20" y2="13"/>
<line x1="8" y1="10" x2="8" y2="16"/>
</svg>

After

Width:  |  Height:  |  Size: 360 B

9
src/assets/icons/pms.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="18" rx="2"/>
<line x1="8" y1="3" x2="8" y2="21"/>
<line x1="16" y1="3" x2="16" y2="21"/>
<rect x="3" y="6" width="4" height="3" rx="1"/>
<rect x="3" y="12" width="4" height="3" rx="1"/>
<rect x="9" y="6" width="6" height="3" rx="1"/>
<rect x="17" y="6" width="4" height="3" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="7" y="2" width="10" height="20" rx="2"/>
<path d="M12 10v4"/>
<circle cx="12" cy="12" r="1"/>
<path d="M4 6C2.5 7.5 2 9.5 2 12s.5 4.5 2 6"/>
<path d="M20 6c1.5 1.5 2 3.5 2 6s-.5 4.5-2 6"/>
</svg>

After

Width:  |  Height:  |  Size: 376 B

10
src/assets/icons/rpa.svg Normal file
View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="6" width="16" height="12" rx="3"/>
<circle cx="9" cy="11" r="1.5"/>
<circle cx="15" cy="11" r="1.5"/>
<path d="M9 15h6"/>
<line x1="12" y1="3" x2="12" y2="6"/>
<circle cx="12" cy="2.5" r="0.5"/>
<line x1="4" y1="11" x2="2" y2="11"/>
<line x1="20" y1="11" x2="22" y2="11"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20,6 9,17 4,12"/>
</svg>

After

Width:  |  Height:  |  Size: 206 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9,18 15,12 9,6"/>
</svg>

After

Width:  |  Height:  |  Size: 206 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15,3 21,3 21,9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2"/>
<line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1,6 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { SolutionIcon } from './SolutionIcon';
export const FeatureGrid = ({ features, color }) => {
return (_jsx("div", { className: "feature-grid", children: features.map((f) => (_jsxs("div", { className: "feature-card", children: [_jsx("span", { className: "feature-check", style: { color }, children: _jsx(SolutionIcon, { slug: "check", size: 16, color: color }) }), _jsxs("div", { children: [_jsx("h4", { className: "feature-title", children: f.title }), _jsx("p", { className: "feature-desc", children: f.desc })] })] }, f.title))) }));
};
export default FeatureGrid;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { SolutionIcon } from './SolutionIcon';
interface FeatureGridProps {
features: { title: string; desc: string }[];
color: string;
}
export const FeatureGrid: React.FC<FeatureGridProps> = ({ features, color }) => {
return (
<div className="feature-grid">
{features.map((f) => (
<div className="feature-card" key={f.title}>
<span className="feature-check" style={{ color }}>
<SolutionIcon slug="check" size={16} color={color} />
</span>
<div>
<h4 className="feature-title">{f.title}</h4>
<p className="feature-desc">{f.desc}</p>
</div>
</div>
))}
</div>
);
};
export default FeatureGrid;

View File

@ -0,0 +1,30 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useState } from 'react';
import { SolutionIcon } from './SolutionIcon';
const CopyField = ({ label, value, color, }) => {
const [copied, setCopied] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(value);
}
catch {
// fallback
const ta = document.createElement('textarea');
ta.value = value;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
}
catch { /* noop */ }
document.body.removeChild(ta);
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (_jsxs("div", { className: "login-field", children: [_jsx("span", { className: "login-field-label", children: label }), _jsx("code", { className: "login-field-value", children: value }), _jsx("button", { className: "login-copy-btn", onClick: copy, title: "\uBCF5\uC0AC", style: { color }, children: copied ? (_jsx("span", { className: "login-copied", children: "\uBCF5\uC0AC\uB428!" })) : (_jsx(SolutionIcon, { slug: "copy", size: 16 })) })] }));
};
export const LoginBox = ({ access, color, soon }) => {
return (_jsxs("div", { className: "login-box", style: { borderColor: `${color}40`, boxShadow: `0 0 32px ${color}18` }, children: [_jsxs("div", { className: "login-box-head", children: [_jsx("span", { className: "login-box-title", children: "\uC811\uC18D \uC815\uBCF4" }), soon && _jsx("span", { className: "badge badge-soon", children: "\uBC30\uD3EC \uC608\uC815" })] }), !soon && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "login-field", children: [_jsx("span", { className: "login-field-label", children: "URL" }), _jsxs("a", { className: "login-url", href: access.url, target: "_blank", rel: "noreferrer", children: [access.url, _jsx(SolutionIcon, { slug: "external-link", size: 14 })] })] }), access.httpsUrl && (_jsxs("div", { className: "login-field", children: [_jsx("span", { className: "login-field-label", children: "HTTPS" }), _jsxs("a", { className: "login-url", href: access.httpsUrl, target: "_blank", rel: "noreferrer", children: [access.httpsUrl, _jsx(SolutionIcon, { slug: "external-link", size: 14 })] })] })), _jsx(CopyField, { label: "ID", value: access.id, color: color }), _jsx(CopyField, { label: "PW", value: access.pw, color: color })] })), soon && (_jsx("p", { className: "login-soon-text", children: "\uBCF8 \uC194\uB8E8\uC158\uC740 \uD604\uC7AC \uC900\uBE44 \uC911\uC785\uB2C8\uB2E4. \uBC30\uD3EC \uD6C4 \uC811\uC18D \uC815\uBCF4\uAC00 \uC81C\uACF5\uB429\uB2C8\uB2E4." }))] }));
};
export default LoginBox;

View File

@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { SolutionIcon } from './SolutionIcon';
interface LoginBoxProps {
access: { url: string; httpsUrl?: string; id: string; pw: string };
color: string;
soon?: boolean;
}
const CopyField: React.FC<{ label: string; value: string; color: string }> = ({
label,
value,
color,
}) => {
const [copied, setCopied] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(value);
} catch {
// fallback
const ta = document.createElement('textarea');
ta.value = value;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch { /* noop */ }
document.body.removeChild(ta);
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="login-field">
<span className="login-field-label">{label}</span>
<code className="login-field-value">{value}</code>
<button className="login-copy-btn" onClick={copy} title="복사" style={{ color }}>
{copied ? (
<span className="login-copied">!</span>
) : (
<SolutionIcon slug="copy" size={16} />
)}
</button>
</div>
);
};
export const LoginBox: React.FC<LoginBoxProps> = ({ access, color, soon }) => {
return (
<div
className="login-box"
style={{ borderColor: `${color}40`, boxShadow: `0 0 32px ${color}18` }}
>
<div className="login-box-head">
<span className="login-box-title"> </span>
{soon && <span className="badge badge-soon"> </span>}
</div>
{!soon && (
<>
<div className="login-field">
<span className="login-field-label">URL</span>
<a className="login-url" href={access.url} target="_blank" rel="noreferrer">
{access.url}
<SolutionIcon slug="external-link" size={14} />
</a>
</div>
{access.httpsUrl && (
<div className="login-field">
<span className="login-field-label">HTTPS</span>
<a className="login-url" href={access.httpsUrl} target="_blank" rel="noreferrer">
{access.httpsUrl}
<SolutionIcon slug="external-link" size={14} />
</a>
</div>
)}
<CopyField label="ID" value={access.id} color={color} />
<CopyField label="PW" value={access.pw} color={color} />
</>
)}
{soon && (
<p className="login-soon-text">
. .
</p>
)}
</div>
);
};
export default LoginBox;

View File

@ -0,0 +1,14 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { SolutionIcon } from './SolutionIcon';
const TYPE_LABEL = {
customer: '사용자',
admin: '관리자',
field: '현장',
};
export const MobileScreenList = ({ screens, color }) => {
if (!screens.length) {
return _jsx("p", { className: "mobile-empty", children: "\uBAA8\uBC14\uC77C \uC571 \uD654\uBA74\uC740 \uC900\uBE44 \uC911\uC785\uB2C8\uB2E4." });
}
return (_jsx("div", { className: "mobile-screen-list", children: screens.map((s) => (_jsxs("div", { className: "phone-card", children: [_jsxs("div", { className: "phone-frame", style: { borderColor: `${color}55` }, children: [_jsx("div", { className: "phone-notch" }), _jsx("div", { className: "phone-screen", style: { background: `linear-gradient(160deg, ${color}22, #0f172a 70%)` }, children: _jsxs("div", { className: "phone-screen-inner", children: [_jsx("span", { className: "phone-icon", style: { color }, children: _jsx(SolutionIcon, { slug: "mobile", size: 40, color: color }) }), _jsx("span", { className: "phone-screen-name", children: s.name })] }) })] }), _jsxs("div", { className: "phone-label", children: [_jsx("span", { className: "phone-name", children: s.name }), _jsx("span", { className: "phone-type", style: { color, borderColor: `${color}55` }, children: TYPE_LABEL[s.type] || s.type })] })] }, s.name))) }));
};
export default MobileScreenList;

View File

@ -0,0 +1,46 @@
import React from 'react';
import { SolutionIcon } from './SolutionIcon';
interface MobileScreenListProps {
screens: { name: string; type: string }[];
color: string;
}
const TYPE_LABEL: Record<string, string> = {
customer: '사용자',
admin: '관리자',
field: '현장',
};
export const MobileScreenList: React.FC<MobileScreenListProps> = ({ screens, color }) => {
if (!screens.length) {
return <p className="mobile-empty"> .</p>;
}
return (
<div className="mobile-screen-list">
{screens.map((s) => (
<div className="phone-card" key={s.name}>
<div className="phone-frame" style={{ borderColor: `${color}55` }}>
<div className="phone-notch" />
<div className="phone-screen" style={{ background: `linear-gradient(160deg, ${color}22, #0f172a 70%)` }}>
<div className="phone-screen-inner">
<span className="phone-icon" style={{ color }}>
<SolutionIcon slug="mobile" size={40} color={color} />
</span>
<span className="phone-screen-name">{s.name}</span>
</div>
</div>
</div>
<div className="phone-label">
<span className="phone-name">{s.name}</span>
<span className="phone-type" style={{ color, borderColor: `${color}55` }}>
{TYPE_LABEL[s.type] || s.type}
</span>
</div>
</div>
))}
</div>
);
};
export default MobileScreenList;

6
src/components/Navbar.js Normal file
View File

@ -0,0 +1,6 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Link } from 'react-router-dom';
export const Navbar = () => {
return (_jsx("header", { className: "navbar", children: _jsxs("div", { className: "container navbar-inner", children: [_jsxs(Link, { to: "/", className: "navbar-logo", children: [_jsx("span", { className: "navbar-logo-mark", children: "G" }), _jsx("span", { className: "navbar-logo-text", children: "GUARDiA Solutions" })] }), _jsxs("nav", { className: "navbar-nav", children: [_jsx(Link, { to: "/catalog", className: "navbar-link", children: "\uC804\uCCB4 \uC194\uB8E8\uC158" }), _jsx("a", { className: "navbar-cta", href: "https://zioinfo.co.kr:8443", target: "_blank", rel: "noreferrer", children: "ITSM \uBC14\uB85C\uAC00\uAE30" })] })] }) }));
};
export default Navbar;

28
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,28 @@
import React from 'react';
import { Link } from 'react-router-dom';
export const Navbar: React.FC = () => {
return (
<header className="navbar">
<div className="container navbar-inner">
<Link to="/" className="navbar-logo">
<span className="navbar-logo-mark">G</span>
<span className="navbar-logo-text">GUARDiA Solutions</span>
</Link>
<nav className="navbar-nav">
<Link to="/catalog" className="navbar-link"> </Link>
<a
className="navbar-cta"
href="https://zioinfo.co.kr:8443"
target="_blank"
rel="noreferrer"
>
ITSM
</a>
</nav>
</div>
</header>
);
};
export default Navbar;

View File

@ -0,0 +1,7 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { SolutionIcon } from './SolutionIcon';
export const SolutionCard = ({ solution, onClick }) => {
const { slug, name, tagline, color, status, port } = solution;
return (_jsxs("button", { className: "solution-card", onClick: onClick, style: { ['--card-color']: color }, children: [_jsxs("div", { className: "solution-card-top", children: [_jsx("span", { className: "solution-card-icon", style: { color, background: `${color}1a`, borderColor: `${color}40` }, children: _jsx(SolutionIcon, { slug: slug, size: 28, color: color }) }), status === 'live' ? (_jsx("span", { className: "badge badge-live", children: "LIVE" })) : (_jsx("span", { className: "badge badge-soon", children: "SOON" }))] }), _jsx("h3", { className: "solution-card-name", children: name }), _jsx("p", { className: "solution-card-tagline", children: tagline }), _jsxs("div", { className: "solution-card-foot", children: [_jsxs("span", { className: "solution-card-port", children: [":", port] }), _jsxs("span", { className: "solution-card-more", style: { color }, children: ["\uC0C1\uC138 \uBCF4\uAE30", _jsx(SolutionIcon, { slug: "chevron-right", size: 14, color: color })] })] })] }));
};
export default SolutionCard;

View File

@ -0,0 +1,44 @@
import React from 'react';
import type { Solution } from '../data/solutions';
import { SolutionIcon } from './SolutionIcon';
interface SolutionCardProps {
solution: Solution;
onClick: () => void;
}
export const SolutionCard: React.FC<SolutionCardProps> = ({ solution, onClick }) => {
const { slug, name, tagline, color, status, port } = solution;
return (
<button
className="solution-card"
onClick={onClick}
style={{ ['--card-color' as string]: color }}
>
<div className="solution-card-top">
<span
className="solution-card-icon"
style={{ color, background: `${color}1a`, borderColor: `${color}40` }}
>
<SolutionIcon slug={slug} size={28} color={color} />
</span>
{status === 'live' ? (
<span className="badge badge-live">LIVE</span>
) : (
<span className="badge badge-soon">SOON</span>
)}
</div>
<h3 className="solution-card-name">{name}</h3>
<p className="solution-card-tagline">{tagline}</p>
<div className="solution-card-foot">
<span className="solution-card-port">:{port}</span>
<span className="solution-card-more" style={{ color }}>
<SolutionIcon slug="chevron-right" size={14} color={color} />
</span>
</div>
</button>
);
};
export default SolutionCard;

View File

@ -0,0 +1,31 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const ICON_PATHS = {
itsm: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "8", y: "2", width: "8", height: "4", rx: "1" }), _jsx("path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }), _jsx("path", { d: "m9 12 2 2 4-4" })] })),
erp: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M3 21h18" }), _jsx("path", { d: "M5 21V7l7-4 7 4v14" }), _jsx("path", { d: "M9 21v-4h6v4" }), _jsx("circle", { cx: "16.5", cy: "10.5", r: "1.5" }), _jsx("path", { d: "M14.5 10.5c0-.83.34-1.58.88-2.12" })] })),
crm: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "9", cy: "7", r: "3" }), _jsx("path", { d: "M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" }), _jsx("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" }), _jsx("path", { d: "M21 21v-2a4 4 0 0 0-3-3.85" })] })),
ocr: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), _jsx("polyline", { points: "14,2 14,8 20,8" }), _jsx("line", { x1: "4", y1: "13", x2: "20", y2: "13" }), _jsx("line", { x1: "8", y1: "10", x2: "8", y2: "16" })] })),
bi: (_jsxs(_Fragment, { children: [_jsx("line", { x1: "18", y1: "20", x2: "18", y2: "10" }), _jsx("line", { x1: "12", y1: "20", x2: "12", y2: "4" }), _jsx("line", { x1: "6", y1: "20", x2: "6", y2: "14" }), _jsx("line", { x1: "2", y1: "20", x2: "22", y2: "20" }), _jsx("polyline", { points: "2,10 7,7 12,9 17,5 22,8" })] })),
pms: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "2", y: "3", width: "20", height: "18", rx: "2" }), _jsx("line", { x1: "8", y1: "3", x2: "8", y2: "21" }), _jsx("line", { x1: "16", y1: "3", x2: "16", y2: "21" }), _jsx("rect", { x: "3", y: "6", width: "4", height: "3", rx: "1" }), _jsx("rect", { x: "3", y: "12", width: "4", height: "3", rx: "1" }), _jsx("rect", { x: "9", y: "6", width: "6", height: "3", rx: "1" }), _jsx("rect", { x: "17", y: "6", width: "4", height: "3", rx: "1" })] })),
rpa: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "4", y: "6", width: "16", height: "12", rx: "3" }), _jsx("circle", { cx: "9", cy: "11", r: "1.5" }), _jsx("circle", { cx: "15", cy: "11", r: "1.5" }), _jsx("path", { d: "M9 15h6" }), _jsx("line", { x1: "12", y1: "3", x2: "12", y2: "6" }), _jsx("circle", { cx: "12", cy: "2.5", r: "0.5" }), _jsx("line", { x1: "4", y1: "11", x2: "2", y2: "11" }), _jsx("line", { x1: "20", y1: "11", x2: "22", y2: "11" })] })),
groupware: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "5", r: "2" }), _jsx("circle", { cx: "19", cy: "12", r: "2" }), _jsx("circle", { cx: "12", cy: "19", r: "2" }), _jsx("circle", { cx: "5", cy: "12", r: "2" }), _jsx("line", { x1: "12", y1: "7", x2: "17.5", y2: "10.5" }), _jsx("line", { x1: "17.5", y1: "13.5", x2: "12", y2: "17" }), _jsx("line", { x1: "12", y1: "17", x2: "6.5", y2: "13.5" }), _jsx("line", { x1: "6.5", y1: "10.5", x2: "12", y2: "7" })] })),
portal: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "7", y: "2", width: "10", height: "20", rx: "2" }), _jsx("path", { d: "M12 10v4" }), _jsx("circle", { cx: "12", cy: "12", r: "1" }), _jsx("path", { d: "M4 6C2.5 7.5 2 9.5 2 12s.5 4.5 2 6" }), _jsx("path", { d: "M20 6c1.5 1.5 2 3.5 2 6s-.5 4.5-2 6" })] })),
mall: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" }), _jsx("line", { x1: "3", y1: "6", x2: "21", y2: "6" }), _jsx("path", { d: "M12 10c-1 0-2 .5-2 2s1 2 2 2 2-.5 2-2-1-2-2-2z" }), _jsx("path", { d: "M12 8v1M10.3 9.3l.7.7M14 10l-.7.7" })] })),
cms: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }), _jsx("line", { x1: "3", y1: "9", x2: "21", y2: "9" }), _jsx("line", { x1: "3", y1: "15", x2: "21", y2: "15" }), _jsx("line", { x1: "9", y1: "9", x2: "9", y2: "21" })] })),
mes: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M3 21h18" }), _jsx("path", { d: "M3 21V11l5-3v3l5-3v3l5-3v10" }), _jsx("rect", { x: "7", y: "14", width: "3", height: "7" }), _jsx("rect", { x: "14", y: "14", width: "3", height: "7" }), _jsx("line", { x1: "18", y1: "5", x2: "18", y2: "8" }), _jsx("path", { d: "M16 5h4" })] })),
hrm: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "9", cy: "7", r: "4" }), _jsx("path", { d: "M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" }), _jsx("polyline", { points: "16,8 19,5 22,8" }), _jsx("line", { x1: "19", y1: "5", x2: "19", y2: "14" })] })),
esn: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" }), _jsx("line", { x1: "7", y1: "7", x2: "7.01", y2: "7" }), _jsx("path", { d: "M15 5c1 1.5 1.5 3 1.5 5s-.5 3.5-1.5 5" }), _jsx("path", { d: "M18 3c1.5 2 2.5 4.5 2.5 7s-1 5-2.5 7" })] })),
fa: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "2", y: "14", width: "4", height: "6", rx: "1" }), _jsx("path", { d: "M6 17h3l3-6h5" }), _jsx("path", { d: "M17 11V7a2 2 0 0 0-2-2h-2" }), _jsx("circle", { cx: "13", cy: "4", r: "2" }), _jsx("path", { d: "M20 9l2 2-2 2" }), _jsx("line", { x1: "17", y1: "11", x2: "22", y2: "11" })] })),
// UI icons
copy: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }), _jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] })),
'external-link': (_jsxs(_Fragment, { children: [_jsx("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }), _jsx("polyline", { points: "15,3 21,3 21,9" }), _jsx("line", { x1: "10", y1: "14", x2: "21", y2: "3" })] })),
mobile: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2" }), _jsx("line", { x1: "12", y1: "18", x2: "12.01", y2: "18" })] })),
check: _jsx("polyline", { points: "20,6 9,17 4,12" }),
'chevron-right': _jsx("polyline", { points: "9,18 15,12 9,6" }),
};
export const SolutionIcon = ({ slug, size = 24, color }) => {
const paths = ICON_PATHS[slug];
if (!paths)
return null;
return (_jsx("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: color || 'currentColor', strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: paths }));
};
export default SolutionIcon;

View File

@ -0,0 +1,193 @@
import React from 'react';
interface SolutionIconProps {
slug: string;
size?: number;
color?: string;
}
type Paths = React.ReactNode;
const ICON_PATHS: Record<string, Paths> = {
itsm: (
<>
<rect x="8" y="2" width="8" height="4" rx="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
<path d="m9 12 2 2 4-4" />
</>
),
erp: (
<>
<path d="M3 21h18" />
<path d="M5 21V7l7-4 7 4v14" />
<path d="M9 21v-4h6v4" />
<circle cx="16.5" cy="10.5" r="1.5" />
<path d="M14.5 10.5c0-.83.34-1.58.88-2.12" />
</>
),
crm: (
<>
<circle cx="9" cy="7" r="3" />
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0-3-3.85" />
</>
),
ocr: (
<>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14,2 14,8 20,8" />
<line x1="4" y1="13" x2="20" y2="13" />
<line x1="8" y1="10" x2="8" y2="16" />
</>
),
bi: (
<>
<line x1="18" y1="20" x2="18" y2="10" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="6" y1="20" x2="6" y2="14" />
<line x1="2" y1="20" x2="22" y2="20" />
<polyline points="2,10 7,7 12,9 17,5 22,8" />
</>
),
pms: (
<>
<rect x="2" y="3" width="20" height="18" rx="2" />
<line x1="8" y1="3" x2="8" y2="21" />
<line x1="16" y1="3" x2="16" y2="21" />
<rect x="3" y="6" width="4" height="3" rx="1" />
<rect x="3" y="12" width="4" height="3" rx="1" />
<rect x="9" y="6" width="6" height="3" rx="1" />
<rect x="17" y="6" width="4" height="3" rx="1" />
</>
),
rpa: (
<>
<rect x="4" y="6" width="16" height="12" rx="3" />
<circle cx="9" cy="11" r="1.5" />
<circle cx="15" cy="11" r="1.5" />
<path d="M9 15h6" />
<line x1="12" y1="3" x2="12" y2="6" />
<circle cx="12" cy="2.5" r="0.5" />
<line x1="4" y1="11" x2="2" y2="11" />
<line x1="20" y1="11" x2="22" y2="11" />
</>
),
groupware: (
<>
<circle cx="12" cy="5" r="2" />
<circle cx="19" cy="12" r="2" />
<circle cx="12" cy="19" r="2" />
<circle cx="5" cy="12" r="2" />
<line x1="12" y1="7" x2="17.5" y2="10.5" />
<line x1="17.5" y1="13.5" x2="12" y2="17" />
<line x1="12" y1="17" x2="6.5" y2="13.5" />
<line x1="6.5" y1="10.5" x2="12" y2="7" />
</>
),
portal: (
<>
<rect x="7" y="2" width="10" height="20" rx="2" />
<path d="M12 10v4" />
<circle cx="12" cy="12" r="1" />
<path d="M4 6C2.5 7.5 2 9.5 2 12s.5 4.5 2 6" />
<path d="M20 6c1.5 1.5 2 3.5 2 6s-.5 4.5-2 6" />
</>
),
mall: (
<>
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
<line x1="3" y1="6" x2="21" y2="6" />
<path d="M12 10c-1 0-2 .5-2 2s1 2 2 2 2-.5 2-2-1-2-2-2z" />
<path d="M12 8v1M10.3 9.3l.7.7M14 10l-.7.7" />
</>
),
cms: (
<>
<rect x="3" y="3" width="18" height="18" rx="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="3" y1="15" x2="21" y2="15" />
<line x1="9" y1="9" x2="9" y2="21" />
</>
),
mes: (
<>
<path d="M3 21h18" />
<path d="M3 21V11l5-3v3l5-3v3l5-3v10" />
<rect x="7" y="14" width="3" height="7" />
<rect x="14" y="14" width="3" height="7" />
<line x1="18" y1="5" x2="18" y2="8" />
<path d="M16 5h4" />
</>
),
hrm: (
<>
<circle cx="9" cy="7" r="4" />
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" />
<polyline points="16,8 19,5 22,8" />
<line x1="19" y1="5" x2="19" y2="14" />
</>
),
esn: (
<>
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
<line x1="7" y1="7" x2="7.01" y2="7" />
<path d="M15 5c1 1.5 1.5 3 1.5 5s-.5 3.5-1.5 5" />
<path d="M18 3c1.5 2 2.5 4.5 2.5 7s-1 5-2.5 7" />
</>
),
fa: (
<>
<rect x="2" y="14" width="4" height="6" rx="1" />
<path d="M6 17h3l3-6h5" />
<path d="M17 11V7a2 2 0 0 0-2-2h-2" />
<circle cx="13" cy="4" r="2" />
<path d="M20 9l2 2-2 2" />
<line x1="17" y1="11" x2="22" y2="11" />
</>
),
// UI icons
copy: (
<>
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</>
),
'external-link': (
<>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15,3 21,3 21,9" />
<line x1="10" y1="14" x2="21" y2="3" />
</>
),
mobile: (
<>
<rect x="5" y="2" width="14" height="20" rx="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</>
),
check: <polyline points="20,6 9,17 4,12" />,
'chevron-right': <polyline points="9,18 15,12 9,6" />,
};
export const SolutionIcon: React.FC<SolutionIconProps> = ({ slug, size = 24, color }) => {
const paths = ICON_PATHS[slug];
if (!paths) return null;
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="none"
stroke={color || 'currentColor'}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{paths}
</svg>
);
};
export default SolutionIcon;

View File

@ -0,0 +1,5 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export const StepGuide = ({ steps, color }) => {
return (_jsx("ol", { className: "step-guide", children: steps.map((s, i) => (_jsxs("li", { className: "step-guide-item", children: [_jsxs("div", { className: "step-guide-rail", children: [_jsx("span", { className: "step-guide-num", style: { background: color }, children: s.num }), i < steps.length - 1 && _jsx("span", { className: "step-guide-line" })] }), _jsxs("div", { className: "step-guide-body", children: [_jsx("h4", { children: s.title }), _jsx("p", { children: s.desc })] })] }, s.num))) }));
};
export default StepGuide;

View File

@ -0,0 +1,29 @@
import React from 'react';
interface StepGuideProps {
steps: { num: number; title: string; desc: string }[];
color: string;
}
export const StepGuide: React.FC<StepGuideProps> = ({ steps, color }) => {
return (
<ol className="step-guide">
{steps.map((s, i) => (
<li className="step-guide-item" key={s.num}>
<div className="step-guide-rail">
<span className="step-guide-num" style={{ background: color }}>
{s.num}
</span>
{i < steps.length - 1 && <span className="step-guide-line" />}
</div>
<div className="step-guide-body">
<h4>{s.title}</h4>
<p>{s.desc}</p>
</div>
</li>
))}
</ol>
);
};
export default StepGuide;

521
src/data/solutions.js Normal file
View File

@ -0,0 +1,521 @@
const IP = '101.79.17.164';
const url = (port) => `http://${IP}:${port}`;
export const SOLUTIONS = [
{
id: 'itsm',
slug: 'itsm',
name: 'GUARDiA ITSM',
tagline: 'AI 기반 레거시 인프라 자율 운영 플랫폼',
category: '운영관리',
port: 9001,
color: '#6366f1',
status: 'live',
access: { url: url(9001), httpsUrl: 'https://zioinfo.co.kr:8443', id: 'admin', pw: '1111' },
features: [
{ title: 'SR 자동화', desc: 'SR(서비스요청) 접수·처리·완료 전 과정 자동화' },
{ title: 'CMDB 자산관리', desc: 'CMDB 서버 자산 등록·조회·의존성 관리' },
{ title: 'AI 자연어 명령', desc: '`/배포 서버A 앱B` 자연어 명령으로 운영 자동화' },
{ title: '인시던트·변경관리', desc: '인시던트 대응 및 CAB 승인 변경관리' },
{ title: 'SLA 모니터링', desc: 'SLA 실시간 모니터링 및 위반 알림' },
{ title: '배포 자동화', desc: 'SSH/SFTP 에이전트리스 배포 자동화' },
{ title: '보안 점검', desc: 'CSAP·ISMS 보안 점검 자동화' },
{ title: '메신저 봇 연동', desc: '`/sr`, `/status` 메신저 봇 명령어 연동' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'https://zioinfo.co.kr:8443 접속 → admin / 1111 로그인' },
{ num: 2, title: '대시보드 확인', desc: '오늘 SR 수, 처리율, SLA 현황 확인' },
{ num: 3, title: 'SR 접수', desc: '[새 요청] → 제목 입력 → 자동 분류 확인' },
{ num: 4, title: 'AI 명령', desc: '메신저 탭 → `/sr 서버 장애 발생` 입력' },
{ num: 5, title: '배포', desc: '[배포관리] → [새 배포] → 서버 선택 → 실행' },
{ num: 6, title: 'CMDB', desc: '[자산관리] → 서버 목록 확인' },
],
mobileScreens: [
{ name: 'SR 목록', type: 'customer' },
{ name: 'SR 상세', type: 'customer' },
{ name: '빠른 SR 등록', type: 'customer' },
{ name: '승인 요청', type: 'admin' },
{ name: '서버 현황', type: 'admin' },
{ name: 'AI 챗봇', type: 'customer' },
],
},
{
id: 'erp',
slug: 'erp',
name: 'GUARDiA ERP',
tagline: 'AI 기반 스마트 엔터프라이즈 자원관리',
category: '운영관리',
port: 8003,
color: '#10b981',
status: 'live',
access: { url: url(8003), id: 'admin', pw: 'admin123' },
features: [
{ title: '전표 자동 분류', desc: 'AI 기반 전표 자동 분류 및 계정 매핑' },
{ title: '예산·KPI', desc: '예산 관리 및 KPI 대시보드' },
{ title: '구매 발주', desc: '구매 발주 · 입고 처리 워크플로우' },
{ title: '인사·급여 연동', desc: 'HRM 인사 · 급여 시스템 연동' },
{ title: '영업·매출 분석', desc: '영업 활동 및 매출 분석' },
{ title: 'FinOps', desc: 'FinOps 클라우드 비용 추적' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8003 접속 → admin / admin123 로그인' },
{ num: 2, title: '대시보드', desc: '매출·비용·KPI 현황 확인' },
{ num: 3, title: '재무', desc: '[전표입력] → 항목 입력 → [AI 분류] 클릭 → 계정 자동 입력 확인' },
{ num: 4, title: '예산', desc: '[예산현황] → 부서별 집행률 확인' },
{ num: 5, title: '구매', desc: '[발주서] → [신규] → 품목 입력 → 공급업체 선택 → 승인 요청' },
],
mobileScreens: [
{ name: 'KPI 대시보드', type: 'admin' },
{ name: '전표 조회', type: 'admin' },
{ name: '예산 현황', type: 'admin' },
{ name: '구매 승인', type: 'admin' },
{ name: '영업 매출', type: 'admin' },
],
},
{
id: 'crm',
slug: 'crm',
name: 'GUARDiA CRM',
tagline: 'AI 기반 공공기관 고객관계관리',
category: '운영관리',
port: 8004,
color: '#f59e0b',
status: 'live',
access: { url: url(8004), id: 'admin', pw: 'admin123' },
features: [
{ title: '고객 관리', desc: '고객(기관) 등록 · 히스토리 관리' },
{ title: '영업 파이프라인', desc: '영업기회 파이프라인 단계별 관리' },
{ title: '계약·입찰 추적', desc: '나라장터 G2B 연동 계약 · 입찰 추적' },
{ title: '캠페인 관리', desc: '마케팅 캠페인 기획·실행·분석' },
{ title: 'AI 고객 인사이트', desc: 'AI 기반 고객 행동 인사이트' },
{ title: 'ITSM SR 연동', desc: '고객 문의 → SR 자동 생성' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8004 접속 → admin / admin123 로그인' },
{ num: 2, title: '대시보드', desc: '영업 파이프라인 현황 확인' },
{ num: 3, title: '고객', desc: '[고객등록] → 기관명·담당자·연락처 입력' },
{ num: 4, title: '영업기회', desc: '[새 기회] → 고객 연결 → 단계 설정 (발굴→제안→협상→계약)' },
{ num: 5, title: '계약', desc: '[계약등록] → 나라장터 계약번호 입력 → OCR로 계약서 자동 파싱' },
],
mobileScreens: [
{ name: '고객 목록', type: 'admin' },
{ name: '영업기회', type: 'admin' },
{ name: '계약 현황', type: 'admin' },
{ name: '입찰 알림', type: 'admin' },
{ name: 'AI 분석', type: 'admin' },
],
},
{
id: 'ocr',
slug: 'ocr',
name: 'GUARDiA OCR',
tagline: 'AI 기반 지능형 문서 인식 플랫폼',
category: 'AI문서',
port: 8005,
color: '#8b5cf6',
status: 'live',
access: { url: url(8005), id: 'admin', pw: 'admin123' },
features: [
{ title: '계약서 등록', desc: '계약서 → 나라장터 계약 자동 등록' },
{ title: '납품서 처리', desc: '납품서 → CMDB 서버 자동 등록' },
{ title: '청구서 처리', desc: '청구서 → 세금계산서 처리' },
{ title: '장애보고서 → SR', desc: '장애보고서 이미지 → SR 자동 생성' },
{ title: '회의록 → SR', desc: '회의록 → 액션아이템 SR 생성' },
{ title: '감사보고서', desc: '감사보고서 → CSAP 준수율 업데이트' },
{ title: '브랜드 계약서', desc: '현대백화점 등 브랜드 계약서 → 계약 이력' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8005 접속 → admin / admin123 로그인' },
{ num: 2, title: '문서 업로드', desc: '[문서 업로드] → 문서 이미지/PDF 선택' },
{ num: 3, title: '워크플로우 선택', desc: '워크플로우 선택 → 장애보고서→SR 선택' },
{ num: 4, title: 'OCR 실행', desc: '[OCR 실행] 클릭 → AI 텍스트 추출 확인' },
{ num: 5, title: 'SR 생성', desc: '추출 결과 확인 → [SR 자동 생성] 클릭 → ITSM SR 생성 확인' },
],
mobileScreens: [
{ name: '카메라 촬영', type: 'field' },
{ name: '문서 업로드', type: 'field' },
{ name: 'OCR 결과', type: 'customer' },
{ name: '워크플로우', type: 'customer' },
{ name: '처리 이력', type: 'customer' },
],
},
{
id: 'bi',
slug: 'bi',
name: 'GUARDiA BI',
tagline: 'AI 기반 비즈니스 인텔리전스 플랫폼',
category: 'AI문서',
port: 8006,
color: '#06b6d4',
status: 'live',
access: { url: url(8006), id: 'admin', pw: 'admin123' },
features: [
{ title: '데이터소스 연결', desc: 'PostgreSQL · API · CSV 데이터소스 연결' },
{ title: '대시보드 빌더', desc: '드래그앤드롭 대시보드 빌더' },
{ title: 'KPI 위젯·알림', desc: 'KPI 위젯 및 실시간 알림' },
{ title: 'AI Text-to-SQL', desc: '자연어 → SQL 쿼리 자동 생성' },
{ title: '예측·이상탐지', desc: 'AI 예측 및 이상탐지 분석' },
{ title: 'ITSM 데이터 피드', desc: 'SR·SLA·배포 통계 데이터 피드' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8006 접속 → admin / admin123 로그인' },
{ num: 2, title: '데이터소스', desc: '[추가] → PostgreSQL 선택 → ITSM DB 연결' },
{ num: 3, title: '대시보드', desc: '[새 대시보드] → 위젯 추가 → 막대/라인/KPI 선택' },
{ num: 4, title: 'AI 쿼리', desc: '검색창에 "이번 달 미처리 SR 현황" 입력 → SQL 자동 생성 확인' },
{ num: 5, title: '리포트', desc: '[내보내기] → PDF 저장' },
],
mobileScreens: [
{ name: 'KPI 대시보드', type: 'admin' },
{ name: '차트 조회', type: 'admin' },
{ name: 'AI 인사이트', type: 'admin' },
{ name: '리포트', type: 'admin' },
{ name: '알림', type: 'admin' },
],
},
{
id: 'pms',
slug: 'pms',
name: 'GUARDiA PMS',
tagline: 'JIRA 호환 AI 프로젝트 관리 시스템',
category: '프로젝트',
port: 8007,
color: '#f97316',
status: 'live',
access: { url: url(8007), id: 'admin', pw: 'admin123' },
features: [
{ title: '이슈 추적', desc: '이슈 생성·추적 (JIRA 호환)' },
{ title: 'Scrum·Kanban', desc: 'Scrum · Kanban 보드' },
{ title: 'WBS·마일스톤', desc: 'WBS · 마일스톤 관리' },
{ title: '전자결재', desc: '다단계 결재선 전자결재' },
{ title: 'SI 산출물', desc: 'SI 산출물 관리 (감리/준공)' },
{ title: '요구사항 추적', desc: '요구사항 추적 (RTM)' },
{ title: 'AI 회의록', desc: 'AI 회의록 자동 작성' },
{ title: 'RFP 분석', desc: 'RFP → 요구사항분석서 자동 생성' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8007 접속 → admin / admin123 로그인' },
{ num: 2, title: '프로젝트', desc: '[새 프로젝트] → 이름·유형(Scrum) 선택' },
{ num: 3, title: '백로그', desc: '[이슈 추가] → 제목·담당자·우선순위 입력' },
{ num: 4, title: '보드', desc: 'Kanban 뷰 전환 → 이슈 드래그앤드롭으로 상태 변경' },
{ num: 5, title: 'AI 기능', desc: '[회의록] → 텍스트 붙여넣기 → [액션아이템 추출] 클릭' },
],
mobileScreens: [
{ name: '내 이슈', type: 'customer' },
{ name: '보드', type: 'customer' },
{ name: '스프린트', type: 'customer' },
{ name: '승인 요청', type: 'admin' },
{ name: 'WBS 조회', type: 'admin' },
],
},
{
id: 'rpa',
slug: 'rpa',
name: 'GUARDiA RPA',
tagline: 'AI 기반 노코드 업무 자동화 플랫폼',
category: '프로젝트',
port: 8008,
color: '#ec4899',
status: 'live',
access: { url: url(8008), id: 'admin', pw: 'admin123' },
features: [
{ title: '노코드 워크플로우', desc: '노코드 워크플로우 빌더' },
{ title: '봇 스케줄링', desc: '봇 등록 · 스케줄링' },
{ title: '자연어 생성', desc: '자연어 → 워크플로우 자동 생성' },
{ title: 'ROI 분석', desc: 'ROI 분석 대시보드' },
{ title: 'ITSM 연동', desc: 'ITSM SR 자동 처리 연동' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8008 접속 → admin / admin123 로그인' },
{ num: 2, title: '워크플로우', desc: '[새 워크플로우] → 노드 드래그앤드롭' },
{ num: 3, title: '트리거 설정', desc: '[스케줄] 선택 → 매일 09:00 설정' },
{ num: 4, title: '액션 추가', desc: '[ITSM SR 생성] 노드 추가 → 파라미터 입력' },
{ num: 5, title: 'AI 생성', desc: '입력창에 "매일 아침 서버 상태 체크 후 이상 시 SR 생성" → [생성] 클릭' },
],
mobileScreens: [
{ name: '봇 현황', type: 'admin' },
{ name: '워크플로우 목록', type: 'admin' },
{ name: '실행 이력', type: 'admin' },
{ name: 'ROI 현황', type: 'admin' },
{ name: '알림', type: 'admin' },
],
},
{
id: 'groupware',
slug: 'groupware',
name: 'GUARDiA Groupware',
tagline: 'AI 기반 통합 협업 그룹웨어',
category: '협업',
port: 8009,
color: '#14b8a6',
status: 'live',
access: { url: url(8009), id: 'admin', pw: 'admin123' },
features: [
{ title: '전자결재', desc: '다단계 결재선 · 대리결재' },
{ title: '게시판', desc: '공지 · 자유 · 팀별 게시판' },
{ title: '일정/캘린더', desc: '팀 공유 일정/캘린더' },
{ title: '근태/휴가', desc: '근태/휴가 신청·승인' },
{ title: '자원예약', desc: '회의실 · 장비 자원예약' },
{ title: '주소록·조직도', desc: '주소록 · 조직도 관리' },
{ title: '문서함', desc: '버전관리 문서함' },
{ title: '쪽지', desc: '내부 메시지 쪽지' },
{ title: 'AI 문서 요약', desc: 'AI 문서 자동 요약' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8009 접속 → admin / admin123 로그인' },
{ num: 2, title: '전자결재', desc: '[기안] → 양식 선택 → 내용 작성 → 결재선 설정 → 상신' },
{ num: 3, title: '일정', desc: '[일정등록] → 제목·날짜·참석자 입력 → 팀 공유' },
{ num: 4, title: '근태', desc: '[휴가신청] → 날짜·종류 선택 → 결재 요청' },
{ num: 5, title: '회의실', desc: '[예약] → 날짜·시간·회의실 선택 → 확정' },
],
mobileScreens: [
{ name: '결재함', type: 'admin' },
{ name: '공지사항', type: 'customer' },
{ name: '일정', type: 'customer' },
{ name: '근태', type: 'customer' },
{ name: '주소록', type: 'customer' },
],
},
{
id: 'portal',
slug: 'portal',
name: 'GUARDiA Portal',
tagline: 'SSO 단일 로그인 통합 업무 포털',
category: '협업',
port: 8010,
color: '#7c3aed',
status: 'live',
access: { url: url(8010), id: 'admin', pw: 'admin123' },
features: [
{ title: 'SSO 단일 로그인', desc: 'GUARDiA 전 시스템 SSO 바로가기' },
{ title: '위젯 대시보드', desc: '역할별 위젯 대시보드' },
{ title: '즐겨찾기', desc: '즐겨찾기 시스템' },
{ title: '통합 공지', desc: '통합 공지사항' },
{ title: '알림 센터', desc: '전 시스템 통합 알림 센터' },
{ title: '통합 검색', desc: '전 시스템 통합 검색' },
{ title: '마이페이지', desc: '프로필 · 설정 마이페이지' },
{ title: 'AI 공지 요약', desc: 'AI 공지 자동 요약' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8010 접속 → admin / admin123 로그인' },
{ num: 2, title: '포털 메인', desc: '9개 시스템 바로가기 카드 확인' },
{ num: 3, title: '위젯', desc: '[위젯 추가] → SR현황·KPI·일정 선택 → 대시보드 배치' },
{ num: 4, title: '통합검색', desc: '검색창에 "장애 처리" 입력 → ITSM·KB·문서 통합 결과 확인' },
{ num: 5, title: '알림센터', desc: '전 시스템 알림 통합 확인' },
],
mobileScreens: [
{ name: '포털 홈', type: 'customer' },
{ name: '알림', type: 'customer' },
{ name: '바로가기', type: 'customer' },
{ name: '통합검색', type: 'customer' },
{ name: '마이페이지', type: 'customer' },
],
},
{
id: 'mall',
slug: 'mall',
name: 'GUARDiA Mall',
tagline: '미국 다지점 꽃집 옴니채널 e-커머스',
category: '커머스',
port: 8011,
color: '#f43f5e',
status: 'live',
access: { url: url(8011), id: 'admin', pw: 'admin123' },
features: [
{ title: 'ZIP 배송권역', desc: '다매장 ZIP 배송권역 관리' },
{ title: '타임슬롯', desc: '타임슬롯 스케줄링 (당일배송)' },
{ title: '구독 서비스', desc: '정기 구독 서비스' },
{ title: '게이트웨이 어댑터', desc: 'Stripe · Twilio mock 게이트웨이 어댑터' },
{ title: 'AI 추천·예측', desc: 'AI 상품 추천 · 수요예측' },
{ title: '매장간 재고 이양', desc: '매장간 재고 이양' },
{ title: '관리자 앱', desc: '매장 매니저용 관리자 앱' },
{ title: 'i18n 한/영', desc: '한국어/영어 다국어 지원' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8011 접속 → admin / admin123 로그인' },
{ num: 2, title: '상품 확인', desc: '상품 목록 확인 (Montvale Florist 실제 상품)' },
{ num: 3, title: '주문', desc: '상품 선택 → ZIP 코드 입력 → 배송 슬롯 선택 → 장바구니 추가' },
{ num: 4, title: '결제', desc: '카드 정보 (mock: 4242-4242-4242-4242) → 주문 완료' },
{ num: 5, title: '관리자', desc: '[주문관리] → 주문 확인 → 상태 변경 (준비중→배송중→완료)' },
],
mobileScreens: [
{ name: '상품 목록', type: 'customer' },
{ name: '상품 상세', type: 'customer' },
{ name: '장바구니', type: 'customer' },
{ name: '결제', type: 'customer' },
{ name: '주문 칸반', type: 'admin' },
{ name: '재고 ON/OFF', type: 'admin' },
],
},
{
id: 'cms',
slug: 'cms',
name: 'GUARDiA CMS',
tagline: 'AI 기반 통합 콘텐츠 관리 시스템',
category: '커머스',
port: 8012,
color: '#0ea5e9',
status: 'live',
access: { url: url(8012), id: 'admin', pw: 'admin123' },
features: [
{ title: '헤드리스 CMS', desc: '페이지 · 포스트 · 블록 헤드리스 CMS' },
{ title: '게시 워크플로우', desc: '버전관리 · 게시 워크플로우 (draft→published)' },
{ title: '미디어 라이브러리', desc: '미디어 라이브러리 관리' },
{ title: '배너·프로모션', desc: '배너 · 프로모션 관리' },
{ title: 'AI SEO 제안', desc: 'AI 기반 SEO 자동 제안' },
{ title: '다국어·테마', desc: '다국어 · 테마 관리' },
{ title: 'UGC 모더레이션', desc: 'UGC 콘텐츠 모더레이션' },
{ title: '폼 빌더', desc: '드래그앤드롭 폼 빌더' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8012 접속 → admin / admin123 로그인' },
{ num: 2, title: '페이지', desc: '[새 페이지] → 제목 입력 → 블록 에디터에서 콘텐츠 추가' },
{ num: 3, title: '미디어', desc: '[업로드] → 이미지 선택 → AI 자동 태그 확인' },
{ num: 4, title: '워크플로우', desc: '페이지 상태를 draft → review → published 변경' },
{ num: 5, title: 'SEO', desc: '페이지 선택 → [AI SEO 제안] → 메타태그 자동 입력 확인' },
],
mobileScreens: [
{ name: '콘텐츠 목록', type: 'admin' },
{ name: '콘텐츠 승인', type: 'admin' },
{ name: '미디어 촬영업로드', type: 'field' },
{ name: 'UGC 모더레이션', type: 'admin' },
{ name: '성과', type: 'admin' },
],
},
{
id: 'mes',
slug: 'mes',
name: 'GUARDiA MES',
tagline: 'AI 기반 제조실행시스템 (WMS+MES+QMS)',
category: '제조',
port: 8013,
color: '#84cc16',
status: 'live',
access: { url: url(8013), id: 'admin', pw: 'admin123' },
features: [
{ title: '작업지시·실적', desc: '작업지시 · 생산실적 등록' },
{ title: 'BOM·라우팅', desc: 'BOM · 공정/라우팅 관리' },
{ title: '설비 OEE', desc: '설비 OEE 모니터링' },
{ title: 'LOT 추적', desc: 'LOT 추적' },
{ title: '창고관리', desc: '입고·출고·재고·피킹 창고관리' },
{ title: '검사', desc: '수입·공정·출하 검사' },
{ title: 'SPC 관리도', desc: 'SPC 관리도 (Cp/Cpk)' },
{ title: 'CAPA', desc: 'CAPA (시정조치)' },
{ title: 'AI 불량분석', desc: 'AI 불량원인 분석' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8013 접속 → admin / admin123 로그인' },
{ num: 2, title: '작업지시', desc: '[신규] → 품목·수량·공정 입력 → 발행' },
{ num: 3, title: '생산실적', desc: '작업지시 선택 → 생산량·불량량 입력 → 등록' },
{ num: 4, title: '검사', desc: '[공정검사] → LOT 번호 입력 → 검사항목 체크 → 합격/불합격' },
{ num: 5, title: 'SPC', desc: '관리도 차트 확인 → Cp/Cpk 수치 확인 → 이상 점 알림' },
],
mobileScreens: [
{ name: '작업지시 조회', type: 'field' },
{ name: '실적입력', type: 'field' },
{ name: '바코드스캔', type: 'field' },
{ name: '검사입력', type: 'field' },
{ name: '설비점검', type: 'field' },
],
},
{
id: 'hrm',
slug: 'hrm',
name: 'GUARDiA HRM',
tagline: 'AI 기반 인사관리 플랫폼',
category: '운영관리',
port: 8014,
color: '#a855f7',
status: 'live',
access: { url: url(8014), id: 'admin', pw: 'admin123' },
features: [
{ title: '사원 관리', desc: '사원 등록 · 이력관리' },
{ title: '조직도·직급', desc: '조직도 · 직급체계' },
{ title: '급여·명세서', desc: '급여 계산 · 명세서' },
{ title: '근태·휴가', desc: '근태 · 휴가 관리' },
{ title: '성과 평가', desc: '성과 평가 · KPI' },
{ title: '채용 관리', desc: '채용 공고 · 지원자 관리' },
{ title: '교육 관리', desc: '교육 이수 관리' },
{ title: 'AI 인사 분석', desc: 'AI 인사 분석' },
{ title: 'Groupware 연동', desc: 'Groupware 연동' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8014 접속 → admin / admin123 로그인' },
{ num: 2, title: '사원', desc: '[신규 등록] → 기본정보·부서·직급 입력 → 저장' },
{ num: 3, title: '급여', desc: '사원 선택 → 월급여 입력 → [명세서 생성] → PDF 확인' },
{ num: 4, title: '성과', desc: '[평가 등록] → KPI 항목 입력 → 점수 부여' },
{ num: 5, title: 'AI', desc: '[인사 인사이트] → 이탈 위험도·성과 트렌드 확인' },
],
mobileScreens: [
{ name: '사원증', type: 'customer' },
{ name: '근태 체크인', type: 'customer' },
{ name: '급여 명세서', type: 'customer' },
{ name: '휴가신청', type: 'customer' },
{ name: '성과현황', type: 'customer' },
],
},
{
id: 'esn',
slug: 'esn',
name: 'GUARDiA ESN',
tagline: 'ESL 전자가격표 통합 관리 플랫폼',
category: '커머스',
port: 8015,
color: '#f59e0b',
status: 'live',
access: { url: url(8015), id: 'admin', pw: 'admin123' },
features: [
{ title: '멀티테넌트', desc: 'LGInnotek · LGIT · Emart · ZIOINFO 멀티테넌트' },
{ title: 'ESL 기기 관리', desc: 'ESL 기기 등록 · 상태 모니터링' },
{ title: 'POS 자동 변환', desc: 'POS 데이터 → ESL 가격 자동 변환' },
{ title: '템플릿 관리', desc: '크기별 레이아웃 템플릿 관리' },
{ title: 'HCore 관제', desc: 'HCore 게이트웨이/허브 관제' },
{ title: '알람·장애 이력', desc: '알람 · 장애 이력 관리' },
{ title: '펌웨어 업데이트', desc: '펌웨어 업데이트 관리' },
{ title: 'AI 알람 분석', desc: 'Ollama AI 알람 원인 분석' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8015 접속 → admin / admin123 로그인' },
{ num: 2, title: '테넌트 선택', desc: '드롭다운에서 LGINNOTEK 선택' },
{ num: 3, title: 'ESL 기기', desc: '기기 목록 확인 → 상태(온라인/오프라인) 확인' },
{ num: 4, title: 'POS 변환', desc: '[POS 데이터 업로드] → CSV 파일 선택 → 변환 실행' },
{ num: 5, title: '알람', desc: '알람 목록 → [AI 원인분석] 클릭 → Ollama 분석 결과 확인' },
],
mobileScreens: [
{ name: 'ESL 기기 목록', type: 'field' },
{ name: '알람', type: 'field' },
{ name: 'HCore 상태', type: 'field' },
{ name: '펌웨어', type: 'admin' },
{ name: '작업 이력', type: 'field' },
],
},
{
id: 'fa',
slug: 'fa',
name: 'GUARDiA FA',
tagline: 'AI 기반 스마트 팩토리 자동화 플랫폼 (준비중)',
category: '제조',
port: 8017,
color: '#64748b',
status: 'soon',
access: { url: url(8017), id: '', pw: '' },
features: [
{ title: 'e-Paper 지시서', desc: 'e-Paper 전자 공정 지시서' },
{ title: 'QR 작업 추적', desc: 'QR 기반 작업 추적' },
{ title: 'MES 연동', desc: 'MES 연동 실시간 현황' },
{ title: '로봇·PLC 인터페이스', desc: '로봇 · PLC 인터페이스' },
{ title: 'AI 공정 최적화', desc: 'AI 공정 최적화' },
],
steps: [
{ num: 1, title: '배포 예정', desc: 'GUARDiA FA(공장 자동화)는 현재 준비 중입니다.' },
],
mobileScreens: [],
},
];
export const CATEGORIES = [
{ name: '운영관리', slugs: ['itsm', 'erp', 'crm', 'hrm'] },
{ name: 'AI문서', slugs: ['ocr', 'bi'] },
{ name: '프로젝트', slugs: ['pms', 'rpa'] },
{ name: '협업', slugs: ['groupware', 'portal'] },
{ name: '커머스', slugs: ['mall', 'cms', 'esn'] },
{ name: '제조', slugs: ['mes', 'fa'] },
];
export const getSolution = (slug) => SOLUTIONS.find((s) => s.slug === slug);

545
src/data/solutions.ts Normal file
View File

@ -0,0 +1,545 @@
export interface Solution {
id: string;
slug: string;
name: string;
tagline: string;
category: string;
port: number;
color: string;
status: 'live' | 'soon' | 'dev';
access: {
url: string;
httpsUrl?: string;
id: string;
pw: string;
};
features: { title: string; desc: string }[];
steps: { num: number; title: string; desc: string }[];
mobileScreens: { name: string; type: 'customer' | 'admin' | 'field' }[];
}
const IP = '101.79.17.164';
const url = (port: number) => `http://${IP}:${port}`;
export const SOLUTIONS: Solution[] = [
{
id: 'itsm',
slug: 'itsm',
name: 'GUARDiA ITSM',
tagline: 'AI 기반 레거시 인프라 자율 운영 플랫폼',
category: '운영관리',
port: 9001,
color: '#6366f1',
status: 'live',
access: { url: url(9001), httpsUrl: 'https://zioinfo.co.kr:8443', id: 'admin', pw: '1111' },
features: [
{ title: 'SR 자동화', desc: 'SR(서비스요청) 접수·처리·완료 전 과정 자동화' },
{ title: 'CMDB 자산관리', desc: 'CMDB 서버 자산 등록·조회·의존성 관리' },
{ title: 'AI 자연어 명령', desc: '`/배포 서버A 앱B` 자연어 명령으로 운영 자동화' },
{ title: '인시던트·변경관리', desc: '인시던트 대응 및 CAB 승인 변경관리' },
{ title: 'SLA 모니터링', desc: 'SLA 실시간 모니터링 및 위반 알림' },
{ title: '배포 자동화', desc: 'SSH/SFTP 에이전트리스 배포 자동화' },
{ title: '보안 점검', desc: 'CSAP·ISMS 보안 점검 자동화' },
{ title: '메신저 봇 연동', desc: '`/sr`, `/status` 메신저 봇 명령어 연동' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'https://zioinfo.co.kr:8443 접속 → admin / 1111 로그인' },
{ num: 2, title: '대시보드 확인', desc: '오늘 SR 수, 처리율, SLA 현황 확인' },
{ num: 3, title: 'SR 접수', desc: '[새 요청] → 제목 입력 → 자동 분류 확인' },
{ num: 4, title: 'AI 명령', desc: '메신저 탭 → `/sr 서버 장애 발생` 입력' },
{ num: 5, title: '배포', desc: '[배포관리] → [새 배포] → 서버 선택 → 실행' },
{ num: 6, title: 'CMDB', desc: '[자산관리] → 서버 목록 확인' },
],
mobileScreens: [
{ name: 'SR 목록', type: 'customer' },
{ name: 'SR 상세', type: 'customer' },
{ name: '빠른 SR 등록', type: 'customer' },
{ name: '승인 요청', type: 'admin' },
{ name: '서버 현황', type: 'admin' },
{ name: 'AI 챗봇', type: 'customer' },
],
},
{
id: 'erp',
slug: 'erp',
name: 'GUARDiA ERP',
tagline: 'AI 기반 스마트 엔터프라이즈 자원관리',
category: '운영관리',
port: 8003,
color: '#10b981',
status: 'live',
access: { url: url(8003), id: 'admin', pw: 'admin123' },
features: [
{ title: '전표 자동 분류', desc: 'AI 기반 전표 자동 분류 및 계정 매핑' },
{ title: '예산·KPI', desc: '예산 관리 및 KPI 대시보드' },
{ title: '구매 발주', desc: '구매 발주 · 입고 처리 워크플로우' },
{ title: '인사·급여 연동', desc: 'HRM 인사 · 급여 시스템 연동' },
{ title: '영업·매출 분석', desc: '영업 활동 및 매출 분석' },
{ title: 'FinOps', desc: 'FinOps 클라우드 비용 추적' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8003 접속 → admin / admin123 로그인' },
{ num: 2, title: '대시보드', desc: '매출·비용·KPI 현황 확인' },
{ num: 3, title: '재무', desc: '[전표입력] → 항목 입력 → [AI 분류] 클릭 → 계정 자동 입력 확인' },
{ num: 4, title: '예산', desc: '[예산현황] → 부서별 집행률 확인' },
{ num: 5, title: '구매', desc: '[발주서] → [신규] → 품목 입력 → 공급업체 선택 → 승인 요청' },
],
mobileScreens: [
{ name: 'KPI 대시보드', type: 'admin' },
{ name: '전표 조회', type: 'admin' },
{ name: '예산 현황', type: 'admin' },
{ name: '구매 승인', type: 'admin' },
{ name: '영업 매출', type: 'admin' },
],
},
{
id: 'crm',
slug: 'crm',
name: 'GUARDiA CRM',
tagline: 'AI 기반 공공기관 고객관계관리',
category: '운영관리',
port: 8004,
color: '#f59e0b',
status: 'live',
access: { url: url(8004), id: 'admin', pw: 'admin123' },
features: [
{ title: '고객 관리', desc: '고객(기관) 등록 · 히스토리 관리' },
{ title: '영업 파이프라인', desc: '영업기회 파이프라인 단계별 관리' },
{ title: '계약·입찰 추적', desc: '나라장터 G2B 연동 계약 · 입찰 추적' },
{ title: '캠페인 관리', desc: '마케팅 캠페인 기획·실행·분석' },
{ title: 'AI 고객 인사이트', desc: 'AI 기반 고객 행동 인사이트' },
{ title: 'ITSM SR 연동', desc: '고객 문의 → SR 자동 생성' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8004 접속 → admin / admin123 로그인' },
{ num: 2, title: '대시보드', desc: '영업 파이프라인 현황 확인' },
{ num: 3, title: '고객', desc: '[고객등록] → 기관명·담당자·연락처 입력' },
{ num: 4, title: '영업기회', desc: '[새 기회] → 고객 연결 → 단계 설정 (발굴→제안→협상→계약)' },
{ num: 5, title: '계약', desc: '[계약등록] → 나라장터 계약번호 입력 → OCR로 계약서 자동 파싱' },
],
mobileScreens: [
{ name: '고객 목록', type: 'admin' },
{ name: '영업기회', type: 'admin' },
{ name: '계약 현황', type: 'admin' },
{ name: '입찰 알림', type: 'admin' },
{ name: 'AI 분석', type: 'admin' },
],
},
{
id: 'ocr',
slug: 'ocr',
name: 'GUARDiA OCR',
tagline: 'AI 기반 지능형 문서 인식 플랫폼',
category: 'AI문서',
port: 8005,
color: '#8b5cf6',
status: 'live',
access: { url: url(8005), id: 'admin', pw: 'admin123' },
features: [
{ title: '계약서 등록', desc: '계약서 → 나라장터 계약 자동 등록' },
{ title: '납품서 처리', desc: '납품서 → CMDB 서버 자동 등록' },
{ title: '청구서 처리', desc: '청구서 → 세금계산서 처리' },
{ title: '장애보고서 → SR', desc: '장애보고서 이미지 → SR 자동 생성' },
{ title: '회의록 → SR', desc: '회의록 → 액션아이템 SR 생성' },
{ title: '감사보고서', desc: '감사보고서 → CSAP 준수율 업데이트' },
{ title: '브랜드 계약서', desc: '현대백화점 등 브랜드 계약서 → 계약 이력' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8005 접속 → admin / admin123 로그인' },
{ num: 2, title: '문서 업로드', desc: '[문서 업로드] → 문서 이미지/PDF 선택' },
{ num: 3, title: '워크플로우 선택', desc: '워크플로우 선택 → 장애보고서→SR 선택' },
{ num: 4, title: 'OCR 실행', desc: '[OCR 실행] 클릭 → AI 텍스트 추출 확인' },
{ num: 5, title: 'SR 생성', desc: '추출 결과 확인 → [SR 자동 생성] 클릭 → ITSM SR 생성 확인' },
],
mobileScreens: [
{ name: '카메라 촬영', type: 'field' },
{ name: '문서 업로드', type: 'field' },
{ name: 'OCR 결과', type: 'customer' },
{ name: '워크플로우', type: 'customer' },
{ name: '처리 이력', type: 'customer' },
],
},
{
id: 'bi',
slug: 'bi',
name: 'GUARDiA BI',
tagline: 'AI 기반 비즈니스 인텔리전스 플랫폼',
category: 'AI문서',
port: 8006,
color: '#06b6d4',
status: 'live',
access: { url: url(8006), id: 'admin', pw: 'admin123' },
features: [
{ title: '데이터소스 연결', desc: 'PostgreSQL · API · CSV 데이터소스 연결' },
{ title: '대시보드 빌더', desc: '드래그앤드롭 대시보드 빌더' },
{ title: 'KPI 위젯·알림', desc: 'KPI 위젯 및 실시간 알림' },
{ title: 'AI Text-to-SQL', desc: '자연어 → SQL 쿼리 자동 생성' },
{ title: '예측·이상탐지', desc: 'AI 예측 및 이상탐지 분석' },
{ title: 'ITSM 데이터 피드', desc: 'SR·SLA·배포 통계 데이터 피드' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8006 접속 → admin / admin123 로그인' },
{ num: 2, title: '데이터소스', desc: '[추가] → PostgreSQL 선택 → ITSM DB 연결' },
{ num: 3, title: '대시보드', desc: '[새 대시보드] → 위젯 추가 → 막대/라인/KPI 선택' },
{ num: 4, title: 'AI 쿼리', desc: '검색창에 "이번 달 미처리 SR 현황" 입력 → SQL 자동 생성 확인' },
{ num: 5, title: '리포트', desc: '[내보내기] → PDF 저장' },
],
mobileScreens: [
{ name: 'KPI 대시보드', type: 'admin' },
{ name: '차트 조회', type: 'admin' },
{ name: 'AI 인사이트', type: 'admin' },
{ name: '리포트', type: 'admin' },
{ name: '알림', type: 'admin' },
],
},
{
id: 'pms',
slug: 'pms',
name: 'GUARDiA PMS',
tagline: 'JIRA 호환 AI 프로젝트 관리 시스템',
category: '프로젝트',
port: 8007,
color: '#f97316',
status: 'live',
access: { url: url(8007), id: 'admin', pw: 'admin123' },
features: [
{ title: '이슈 추적', desc: '이슈 생성·추적 (JIRA 호환)' },
{ title: 'Scrum·Kanban', desc: 'Scrum · Kanban 보드' },
{ title: 'WBS·마일스톤', desc: 'WBS · 마일스톤 관리' },
{ title: '전자결재', desc: '다단계 결재선 전자결재' },
{ title: 'SI 산출물', desc: 'SI 산출물 관리 (감리/준공)' },
{ title: '요구사항 추적', desc: '요구사항 추적 (RTM)' },
{ title: 'AI 회의록', desc: 'AI 회의록 자동 작성' },
{ title: 'RFP 분석', desc: 'RFP → 요구사항분석서 자동 생성' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8007 접속 → admin / admin123 로그인' },
{ num: 2, title: '프로젝트', desc: '[새 프로젝트] → 이름·유형(Scrum) 선택' },
{ num: 3, title: '백로그', desc: '[이슈 추가] → 제목·담당자·우선순위 입력' },
{ num: 4, title: '보드', desc: 'Kanban 뷰 전환 → 이슈 드래그앤드롭으로 상태 변경' },
{ num: 5, title: 'AI 기능', desc: '[회의록] → 텍스트 붙여넣기 → [액션아이템 추출] 클릭' },
],
mobileScreens: [
{ name: '내 이슈', type: 'customer' },
{ name: '보드', type: 'customer' },
{ name: '스프린트', type: 'customer' },
{ name: '승인 요청', type: 'admin' },
{ name: 'WBS 조회', type: 'admin' },
],
},
{
id: 'rpa',
slug: 'rpa',
name: 'GUARDiA RPA',
tagline: 'AI 기반 노코드 업무 자동화 플랫폼',
category: '프로젝트',
port: 8008,
color: '#ec4899',
status: 'live',
access: { url: url(8008), id: 'admin', pw: 'admin123' },
features: [
{ title: '노코드 워크플로우', desc: '노코드 워크플로우 빌더' },
{ title: '봇 스케줄링', desc: '봇 등록 · 스케줄링' },
{ title: '자연어 생성', desc: '자연어 → 워크플로우 자동 생성' },
{ title: 'ROI 분석', desc: 'ROI 분석 대시보드' },
{ title: 'ITSM 연동', desc: 'ITSM SR 자동 처리 연동' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8008 접속 → admin / admin123 로그인' },
{ num: 2, title: '워크플로우', desc: '[새 워크플로우] → 노드 드래그앤드롭' },
{ num: 3, title: '트리거 설정', desc: '[스케줄] 선택 → 매일 09:00 설정' },
{ num: 4, title: '액션 추가', desc: '[ITSM SR 생성] 노드 추가 → 파라미터 입력' },
{ num: 5, title: 'AI 생성', desc: '입력창에 "매일 아침 서버 상태 체크 후 이상 시 SR 생성" → [생성] 클릭' },
],
mobileScreens: [
{ name: '봇 현황', type: 'admin' },
{ name: '워크플로우 목록', type: 'admin' },
{ name: '실행 이력', type: 'admin' },
{ name: 'ROI 현황', type: 'admin' },
{ name: '알림', type: 'admin' },
],
},
{
id: 'groupware',
slug: 'groupware',
name: 'GUARDiA Groupware',
tagline: 'AI 기반 통합 협업 그룹웨어',
category: '협업',
port: 8009,
color: '#14b8a6',
status: 'live',
access: { url: url(8009), id: 'admin', pw: 'admin123' },
features: [
{ title: '전자결재', desc: '다단계 결재선 · 대리결재' },
{ title: '게시판', desc: '공지 · 자유 · 팀별 게시판' },
{ title: '일정/캘린더', desc: '팀 공유 일정/캘린더' },
{ title: '근태/휴가', desc: '근태/휴가 신청·승인' },
{ title: '자원예약', desc: '회의실 · 장비 자원예약' },
{ title: '주소록·조직도', desc: '주소록 · 조직도 관리' },
{ title: '문서함', desc: '버전관리 문서함' },
{ title: '쪽지', desc: '내부 메시지 쪽지' },
{ title: 'AI 문서 요약', desc: 'AI 문서 자동 요약' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8009 접속 → admin / admin123 로그인' },
{ num: 2, title: '전자결재', desc: '[기안] → 양식 선택 → 내용 작성 → 결재선 설정 → 상신' },
{ num: 3, title: '일정', desc: '[일정등록] → 제목·날짜·참석자 입력 → 팀 공유' },
{ num: 4, title: '근태', desc: '[휴가신청] → 날짜·종류 선택 → 결재 요청' },
{ num: 5, title: '회의실', desc: '[예약] → 날짜·시간·회의실 선택 → 확정' },
],
mobileScreens: [
{ name: '결재함', type: 'admin' },
{ name: '공지사항', type: 'customer' },
{ name: '일정', type: 'customer' },
{ name: '근태', type: 'customer' },
{ name: '주소록', type: 'customer' },
],
},
{
id: 'portal',
slug: 'portal',
name: 'GUARDiA Portal',
tagline: 'SSO 단일 로그인 통합 업무 포털',
category: '협업',
port: 8010,
color: '#7c3aed',
status: 'live',
access: { url: url(8010), id: 'admin', pw: 'admin123' },
features: [
{ title: 'SSO 단일 로그인', desc: 'GUARDiA 전 시스템 SSO 바로가기' },
{ title: '위젯 대시보드', desc: '역할별 위젯 대시보드' },
{ title: '즐겨찾기', desc: '즐겨찾기 시스템' },
{ title: '통합 공지', desc: '통합 공지사항' },
{ title: '알림 센터', desc: '전 시스템 통합 알림 센터' },
{ title: '통합 검색', desc: '전 시스템 통합 검색' },
{ title: '마이페이지', desc: '프로필 · 설정 마이페이지' },
{ title: 'AI 공지 요약', desc: 'AI 공지 자동 요약' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8010 접속 → admin / admin123 로그인' },
{ num: 2, title: '포털 메인', desc: '9개 시스템 바로가기 카드 확인' },
{ num: 3, title: '위젯', desc: '[위젯 추가] → SR현황·KPI·일정 선택 → 대시보드 배치' },
{ num: 4, title: '통합검색', desc: '검색창에 "장애 처리" 입력 → ITSM·KB·문서 통합 결과 확인' },
{ num: 5, title: '알림센터', desc: '전 시스템 알림 통합 확인' },
],
mobileScreens: [
{ name: '포털 홈', type: 'customer' },
{ name: '알림', type: 'customer' },
{ name: '바로가기', type: 'customer' },
{ name: '통합검색', type: 'customer' },
{ name: '마이페이지', type: 'customer' },
],
},
{
id: 'mall',
slug: 'mall',
name: 'GUARDiA Mall',
tagline: '미국 다지점 꽃집 옴니채널 e-커머스',
category: '커머스',
port: 8011,
color: '#f43f5e',
status: 'live',
access: { url: url(8011), id: 'admin', pw: 'admin123' },
features: [
{ title: 'ZIP 배송권역', desc: '다매장 ZIP 배송권역 관리' },
{ title: '타임슬롯', desc: '타임슬롯 스케줄링 (당일배송)' },
{ title: '구독 서비스', desc: '정기 구독 서비스' },
{ title: '게이트웨이 어댑터', desc: 'Stripe · Twilio mock 게이트웨이 어댑터' },
{ title: 'AI 추천·예측', desc: 'AI 상품 추천 · 수요예측' },
{ title: '매장간 재고 이양', desc: '매장간 재고 이양' },
{ title: '관리자 앱', desc: '매장 매니저용 관리자 앱' },
{ title: 'i18n 한/영', desc: '한국어/영어 다국어 지원' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8011 접속 → admin / admin123 로그인' },
{ num: 2, title: '상품 확인', desc: '상품 목록 확인 (Montvale Florist 실제 상품)' },
{ num: 3, title: '주문', desc: '상품 선택 → ZIP 코드 입력 → 배송 슬롯 선택 → 장바구니 추가' },
{ num: 4, title: '결제', desc: '카드 정보 (mock: 4242-4242-4242-4242) → 주문 완료' },
{ num: 5, title: '관리자', desc: '[주문관리] → 주문 확인 → 상태 변경 (준비중→배송중→완료)' },
],
mobileScreens: [
{ name: '상품 목록', type: 'customer' },
{ name: '상품 상세', type: 'customer' },
{ name: '장바구니', type: 'customer' },
{ name: '결제', type: 'customer' },
{ name: '주문 칸반', type: 'admin' },
{ name: '재고 ON/OFF', type: 'admin' },
],
},
{
id: 'cms',
slug: 'cms',
name: 'GUARDiA CMS',
tagline: 'AI 기반 통합 콘텐츠 관리 시스템',
category: '커머스',
port: 8012,
color: '#0ea5e9',
status: 'live',
access: { url: url(8012), id: 'admin', pw: 'admin123' },
features: [
{ title: '헤드리스 CMS', desc: '페이지 · 포스트 · 블록 헤드리스 CMS' },
{ title: '게시 워크플로우', desc: '버전관리 · 게시 워크플로우 (draft→published)' },
{ title: '미디어 라이브러리', desc: '미디어 라이브러리 관리' },
{ title: '배너·프로모션', desc: '배너 · 프로모션 관리' },
{ title: 'AI SEO 제안', desc: 'AI 기반 SEO 자동 제안' },
{ title: '다국어·테마', desc: '다국어 · 테마 관리' },
{ title: 'UGC 모더레이션', desc: 'UGC 콘텐츠 모더레이션' },
{ title: '폼 빌더', desc: '드래그앤드롭 폼 빌더' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8012 접속 → admin / admin123 로그인' },
{ num: 2, title: '페이지', desc: '[새 페이지] → 제목 입력 → 블록 에디터에서 콘텐츠 추가' },
{ num: 3, title: '미디어', desc: '[업로드] → 이미지 선택 → AI 자동 태그 확인' },
{ num: 4, title: '워크플로우', desc: '페이지 상태를 draft → review → published 변경' },
{ num: 5, title: 'SEO', desc: '페이지 선택 → [AI SEO 제안] → 메타태그 자동 입력 확인' },
],
mobileScreens: [
{ name: '콘텐츠 목록', type: 'admin' },
{ name: '콘텐츠 승인', type: 'admin' },
{ name: '미디어 촬영업로드', type: 'field' },
{ name: 'UGC 모더레이션', type: 'admin' },
{ name: '성과', type: 'admin' },
],
},
{
id: 'mes',
slug: 'mes',
name: 'GUARDiA MES',
tagline: 'AI 기반 제조실행시스템 (WMS+MES+QMS)',
category: '제조',
port: 8013,
color: '#84cc16',
status: 'live',
access: { url: url(8013), id: 'admin', pw: 'admin123' },
features: [
{ title: '작업지시·실적', desc: '작업지시 · 생산실적 등록' },
{ title: 'BOM·라우팅', desc: 'BOM · 공정/라우팅 관리' },
{ title: '설비 OEE', desc: '설비 OEE 모니터링' },
{ title: 'LOT 추적', desc: 'LOT 추적' },
{ title: '창고관리', desc: '입고·출고·재고·피킹 창고관리' },
{ title: '검사', desc: '수입·공정·출하 검사' },
{ title: 'SPC 관리도', desc: 'SPC 관리도 (Cp/Cpk)' },
{ title: 'CAPA', desc: 'CAPA (시정조치)' },
{ title: 'AI 불량분석', desc: 'AI 불량원인 분석' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8013 접속 → admin / admin123 로그인' },
{ num: 2, title: '작업지시', desc: '[신규] → 품목·수량·공정 입력 → 발행' },
{ num: 3, title: '생산실적', desc: '작업지시 선택 → 생산량·불량량 입력 → 등록' },
{ num: 4, title: '검사', desc: '[공정검사] → LOT 번호 입력 → 검사항목 체크 → 합격/불합격' },
{ num: 5, title: 'SPC', desc: '관리도 차트 확인 → Cp/Cpk 수치 확인 → 이상 점 알림' },
],
mobileScreens: [
{ name: '작업지시 조회', type: 'field' },
{ name: '실적입력', type: 'field' },
{ name: '바코드스캔', type: 'field' },
{ name: '검사입력', type: 'field' },
{ name: '설비점검', type: 'field' },
],
},
{
id: 'hrm',
slug: 'hrm',
name: 'GUARDiA HRM',
tagline: 'AI 기반 인사관리 플랫폼',
category: '운영관리',
port: 8014,
color: '#a855f7',
status: 'live',
access: { url: url(8014), id: 'admin', pw: 'admin123' },
features: [
{ title: '사원 관리', desc: '사원 등록 · 이력관리' },
{ title: '조직도·직급', desc: '조직도 · 직급체계' },
{ title: '급여·명세서', desc: '급여 계산 · 명세서' },
{ title: '근태·휴가', desc: '근태 · 휴가 관리' },
{ title: '성과 평가', desc: '성과 평가 · KPI' },
{ title: '채용 관리', desc: '채용 공고 · 지원자 관리' },
{ title: '교육 관리', desc: '교육 이수 관리' },
{ title: 'AI 인사 분석', desc: 'AI 인사 분석' },
{ title: 'Groupware 연동', desc: 'Groupware 연동' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8014 접속 → admin / admin123 로그인' },
{ num: 2, title: '사원', desc: '[신규 등록] → 기본정보·부서·직급 입력 → 저장' },
{ num: 3, title: '급여', desc: '사원 선택 → 월급여 입력 → [명세서 생성] → PDF 확인' },
{ num: 4, title: '성과', desc: '[평가 등록] → KPI 항목 입력 → 점수 부여' },
{ num: 5, title: 'AI', desc: '[인사 인사이트] → 이탈 위험도·성과 트렌드 확인' },
],
mobileScreens: [
{ name: '사원증', type: 'customer' },
{ name: '근태 체크인', type: 'customer' },
{ name: '급여 명세서', type: 'customer' },
{ name: '휴가신청', type: 'customer' },
{ name: '성과현황', type: 'customer' },
],
},
{
id: 'esn',
slug: 'esn',
name: 'GUARDiA ESN',
tagline: 'ESL 전자가격표 통합 관리 플랫폼',
category: '커머스',
port: 8015,
color: '#f59e0b',
status: 'live',
access: { url: url(8015), id: 'admin', pw: 'admin123' },
features: [
{ title: '멀티테넌트', desc: 'LGInnotek · LGIT · Emart · ZIOINFO 멀티테넌트' },
{ title: 'ESL 기기 관리', desc: 'ESL 기기 등록 · 상태 모니터링' },
{ title: 'POS 자동 변환', desc: 'POS 데이터 → ESL 가격 자동 변환' },
{ title: '템플릿 관리', desc: '크기별 레이아웃 템플릿 관리' },
{ title: 'HCore 관제', desc: 'HCore 게이트웨이/허브 관제' },
{ title: '알람·장애 이력', desc: '알람 · 장애 이력 관리' },
{ title: '펌웨어 업데이트', desc: '펌웨어 업데이트 관리' },
{ title: 'AI 알람 분석', desc: 'Ollama AI 알람 원인 분석' },
],
steps: [
{ num: 1, title: '접속·로그인', desc: 'http://101.79.17.164:8015 접속 → admin / admin123 로그인' },
{ num: 2, title: '테넌트 선택', desc: '드롭다운에서 LGINNOTEK 선택' },
{ num: 3, title: 'ESL 기기', desc: '기기 목록 확인 → 상태(온라인/오프라인) 확인' },
{ num: 4, title: 'POS 변환', desc: '[POS 데이터 업로드] → CSV 파일 선택 → 변환 실행' },
{ num: 5, title: '알람', desc: '알람 목록 → [AI 원인분석] 클릭 → Ollama 분석 결과 확인' },
],
mobileScreens: [
{ name: 'ESL 기기 목록', type: 'field' },
{ name: '알람', type: 'field' },
{ name: 'HCore 상태', type: 'field' },
{ name: '펌웨어', type: 'admin' },
{ name: '작업 이력', type: 'field' },
],
},
{
id: 'fa',
slug: 'fa',
name: 'GUARDiA FA',
tagline: 'AI 기반 스마트 팩토리 자동화 플랫폼 (준비중)',
category: '제조',
port: 8017,
color: '#64748b',
status: 'soon',
access: { url: url(8017), id: '', pw: '' },
features: [
{ title: 'e-Paper 지시서', desc: 'e-Paper 전자 공정 지시서' },
{ title: 'QR 작업 추적', desc: 'QR 기반 작업 추적' },
{ title: 'MES 연동', desc: 'MES 연동 실시간 현황' },
{ title: '로봇·PLC 인터페이스', desc: '로봇 · PLC 인터페이스' },
{ title: 'AI 공정 최적화', desc: 'AI 공정 최적화' },
],
steps: [
{ num: 1, title: '배포 예정', desc: 'GUARDiA FA(공장 자동화)는 현재 준비 중입니다.' },
],
mobileScreens: [],
},
];
export const CATEGORIES: { name: string; slugs: string[] }[] = [
{ name: '운영관리', slugs: ['itsm', 'erp', 'crm', 'hrm'] },
{ name: 'AI문서', slugs: ['ocr', 'bi'] },
{ name: '프로젝트', slugs: ['pms', 'rpa'] },
{ name: '협업', slugs: ['groupware', 'portal'] },
{ name: '커머스', slugs: ['mall', 'cms', 'esn'] },
{ name: '제조', slugs: ['mes', 'fa'] },
];
export const getSolution = (slug: string): Solution | undefined =>
SOLUTIONS.find((s) => s.slug === slug);

9
src/main.js Normal file
View File

@ -0,0 +1,9 @@
import { jsx as _jsx } from "react/jsx-runtime";
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles/tokens.css';
import './styles/global.css';
import './styles/app.css';
ReactDOM.createRoot(document.getElementById('root')).render(_jsx(React.StrictMode, { children: _jsx(BrowserRouter, { children: _jsx(App, {}) }) }));

15
src/main.tsx Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles/tokens.css';
import './styles/global.css';
import './styles/app.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

14
src/pages/CatalogPage.js Normal file
View File

@ -0,0 +1,14 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useNavigate } from 'react-router-dom';
import { CATEGORIES, getSolution } from '../data/solutions';
import { SolutionCard } from '../components/SolutionCard';
export const CatalogPage = () => {
const navigate = useNavigate();
return (_jsx("main", { className: "section", children: _jsxs("div", { className: "container", children: [_jsx("h1", { className: "section-title", children: "\uC804\uCCB4 \uC194\uB8E8\uC158" }), _jsx("p", { className: "section-sub", children: "\uCE74\uD14C\uACE0\uB9AC\uBCC4 GUARDiA \uC194\uB8E8\uC158 \u2014 \uCD1D 15\uAC1C" }), CATEGORIES.map((cat) => (_jsxs("section", { className: "catalog-cat", children: [_jsxs("h2", { className: "catalog-cat-title", children: [cat.name, _jsx("span", { className: "catalog-cat-count", children: cat.slugs.length })] }), _jsx("div", { className: "card-grid", children: cat.slugs.map((slug) => {
const sol = getSolution(slug);
if (!sol)
return null;
return (_jsx(SolutionCard, { solution: sol, onClick: () => navigate(`/solution/${slug}`) }, slug));
}) })] }, cat.name)))] }) }));
};
export default CatalogPage;

41
src/pages/CatalogPage.tsx Normal file
View File

@ -0,0 +1,41 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { CATEGORIES, getSolution } from '../data/solutions';
import { SolutionCard } from '../components/SolutionCard';
export const CatalogPage: React.FC = () => {
const navigate = useNavigate();
return (
<main className="section">
<div className="container">
<h1 className="section-title"> </h1>
<p className="section-sub"> GUARDiA 15</p>
{CATEGORIES.map((cat) => (
<section className="catalog-cat" key={cat.name}>
<h2 className="catalog-cat-title">
{cat.name}
<span className="catalog-cat-count">{cat.slugs.length}</span>
</h2>
<div className="card-grid">
{cat.slugs.map((slug) => {
const sol = getSolution(slug);
if (!sol) return null;
return (
<SolutionCard
key={slug}
solution={sol}
onClick={() => navigate(`/solution/${slug}`)}
/>
);
})}
</div>
</section>
))}
</div>
</main>
);
};
export default CatalogPage;

10
src/pages/HomePage.js Normal file
View File

@ -0,0 +1,10 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useNavigate } from 'react-router-dom';
import { SOLUTIONS } from '../data/solutions';
import { SolutionCard } from '../components/SolutionCard';
const liveCount = SOLUTIONS.filter((s) => s.status === 'live').length;
export const HomePage = () => {
const navigate = useNavigate();
return (_jsxs("main", { children: [_jsx("section", { className: "hero", children: _jsxs("div", { className: "container hero-inner", children: [_jsx("span", { className: "hero-eyebrow", children: "\uC9C0\uC624\uC815\uBCF4\uAE30\uC220 \u00B7 GUARDiA Platform" }), _jsxs("h1", { className: "hero-title", children: ["GUARDiA ", _jsx("span", { className: "hero-grad", children: "\uC194\uB8E8\uC158 \uD3EC\uD138" })] }), _jsx("p", { className: "hero-sub", children: "15\uAC1C \uC194\uB8E8\uC158 \uC2DC\uC5F0 \uAC00\uC774\uB4DC \u00B7 \uC811\uC18D \uC815\uBCF4 \u00B7 \uBAA8\uBC14\uC77C \uC571" }), _jsxs("div", { className: "hero-actions", children: [_jsx("button", { className: "btn-primary", onClick: () => navigate('/catalog'), children: "\uC804\uCCB4 \uC194\uB8E8\uC158 \uBCF4\uAE30" }), _jsx("a", { className: "btn-ghost", href: "http://101.79.17.164:9001/app", target: "_blank", rel: "noreferrer", children: "\uBAA8\uBC14\uC77C \uC571 \uB2E4\uC6B4\uB85C\uB4DC" })] })] }) }), _jsx("section", { className: "stats-strip", children: _jsxs("div", { className: "container stats-inner", children: [_jsxs("div", { className: "stat", children: [_jsx("span", { className: "stat-value", children: "15\uAC1C" }), _jsx("span", { className: "stat-label", children: "\uC194\uB8E8\uC158" })] }), _jsxs("div", { className: "stat", children: [_jsxs("span", { className: "stat-value", children: [liveCount, "\uAC1C"] }), _jsx("span", { className: "stat-label", children: "LIVE \uC6B4\uC601 \uC911" })] }), _jsxs("div", { className: "stat", children: [_jsx("span", { className: "stat-value", children: "\uC628\uD504\uB808\uBBF8\uC2A4" }), _jsx("span", { className: "stat-label", children: "AI (Ollama)" })] })] }) }), _jsx("section", { className: "section", children: _jsxs("div", { className: "container", children: [_jsx("h2", { className: "section-title", children: "\uC194\uB8E8\uC158 \uCE74\uD0C8\uB85C\uADF8" }), _jsx("p", { className: "section-sub", children: "\uCE74\uB4DC\uB97C \uD074\uB9AD\uD558\uBA74 \uC811\uC18D \uC815\uBCF4\uC640 \uB2E8\uACC4\uBCC4 \uC2DC\uC5F0 \uAC00\uC774\uB4DC\uB97C \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." }), _jsx("div", { className: "card-grid", children: SOLUTIONS.map((s) => (_jsx(SolutionCard, { solution: s, onClick: () => navigate(`/solution/${s.slug}`) }, s.id))) })] }) }), _jsx("footer", { className: "home-footer", children: _jsxs("div", { className: "container", children: [_jsxs("p", { children: ["\uC11C\uBC84 IP: ", _jsx("code", { children: "101.79.17.164" })] }), _jsxs("p", { children: ["\uBAA8\uBC14\uC77C \uC571 \uB2E4\uC6B4\uB85C\uB4DC:", ' ', _jsx("a", { href: "http://101.79.17.164:9001/app", target: "_blank", rel: "noreferrer", children: "http://101.79.17.164:9001/app" })] }), _jsx("p", { className: "home-footer-copy", children: "\u00A9 2026 \uC9C0\uC624\uC815\uBCF4\uAE30\uC220 GUARDiA Platform" })] }) })] }));
};
export default HomePage;

82
src/pages/HomePage.tsx Normal file
View File

@ -0,0 +1,82 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { SOLUTIONS } from '../data/solutions';
import { SolutionCard } from '../components/SolutionCard';
const liveCount = SOLUTIONS.filter((s) => s.status === 'live').length;
export const HomePage: React.FC = () => {
const navigate = useNavigate();
return (
<main>
{/* Hero */}
<section className="hero">
<div className="container hero-inner">
<span className="hero-eyebrow"> · GUARDiA Platform</span>
<h1 className="hero-title">
GUARDiA <span className="hero-grad"> </span>
</h1>
<p className="hero-sub">
15 · ·
</p>
<div className="hero-actions">
<button className="btn-primary" onClick={() => navigate('/catalog')}>
</button>
<a className="btn-ghost" href="http://101.79.17.164:9001/app" target="_blank" rel="noreferrer">
</a>
</div>
</div>
</section>
{/* Stats */}
<section className="stats-strip">
<div className="container stats-inner">
<div className="stat">
<span className="stat-value">15</span>
<span className="stat-label"></span>
</div>
<div className="stat">
<span className="stat-value">{liveCount}</span>
<span className="stat-label">LIVE </span>
</div>
<div className="stat">
<span className="stat-value"></span>
<span className="stat-label">AI (Ollama)</span>
</div>
</div>
</section>
{/* Grid */}
<section className="section">
<div className="container">
<h2 className="section-title"> </h2>
<p className="section-sub"> .</p>
<div className="card-grid">
{SOLUTIONS.map((s) => (
<SolutionCard key={s.id} solution={s} onClick={() => navigate(`/solution/${s.slug}`)} />
))}
</div>
</div>
</section>
{/* Footer info */}
<footer className="home-footer">
<div className="container">
<p> IP: <code>101.79.17.164</code></p>
<p>
:{' '}
<a href="http://101.79.17.164:9001/app" target="_blank" rel="noreferrer">
http://101.79.17.164:9001/app
</a>
</p>
<p className="home-footer-copy">© 2026 GUARDiA Platform</p>
</div>
</footer>
</main>
);
};
export default HomePage;

View File

@ -0,0 +1,19 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useParams, Link } from 'react-router-dom';
import { getSolution } from '../data/solutions';
import { SolutionIcon } from '../components/SolutionIcon';
import { LoginBox } from '../components/LoginBox';
import { FeatureGrid } from '../components/FeatureGrid';
import { StepGuide } from '../components/StepGuide';
import { MobileScreenList } from '../components/MobileScreenList';
export const SolutionDetailPage = () => {
const { slug } = useParams();
const solution = slug ? getSolution(slug) : undefined;
if (!solution) {
return (_jsx("main", { className: "section", children: _jsxs("div", { className: "container", children: [_jsx("h1", { className: "section-title", children: "\uC194\uB8E8\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4" }), _jsx(Link, { to: "/catalog", className: "btn-primary", style: { marginTop: 16, display: 'inline-block' }, children: "\uC804\uCCB4 \uC194\uB8E8\uC158\uC73C\uB85C" })] }) }));
}
const { name, tagline, color, status, category, access, features, steps, mobileScreens } = solution;
const soon = status === 'soon';
return (_jsxs("main", { className: "detail", children: [_jsx("section", { className: "detail-header", style: { background: `radial-gradient(900px 360px at 18% -20%, ${color}33, transparent 70%)` }, children: _jsxs("div", { className: "container", children: [_jsx(Link, { to: "/catalog", className: "detail-back", children: "\u2190 \uC804\uCCB4 \uC194\uB8E8\uC158" }), _jsxs("div", { className: "detail-header-row", children: [_jsx("span", { className: "detail-icon", style: { color, background: `${color}1a`, borderColor: `${color}40` }, children: _jsx(SolutionIcon, { slug: solution.slug, size: 44, color: color }) }), _jsxs("div", { children: [_jsxs("div", { className: "detail-title-row", children: [_jsx("h1", { className: "detail-title", children: name }), status === 'live' ? (_jsx("span", { className: "badge badge-live", children: "LIVE" })) : (_jsx("span", { className: "badge badge-soon", children: "SOON" }))] }), _jsx("p", { className: "detail-tagline", children: tagline }), _jsx("span", { className: "detail-cat", children: category })] })] })] }) }), _jsxs("div", { className: "container detail-body", children: [_jsx("section", { className: "detail-section", children: _jsx(LoginBox, { access: access, color: color, soon: soon }) }), _jsxs("section", { className: "detail-section", children: [_jsx("h2", { className: "section-title", children: "\uC8FC\uC694 \uAE30\uB2A5" }), _jsxs("p", { className: "section-sub", children: [name, "\uC758 \uD575\uC2EC \uAE30\uB2A5\uC785\uB2C8\uB2E4."] }), _jsx(FeatureGrid, { features: features, color: color })] }), _jsxs("section", { className: "detail-section", children: [_jsx("h2", { className: "section-title", children: "\uB2E8\uACC4\uBCC4 \uC2DC\uC5F0 \uAC00\uC774\uB4DC" }), _jsx("p", { className: "section-sub", children: "\uC544\uB798 \uC21C\uC11C\uB300\uB85C \uB530\uB77C \uD558\uBA74 \uC8FC\uC694 \uAE30\uB2A5\uC744 \uD655\uC778\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." }), _jsx(StepGuide, { steps: steps, color: color })] }), _jsxs("section", { className: "detail-section", children: [_jsx("h2", { className: "section-title", children: "\uBAA8\uBC14\uC77C \uC571 \uD654\uBA74" }), _jsx("p", { className: "section-sub", children: "\uD604\uC7A5\u00B7\uAD00\uB9AC\uC790\u00B7\uC0AC\uC6A9\uC790\uC6A9 \uBAA8\uBC14\uC77C \uD654\uBA74 \uAD6C\uC131\uC785\uB2C8\uB2E4." }), _jsx(MobileScreenList, { screens: mobileScreens, color: color })] })] })] }));
};
export default SolutionDetailPage;

View File

@ -0,0 +1,93 @@
import React from 'react';
import { useParams, Link } from 'react-router-dom';
import { getSolution } from '../data/solutions';
import { SolutionIcon } from '../components/SolutionIcon';
import { LoginBox } from '../components/LoginBox';
import { FeatureGrid } from '../components/FeatureGrid';
import { StepGuide } from '../components/StepGuide';
import { MobileScreenList } from '../components/MobileScreenList';
export const SolutionDetailPage: React.FC = () => {
const { slug } = useParams<{ slug: string }>();
const solution = slug ? getSolution(slug) : undefined;
if (!solution) {
return (
<main className="section">
<div className="container">
<h1 className="section-title"> </h1>
<Link to="/catalog" className="btn-primary" style={{ marginTop: 16, display: 'inline-block' }}>
</Link>
</div>
</main>
);
}
const { name, tagline, color, status, category, access, features, steps, mobileScreens } = solution;
const soon = status === 'soon';
return (
<main className="detail">
{/* Header */}
<section
className="detail-header"
style={{ background: `radial-gradient(900px 360px at 18% -20%, ${color}33, transparent 70%)` }}
>
<div className="container">
<Link to="/catalog" className="detail-back"> </Link>
<div className="detail-header-row">
<span
className="detail-icon"
style={{ color, background: `${color}1a`, borderColor: `${color}40` }}
>
<SolutionIcon slug={solution.slug} size={44} color={color} />
</span>
<div>
<div className="detail-title-row">
<h1 className="detail-title">{name}</h1>
{status === 'live' ? (
<span className="badge badge-live">LIVE</span>
) : (
<span className="badge badge-soon">SOON</span>
)}
</div>
<p className="detail-tagline">{tagline}</p>
<span className="detail-cat">{category}</span>
</div>
</div>
</div>
</section>
<div className="container detail-body">
{/* Login */}
<section className="detail-section">
<LoginBox access={access} color={color} soon={soon} />
</section>
{/* Features */}
<section className="detail-section">
<h2 className="section-title"> </h2>
<p className="section-sub">{name} .</p>
<FeatureGrid features={features} color={color} />
</section>
{/* Steps */}
<section className="detail-section">
<h2 className="section-title"> </h2>
<p className="section-sub"> .</p>
<StepGuide steps={steps} color={color} />
</section>
{/* Mobile */}
<section className="detail-section">
<h2 className="section-title"> </h2>
<p className="section-sub">·· .</p>
<MobileScreenList screens={mobileScreens} color={color} />
</section>
</div>
</main>
);
};
export default SolutionDetailPage;

217
src/styles/app.css Normal file
View File

@ -0,0 +1,217 @@
/* ============ Navbar ============ */
.navbar {
position: sticky; top: 0; z-index: 50;
background: rgba(6, 11, 24, 0.78);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--border);
}
.navbar-inner { display: flex; align-items: center; justify-content: space-between; height: 64px; }
.navbar-logo { display: flex; align-items: center; gap: 10px; }
.navbar-logo-mark {
width: 32px; height: 32px; border-radius: 9px;
background: var(--accent-grad);
display: grid; place-items: center;
font-weight: 800; font-size: 18px; color: #fff;
}
.navbar-logo-text { font-weight: 700; font-size: 16px; letter-spacing: -0.2px; }
.navbar-nav { display: flex; align-items: center; gap: 20px; }
.navbar-link { font-size: 14px; color: var(--text-secondary); transition: color var(--transition); }
.navbar-link:hover { color: var(--text-primary); }
.navbar-cta {
padding: 8px 16px; border-radius: var(--radius-sm);
background: var(--accent-grad); color: #fff; font-size: 13px; font-weight: 600;
transition: opacity var(--transition);
}
.navbar-cta:hover { opacity: 0.88; }
/* ============ Hero ============ */
.hero {
position: relative; overflow: hidden;
padding: 110px 0 90px;
background:
radial-gradient(800px 380px at 50% -10%, rgba(99,102,241,0.25), transparent 70%),
radial-gradient(600px 300px at 85% 10%, rgba(139,92,246,0.18), transparent 70%);
}
.hero-inner { text-align: center; }
.hero-eyebrow {
display: inline-block; font-size: 13px; letter-spacing: 1px; text-transform: uppercase;
color: var(--accent-2); font-weight: 600; margin-bottom: 18px;
}
.hero-title { font-size: 56px; font-weight: 800; line-height: 1.1; letter-spacing: -1.5px; }
.hero-grad {
background: var(--accent-grad);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero-sub { margin-top: 18px; font-size: 18px; color: var(--text-secondary); }
.hero-actions { margin-top: 36px; display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
.btn-primary {
padding: 13px 26px; border: none; cursor: pointer;
border-radius: var(--radius-sm); background: var(--accent-grad);
color: #fff; font-size: 15px; font-weight: 600;
transition: transform var(--transition), box-shadow var(--transition);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--shadow-glow); }
.btn-ghost {
padding: 13px 26px; cursor: pointer;
border-radius: var(--radius-sm);
border: 1px solid var(--border); background: var(--bg-card);
color: var(--text-primary); font-size: 15px; font-weight: 600;
transition: background var(--transition);
}
.btn-ghost:hover { background: var(--bg-card-hover); }
/* ============ Stats ============ */
.stats-strip { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); background: var(--bg-surface); }
.stats-inner { display: flex; justify-content: center; gap: 80px; padding: 36px 0; flex-wrap: wrap; }
.stat { text-align: center; }
.stat-value {
display: block; font-size: 30px; font-weight: 800;
background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent;
}
.stat-label { font-size: 13px; color: var(--text-muted); }
/* ============ Card Grid ============ */
.card-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;
}
@media (max-width: 980px) { .card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .card-grid { grid-template-columns: 1fr; } }
.solution-card {
text-align: left; cursor: pointer; width: 100%;
display: flex; flex-direction: column; gap: 12px;
padding: 22px; border-radius: var(--radius-card);
background: var(--bg-card); border: 1px solid var(--border);
color: inherit; font-family: inherit;
transition: transform var(--transition), border-color var(--transition), box-shadow var(--transition);
}
.solution-card:hover {
transform: translateY(-4px);
border-color: var(--card-color);
box-shadow: 0 0 36px color-mix(in srgb, var(--card-color) 22%, transparent);
}
.solution-card-top { display: flex; align-items: center; justify-content: space-between; }
.solution-card-icon {
width: 52px; height: 52px; border-radius: 14px;
display: grid; place-items: center; border: 1px solid;
}
.solution-card-name { font-size: 18px; font-weight: 700; }
.solution-card-tagline { font-size: 13.5px; color: var(--text-muted); line-height: 1.5; flex: 1; }
.solution-card-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }
.solution-card-port { font-family: var(--font-mono); font-size: 13px; color: var(--text-muted); }
.solution-card-more { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; font-weight: 600; }
/* ============ Catalog ============ */
.catalog-cat { margin-bottom: 48px; }
.catalog-cat-title {
display: flex; align-items: center; gap: 10px;
font-size: 20px; font-weight: 700; margin-bottom: 20px;
}
.catalog-cat-count {
font-size: 12px; font-weight: 700; color: var(--accent-2);
background: rgba(139,92,246,0.15); border: 1px solid rgba(139,92,246,0.3);
border-radius: 20px; padding: 2px 10px;
}
/* ============ Detail ============ */
.detail-header { padding: 36px 0 44px; border-bottom: 1px solid var(--border); }
.detail-back { display: inline-block; font-size: 14px; color: var(--text-muted); margin-bottom: 24px; transition: color var(--transition); }
.detail-back:hover { color: var(--text-primary); }
.detail-header-row { display: flex; align-items: center; gap: 22px; }
.detail-icon { width: 80px; height: 80px; border-radius: 20px; display: grid; place-items: center; border: 1px solid; flex-shrink: 0; }
.detail-title-row { display: flex; align-items: center; gap: 12px; }
.detail-title { font-size: 34px; font-weight: 800; letter-spacing: -0.8px; }
.detail-tagline { margin-top: 6px; font-size: 16px; color: var(--text-secondary); }
.detail-cat {
display: inline-block; margin-top: 12px; font-size: 12px; font-weight: 600;
color: var(--text-muted); border: 1px solid var(--border); border-radius: 20px; padding: 3px 12px;
}
.detail-body { padding: 48px 0 90px; }
.detail-section { margin-bottom: 56px; }
/* ============ Login Box ============ */
.login-box {
background: rgba(0, 0, 0, 0.4);
border: 1px solid var(--border);
border-radius: var(--radius-card);
padding: 24px;
max-width: 560px;
}
.login-box-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.login-box-title { font-size: 14px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-muted); }
.login-field {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px; border-radius: var(--radius-sm);
background: rgba(255,255,255,0.03); margin-bottom: 10px;
}
.login-field-label { width: 52px; font-size: 12px; font-weight: 700; color: var(--text-muted); flex-shrink: 0; }
.login-field-value { flex: 1; font-family: var(--font-mono); font-size: 16px; color: var(--text-primary); letter-spacing: 0.5px; }
.login-url {
flex: 1; display: inline-flex; align-items: center; gap: 6px;
font-family: var(--font-mono); font-size: 14px; color: #60a5fa; word-break: break-all;
}
.login-url:hover { text-decoration: underline; }
.login-copy-btn {
display: inline-flex; align-items: center; gap: 4px;
background: none; border: none; cursor: pointer; padding: 4px 6px; border-radius: 6px;
transition: background var(--transition);
}
.login-copy-btn:hover { background: rgba(255,255,255,0.08); }
.login-copied { font-size: 12px; font-weight: 700; color: #10b981; }
.login-soon-text { font-size: 14px; color: var(--text-muted); line-height: 1.6; }
/* ============ Feature Grid ============ */
.feature-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
@media (max-width: 640px) { .feature-grid { grid-template-columns: 1fr; } }
.feature-card {
display: flex; gap: 12px; align-items: flex-start;
padding: 16px 18px; border-radius: var(--radius-md);
background: var(--bg-card); border: 1px solid var(--border);
}
.feature-check { flex-shrink: 0; margin-top: 2px; display: inline-flex; }
.feature-title { font-size: 15px; font-weight: 600; margin-bottom: 3px; }
.feature-desc { font-size: 13.5px; color: var(--text-muted); line-height: 1.5; }
/* ============ Step Guide ============ */
.step-guide { list-style: none; }
.step-guide-item { display: grid; grid-template-columns: 44px 1fr; gap: 18px; }
.step-guide-rail { display: flex; flex-direction: column; align-items: center; }
.step-guide-num {
width: 40px; height: 40px; border-radius: 50%;
display: grid; place-items: center; font-size: 15px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.step-guide-line { flex: 1; width: 2px; background: var(--border); margin: 6px 0; min-height: 16px; }
.step-guide-body { padding-bottom: 24px; }
.step-guide-body h4 { font-size: 16px; font-weight: 600; margin-bottom: 5px; }
.step-guide-body p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
/* ============ Mobile Screens ============ */
.mobile-screen-list { display: flex; gap: 24px; flex-wrap: wrap; }
.mobile-empty { font-size: 14px; color: var(--text-muted); }
.phone-card { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.phone-frame { width: 150px; height: 300px; border-radius: 26px; }
.phone-notch { width: 64px; height: 18px; }
.phone-screen-inner {
width: 100%; height: 100%; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px; padding: 16px;
}
.phone-icon { display: inline-flex; }
.phone-screen-name { font-size: 13px; font-weight: 600; color: var(--text-secondary); text-align: center; }
.phone-label { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.phone-name { font-size: 13px; font-weight: 600; }
.phone-type { font-size: 11px; font-weight: 600; border: 1px solid; border-radius: 20px; padding: 1px 8px; }
/* ============ Home Footer ============ */
.home-footer { border-top: 1px solid var(--border); padding: 40px 0; background: var(--bg-surface); }
.home-footer p { font-size: 14px; color: var(--text-secondary); margin-bottom: 6px; }
.home-footer code { font-family: var(--font-mono); color: var(--accent-2); }
.home-footer a { color: #60a5fa; }
.home-footer a:hover { text-decoration: underline; }
.home-footer-copy { margin-top: 16px; font-size: 13px; color: var(--text-muted); }
@media (max-width: 640px) {
.hero-title { font-size: 38px; }
.stats-inner { gap: 40px; }
.detail-header-row { flex-direction: column; align-items: flex-start; }
}

82
src/styles/global.css Normal file
View File

@ -0,0 +1,82 @@
@import './tokens.css';
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font-sans);
background: var(--bg-base);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg-base); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* 유틸 */
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.badge {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600;
letter-spacing: 0.5px; text-transform: uppercase;
}
.badge-live { background: rgba(16,185,129,0.15); color: #10b981; border: 1px solid rgba(16,185,129,0.3); }
.badge-soon { background: rgba(251,191,36,0.15); color: #fbbf24; border: 1px solid rgba(251,191,36,0.3); }
.badge-mobile { background: rgba(99,102,241,0.15); color: #818cf8; border: 1px solid rgba(99,102,241,0.3); }
/* 공통 섹션 */
.section { padding: 80px 0; }
.section-title { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
.section-sub { color: var(--text-muted); font-size: 15px; margin-bottom: 40px; }
/* 코드 블록 */
.code-block {
background: rgba(0,0,0,0.5);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px 20px;
font-family: var(--font-mono);
font-size: 13px;
color: #a5f3fc;
overflow-x: auto;
line-height: 1.7;
}
.code-block .comment { color: #475569; }
.code-block .cmd { color: #34d399; }
.code-block .url { color: #f472b6; }
/* 스텝 */
.step-list { list-style: none; display: flex; flex-direction: column; gap: 16px; }
.step-item {
display: grid; grid-template-columns: 40px 1fr; gap: 16px; align-items: start;
}
.step-num {
width: 40px; height: 40px; border-radius: 50%;
background: var(--accent-grad); display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; flex-shrink: 0;
}
.step-body h4 { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
.step-body p { font-size: 14px; color: var(--text-muted); }
/* 모바일 프레임 */
.phone-frame {
width: 240px; height: 480px;
border-radius: 32px;
border: 2px solid var(--border);
background: #0f172a;
overflow: hidden;
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.phone-notch {
position: absolute; top: 0; left: 50%; transform: translateX(-50%);
width: 100px; height: 24px; background: #000; border-radius: 0 0 16px 16px; z-index: 2;
}
.phone-screen { width: 100%; height: 100%; overflow: hidden; }

29
src/styles/tokens.css Normal file
View File

@ -0,0 +1,29 @@
:root {
--bg-base: #060b18;
--bg-surface: #0d1525;
--bg-card: rgba(255, 255, 255, 0.04);
--bg-card-hover: rgba(255, 255, 255, 0.08);
--border: rgba(255, 255, 255, 0.10);
--border-accent: rgba(99, 102, 241, 0.40);
--accent-1: #6366f1;
--accent-2: #8b5cf6;
--accent-grad: linear-gradient(135deg, #6366f1, #8b5cf6);
--text-primary: #f1f5f9;
--text-secondary:#cbd5e1;
--text-muted: #64748b;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-card:16px;
--shadow-card: 0 4px 32px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
--font-sans: 'Pretendard', 'Apple SD Gothic Neo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--transition: 0.2s ease;
}

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true
},
"include": ["src"]
}

8
vite.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: { outDir: 'dist' },
server: { port: 3020 }
})