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:
유병철
2026-02-09 16:14:06 +09:00
parent d014227e9c
commit 55e0791e16
85 changed files with 7211 additions and 17638 deletions

View File

@@ -15,8 +15,7 @@
* - GET /api/v1/orders/select - 수주 선택 목록 조회
*/
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { executeServerAction } from '@/lib/api/execute-server-action';
import type {
ProductInspection,
InspectionStats,
@@ -307,81 +306,60 @@ export async function getInspections(params?: {
}> {
const defaultPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
try {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.status && params.status !== '전체') {
searchParams.set('status', mapFrontendStatus(params.status));
}
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.status && params.status !== '전체') {
searchParams.set('status', mapFrontendStatus(params.status));
}
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
const queryString = searchParams.toString();
const url = `${API_BASE}${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const result = await executeServerAction<PaginatedResponse>({
url: `${API_BASE}${queryString ? `?${queryString}` : ''}`,
errorMessage: '제품검사 목록 조회에 실패했습니다.',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[InspectionActions] API 실패, Mock 데이터 사용');
let filtered = [...mockInspections];
if (params?.status && params.status !== '전체') {
filtered = filtered.filter(i => i.status === params.status);
}
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) ||
i.client.toLowerCase().includes(q) ||
i.qualityDocNumber.toLowerCase().includes(q) ||
i.inspector.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
const paged = filtered.slice(start, start + size);
return {
success: true,
data: paged,
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, data: [], pagination: defaultPagination, error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], pagination: defaultPagination, error: result.message || '목록 조회 실패' };
}
const paginatedData: PaginatedResponse = result.data || { items: [], current_page: 1, last_page: 1, per_page: 20, total: 0 };
return {
success: true,
data: paginatedData.items.map(transformApiToFrontend),
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('[InspectionActions] getInspections error:', error);
if (!result.success) {
if (USE_MOCK_FALLBACK) {
let filtered = [...mockInspections];
if (params?.status && params.status !== '전체') {
filtered = filtered.filter(i => i.status === params.status);
}
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) ||
i.client.toLowerCase().includes(q) ||
i.qualityDocNumber.toLowerCase().includes(q) ||
i.inspector.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
return {
success: true,
data: mockInspections,
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: mockInspections.length },
data: filtered.slice(start, start + size),
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
return { success: false, data: [], pagination: defaultPagination, error: '서버 오류가 발생했습니다.' };
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data || { items: [], current_page: 1, last_page: 1, per_page: 20, total: 0 };
return {
success: true,
data: d.items.map(transformApiToFrontend),
pagination: {
currentPage: d.current_page,
lastPage: d.last_page,
perPage: d.per_page,
total: d.total,
},
};
}
// ===== 통계 조회 =====
@@ -395,45 +373,30 @@ export async function getInspectionStats(params?: {
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
const searchParams = new URLSearchParams();
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
const queryString = searchParams.toString();
const url = `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const result = await executeServerAction<InspectionStatsApi>({
url: `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`,
errorMessage: '제품검사 통계 조회에 실패했습니다.',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[InspectionActions] Stats API 실패, Mock 데이터 사용');
return { success: true, data: mockStats };
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success) {
return { success: false, error: result.message || '통계 조회 실패' };
}
const statsApi: InspectionStatsApi = result.data;
return {
success: true,
data: {
receptionCount: statsApi.reception_count,
inProgressCount: statsApi.in_progress_count,
completedCount: statsApi.completed_count,
},
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getInspectionStats error:', error);
if (!result.success) {
if (USE_MOCK_FALLBACK) return { success: true, data: mockStats };
return { success: false, error: '서버 오류가 발생했습니다.' };
return { success: false, error: result.error, __authError: result.__authError };
}
const s = result.data;
return {
success: true,
data: s ? {
receptionCount: s.reception_count,
inProgressCount: s.in_progress_count,
completedCount: s.completed_count,
} : undefined,
};
}
// ===== 캘린더 스케줄 조회 =====
@@ -449,52 +412,37 @@ export async function getInspectionCalendar(params?: {
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
if (params?.month) searchParams.set('month', String(params.month));
if (params?.inspector) searchParams.set('inspector', params.inspector);
if (params?.status && params.status !== '전체') {
searchParams.set('status', mapFrontendStatus(params.status));
}
const queryString = searchParams.toString();
const url = `${API_BASE}/calendar${queryString ? `?${queryString}` : ''}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[InspectionActions] Calendar API 실패, Mock 데이터 사용');
return { success: true, data: mockCalendarItems };
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, data: [], error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message || '캘린더 조회 실패' };
}
const items: CalendarItemApi[] = result.data || [];
return {
success: true,
data: items.map((item) => ({
id: String(item.id),
startDate: item.start_date,
endDate: item.end_date,
inspector: item.inspector,
siteName: item.site_name,
status: mapApiStatus(item.status as ProductInspectionApi['status']),
})),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getInspectionCalendar error:', error);
if (USE_MOCK_FALLBACK) return { success: true, data: mockCalendarItems };
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
if (params?.month) searchParams.set('month', String(params.month));
if (params?.inspector) searchParams.set('inspector', params.inspector);
if (params?.status && params.status !== '전체') {
searchParams.set('status', mapFrontendStatus(params.status));
}
const queryString = searchParams.toString();
const result = await executeServerAction<CalendarItemApi[]>({
url: `${API_BASE}/calendar${queryString ? `?${queryString}` : ''}`,
errorMessage: '캘린더 스케줄 조회에 실패했습니다.',
});
if (!result.success) {
if (USE_MOCK_FALLBACK) return { success: true, data: mockCalendarItems };
return { success: false, data: [], error: result.error, __authError: result.__authError };
}
const items = result.data || [];
return {
success: true,
data: items.map((item) => ({
id: String(item.id),
startDate: item.start_date,
endDate: item.end_date,
inspector: item.inspector,
siteName: item.site_name,
status: mapApiStatus(item.status as ProductInspectionApi['status']),
})),
};
}
// ===== 상세 조회 =====
@@ -505,36 +453,23 @@ export async function getInspectionById(id: string): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const url = `${API_BASE}/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const result = await executeServerAction<ProductInspectionApi>({
url: `${API_BASE}/${id}`,
errorMessage: '제품검사 상세 조회에 실패했습니다.',
});
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[InspectionActions] Detail API 실패, Mock 데이터 사용');
const mockItem = mockInspections.find(i => i.id === id);
if (mockItem) return { success: true, data: mockItem };
return { success: false, error: '해당 데이터를 찾을 수 없습니다.' };
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success || !result.data) {
return { success: false, error: result.message || '상세 조회 실패' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getInspectionById error:', error);
if (!result.success) {
if (USE_MOCK_FALLBACK) {
const mockItem = mockInspections.find(i => i.id === id);
if (mockItem) return { success: true, data: mockItem };
return { success: false, error: '해당 데이터를 찾을 수 없습니다.' };
}
return { success: false, error: '서버 오류가 발생했습니다.' };
return { success: false, error: result.error, __authError: result.__authError };
}
return result.data
? { success: true, data: transformApiToFrontend(result.data) }
: { success: false, error: '상세 조회 실패' };
}
// ===== 등록 =====
@@ -545,32 +480,18 @@ export async function createInspection(data: InspectionFormData): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const apiData = transformFormToApi(data);
const apiData = transformFormToApi(data);
const result = await executeServerAction<ProductInspectionApi>({
url: API_BASE,
method: 'POST',
body: apiData,
errorMessage: '제품검사 등록에 실패했습니다.',
});
const { response, error } = await serverFetch(API_BASE, {
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: transformApiToFrontend(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] createInspection error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
return result.data
? { success: true, data: transformApiToFrontend(result.data) }
: { success: true };
}
// ===== 수정 =====
@@ -584,93 +505,80 @@ export async function updateInspection(
error?: string;
__authError?: boolean;
}> {
try {
const apiData: Record<string, unknown> = {};
const apiData: Record<string, unknown> = {};
if (data.qualityDocNumber !== undefined) apiData.quality_doc_number = data.qualityDocNumber;
if (data.siteName !== undefined) apiData.site_name = data.siteName;
if (data.client !== undefined) apiData.client = data.client;
if (data.manager !== undefined) apiData.manager = data.manager;
if (data.managerContact !== undefined) apiData.manager_contact = data.managerContact;
if (data.qualityDocNumber !== undefined) apiData.quality_doc_number = data.qualityDocNumber;
if (data.siteName !== undefined) apiData.site_name = data.siteName;
if (data.client !== undefined) apiData.client = data.client;
if (data.manager !== undefined) apiData.manager = data.manager;
if (data.managerContact !== undefined) apiData.manager_contact = data.managerContact;
if (data.constructionSite) {
apiData.construction_site = {
site_name: data.constructionSite.siteName,
land_location: data.constructionSite.landLocation,
lot_number: data.constructionSite.lotNumber,
};
}
if (data.materialDistributor) {
apiData.material_distributor = {
company_name: data.materialDistributor.companyName,
company_address: data.materialDistributor.companyAddress,
representative_name: data.materialDistributor.representativeName,
phone: data.materialDistributor.phone,
};
}
if (data.constructorInfo) {
apiData.constructor_info = {
company_name: data.constructorInfo.companyName,
company_address: data.constructorInfo.companyAddress,
name: data.constructorInfo.name,
phone: data.constructorInfo.phone,
};
}
if (data.supervisor) {
apiData.supervisor = {
office_name: data.supervisor.officeName,
office_address: data.supervisor.officeAddress,
name: data.supervisor.name,
phone: data.supervisor.phone,
};
}
if (data.scheduleInfo) {
apiData.schedule_info = {
visit_request_date: data.scheduleInfo.visitRequestDate,
start_date: data.scheduleInfo.startDate,
end_date: data.scheduleInfo.endDate,
inspector: data.scheduleInfo.inspector,
site_postal_code: data.scheduleInfo.sitePostalCode,
site_address: data.scheduleInfo.siteAddress,
site_address_detail: data.scheduleInfo.siteAddressDetail,
};
}
if (data.orderItems) {
apiData.order_items = data.orderItems.map((item) => ({
order_number: item.orderNumber,
floor: item.floor,
symbol: item.symbol,
order_width: item.orderWidth,
order_height: item.orderHeight,
construction_width: item.constructionWidth,
construction_height: item.constructionHeight,
change_reason: item.changeReason,
}));
}
const { response, error } = await serverFetch(`${API_BASE}/${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: transformApiToFrontend(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] updateInspection error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (data.constructionSite) {
apiData.construction_site = {
site_name: data.constructionSite.siteName,
land_location: data.constructionSite.landLocation,
lot_number: data.constructionSite.lotNumber,
};
}
if (data.materialDistributor) {
apiData.material_distributor = {
company_name: data.materialDistributor.companyName,
company_address: data.materialDistributor.companyAddress,
representative_name: data.materialDistributor.representativeName,
phone: data.materialDistributor.phone,
};
}
if (data.constructorInfo) {
apiData.constructor_info = {
company_name: data.constructorInfo.companyName,
company_address: data.constructorInfo.companyAddress,
name: data.constructorInfo.name,
phone: data.constructorInfo.phone,
};
}
if (data.supervisor) {
apiData.supervisor = {
office_name: data.supervisor.officeName,
office_address: data.supervisor.officeAddress,
name: data.supervisor.name,
phone: data.supervisor.phone,
};
}
if (data.scheduleInfo) {
apiData.schedule_info = {
visit_request_date: data.scheduleInfo.visitRequestDate,
start_date: data.scheduleInfo.startDate,
end_date: data.scheduleInfo.endDate,
inspector: data.scheduleInfo.inspector,
site_postal_code: data.scheduleInfo.sitePostalCode,
site_address: data.scheduleInfo.siteAddress,
site_address_detail: data.scheduleInfo.siteAddressDetail,
};
}
if (data.orderItems) {
apiData.order_items = data.orderItems.map((item) => ({
order_number: item.orderNumber,
floor: item.floor,
symbol: item.symbol,
order_width: item.orderWidth,
order_height: item.orderHeight,
construction_width: item.constructionWidth,
construction_height: item.constructionHeight,
change_reason: item.changeReason,
}));
}
const result = await executeServerAction<ProductInspectionApi>({
url: `${API_BASE}/${id}`,
method: 'PUT',
body: apiData,
errorMessage: '제품검사 수정에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
return result.data
? { success: true, data: transformApiToFrontend(result.data) }
: { success: true };
}
// ===== 삭제 =====
@@ -680,27 +588,13 @@ export async function deleteInspection(id: string): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE}/${id}`, { method: 'DELETE' });
const result = await executeServerAction({
url: `${API_BASE}/${id}`,
method: 'DELETE',
errorMessage: '제품검사 삭제에 실패했습니다.',
});
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('[InspectionActions] deleteInspection error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 검사 완료 처리 =====
@@ -714,35 +608,22 @@ export async function completeInspection(
error?: string;
__authError?: boolean;
}> {
try {
const apiData: Record<string, unknown> = {};
if (data?.result) {
apiData.result = data.result === '합격' ? 'pass' : 'fail';
}
const { response, error } = await serverFetch(`${API_BASE}/${id}/complete`, {
method: 'PATCH',
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: transformApiToFrontend(result.data) };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] completeInspection error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
const apiData: Record<string, unknown> = {};
if (data?.result) {
apiData.result = data.result === '합격' ? 'pass' : 'fail';
}
const result = await executeServerAction<ProductInspectionApi>({
url: `${API_BASE}/${id}/complete`,
method: 'PATCH',
body: apiData,
errorMessage: '검사 완료 처리에 실패했습니다.',
});
if (!result.success) return { success: false, error: result.error, __authError: result.__authError };
return result.data
? { success: true, data: transformApiToFrontend(result.data) }
: { success: true };
}
// ===== 수주 선택 목록 조회 =====
@@ -755,51 +636,38 @@ export async function getOrderSelectList(params?: {
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
if (params?.q) searchParams.set('q', params.q);
const searchParams = new URLSearchParams();
if (params?.q) searchParams.set('q', params.q);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/select${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const result = await executeServerAction<OrderSelectItemApi[]>({
url: `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/select${queryString ? `?${queryString}` : ''}`,
errorMessage: '수주 선택 목록 조회에 실패했습니다.',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[InspectionActions] OrderSelect API 실패, Mock 데이터 사용');
let filtered = [...mockOrderSelectItems];
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.orderNumber.toLowerCase().includes(q) || i.siteName.toLowerCase().includes(q)
);
}
return { success: true, data: filtered };
if (!result.success) {
if (USE_MOCK_FALLBACK) {
let filtered = [...mockOrderSelectItems];
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.orderNumber.toLowerCase().includes(q) || i.siteName.toLowerCase().includes(q)
);
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, data: [], error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
return { success: true, data: filtered };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message || '수주 목록 조회 실패' };
}
const items: OrderSelectItemApi[] = result.data || [];
return {
success: true,
data: items.map((item) => ({
id: String(item.id),
orderNumber: item.order_number,
siteName: item.site_name,
deliveryDate: item.delivery_date,
locationCount: item.location_count,
})),
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[InspectionActions] getOrderSelectList error:', error);
if (USE_MOCK_FALLBACK) return { success: true, data: mockOrderSelectItems };
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
return { success: false, data: [], error: result.error, __authError: result.__authError };
}
const items = result.data || [];
return {
success: true,
data: items.map((item) => ({
id: String(item.id),
orderNumber: item.order_number,
siteName: item.site_name,
deliveryDate: item.delivery_date,
locationCount: item.location_count,
})),
};
}

View File

@@ -13,8 +13,7 @@
* - PATCH /api/v1/performance-reports/memo - 메모 일괄 적용
*/
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { executeServerAction } from '@/lib/api/execute-server-action';
import type {
PerformanceReport,
PerformanceReportStats,
@@ -58,80 +57,56 @@ export async function getPerformanceReports(params?: {
}> {
const defaultPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
try {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.year) searchParams.set('year', String(params.year));
if (params?.quarter && params.quarter !== '전체') {
searchParams.set('quarter', params.quarter);
}
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.year) searchParams.set('year', String(params.year));
if (params?.quarter && params.quarter !== '전체') {
searchParams.set('quarter', params.quarter);
}
const queryString = searchParams.toString();
const url = `${API_BASE}${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
interface ApiListData { items?: PerformanceReport[]; current_page?: number; last_page?: number; per_page?: number; total?: number }
const result = await executeServerAction<ApiListData>({
url: `${API_BASE}${queryString ? `?${queryString}` : ''}`,
errorMessage: '실적신고 목록 조회에 실패했습니다.',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[PerformanceReportActions] API 실패, Mock 데이터 사용');
let filtered = [...mockPerformanceReports];
if (params?.year) {
filtered = filtered.filter(i => i.year === params.year);
}
if (params?.quarter && params.quarter !== '전체') {
filtered = filtered.filter(i => i.quarter === params.quarter);
}
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) ||
i.client.toLowerCase().includes(q) ||
i.qualityDocNumber.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
const paged = filtered.slice(start, start + size);
return {
success: true,
data: paged,
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, data: [], pagination: defaultPagination, error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], pagination: defaultPagination, error: result.message || '목록 조회 실패' };
}
return {
success: true,
data: result.data?.items || [],
pagination: {
currentPage: result.data?.current_page || 1,
lastPage: result.data?.last_page || 1,
perPage: result.data?.per_page || 20,
total: result.data?.total || 0,
},
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] getPerformanceReports error:', error);
if (!result.success) {
if (USE_MOCK_FALLBACK) {
let filtered = [...mockPerformanceReports];
if (params?.year) filtered = filtered.filter(i => i.year === params.year);
if (params?.quarter && params.quarter !== '전체') filtered = filtered.filter(i => i.quarter === params.quarter);
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) || i.client.toLowerCase().includes(q) || i.qualityDocNumber.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
return {
success: true,
data: mockPerformanceReports,
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: mockPerformanceReports.length },
data: filtered.slice(start, start + size),
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
return { success: false, data: [], pagination: defaultPagination, error: '서버 오류가 발생했습니다.' };
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data;
return {
success: true,
data: d?.items || [],
pagination: {
currentPage: d?.current_page || 1,
lastPage: d?.last_page || 1,
perPage: d?.per_page || 20,
total: d?.total || 0,
},
};
}
// ===== 통계 조회 =====
@@ -145,39 +120,23 @@ export async function getPerformanceReportStats(params?: {
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
if (params?.quarter && params.quarter !== '전체') {
searchParams.set('quarter', params.quarter);
}
const queryString = searchParams.toString();
const url = `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[PerformanceReportActions] Stats API 실패, Mock 데이터 사용');
return { success: true, data: mockPerformanceReportStats };
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success) {
return { success: false, error: result.message || '통계 조회 실패' };
}
return { success: true, data: result.data };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] getPerformanceReportStats error:', error);
if (USE_MOCK_FALLBACK) return { success: true, data: mockPerformanceReportStats };
return { success: false, error: '서버 오류가 발생했습니다.' };
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
if (params?.quarter && params.quarter !== '전체') {
searchParams.set('quarter', params.quarter);
}
const queryString = searchParams.toString();
const result = await executeServerAction<PerformanceReportStats>({
url: `${API_BASE}/stats${queryString ? `?${queryString}` : ''}`,
errorMessage: '실적신고 통계 조회에 실패했습니다.',
});
if (!result.success) {
if (USE_MOCK_FALLBACK) return { success: true, data: mockPerformanceReportStats };
return { success: false, error: result.error, __authError: result.__authError };
}
return { success: true, data: result.data };
}
// ===== 누락체크 목록 조회 =====
@@ -195,70 +154,50 @@ export async function getMissedReports(params?: {
}> {
const defaultPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 };
try {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('per_page', String(params.size));
if (params?.q) searchParams.set('q', params.q);
const queryString = searchParams.toString();
const url = `${API_BASE}/missed${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
interface ApiMissedData { items?: MissedReport[]; current_page?: number; last_page?: number; per_page?: number; total?: number }
const result = await executeServerAction<ApiMissedData>({
url: `${API_BASE}/missed${queryString ? `?${queryString}` : ''}`,
errorMessage: '누락체크 목록 조회에 실패했습니다.',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response || !response.ok) {
if (USE_MOCK_FALLBACK) {
console.warn('[PerformanceReportActions] Missed API 실패, Mock 데이터 사용');
let filtered = [...mockMissedReports];
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) ||
i.client.toLowerCase().includes(q) ||
i.qualityDocNumber.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
const paged = filtered.slice(start, start + size);
return {
success: true,
data: paged,
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
const errMsg = error ? error.message : `API 오류: ${response?.status || 'no response'}`;
return { success: false, data: [], pagination: defaultPagination, error: errMsg, __authError: error?.code === 'UNAUTHORIZED' };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], pagination: defaultPagination, error: result.message || '누락체크 조회 실패' };
}
return {
success: true,
data: result.data?.items || [],
pagination: {
currentPage: result.data?.current_page || 1,
lastPage: result.data?.last_page || 1,
perPage: result.data?.per_page || 20,
total: result.data?.total || 0,
},
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] getMissedReports error:', error);
if (!result.success) {
if (USE_MOCK_FALLBACK) {
let filtered = [...mockMissedReports];
if (params?.q) {
const q = params.q.toLowerCase();
filtered = filtered.filter(i =>
i.siteName.toLowerCase().includes(q) || i.client.toLowerCase().includes(q) || i.qualityDocNumber.toLowerCase().includes(q)
);
}
const page = params?.page || 1;
const size = params?.size || 20;
const start = (page - 1) * size;
return {
success: true,
data: mockMissedReports,
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: mockMissedReports.length },
data: filtered.slice(start, start + size),
pagination: { currentPage: page, lastPage: Math.ceil(filtered.length / size), perPage: size, total: filtered.length },
};
}
return { success: false, data: [], pagination: defaultPagination, error: '서버 오류가 발생했습니다.' };
return { success: false, data: [], pagination: defaultPagination, error: result.error, __authError: result.__authError };
}
const d = result.data;
return {
success: true,
data: d?.items || [],
pagination: {
currentPage: d?.current_page || 1,
lastPage: d?.last_page || 1,
perPage: d?.per_page || 20,
total: d?.total || 0,
},
};
}
// ===== 선택 확정 =====
@@ -268,34 +207,14 @@ export async function confirmReports(ids: string[]): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE}/confirm`, {
method: 'PATCH',
body: JSON.stringify({ ids }),
});
if (error) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '확정 처리에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: result.message || '확정 처리에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] confirmReports error:', error);
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_BASE}/confirm`,
method: 'PATCH',
body: { ids },
errorMessage: '확정 처리에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 확정 해제 =====
@@ -305,34 +224,14 @@ export async function unconfirmReports(ids: string[]): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE}/unconfirm`, {
method: 'PATCH',
body: JSON.stringify({ ids }),
});
if (error) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '확정 해제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: result.message || '확정 해제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] unconfirmReports error:', error);
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_BASE}/unconfirm`,
method: 'PATCH',
body: { ids },
errorMessage: '확정 해제에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 배포 =====
@@ -342,34 +241,14 @@ export async function distributeReports(ids: string[]): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE}/distribute`, {
method: 'POST',
body: JSON.stringify({ ids }),
});
if (error) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '배포에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: result.message || '배포에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] distributeReports error:', error);
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_BASE}/distribute`,
method: 'POST',
body: { ids },
errorMessage: '배포에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}
// ===== 메모 일괄 적용 =====
@@ -379,32 +258,12 @@ export async function updateMemo(ids: string[], memo: string): Promise<{
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(`${API_BASE}/memo`, {
method: 'PATCH',
body: JSON.stringify({ ids, memo }),
});
if (error) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '메모 저장에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: result.message || '메모 저장에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[PerformanceReportActions] updateMemo error:', error);
if (USE_MOCK_FALLBACK) return { success: true };
return { success: false, error: '서버 오류가 발생했습니다.' };
}
const result = await executeServerAction({
url: `${API_BASE}/memo`,
method: 'PATCH',
body: { ids, memo },
errorMessage: '메모 저장에 실패했습니다.',
});
if (!result.success && USE_MOCK_FALLBACK) return { success: true };
return { success: result.success, error: result.error, __authError: result.__authError };
}