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

This commit is contained in:
GUARDiA 2026-06-21 10:27:22 +09:00
parent b63a01acd1
commit cc6e3b2c2a
3 changed files with 133 additions and 1 deletions

View File

@ -22,6 +22,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 UiwsLayout from './components/uiws/UiwsLayout' import UiwsLayout from './components/uiws/UiwsLayout'
import WorklogList from './pages/uiws/WorklogList' import WorklogList from './pages/uiws/WorklogList'
@ -64,6 +65,7 @@ export default function App() {
<Route path="/admin/settings" element={ <Route path="/admin/settings" element={
<ProtectedRoute min="EDITOR"><SystemSettings /></ProtectedRoute> <ProtectedRoute min="EDITOR"><SystemSettings /></ProtectedRoute>
} /> } />
<Route path="/admin/mobile-app" element={<MobileApp />} />
{/* 업무(UIWS 이식) — 인증 사용자 전체(Viewer+). UiwsLayout = .uiws-scope 다크/라이트 토큰 */} {/* 업무(UIWS 이식) — 인증 사용자 전체(Viewer+). UiwsLayout = .uiws-scope 다크/라이트 토큰 */}
<Route path="/uiws/worklog" element={<UiwsLayout><WorklogList /></UiwsLayout>} /> <Route path="/uiws/worklog" element={<UiwsLayout><WorklogList /></UiwsLayout>} />

View File

@ -4,7 +4,7 @@ import {
LayoutDashboard, FileText, Image, Package, Megaphone, ListTree, Palette, LayoutDashboard, FileText, Image, Package, Megaphone, ListTree, Palette,
Languages, Search, FileInput, MessageSquareWarning, Share2, UsersRound, Languages, Search, FileInput, MessageSquareWarning, Share2, UsersRound,
Sparkles, BarChart3, ShieldCheck, ScrollText, Settings, FileStack, Sparkles, BarChart3, ShieldCheck, ScrollText, Settings, FileStack,
BookOpen, CalendarDays, Mail, BookOpen, CalendarDays, Mail, 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'
@ -44,6 +44,10 @@ const uiwsLinks: Link[] = [
{ to: '/uiws/message', label: '쪽지', icon: Mail }, { to: '/uiws/message', label: '쪽지', icon: Mail },
{ to: '/uiws/stats', label: '업무통계', icon: BarChart3 }, { to: '/uiws/stats', label: '업무통계', icon: BarChart3 },
] ]
// 통합 메신저 앱(읽기전용 QR 설치) — 인증 사용자 전체
const appLinks: 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-2 text-sm transition-colors ${ `flex items-center gap-3 px-5 py-2 text-sm transition-colors ${
@ -98,6 +102,9 @@ export default function Sidebar() {
<div className="border-t border-edge mt-3"> <div className="border-t border-edge mt-3">
<Section title="업무(UIWS)" links={uiwsLinks} /> <Section title="업무(UIWS)" links={uiwsLinks} />
</div> </div>
<div className="border-t border-edge mt-3">
<Section title="모바일" links={appLinks} />
</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} /> <Section title="관리자" links={visibleAdmin} />

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>
)
}