- 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>
187 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|