diff --git a/next-build_0113.tar.gz b/next-build_0113.tar.gz deleted file mode 100644 index 96988077..00000000 Binary files a/next-build_0113.tar.gz and /dev/null differ diff --git a/src/components/production/ProductionDashboard/actions.ts b/src/components/production/ProductionDashboard/actions.ts index 5635e21f..5814b2ae 100644 --- a/src/components/production/ProductionDashboard/actions.ts +++ b/src/components/production/ProductionDashboard/actions.ts @@ -2,6 +2,7 @@ * 생산 현황판 서버 액션 * API 연동 완료 (2025-12-26) * serverFetch 마이그레이션 (2025-12-30) + * 공정 기반 동적 탭 전환 (2025-01-15) */ 'use server'; @@ -9,14 +10,19 @@ import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; -import type { WorkOrder, WorkerStatus, ProcessType, DashboardStats } from './types'; +import type { WorkOrder, WorkerStatus, DashboardStats, ProcessOption } from './types'; // ===== API 타입 ===== interface WorkOrderApiItem { id: number; work_order_no: string; project_name: string | null; - process_type: 'screen' | 'slat' | 'bending'; + process_id: number | null; + process?: { + id: number; + process_code: string; + process_name: string; + }; status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped'; scheduled_date: string | null; memo: string | null; @@ -67,7 +73,8 @@ function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder { id: String(api.id), orderNo: api.work_order_no, productName, - process: api.process_type, + processCode: api.process?.process_code || '-', + processName: api.process?.process_name || '-', client: api.sales_order?.client?.name || '-', projectName: api.project_name || '-', assignees: api.assignee ? [api.assignee.name] : [], @@ -83,8 +90,78 @@ function transformToProductionFormat(api: WorkOrderApiItem): WorkOrder { }; } +// ===== 공정 옵션 목록 조회 ===== +export async function getProcessOptions(): Promise<{ + success: boolean; + data: ProcessOption[]; + error?: string; +}> { + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`; + console.log('[ProductionDashboardActions] GET processes:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + console.warn('[ProductionDashboardActions] GET processes error:', error?.message); + return { + success: false, + data: [], + error: error?.message || '공정 목록 조회에 실패했습니다.', + }; + } + + if (!response.ok) { + console.warn('[ProductionDashboardActions] GET processes error:', response.status); + return { + success: false, + data: [], + error: `API 오류: ${response.status}`, + }; + } + + const result = await response.json(); + + if (!result.success) { + return { + success: false, + data: [], + error: result.message || '공정 목록 조회에 실패했습니다.', + }; + } + + // API 응답: { success, data: [{ id, process_code, process_name, process_type, department }] } + const processes: ProcessOption[] = (result.data || []).map((p: { + id: number; + process_code: string; + process_name: string; + process_type?: string; + department?: string; + }) => ({ + id: p.id, + code: p.process_code, + name: p.process_name, + type: p.process_type, + department: p.department, + })); + + return { + success: true, + data: processes, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[ProductionDashboardActions] getProcessOptions error:', error); + return { + success: false, + data: [], + error: '서버 오류가 발생했습니다.', + }; + } +} + // ===== 대시보드 데이터 조회 ===== -export async function getDashboardData(processType?: ProcessType): Promise<{ +export async function getDashboardData(processCode?: string): Promise<{ success: boolean; workOrders: WorkOrder[]; workerStatus: WorkerStatus[]; @@ -101,8 +178,8 @@ export async function getDashboardData(processType?: ProcessType): Promise<{ try { // 작업지시 목록 조회 const params = new URLSearchParams({ per_page: '100' }); - if (processType && processType !== 'all') { - params.set('process_type', processType); + if (processCode && processCode !== 'all') { + params.set('process_code', processCode); } const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?${params.toString()}`; diff --git a/src/components/production/ProductionDashboard/index.tsx b/src/components/production/ProductionDashboard/index.tsx index f56aa79e..5bd7f29f 100644 --- a/src/components/production/ProductionDashboard/index.tsx +++ b/src/components/production/ProductionDashboard/index.tsx @@ -20,16 +20,18 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; import { toast } from 'sonner'; -import { getDashboardData } from './actions'; -import type { WorkOrder, WorkerStatus, ProcessType, DashboardStats } from './types'; +import { getDashboardData, getProcessOptions } from './actions'; +import type { WorkOrder, WorkerStatus, DashboardStats, ProcessOption, TabOption } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { TAB_OPTIONS, PROCESS_LABELS, STATUS_LABELS } from './types'; +import { STATUS_LABELS } from './types'; export default function ProductionDashboard() { const router = useRouter(); // ===== 상태 관리 ===== - const [selectedTab, setSelectedTab] = useState('all'); + const [selectedTab, setSelectedTab] = useState('all'); + const [processOptions, setProcessOptions] = useState([]); + const [tabOptions, setTabOptions] = useState([{ value: 'all', label: '전체' }]); const [workOrders, setWorkOrders] = useState([]); const [workerStatus, setWorkerStatus] = useState([]); const [stats, setStats] = useState({ @@ -41,13 +43,45 @@ export default function ProductionDashboard() { delayed: 0, }); const [isLoading, setIsLoading] = useState(true); + const [isLoadingProcesses, setIsLoadingProcesses] = useState(true); - // ===== 데이터 로드 ===== + // ===== 공정 목록 로드 (최초 1회) ===== + useEffect(() => { + const loadProcesses = async () => { + setIsLoadingProcesses(true); + try { + const result = await getProcessOptions(); + if (result.success) { + setProcessOptions(result.data); + // 동적 탭 옵션 생성: 전체 + 공정 목록 + const dynamicTabs: TabOption[] = [ + { value: 'all', label: '전체' }, + ...result.data.map((p) => ({ + value: p.code, + label: p.name, + })), + ]; + setTabOptions(dynamicTabs); + } else { + toast.error(result.error || '공정 목록 조회에 실패했습니다.'); + } + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[ProductionDashboard] loadProcesses error:', error); + toast.error('공정 목록 로드 중 오류가 발생했습니다.'); + } finally { + setIsLoadingProcesses(false); + } + }; + loadProcesses(); + }, []); + + // ===== 대시보드 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { - const processType = selectedTab === 'all' ? undefined : selectedTab; - const result = await getDashboardData(processType); + const processCode = selectedTab === 'all' ? undefined : selectedTab; + const result = await getDashboardData(processCode); if (result.success) { setWorkOrders(result.workOrders); @@ -121,14 +155,18 @@ export default function ProductionDashboard() { - {/* 탭 */} - setSelectedTab(v as ProcessType | 'all')}> + {/* 공정별 탭 (동적 생성) */} + - {TAB_OPTIONS.map((tab) => ( - - {tab.label} - - ))} + {isLoadingProcesses ? ( + 전체 + ) : ( + tabOptions.map((tab) => ( + + {tab.label} + + )) + )} @@ -320,7 +358,7 @@ function WorkOrderCard({ order, onClick, showDelay }: WorkOrderCardProps) {
- {PROCESS_LABELS[order.process]} + {order.processName} {showDelay && order.delayDays && (

+{order.delayDays}일 지연

diff --git a/src/components/production/ProductionDashboard/types.ts b/src/components/production/ProductionDashboard/types.ts index 6f146034..a10f9be1 100644 --- a/src/components/production/ProductionDashboard/types.ts +++ b/src/components/production/ProductionDashboard/types.ts @@ -1,15 +1,22 @@ // 작업 지시 상태 export type WorkOrderStatus = 'waiting' | 'inProgress' | 'completed'; -// 공정 타입 -export type ProcessType = 'screen' | 'slat' | 'bending' | 'all'; +// 공정 옵션 (API에서 동적으로 조회) +export interface ProcessOption { + id: number; + code: string; // process_code (P-001, P-002, ...) + name: string; // process_name (슬랫, 스크린, 절곡, ...) + type?: string; // process_type (생산, 검수, ...) + department?: string; // department (경영본부, 개발팀, ...) +} // 작업 지시 export interface WorkOrder { id: string; orderNo: string; // KD-WO-251216-01 productName: string; // 스크린 서터 (표준형) - 추가 - process: ProcessType; // 스크린, 슬랫, 절곡 + processCode: string; // 공정 코드 (P-001, P-002, ...) + processName: string; // 공정명 (슬랫, 스크린, 절곡, ...) client: string; // 삼성물산(주) projectName: string; // 강남 타워 신축현장 assignees: string[]; // 담당자 배열 @@ -43,27 +50,12 @@ export interface DashboardStats { delayed: number; // 지연 } -// 탭 옵션 +// 탭 옵션 (동적 생성용) export interface TabOption { - value: ProcessType | 'all'; - label: string; + value: string; // 'all' 또는 process_code + label: string; // '전체' 또는 process_name } -export const TAB_OPTIONS: TabOption[] = [ - { value: 'all', label: '전체' }, - { value: 'screen', label: '스크린공장' }, - { value: 'slat', label: '슬랫공장' }, - { value: 'bending', label: '절곡공장' }, -]; - -// 공정 타입 라벨 -export const PROCESS_LABELS: Record = { - screen: '스크린', - slat: '슬랫', - bending: '절곡', - all: '전체', -}; - // 상태 라벨 export const STATUS_LABELS: Record = { waiting: '대기',