refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import type { Process, ProcessFormData, ClassificationRule, IndividualItem, ProcessStep } from '@/types/process';
|
||||
|
||||
// ============================================================================
|
||||
@@ -201,6 +200,8 @@ function transformFrontendToApi(data: ProcessFormData): Record<string, unknown>
|
||||
// API 함수
|
||||
// ============================================================================
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 공정 목록 조회
|
||||
*/
|
||||
@@ -211,244 +212,113 @@ export async function getProcessList(params?: {
|
||||
status?: string;
|
||||
process_type?: string;
|
||||
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.process_type) searchParams.set('process_type', params.process_type);
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.size) searchParams.set('size', String(params.size));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.process_type) searchParams.set('process_type', params.process_type);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes?${searchParams.toString()}`,
|
||||
{ method: 'GET', cache: 'no-store' }
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<ApiProcess>> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const transformed = result.data.data.map(transformApiToFrontend);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: transformed,
|
||||
total: result.data.total,
|
||||
page: result.data.current_page,
|
||||
totalPages: result.data.last_page,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getProcessList] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction<PaginatedResponse<ApiProcess>>({
|
||||
url: `${API_URL}/api/v1/processes?${searchParams.toString()}`,
|
||||
errorMessage: '공정 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformApiToFrontend),
|
||||
total: result.data.total,
|
||||
page: result.data.current_page,
|
||||
totalPages: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 상세 조회
|
||||
*/
|
||||
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${id}`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getProcessById] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}`,
|
||||
transform: (data: ApiProcess) => transformApiToFrontend(data),
|
||||
errorMessage: '공정 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 생성
|
||||
*/
|
||||
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createProcess] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes`,
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
errorMessage: '공정 등록에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 수정
|
||||
*/
|
||||
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[updateProcess] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}`,
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
errorMessage: '공정 수정에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 삭제
|
||||
*/
|
||||
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
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('[deleteProcess] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '공정 삭제에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 일괄 삭제
|
||||
*/
|
||||
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ ids: ids.map((id) => parseInt(id, 10)) }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ deleted_count: number }> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, deletedCount: result.data.deleted_count };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteProcesses] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction<{ deleted_count: number }>({
|
||||
url: `${API_URL}/api/v1/processes`,
|
||||
method: 'DELETE',
|
||||
body: { ids: ids.map((id) => parseInt(id, 10)) },
|
||||
errorMessage: '공정 일괄 삭제에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return { success: true, deletedCount: result.data.deleted_count };
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 상태 토글
|
||||
*/
|
||||
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${id}/toggle`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiProcess> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToFrontend(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[toggleProcessActive] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${id}/toggle`,
|
||||
method: 'PATCH',
|
||||
transform: (d: ApiProcess) => transformApiToFrontend(d),
|
||||
errorMessage: '공정 상태 변경에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,42 +330,23 @@ export async function getProcessOptions(): Promise<{
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '옵션 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Array<{ id: number; process_code: string; process_name: string; process_type: string; department: string }>> =
|
||||
await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '옵션 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data.map((item) => ({
|
||||
id: String(item.id),
|
||||
processCode: item.process_code,
|
||||
processName: item.process_name,
|
||||
processType: item.process_type,
|
||||
department: item.department ?? '',
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getProcessOptions] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
interface ApiOptionItem { id: number; process_code: string; process_name: string; process_type: string; department: string }
|
||||
const result = await executeServerAction<ApiOptionItem[]>({
|
||||
url: `${API_URL}/api/v1/processes/options`,
|
||||
errorMessage: '공정 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return {
|
||||
success: true,
|
||||
data: result.data.map((item) => ({
|
||||
id: String(item.id),
|
||||
processCode: item.process_code,
|
||||
processName: item.process_name,
|
||||
processType: item.process_type,
|
||||
department: item.department ?? '',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -507,40 +358,22 @@ export async function getProcessStats(): Promise<{
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/stats`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ total: number; active: number; inactive: number; by_type: Record<string, number> }> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '통계 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
total: result.data.total,
|
||||
active: result.data.active,
|
||||
inactive: result.data.inactive,
|
||||
byType: result.data.by_type,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getProcessStats] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
interface ApiStats { total: number; active: number; inactive: number; by_type: Record<string, number> }
|
||||
const result = await executeServerAction<ApiStats>({
|
||||
url: `${API_URL}/api/v1/processes/stats`,
|
||||
errorMessage: '공정 통계 조회에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
total: result.data.total,
|
||||
active: result.data.active,
|
||||
inactive: result.data.inactive,
|
||||
byType: result.data.by_type,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -557,50 +390,30 @@ export interface DepartmentOption {
|
||||
* 부서 목록 조회
|
||||
*/
|
||||
export async function getDepartmentOptions(): Promise<DepartmentOption[]> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response?.ok) {
|
||||
// 기본 부서 옵션 반환
|
||||
return [
|
||||
{ id: 'default-1', value: '생산부', label: '생산부' },
|
||||
{ id: 'default-2', value: '품질관리부', label: '품질관리부' },
|
||||
{ id: 'default-3', value: '물류부', label: '물류부' },
|
||||
{ id: 'default-4', value: '영업부', label: '영업부' },
|
||||
];
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.data?.data) {
|
||||
// 중복 부서명 제거 (같은 이름이 여러 개일 경우 첫 번째만 사용)
|
||||
const seenNames = new Set<string>();
|
||||
return result.data.data
|
||||
.filter((dept: { id: number; name: string }) => {
|
||||
if (seenNames.has(dept.name)) {
|
||||
return false;
|
||||
}
|
||||
seenNames.add(dept.name);
|
||||
return true;
|
||||
})
|
||||
.map((dept: { id: number; name: string }) => ({
|
||||
id: String(dept.id),
|
||||
value: dept.name,
|
||||
label: dept.name,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getDepartmentOptions] Error:', error);
|
||||
return [];
|
||||
}
|
||||
const defaultOptions: DepartmentOption[] = [
|
||||
{ id: 'default-1', value: '생산부', label: '생산부' },
|
||||
{ id: 'default-2', value: '품질관리부', label: '품질관리부' },
|
||||
{ id: 'default-3', value: '물류부', label: '물류부' },
|
||||
{ id: 'default-4', value: '영업부', label: '영업부' },
|
||||
];
|
||||
interface DeptResponse { data: Array<{ id: number; name: string }> }
|
||||
const result = await executeServerAction<DeptResponse>({
|
||||
url: `${API_URL}/api/v1/departments`,
|
||||
errorMessage: '부서 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return defaultOptions;
|
||||
const seenNames = new Set<string>();
|
||||
return result.data.data
|
||||
.filter((dept) => {
|
||||
if (seenNames.has(dept.name)) return false;
|
||||
seenNames.add(dept.name);
|
||||
return true;
|
||||
})
|
||||
.map((dept) => ({
|
||||
id: String(dept.id),
|
||||
value: dept.name,
|
||||
label: dept.name,
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -636,76 +449,38 @@ interface GetItemListParams {
|
||||
* - 파라미터에 processName, processCategory 필터 추가 필요 (공정/구분 필터링용)
|
||||
*/
|
||||
export async function getItemList(params?: GetItemListParams): Promise<ItemOption[]> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('size', String(params?.size || 1000));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.itemType) searchParams.set('item_type', params.itemType);
|
||||
if (params?.excludeProcessId) searchParams.set('exclude_process_id', params.excludeProcessId);
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('size', String(params?.size || 1000));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.itemType) searchParams.set('item_type', params.itemType);
|
||||
if (params?.excludeProcessId) searchParams.set('exclude_process_id', params.excludeProcessId);
|
||||
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response?.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success && result.data?.data) {
|
||||
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.item_code || '',
|
||||
id: String(item.id),
|
||||
fullName: item.name,
|
||||
type: item.item_type_name || item.item_type || '',
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getItemList] Error:', error);
|
||||
return [];
|
||||
}
|
||||
interface ItemListResponse { data: Array<{ id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string }> }
|
||||
const result = await executeServerAction<ItemListResponse>({
|
||||
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
|
||||
errorMessage: '품목 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return [];
|
||||
return result.data.data.map((item) => ({
|
||||
value: String(item.id),
|
||||
label: item.name,
|
||||
code: item.item_code || '',
|
||||
id: String(item.id),
|
||||
fullName: item.name,
|
||||
type: item.item_type_name || item.item_type || '',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 유형 옵션 조회 (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 [];
|
||||
}
|
||||
const result = await executeServerAction<Array<{ code: string; name: string }>>({
|
||||
url: `${API_URL}/api/v1/settings/common/item_type`,
|
||||
errorMessage: '품목 유형 옵션 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return [];
|
||||
return result.data.map((item) => ({ value: item.code, label: item.name }));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -753,32 +528,12 @@ export async function getProcessSteps(processId: string): Promise<{
|
||||
data?: ProcessStep[];
|
||||
error?: string;
|
||||
}> {
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction<ApiProcessStep[]>({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps`,
|
||||
errorMessage: '공정 단계 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return { success: true, data: result.data.map(transformStepApiToFrontend) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -789,32 +544,12 @@ export async function getProcessStepById(processId: string, stepId: string): Pro
|
||||
data?: ProcessStep;
|
||||
error?: string;
|
||||
}> {
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
transform: (d: ApiProcessStep) => transformStepApiToFrontend(d),
|
||||
errorMessage: '공정 단계 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -824,8 +559,10 @@ export async function createProcessStep(
|
||||
processId: string,
|
||||
data: Omit<ProcessStep, 'id'>
|
||||
): Promise<{ success: boolean; data?: ProcessStep; error?: string }> {
|
||||
try {
|
||||
const apiData = {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
step_name: data.stepName,
|
||||
is_required: data.isRequired,
|
||||
needs_approval: data.needsApproval,
|
||||
@@ -834,33 +571,11 @@ export async function createProcessStep(
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
},
|
||||
transform: (d: ApiProcessStep) => transformStepApiToFrontend(d),
|
||||
errorMessage: '공정 단계 등록에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -871,42 +586,24 @@ export async function updateProcessStep(
|
||||
stepId: string,
|
||||
data: Partial<ProcessStep>
|
||||
): Promise<{ success: boolean; data?: ProcessStep; error?: string }> {
|
||||
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 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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: (d: ApiProcessStep) => transformStepApiToFrontend(d),
|
||||
errorMessage: '공정 단계 수정에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,32 +613,12 @@ export async function deleteProcessStep(
|
||||
processId: string,
|
||||
stepId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/${stepId}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '공정 단계 삭제에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -951,37 +628,17 @@ export async function reorderProcessSteps(
|
||||
processId: string,
|
||||
steps: { id: string; order: number }[]
|
||||
): Promise<{ success: boolean; data?: ProcessStep[]; error?: string }> {
|
||||
try {
|
||||
const apiData = {
|
||||
const result = await executeServerAction<ApiProcessStep[]>({
|
||||
url: `${API_URL}/api/v1/processes/${processId}/steps/reorder`,
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
},
|
||||
errorMessage: '공정 단계 순서 변경에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return { success: true, data: result.data.map(transformStepApiToFrontend) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user