import { MoreHorizontal, Pencil, Plus, ShieldCheck, Trash2 } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table'; import { Button, Card, Modal } from '../../components/ui'; import { useToast } from '../../components/toast'; import { PageTitle } from '../../layout/AppLayout'; import { formatDateOnly } from '../../lib/date-format'; import { LoadingModal, StatusBadge, useMinimumLoading } from './operation-utils'; import { assignUserLicense, loadLicensePlans, loadLicenseUsers, loadLauncherUserAppPermissions, revokeUserLicense, updateLauncherUserAppPermissions, updateUserLicense, type AuthLicenseUser, type LauncherUserAppPermission, type LicensePlan } from './operations-api'; type LicenseAssignmentRow = { id: string; userId: string; user: string; name: string; role: string; code: string; licenseName: string; startsAt: string; expiresAt?: string | null; assignedBy?: string | null; status: string; }; function today() { return new Date().toISOString().slice(0, 10); } function addDays(dateValue: string, days?: number | null) { if (!dateValue || !days) return ''; const date = new Date(`${dateValue}T00:00:00`); date.setDate(date.getDate() + days); return date.toISOString().slice(0, 10); } function formatOptionalDate(value?: string | null) { if (value == null) return '-'; const trimmed = String(value).trim(); if (!trimmed || trimmed === 'null' || trimmed === 'undefined') return '-'; return formatDateOnly(trimmed); } function toDateInputValue(value?: string | null) { if (!value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; return date.toISOString().slice(0, 10); } function assignmentRows(users: AuthLicenseUser[]) { return users.flatMap((user) => user.licenses.map((license) => ({ id: license.id, userId: user.id, user: user.email, name: user.name, role: user.role, code: license.license.code, licenseName: license.license.name, startsAt: license.startsAt, expiresAt: license.expiresAt, assignedBy: license.assignedById, status: license.status })) ); } export function LicensesPage() { const { toast } = useToast(); const [plans, setPlans] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [assignOpen, setAssignOpen] = useState(false); const [editingRow, setEditingRow] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [appPermissionTarget, setAppPermissionTarget] = useState(null); const [appPermissions, setAppPermissions] = useState([]); const [appPermissionLoading, setAppPermissionLoading] = useState(false); const [menuRow, setMenuRow] = useState<{ id: string; x: number; y: number } | null>(null); const [saving, setSaving] = useState(false); const [assignForm, setAssignForm] = useState({ userId: '', licenseCode: '', startsAt: today(), expiresAt: '' }); const [userKeyword, setUserKeyword] = useState(''); const loadingVisible = useMinimumLoading(loading); const rows = useMemo(() => assignmentRows(users), [users]); const calculateExpiresAt = (licenseCode: string, startsAt: string) => { const plan = plans.find((item) => item.code === licenseCode); return addDays(startsAt, plan?.defaultDurationDays); }; const filteredUsers = useMemo(() => { const keyword = userKeyword.trim().toLowerCase(); if (!keyword) return []; return users.filter((user) => `${user.email} ${user.name}`.toLowerCase().includes(keyword)); }, [userKeyword, users]); const selectedUser = useMemo(() => users.find((user) => user.id === assignForm.userId), [assignForm.userId, users]); const selectedUserCurrentLicense = selectedUser?.currentLicense ?? null; const cannotAssignBecauseActive = Boolean(!editingRow && selectedUserCurrentLicense); const refresh = async () => { setLoading(true); try { const [nextPlans, nextUsers] = await Promise.all([loadLicensePlans(), loadLicenseUsers()]); setPlans(nextPlans); setUsers(nextUsers); setAssignForm((prev) => ({ ...prev, licenseCode: prev.licenseCode || nextPlans[0]?.code || '', expiresAt: prev.expiresAt || addDays(prev.startsAt, nextPlans.find((plan) => plan.code === (prev.licenseCode || nextPlans[0]?.code || ''))?.defaultDurationDays) })); } catch (error) { toast({ type: 'error', message: error instanceof Error ? error.message : '라이센스 정보를 불러오지 못했습니다.' }); } finally { setLoading(false); } }; useEffect(() => { void refresh(); }, []); useEffect(() => { if (!menuRow) return; const close = () => setMenuRow(null); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setMenuRow(null); }; window.addEventListener('click', close); window.addEventListener('keydown', closeOnEscape); return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', closeOnEscape); }; }, [menuRow]); const openAssignModal = () => { const startsAt = today(); const licenseCode = assignForm.licenseCode || plans[0]?.code || ''; setEditingRow(null); setAssignForm((prev) => ({ userId: '', licenseCode, startsAt, expiresAt: calculateExpiresAt(licenseCode, startsAt) })); setUserKeyword(''); setAssignOpen(true); }; const openEditModal = (row: LicenseAssignmentRow) => { const startsAt = toDateInputValue(row.startsAt) || today(); setMenuRow(null); setEditingRow(row); setAssignForm({ userId: row.userId, licenseCode: row.code, startsAt, expiresAt: toDateInputValue(row.expiresAt) }); setUserKeyword(''); setAssignOpen(true); }; const closeAssignModal = () => { setAssignOpen(false); setEditingRow(null); setUserKeyword(''); }; const submitAssign = async () => { if (!assignForm.userId || !assignForm.licenseCode) return; setSaving(true); try { const payload = { licenseCode: assignForm.licenseCode, startsAt: assignForm.startsAt || undefined, expiresAt: assignForm.expiresAt || undefined }; if (editingRow) { await updateUserLicense(editingRow.id, payload); } else { await assignUserLicense(assignForm.userId, payload); } toast({ type: 'success', message: editingRow ? '라이센스를 수정했습니다.' : '라이센스를 부여했습니다.' }); closeAssignModal(); await refresh(); } catch (error) { toast({ type: 'error', message: error instanceof Error ? error.message : editingRow ? '라이센스 수정에 실패했습니다.' : '라이센스 부여에 실패했습니다.' }); } finally { setSaving(false); } }; const revoke = async () => { if (!deleteTarget) return; setSaving(true); try { await revokeUserLicense(deleteTarget.id); toast({ type: 'success', message: '라이센스를 삭제했습니다.' }); setDeleteTarget(null); await refresh(); } catch (error) { toast({ type: 'error', message: error instanceof Error ? error.message : '라이센스 삭제에 실패했습니다.' }); } finally { setSaving(false); } }; const openAppPermissionModal = async (row: LicenseAssignmentRow) => { setMenuRow(null); setAppPermissionTarget(row); setAppPermissions([]); setAppPermissionLoading(true); try { setAppPermissions(await loadLauncherUserAppPermissions(row.userId)); } catch (error) { toast({ type: 'error', message: error instanceof Error ? error.message : '앱 권한을 불러오지 못했습니다.' }); } finally { setAppPermissionLoading(false); } }; const closeAppPermissionModal = () => { setAppPermissionTarget(null); setAppPermissions([]); }; const toggleAppPermission = (appKey: string, field: 'canView' | 'canLaunch') => { setAppPermissions((prev) => prev.map((permission) => { if (permission.appKey !== appKey) return permission; if (field === 'canView') { const canView = !permission.canView; return { ...permission, canView, canLaunch: canView ? permission.canLaunch : false }; } const canLaunch = !permission.canLaunch; return { ...permission, canView: canLaunch ? true : permission.canView, canLaunch }; })); }; const saveAppPermissions = async () => { if (!appPermissionTarget) return; setSaving(true); try { const result = await updateLauncherUserAppPermissions( appPermissionTarget.userId, appPermissions.map((permission) => ({ appKey: permission.appKey, canView: permission.canView, canLaunch: permission.canView && permission.canLaunch })) ); setAppPermissions(result.permissions ?? []); toast({ type: 'success', message: '앱 권한을 저장했습니다.' }); closeAppPermissionModal(); } catch (error) { toast({ type: 'error', message: error instanceof Error ? error.message : '앱 권한 저장에 실패했습니다.' }); } finally { setSaving(false); } }; const columns = useMemo>>( () => [ { key: 'user', header: '사용자', className: 'w-[260px] px-5 py-3 font-medium', cell: (row) => (

{row.user}

{row.name}

) }, { key: 'licenseName', header: '라이선스명', cell: (row) => {row.licenseName} }, { key: 'startsAt', header: '시작일', cell: (row) => {formatDateOnly(row.startsAt)} }, { key: 'expiresAt', header: '만료일', cell: (row) => {formatOptionalDate(row.expiresAt)} }, { key: 'assignedBy', header: '부여자', cell: (row) => {row.assignedBy || '-'} }, { key: 'status', header: '상태', cell: (row) => }, { key: 'actions', header: '', className: 'w-20 px-5 py-3 text-right font-medium', cell: (row) => (
) } ], [] ); const filters = useMemo>>( () => [ { key: 'licenseName', label: '라이센스', options: plans.map((plan) => ({ label: plan.name, value: plan.name })), getValue: (row) => row.licenseName }, { key: 'status', label: '상태', options: [...new Set(rows.map((row) => row.status))].map((status) => ({ label: status, value: status })), getValue: (row) => row.status } ], [plans, rows] ); return ( <> 라이센스 부여 } />
{plans.map((plan) => (

{plan.name}

{plan.description || '-'}

부여

{plan.assignedCount}

활성

{plan.activeUserCount}

만료임박

{plan.expiringSoonCount}

기본 기간 {plan.defaultDurationDays ?? '-'}일

))} {!loading && plans.length === 0 ? ( 등록된 라이센스 플랜이 없습니다. ) : null}
`${row.user} ${row.name} ${row.licenseName} ${row.assignedBy ?? ''} ${row.status}`} getRowKey={(row) => row.id} pageSize={6} minWidth={980} loading={loading} emptyMessage="부여된 라이센스가 없습니다." />
{menuRow ? (() => { const row = rows.find((item) => item.id === menuRow.id); if (!row) return null; return (
event.stopPropagation()} className="fixed z-[70] min-w-28 overflow-hidden rounded-md border border-border bg-card py-1 text-left shadow-xl" style={{ left: menuRow.x, top: menuRow.y, transform: 'translateX(-100%)' }}>
); })() : null} {editingRow ? '라이센스 수정' : '라이센스 부여'} } onClose={closeAssignModal} footer={ <> } >
{editingRow ? (
사용자

{editingRow.user}

{editingRow.name || '-'}

) : (
사용자 setUserKeyword(event.target.value)} placeholder="이메일 또는 이름 검색" /> {selectedUser ? (

{selectedUser.email}

{selectedUser.name || '-'}

{selectedUserCurrentLicense ? (

이미 활성 라이센스가 있습니다. 기존 이력에서 수정하세요.

) : null}
) : null} {userKeyword.trim() ? (
{filteredUsers.map((user) => { const selected = assignForm.userId === user.id; return ( ); })} {filteredUsers.length === 0 ? (
검색된 사용자가 없습니다.
) : null}
) : null}
)}
라이센스 삭제 } onClose={() => setDeleteTarget(null)} footer={ <> } >

라이센스를 삭제할까요?

앱 권한 설정 } onClose={closeAppPermissionModal} className="max-w-2xl" footer={ <> } >

{appPermissionTarget?.user ?? '-'}

{appPermissionTarget?.name || '-'}

{appPermissionLoading ? (
앱 권한을 불러오는 중입니다.
) : appPermissions.length === 0 ? (
런처에 연결된 앱이 없습니다.
) : ( appPermissions.map((permission) => (

{permission.displayName}

{permission.appKey}

)) )}
); }