feat: 솔루션 포털 홈페이지 이식 + 솔루션 소개 최신화 + 통합 메신저앱 QR 페이지 추가

This commit is contained in:
GUARDiA 2026-06-21 10:27:52 +09:00
parent 6d2d6840d9
commit 0c5436291c
3 changed files with 133 additions and 1 deletions

View File

@ -38,6 +38,7 @@ import AiTools from './pages/AiTools'
import UserManagement from './pages/UserManagement' import UserManagement from './pages/UserManagement'
import AuditLog from './pages/AuditLog' import AuditLog from './pages/AuditLog'
import SystemSettings from './pages/SystemSettings' import SystemSettings from './pages/SystemSettings'
import MobileApp from './pages/MobileApp'
// 업무 (UIWS 이식) — 업무일지/일정/쪽지/업무통계 // 업무 (UIWS 이식) — 업무일지/일정/쪽지/업무통계
import UiwsWorklog from './pages/uiws/WorklogList' import UiwsWorklog from './pages/uiws/WorklogList'
import UiwsSchedule from './pages/uiws/ScheduleCalendar' import UiwsSchedule from './pages/uiws/ScheduleCalendar'
@ -102,6 +103,7 @@ export default function App() {
<Route path="/admin/settings" element={ <Route path="/admin/settings" element={
<ProtectedRoute min="MANAGER"><SystemSettings /></ProtectedRoute> <ProtectedRoute min="MANAGER"><SystemSettings /></ProtectedRoute>
} /> } />
<Route path="/admin/mobile-app" element={<MobileApp />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} /> <Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes> </Routes>

View File

@ -5,7 +5,7 @@ import {
Gauge, Network, CalendarRange, PackagePlus, Truck, Boxes, ClipboardCheck, Barcode, Gauge, Network, CalendarRange, PackagePlus, Truck, Boxes, ClipboardCheck, Barcode,
ScanSearch, Ruler, AlertTriangle, ListChecks, LineChart, FileBadge, Package, ListTree, ScanSearch, Ruler, AlertTriangle, ListChecks, LineChart, FileBadge, Package, ListTree,
Route as RouteIcon, Building2, Warehouse, BarChart3, Sparkles, ShieldCheck, ScrollText, Route as RouteIcon, Building2, Warehouse, BarChart3, Sparkles, ShieldCheck, ScrollText,
Settings, Cpu, NotebookPen, CalendarDays, Mail, PieChart, Settings, Cpu, NotebookPen, CalendarDays, Mail, PieChart, Smartphone,
} from 'lucide-react' } from 'lucide-react'
import { currentRole, hasRole } from './rbac' import { currentRole, hasRole } from './rbac'
import ThemeToggle from './uiws/ThemeToggle' import ThemeToggle from './uiws/ThemeToggle'
@ -63,6 +63,10 @@ const adminLinks: Link[] = [
{ to: '/admin/audit', label: '감사 로그', icon: ScrollText, min: 'MANAGER' }, { to: '/admin/audit', label: '감사 로그', icon: ScrollText, min: 'MANAGER' },
{ to: '/admin/settings', label: '시스템 설정', icon: Settings, min: 'MANAGER' }, { to: '/admin/settings', label: '시스템 설정', icon: Settings, min: 'MANAGER' },
] ]
// 통합 메신저 앱(읽기전용 QR 설치) — 인증 사용자 전체
const mobileLinks: Link[] = [
{ to: '/admin/mobile-app', label: '모바일 앱 설치', icon: Smartphone },
]
const linkClass = ({ isActive }: { isActive: boolean }) => const linkClass = ({ isActive }: { isActive: boolean }) =>
`flex items-center gap-3 px-5 py-1.5 text-sm transition-colors ${ `flex items-center gap-3 px-5 py-1.5 text-sm transition-colors ${
@ -120,6 +124,9 @@ export default function Sidebar() {
<Section title="기준정보" links={masterLinks} role={role} /> <Section title="기준정보" links={masterLinks} role={role} />
<Section title="공통" links={commonLinks} role={role} /> <Section title="공통" links={commonLinks} role={role} />
<Section title="업무 (UIWS)" links={uiwsLinks} role={role} /> <Section title="업무 (UIWS)" links={uiwsLinks} role={role} />
<div className="border-t border-edge mt-3">
<Section title="모바일" links={mobileLinks} role={role} />
</div>
{visibleAdmin.length > 0 && ( {visibleAdmin.length > 0 && (
<div className="border-t border-edge mt-3"> <div className="border-t border-edge mt-3">
<Section title="관리자" links={visibleAdmin} role={role} /> <Section title="관리자" links={visibleAdmin} role={role} />

View File

@ -0,0 +1,123 @@
import { useEffect, useState } from 'react'
import { Smartphone, Download, Copy, Check, ExternalLink, RefreshCw } from 'lucide-react'
// 중앙 APK 저장소(ITSM)의 공개 엔드포인트 — 인증 불필요·CORS 허용 (읽기전용)
// 업로드·버전 관리는 GUARDiA Manager/ITSM에서 일원화. 본 화면은 표시 전용.
const ITSM_APP_BASE = 'https://zioinfo.co.kr:8443'
interface AppInfo {
has_version: boolean
app_name?: string
version?: string
platform?: string
file_size_mb?: number
release_notes?: string
download_count?: number
qr_url?: string
landing_url?: string
download_url?: string
updated_at?: string
}
export default function MobileApp() {
const [info, setInfo] = useState<AppInfo | null>(null)
const [loading, setLoading] = useState(true)
const [err, setErr] = useState('')
const [copied, setCopied] = useState(false)
const load = () => {
setLoading(true); setErr('')
fetch(`${ITSM_APP_BASE}/api/app/public-latest`)
.then(r => r.json())
.then((d: AppInfo) => setInfo(d))
.catch(() => setErr('버전 정보를 불러올 수 없습니다. (중앙 앱 저장소 연결 실패)'))
.finally(() => setLoading(false))
}
useEffect(() => { load() }, [])
const copyLink = async () => {
if (!info?.landing_url) return
try {
await navigator.clipboard.writeText(info.landing_url)
setCopied(true); setTimeout(() => setCopied(false), 2000)
} catch { /* clipboard 권한 없을 때 무시 */ }
}
return (
<div>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Smartphone className="text-brand" size={22} />
<h1 className="text-xl font-bold">GUARDiA </h1>
</div>
<button onClick={load} className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-card border border-edge text-sm text-slate-300 hover:text-brand">
<RefreshCw size={15} />
</button>
</div>
<p className="text-sm text-slate-400 mb-6">
QR <span className="text-brand font-semibold">GUARDiA Messenger</span> .
, · GUARDiA Manager에서 .
</p>
{loading && <div className="text-slate-400 text-sm"> </div>}
{err && <div className="bg-card border border-edge rounded-xl p-6 text-rose-400 text-sm">{err}</div>}
{!loading && !err && info && !info.has_version && (
<div className="bg-card border border-edge rounded-xl p-8 text-center text-slate-400">
<Smartphone size={36} className="mx-auto mb-3 text-slate-600" />
.<br />
<span className="text-xs">GUARDiA Manager에서 APK를 QR이 .</span>
</div>
)}
{!loading && !err && info?.has_version && (
<div className="bg-card border border-edge rounded-xl p-6 grid md:grid-cols-[200px_1fr] gap-6 items-start">
<div className="bg-white rounded-xl p-3 flex items-center justify-center">
<img src={info.qr_url} alt="앱 설치 QR" className="w-44 h-44" />
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-lg font-bold">{info.app_name || 'GUARDiA Messenger'}</span>
<span className="px-2 py-0.5 rounded-md bg-brand2 text-white text-xs font-semibold">v{info.version}</span>
</div>
<div className="text-xs text-slate-400 mb-4">
{info.platform} · {info.file_size_mb ? `${info.file_size_mb}MB` : ''}
{info.download_count != null && ` · 다운로드 ${info.download_count}`}
</div>
{info.release_notes && (
<div className="mb-4">
<div className="text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1"></div>
<div className="text-sm text-slate-300 whitespace-pre-line bg-ink border border-edge rounded-lg p-3 max-h-32 overflow-auto">
{info.release_notes}
</div>
</div>
)}
<div className="flex flex-wrap gap-2">
<a href={info.landing_url} target="_blank" rel="noreferrer"
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-brand text-ink text-sm font-semibold">
<ExternalLink size={15} />
</a>
<a href={info.download_url} target="_blank" rel="noreferrer"
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-card border border-edge text-sm text-slate-300 hover:text-brand">
<Download size={15} /> APK
</a>
<button onClick={copyLink}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-card border border-edge text-sm text-slate-300 hover:text-brand">
{copied ? <Check size={15} className="text-accent" /> : <Copy size={15} />}
{copied ? '복사됨' : '다운로드 링크 복사'}
</button>
</div>
<div className="mt-4 text-[12px] text-slate-500 leading-relaxed">
📱 Android: APK "알 수 없는 소스" .
.
</div>
</div>
</div>
)}
</div>
)
}