feat(WEB): 생산 현황판 공정 기반 동적 탭 전환
- 하드코딩된 공장 탭을 공정 마스터 데이터 기반 동적 탭으로 변경 - getProcessOptions() API 함수 추가 (/api/v1/processes/options) - ProcessOption 타입 및 동적 TabOption 타입 추가 - WorkOrder 타입에 processCode, processName 필드 추가 - 탭 선택 시 process_code로 서버 사이드 필터링 변경 전: 전체/스크린공장/슬랫공장/절곡공장 (하드코딩) 변경 후: 전체 + 공정 마스터 데이터 기반 동적 탭
This commit is contained in:
Binary file not shown.
@@ -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()}`;
|
||||
|
||||
@@ -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<ProcessType | 'all'>('all');
|
||||
const [selectedTab, setSelectedTab] = useState<string>('all');
|
||||
const [processOptions, setProcessOptions] = useState<ProcessOption[]>([]);
|
||||
const [tabOptions, setTabOptions] = useState<TabOption[]>([{ value: 'all', label: '전체' }]);
|
||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||
const [workerStatus, setWorkerStatus] = useState<WorkerStatus[]>([]);
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 */}
|
||||
<Tabs value={selectedTab} onValueChange={(v) => setSelectedTab(v as ProcessType | 'all')}>
|
||||
{/* 공정별 탭 (동적 생성) */}
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabsList>
|
||||
{TAB_OPTIONS.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value} className="px-6">
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
{isLoadingProcesses ? (
|
||||
<TabsTrigger value="all" className="px-6">전체</TabsTrigger>
|
||||
) : (
|
||||
tabOptions.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value} className="px-6">
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))
|
||||
)}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -320,7 +358,7 @@ function WorkOrderCard({ order, onClick, showDelay }: WorkOrderCardProps) {
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{PROCESS_LABELS[order.process]}
|
||||
{order.processName}
|
||||
</Badge>
|
||||
{showDelay && order.delayDays && (
|
||||
<p className="text-xs text-orange-600 mt-1">+{order.delayDays}일 지연</p>
|
||||
|
||||
@@ -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<ProcessType, string> = {
|
||||
screen: '스크린',
|
||||
slat: '슬랫',
|
||||
bending: '절곡',
|
||||
all: '전체',
|
||||
};
|
||||
|
||||
// 상태 라벨
|
||||
export const STATUS_LABELS: Record<WorkOrderStatus, string> = {
|
||||
waiting: '대기',
|
||||
|
||||
Reference in New Issue
Block a user