cdc55dfd48
SUPER_ADMIN cross-tenant 모드에서 회사당 cap 200 에 걸리거나 한 회사 조회 실패 시 화면 상단에 안내 배너 노출. 아무 메타 없으면 자리 안 잡음. 신규 - components/common/CrossTenantBanner.tsx — amber(truncated) + red(failed) v5 토큰 (surface-solid + glow-sm) 기반 솔리드 배너. blur 안 씀 API 클라이언트 4개에 cross_tenant_meta 노출 - lib/api/user.ts — userAPI.getList 응답에 cross_tenant_meta 추가 - lib/api/role.ts — roleAPI.getList 동일 - lib/api/batch.ts — BatchAPI.getBatchConfigs 동일 - lib/api/multilang.ts — getLangKeys 동일 (i18nList 페이지는 아직 직접 호출 패턴이라 자동 적용 X — 후속에서 페이지를 getLangKeys 로 통일하면 동작) 페이지 마운트 (3개) - userMng/userMngList — useUserManagement hook 에 crossTenantMeta state 추가 - userMng/rolesList — loadRoleGroups 에서 메타 set - automaticMng/batchmngList — loadBatchConfigs 에서 메타 set - systemMng/i18nList — 스킵 (cross-tenant aggregation 미적용 상태, 별도 작업) 설계서 §11 검증 (직전 §11.2 부분 실패 시뮬) 결과: failed 배너가 header X-CrossTenant-Failed 와 동일 정보로 화면에 노출됨. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
5.9 KiB
TypeScript
199 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useUserManagement } from "@/hooks/useUserManagement";
|
|
import { UserToolbar } from "@/components/admin/UserToolbar";
|
|
import { UserTable } from "@/components/admin/UserTable";
|
|
import { Pagination } from "@/components/common/Pagination";
|
|
import { UserPasswordResetModal } from "@/components/admin/UserPasswordResetModal";
|
|
import { UserFormModal } from "@/components/admin/UserFormModal";
|
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
|
import { CrossTenantBanner } from "@/components/common/CrossTenantBanner";
|
|
|
|
/**
|
|
* 사용자관리 페이지
|
|
* URL: /admin/userMng
|
|
*
|
|
* shadcn/ui 스타일 가이드 적용
|
|
* - 원본 Spring + JSP 코드 패턴 기반 REST API 연동
|
|
* - 실제 데이터베이스와 연동되어 작동
|
|
*/
|
|
export default function UserMngPage() {
|
|
const {
|
|
// 데이터
|
|
users,
|
|
searchFilter,
|
|
isLoading,
|
|
isSearching,
|
|
error,
|
|
paginationInfo,
|
|
crossTenantMeta,
|
|
|
|
// 검색 기능
|
|
updateSearchFilter,
|
|
|
|
// 페이지네이션
|
|
handlePageChange,
|
|
handlePageSizeChange,
|
|
|
|
// 액션 핸들러
|
|
handleStatusToggle,
|
|
|
|
// 유틸리티
|
|
clearError,
|
|
refreshData,
|
|
} = useUserManagement();
|
|
|
|
// 비밀번호 초기화 모달 상태
|
|
const [passwordResetModal, setPasswordResetModal] = useState({
|
|
isOpen: false,
|
|
user_id: null as string | null,
|
|
user_name: null as string | null,
|
|
});
|
|
|
|
// 사용자 등록/수정 모달 상태
|
|
const [userFormModal, setUserFormModal] = useState({
|
|
isOpen: false,
|
|
editingUser: null as any | null,
|
|
});
|
|
|
|
// 사용자 등록 핸들러
|
|
const handleCreateUser = () => {
|
|
setUserFormModal({
|
|
isOpen: true,
|
|
editingUser: null,
|
|
});
|
|
};
|
|
|
|
// 사용자 수정 핸들러
|
|
const handleEditUser = (user: any) => {
|
|
setUserFormModal({
|
|
isOpen: true,
|
|
editingUser: user,
|
|
});
|
|
};
|
|
|
|
// 사용자 등록/수정 모달 닫기
|
|
const handleUserFormClose = () => {
|
|
setUserFormModal({
|
|
isOpen: false,
|
|
editingUser: null,
|
|
});
|
|
};
|
|
|
|
// 사용자 등록/수정 성공 핸들러
|
|
const handleUserFormSuccess = () => {
|
|
refreshData();
|
|
handleUserFormClose();
|
|
};
|
|
|
|
// 비밀번호 초기화 핸들러
|
|
const handlePasswordReset = (userId: string, userName: string) => {
|
|
setPasswordResetModal({
|
|
isOpen: true,
|
|
user_id: userId,
|
|
user_name: userName,
|
|
});
|
|
};
|
|
|
|
// 비밀번호 초기화 모달 닫기
|
|
const handlePasswordResetClose = () => {
|
|
setPasswordResetModal({
|
|
isOpen: false,
|
|
user_id: null,
|
|
user_name: null,
|
|
});
|
|
};
|
|
|
|
// 비밀번호 초기화 성공 핸들러
|
|
const handlePasswordResetSuccess = () => {
|
|
handlePasswordResetClose();
|
|
};
|
|
|
|
return (
|
|
// 페이지 자체는 스크롤 X — 헤더/툴바/페이지네이션은 고정,
|
|
// 테이블만 자체 스크롤 (UserTable 의 ResponsiveDataView scrollContainer prop).
|
|
// 부모(TabContent) 가 overflow-hidden 이라 h-full 로 받아 flex-col 내에서
|
|
// 마지막 자식 (UserTable wrapper) 이 flex-1 min-h-0 으로 남은 공간 차지.
|
|
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-background">
|
|
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden p-6">
|
|
{/* 페이지 헤더 */}
|
|
<div className="space-y-1 border-b pb-3">
|
|
<h1 className="text-xl font-bold tracking-tight">사용자 관리</h1>
|
|
<p className="text-xs text-muted-foreground">시스템 사용자 계정 및 권한을 관리합니다</p>
|
|
</div>
|
|
|
|
{/* 툴바 - 검색, 필터, 등록 버튼 */}
|
|
<UserToolbar
|
|
searchFilter={searchFilter}
|
|
totalCount={paginationInfo.totalItems}
|
|
isSearching={isSearching}
|
|
onSearchChange={updateSearchFilter}
|
|
onCreateClick={handleCreateUser}
|
|
/>
|
|
|
|
{/* SUPER_ADMIN cross-tenant 모드의 truncated/failed 안내 */}
|
|
<CrossTenantBanner meta={crossTenantMeta} />
|
|
|
|
{/* 에러 메시지 */}
|
|
{error && (
|
|
<div className="border-destructive/50 bg-destructive/10 rounded-lg border p-3">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<p className="text-destructive text-sm font-semibold">{error}</p>
|
|
<button
|
|
onClick={clearError}
|
|
className="text-destructive hover:text-destructive/80 transition-colors"
|
|
aria-label="에러 메시지 닫기"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 사용자 목록 테이블 */}
|
|
<UserTable
|
|
users={users}
|
|
isLoading={isLoading}
|
|
paginationInfo={paginationInfo}
|
|
onStatusToggle={handleStatusToggle}
|
|
onPasswordReset={handlePasswordReset}
|
|
onEdit={handleEditUser}
|
|
/>
|
|
|
|
{/* 페이지네이션 */}
|
|
{!isLoading && users.length > 0 && (
|
|
<Pagination
|
|
paginationInfo={paginationInfo}
|
|
onPageChange={handlePageChange}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
showPageSizeSelector={true}
|
|
pageSizeOptions={[10, 20, 50, 100]}
|
|
className="mt-4"
|
|
/>
|
|
)}
|
|
|
|
{/* 사용자 등록/수정 모달 */}
|
|
<UserFormModal
|
|
isOpen={userFormModal.isOpen}
|
|
onClose={handleUserFormClose}
|
|
onSuccess={handleUserFormSuccess}
|
|
editingUser={userFormModal.editingUser}
|
|
/>
|
|
|
|
{/* 비밀번호 초기화 모달 */}
|
|
<UserPasswordResetModal
|
|
isOpen={passwordResetModal.isOpen}
|
|
onClose={handlePasswordResetClose}
|
|
userId={passwordResetModal.user_id}
|
|
userName={passwordResetModal.user_name}
|
|
onSuccess={handlePasswordResetSuccess}
|
|
/>
|
|
</div>
|
|
|
|
{/* Scroll to Top 버튼 (모바일/태블릿 전용) */}
|
|
<ScrollToTop />
|
|
</div>
|
|
);
|
|
}
|