import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, Clock, Plus, ShieldAlert, X, XCircle, } from "lucide-react"; import { useEffect, useState } from "react"; import { useAuth } from "react-oidc-context"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import { commonTableShellClass, commonTableViewportClass, } from "../../../../common/ui/table"; import { Textarea } from "../../components/ui/textarea"; import { approveDeveloperRequest, cancelDeveloperRequestApproval, fetchDeveloperRequests, fetchMyTenants, rejectDeveloperRequest, requestDeveloperAccess, } from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; import { fetchMe } from "../auth/authApi"; export default function DeveloperRequestPage() { const auth = useAuth(); const queryClient = useQueryClient(); const userProfile = auth.user?.profile as Record | undefined; const role = resolveProfileRole(userProfile); const isSuperAdmin = role === "super_admin"; const tenantId = userProfile?.tenant_id as string | undefined; const companyCode = userProfile?.companyCode as string | undefined; const [isRequestModalOpen, setIsRequestModalOpen] = useState(false); const [adminNotes, setAdminNotes] = useState>({}); const { data: requests, isLoading } = useQuery({ queryKey: ["developer-requests"], queryFn: () => fetchDeveloperRequests(), enabled: !!auth.user?.access_token, }); const { data: tenants } = useQuery({ queryKey: ["myTenants"], queryFn: fetchMyTenants, enabled: !!auth.user?.access_token, }); const { data: me } = useQuery({ queryKey: ["userMe"], queryFn: fetchMe, enabled: !!auth.user?.access_token, }); const currentTenant = tenants?.find( (tenant) => tenant.id === tenantId || tenant.slug === companyCode, ); const organizationName = currentTenant?.name || companyCode || ""; const profileName = me?.name || (userProfile?.name as string) || ""; const profileEmail = me?.email || (userProfile?.email as string) || ""; const profilePhone = me?.phone || (userProfile?.phone as string | undefined) || (userProfile?.phone_number as string | undefined) || ""; const profileRole = me?.role || role; const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole); const approveMutation = useMutation({ mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) => approveDeveloperRequest(id, adminNotes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["developer-requests"] }); alert(t("msg.dev.request.approved", "승인되었습니다.")); }, }); const rejectMutation = useMutation({ mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) => rejectDeveloperRequest(id, adminNotes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["developer-requests"] }); alert(t("msg.dev.request.rejected", "반려되었습니다.")); }, }); const cancelApprovalMutation = useMutation({ mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) => cancelDeveloperRequestApproval(id, adminNotes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["developer-requests"] }); queryClient.invalidateQueries({ queryKey: ["developer-request"] }); alert(t("msg.dev.request.cancelled", "승인이 취소되었습니다.")); }, }); const handleApprove = (id: number) => { approveMutation.mutate({ id, adminNotes: adminNotes[id] || "" }); }; const handleReject = (id: number) => { if (!adminNotes[id]) { alert(t("msg.dev.request.need_notes", "반려 사유를 입력해주세요.")); return; } rejectMutation.mutate({ id, adminNotes: adminNotes[id] }); }; const handleCancelApproval = (id: number) => { if (!adminNotes[id]) { alert( t( "msg.dev.request.need_cancel_notes", "승인 취소 사유를 입력해주세요.", ), ); return; } cancelApprovalMutation.mutate({ id, adminNotes: adminNotes[id] }); }; if (isLoading) { return (
{t("ui.common.loading", "Loading...")}
); } const hasActiveRequest = requests?.some( (r) => r.status === "pending" || r.status === "approved", ); const isActionPending = approveMutation.isPending || rejectMutation.isPending || cancelApprovalMutation.isPending; return (

{t("ui.dev.nav.developer_request", "개발자 권한 신청")}

{isSuperAdmin ? t( "msg.dev.request.admin_desc", "사용자들의 개발자 권한 신청 내역을 관리합니다.", ) : t( "msg.dev.request.user_desc", "내 신청 내역을 확인하고 새로운 권한을 신청할 수 있습니다.", )}

{!isSuperAdmin && !hasActiveRequest && ( )}
{t("ui.dev.request.list.title", "신청 내역")}
{isSuperAdmin && ( {t("ui.dev.request.table.user", "사용자")} )} {t("ui.dev.request.table.org", "소속")} {t("ui.dev.request.table.reason", "신청 사유")} {t("ui.dev.request.table.status", "상태")} {t("ui.dev.request.table.date", "신청일")} {isSuperAdmin && ( {t("ui.dev.request.table.actions", "관리")} )} {!requests || requests.length === 0 ? ( {t("msg.dev.request.empty", "신청 내역이 없습니다.")} ) : ( requests.map((req) => ( {isSuperAdmin && (
{req.name}
{req.email || req.userId}
{(req.phone || req.role) && (
{[req.phone, req.role].filter(Boolean).join(" / ")}
)}
)} {req.organization}
{req.reason}
{req.adminNotes && (
Admin: {req.adminNotes}
)}
{new Date(req.createdAt).toLocaleDateString()} {isSuperAdmin && ( {req.status === "pending" ? (
setAdminNotes({ ...adminNotes, [req.id]: e.target.value, }) } />
) : req.status === "approved" ? (
setAdminNotes({ ...adminNotes, [req.id]: e.target.value, }) } />
) : ( {req.status === "cancelled" ? t( "ui.dev.request.status.cancelled", "승인 취소됨", ) : t("ui.common.rejected", "반려됨")} )}
)}
)) )}
setIsRequestModalOpen(false)} onSuccess={() => { queryClient.invalidateQueries({ queryKey: ["developer-requests"] }); setIsRequestModalOpen(false); }} tenantId={tenantId || ""} initialName={profileName} initialOrg={organizationName} initialEmail={profileEmail} initialPhone={profilePhone} initialRole={profileRoleLabel} />
); } function StatusBadge({ status }: { status: string }) { switch (status) { case "pending": return ( {t("ui.dev.request.status.pending", "대기 중")} ); case "approved": return ( {t("ui.dev.request.status.approved", "승인됨")} ); case "rejected": return ( {t("ui.dev.request.status.rejected", "반려됨")} ); case "cancelled": return ( {t("ui.dev.request.status.cancelled", "승인 취소됨")} ); default: return {status}; } } interface RequestAccessModalProps { isOpen: boolean; onClose: () => void; onSuccess: () => void; tenantId: string; initialName: string; initialOrg: string; initialEmail: string; initialPhone: string; initialRole: string; } function RequestAccessModal({ isOpen, onClose, onSuccess, tenantId, initialName, initialOrg, initialEmail, initialPhone, initialRole, }: RequestAccessModalProps) { const [name, setName] = useState(initialName); const [organization, setOrganization] = useState(initialOrg); const [reason, setReason] = useState(""); useEffect(() => { if (!isOpen) return; setName(initialName); setOrganization(initialOrg); }, [initialName, initialOrg, isOpen]); const mutation = useMutation({ mutationFn: requestDeveloperAccess, onSuccess: () => { onSuccess(); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); mutation.mutate({ name, organization, reason, tenantId, }); }; if (!isOpen) return null; return (

{t("ui.dev.request.modal.title", "개발자 등록 신청")}

{t( "msg.dev.request.modal.desc", "신청 사유를 입력해 주세요. 관리자 확인 후 승인됩니다.", )}