- 입금관리, 출금관리 리스트에 등록 버튼 추가 - skeleton, confirm-dialog, empty-state, status-badge UI 컴포넌트 추가 - document-system 컴포넌트 추상화 (ApprovalLine, DocumentHeader 등) - 여러 페이지 컴포넌트 리팩토링 및 코드 정리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useMemo, useEffect, useCallback } from 'react';
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
import { Building2 } from 'lucide-react';
|
|
import { DepartmentStats } from './DepartmentStats';
|
|
import { DepartmentToolbar } from './DepartmentToolbar';
|
|
import { DepartmentTree } from './DepartmentTree';
|
|
import { DepartmentDialog } from './DepartmentDialog';
|
|
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
import type { Department } from './types';
|
|
import { countAllDepartments, getAllDepartmentIds, findDepartmentById } from './types';
|
|
import {
|
|
getDepartmentTree,
|
|
createDepartment,
|
|
updateDepartment,
|
|
deleteDepartment,
|
|
deleteDepartmentsMany,
|
|
type DepartmentRecord,
|
|
} from './actions';
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
|
|
/**
|
|
* API 응답을 로컬 Department 타입으로 변환
|
|
*/
|
|
function convertApiToLocal(record: DepartmentRecord): Department {
|
|
return {
|
|
id: record.id,
|
|
name: record.name,
|
|
parentId: record.parentId,
|
|
depth: record.depth,
|
|
children: record.children ? record.children.map(convertApiToLocal) : [],
|
|
};
|
|
}
|
|
|
|
export function DepartmentManagement() {
|
|
// 부서 데이터 상태
|
|
const [departments, setDepartments] = useState<Department[]>([]);
|
|
|
|
// 로딩/처리 상태
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
|
|
// 선택 상태
|
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
|
|
|
// 펼침 상태 (기본: 최상위만 펼침)
|
|
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
|
|
|
// 검색어
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
/**
|
|
* 부서 트리 조회 API
|
|
*/
|
|
const fetchDepartments = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await getDepartmentTree();
|
|
if (result.success && result.data) {
|
|
const converted = result.data.map(convertApiToLocal);
|
|
setDepartments(converted);
|
|
// 최상위 부서 펼침
|
|
if (converted.length > 0) {
|
|
setExpandedIds(new Set([converted[0].id]));
|
|
}
|
|
} else {
|
|
console.error('[DepartmentManagement] fetchDepartments error:', result.error);
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[DepartmentManagement] fetchDepartments error:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// 초기 데이터 로드
|
|
useEffect(() => {
|
|
fetchDepartments();
|
|
}, [fetchDepartments]);
|
|
|
|
// 다이얼로그 상태
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [dialogMode, setDialogMode] = useState<'add' | 'edit'>('add');
|
|
const [selectedDepartment, setSelectedDepartment] = useState<Department | undefined>();
|
|
const [parentDepartment, setParentDepartment] = useState<Department | undefined>();
|
|
|
|
// 삭제 확인 다이얼로그
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [departmentToDelete, setDepartmentToDelete] = useState<Department | null>(null);
|
|
const [isBulkDelete, setIsBulkDelete] = useState(false);
|
|
|
|
// 전체 부서 수 계산
|
|
const totalCount = useMemo(() => countAllDepartments(departments), [departments]);
|
|
|
|
// 모든 부서 ID
|
|
const allIds = useMemo(() => getAllDepartmentIds(departments), [departments]);
|
|
|
|
// 펼침/접힘 토글
|
|
const handleToggleExpand = (id: number) => {
|
|
setExpandedIds(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// 선택 토글
|
|
const handleToggleSelect = (id: number) => {
|
|
setSelectedIds(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// 전체 선택/해제
|
|
const handleToggleSelectAll = () => {
|
|
if (selectedIds.size === allIds.length) {
|
|
setSelectedIds(new Set());
|
|
} else {
|
|
setSelectedIds(new Set(allIds));
|
|
}
|
|
};
|
|
|
|
// 부서 추가 (행 버튼)
|
|
const handleAdd = (parentId: number) => {
|
|
const parent = findDepartmentById(departments, parentId);
|
|
setParentDepartment(parent || undefined);
|
|
setSelectedDepartment(undefined);
|
|
setDialogMode('add');
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
// 부서 추가 (상단 버튼 - 선택된 부서의 하위에 일괄 추가)
|
|
const handleBulkAdd = () => {
|
|
if (selectedIds.size === 0) {
|
|
// 선택된 부서가 없으면 최상위에 추가
|
|
setParentDepartment(undefined);
|
|
} else {
|
|
// 선택된 첫 번째 부서를 부모로 설정
|
|
const firstSelectedId = Array.from(selectedIds)[0];
|
|
const parent = findDepartmentById(departments, firstSelectedId);
|
|
setParentDepartment(parent || undefined);
|
|
}
|
|
setSelectedDepartment(undefined);
|
|
setDialogMode('add');
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
// 부서 수정
|
|
const handleEdit = (department: Department) => {
|
|
setSelectedDepartment(department);
|
|
setParentDepartment(undefined);
|
|
setDialogMode('edit');
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
// 부서 삭제 (단일)
|
|
const handleDelete = (department: Department) => {
|
|
setDepartmentToDelete(department);
|
|
setIsBulkDelete(false);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
// 부서 삭제 (일괄)
|
|
const handleBulkDelete = () => {
|
|
if (selectedIds.size === 0) return;
|
|
setDepartmentToDelete(null);
|
|
setIsBulkDelete(true);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
/**
|
|
* 삭제 확인 핸들러 (API 연동)
|
|
*/
|
|
const confirmDelete = useCallback(async () => {
|
|
if (isProcessing) return;
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
if (isBulkDelete) {
|
|
// 일괄 삭제 API
|
|
const ids = Array.from(selectedIds);
|
|
const result = await deleteDepartmentsMany(ids);
|
|
if (result.success) {
|
|
await fetchDepartments();
|
|
setSelectedIds(new Set());
|
|
} else {
|
|
console.error('[DepartmentManagement] 일괄 삭제 실패:', result.error);
|
|
}
|
|
} else if (departmentToDelete) {
|
|
// 단일 삭제 API
|
|
const result = await deleteDepartment(departmentToDelete.id);
|
|
if (result.success) {
|
|
await fetchDepartments();
|
|
setSelectedIds((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(departmentToDelete.id);
|
|
return next;
|
|
});
|
|
} else {
|
|
console.error('[DepartmentManagement] 삭제 실패:', result.error);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[DepartmentManagement] confirmDelete error:', error);
|
|
} finally {
|
|
setDeleteDialogOpen(false);
|
|
setDepartmentToDelete(null);
|
|
setIsProcessing(false);
|
|
}
|
|
}, [isBulkDelete, selectedIds, departmentToDelete, isProcessing, fetchDepartments]);
|
|
|
|
/**
|
|
* 부서 추가/수정 제출 핸들러 (API 연동)
|
|
* @note parentId는 현재 API에서 미지원 - 모든 부서가 최상위로 생성됨
|
|
*/
|
|
const handleDialogSubmit = useCallback(async (name: string) => {
|
|
if (isProcessing) return;
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
if (dialogMode === 'add') {
|
|
// 새 부서 추가 API
|
|
// NOTE: parentId는 현재 API에서 지원하지 않아 최상위에만 생성됨
|
|
const result = await createDepartment({
|
|
name,
|
|
parentId: parentDepartment?.id,
|
|
});
|
|
if (result.success) {
|
|
await fetchDepartments();
|
|
} else {
|
|
console.error('[DepartmentManagement] 부서 생성 실패:', result.error);
|
|
}
|
|
} else if (dialogMode === 'edit' && selectedDepartment) {
|
|
// 부서 수정 API
|
|
const result = await updateDepartment(selectedDepartment.id, { name });
|
|
if (result.success) {
|
|
await fetchDepartments();
|
|
} else {
|
|
console.error('[DepartmentManagement] 부서 수정 실패:', result.error);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[DepartmentManagement] handleDialogSubmit error:', error);
|
|
} finally {
|
|
setDialogOpen(false);
|
|
setIsProcessing(false);
|
|
}
|
|
}, [dialogMode, parentDepartment, selectedDepartment, isProcessing, fetchDepartments]);
|
|
|
|
return (
|
|
<PageLayout>
|
|
<PageHeader
|
|
title="부서관리"
|
|
description="부서 정보를 관리합니다"
|
|
icon={Building2}
|
|
/>
|
|
|
|
<div className="space-y-4">
|
|
{/* 전체 부서 카운트 */}
|
|
<DepartmentStats totalCount={totalCount} />
|
|
|
|
{/* 검색 + 추가/삭제 버튼 */}
|
|
<DepartmentToolbar
|
|
totalCount={totalCount}
|
|
selectedCount={selectedIds.size}
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
onAdd={handleBulkAdd}
|
|
onDelete={handleBulkDelete}
|
|
/>
|
|
|
|
{/* 트리 테이블 */}
|
|
<DepartmentTree
|
|
departments={departments}
|
|
expandedIds={expandedIds}
|
|
selectedIds={selectedIds}
|
|
onToggleExpand={handleToggleExpand}
|
|
onToggleSelect={handleToggleSelect}
|
|
onToggleSelectAll={handleToggleSelectAll}
|
|
onAdd={handleAdd}
|
|
onEdit={handleEdit}
|
|
onDelete={handleDelete}
|
|
/>
|
|
</div>
|
|
|
|
{/* 추가/수정 다이얼로그 */}
|
|
<DepartmentDialog
|
|
isOpen={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
mode={dialogMode}
|
|
parentDepartment={parentDepartment}
|
|
department={selectedDepartment}
|
|
onSubmit={handleDialogSubmit}
|
|
/>
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<DeleteConfirmDialog
|
|
open={deleteDialogOpen}
|
|
onOpenChange={setDeleteDialogOpen}
|
|
description={
|
|
<>
|
|
{isBulkDelete
|
|
? `선택한 부서 ${selectedIds.size}개를 삭제하시겠습니까?`
|
|
: `"${departmentToDelete?.name}" 부서를 삭제하시겠습니까?`
|
|
}
|
|
<br />
|
|
<span className="text-destructive">
|
|
삭제된 부서의 인원은 회사(기본) 인원으로 변경됩니다.
|
|
</span>
|
|
</>
|
|
}
|
|
onConfirm={confirmDelete}
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
} |