import { ChevronDown, Link2, MoreHorizontal, RotateCcw, Unlink } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui'; import { useToast } from '../../components/toast'; import { PageTitle } from '../../layout/AppLayout'; import { LoadingModal, useMinimumLoading } from '../operations/operation-utils'; import { createLauncherAppLink, deleteLauncherAppLink, listLauncherAppLinks, moveLauncherAppLink, syncLauncherAppsToAdminApps, updateLauncherAppLink, type LauncherAppLink, type LauncherUnlinkedApp } from './launcher-api'; type DisplayForm = { displayName: string; description: string; subtitle: string; detailUrl: string; videoPath: string; iconKey: string; launchUrl: string; devAppPath: string; execPath: string; displayOrder: string; isEnabled: boolean; }; function releaseLabel(link: LauncherAppLink) { return link.release?.version ?? '-'; } function launchUrl(link: LauncherAppLink) { return link.launchUrl || link.execPath || link.devAppPath || link.artifact || '-'; } function toDisplayForm(link: LauncherAppLink): DisplayForm { return { displayName: link.displayName || link.app.name || '', description: link.description || link.app.description || '', subtitle: link.subtitle || '', detailUrl: link.detailUrl || '', videoPath: link.videoPath || '', iconKey: link.iconKey || link.app.key.toLowerCase(), launchUrl: link.launchUrl || '', devAppPath: link.devAppPath || '', execPath: link.execPath || '', displayOrder: String(link.displayOrder || ''), isEnabled: link.isEnabled }; } export function LauncherAppsPage() { const { toast } = useToast(); const [linkedApps, setLinkedApps] = useState([]); const [unlinkedApps, setUnlinkedApps] = useState([]); const [loading, setLoading] = useState(true); const [workingId, setWorkingId] = useState(''); const [syncing, setSyncing] = useState(false); const [unlinking, setUnlinking] = useState(false); const [editingLink, setEditingLink] = useState(null); const [unlinkTarget, setUnlinkTarget] = useState(null); const [displayForm, setDisplayForm] = useState(null); const loadingVisible = useMinimumLoading(loading); const refresh = () => { setLoading(true); listLauncherAppLinks() .then(({ linked, unlinked }) => { setLinkedApps(linked); setUnlinkedApps(unlinked); }) .catch((error: Error) => { toast({ type: 'error', title: '앱 연결 조회 실패', message: error.message }); }) .finally(() => setLoading(false)); }; useEffect(() => { refresh(); }, []); const connect = (app: LauncherUnlinkedApp) => { setWorkingId(app.id); createLauncherAppLink({ appId: app.id, releaseId: app.releases?.[0]?.id }) .then(() => { toast({ type: 'success', message: '런처에 앱을 연결했습니다.' }); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: '앱 연결 실패', message: error.message }); }) .finally(() => setWorkingId('')); }; const move = (link: LauncherAppLink, direction: 'up' | 'down') => { setWorkingId(`${link.id}-${direction}`); moveLauncherAppLink(link.id, direction) .then(({ linked, unlinked }) => { setLinkedApps(linked); setUnlinkedApps(unlinked); }) .catch((error: Error) => { toast({ type: 'error', title: '순서 변경 실패', message: error.message }); }) .finally(() => setWorkingId('')); }; const unlink = () => { if (!unlinkTarget || unlinking) return; setUnlinking(true); setWorkingId(unlinkTarget.id); deleteLauncherAppLink(unlinkTarget.id) .then(() => { toast({ type: 'success', message: '런처 앱 연결을 해제했습니다.' }); setUnlinkTarget(null); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: '연결 해제 실패', message: error.message }); }) .finally(() => { setWorkingId(''); setUnlinking(false); }); }; const syncAdminApps = () => { setSyncing(true); syncLauncherAppsToAdminApps() .then(({ syncedApps, syncedReleases }) => { toast({ type: 'success', message: `앱 ${syncedApps}개, 릴리즈 ${syncedReleases}개를 동기화했습니다.` }); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: '앱 관리 동기화 실패', message: error.message }); }) .finally(() => setSyncing(false)); }; const openSettings = (link: LauncherAppLink) => { setEditingLink(link); setDisplayForm(toDisplayForm(link)); }; const closeSettings = () => { setEditingLink(null); setDisplayForm(null); }; const saveSettings = () => { if (!editingLink || !displayForm) return; setWorkingId(`${editingLink.id}-settings`); updateLauncherAppLink(editingLink.id, { displayName: displayForm.displayName, description: displayForm.description, subtitle: displayForm.subtitle, detailUrl: displayForm.detailUrl, videoPath: displayForm.videoPath, iconKey: displayForm.iconKey, launchUrl: displayForm.launchUrl, devAppPath: displayForm.devAppPath, execPath: displayForm.execPath, displayOrder: Number(displayForm.displayOrder) || undefined, isEnabled: displayForm.isEnabled }) .then(({ link }) => { setLinkedApps((prev) => prev.map((item) => (item.id === link.id ? link : item)).sort((a, b) => a.displayOrder - b.displayOrder)); toast({ type: 'success', message: '런처 표시 설정을 저장했습니다.' }); closeSettings(); refresh(); }) .catch((error: Error) => { toast({ type: 'error', title: '설정 저장 실패', message: error.message }); }) .finally(() => setWorkingId('')); }; return ( <> } />
{!loading && linkedApps.length === 0 ? ( ) : null} {linkedApps.map((link, index) => ( ))}
노출순서 앱 릴리즈 실행 경로 관리
연결된 앱이 없습니다.
{link.displayOrder || index + 1}

{link.displayName || link.app.name}

{[link.app.key, link.subtitle].filter(Boolean).join(' · ')}

{releaseLabel(link)} {launchUrl(link)}
{!loading && unlinkedApps.length === 0 ? (
미연결 앱이 없습니다.
) : null} {unlinkedApps.map((app) => (

{app.name}

{app.key} · {app.releases?.[0]?.version ?? '릴리즈 없음'}

))}
} > {displayForm ? (
{ event.preventDefault(); saveSettings(); }}>
상태