Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-09 16:14:40 +09:00
85 changed files with 7211 additions and 17638 deletions

View File

@@ -17,8 +17,12 @@
const USE_MOCK_DATA = false;
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { executeServerAction } from '@/lib/api/execute-server-action';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
import type {
ReceivingItem,
ReceivingDetail,
@@ -577,86 +581,29 @@ export async function getReceivings(params?: {
};
}
try {
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.search) searchParams.set('search', params.search);
const queryString = searchParams.toString();
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
const result = await executeServerAction<ReceivingApiPaginatedResponse>({
url: `${API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`,
errorMessage: '입고 목록 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`;
const { response, error } = await serverFetch(url, {
method: 'GET',
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '입고 목록 조회에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '입고 목록 조회에 실패했습니다.',
};
}
const paginatedData: ReceivingApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const receivings = (paginatedData.data || []).map(transformApiToListItem);
return {
success: true,
data: receivings,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getReceivings error:', error);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
};
}
const pd = result.data;
return {
success: true,
data: (pd.data || []).map(transformApiToListItem),
pagination: { currentPage: pd.current_page, lastPage: pd.last_page, perPage: pd.per_page, total: pd.total },
};
}
// ===== 입고 통계 조회 =====
@@ -666,37 +613,15 @@ export async function getReceivingStats(): Promise<{
error?: string;
__authError?: boolean;
}> {
// ===== 목데이터 모드 =====
if (USE_MOCK_DATA) {
return { success: true, data: MOCK_RECEIVING_STATS };
}
if (USE_MOCK_DATA) return { success: true, data: MOCK_RECEIVING_STATS };
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '입고 통계 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToStats(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getReceivingStats error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/stats`,
transform: (data: ReceivingApiStatsResponse) => transformApiToStats(data),
errorMessage: '입고 통계 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 입고 상세 조회 =====
@@ -706,75 +631,34 @@ export async function getReceivingById(id: string): Promise<{
error?: string;
__authError?: boolean;
}> {
// ===== 목데이터 모드 =====
if (USE_MOCK_DATA) {
const detail = MOCK_RECEIVING_DETAIL[id];
if (detail) {
return { success: true, data: detail };
}
return { success: false, error: '입고 정보를 찾을 수 없습니다.' };
return detail ? { success: true, data: detail } : { success: false, error: '입고 정보를 찾을 수 없습니다.' };
}
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '입고 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getReceivingById error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}`,
transform: (data: ReceivingApiData) => transformApiToDetail(data),
errorMessage: '입고 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 입고 등록 =====
export async function createReceiving(
data: Partial<ReceivingDetail>
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings`,
{ 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 = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] createReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const apiData = transformFrontendToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings`,
method: 'POST',
body: apiData,
transform: (d: ReceivingApiData) => transformApiToDetail(d),
errorMessage: '입고 등록에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 입고 수정 =====
@@ -782,66 +666,29 @@ export async function updateReceiving(
id: string,
data: Partial<ReceivingDetail>
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${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 = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] updateReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const apiData = transformFrontendToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}`,
method: 'PUT',
body: apiData,
transform: (d: ReceivingApiData) => transformApiToDetail(d),
errorMessage: '입고 수정에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 입고 삭제 =====
export async function deleteReceiving(
id: string
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
{ method: 'DELETE' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '입고 삭제에 실패했습니다.' };
}
const result = 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('[ReceivingActions] deleteReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}`,
method: 'DELETE',
errorMessage: '입고 삭제에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, error: result.error };
}
// ===== 입고처리 =====
@@ -849,34 +696,16 @@ export async function processReceiving(
id: string,
data: ReceivingProcessFormData
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const apiData = transformProcessDataToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}/process`,
{ 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 = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고처리에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] processReceiving error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const apiData = transformProcessDataToApi(data);
const result = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${id}/process`,
method: 'POST',
body: apiData,
transform: (d: ReceivingApiData) => transformApiToDetail(d),
errorMessage: '입고처리에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 품목 검색 (입고 등록용) =====
@@ -915,39 +744,24 @@ export async function searchItems(query?: string): Promise<{
return { success: true, data: filtered };
}
try {
const searchParams = new URLSearchParams();
if (query) searchParams.set('search', query);
searchParams.set('per_page', '50');
const searchParams = new URLSearchParams();
if (query) searchParams.set('search', query);
searchParams.set('per_page', '50');
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error || !response) {
return { success: false, data: [] };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, data: [] };
}
const items: ItemOption[] = (result.data?.data || []).map((item: Record<string, string>) => ({
interface ItemApiData { data: Array<Record<string, string>> }
const result = await executeServerAction<ItemApiData, ItemOption[]>({
url: `${API_URL}/api/v1/items?${searchParams.toString()}`,
transform: (d) => (d.data || []).map((item) => ({
value: item.item_code,
label: item.item_code,
description: `${item.item_name} (${item.specification || '-'})`,
itemName: item.item_name,
specification: item.specification || '',
unit: item.unit || 'EA',
}));
return { success: true, data: items };
} catch (error) {
if (isNextRedirectError(error)) throw error;
return { success: false, data: [] };
}
})),
errorMessage: '품목 검색에 실패했습니다.',
});
return { success: result.success, data: result.data || [] };
}
// ===== 발주처 검색 (입고 등록용) =====
@@ -977,35 +791,20 @@ export async function searchSuppliers(query?: string): Promise<{
return { success: true, data: filtered };
}
try {
const searchParams = new URLSearchParams();
if (query) searchParams.set('search', query);
searchParams.set('per_page', '50');
const searchParams = new URLSearchParams();
if (query) searchParams.set('search', query);
searchParams.set('per_page', '50');
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/suppliers?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error || !response) {
return { success: false, data: [] };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, data: [] };
}
const suppliers: SupplierOption[] = (result.data?.data || []).map((s: Record<string, string>) => ({
interface SupplierApiData { data: Array<Record<string, string>> }
const result = await executeServerAction<SupplierApiData, SupplierOption[]>({
url: `${API_URL}/api/v1/suppliers?${searchParams.toString()}`,
transform: (d) => (d.data || []).map((s) => ({
value: s.name,
label: s.name,
}));
return { success: true, data: suppliers };
} catch (error) {
if (isNextRedirectError(error)) throw error;
return { success: false, data: [] };
}
})),
errorMessage: '발주처 검색에 실패했습니다.',
});
return { success: result.success, data: result.data || [] };
}
// ===== 수입검사 템플릿 타입 (ImportInspectionDocument와 동일) =====
@@ -1251,7 +1050,7 @@ export async function checkInspectionTemplate(itemId?: number): Promise<{
searchParams.append('category', 'incoming_inspection');
searchParams.append('item_id', String(itemId));
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/documents/resolve?${searchParams.toString()}`;
const url = `${API_URL}/api/v1/documents/resolve?${searchParams.toString()}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
@@ -1446,45 +1245,20 @@ export async function getInspectionTemplate(params: {
return { success: false, error: '품목 ID가 필요합니다.' };
}
try {
const searchParams = new URLSearchParams();
searchParams.set('category', 'incoming_inspection');
searchParams.set('item_id', String(params.itemId));
const searchParams = new URLSearchParams();
searchParams.set('category', 'incoming_inspection');
searchParams.set('item_id', String(params.itemId));
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/documents/resolve?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
const result = await executeServerAction<DocumentResolveResponse>({
url: `${API_URL}/api/v1/documents/resolve?${searchParams.toString()}`,
errorMessage: '검사 템플릿 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
if (!result.success || !result.data) return { success: false, error: result.error };
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '검사 템플릿 조회에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '검사 템플릿 조회에 실패했습니다.' };
}
const resolveData: DocumentResolveResponse = result.data;
// API 응답을 기존 InspectionTemplateResponse 형식으로 변환
const template = transformResolveToTemplate(resolveData, params);
return {
success: true,
data: template,
resolveData,
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] getInspectionTemplate error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const resolveData = result.data;
const template = transformResolveToTemplate(resolveData, params);
return { success: true, data: template, resolveData };
}
// ===== Tolerance 타입 정의 =====
@@ -2091,7 +1865,7 @@ export async function uploadInspectionFiles(files: File[]): Promise<{
formData.append('file', file);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/upload`,
`${API_URL}/api/v1/files/upload`,
{
method: 'POST',
headers: {
@@ -2112,7 +1886,7 @@ export async function uploadInspectionFiles(files: File[]): Promise<{
uploadedFiles.push({
id: result.data.id,
name: result.data.display_name || file.name,
url: `${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/${result.data.id}/download`,
url: `${API_URL}/api/v1/files/${result.data.id}/download`,
size: result.data.file_size,
});
}
@@ -2140,62 +1914,41 @@ export async function saveInspectionData(params: {
error?: string;
__authError?: boolean;
}> {
try {
// Step 1: POST /v1/documents/upsert - 검사 데이터 저장
const upsertBody = {
// Step 1: POST /v1/documents/upsert - 검사 데이터 저장
const docResult = await executeServerAction({
url: `${API_URL}/api/v1/documents/upsert`,
method: 'POST',
body: {
template_id: params.templateId,
item_id: params.itemId,
title: params.title || '수입검사 성적서',
data: params.data,
attachments: params.attachments || [],
};
},
errorMessage: '검사 데이터 저장에 실패했습니다.',
});
if (docResult.__authError) return { success: false, __authError: true };
if (!docResult.success) return { success: false, error: docResult.error };
const { response: docResponse, error: docError } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/documents/upsert`,
{ method: 'POST', body: JSON.stringify(upsertBody) }
);
// Step 2: PUT /v1/receivings/{id} - 검사 완료 후 입고대기로 상태 변경 (비필수)
const today = new Date().toISOString().split('T')[0];
const inspectionStatus = params.inspectionResult === 'pass' ? '적' : params.inspectionResult === 'fail' ? '부적' : '-';
const inspectionResultLabel = params.inspectionResult === 'pass' ? '합격' : params.inspectionResult === 'fail' ? '불합격' : null;
if (docError) {
return { success: false, error: docError.message, __authError: docError.code === 'UNAUTHORIZED' };
}
if (!docResponse) {
return { success: false, error: '검사 데이터 저장에 실패했습니다.' };
}
const docResult = await docResponse.json();
if (!docResponse.ok || !docResult.success) {
return { success: false, error: docResult.message || '검사 데이터 저장에 실패했습니다.' };
}
// Step 2: PUT /v1/receivings/{id} - 검사 완료 후 입고대기로 상태 변경 + 검사 정보 저장
const today = new Date().toISOString().split('T')[0];
const inspectionStatus = params.inspectionResult === 'pass' ? '적' : params.inspectionResult === 'fail' ? '부적' : '-';
const inspectionResultLabel = params.inspectionResult === 'pass' ? '합격' : params.inspectionResult === 'fail' ? '불합격' : null;
const { response: recResponse, error: recError } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${params.receivingId}`,
{
method: 'PUT',
body: JSON.stringify({
status: 'receiving_pending',
inspection_status: inspectionStatus,
inspection_date: today,
inspection_result: inspectionResultLabel,
}),
}
);
if (recError) {
console.error('[ReceivingActions] 입고 상태 업데이트 실패 (검사 데이터는 저장됨):', recError.message);
} else if (recResponse && !recResponse.ok) {
console.error('[ReceivingActions] 입고 상태 업데이트 실패 (검사 데이터는 저장됨)');
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[ReceivingActions] saveInspectionData error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
const recResult = await executeServerAction({
url: `${API_URL}/api/v1/receivings/${params.receivingId}`,
method: 'PUT',
body: {
status: 'receiving_pending',
inspection_status: inspectionStatus,
inspection_date: today,
inspection_result: inspectionResultLabel,
},
errorMessage: '입고 상태 업데이트에 실패했습니다.',
});
if (!recResult.success) {
console.error('[ReceivingActions] 입고 상태 업데이트 실패 (검사 데이터는 저장됨):', recResult.error);
}
return { success: true };
}

View File

@@ -11,8 +11,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 {
StockItem,
StockDetail,
@@ -234,312 +233,101 @@ interface PaginationMeta {
total: number;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL;
// ===== 재고 목록 조회 =====
export async function getStocks(params?: {
page?: number;
perPage?: number;
search?: string;
itemType?: string;
status?: string;
useStatus?: string;
location?: string;
sortBy?: string;
sortDir?: string;
startDate?: string;
endDate?: string;
}): Promise<{
success: boolean;
data: StockItem[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
page?: number; perPage?: number; search?: string; itemType?: string;
status?: string; useStatus?: string; location?: string;
sortBy?: string; sortDir?: string; startDate?: string; endDate?: string;
}): Promise<{ success: boolean; data: StockItem[]; pagination: PaginationMeta; error?: string; __authError?: boolean }> {
const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.search) searchParams.set('search', params.search);
if (params?.itemType && params.itemType !== 'all') searchParams.set('item_type', params.itemType);
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
if (params?.useStatus && params.useStatus !== 'all') searchParams.set('is_active', params.useStatus === 'active' ? '1' : '0');
if (params?.location) searchParams.set('location', params.location);
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.search) searchParams.set('search', params.search);
if (params?.itemType && params.itemType !== 'all') {
searchParams.set('item_type', params.itemType);
}
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.useStatus && params.useStatus !== 'all') {
searchParams.set('is_active', params.useStatus === 'active' ? '1' : '0');
}
if (params?.location) searchParams.set('location', params.location);
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
const queryString = searchParams.toString();
const result = await executeServerAction<ItemApiPaginatedResponse>({
url: `${API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`,
errorMessage: '재고 목록 조회에 실패했습니다.',
});
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`;
if (result.__authError) return { success: false, data: [], pagination: emptyPagination, __authError: true };
if (!result.success || !result.data) return { success: false, data: [], pagination: emptyPagination, error: result.error };
const { response, error } = await serverFetch(url, {
method: 'GET',
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '재고 목록 조회에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '재고 목록 조회에 실패했습니다.',
};
}
const paginatedData: ItemApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const stocks = (paginatedData.data || []).map(transformApiToListItem);
return {
success: true,
data: stocks,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockActions] getStocks error:', error);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
};
}
return {
success: true,
data: (result.data.data || []).map(transformApiToListItem),
pagination: {
currentPage: result.data.current_page, lastPage: result.data.last_page,
perPage: result.data.per_page, total: result.data.total,
},
};
}
// ===== 재고 통계 조회 =====
export async function getStockStats(): Promise<{
success: boolean;
data?: StockStats;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '재고 통계 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToStats(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockActions] getStockStats error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
export async function getStockStats(): Promise<{ success: boolean; data?: StockStats; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/stocks/stats`,
transform: (data: StockApiStatsResponse) => transformApiToStats(data),
errorMessage: '재고 통계 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 품목유형별 통계 조회 =====
export async function getStockStatsByType(): Promise<{
success: boolean;
data?: StockApiStatsByTypeResponse;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/stats-by-type`,
{ 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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '품목유형별 통계 조회에 실패했습니다.' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockActions] getStockStatsByType error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
export async function getStockStatsByType(): Promise<{ success: boolean; data?: StockApiStatsByTypeResponse; error?: string; __authError?: boolean }> {
const result = await executeServerAction<StockApiStatsByTypeResponse>({
url: `${API_URL}/api/v1/stocks/stats-by-type`,
errorMessage: '품목유형별 통계 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 재고 상세 조회 (Item 기준, LOT 포함) =====
export async function getStockById(id: string): Promise<{
success: boolean;
data?: StockDetail;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/${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 = await response.json();
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '재고 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockActions] getStockById error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
export async function getStockById(id: string): Promise<{ success: boolean; data?: StockDetail; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/stocks/${id}`,
transform: (data: ItemApiData) => transformApiToDetail(data),
errorMessage: '재고 조회에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, data: result.data, error: result.error };
}
// ===== 재고 단건 수정 =====
export async function updateStock(
id: string,
data: {
safetyStock: number;
useStatus: 'active' | 'inactive';
}
): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/${id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
safety_stock: data.safetyStock,
is_active: data.useStatus === 'active',
}),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '재고 수정에 실패했습니다.' };
}
const result = 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('[StockActions] updateStock error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
id: string, data: { safetyStock: number; useStatus: 'active' | 'inactive' }
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/stocks/${id}`,
method: 'PUT',
body: { safety_stock: data.safetyStock, is_active: data.useStatus === 'active' },
errorMessage: '재고 수정에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, error: result.error };
}
// ===== 재고 실사 (일괄 업데이트) =====
export async function updateStockAudit(updates: { id: string; actualQty: number }[]): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/audit`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: updates.map((u) => ({
item_id: u.id,
actual_qty: u.actualQty,
})),
}),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '재고 실사 저장에 실패했습니다.' };
}
const result = 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('[StockActions] updateStockAudit error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
export async function updateStockAudit(updates: { id: string; actualQty: number }[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
const result = await executeServerAction({
url: `${API_URL}/api/v1/stocks/audit`,
method: 'POST',
body: { items: updates.map((u) => ({ item_id: u.id, actual_qty: u.actualQty })) },
errorMessage: '재고 실사 저장에 실패했습니다.',
});
if (result.__authError) return { success: false, __authError: true };
return { success: result.success, error: result.error };
}