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:
2026-02-04 13:13:08 +09:00
parent 3500e3f520
commit 8250420d99
3 changed files with 280 additions and 109 deletions

View File

@@ -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>

View File

@@ -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>
}

View File

@@ -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: '서버 오류가 발생했습니다.' };
}
}