'use server'; /** * 제품검사 관리 Server Actions * * API Endpoints: * - GET /api/v1/product-inspections - 목록 조회 * - GET /api/v1/product-inspections/stats - 통계 조회 * - GET /api/v1/product-inspections/calendar - 캘린더 스케줄 조회 * - GET /api/v1/product-inspections/{id} - 상세 조회 * - POST /api/v1/product-inspections - 등록 * - PUT /api/v1/product-inspections/{id} - 수정 * - DELETE /api/v1/product-inspections/{id} - 삭제 * - PATCH /api/v1/product-inspections/{id}/complete - 검사 완료 처리 * - GET /api/v1/orders/select - 수주 선택 목록 조회 */ import { serverFetch } from '@/lib/api/fetch-wrapper'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import type { ProductInspection, InspectionStats, InspectionStatus, InspectionCalendarItem, OrderSelectItem, InspectionFormData, } from './types'; import { mockInspections, mockStats, mockCalendarItems, mockOrderSelectItems, } from './mockData'; // 개발환경 Mock 데이터 fallback 플래그 const USE_MOCK_FALLBACK = true; // ===== API 응답 타입 ===== interface ProductInspectionApi { id: number; quality_doc_number: string; site_name: string; client: string; location_count: number; required_info: string; inspection_period: string; inspector: string; status: 'reception' | 'in_progress' | 'completed'; author: string; reception_date: string; manager: string; manager_contact: string; construction_site: { site_name: string; land_location: string; lot_number: string; }; material_distributor: { company_name: string; company_address: string; representative_name: string; phone: string; }; constructor_info: { company_name: string; company_address: string; name: string; phone: string; }; supervisor: { office_name: string; office_address: string; name: string; phone: string; }; schedule_info: { visit_request_date: string; start_date: string; end_date: string; inspector: string; site_postal_code: string; site_address: string; site_address_detail: string; }; order_items: Array<{ id: string; order_number: string; site_name: string; delivery_date: string; floor: string; symbol: string; order_width: number; order_height: number; construction_width: number; construction_height: number; change_reason: string; }>; created_at: string; updated_at: string; } interface PaginatedResponse { items: ProductInspectionApi[]; current_page: number; last_page: number; per_page: number; total: number; } interface InspectionStatsApi { reception_count: number; in_progress_count: number; completed_count: number; } interface CalendarItemApi { id: number; start_date: string; end_date: string; inspector: string; site_name: string; status: 'reception' | 'in_progress' | 'completed'; } interface OrderSelectItemApi { id: number; order_number: string; site_name: string; delivery_date: string; location_count: number; } // ===== 페이지네이션 ===== interface PaginationMeta { currentPage: number; lastPage: number; perPage: number; total: number; } // ===== 상태 변환 ===== function mapApiStatus(status: ProductInspectionApi['status']): InspectionStatus { switch (status) { case 'reception': return '접수'; case 'in_progress': return '진행중'; case 'completed': return '완료'; default: return '접수'; } } function mapFrontendStatus(status: InspectionStatus): string { switch (status) { case '접수': return 'reception'; case '진행중': return 'in_progress'; case '완료': return 'completed'; default: return 'reception'; } } // ===== API → Frontend 변환 ===== function transformApiToFrontend(api: ProductInspectionApi): ProductInspection { return { id: String(api.id), qualityDocNumber: api.quality_doc_number, siteName: api.site_name, client: api.client, locationCount: api.location_count, requiredInfo: api.required_info, inspectionPeriod: api.inspection_period, inspector: api.inspector, status: mapApiStatus(api.status), author: api.author, receptionDate: api.reception_date, manager: api.manager, managerContact: api.manager_contact, constructionSite: { siteName: api.construction_site?.site_name || '', landLocation: api.construction_site?.land_location || '', lotNumber: api.construction_site?.lot_number || '', }, materialDistributor: { companyName: api.material_distributor?.company_name || '', companyAddress: api.material_distributor?.company_address || '', representativeName: api.material_distributor?.representative_name || '', phone: api.material_distributor?.phone || '', }, constructorInfo: { companyName: api.constructor_info?.company_name || '', companyAddress: api.constructor_info?.company_address || '', name: api.constructor_info?.name || '', phone: api.constructor_info?.phone || '', }, supervisor: { officeName: api.supervisor?.office_name || '', officeAddress: api.supervisor?.office_address || '', name: api.supervisor?.name || '', phone: api.supervisor?.phone || '', }, scheduleInfo: { visitRequestDate: api.schedule_info?.visit_request_date || '', startDate: api.schedule_info?.start_date || '', endDate: api.schedule_info?.end_date || '', inspector: api.schedule_info?.inspector || '', sitePostalCode: api.schedule_info?.site_postal_code || '', siteAddress: api.schedule_info?.site_address || '', siteAddressDetail: api.schedule_info?.site_address_detail || '', }, orderItems: (api.order_items || []).map((item) => ({ id: item.id, orderNumber: item.order_number, siteName: item.site_name || '', deliveryDate: item.delivery_date || '', floor: item.floor, symbol: item.symbol, orderWidth: item.order_width, orderHeight: item.order_height, constructionWidth: item.construction_width, constructionHeight: item.construction_height, changeReason: item.change_reason, })), }; } // ===== Frontend → API 변환 ===== function transformFormToApi(data: InspectionFormData): Record { return { quality_doc_number: data.qualityDocNumber, site_name: data.siteName, client: data.client, manager: data.manager, manager_contact: data.managerContact, construction_site: { site_name: data.constructionSite.siteName, land_location: data.constructionSite.landLocation, lot_number: data.constructionSite.lotNumber, }, material_distributor: { company_name: data.materialDistributor.companyName, company_address: data.materialDistributor.companyAddress, representative_name: data.materialDistributor.representativeName, phone: data.materialDistributor.phone, }, constructor_info: { company_name: data.constructorInfo.companyName, company_address: data.constructorInfo.companyAddress, name: data.constructorInfo.name, phone: data.constructorInfo.phone, }, supervisor: { office_name: data.supervisor.officeName, office_address: data.supervisor.officeAddress, name: data.supervisor.name, phone: data.supervisor.phone, }, 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, }, 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 API_BASE = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/product-inspections`; // ===== 제품검사 목록 조회 ===== export async function getInspections(params?: { page?: number; size?: number; q?: string; status?: InspectionStatus | '전체'; dateFrom?: string; dateTo?: string; }): Promise<{ success: boolean; data: ProductInspection[]; pagination: PaginationMeta; error?: string; __authError?: boolean; }> { 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 queryString = searchParams.toString(); const url = `${API_BASE}${queryString ? `?${queryString}` : ''}`; 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 (USE_MOCK_FALLBACK) { return { success: true, data: mockInspections, pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: mockInspections.length }, }; } return { success: false, data: [], pagination: defaultPagination, error: '서버 오류가 발생했습니다.' }; } } // ===== 통계 조회 ===== export async function getInspectionStats(params?: { dateFrom?: string; dateTo?: string; }): Promise<{ success: boolean; data?: InspectionStats; 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 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('[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 (USE_MOCK_FALLBACK) return { success: true, data: mockStats }; return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 캘린더 스케줄 조회 ===== export async function getInspectionCalendar(params?: { year?: number; month?: number; inspector?: string; status?: InspectionStatus | '전체'; }): Promise<{ success: boolean; data: InspectionCalendarItem[]; 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: '서버 오류가 발생했습니다.' }; } } // ===== 상세 조회 ===== export async function getInspectionById(id: string): Promise<{ success: boolean; data?: ProductInspection; error?: string; __authError?: boolean; }> { try { const url = `${API_BASE}/${id}`; const { response, error } = await serverFetch(url, { method: 'GET' }); 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 (USE_MOCK_FALLBACK) { const mockItem = mockInspections.find(i => i.id === id); if (mockItem) return { success: true, data: mockItem }; } return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 등록 ===== export async function createInspection(data: InspectionFormData): Promise<{ success: boolean; data?: ProductInspection; error?: string; __authError?: boolean; }> { try { const apiData = transformFormToApi(data); 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: '서버 오류가 발생했습니다.' }; } } // ===== 수정 ===== export async function updateInspection( id: string, data: Partial ): Promise<{ success: boolean; data?: ProductInspection; error?: string; __authError?: boolean; }> { try { const apiData: Record = {}; 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: '서버 오류가 발생했습니다.' }; } } // ===== 삭제 ===== export async function deleteInspection(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean; }> { try { const { response, error } = await serverFetch(`${API_BASE}/${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('[InspectionActions] deleteInspection error:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } } // ===== 검사 완료 처리 ===== export async function completeInspection( id: string, data?: { result?: '합격' | '불합격' } ): Promise<{ success: boolean; data?: ProductInspection; error?: string; __authError?: boolean; }> { try { const apiData: Record = {}; 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: '서버 오류가 발생했습니다.' }; } } // ===== 수주 선택 목록 조회 ===== export async function getOrderSelectList(params?: { q?: string; }): Promise<{ success: boolean; data: OrderSelectItem[]; error?: string; __authError?: boolean; }> { try { 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 { 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 }; } 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: 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: '서버 오류가 발생했습니다.' }; } }