refactor(work-orders): process_type을 process_id FK로 변환
- types.ts: processId, processName, processCode 추가, transform 함수 구현 - actions.ts: getProcessOptions() 추가, CRUD에 transform 적용 - WorkOrderCreate.tsx: 공정 목록 API 동적 로딩 - WorkOrderList.tsx: processName 표시로 변경 - WorkOrderDetail.tsx: processName 표시, processType은 로직용 유지
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* API 연동 완료 (2025-12-26)
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, FileText, X, Edit2, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -25,8 +25,8 @@ import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { SalesOrderSelectModal } from './SalesOrderSelectModal';
|
||||
import { AssigneeSelectModal } from './AssigneeSelectModal';
|
||||
import { toast } from 'sonner';
|
||||
import { createWorkOrder } from './actions';
|
||||
import { PROCESS_TYPE_LABELS, type ProcessType, type SalesOrder } from './types';
|
||||
import { createWorkOrder, getProcessOptions, type ProcessOption } from './actions';
|
||||
import { type SalesOrder } from './types';
|
||||
|
||||
// Validation 에러 타입
|
||||
interface ValidationErrors {
|
||||
@@ -38,6 +38,7 @@ const FIELD_NAME_MAP: Record<string, string> = {
|
||||
selectedOrder: '수주',
|
||||
client: '발주처',
|
||||
projectName: '현장명',
|
||||
processId: '공정',
|
||||
shipmentDate: '출고예정일',
|
||||
};
|
||||
|
||||
@@ -55,7 +56,7 @@ interface FormData {
|
||||
itemCount: number;
|
||||
|
||||
// 작업지시 정보
|
||||
processType: ProcessType;
|
||||
processId: number | null; // 공정 ID (FK → processes.id)
|
||||
shipmentDate: string;
|
||||
priority: number;
|
||||
assignees: string[];
|
||||
@@ -71,7 +72,7 @@ const initialFormData: FormData = {
|
||||
projectName: '',
|
||||
orderNo: '',
|
||||
itemCount: 0,
|
||||
processType: 'screen',
|
||||
processId: null,
|
||||
shipmentDate: '',
|
||||
priority: 5,
|
||||
assignees: [],
|
||||
@@ -87,6 +88,27 @@ export function WorkOrderCreate() {
|
||||
const [assigneeNames, setAssigneeNames] = useState<string[]>([]);
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [processOptions, setProcessOptions] = useState<ProcessOption[]>([]);
|
||||
const [isLoadingProcesses, setIsLoadingProcesses] = useState(true);
|
||||
|
||||
// 공정 옵션 로드
|
||||
useEffect(() => {
|
||||
async function loadProcessOptions() {
|
||||
setIsLoadingProcesses(true);
|
||||
const result = await getProcessOptions();
|
||||
if (result.success) {
|
||||
setProcessOptions(result.data);
|
||||
// 첫 번째 공정을 기본값으로 설정
|
||||
if (result.data.length > 0 && !formData.processId) {
|
||||
setFormData(prev => ({ ...prev, processId: result.data[0].id }));
|
||||
}
|
||||
} else {
|
||||
toast.error(result.error || '공정 목록을 불러오는데 실패했습니다.');
|
||||
}
|
||||
setIsLoadingProcesses(false);
|
||||
}
|
||||
loadProcessOptions();
|
||||
}, []);
|
||||
|
||||
// 수주 선택 핸들러
|
||||
const handleSelectOrder = (order: SalesOrder) => {
|
||||
@@ -104,7 +126,7 @@ export function WorkOrderCreate() {
|
||||
const handleClearOrder = () => {
|
||||
setFormData({
|
||||
...initialFormData,
|
||||
processType: formData.processType,
|
||||
processId: formData.processId,
|
||||
shipmentDate: formData.shipmentDate,
|
||||
priority: formData.priority,
|
||||
});
|
||||
@@ -128,6 +150,10 @@ export function WorkOrderCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!formData.processId) {
|
||||
errors.processId = '공정을 선택해주세요';
|
||||
}
|
||||
|
||||
if (!formData.shipmentDate) {
|
||||
errors.shipmentDate = '출고예정일을 선택해주세요';
|
||||
}
|
||||
@@ -149,7 +175,7 @@ export function WorkOrderCreate() {
|
||||
const result = await createWorkOrder({
|
||||
salesOrderId: formData.selectedOrder?.id ? parseInt(formData.selectedOrder.id) : undefined,
|
||||
projectName: formData.projectName,
|
||||
processType: formData.processType,
|
||||
processId: formData.processId!, // 공정 ID (FK → processes.id)
|
||||
scheduledDate: formData.shipmentDate,
|
||||
assigneeIds: formData.assignees.map(id => parseInt(id)),
|
||||
memo: formData.note || undefined,
|
||||
@@ -174,14 +200,10 @@ export function WorkOrderCreate() {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 공정 코드 표시
|
||||
const getProcessCode = (type: ProcessType) => {
|
||||
const codes: Record<ProcessType, string> = {
|
||||
screen: 'P-001 | 작업일지: WL-SCR',
|
||||
slat: 'P-002 | 작업일지: WL-SLT',
|
||||
bending: 'P-003 | 작업일지: WL-FLD',
|
||||
};
|
||||
return codes[type];
|
||||
// 선택된 공정의 코드 가져오기
|
||||
const getSelectedProcessCode = (): string => {
|
||||
const selectedProcess = processOptions.find(p => p.id === formData.processId);
|
||||
return selectedProcess?.processCode || '-';
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -396,22 +418,23 @@ export function WorkOrderCreate() {
|
||||
<div className="space-y-2">
|
||||
<Label>공정구분 *</Label>
|
||||
<Select
|
||||
value={formData.processType}
|
||||
onValueChange={(value) => setFormData({ ...formData, processType: value as ProcessType })}
|
||||
value={formData.processId?.toString() || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
|
||||
disabled={isLoadingProcesses}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue />
|
||||
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정을 선택하세요'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(PROCESS_TYPE_LABELS).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{label}
|
||||
{processOptions.map((process) => (
|
||||
<SelectItem key={process.id} value={process.id.toString()}>
|
||||
{process.processName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
공정코드: {getProcessCode(formData.processType)}
|
||||
공정코드: {getSelectedProcessCode()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import { WorkLogModal } from '../WorkerScreen/WorkLogModal';
|
||||
import { toast } from 'sonner';
|
||||
import { getWorkOrderById, updateWorkOrderStatus } from './actions';
|
||||
import {
|
||||
PROCESS_TYPE_LABELS,
|
||||
WORK_ORDER_STATUS_LABELS,
|
||||
WORK_ORDER_STATUS_COLORS,
|
||||
ITEM_STATUS_LABELS,
|
||||
@@ -344,7 +343,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">공정구분</p>
|
||||
<p className="font-medium">{PROCESS_TYPE_LABELS[order.processType]}</p>
|
||||
<p className="font-medium">{order.processName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">작업상태</p>
|
||||
|
||||
@@ -23,7 +23,6 @@ import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard
|
||||
import { toast } from 'sonner';
|
||||
import { getWorkOrders, getWorkOrderStats } from './actions';
|
||||
import {
|
||||
PROCESS_TYPE_LABELS,
|
||||
WORK_ORDER_STATUS_LABELS,
|
||||
WORK_ORDER_STATUS_COLORS,
|
||||
type WorkOrder,
|
||||
@@ -248,7 +247,7 @@ export function WorkOrderList() {
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
||||
<TableCell className="font-medium">{order.workOrderNo}</TableCell>
|
||||
<TableCell>{PROCESS_TYPE_LABELS[order.processType]}</TableCell>
|
||||
<TableCell>{order.processName}</TableCell>
|
||||
<TableCell>{order.lotNo}</TableCell>
|
||||
<TableCell>{order.orderDate}</TableCell>
|
||||
<TableCell className="text-center">{order.isAssigned ? 'Y' : '-'}</TableCell>
|
||||
@@ -297,7 +296,7 @@ export function WorkOrderList() {
|
||||
}
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<InfoField label="공정" value={PROCESS_TYPE_LABELS[order.processType]} />
|
||||
<InfoField label="공정" value={order.processName} />
|
||||
<InfoField label="로트번호" value={order.lotNo} />
|
||||
<InfoField label="발주처" value={order.client} />
|
||||
<InfoField label="작업자" value={order.assignee || '-'} />
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
WorkOrder,
|
||||
WorkOrderStats,
|
||||
WorkOrderStatus,
|
||||
ProcessType,
|
||||
WorkOrderApiPaginatedResponse,
|
||||
WorkOrderStatsApi,
|
||||
} from './types';
|
||||
@@ -47,7 +46,7 @@ export async function getWorkOrders(params?: {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
status?: WorkOrderStatus | 'all';
|
||||
processType?: ProcessType | 'all';
|
||||
processId?: number | 'all'; // 공정 ID (FK → processes.id)
|
||||
search?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
@@ -71,8 +70,8 @@ export async function getWorkOrders(params?: {
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.processType && params.processType !== 'all') {
|
||||
searchParams.set('process_type', params.processType);
|
||||
if (params?.processId && params.processId !== 'all') {
|
||||
searchParams.set('process_id', String(params.processId));
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
@@ -727,3 +726,65 @@ export async function getDepartmentsWithUsers(): Promise<{
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 공정 목록 조회 (작업지시 생성용) =====
|
||||
export interface ProcessOption {
|
||||
id: number;
|
||||
processCode: string;
|
||||
processName: string;
|
||||
}
|
||||
|
||||
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('[WorkOrderActions] GET process options:', url);
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[WorkOrderActions] GET process options 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 응답 변환
|
||||
const processes: ProcessOption[] = (result.data || []).map(
|
||||
(item: {
|
||||
id: number;
|
||||
process_code: string;
|
||||
process_name: string;
|
||||
}) => ({
|
||||
id: item.id,
|
||||
processCode: item.process_code,
|
||||
processName: item.process_name,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: processes,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkOrderActions] getProcessOptions error:', error);
|
||||
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
* 작업지시 관리 타입 정의
|
||||
*/
|
||||
|
||||
// 공정 구분
|
||||
// 공정 정보 (API 관계)
|
||||
export interface ProcessInfo {
|
||||
id: number;
|
||||
process_code: string;
|
||||
process_name: string;
|
||||
}
|
||||
|
||||
// @deprecated process_type은 process_id FK로 변경됨
|
||||
// 하위 호환성을 위해 유지
|
||||
export type ProcessType = 'screen' | 'slat' | 'bending';
|
||||
|
||||
export const PROCESS_TYPE_LABELS: Record<ProcessType, string> = {
|
||||
@@ -139,7 +147,11 @@ export interface WorkOrder {
|
||||
id: string;
|
||||
workOrderNo: string; // 작업지시번호 (KD-WO-251217-12)
|
||||
lotNo: string; // 로트번호 (KD-TS-251217-10)
|
||||
processType: ProcessType; // 공정구분
|
||||
processId: number; // 공정 ID (FK)
|
||||
processName: string; // 공정명 (표시용)
|
||||
processCode: string; // 공정코드 (표시용)
|
||||
/** @deprecated process_id FK 사용 */
|
||||
processType: ProcessType; // 하위 호환용
|
||||
status: WorkOrderStatus; // 작업상태
|
||||
|
||||
// 기본 정보
|
||||
@@ -272,7 +284,7 @@ export interface WorkOrderApi {
|
||||
work_order_no: string;
|
||||
sales_order_id: number | null;
|
||||
project_name: string | null;
|
||||
process_type: 'screen' | 'slat' | 'bending';
|
||||
process_id: number; // FK to processes.id
|
||||
status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped';
|
||||
assignee_id: number | null;
|
||||
team_id: number | null;
|
||||
@@ -290,6 +302,11 @@ export interface WorkOrderApi {
|
||||
order_no: string;
|
||||
client?: { id: number; name: string };
|
||||
};
|
||||
process?: {
|
||||
id: number;
|
||||
process_code: string;
|
||||
process_name: string;
|
||||
};
|
||||
assignee?: { id: number; name: string };
|
||||
assignees?: WorkOrderAssigneeApi[];
|
||||
team?: { id: number; name: string };
|
||||
@@ -333,11 +350,24 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
|
||||
const primaryAssignee = assignees.find(a => a.isPrimary);
|
||||
const assigneeName = primaryAssignee?.name || api.assignee?.name || '-';
|
||||
|
||||
// 공정명 → 하위호환용 processType 매핑
|
||||
const processNameToType = (name: string): ProcessType => {
|
||||
const mapping: Record<string, ProcessType> = {
|
||||
'스크린': 'screen',
|
||||
'슬랫': 'slat',
|
||||
'절곡': 'bending',
|
||||
};
|
||||
return mapping[name] || 'screen';
|
||||
};
|
||||
|
||||
return {
|
||||
id: String(api.id),
|
||||
workOrderNo: api.work_order_no,
|
||||
lotNo: api.sales_order?.order_no || '-',
|
||||
processType: api.process_type,
|
||||
processId: api.process_id,
|
||||
processName: api.process?.process_name || '-',
|
||||
processCode: api.process?.process_code || '-',
|
||||
processType: processNameToType(api.process?.process_name || ''), // 하위 호환
|
||||
status: api.status,
|
||||
client: api.sales_order?.client?.name || '-',
|
||||
projectName: api.project_name || '-',
|
||||
@@ -414,11 +444,11 @@ function getStatusStep(status: WorkOrderStatus): number {
|
||||
}
|
||||
|
||||
// Frontend → API 변환 (등록/수정용)
|
||||
export function transformFrontendToApi(data: Partial<WorkOrder>): Record<string, unknown> {
|
||||
export function transformFrontendToApi(data: Partial<WorkOrder> & { processId?: number }): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
if (data.projectName !== undefined) result.project_name = data.projectName;
|
||||
if (data.processType !== undefined) result.process_type = data.processType;
|
||||
if (data.processId !== undefined) result.process_id = data.processId;
|
||||
if (data.status !== undefined) result.status = data.status;
|
||||
if (data.scheduledDate !== undefined) result.scheduled_date = data.scheduledDate;
|
||||
if (data.dueDate !== undefined && data.scheduledDate === undefined) result.scheduled_date = data.dueDate;
|
||||
|
||||
Reference in New Issue
Block a user