Files
vexplor_dev/frontend/components/pop/shell/CompanySwitchModal.tsx
kmh dcbcdb2f52 refactor(pop): isolate new shell, add super-admin entry, drop /pop fallback
- per-company PopShell copies under (main)/COMPANY_*/pop/_components/common/
  (no longer imports @/components/pop/hardcoded/PopShell)
- new components/pop/shell/CompanySwitchModal for new POP entry
- AppLayout: SUPER_ADMIN POP-mode toggle + company-select modal flow
- usePopSettings: handle /COMPANY_X/pop/<tail> URLs (extractScreenKey)
- authController + AppLayout: drop legacy /pop fallback;
  use /\${companyCode}/pop/main when childMenus>1 lacks [POP_LANDING]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:07:44 +09:00

187 lines
6.1 KiB
TypeScript

"use client";
import React, { useState, useEffect, useMemo } from "react";
import { apiClient } from "@/lib/api/client";
interface Company {
company_code: string;
company_name: string;
status: string;
}
interface CompanySwitchModalProps {
open: boolean;
onClose: () => void;
onSelect: (companyCode: string) => void;
currentCompanyCode?: string;
}
export function CompanySwitchModal({
open,
onClose,
onSelect,
currentCompanyCode,
}: CompanySwitchModalProps) {
const [companies, setCompanies] = useState<Company[]>([]);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
if (open) {
fetchCompanies();
setSearch("");
}
}, [open]);
const fetchCompanies = async () => {
try {
setLoading(true);
const response = await apiClient.get("/admin/companies/db");
if (response.data.success) {
const activeCompanies = response.data.data
.filter((c: Company) => c.company_code !== "*")
.filter((c: Company) => c.status === "active" || !c.status)
.sort((a: Company, b: Company) =>
a.company_name.localeCompare(b.company_name, "ko")
);
const companiesWithWace: Company[] = [
{
company_code: "*",
company_name: "WACE (최고 관리자)",
status: "active",
},
...activeCompanies,
];
setCompanies(companiesWithWace);
}
} catch {
setCompanies([]);
} finally {
setLoading(false);
}
};
const filtered = useMemo(() => {
if (!search.trim()) return companies;
const q = search.toLowerCase();
return companies.filter(
(c) =>
c.company_name.toLowerCase().includes(q) ||
c.company_code.toLowerCase().includes(q)
);
}, [companies, search]);
const handleSelect = (companyCode: string) => {
onSelect(companyCode);
onClose();
};
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
{/* Modal */}
<div className="relative w-full max-w-md max-h-[80vh] flex flex-col rounded-2xl shadow-2xl overflow-hidden z-10 bg-[var(--pop-card-bg,#1e293b)] text-[var(--pop-text,#e2e8f0)]">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--pop-border,#334155)]">
<h3 className="text-lg font-bold"> </h3>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg flex items-center justify-center transition-colors bg-[var(--pop-hover,#334155)] hover:bg-[var(--pop-hover-strong,#475569)]"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Search */}
<div className="px-5 py-3">
<div className="relative">
<svg
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 opacity-50"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="회사명 또는 코드 검색..."
className="w-full pl-10 pr-4 py-2.5 rounded-xl text-sm outline-none transition-all bg-[var(--pop-input-bg,#0f172a)] border border-[var(--pop-border,#334155)] focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 text-[var(--pop-text,#e2e8f0)] placeholder:opacity-50"
/>
</div>
</div>
{/* Company List */}
<div className="flex-1 overflow-y-auto px-5 pb-5 space-y-2">
{loading ? (
<div className="flex items-center justify-center py-12 text-sm opacity-50">
...
</div>
) : filtered.length === 0 ? (
<div className="flex items-center justify-center py-12 text-sm opacity-50">
{search ? "검색 결과가 없습니다" : "회사 목록이 없습니다"}
</div>
) : (
filtered.map((company) => {
const isCurrent = company.company_code === currentCompanyCode;
return (
<button
key={company.company_code}
onClick={() => handleSelect(company.company_code)}
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl text-left transition-all ${
isCurrent
? "border-2 border-green-500 bg-green-500/10"
: "border border-[var(--pop-border,#334155)] hover:bg-[var(--pop-hover,#334155)]"
}`}
>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-semibold">
{company.company_name}
</span>
<span className="text-xs opacity-50">
{company.company_code === "*"
? "슈퍼관리자 모드"
: company.company_code}
</span>
</div>
{isCurrent && (
<span className="text-xs font-medium text-green-400 bg-green-500/20 px-2 py-0.5 rounded-full">
</span>
)}
</button>
);
})
)}
</div>
</div>
</div>
);
}