/** * 품목 목록 및 테이블 컬럼 관리 훅 * * - 품목 목록 API 조회 (서버 사이드 검색/필터링) * - custom-tabs 기반 동적 테이블 컬럼 */ 'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import type { ItemMaster, ItemType, PartType } from '@/types/item'; import type { CustomTabResponse, TabColumnResponse } from '@/types/item-master-api'; import { itemMasterApi } from '@/lib/api/item-master'; // 기본 테이블 컬럼 설정 (API 응답 실패 시 폴백용) const defaultTableColumns = [ { key: 'item_code', label: '품목코드', visible: true, order: 0 }, { key: 'item_type', label: '품목유형', visible: true, order: 1 }, { key: 'item_name', label: '품목명', visible: true, order: 2 }, { key: 'specification', label: '규격', visible: true, order: 3 }, { key: 'unit', label: '단위', visible: true, order: 4 }, { key: 'is_active', label: '상태', visible: true, order: 5 }, ]; export interface TableColumn { key: string; label: string; visible: boolean; order: number; } export interface ItemListFilters { search?: string; type?: string; page?: number; size?: number; } export interface PaginationInfo { currentPage: number; totalPages: number; totalItems: number; perPage: number; } export interface ItemStats { total: number; byType: Record; } export interface TotalStats { totalAll: number; // 전체 품목 수 (필터 무관) totalFG: number; // 제품 수 totalPT: number; // 부품 수 totalSM: number; // 부자재 수 totalRM: number; // 원자재 수 totalCS: number; // 소모품 수 } export interface UseItemListResult { // 데이터 items: ItemMaster[]; tabs: CustomTabResponse[]; columns: TableColumn[]; pagination: PaginationInfo; totalStats: TotalStats; // 필터 무관한 전체 통계 // 상태 isLoading: boolean; isSearching: boolean; error: string | null; // 액션 refresh: () => Promise; search: (filters: ItemListFilters) => Promise; setActiveTab: (tabId: number | null) => void; activeTabId: number | null; } export function useItemList(): UseItemListResult { // 데이터 상태 const [items, setItems] = useState([]); const [tabs, setTabs] = useState([]); const [tabColumns, setTabColumns] = useState>({}); const [pagination, setPagination] = useState({ currentPage: 1, totalPages: 1, totalItems: 0, perPage: 20, }); // 전체 통계 (필터와 무관하게 유지) const [totalStats, setTotalStats] = useState({ totalAll: 0, totalFG: 0, totalPT: 0, totalSM: 0, totalRM: 0, totalCS: 0, }); // UI 상태 const [activeTabId, setActiveTabId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSearching, setIsSearching] = useState(false); const [error, setError] = useState(null); // 현재 필터 상태 (ref로 관리하여 불필요한 리렌더링 방지) const currentFilters = useRef({}); // 현재 탭의 컬럼 설정 const columns: TableColumn[] = activeTabId && tabColumns[activeTabId] ? tabColumns[activeTabId].map((col, idx) => ({ key: col.key, label: col.label, visible: col.visible, order: col.order ?? idx, })) : defaultTableColumns; // API 응답을 프론트엔드 타입으로 변환하는 함수 const mapItemResponse = (item: Record): ItemMaster => { // 디버깅: API 응답에서 id 필드 확인 const itemId = item.id ?? item.item_id ?? item.ID; if (!itemId) { console.warn('[useItemList] id 필드 누락:', { availableKeys: Object.keys(item), item }); } return { id: String(itemId ?? ''), itemCode: (item.code ?? item.item_code ?? '') as string, itemName: (item.name ?? item.item_name ?? '') as string, itemType: (item.type_code ?? item.item_type ?? '') as ItemType, partType: item.part_type as PartType | undefined, unit: (item.unit ?? '') as string, specification: (item.specification ?? '') as string, // is_active가 null/undefined면 deleted_at 기준으로 판단 (삭제 안됐으면 활성) // deleted_at이 없거나 null이면 활성, 값이 있으면 비활성 isActive: item.is_active != null ? Boolean(item.is_active) : !item.deleted_at, category1: (item.category1 ?? '') as string, category2: (item.category2 ?? '') as string, category3: (item.category3 ?? '') as string, salesPrice: (item.sales_price ?? 0) as number, purchasePrice: (item.purchase_price ?? 0) as number, currentRevision: (item.current_revision ?? 0) as number, isFinal: Boolean(item.is_final ?? false), createdAt: (item.created_at ?? '') as string, updatedAt: (item.updated_at ?? '') as string, }; }; // 전체 통계 조회 (필터 없이 전체 데이터 기준) const fetchTotalStats = useCallback(async () => { try { // 각 유형별로 병렬 조회 const [allResponse, fgResponse, ptResponse, smResponse, rmResponse, csResponse] = await Promise.all([ fetch('/api/proxy/items?group_id=1&size=1'), // 전체 (품목관리 그룹) fetch('/api/proxy/items?type=FG&size=1'), // 제품 fetch('/api/proxy/items?type=PT&size=1'), // 부품 fetch('/api/proxy/items?type=SM&size=1'), // 부자재 fetch('/api/proxy/items?type=RM&size=1'), // 원자재 fetch('/api/proxy/items?type=CS&size=1'), // 소모품 ]); const [allResult, fgResult, ptResult, smResult, rmResult, csResult] = await Promise.all([ allResponse.json(), fgResponse.json(), ptResponse.json(), smResponse.json(), rmResponse.json(), csResponse.json(), ]); return { totalAll: allResult.data?.total ?? 0, totalFG: fgResult.data?.total ?? 0, totalPT: ptResult.data?.total ?? 0, totalSM: smResult.data?.total ?? 0, totalRM: rmResult.data?.total ?? 0, totalCS: csResult.data?.total ?? 0, }; } catch (err) { console.error('전체 통계 조회 실패:', err); return { totalAll: 0, totalFG: 0, totalPT: 0, totalSM: 0, totalRM: 0, totalCS: 0 }; } }, []); // 품목 목록 조회 (필터 적용) const fetchItems = useCallback(async (filters: ItemListFilters = {}) => { // URL 쿼리 파라미터 생성 const params = new URLSearchParams(); if (filters.search && filters.search.trim()) { params.append('search', filters.search.trim()); } // 타입별 조회 vs 전체 조회 if (filters.type && filters.type !== 'all') { // 특정 타입 조회: type 파라미터 사용 params.append('type', filters.type); } else { // 전체 조회: group_id=1 (품목관리 그룹) params.append('group_id', '1'); } if (filters.page) { params.append('page', String(filters.page)); } if (filters.size) { params.append('size', String(filters.size)); } const queryString = params.toString(); const url = `/api/proxy/items${queryString ? `?${queryString}` : ''}`; const itemsResponse = await fetch(url); const itemsResult = await itemsResponse.json(); if (itemsResult.success) { // API 응답 구조: { success, data: { data: [...], current_page, total, per_page, last_page } } const paginatedData = itemsResult.data; const rawItems = paginatedData?.data ?? paginatedData ?? []; if (!Array.isArray(rawItems)) { return { items: [], pagination: { currentPage: 1, totalPages: 1, totalItems: 0, perPage: 20 } }; } const mappedItems = rawItems.map(mapItemResponse); return { items: mappedItems, pagination: { currentPage: paginatedData?.current_page ?? 1, totalPages: paginatedData?.last_page ?? 1, totalItems: paginatedData?.total ?? mappedItems.length, perPage: paginatedData?.per_page ?? 20, }, }; } else { throw new Error(itemsResult.message || '품목 목록을 불러올 수 없습니다.'); } }, []); // 초기 데이터 로드 const loadData = useCallback(async () => { setIsLoading(true); setError(null); try { // 1. 품목기준관리 init API 호출 (탭 + 컬럼 설정 포함) const initData = await itemMasterApi.init(); if (initData.customTabs) { setTabs(initData.customTabs); // 기본 탭 설정 const defaultTab = initData.customTabs.find((t) => t.is_default); if (defaultTab) { setActiveTabId(defaultTab.id); } else if (initData.customTabs.length > 0) { setActiveTabId(initData.customTabs[0].id); } } if (initData.tabColumns) { setTabColumns(initData.tabColumns); } // 2. 품목 목록 API 호출 + 전체 통계 병렬 조회 const [result, stats] = await Promise.all([ fetchItems(currentFilters.current), fetchTotalStats(), ]); setItems(result.items); setPagination(result.pagination); setTotalStats(stats); } catch (err) { console.error('품목 목록 로드 실패:', err); // API 실패 시 에러만 표시 setItems([]); setTabs([]); setTabColumns({}); setPagination({ currentPage: 1, totalPages: 1, totalItems: 0, perPage: 20 }); setError(err instanceof Error ? err.message : '데이터 로드 실패'); } finally { setIsLoading(false); } }, [fetchItems, fetchTotalStats]); // 검색/필터 적용 const search = useCallback(async (filters: ItemListFilters) => { setIsSearching(true); setError(null); currentFilters.current = { ...currentFilters.current, ...filters }; try { const result = await fetchItems(currentFilters.current); setItems(result.items); setPagination(result.pagination); } catch (err) { console.error('품목 검색 실패:', err); setError(err instanceof Error ? err.message : '검색 실패'); } finally { setIsSearching(false); } }, [fetchItems]); // 초기 로드 useEffect(() => { loadData(); }, [loadData]); // 탭 변경 const setActiveTab = useCallback((tabId: number | null) => { setActiveTabId(tabId); }, []); return { items, tabs, columns, pagination, totalStats, isLoading, isSearching, error, refresh: loadData, search, setActiveTab, activeTabId, }; }