feat: [생산지시] 목록/상세 페이지 API 연동

- types.ts: API/프론트 타입 정의 (ProductionOrder, Detail, BOM 타입)
- actions.ts: Server Actions (getProductionOrders, getProductionOrderStats, getProductionOrderDetail)
  - executePaginatedAction + buildApiUrl 패턴 적용
  - snake_case → camelCase 변환 함수
- 목록 page.tsx: 샘플데이터 → API 연동
  - 서버사이드 페이지네이션 (clientSideFiltering: false)
  - stats API로 탭 카운트 동적 반영
  - ProgressSteps 동적화 (statusCode 기반)
  - 생산지시번호 → 수주번호로 변경 (별도 PO 번호 없음)
- 상세 page.tsx: 샘플데이터 → API 연동
  - getProductionOrderDetail() API 호출
  - createProductionOrder() orders/actions.ts에서 재사용
  - BOM null 처리 (빈 상태 표시)
  - WorkOrder 상태 배지 확장 (6종: unassigned~shipped)
This commit is contained in:
2026-03-05 16:41:53 +09:00
parent bec933b3b4
commit fa7efb7b24
4 changed files with 518 additions and 725 deletions

View File

@@ -0,0 +1,105 @@
'use server';
import { executePaginatedAction } from '@/lib/api/execute-paginated-action';
import { executeServerAction } from '@/lib/api/execute-server-action';
import { buildApiUrl } from '@/lib/api/query-params';
import type {
ApiProductionOrder,
ApiProductionOrderDetail,
ProductionOrder,
ProductionOrderDetail,
ProductionOrderStats,
ProductionOrderListParams,
ProductionStatus,
} from './types';
// ===== 변환 함수 =====
function transformApiToFrontend(data: ApiProductionOrder): ProductionOrder {
return {
id: String(data.id),
orderNumber: data.order_no,
siteName: data.site_name || '',
clientName: data.client_name || data.client?.name || '',
quantity: data.quantity,
deliveryDate: data.delivery_date || '',
productionOrderedAt: data.production_ordered_at || '',
productionStatus: data.production_status,
workOrderCount: data.work_orders_count,
workOrderProgress: {
total: data.work_order_progress?.total || 0,
completed: data.work_order_progress?.completed || 0,
inProgress: data.work_order_progress?.in_progress || 0,
},
};
}
function transformDetailApiToFrontend(data: ApiProductionOrderDetail): ProductionOrderDetail {
const order = transformApiToFrontend(data.order);
return {
...order,
productionOrderedAt: data.production_ordered_at || order.productionOrderedAt,
productionStatus: data.production_status || order.productionStatus,
workOrderProgress: {
total: data.work_order_progress?.total || 0,
completed: data.work_order_progress?.completed || 0,
inProgress: data.work_order_progress?.in_progress || 0,
},
workOrders: (data.work_orders || []).map((wo) => ({
id: wo.id,
workOrderNo: wo.work_order_no,
processName: wo.process_name,
quantity: wo.quantity,
status: wo.status,
assignees: wo.assignees || [],
})),
bomProcessGroups: (data.bom_process_groups || []).map((group) => ({
processName: group.process_name,
sizeSpec: group.size_spec,
items: (group.items || []).map((item) => ({
id: item.id,
itemCode: item.item_code,
itemName: item.item_name,
spec: item.spec,
lotNo: item.lot_no,
requiredQty: item.required_qty,
qty: item.qty,
})),
})),
};
}
// ===== Server Actions =====
// 목록 조회
export async function getProductionOrders(params: ProductionOrderListParams) {
return executePaginatedAction<ApiProductionOrder, ProductionOrder>({
url: buildApiUrl('/api/v1/production-orders', {
search: params.search,
production_status: params.productionStatus,
sort_by: params.sortBy,
sort_dir: params.sortDir,
page: params.page,
per_page: params.perPage,
}),
transform: transformApiToFrontend,
errorMessage: '생산지시 목록 조회에 실패했습니다.',
});
}
// 상태별 통계
export async function getProductionOrderStats() {
return executeServerAction<ProductionOrderStats>({
url: buildApiUrl('/api/v1/production-orders/stats'),
errorMessage: '생산지시 통계 조회에 실패했습니다.',
});
}
// 상세 조회
export async function getProductionOrderDetail(orderId: string) {
return executeServerAction<ApiProductionOrderDetail, ProductionOrderDetail>({
url: buildApiUrl(`/api/v1/production-orders/${orderId}`),
transform: transformDetailApiToFrontend,
errorMessage: '생산지시 상세 조회에 실패했습니다.',
});
}

View File

@@ -0,0 +1,133 @@
// 생산지시 상태 (프론트 탭용)
export type ProductionStatus = 'waiting' | 'in_production' | 'completed';
// API 응답 타입 (snake_case)
export interface ApiProductionOrder {
id: number;
order_no: string;
site_name: string;
client_name: string;
quantity: number;
delivery_date: string | null;
status_code: string;
production_ordered_at: string | null;
production_status: ProductionStatus;
work_orders_count: number;
work_order_progress: {
total: number;
completed: number;
in_progress: number;
};
client?: {
id: number;
name: string;
};
}
// 프론트 타입 (camelCase)
export interface ProductionOrder {
id: string;
orderNumber: string;
siteName: string;
clientName: string;
quantity: number;
deliveryDate: string;
productionOrderedAt: string;
productionStatus: ProductionStatus;
workOrderCount: number;
workOrderProgress: {
total: number;
completed: number;
inProgress: number;
};
}
// 생산지시 통계
export interface ProductionOrderStats {
total: number;
waiting: number;
in_production: number;
completed: number;
}
// 생산지시 상세 API 응답
export interface ApiProductionOrderDetail {
order: ApiProductionOrder;
production_ordered_at: string | null;
production_status: ProductionStatus;
work_order_progress: {
total: number;
completed: number;
in_progress: number;
};
work_orders: ApiProductionWorkOrder[];
bom_process_groups: ApiBomProcessGroup[];
}
// 상세 내 작업지시 정보
export interface ApiProductionWorkOrder {
id: number;
work_order_no: string;
process_name: string;
quantity: number;
status: string;
assignees: string[];
}
// BOM 공정 분류
export interface ApiBomProcessGroup {
process_name: string;
size_spec?: string;
items: ApiBomItem[];
}
export interface ApiBomItem {
id: number | null;
item_code: string;
item_name: string;
spec: string;
lot_no: string;
required_qty: number;
qty: number;
}
// 프론트 상세 타입
export interface ProductionOrderDetail extends ProductionOrder {
workOrders: ProductionWorkOrder[];
bomProcessGroups: BomProcessGroup[];
}
export interface ProductionWorkOrder {
id: number;
workOrderNo: string;
processName: string;
quantity: number;
status: string;
assignees: string[];
}
export interface BomProcessGroup {
processName: string;
sizeSpec?: string;
items: BomItem[];
}
export interface BomItem {
id: number | null;
itemCode: string;
itemName: string;
spec: string;
lotNo: string;
requiredQty: number;
qty: number;
}
// 조회 파라미터
export interface ProductionOrderListParams {
search?: string;
productionStatus?: ProductionStatus;
sortBy?: string;
sortDir?: 'asc' | 'desc';
page?: number;
perPage?: number;
}