feat: [부서관리] 기능 보완 - 필드 확장, 검색/필터, UI 개선
- Department 타입에 code, description, isActive, sortOrder 필드 추가 - DepartmentDialog: Zod + react-hook-form 폼 검증 (5개 필드) - DepartmentToolbar: 상태 필터(전체/활성/비활성) + 검색 기능 - DepartmentTree: 트리 필터링 (검색어 + 상태) - DepartmentTreeItem: 코드 Badge, 부서명 볼드, 설명 표시, 체크박스 크기 조정 - convertApiToLocal에서 누락 필드 매핑 복원
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,7 +14,19 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import type { DepartmentDialogProps } from './types';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { DepartmentDialogProps, DepartmentFormData } from './types';
|
||||
|
||||
const departmentFormSchema = z.object({
|
||||
code: z.string().min(1, '부서 코드를 입력하세요').max(50, '50자 이내로 입력하세요'),
|
||||
name: z.string().min(1, '부서명을 입력하세요').max(100, '100자 이내로 입력하세요'),
|
||||
description: z.string().max(500, '500자 이내로 입력하세요').default(''),
|
||||
sortOrder: z.coerce.number().min(0, '0 이상 입력하세요').default(0),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof departmentFormSchema>;
|
||||
|
||||
/**
|
||||
* 부서 추가/수정 다이얼로그
|
||||
@@ -22,27 +37,59 @@ export function DepartmentDialog({
|
||||
mode,
|
||||
parentDepartment,
|
||||
department,
|
||||
onSubmit
|
||||
onSubmit,
|
||||
}: DepartmentDialogProps) {
|
||||
const [name, setName] = useState('');
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(departmentFormSchema),
|
||||
defaultValues: {
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isActive = watch('isActive');
|
||||
|
||||
// 다이얼로그 열릴 때 초기값 설정
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (mode === 'edit' && department) {
|
||||
setName(department.name);
|
||||
reset({
|
||||
code: department.code || '',
|
||||
name: department.name,
|
||||
description: department.description || '',
|
||||
sortOrder: department.sortOrder,
|
||||
isActive: department.isActive,
|
||||
});
|
||||
} else {
|
||||
setName('');
|
||||
reset({
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isOpen, mode, department]);
|
||||
}, [isOpen, mode, department, reset]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (name.trim()) {
|
||||
onSubmit(name.trim());
|
||||
setName('');
|
||||
}
|
||||
const onFormSubmit = (data: FormData) => {
|
||||
onSubmit({
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description || '',
|
||||
sortOrder: data.sortOrder,
|
||||
isActive: data.isActive,
|
||||
} as DepartmentFormData);
|
||||
};
|
||||
|
||||
const title = mode === 'add' ? '부서 추가' : '부서 수정';
|
||||
@@ -50,12 +97,12 @@ export function DepartmentDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className="space-y-4 py-4">
|
||||
{/* 부모 부서 표시 (추가 모드일 때) */}
|
||||
{mode === 'add' && parentDepartment && (
|
||||
@@ -64,16 +111,78 @@ export function DepartmentDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 부서명 입력 */}
|
||||
{/* 부서 코드 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department-name">부서명</Label>
|
||||
<Label htmlFor="department-code">
|
||||
부서 코드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="department-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="부서명을 입력하세요"
|
||||
id="department-code"
|
||||
{...register('code')}
|
||||
placeholder="예: DEV, SALES, HR"
|
||||
autoFocus
|
||||
/>
|
||||
{errors.code && (
|
||||
<p className="text-sm text-destructive">{errors.code.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 부서명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department-name">
|
||||
부서명 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="department-name"
|
||||
{...register('name')}
|
||||
placeholder="부서명을 입력하세요"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-destructive">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 설명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="department-description">설명</Label>
|
||||
<Textarea
|
||||
id="department-description"
|
||||
{...register('description')}
|
||||
placeholder="부서 설명을 입력하세요"
|
||||
rows={3}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="text-sm text-destructive">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 정렬순서 + 활성상태 (가로 배치) */}
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="space-y-2 flex-1">
|
||||
<Label htmlFor="department-sort-order">정렬순서</Label>
|
||||
<Input
|
||||
id="department-sort-order"
|
||||
type="number"
|
||||
{...register('sortOrder')}
|
||||
min={0}
|
||||
/>
|
||||
{errors.sortOrder && (
|
||||
<p className="text-sm text-destructive">{errors.sortOrder.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>활성 상태</Label>
|
||||
<div className="flex items-center gap-2 h-9">
|
||||
<Switch
|
||||
checked={isActive}
|
||||
onCheckedChange={(checked) => setValue('isActive', checked)}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{isActive ? '활성' : '비활성'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +190,7 @@ export function DepartmentDialog({
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={!name.trim()}>
|
||||
<Button type="submit">
|
||||
{submitText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -89,4 +198,4 @@ export function DepartmentDialog({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,58 @@
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Search, Plus, Trash2 } from 'lucide-react';
|
||||
import type { DepartmentToolbarProps } from './types';
|
||||
|
||||
/**
|
||||
* 검색 + 추가/삭제 버튼 툴바
|
||||
* 검색 + 필터 + 추가/삭제 버튼 툴바
|
||||
*/
|
||||
export function DepartmentToolbar({
|
||||
totalCount,
|
||||
selectedCount,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
statusFilter,
|
||||
onStatusFilterChange,
|
||||
onAdd,
|
||||
onDelete
|
||||
onDelete,
|
||||
}: DepartmentToolbarProps) {
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
{/* 검색창 */}
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="부서명 검색"
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
{/* 검색 + 필터 */}
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
{/* 검색창 */}
|
||||
<div className="relative flex-1 sm:w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="부서명, 코드 검색"
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 상태 필터 */}
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onValueChange={(value) => onStatusFilterChange(value as 'all' | 'active' | 'inactive')}
|
||||
>
|
||||
<SelectTrigger className="w-28 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="active">활성</SelectItem>
|
||||
<SelectItem value="inactive">비활성</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 선택 카운트 + 버튼 */}
|
||||
@@ -54,4 +81,4 @@ export function DepartmentToolbar({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function DepartmentTree({
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
<span className="font-medium text-sm">부서명</span>
|
||||
<span className="font-medium text-sm">코드 / 부서명</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { memo } from 'react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ChevronRight, ChevronDown, Plus, SquarePen, Trash2 } from 'lucide-react';
|
||||
import type { DepartmentTreeItemProps } from './types';
|
||||
|
||||
@@ -56,11 +57,35 @@ export const DepartmentTreeItem = memo(function DepartmentTreeItem({
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => onToggleSelect(department.id)}
|
||||
aria-label={`${department.name} 선택`}
|
||||
className="shrink-0"
|
||||
className="shrink-0 size-[18px]"
|
||||
/>
|
||||
|
||||
{/* 부서 코드 */}
|
||||
<span
|
||||
className="w-20 shrink-0 cursor-pointer"
|
||||
onClick={() => onToggleSelect(department.id)}
|
||||
>
|
||||
{department.code && (
|
||||
<Badge variant="outline" className="text-xs font-mono rounded-sm w-16 justify-center">
|
||||
{department.code}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* 부서명 */}
|
||||
<span className="break-words">{department.name}</span>
|
||||
<span className="w-20 shrink-0 font-bold truncate">{department.name}</span>
|
||||
|
||||
{/* 설명 */}
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{department.description || ''}
|
||||
</span>
|
||||
|
||||
{/* 상태 뱃지 */}
|
||||
{!department.isActive && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
비활성
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 작업 버튼 (선택 시 부서명 아래에 표시, 데스크톱: 호버 시에도 표시) */}
|
||||
|
||||
@@ -9,8 +9,8 @@ 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 type { Department, DepartmentFormData, StatusFilter } from './types';
|
||||
import { countAllDepartments, getAllDepartmentIds, findDepartmentById, filterDepartmentTree } from './types';
|
||||
import {
|
||||
getDepartmentTree,
|
||||
createDepartment,
|
||||
@@ -27,8 +27,12 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
function convertApiToLocal(record: DepartmentRecord): Department {
|
||||
return {
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
description: record.description,
|
||||
parentId: record.parentId,
|
||||
isActive: record.isActive,
|
||||
sortOrder: record.sortOrder,
|
||||
depth: record.depth,
|
||||
children: record.children ? record.children.map(convertApiToLocal) : [],
|
||||
};
|
||||
@@ -51,6 +55,9 @@ export function DepartmentManagement() {
|
||||
// 검색어
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 상태 필터
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||
|
||||
/**
|
||||
* 부서 트리 조회 API
|
||||
*/
|
||||
@@ -92,11 +99,20 @@ export function DepartmentManagement() {
|
||||
const [departmentToDelete, setDepartmentToDelete] = useState<Department | null>(null);
|
||||
const [isBulkDelete, setIsBulkDelete] = useState(false);
|
||||
|
||||
// 전체 부서 수 계산
|
||||
// 필터링된 부서 트리
|
||||
const filteredDepartments = useMemo(
|
||||
() => filterDepartmentTree(departments, searchQuery, statusFilter),
|
||||
[departments, searchQuery, statusFilter],
|
||||
);
|
||||
|
||||
// 전체 부서 수 (필터 전)
|
||||
const totalCount = useMemo(() => countAllDepartments(departments), [departments]);
|
||||
|
||||
// 모든 부서 ID
|
||||
const allIds = useMemo(() => getAllDepartmentIds(departments), [departments]);
|
||||
// 필터된 부서 수
|
||||
const filteredCount = useMemo(() => countAllDepartments(filteredDepartments), [filteredDepartments]);
|
||||
|
||||
// 필터된 부서 ID
|
||||
const filteredAllIds = useMemo(() => getAllDepartmentIds(filteredDepartments), [filteredDepartments]);
|
||||
|
||||
// 펼침/접힘 토글
|
||||
const handleToggleExpand = (id: number) => {
|
||||
@@ -124,12 +140,12 @@ export function DepartmentManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
// 전체 선택/해제
|
||||
// 전체 선택/해제 (필터된 부서 기준)
|
||||
const handleToggleSelectAll = () => {
|
||||
if (selectedIds.size === allIds.length) {
|
||||
if (selectedIds.size === filteredAllIds.length) {
|
||||
setSelectedIds(new Set());
|
||||
} else {
|
||||
setSelectedIds(new Set(allIds));
|
||||
setSelectedIds(new Set(filteredAllIds));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -225,18 +241,19 @@ export function DepartmentManagement() {
|
||||
|
||||
/**
|
||||
* 부서 추가/수정 제출 핸들러 (API 연동)
|
||||
* @note parentId는 현재 API에서 미지원 - 모든 부서가 최상위로 생성됨
|
||||
*/
|
||||
const handleDialogSubmit = useCallback(async (name: string) => {
|
||||
const handleDialogSubmit = useCallback(async (formData: DepartmentFormData) => {
|
||||
if (isProcessing) return;
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
if (dialogMode === 'add') {
|
||||
// 새 부서 추가 API
|
||||
// NOTE: parentId는 현재 API에서 지원하지 않아 최상위에만 생성됨
|
||||
const result = await createDepartment({
|
||||
name,
|
||||
code: formData.code,
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
sortOrder: formData.sortOrder,
|
||||
isActive: formData.isActive,
|
||||
parentId: parentDepartment?.id,
|
||||
});
|
||||
if (result.success) {
|
||||
@@ -245,8 +262,13 @@ export function DepartmentManagement() {
|
||||
console.error('[DepartmentManagement] 부서 생성 실패:', result.error);
|
||||
}
|
||||
} else if (dialogMode === 'edit' && selectedDepartment) {
|
||||
// 부서 수정 API
|
||||
const result = await updateDepartment(selectedDepartment.id, { name });
|
||||
const result = await updateDepartment(selectedDepartment.id, {
|
||||
code: formData.code,
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
sortOrder: formData.sortOrder,
|
||||
isActive: formData.isActive,
|
||||
});
|
||||
if (result.success) {
|
||||
await fetchDepartments();
|
||||
} else {
|
||||
@@ -262,6 +284,9 @@ export function DepartmentManagement() {
|
||||
}
|
||||
}, [dialogMode, parentDepartment, selectedDepartment, isProcessing, fetchDepartments]);
|
||||
|
||||
// 필터 활성 여부
|
||||
const isFiltered = searchQuery.trim() !== '' || statusFilter !== 'all';
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
@@ -274,19 +299,21 @@ export function DepartmentManagement() {
|
||||
{/* 전체 부서 카운트 */}
|
||||
<DepartmentStats totalCount={totalCount} />
|
||||
|
||||
{/* 검색 + 추가/삭제 버튼 */}
|
||||
{/* 검색 + 필터 + 추가/삭제 버튼 */}
|
||||
<DepartmentToolbar
|
||||
totalCount={totalCount}
|
||||
totalCount={isFiltered ? filteredCount : totalCount}
|
||||
selectedCount={selectedIds.size}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
statusFilter={statusFilter}
|
||||
onStatusFilterChange={setStatusFilter}
|
||||
onAdd={handleBulkAdd}
|
||||
onDelete={handleBulkDelete}
|
||||
/>
|
||||
|
||||
{/* 트리 테이블 */}
|
||||
<DepartmentTree
|
||||
departments={departments}
|
||||
departments={filteredDepartments}
|
||||
expandedIds={expandedIds}
|
||||
selectedIds={selectedIds}
|
||||
onToggleExpand={handleToggleExpand}
|
||||
@@ -328,4 +355,4 @@ export function DepartmentManagement() {
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,27 @@
|
||||
*/
|
||||
export interface Department {
|
||||
id: number;
|
||||
code: string | null;
|
||||
name: string;
|
||||
description: string | null;
|
||||
parentId: number | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
depth: number; // 깊이 (0: 최상위, 1, 2, 3, ... 무제한)
|
||||
children?: Department[]; // 하위 부서 (재귀 - 무제한 깊이)
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 폼 데이터
|
||||
*/
|
||||
export interface DepartmentFormData {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 추가/수정 다이얼로그 Props
|
||||
*/
|
||||
@@ -23,7 +38,7 @@ export interface DepartmentDialogProps {
|
||||
mode: 'add' | 'edit';
|
||||
parentDepartment?: Department; // 추가 시 부모 부서
|
||||
department?: Department; // 수정 시 대상 부서
|
||||
onSubmit: (name: string) => void;
|
||||
onSubmit: (data: DepartmentFormData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +63,11 @@ export interface DepartmentStatsProps {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 필터 타입
|
||||
*/
|
||||
export type StatusFilter = 'all' | 'active' | 'inactive';
|
||||
|
||||
/**
|
||||
* 툴바 Props
|
||||
*/
|
||||
@@ -56,6 +76,8 @@ export interface DepartmentToolbarProps {
|
||||
selectedCount: number;
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
statusFilter: StatusFilter;
|
||||
onStatusFilterChange: (filter: StatusFilter) => void;
|
||||
onAdd: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
@@ -107,3 +129,45 @@ export const findDepartmentById = (departments: Department[], id: number): Depar
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 트리 필터링 유틸리티 (재귀 — 검색/상태 필터)
|
||||
* 자식이 매칭되면 부모도 유지
|
||||
*/
|
||||
export const filterDepartmentTree = (
|
||||
departments: Department[],
|
||||
searchQuery: string,
|
||||
statusFilter: StatusFilter,
|
||||
): Department[] => {
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
|
||||
const filterNode = (dept: Department): Department | null => {
|
||||
// 자식 먼저 필터링
|
||||
const filteredChildren = dept.children
|
||||
? dept.children.map(filterNode).filter(Boolean) as Department[]
|
||||
: [];
|
||||
|
||||
// 현재 노드가 매칭되는지 확인
|
||||
const matchesSearch = !query ||
|
||||
dept.name.toLowerCase().includes(query) ||
|
||||
(dept.code && dept.code.toLowerCase().includes(query));
|
||||
|
||||
const matchesStatus = statusFilter === 'all' ||
|
||||
(statusFilter === 'active' && dept.isActive) ||
|
||||
(statusFilter === 'inactive' && !dept.isActive);
|
||||
|
||||
// 자식이 매칭되면 부모도 유지
|
||||
if (filteredChildren.length > 0) {
|
||||
return { ...dept, children: filteredChildren };
|
||||
}
|
||||
|
||||
// 현재 노드가 매칭되면 유지
|
||||
if (matchesSearch && matchesStatus) {
|
||||
return { ...dept, children: [] };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return departments.map(filterNode).filter(Boolean) as Department[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user