'use client'; /** * 계정과목 설정 모달 (공용) * * - 계정과목 추가: 코드, 계정과목명, 분류 Select, 추가 버튼 * - 검색: 검색 Input, 분류 필터 Select, 건수 표시 * - 테이블: 코드 | 계정과목명 | 분류 | 부문 | 상태(사용중/미사용 토글) | 작업(삭제) * - 기본 계정과목표 일괄 생성 버튼 * - 버튼: 닫기 */ import { useState, useCallback, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; import { Plus, Trash2, Loader2, Database } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { FormField } from '@/components/molecules/FormField'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { getAccountSubjects, createAccountSubject, updateAccountSubjectStatus, deleteAccountSubject, seedDefaultAccountSubjects, } from './actions'; import type { AccountSubject, AccountSubjectCategory } from './types'; import { ACCOUNT_CATEGORY_OPTIONS, ACCOUNT_CATEGORY_FILTER_OPTIONS, ACCOUNT_CATEGORY_LABELS, DEPARTMENT_TYPE_LABELS, } from './types'; import type { DepartmentType } from './types'; interface AccountSubjectSettingModalProps { open: boolean; onOpenChange: (open: boolean) => void; } export function AccountSubjectSettingModal({ open, onOpenChange, }: AccountSubjectSettingModalProps) { // 추가 폼 const [newCode, setNewCode] = useState(''); const [newName, setNewName] = useState(''); const [newCategory, setNewCategory] = useState('asset'); const [isAdding, setIsAdding] = useState(false); // 검색/필터 const [search, setSearch] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); // 데이터 const [subjects, setSubjects] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSeeding, setIsSeeding] = useState(false); // 삭제 확인 const [deleteTarget, setDeleteTarget] = useState(null); // 데이터 로드 const loadSubjects = useCallback(async () => { setIsLoading(true); try { const result = await getAccountSubjects({ search, category: categoryFilter, }); if (result.success && result.data) { setSubjects(result.data); } } catch { toast.error('계정과목 목록 조회에 실패했습니다.'); } finally { setIsLoading(false); } }, [search, categoryFilter]); useEffect(() => { if (open) { loadSubjects(); } }, [open, loadSubjects]); // 필터링된 목록 const filteredSubjects = useMemo(() => { return subjects.filter((s) => { const matchSearch = !search || s.code.toLowerCase().includes(search.toLowerCase()) || s.name.toLowerCase().includes(search.toLowerCase()); const matchCategory = categoryFilter === 'all' || s.category === categoryFilter; return matchSearch && matchCategory; }); }, [subjects, search, categoryFilter]); // 계정과목 추가 const handleAdd = useCallback(async () => { if (!newCode.trim()) { toast.warning('코드를 입력해주세요.'); return; } if (!newName.trim()) { toast.warning('계정과목명을 입력해주세요.'); return; } setIsAdding(true); try { const result = await createAccountSubject({ code: newCode.trim(), name: newName.trim(), category: newCategory, }); if (result.success) { toast.success('계정과목이 추가되었습니다.'); setNewCode(''); setNewName(''); setNewCategory('asset'); loadSubjects(); } else { toast.error(result.error || '추가에 실패했습니다.'); } } catch { toast.error('추가 중 오류가 발생했습니다.'); } finally { setIsAdding(false); } }, [newCode, newName, newCategory, loadSubjects]); // 상태 토글 const handleToggleStatus = useCallback( async (subject: AccountSubject) => { try { const result = await updateAccountSubjectStatus(subject.id, !subject.isActive); if (result.success) { toast.success( `${subject.name}이(가) ${!subject.isActive ? '사용중' : '미사용'}으로 변경되었습니다.` ); loadSubjects(); } else { toast.error(result.error || '상태 변경에 실패했습니다.'); } } catch { toast.error('상태 변경 중 오류가 발생했습니다.'); } }, [loadSubjects] ); // 삭제 const handleDelete = useCallback(async () => { if (!deleteTarget) return; try { const result = await deleteAccountSubject(deleteTarget.id); if (result.success) { toast.success('계정과목이 삭제되었습니다.'); setDeleteTarget(null); loadSubjects(); } else { toast.error(result.error || '삭제에 실패했습니다.'); } } catch { toast.error('삭제 중 오류가 발생했습니다.'); } }, [deleteTarget, loadSubjects]); // 기본 계정과목표 생성 const handleSeedDefaults = useCallback(async () => { setIsSeeding(true); try { const result = await seedDefaultAccountSubjects(); if (result.success) { const count = result.data?.inserted_count ?? 0; if (count > 0) { toast.success(`기본 계정과목 ${count}건이 생성되었습니다.`); } else { toast.info('이미 모든 기본 계정과목이 등록되어 있습니다.'); } loadSubjects(); } else { toast.error(result.error || '기본 계정과목 생성에 실패했습니다.'); } } catch { toast.error('기본 계정과목 생성 중 오류가 발생했습니다.'); } finally { setIsSeeding(false); } }, [loadSubjects]); // depth에 따른 들여쓰기 const getIndentClass = (depth: number) => { if (depth === 1) return 'font-bold'; if (depth === 2) return 'pl-4 font-medium'; return 'pl-8'; }; return ( <> 계정과목 설정 계정과목을 추가, 검색, 상태변경, 삭제합니다 {/* 추가 영역 */}
{/* 검색/필터 영역 */}
setSearch(e.target.value)} className="w-full sm:max-w-[250px] h-9 text-sm" />
{filteredSubjects.length}건
{/* 테이블 */}
{isLoading ? (
로딩 중...
) : ( 코드 계정과목명 분류 부문 상태 작업 {filteredSubjects.length === 0 ? ( 계정과목이 없습니다. "기본 계정과목 생성" 버튼을 클릭하면 표준 계정과목표가 생성됩니다. ) : ( filteredSubjects.map((subject) => ( {subject.code} {subject.name} {ACCOUNT_CATEGORY_LABELS[subject.category]} {DEPARTMENT_TYPE_LABELS[subject.departmentType as DepartmentType] || '-'} )) )}
)}
{/* 삭제 확인 */} !v && setDeleteTarget(null)}> 계정과목 삭제 "{deleteTarget?.name}" 계정과목을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 취소 삭제 ); }