feat(WEB): 공정 단계 API 연동 - mock 데이터 제거
- actions.ts: 목데이터/스텁 함수를 실제 API 호출로 교체 (CRUD + reorder) - actions.ts: ApiProcessStep 타입 + transformStepApiToFrontend 변환 추가 - actions.ts: ApiProcess에 steps 관계 매핑 추가 - ProcessListClient: 단계 수 workSteps → steps 기반으로 변경 - ProcessDetail: 드래그&드롭 후 reorder API 호출 추가
This commit is contained in:
@@ -19,7 +19,7 @@ import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { getProcessSteps } from './actions';
|
||||
import { getProcessSteps, reorderProcessSteps } from './actions';
|
||||
import type { Process, ProcessStep } from '@/types/process';
|
||||
|
||||
interface ProcessDetailProps {
|
||||
@@ -111,12 +111,19 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
|
||||
const updated = [...prev];
|
||||
const [moved] = updated.splice(dragIndex, 1);
|
||||
updated.splice(dropIndex, 0, moved);
|
||||
// 순서 재할당
|
||||
return updated.map((step, i) => ({ ...step, order: i + 1 }));
|
||||
const reordered = updated.map((step, i) => ({ ...step, order: i + 1 }));
|
||||
|
||||
// API 순서 변경 호출
|
||||
reorderProcessSteps(
|
||||
process.id,
|
||||
reordered.map((s) => ({ id: s.id, order: s.order }))
|
||||
);
|
||||
|
||||
return reordered;
|
||||
});
|
||||
|
||||
handleDragEnd();
|
||||
}, [dragIndex, handleDragEnd]);
|
||||
}, [dragIndex, handleDragEnd, process.id]);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
|
||||
@@ -329,7 +329,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
<TableCell className="font-medium">{process.processCode}</TableCell>
|
||||
<TableCell>{process.processName}</TableCell>
|
||||
<TableCell>{process.department}</TableCell>
|
||||
<TableCell className="text-center">{process.workSteps.length}</TableCell>
|
||||
<TableCell className="text-center">{process.steps?.length ?? 0}</TableCell>
|
||||
<TableCell className="text-center">{itemCount > 0 ? itemCount : '-'}</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Badge
|
||||
@@ -383,7 +383,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<InfoField label="담당부서" value={process.department} />
|
||||
<InfoField label="단계" value={`${process.workSteps.length}개`} />
|
||||
<InfoField label="단계" value={`${process.steps?.length ?? 0}개`} />
|
||||
<InfoField label="품목" value={itemCount > 0 ? `${itemCount}개` : '-'} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ interface ApiProcess {
|
||||
updated_at: string;
|
||||
classification_rules?: ApiClassificationRule[];
|
||||
process_items?: ApiProcessItem[];
|
||||
steps?: ApiProcessStep[];
|
||||
}
|
||||
|
||||
interface ApiClassificationRule {
|
||||
@@ -93,6 +94,7 @@ function transformApiToFrontend(apiData: ApiProcess): Process {
|
||||
requiredWorkers: apiData.required_workers,
|
||||
equipmentInfo: apiData.equipment_info ?? undefined,
|
||||
workSteps: apiData.work_steps ?? [],
|
||||
steps: (apiData.steps ?? []).map(transformStepApiToFrontend),
|
||||
note: apiData.note ?? undefined,
|
||||
status: apiData.is_active ? '사용중' : '미사용',
|
||||
createdAt: apiData.created_at,
|
||||
@@ -644,13 +646,13 @@ export async function getItemList(params?: GetItemListParams): Promise<ItemOptio
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.data?.data) {
|
||||
return result.data.data.map((item: { id: number; name: string; code?: string; item_type?: string }) => ({
|
||||
return result.data.data.map((item: { id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string }) => ({
|
||||
value: String(item.id),
|
||||
label: item.name,
|
||||
code: item.code || '',
|
||||
code: item.item_code || '',
|
||||
id: String(item.id),
|
||||
fullName: item.name,
|
||||
type: item.item_type || '',
|
||||
type: item.item_type_name || item.item_type || '',
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -662,151 +664,313 @@ export async function getItemList(params?: GetItemListParams): Promise<ItemOptio
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 유형 옵션 조회 (common_codes에서 동적 조회)
|
||||
*/
|
||||
export async function getItemTypeOptions(): Promise<Array<{ value: string; label: string }>> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/common/item_type`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response?.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && Array.isArray(result.data)) {
|
||||
return result.data.map((item: { code: string; name: string }) => ({
|
||||
value: item.code,
|
||||
label: item.name,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getItemTypeOptions] Error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 공정 단계 (Process Step) - 목데이터 및 스텁 함수
|
||||
// 백엔드 API 미준비 → 프론트엔드 목데이터로 운영
|
||||
// 공정 단계 (Process Step) API
|
||||
// ============================================================================
|
||||
|
||||
const MOCK_STEPS: Record<string, ProcessStep[]> = {
|
||||
// processId별로 목데이터 보유
|
||||
default: [
|
||||
{
|
||||
id: 'step-1',
|
||||
stepCode: 'STP-001',
|
||||
stepName: '자재투입',
|
||||
isRequired: true,
|
||||
needsApproval: false,
|
||||
needsInspection: false,
|
||||
isActive: true,
|
||||
order: 1,
|
||||
connectionType: '팝업',
|
||||
connectionTarget: '입고완료 자재 목록',
|
||||
completionType: '선택 완료 시 완료',
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
stepCode: 'STP-002',
|
||||
stepName: '미싱',
|
||||
isRequired: true,
|
||||
needsApproval: false,
|
||||
needsInspection: false,
|
||||
isActive: true,
|
||||
order: 2,
|
||||
connectionType: '없음',
|
||||
completionType: '클릭 시 완료',
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
stepCode: 'STP-003',
|
||||
stepName: '중간검사',
|
||||
isRequired: false,
|
||||
needsApproval: true,
|
||||
needsInspection: true,
|
||||
isActive: true,
|
||||
order: 3,
|
||||
connectionType: '없음',
|
||||
completionType: '클릭 시 완료',
|
||||
},
|
||||
{
|
||||
id: 'step-4',
|
||||
stepCode: 'STP-004',
|
||||
stepName: '포장',
|
||||
isRequired: true,
|
||||
needsApproval: false,
|
||||
needsInspection: false,
|
||||
isActive: true,
|
||||
order: 4,
|
||||
connectionType: '없음',
|
||||
completionType: '클릭 시 완료',
|
||||
},
|
||||
],
|
||||
};
|
||||
interface ApiProcessStep {
|
||||
id: number;
|
||||
process_id: number;
|
||||
step_code: string;
|
||||
step_name: string;
|
||||
is_required: boolean;
|
||||
needs_approval: boolean;
|
||||
needs_inspection: boolean;
|
||||
is_active: boolean;
|
||||
sort_order: number;
|
||||
connection_type: string | null;
|
||||
connection_target: string | null;
|
||||
completion_type: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
function transformStepApiToFrontend(apiStep: ApiProcessStep): ProcessStep {
|
||||
return {
|
||||
id: String(apiStep.id),
|
||||
stepCode: apiStep.step_code,
|
||||
stepName: apiStep.step_name,
|
||||
isRequired: apiStep.is_required,
|
||||
needsApproval: apiStep.needs_approval,
|
||||
needsInspection: apiStep.needs_inspection,
|
||||
isActive: apiStep.is_active,
|
||||
order: apiStep.sort_order,
|
||||
connectionType: (apiStep.connection_type as ProcessStep['connectionType']) || '없음',
|
||||
connectionTarget: apiStep.connection_target ?? undefined,
|
||||
completionType: (apiStep.completion_type as ProcessStep['completionType']) || '클릭 시 완료',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 단계 목록 조회 (목데이터)
|
||||
* 공정 단계 목록 조회
|
||||
*/
|
||||
export async function getProcessSteps(processId: string): Promise<{
|
||||
success: boolean;
|
||||
data?: ProcessStep[];
|
||||
error?: string;
|
||||
}> {
|
||||
// 목데이터 반환
|
||||
const steps = MOCK_STEPS[processId] || MOCK_STEPS['default'];
|
||||
return { success: true, data: steps };
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps`,
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '단계 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcessStep[]> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '단계 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data.map(transformStepApiToFrontend) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getProcessSteps] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 단계 상세 조회 (목데이터)
|
||||
* 공정 단계 상세 조회
|
||||
*/
|
||||
export async function getProcessStepById(processId: string, stepId: string): Promise<{
|
||||
success: boolean;
|
||||
data?: ProcessStep;
|
||||
error?: string;
|
||||
}> {
|
||||
const steps = MOCK_STEPS[processId] || MOCK_STEPS['default'];
|
||||
const step = steps.find((s) => s.id === stepId);
|
||||
if (!step) {
|
||||
return { success: false, error: '단계를 찾을 수 없습니다.' };
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '단계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcessStep> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '단계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformStepApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getProcessStepById] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
return { success: true, data: step };
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 단계 생성 (스텁)
|
||||
* 공정 단계 생성
|
||||
*/
|
||||
export async function createProcessStep(
|
||||
_processId: string,
|
||||
processId: string,
|
||||
data: Omit<ProcessStep, 'id'>
|
||||
): Promise<{ success: boolean; data?: ProcessStep; error?: string }> {
|
||||
const newStep: ProcessStep = {
|
||||
...data,
|
||||
id: `step-${Date.now()}`,
|
||||
};
|
||||
return { success: true, data: newStep };
|
||||
try {
|
||||
const apiData = {
|
||||
step_name: data.stepName,
|
||||
is_required: data.isRequired,
|
||||
needs_approval: data.needsApproval,
|
||||
needs_inspection: data.needsInspection,
|
||||
is_active: data.isActive,
|
||||
connection_type: data.connectionType || null,
|
||||
connection_target: data.connectionTarget || null,
|
||||
completion_type: data.completionType || null,
|
||||
};
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps`,
|
||||
{ method: 'POST', body: JSON.stringify(apiData) }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '단계 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcessStep> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '단계 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformStepApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createProcessStep] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 단계 수정 (스텁)
|
||||
* 공정 단계 수정
|
||||
*/
|
||||
export async function updateProcessStep(
|
||||
_processId: string,
|
||||
processId: string,
|
||||
stepId: string,
|
||||
data: Partial<ProcessStep>
|
||||
): Promise<{ success: boolean; data?: ProcessStep; error?: string }> {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: stepId,
|
||||
stepCode: data.stepCode || '',
|
||||
stepName: data.stepName || '',
|
||||
isRequired: data.isRequired ?? false,
|
||||
needsApproval: data.needsApproval ?? false,
|
||||
needsInspection: data.needsInspection ?? false,
|
||||
isActive: data.isActive ?? true,
|
||||
order: data.order ?? 0,
|
||||
connectionType: data.connectionType || '없음',
|
||||
connectionTarget: data.connectionTarget,
|
||||
completionType: data.completionType || '클릭 시 완료',
|
||||
},
|
||||
};
|
||||
try {
|
||||
const apiData: Record<string, unknown> = {};
|
||||
if (data.stepName !== undefined) apiData.step_name = data.stepName;
|
||||
if (data.isRequired !== undefined) apiData.is_required = data.isRequired;
|
||||
if (data.needsApproval !== undefined) apiData.needs_approval = data.needsApproval;
|
||||
if (data.needsInspection !== undefined) apiData.needs_inspection = data.needsInspection;
|
||||
if (data.isActive !== undefined) apiData.is_active = data.isActive;
|
||||
if (data.connectionType !== undefined) apiData.connection_type = data.connectionType || null;
|
||||
if (data.connectionTarget !== undefined) apiData.connection_target = data.connectionTarget || null;
|
||||
if (data.completionType !== undefined) apiData.completion_type = data.completionType || null;
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
{ method: 'PUT', body: JSON.stringify(apiData) }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '단계 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcessStep> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '단계 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformStepApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[updateProcessStep] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 단계 삭제 (스텁)
|
||||
* 공정 단계 삭제
|
||||
*/
|
||||
export async function deleteProcessStep(
|
||||
_processId: string,
|
||||
_stepId: string
|
||||
processId: string,
|
||||
stepId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return { success: true };
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '단계 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<null> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '단계 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteProcessStep] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 단계 순서 변경 (스텁)
|
||||
* 공정 단계 순서 변경
|
||||
*/
|
||||
export async function reorderProcessSteps(
|
||||
_processId: string,
|
||||
_stepIds: string[]
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return { success: true };
|
||||
processId: string,
|
||||
steps: { id: string; order: number }[]
|
||||
): Promise<{ success: boolean; data?: ProcessStep[]; error?: string }> {
|
||||
try {
|
||||
const apiData = {
|
||||
items: steps.map((s) => ({
|
||||
id: parseInt(s.id, 10),
|
||||
sort_order: s.order,
|
||||
})),
|
||||
};
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/reorder`,
|
||||
{ method: 'PATCH', body: JSON.stringify(apiData) }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcessStep[]> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '순서 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data.map(transformStepApiToFrontend) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[reorderProcessSteps] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user