824a3100ce
이번 PR 은 invyone 멀티테넌시 SaaS 의 "관리 plane vs 테넌트 plane" 격리를 4 영역(PR #A~D) 에서 강화하고, 별도로 진행 중이던 부서관리 후속 작업을 포함한다. # 보안 (plane 격리) PR #A — controller/CompanyManagementController 인증 누락 패치 /api/company-management/* 가 JWT/role/host 체크 없이 외부에서 누구나 회사 삭제 + 디스크 통계 호출 가능했던 critical 누수 막음. SuperAdminGuard.enforce() 적용. PR #C — cross-tenant 컨트롤러 호스트 격리 + 감사 로그 CrossTenantContext.requireManagementHost() 헬퍼 추가, 5 컨트롤러 (CrossTenantContext/Controller/UserController/RoleController/DeptController) 모두 테넌트 호스트에서 호출 시 403. CompanyAuditLogService 에 cross-tenant write 4종 (USER_CREATE/DELETE, PW_RESET, ROLE_UPDATE) audit action 추가. SuperAdminGuard.isTenantHost 가시성 public static 으로 승격. PR #B — 프론트 솔루션 전용 admin 페이지 가드 admin/* 페이지 전수 분류 결과 솔루션 전용 3건 식별: subdomainList / companyList / audit-log. 각 페이지에 isManagementHost useEffect 가드 + redirect 추가. 사이드바도 같이 숨김. PR #D — MENU_INFO.IS_SOLUTION_ONLY 컬럼 + DB-driven 메뉴 필터 V023 마이그레이션으로 컬럼 추가 + 솔루션 메뉴 3개 마킹. admin.xml selectUserMenuList 에 호스트 기반 필터 추가, AdminController.getUserMenus 가 Host 헤더로 is_management_host 결정. 프론트 MANAGEMENT_ONLY_MENU_URLS 하드코딩 set 폐기 (DB 가 대신함). 페이지 자체 가드는 defense in depth 로 유지. StartupSchemaMigrator 에 V023 등록되어 모든 테넌트 DB 부팅 시 자동 적용. # 부서관리 후속 (이전 PR #18/#19 follow-up) DepartmentController/Service + frontend deptMngList/department.ts 의 추가 작업분. 이번 격리 작업과 무관하지만 같이 정리. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { isManagementHost } from "@/lib/tenant/subdomain";
|
|
import { useCompanyManagement } from "@/hooks/useCompanyManagement";
|
|
import { CompanyToolbar } from "@/components/admin/CompanyToolbar";
|
|
import { CompanyTable } from "@/components/admin/CompanyTable";
|
|
import { CompanyFormModal } from "@/components/admin/CompanyFormModal";
|
|
import { CompanyDeleteDialog } from "@/components/admin/CompanyDeleteDialog";
|
|
import { DiskUsageSummary } from "@/components/admin/DiskUsageSummary";
|
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
|
|
|
/**
|
|
* 회사 관리 페이지
|
|
* 모든 회사 관리 기능을 통합하여 제공
|
|
*/
|
|
export default function CompanyPage() {
|
|
const router = useRouter();
|
|
const [hostBlocked, setHostBlocked] = useState(false);
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return;
|
|
if (!isManagementHost(window.location.hostname)) {
|
|
setHostBlocked(true);
|
|
router.replace("/main");
|
|
}
|
|
}, [router]);
|
|
|
|
const {
|
|
// 데이터
|
|
companies,
|
|
searchFilter,
|
|
isLoading,
|
|
error,
|
|
|
|
// 디스크 사용량 관련
|
|
diskUsageInfo,
|
|
isDiskUsageLoading,
|
|
loadDiskUsage,
|
|
|
|
// 모달 상태
|
|
modalState,
|
|
deleteState,
|
|
|
|
// 검색 기능
|
|
updateSearchFilter,
|
|
clearSearchFilter,
|
|
|
|
// 모달 제어
|
|
openCreateModal,
|
|
openEditModal,
|
|
closeModal,
|
|
updateFormData,
|
|
|
|
// 삭제 다이얼로그 제어
|
|
openDeleteDialog,
|
|
closeDeleteDialog,
|
|
|
|
// CRUD 작업
|
|
saveCompany,
|
|
deleteCompany,
|
|
|
|
// 에러 처리
|
|
clearError,
|
|
} = useCompanyManagement();
|
|
|
|
if (hostBlocked) return null;
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-col bg-background">
|
|
<div className="space-y-6 p-6">
|
|
{/* 페이지 헤더 */}
|
|
<div className="space-y-2 border-b pb-4">
|
|
<h1 className="text-3xl font-bold tracking-tight">회사 관리</h1>
|
|
<p className="text-sm text-muted-foreground">시스템에서 사용하는 회사 정보를 관리합니다</p>
|
|
</div>
|
|
|
|
{/* 디스크 사용량 요약 */}
|
|
<DiskUsageSummary diskUsageInfo={diskUsageInfo} isLoading={isDiskUsageLoading} onRefresh={loadDiskUsage} />
|
|
|
|
{/* 툴바 - 검색, 필터, 등록 버튼 */}
|
|
<CompanyToolbar
|
|
searchFilter={searchFilter}
|
|
totalCount={companies.length}
|
|
filteredCount={companies.length}
|
|
onSearchChange={updateSearchFilter}
|
|
onSearchClear={clearSearchFilter}
|
|
onCreateClick={openCreateModal}
|
|
/>
|
|
|
|
{/* 회사 목록 테이블 */}
|
|
<CompanyTable companies={companies} isLoading={isLoading} onEdit={openEditModal} onDelete={openDeleteDialog} />
|
|
|
|
{/* 회사 등록/수정 모달 */}
|
|
<CompanyFormModal
|
|
modalState={modalState}
|
|
isLoading={isLoading}
|
|
error={error}
|
|
onClose={closeModal}
|
|
onSave={saveCompany}
|
|
onFormChange={updateFormData}
|
|
onClearError={clearError}
|
|
/>
|
|
|
|
{/* 회사 삭제 확인 다이얼로그 */}
|
|
<CompanyDeleteDialog
|
|
deleteState={deleteState}
|
|
isLoading={isLoading}
|
|
error={error}
|
|
onClose={closeDeleteDialog}
|
|
onConfirm={deleteCompany}
|
|
onClearError={clearError}
|
|
/>
|
|
</div>
|
|
|
|
{/* Scroll to Top 버튼 */}
|
|
<ScrollToTop />
|
|
</div>
|
|
);
|
|
}
|