/** * 참조함 서버 액션 * * API Endpoints: * - GET /api/v1/approvals/reference - 참조함 목록 조회 * - POST /api/v1/approvals/{id}/read - 열람 처리 * - POST /api/v1/approvals/{id}/unread - 미열람 처리 */ 'use server'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { ReferenceRecord, ApprovalType, DocumentStatus } from './types'; // ============================================ // API 응답 타입 정의 // ============================================ interface ApiResponse { success: boolean; data: T; message: string; } interface PaginatedResponse { current_page: number; data: T[]; total: number; per_page: number; last_page: number; } // API 응답의 참조 문서 타입 interface ReferenceApiData { id: number; document_number: string; title: string; status: string; form?: { id: number; name: string; code: string; category: string; }; drafter?: { id: number; name: string; position?: string; department?: { name: string }; }; steps?: ReferenceStepApiData[]; created_at: string; updated_at: string; } interface ReferenceStepApiData { id: number; step_order: number; step_type: string; approver_id: number; approver?: { id: number; name: string; position?: string; department?: { name: string }; }; is_read: boolean; read_at?: string; } // ============================================ // 헬퍼 함수 // ============================================ /** * API 상태 → 프론트엔드 상태 변환 */ function mapApiStatus(apiStatus: string): DocumentStatus { const statusMap: Record = { 'draft': 'pending', 'pending': 'pending', 'in_progress': 'pending', 'approved': 'approved', 'rejected': 'rejected', }; return statusMap[apiStatus] || 'pending'; } /** * 양식 카테고리 → 결재 유형 변환 */ function mapApprovalType(formCategory?: string): ApprovalType { const typeMap: Record = { 'expense_report': 'expense_report', 'proposal': 'proposal', 'expense_estimate': 'expense_estimate', }; return typeMap[formCategory || ''] || 'proposal'; } /** * API 데이터 → 프론트엔드 데이터 변환 */ function transformApiToFrontend(data: ReferenceApiData): ReferenceRecord { // 참조 단계에서 열람 상태 추출 const referenceStep = data.steps?.find(s => s.step_type === 'reference'); const isRead = referenceStep?.is_read ?? false; const readAt = referenceStep?.read_at; return { id: String(data.id), documentNo: data.document_number, approvalType: mapApprovalType(data.form?.category), title: data.title, draftDate: data.created_at.replace('T', ' ').substring(0, 16), drafter: data.drafter?.name || '', drafterDepartment: data.drafter?.department?.name || '', drafterPosition: data.drafter?.position || '', documentStatus: mapApiStatus(data.status), readStatus: isRead ? 'read' : 'unread', readAt: readAt ? readAt.replace('T', ' ').substring(0, 16) : undefined, createdAt: data.created_at, updatedAt: data.updated_at, }; } // ============================================ // API 함수 // ============================================ /** * 참조함 목록 조회 */ export async function getReferences(params?: { page?: number; per_page?: number; search?: string; is_read?: boolean; approval_type?: string; sort_by?: string; sort_dir?: 'asc' | 'desc'; }): Promise<{ data: ReferenceRecord[]; total: number; lastPage: number }> { try { const searchParams = new URLSearchParams(); if (params?.page) searchParams.set('page', String(params.page)); if (params?.per_page) searchParams.set('per_page', String(params.per_page)); if (params?.search) searchParams.set('search', params.search); if (params?.is_read !== undefined) { searchParams.set('is_read', params.is_read ? '1' : '0'); } if (params?.approval_type && params.approval_type !== 'all') { searchParams.set('approval_type', params.approval_type); } if (params?.sort_by) searchParams.set('sort_by', params.sort_by); if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir); const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/reference?${searchParams.toString()}`; const { response, error } = await serverFetch(url, { method: 'GET', }); // serverFetch handles 401 with redirect, so we just check for other errors if (error || !response) { console.error('[ReferenceBoxActions] GET reference error:', error?.message); return { data: [], total: 0, lastPage: 1 }; } if (!response.ok) { console.error('[ReferenceBoxActions] GET reference error:', response.status); return { data: [], total: 0, lastPage: 1 }; } const result: ApiResponse> = await response.json(); if (!result.success || !result.data?.data) { console.warn('[ReferenceBoxActions] No data in response'); return { data: [], total: 0, lastPage: 1 }; } return { data: result.data.data.map(transformApiToFrontend), total: result.data.total, lastPage: result.data.last_page, }; } catch (error) { console.error('[ReferenceBoxActions] getReferences error:', error); return { data: [], total: 0, lastPage: 1 }; } } /** * 참조함 통계 (목록 데이터 기반) */ export async function getReferenceSummary(): Promise<{ all: number; read: number; unread: number } | null> { try { // 전체 데이터를 조회해서 통계 계산 const allResult = await getReferences({ per_page: 1 }); const readResult = await getReferences({ per_page: 1, is_read: true }); const unreadResult = await getReferences({ per_page: 1, is_read: false }); return { all: allResult.total, read: readResult.total, unread: unreadResult.total, }; } catch (error) { console.error('[ReferenceBoxActions] getReferenceSummary error:', error); return null; } } /** * 열람 처리 */ export async function markAsRead(id: string): Promise<{ success: boolean; error?: string }> { try { const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/read`; const { response, error } = await serverFetch(url, { method: 'POST', body: JSON.stringify({}), }); // serverFetch handles 401 with redirect if (error || !response) { return { success: false, error: error?.message || '열람 처리에 실패했습니다.', }; } const result = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '열람 처리에 실패했습니다.', }; } return { success: true }; } catch (error) { console.error('[ReferenceBoxActions] markAsRead error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } /** * 미열람 처리 */ export async function markAsUnread(id: string): Promise<{ success: boolean; error?: string }> { try { const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/unread`; const { response, error } = await serverFetch(url, { method: 'POST', body: JSON.stringify({}), }); // serverFetch handles 401 with redirect if (error || !response) { return { success: false, error: error?.message || '미열람 처리에 실패했습니다.', }; } const result = await response.json(); if (!response.ok || !result.success) { return { success: false, error: result.message || '미열람 처리에 실패했습니다.', }; } return { success: true }; } catch (error) { console.error('[ReferenceBoxActions] markAsUnread error:', error); return { success: false, error: '서버 오류가 발생했습니다.', }; } } /** * 일괄 열람 처리 */ export async function markAsReadBulk(ids: string[]): Promise<{ success: boolean; failedIds?: string[]; error?: string }> { const failedIds: string[] = []; for (const id of ids) { const result = await markAsRead(id); if (!result.success) { failedIds.push(id); } } if (failedIds.length > 0) { return { success: false, failedIds, error: `${failedIds.length}건의 열람 처리에 실패했습니다.`, }; } return { success: true }; } /** * 일괄 미열람 처리 */ export async function markAsUnreadBulk(ids: string[]): Promise<{ success: boolean; failedIds?: string[]; error?: string }> { const failedIds: string[] = []; for (const id of ids) { const result = await markAsUnread(id); if (!result.success) { failedIds.push(id); } } if (failedIds.length > 0) { return { success: false, failedIds, error: `${failedIds.length}건의 미열람 처리에 실패했습니다.`, }; } return { success: true }; }