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:
@@ -15,9 +15,9 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type {
|
||||
AttendanceRecord,
|
||||
AttendanceApiData,
|
||||
@@ -30,12 +30,6 @@ import type {
|
||||
// API 응답 타입 정의
|
||||
// ============================================
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface PaginatedResponse<T> {
|
||||
current_page: number;
|
||||
data: T[];
|
||||
@@ -176,34 +170,16 @@ interface EmployeeApiData {
|
||||
position_key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 목록 조회 (근태 등록용)
|
||||
*/
|
||||
export async function getEmployeesForAttendance(): Promise<EmployeeOption[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('per_page', '100'); // API 최대 500, 드롭다운용 100 충분
|
||||
searchParams.set('status', 'active'); // 재직자만
|
||||
const result = await executeServerAction<PaginatedResponse<EmployeeApiData>>({
|
||||
url: `${API_URL}/v1/employees?per_page=100&status=active`,
|
||||
errorMessage: '사원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
const url = `${API_URL}/v1/employees?${searchParams.toString()}`;
|
||||
if (!result.success || !result.data?.data) return [];
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET employees error:', error?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<EmployeeApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[AttendanceActions] No employees data');
|
||||
return [];
|
||||
}
|
||||
|
||||
// API는 TenantUserProfile을 반환하지만, Attendance.user_id는 User.id를 참조
|
||||
// 따라서 user.id를 사용해야 함
|
||||
return result.data.data.map((emp) => ({
|
||||
id: String(emp.user?.id || emp.user_id), // User.id 사용
|
||||
id: String(emp.user?.id || emp.user_id),
|
||||
name: emp.user?.name || emp.name,
|
||||
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
|
||||
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
|
||||
@@ -215,51 +191,29 @@ export async function getEmployeesForAttendance(): Promise<EmployeeOption[]> {
|
||||
// 근태 API 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 근태 목록 조회
|
||||
*/
|
||||
export async function getAttendances(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
user_id?: string;
|
||||
date?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
status?: string;
|
||||
department_id?: string;
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
page?: number; per_page?: number; user_id?: string; date?: string;
|
||||
date_from?: string; date_to?: string; status?: string;
|
||||
department_id?: string; sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: AttendanceRecord[]; total: number; lastPage: number }> {
|
||||
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?.user_id) searchParams.set('user_id', params.user_id);
|
||||
if (params?.date) searchParams.set('date', params.date);
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
const url = `${API_URL}/v1/attendances?${searchParams.toString()}`;
|
||||
const result = await executeServerAction<PaginatedResponse<AttendanceApiData>>({
|
||||
url: `${API_URL}/v1/attendances?${searchParams.toString()}`,
|
||||
errorMessage: '근태 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET list error:', error?.message);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<AttendanceApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[AttendanceActions] No data in response');
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
if (!result.success || !result.data?.data) return { data: [], total: 0, lastPage: 1 };
|
||||
|
||||
return {
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
@@ -268,172 +222,86 @@ export async function getAttendances(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 상세 조회
|
||||
*/
|
||||
export async function getAttendanceById(id: string): Promise<AttendanceRecord | null> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET attendance error:', error?.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<AttendanceApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
transform: (data: AttendanceApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '근태 조회에 실패했습니다.',
|
||||
});
|
||||
return result.success ? result.data || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 등록
|
||||
*/
|
||||
export async function createAttendance(
|
||||
data: AttendanceFormData
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[AttendanceActions] POST attendance request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances`, {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
body: apiData,
|
||||
transform: (d: AttendanceApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '근태 등록에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] POST attendance response:', result);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 등록에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 수정
|
||||
*/
|
||||
export async function updateAttendance(
|
||||
id: string,
|
||||
data: AttendanceFormData
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
console.log('[AttendanceActions] PATCH attendance request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(apiData),
|
||||
body: apiData,
|
||||
transform: (d: AttendanceApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '근태 수정에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] PATCH attendance response:', result);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 삭제
|
||||
*/
|
||||
export async function deleteAttendance(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, { method: 'DELETE' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] DELETE attendance response:', result);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 일괄 삭제
|
||||
*/
|
||||
export async function deleteAttendances(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/bulk-delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '근태 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '근태 일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[AttendanceActions] BULK DELETE attendance response:', result);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '근태 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
export async function deleteAttendances(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/bulk-delete`,
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '근태 일괄 삭제에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 월간 통계 조회
|
||||
*/
|
||||
export async function getMonthlyStats(params: {
|
||||
year: number;
|
||||
month: number;
|
||||
user_id?: string;
|
||||
year: number; month: number; user_id?: string;
|
||||
}): Promise<AttendanceStats | null> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.set('year', String(params.year));
|
||||
searchParams.set('month', String(params.month));
|
||||
if (params.user_id) searchParams.set('user_id', params.user_id);
|
||||
|
||||
const url = `${API_URL}/v1/attendances/monthly-stats?${searchParams.toString()}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
console.error('[AttendanceActions] GET monthly-stats error:', error?.message);
|
||||
return null;
|
||||
interface MonthlyStatsApiData {
|
||||
year: number; month: number; total_days: number;
|
||||
by_status: {
|
||||
onTime: number; late: number; absent: number; vacation: number;
|
||||
businessTrip: number; fieldWork: number; overtime: number; remote: number;
|
||||
};
|
||||
total_work_minutes: number; total_overtime_minutes: number;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const result = await executeServerAction<MonthlyStatsApiData>({
|
||||
url: `${API_URL}/v1/attendances/monthly-stats?${searchParams.toString()}`,
|
||||
errorMessage: '월간 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
if (!result.success || !result.data) return null;
|
||||
|
||||
return {
|
||||
year: result.data.year,
|
||||
@@ -446,23 +314,14 @@ export async function getMonthlyStats(params: {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 엑셀 내보내기
|
||||
// 엑셀 내보내기 (native fetch - keep as-is)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 근태 엑셀 내보내기
|
||||
*/
|
||||
export async function exportAttendanceExcel(params?: {
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
user_id?: string;
|
||||
status?: string;
|
||||
department_id?: string;
|
||||
date_from?: string; date_to?: string; user_id?: string;
|
||||
status?: string; department_id?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: Blob;
|
||||
filename?: string;
|
||||
error?: string;
|
||||
success: boolean; data?: Blob; filename?: string; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
@@ -478,25 +337,16 @@ export async function exportAttendanceExcel(params?: {
|
||||
if (params?.date_from) searchParams.set('date_from', params.date_from);
|
||||
if (params?.date_to) searchParams.set('date_to', params.date_to);
|
||||
if (params?.user_id) searchParams.set('user_id', params.user_id);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/attendances/export${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
const response = await fetch(url, { method: 'GET', headers });
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[AttendanceActions] GET export error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { success: false, error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
@@ -504,17 +354,9 @@ export async function exportAttendanceExcel(params?: {
|
||||
const filenameMatch = contentDisposition?.match(/filename="?(.+)"?/);
|
||||
const filename = filenameMatch?.[1] || `근태현황_${params?.date_from || 'all'}.xlsx`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: blob,
|
||||
filename,
|
||||
};
|
||||
return { success: true, data: blob, filename };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[AttendanceActions] exportAttendanceExcel error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Card, CardFormData, CardStatus } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -13,10 +13,7 @@ interface TenantProfile {
|
||||
department_id: number | null;
|
||||
position_key: string | null;
|
||||
job_title_key: string | null;
|
||||
department?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
department?: { id: number; name: string } | null;
|
||||
}
|
||||
|
||||
interface CardApiData {
|
||||
@@ -38,22 +35,12 @@ interface CardApiData {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface CardListResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
data: CardApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CardResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: CardApiData;
|
||||
interface CardPaginationData {
|
||||
data: CardApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// API URL (without double /api)
|
||||
@@ -71,7 +58,6 @@ function mapFrontendStatusToApi(frontendStatus: CardStatus): 'active' | 'inactiv
|
||||
|
||||
// API → Frontend 변환
|
||||
function transformApiToFrontend(apiData: CardApiData): Card {
|
||||
// TenantProfile에서 부서/직책 정보 추출
|
||||
const profile = apiData.assigned_user?.tenant_profiles?.[0];
|
||||
const department = profile?.department;
|
||||
|
||||
@@ -80,8 +66,8 @@ function transformApiToFrontend(apiData: CardApiData): Card {
|
||||
cardCompany: apiData.card_company as Card['cardCompany'],
|
||||
cardNumber: `****-****-****-${apiData.card_number_last4}`,
|
||||
cardName: apiData.card_name,
|
||||
expiryDate: apiData.expiry_date ? apiData.expiry_date.replace('/', '') : '', // MM/YY → MMYY
|
||||
pinPrefix: '**', // 보안상 표시 안함
|
||||
expiryDate: apiData.expiry_date ? apiData.expiry_date.replace('/', '') : '',
|
||||
pinPrefix: '**',
|
||||
status: mapApiStatusToFrontend(apiData.status),
|
||||
user: apiData.assigned_user ? {
|
||||
id: String(apiData.assigned_user.id),
|
||||
@@ -90,7 +76,7 @@ function transformApiToFrontend(apiData: CardApiData): Card {
|
||||
employeeId: String(apiData.assigned_user.id),
|
||||
employeeName: apiData.assigned_user.name,
|
||||
positionId: profile?.position_key || '',
|
||||
positionName: profile?.position_key || '', // position_key를 그대로 사용 (별도 조회 필요시 추가)
|
||||
positionName: profile?.position_key || '',
|
||||
} : undefined,
|
||||
createdAt: apiData.created_at,
|
||||
updatedAt: apiData.updated_at,
|
||||
@@ -102,24 +88,21 @@ function transformFrontendToApi(data: CardFormData): Record<string, unknown> {
|
||||
const apiData: Record<string, unknown> = {
|
||||
card_company: data.cardCompany,
|
||||
expiry_date: data.expiryDate.length === 4
|
||||
? `${data.expiryDate.slice(0, 2)}/${data.expiryDate.slice(2)}` // MMYY → MM/YY
|
||||
? `${data.expiryDate.slice(0, 2)}/${data.expiryDate.slice(2)}`
|
||||
: data.expiryDate,
|
||||
card_name: data.cardName,
|
||||
status: mapFrontendStatusToApi(data.status),
|
||||
};
|
||||
|
||||
// 카드번호가 마스킹되지 않은 경우에만 전송 (수정 시 기존 값 유지)
|
||||
const cardNumberDigits = data.cardNumber.replace(/-/g, '');
|
||||
if (cardNumberDigits && !cardNumberDigits.includes('*')) {
|
||||
apiData.card_number = cardNumberDigits;
|
||||
}
|
||||
|
||||
// 비밀번호가 있으면 추가
|
||||
if (data.pinPrefix && data.pinPrefix !== '**') {
|
||||
apiData.card_password = data.pinPrefix;
|
||||
}
|
||||
|
||||
// 사용자 ID가 있으면 추가
|
||||
if (data.userId) {
|
||||
apiData.assigned_user_id = parseInt(data.userId, 10);
|
||||
}
|
||||
@@ -127,33 +110,24 @@ function transformFrontendToApi(data: CardFormData): Record<string, unknown> {
|
||||
return apiData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 목록 조회
|
||||
*/
|
||||
// ===== 카드 목록 조회 =====
|
||||
export async function getCards(params?: {
|
||||
search?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string; status?: string; page?: number; per_page?: number;
|
||||
}): Promise<{ success: boolean; data?: Card[]; pagination?: { total: number; currentPage: number; lastPage: number }; error?: string }> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', mapFrontendStatusToApi(params.status as CardStatus));
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const url = `${API_URL}/v1/cards${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
const result = await executeServerAction<CardPaginationData>({
|
||||
url: `${API_URL}/v1/cards${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '카드 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardListResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 목록을 불러오는데 실패했습니다.' };
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -167,168 +141,94 @@ export async function getCards(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 상세 조회
|
||||
*/
|
||||
export async function getCard(id: string): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
// ===== 카드 상세 조회 =====
|
||||
export async function getCard(id: string): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
transform: (data: CardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '카드 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 등록
|
||||
*/
|
||||
export async function createCard(data: CardFormData): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards`, {
|
||||
// ===== 카드 등록 =====
|
||||
export async function createCard(data: CardFormData): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: CardApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '카드 등록에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 수정
|
||||
*/
|
||||
export async function updateCard(id: string, data: CardFormData): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, {
|
||||
// ===== 카드 수정 =====
|
||||
export async function updateCard(id: string, data: CardFormData): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: CardApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '카드 수정에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 삭제
|
||||
*/
|
||||
export async function deleteCard(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, { method: 'DELETE' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '카드 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '카드 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
// ===== 카드 삭제 =====
|
||||
export async function deleteCard(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '카드 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 일괄 삭제
|
||||
*/
|
||||
// ===== 카드 일괄 삭제 =====
|
||||
export async function deleteCards(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const results = await Promise.all(ids.map(id => deleteCard(id)));
|
||||
const failed = results.filter(r => !r.success);
|
||||
|
||||
if (failed.length > 0) {
|
||||
return { success: false, error: `${failed.length}개의 카드 삭제에 실패했습니다.` };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteCards] Error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 상태 토글
|
||||
*/
|
||||
export async function toggleCardStatus(id: string): Promise<{ success: boolean; data?: Card; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}/toggle`, { method: 'PATCH' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: CardResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
// ===== 카드 상태 토글 =====
|
||||
export async function toggleCardStatus(id: string): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}/toggle`,
|
||||
method: 'PATCH',
|
||||
transform: (data: CardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 직원 목록 조회 (카드 할당용)
|
||||
* 주의: Card.assigned_user_id는 User.id를 참조하므로 user.id를 반환해야 함
|
||||
*/
|
||||
// ===== 활성 직원 목록 조회 (카드 할당용) =====
|
||||
export async function getActiveEmployees(): Promise<{ success: boolean; data?: Array<{ id: string; label: string }>; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/employees?status=active&per_page=50`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '직원 목록을 불러오는데 실패했습니다.' };
|
||||
interface EmployeePaginationData {
|
||||
data: Array<{
|
||||
id: number;
|
||||
user_id: number;
|
||||
user?: { id: number; name: string };
|
||||
department?: { name: string };
|
||||
position_key?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const result = await executeServerAction<EmployeePaginationData>({
|
||||
url: `${API_URL}/v1/employees?status=active&per_page=50`,
|
||||
errorMessage: '직원 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '직원 목록을 불러오는데 실패했습니다.' };
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
|
||||
// API는 TenantUserProfile을 반환하지만, Card.assigned_user_id는 User.id를 참조
|
||||
// 따라서 user.id를 사용해야 함
|
||||
const employees = result.data.data.map((emp: {
|
||||
id: number;
|
||||
user_id: number;
|
||||
user?: { id: number; name: string };
|
||||
department?: { name: string };
|
||||
position_key?: string;
|
||||
}) => ({
|
||||
id: String(emp.user?.id || emp.user_id), // User.id 사용
|
||||
const employees = result.data.data.map(emp => ({
|
||||
id: String(emp.user?.id || emp.user_id),
|
||||
label: `${emp.department?.name || ''} / ${emp.user?.name || ''} / ${emp.position_key || ''}`.replace(/^ \/ | \/ $/g, '').replace(/ \/ $/g, ''),
|
||||
}));
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -83,17 +83,10 @@ export interface UpdateDepartmentRequest {
|
||||
parentId?: number | null; // null이면 최상위로 이동
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
@@ -130,36 +123,18 @@ function transformApiToFrontend(apiData: ApiDepartment, depth: number = 0): Depa
|
||||
*/
|
||||
export async function getDepartmentTree(params?: {
|
||||
withUsers?: boolean;
|
||||
}): Promise<{ success: boolean; data?: DepartmentRecord[]; error?: string }> {
|
||||
}): Promise<ActionResult<DepartmentRecord[]>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params?.withUsers) {
|
||||
queryParams.append('with_users', '1');
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
const url = `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 트리 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment[]> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
const transformed = result.data.map((dept) => transformApiToFrontend(dept, 0));
|
||||
return {
|
||||
success: true,
|
||||
data: transformed,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 트리 조회에 실패했습니다.',
|
||||
};
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: ApiDepartment[]) => data.map((dept) => transformApiToFrontend(dept, 0)),
|
||||
errorMessage: '부서 트리 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,26 +143,12 @@ export async function getDepartmentTree(params?: {
|
||||
*/
|
||||
export async function getDepartmentById(
|
||||
id: number
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 조회에 실패했습니다.',
|
||||
};
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,36 +157,21 @@ export async function getDepartmentById(
|
||||
*/
|
||||
export async function createDepartment(
|
||||
data: CreateDepartmentRequest
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments`, {
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
body: {
|
||||
parent_id: data.parentId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
},
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 생성에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 생성에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,36 +181,21 @@ export async function createDepartment(
|
||||
export async function updateDepartment(
|
||||
id: number,
|
||||
data: UpdateDepartmentRequest
|
||||
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, {
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
parent_id: data.parentId === null ? 0 : data.parentId, // null이면 0(최상위)으로 전환
|
||||
body: {
|
||||
parent_id: data.parentId === null ? 0 : data.parentId,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
|
||||
sort_order: data.sortOrder,
|
||||
}),
|
||||
},
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 수정에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<ApiDepartment> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 수정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,23 +204,12 @@ export async function updateDepartment(
|
||||
*/
|
||||
export async function deleteDepartment(
|
||||
id: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'DELETE' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '부서 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<{ id: number; deleted_at: string }> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '부서 삭제에 실패했습니다.',
|
||||
};
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '부서 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper';
|
||||
import type { Employee, EmployeeFormData, EmployeeStats } from './types';
|
||||
import { transformApiToFrontend, transformFrontendToApi, type EmployeeApiData } from './utils';
|
||||
|
||||
@@ -40,332 +40,138 @@ interface PaginatedResponse<T> {
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ============================================
|
||||
// API 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 직원 목록 조회
|
||||
*/
|
||||
export async function getEmployees(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
q?: string;
|
||||
status?: string;
|
||||
department_id?: string;
|
||||
has_account?: boolean;
|
||||
sort_by?: string;
|
||||
sort_dir?: 'asc' | 'desc';
|
||||
page?: number; per_page?: number; q?: string; status?: string;
|
||||
department_id?: string; has_account?: boolean;
|
||||
sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: Employee[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
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?.q) searchParams.set('q', params.q);
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.has_account !== undefined) searchParams.set('has_account', String(params.has_account));
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
if (params?.q) searchParams.set('q', params.q);
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.department_id) searchParams.set('department_id', params.department_id);
|
||||
if (params?.has_account !== undefined) {
|
||||
searchParams.set('has_account', String(params.has_account));
|
||||
}
|
||||
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
|
||||
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
|
||||
const result = await executeServerAction<PaginatedResponse<EmployeeApiData>>({
|
||||
url: `${API_URL}/api/v1/employees?${searchParams.toString()}`,
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
|
||||
if (result.__authError) return { data: [], total: 0, lastPage: 1, __authError: true };
|
||||
if (!result.success || !result.data?.data) return { data: [], total: 0, lastPage: 1 };
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
// 🚨 401 인증 에러 → 클라이언트에서 로그인 페이지로 리다이렉트
|
||||
if (error?.__authError) {
|
||||
return { data: [], total: 0, lastPage: 1, __authError: true };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.error('[EmployeeActions] GET list error:', response?.status);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<EmployeeApiData>> = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.data) {
|
||||
console.warn('[EmployeeActions] 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) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] getEmployees error:', error);
|
||||
return { data: [], total: 0, lastPage: 1 };
|
||||
}
|
||||
return {
|
||||
data: result.data.data.map(transformApiToFrontend),
|
||||
total: result.data.total,
|
||||
lastPage: result.data.last_page,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 상세 조회
|
||||
*/
|
||||
export async function getEmployeeById(id: string): Promise<Employee | null | { __authError: true }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
transform: (data: EmployeeApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '직원 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { __authError: true };
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.error('[EmployeeActions] GET employee error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: ApiResponse<EmployeeApiData> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformApiToFrontend(result.data);
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] getEmployeeById error:', error);
|
||||
return null;
|
||||
}
|
||||
if (result.__authError) return { __authError: true };
|
||||
return result.success ? result.data || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 등록
|
||||
*/
|
||||
export async function createEmployee(
|
||||
data: EmployeeFormData
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: Employee;
|
||||
error?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
status?: number;
|
||||
__authError?: boolean;
|
||||
success: boolean; data?: Employee; error?: string;
|
||||
errors?: Record<string, string[]>; status?: number; __authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees`;
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees`,
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: EmployeeApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '직원 등록에 실패했습니다.',
|
||||
});
|
||||
|
||||
console.log('[EmployeeActions] POST employee request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] POST employee response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '직원 등록에 실패했습니다.',
|
||||
errors: result.error?.details, // validation errors: error.details 구조
|
||||
status: result.error?.code || response.status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] createEmployee error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 수정
|
||||
*/
|
||||
export async function updateEmployee(
|
||||
id: string,
|
||||
data: EmployeeFormData
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
transform: (d: EmployeeApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '직원 수정에 실패했습니다.',
|
||||
});
|
||||
|
||||
console.log('[EmployeeActions] PATCH employee request:', apiData);
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] PATCH employee response:', result);
|
||||
|
||||
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('[EmployeeActions] updateEmployee error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 삭제 (퇴직 처리)
|
||||
*/
|
||||
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'DELETE' });
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '직원 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] DELETE employee response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '직원 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] deleteEmployee error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 일괄 삭제
|
||||
*/
|
||||
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/bulk-delete`;
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
|
||||
});
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/bulk-delete`,
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '직원 일괄 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[EmployeeActions] BULK DELETE employee response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '직원 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] deleteEmployees error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
interface EmployeeStatsApiData {
|
||||
active_count: number;
|
||||
leave_count: number;
|
||||
resigned_count: number;
|
||||
average_tenure: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 통계 조회
|
||||
*/
|
||||
export async function getEmployeeStats(): Promise<EmployeeStats | null | { __authError: true }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/stats`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
const result = await executeServerAction<EmployeeStatsApiData>({
|
||||
url: `${API_URL}/api/v1/employees/stats`,
|
||||
errorMessage: '직원 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (error?.__authError) {
|
||||
return { __authError: true };
|
||||
}
|
||||
if (result.__authError) return { __authError: true };
|
||||
if (!result.success || !result.data) return null;
|
||||
|
||||
if (!response || !response.ok) {
|
||||
console.error('[EmployeeActions] GET stats error:', response?.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
activeCount: result.data.active_count ?? 0,
|
||||
leaveCount: result.data.leave_count ?? 0,
|
||||
resignedCount: result.data.resigned_count ?? 0,
|
||||
averageTenure: result.data.average_tenure ?? 0,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] getEmployeeStats error:', error);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
activeCount: result.data.active_count ?? 0,
|
||||
leaveCount: result.data.leave_count ?? 0,
|
||||
resignedCount: result.data.resigned_count ?? 0,
|
||||
averageTenure: result.data.average_tenure ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 직급/직책 조회 (positions)
|
||||
// 직급/직책 조회 (native fetch - keep as-is)
|
||||
// ============================================
|
||||
|
||||
export interface PositionItem {
|
||||
@@ -377,47 +183,19 @@ export interface PositionItem {
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 직급/직책 목록 조회
|
||||
* @param type 'rank' (직급) | 'title' (직책) | undefined (전체)
|
||||
*/
|
||||
export async function getPositions(type?: 'rank' | 'title'): Promise<PositionItem[]> {
|
||||
try {
|
||||
const headers = await getServerApiHeaders();
|
||||
const searchParams = new URLSearchParams();
|
||||
if (type) {
|
||||
searchParams.set('type', type);
|
||||
}
|
||||
const searchParams = new URLSearchParams();
|
||||
if (type) searchParams.set('type', type);
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions?${searchParams.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[EmployeeActions] GET positions error:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ApiResponse<PositionItem[]> = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] getPositions error:', error);
|
||||
return [];
|
||||
}
|
||||
const result = await executeServerAction<PositionItem[]>({
|
||||
url: `${API_URL}/api/v1/positions?${searchParams.toString()}`,
|
||||
errorMessage: '직급/직책 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || [];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 부서 조회 (departments)
|
||||
// 부서 조회 (native fetch - keep as-is)
|
||||
// ============================================
|
||||
|
||||
export interface DepartmentItem {
|
||||
@@ -428,45 +206,17 @@ export interface DepartmentItem {
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 목록 조회
|
||||
*/
|
||||
export async function getDepartments(): Promise<DepartmentItem[]> {
|
||||
try {
|
||||
const headers = await getServerApiHeaders();
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[EmployeeActions] GET departments error:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 페이지네이션 응답 또는 배열 직접 반환 모두 처리
|
||||
const departments = Array.isArray(result.data) ? result.data : result.data.data || [];
|
||||
return departments;
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] getDepartments error:', error);
|
||||
return [];
|
||||
}
|
||||
const result = await executeServerAction<DepartmentItem[] | { data: DepartmentItem[] }>({
|
||||
url: `${API_URL}/api/v1/departments`,
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.data) return [];
|
||||
return Array.isArray(result.data) ? result.data : result.data.data || [];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 파일 업로드
|
||||
// 파일 업로드 (native fetch - keep as-is)
|
||||
// ============================================
|
||||
|
||||
export async function uploadProfileImage(inputFormData: FormData): Promise<{
|
||||
@@ -479,16 +229,12 @@ export async function uploadProfileImage(inputFormData: FormData): Promise<{
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('access_token')?.value;
|
||||
|
||||
// 토큰 없으면 인증 에러
|
||||
if (!token) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
if (!token) return { success: false, __authError: true };
|
||||
|
||||
// 디렉토리 정보 추가
|
||||
inputFormData.append('directory', 'employees/profiles');
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/upload`,
|
||||
`${API_URL}/api/v1/files/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -499,29 +245,15 @@ export async function uploadProfileImage(inputFormData: FormData): Promise<{
|
||||
}
|
||||
);
|
||||
|
||||
// 🚨 401 인증 에러
|
||||
if (response.status === 401) {
|
||||
return { success: false, __authError: true };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: `파일 업로드 실패: ${response.status}` };
|
||||
}
|
||||
if (response.status === 401) return { success: false, __authError: true };
|
||||
if (!response.ok) return { success: false, error: `파일 업로드 실패: ${response.status}` };
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) return { success: false, error: result.message || '파일 업로드에 실패했습니다.' };
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '파일 업로드에 실패했습니다.' };
|
||||
}
|
||||
|
||||
// 업로드된 파일 경로 추출 (API 응답: file_path 필드)
|
||||
const uploadedPath = result.data?.file_path || result.data?.path || result.data?.url;
|
||||
if (!uploadedPath) return { success: false, error: '업로드된 파일 경로를 가져올 수 없습니다.' };
|
||||
|
||||
if (!uploadedPath) {
|
||||
return { success: false, error: '업로드된 파일 경로를 가져올 수 없습니다.' };
|
||||
}
|
||||
|
||||
// /storage/tenants/ 경로로 변환 (tenant disk 파일 접근 경로)
|
||||
const storagePath = uploadedPath.startsWith('/storage/')
|
||||
? uploadedPath
|
||||
: `/storage/tenants/${uploadedPath}`;
|
||||
@@ -529,13 +261,12 @@ export async function uploadProfileImage(inputFormData: FormData): Promise<{
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
url: `${process.env.NEXT_PUBLIC_API_URL}${storagePath}`,
|
||||
url: `${API_URL}${storagePath}`,
|
||||
path: uploadedPath,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[EmployeeActions] uploadProfileImage error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { SalaryRecord, SalaryDetail, PaymentStatus } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -46,46 +46,24 @@ interface SalaryApiData {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface SalaryListResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
data: SalaryApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
interface SalaryPaginationData {
|
||||
data: SalaryApiData[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface SalaryResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: SalaryApiData;
|
||||
}
|
||||
|
||||
interface StatisticsResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
total_net_payment: number;
|
||||
total_base_salary: number;
|
||||
total_allowance: number;
|
||||
total_overtime: number;
|
||||
total_bonus: number;
|
||||
total_deduction: number;
|
||||
count: number;
|
||||
scheduled_count: number;
|
||||
completed_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface BulkUpdateResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
updated_count: number;
|
||||
};
|
||||
interface StatisticsApiData {
|
||||
total_net_payment: number;
|
||||
total_base_salary: number;
|
||||
total_allowance: number;
|
||||
total_overtime: number;
|
||||
total_bonus: number;
|
||||
total_deduction: number;
|
||||
count: number;
|
||||
scheduled_count: number;
|
||||
completed_count: number;
|
||||
}
|
||||
|
||||
// API URL
|
||||
@@ -99,8 +77,8 @@ function transformApiToFrontend(apiData: SalaryApiData): SalaryRecord {
|
||||
employeeId: apiData.employee?.user_id || `EMP${String(apiData.employee_id).padStart(3, '0')}`,
|
||||
employeeName: apiData.employee?.name || '-',
|
||||
department: profile?.department?.name || '-',
|
||||
position: profile?.job_title_label || '-', // 직책 (팀장, 팀원)
|
||||
rank: profile?.rank || '-', // 직급 (부장, 과장, 대리)
|
||||
position: profile?.job_title_label || '-',
|
||||
rank: profile?.rank || '-',
|
||||
baseSalary: parseFloat(apiData.base_salary),
|
||||
allowance: parseFloat(apiData.total_allowance),
|
||||
overtime: parseFloat(apiData.total_overtime),
|
||||
@@ -126,8 +104,8 @@ function transformApiToDetail(apiData: SalaryApiData): SalaryDetail {
|
||||
employeeId: apiData.employee?.user_id || `EMP${String(apiData.employee_id).padStart(3, '0')}`,
|
||||
employeeName: apiData.employee?.name || '-',
|
||||
department: profile?.department?.name || '-',
|
||||
position: profile?.job_title_label || '-', // 직책 (팀장, 팀원)
|
||||
rank: profile?.rank || '-', // 직급 (부장, 과장, 대리)
|
||||
position: profile?.job_title_label || '-',
|
||||
rank: profile?.rank || '-',
|
||||
baseSalary: parseFloat(apiData.base_salary),
|
||||
allowances: {
|
||||
positionAllowance: allowanceDetails.position_allowance || 0,
|
||||
@@ -155,19 +133,11 @@ function transformApiToDetail(apiData: SalaryApiData): SalaryDetail {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 목록 조회
|
||||
*/
|
||||
// ===== 급여 목록 조회 =====
|
||||
export async function getSalaries(params?: {
|
||||
search?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
status?: string;
|
||||
employee_id?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string; year?: number; month?: number; status?: string;
|
||||
employee_id?: number; start_date?: string; end_date?: string;
|
||||
page?: number; per_page?: number;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: SalaryRecord[];
|
||||
@@ -175,7 +145,6 @@ export async function getSalaries(params?: {
|
||||
error?: string
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
@@ -185,19 +154,14 @@ export async function getSalaries(params?: {
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
if (params?.page) searchParams.set('page', String(params.page));
|
||||
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const url = `${API_URL}/v1/salaries${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
const result = await executeServerAction<SalaryPaginationData>({
|
||||
url: `${API_URL}/v1/salaries${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '급여 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '급여 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryListResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '급여 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -210,94 +174,49 @@ export async function getSalaries(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 상세 조회
|
||||
*/
|
||||
// ===== 급여 상세 조회 =====
|
||||
export async function getSalary(id: string): Promise<{
|
||||
success: boolean;
|
||||
data?: SalaryDetail;
|
||||
error?: string
|
||||
success: boolean; data?: SalaryDetail; error?: string
|
||||
}> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '급여 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '급여 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}`,
|
||||
transform: (data: SalaryApiData) => transformApiToDetail(data),
|
||||
errorMessage: '급여 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 상태 변경
|
||||
*/
|
||||
// ===== 급여 상태 변경 =====
|
||||
export async function updateSalaryStatus(
|
||||
id: string,
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; data?: SalaryRecord; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}/status`, {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}/status`,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
body: { status },
|
||||
transform: (data: SalaryApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 일괄 상태 변경
|
||||
*/
|
||||
// ===== 급여 일괄 상태 변경 =====
|
||||
export async function bulkUpdateSalaryStatus(
|
||||
ids: string[],
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/bulk-update-status`, {
|
||||
const result = await executeServerAction<{ updated_count: number }>({
|
||||
url: `${API_URL}/v1/salaries/bulk-update-status`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
ids: ids.map(id => parseInt(id, 10)),
|
||||
status
|
||||
}),
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), status },
|
||||
errorMessage: '일괄 상태 변경에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '일괄 상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: BulkUpdateResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '일괄 상태 변경에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount: result.data.updated_count,
|
||||
};
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return { success: true, updatedCount: result.data.updated_count };
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 수정 (수당/공제 항목 포함)
|
||||
*/
|
||||
// ===== 급여 수정 =====
|
||||
export async function updateSalary(
|
||||
id: string,
|
||||
data: {
|
||||
@@ -308,69 +227,41 @@ export async function updateSalary(
|
||||
payment_date?: string;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: SalaryDetail; error?: string }> {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}`, {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}`,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
body: data,
|
||||
transform: (d: SalaryApiData) => transformApiToDetail(d),
|
||||
errorMessage: '급여 수정에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '급여 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: SalaryResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '급여 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToDetail(result.data),
|
||||
};
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 통계 조회
|
||||
*/
|
||||
// ===== 급여 통계 조회 =====
|
||||
export async function getSalaryStatistics(params?: {
|
||||
year?: number;
|
||||
month?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
year?: number; month?: number; start_date?: string; end_date?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
totalNetPayment: number;
|
||||
totalBaseSalary: number;
|
||||
totalAllowance: number;
|
||||
totalOvertime: number;
|
||||
totalBonus: number;
|
||||
totalDeduction: number;
|
||||
count: number;
|
||||
scheduledCount: number;
|
||||
completedCount: number;
|
||||
totalNetPayment: number; totalBaseSalary: number; totalAllowance: number;
|
||||
totalOvertime: number; totalBonus: number; totalDeduction: number;
|
||||
count: number; scheduledCount: number; completedCount: number;
|
||||
};
|
||||
error?: string
|
||||
}> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params?.end_date) searchParams.set('end_date', params.end_date);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const url = `${API_URL}/v1/salaries/statistics${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
const result = await executeServerAction<StatisticsApiData>({
|
||||
url: `${API_URL}/v1/salaries/statistics${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '통계 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '통계 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: StatisticsResponse = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.message || '통계 정보를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -388,9 +279,7 @@ export async function getSalaryStatistics(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 엑셀 내보내기
|
||||
*/
|
||||
// ===== 급여 엑셀 내보내기 (native fetch - keep as-is) =====
|
||||
export async function exportSalaryExcel(params?: {
|
||||
year?: number;
|
||||
month?: number;
|
||||
@@ -433,11 +322,7 @@ export async function exportSalaryExcel(params?: {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[SalaryActions] GET export error:', response.status);
|
||||
return {
|
||||
success: false,
|
||||
error: `API 오류: ${response.status}`,
|
||||
};
|
||||
return { success: false, error: `API 오류: ${response.status}` };
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
@@ -445,17 +330,9 @@ export async function exportSalaryExcel(params?: {
|
||||
const filenameMatch = contentDisposition?.match(/filename="?(.+)"?/);
|
||||
const filename = filenameMatch?.[1] || `급여명세_${params?.year || 'all'}_${params?.month || 'all'}.xlsx`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: blob,
|
||||
filename,
|
||||
};
|
||||
return { success: true, data: blob, filename };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[SalaryActions] exportSalaryExcel error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,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';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -239,569 +238,200 @@ function transformBalanceToFrontend(apiData: Record<string, unknown>): LeaveBala
|
||||
// API 함수
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 휴가 목록 조회
|
||||
* GET /v1/leaves
|
||||
*/
|
||||
/** 휴가 목록 조회 */
|
||||
export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
success: boolean;
|
||||
data?: { items: LeaveRecord[]; total: number; currentPage: number; lastPage: number };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
if (params.userId) searchParams.append('user_id', params.userId.toString());
|
||||
if (params.status) searchParams.append('status', params.status);
|
||||
if (params.leaveType) searchParams.append('leave_type', params.leaveType);
|
||||
if (params.dateFrom) searchParams.append('date_from', params.dateFrom);
|
||||
if (params.dateTo) searchParams.append('date_to', params.dateTo);
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/leaves${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformApiToFrontend),
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 목록 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getLeaves] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 목록 조회에 실패했습니다.',
|
||||
};
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
if (params.userId) searchParams.append('user_id', params.userId.toString());
|
||||
if (params.status) searchParams.append('status', params.status);
|
||||
if (params.leaveType) searchParams.append('leave_type', params.leaveType);
|
||||
if (params.dateFrom) searchParams.append('date_from', params.dateFrom);
|
||||
if (params.dateTo) searchParams.append('date_to', params.dateTo);
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedResponse<Record<string, unknown>>>({
|
||||
url: `${API_URL}/v1/leaves${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '휴가 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformApiToFrontend),
|
||||
total: result.data.total, currentPage: result.data.current_page, lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 상세 조회
|
||||
* GET /v1/leaves/{id}
|
||||
*/
|
||||
export async function getLeaveById(id: number): Promise<{
|
||||
success: boolean;
|
||||
data?: LeaveRecord;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}`, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getLeaveById] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 휴가 상세 조회 */
|
||||
export async function getLeaveById(id: number): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}`,
|
||||
transform: (data: Record<string, unknown>) => transformApiToFrontend(data),
|
||||
errorMessage: '휴가 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 신청
|
||||
* POST /v1/leaves
|
||||
*/
|
||||
export async function createLeave(
|
||||
data: CreateLeaveRequest
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
leave_type: data.leaveType,
|
||||
start_date: data.startDate,
|
||||
end_date: data.endDate,
|
||||
days: data.days,
|
||||
reason: data.reason,
|
||||
}),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 신청에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 신청에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createLeave] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 신청에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 휴가 신청 */
|
||||
export async function createLeave(data: CreateLeaveRequest): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves`,
|
||||
method: 'POST',
|
||||
body: { user_id: data.userId, leave_type: data.leaveType, start_date: data.startDate, end_date: data.endDate, days: data.days, reason: data.reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
errorMessage: '휴가 신청에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 승인
|
||||
* POST /v1/leaves/{id}/approve
|
||||
*/
|
||||
export async function approveLeave(
|
||||
id: number,
|
||||
comment?: string
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/approve`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ comment }),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 승인에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 승인에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[approveLeave] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 승인에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 휴가 승인 */
|
||||
export async function approveLeave(id: number, comment?: string): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}/approve`,
|
||||
method: 'POST',
|
||||
body: { comment },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
errorMessage: '휴가 승인에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 반려
|
||||
* POST /v1/leaves/{id}/reject
|
||||
*/
|
||||
export async function rejectLeave(
|
||||
id: number,
|
||||
reason: string
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/reject`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason }),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 반려에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 반려에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[rejectLeave] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 반려에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 휴가 반려 */
|
||||
export async function rejectLeave(id: number, reason: string): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}/reject`,
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
errorMessage: '휴가 반려에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 취소
|
||||
* POST /v1/leaves/{id}/cancel
|
||||
*/
|
||||
export async function cancelLeave(
|
||||
id: number,
|
||||
reason?: string
|
||||
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/cancel`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason }),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 취소에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformApiToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 취소에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[cancelLeave] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 취소에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 휴가 취소 */
|
||||
export async function cancelLeave(id: number, reason?: string): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}/cancel`,
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
errorMessage: '휴가 취소에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 내 잔여 휴가 조회
|
||||
* GET /v1/leaves/balance
|
||||
*/
|
||||
export async function getMyLeaveBalance(year?: number): Promise<{
|
||||
success: boolean;
|
||||
data?: LeaveBalance;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = year
|
||||
? `${API_URL}/v1/leaves/balance?year=${year}`
|
||||
: `${API_URL}/v1/leaves/balance`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '잔여 휴가 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformBalanceToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '잔여 휴가 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getMyLeaveBalance] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '잔여 휴가 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 내 잔여 휴가 조회 */
|
||||
export async function getMyLeaveBalance(year?: number): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const url = year ? `${API_URL}/v1/leaves/balance?year=${year}` : `${API_URL}/v1/leaves/balance`;
|
||||
const result = await executeServerAction({
|
||||
url,
|
||||
transform: (data: Record<string, unknown>) => transformBalanceToFrontend(data),
|
||||
errorMessage: '잔여 휴가 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자 잔여 휴가 조회
|
||||
* GET /v1/leaves/balance/{userId}
|
||||
*/
|
||||
export async function getUserLeaveBalance(
|
||||
userId: number,
|
||||
year?: number
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: LeaveBalance;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = year
|
||||
? `${API_URL}/v1/leaves/balance/${userId}?year=${year}`
|
||||
: `${API_URL}/v1/leaves/balance/${userId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '잔여 휴가 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformBalanceToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '잔여 휴가 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getUserLeaveBalance] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '잔여 휴가 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 특정 사용자 잔여 휴가 조회 */
|
||||
export async function getUserLeaveBalance(userId: number, year?: number): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const url = year ? `${API_URL}/v1/leaves/balance/${userId}?year=${year}` : `${API_URL}/v1/leaves/balance/${userId}`;
|
||||
const result = await executeServerAction({
|
||||
url,
|
||||
transform: (data: Record<string, unknown>) => transformBalanceToFrontend(data),
|
||||
errorMessage: '잔여 휴가 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 잔여 휴가 설정 (부여)
|
||||
* PUT /v1/leaves/balance
|
||||
*/
|
||||
export async function setLeaveBalance(
|
||||
data: SetLeaveBalanceRequest
|
||||
): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/balance`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
year: data.year,
|
||||
total_days: data.totalDays,
|
||||
}),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '잔여 휴가 설정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformBalanceToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '잔여 휴가 설정에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[setLeaveBalance] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '잔여 휴가 설정에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 잔여 휴가 설정 (부여) */
|
||||
export async function setLeaveBalance(data: SetLeaveBalanceRequest): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/balance`,
|
||||
method: 'PUT',
|
||||
body: { user_id: data.userId, year: data.year, total_days: data.totalDays },
|
||||
transform: (d: Record<string, unknown>) => transformBalanceToFrontend(d),
|
||||
errorMessage: '잔여 휴가 설정에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 삭제
|
||||
* DELETE /v1/leaves/{id}
|
||||
*/
|
||||
/** 휴가 삭제 */
|
||||
export async function deleteLeave(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<null> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 삭제에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteLeave] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '휴가 삭제에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 휴가 일괄 승인
|
||||
*/
|
||||
/** 여러 휴가 일괄 승인 */
|
||||
export async function approveLeavesMany(ids: number[]): Promise<{
|
||||
success: boolean;
|
||||
results?: { id: number; success: boolean; error?: string }[];
|
||||
error?: string;
|
||||
success: boolean; results?: { id: number; success: boolean; error?: string }[]; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const result = await approveLeave(id);
|
||||
return { id, success: result.success, error: result.error };
|
||||
})
|
||||
);
|
||||
|
||||
const allSuccess = results.every((r) => r.success);
|
||||
return {
|
||||
success: allSuccess,
|
||||
results,
|
||||
error: allSuccess ? undefined : '일부 휴가 승인에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[approveLeavesMany] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 일괄 승인에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
const results = await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const r = await approveLeave(id);
|
||||
return { id, success: r.success, error: r.error };
|
||||
})
|
||||
);
|
||||
const allSuccess = results.every((r) => r.success);
|
||||
return { success: allSuccess, results, error: allSuccess ? undefined : '일부 휴가 승인에 실패했습니다.' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 휴가 일괄 반려
|
||||
*/
|
||||
export async function rejectLeavesMany(
|
||||
ids: number[],
|
||||
reason: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
results?: { id: number; success: boolean; error?: string }[];
|
||||
error?: string;
|
||||
/** 여러 휴가 일괄 반려 */
|
||||
export async function rejectLeavesMany(ids: number[], reason: string): Promise<{
|
||||
success: boolean; results?: { id: number; success: boolean; error?: string }[]; error?: string;
|
||||
}> {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const result = await rejectLeave(id, reason);
|
||||
return { id, success: result.success, error: result.error };
|
||||
})
|
||||
);
|
||||
|
||||
const allSuccess = results.every((r) => r.success);
|
||||
return {
|
||||
success: allSuccess,
|
||||
results,
|
||||
error: allSuccess ? undefined : '일부 휴가 반려에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[rejectLeavesMany] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 일괄 반려에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
const results = await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const r = await rejectLeave(id, reason);
|
||||
return { id, success: r.success, error: r.error };
|
||||
})
|
||||
);
|
||||
const allSuccess = results.every((r) => r.success);
|
||||
return { success: allSuccess, results, error: allSuccess ? undefined : '일부 휴가 반려에 실패했습니다.' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 직원 휴가 사용현황 조회
|
||||
* GET /v1/leaves/balances
|
||||
*/
|
||||
/** 전체 직원 휴가 사용현황 조회 */
|
||||
export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise<{
|
||||
success: boolean;
|
||||
data?: { items: LeaveBalanceRecord[]; total: number; currentPage: number; lastPage: number };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.search) searchParams.append('search', params.search);
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/leaves/balances${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 사용현황 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformBalanceRecordToFrontend),
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 사용현황 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getLeaveBalances] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 사용현황 조회에 실패했습니다.',
|
||||
};
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.search) searchParams.append('search', params.search);
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedResponse<Record<string, unknown>>>({
|
||||
url: `${API_URL}/v1/leaves/balances${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '휴가 사용현황 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformBalanceRecordToFrontend),
|
||||
total: result.data.total, currentPage: result.data.current_page, lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -888,147 +518,61 @@ export interface CreateLeaveGrantRequest {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여 이력 조회
|
||||
* GET /v1/leaves/grants
|
||||
*/
|
||||
/** 휴가 부여 이력 조회 */
|
||||
export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
success: boolean;
|
||||
data?: { items: LeaveGrantRecord[]; total: number; currentPage: number; lastPage: number };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
if (params.userId) searchParams.append('user_id', params.userId.toString());
|
||||
if (params.grantType) searchParams.append('grant_type', params.grantType);
|
||||
if (params.dateFrom) searchParams.append('date_from', params.dateFrom);
|
||||
if (params.dateTo) searchParams.append('date_to', params.dateTo);
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.search) searchParams.append('search', params.search);
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/leaves/grants${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 부여 이력 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformGrantRecordToFrontend),
|
||||
total: result.data.total,
|
||||
currentPage: result.data.current_page,
|
||||
lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 부여 이력 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getLeaveGrants] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 부여 이력 조회에 실패했습니다.',
|
||||
};
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params) {
|
||||
if (params.userId) searchParams.append('user_id', params.userId.toString());
|
||||
if (params.grantType) searchParams.append('grant_type', params.grantType);
|
||||
if (params.dateFrom) searchParams.append('date_from', params.dateFrom);
|
||||
if (params.dateTo) searchParams.append('date_to', params.dateTo);
|
||||
if (params.year) searchParams.append('year', params.year.toString());
|
||||
if (params.departmentId) searchParams.append('department_id', params.departmentId.toString());
|
||||
if (params.search) searchParams.append('search', params.search);
|
||||
if (params.sortBy) searchParams.append('sort_by', params.sortBy);
|
||||
if (params.sortDir) searchParams.append('sort_dir', params.sortDir);
|
||||
if (params.perPage) searchParams.append('per_page', params.perPage.toString());
|
||||
if (params.page) searchParams.append('page', params.page.toString());
|
||||
}
|
||||
const queryString = searchParams.toString();
|
||||
const result = await executeServerAction<PaginatedResponse<Record<string, unknown>>>({
|
||||
url: `${API_URL}/v1/leaves/grants${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '휴가 부여 이력 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.data.data.map(transformGrantRecordToFrontend),
|
||||
total: result.data.total, currentPage: result.data.current_page, lastPage: result.data.last_page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여
|
||||
* POST /v1/leaves/grants
|
||||
*/
|
||||
export async function createLeaveGrant(
|
||||
data: CreateLeaveGrantRequest
|
||||
): Promise<{ success: boolean; data?: LeaveGrantRecord; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/grants`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
user_id: data.userId,
|
||||
grant_type: data.grantType,
|
||||
grant_date: data.grantDate,
|
||||
grant_days: data.grantDays,
|
||||
reason: data.reason,
|
||||
}),
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 부여에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<Record<string, unknown>> = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
return {
|
||||
success: true,
|
||||
data: transformGrantRecordToFrontend(result.data),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 부여에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[createLeaveGrant] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 부여에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
/** 휴가 부여 */
|
||||
export async function createLeaveGrant(data: CreateLeaveGrantRequest): Promise<{ success: boolean; data?: LeaveGrantRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/grants`,
|
||||
method: 'POST',
|
||||
body: { user_id: data.userId, grant_type: data.grantType, grant_date: data.grantDate, grant_days: data.grantDays, reason: data.reason },
|
||||
transform: (d: Record<string, unknown>) => transformGrantRecordToFrontend(d),
|
||||
errorMessage: '휴가 부여에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가 부여 삭제
|
||||
* DELETE /v1/leaves/grants/{id}
|
||||
*/
|
||||
/** 휴가 부여 삭제 */
|
||||
export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/grants/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '휴가 부여 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<null> = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '휴가 부여 삭제에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[deleteLeaveGrant] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '휴가 부여 삭제에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/grants/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '휴가 부여 삭제에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1074,53 +618,25 @@ export interface EmployeeOption {
|
||||
position: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 직원 목록 조회 (휴가 신청/부여용)
|
||||
* GET /v1/employees
|
||||
*/
|
||||
export async function getActiveEmployees(): Promise<{
|
||||
success: boolean;
|
||||
data?: EmployeeOption[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${API_URL}/v1/employees?status=active&per_page=100`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '직원 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data?.data) {
|
||||
const employees: EmployeeOption[] = result.data.data.map((item: Record<string, unknown>) => {
|
||||
const user = item.user as Record<string, unknown> | undefined;
|
||||
const department = item.department as Record<string, unknown> | undefined;
|
||||
|
||||
return {
|
||||
id: String(item.id),
|
||||
userId: (item.user_id as number) ?? (user?.id as number) ?? item.id,
|
||||
name: (user?.name as string) ?? (item.name as string) ?? '',
|
||||
department: (department?.name as string) ?? '-',
|
||||
position: (item.position_label as string) ?? (item.position_key as string) ?? '-',
|
||||
};
|
||||
});
|
||||
|
||||
return { success: true, data: employees };
|
||||
}
|
||||
/** 활성 직원 목록 조회 (휴가 신청/부여용) */
|
||||
export async function getActiveEmployees(): Promise<{ success: boolean; data?: EmployeeOption[]; error?: string }> {
|
||||
interface EmployeePaginatedResponse { data: Record<string, unknown>[]; total: number }
|
||||
const result = await executeServerAction<EmployeePaginatedResponse>({
|
||||
url: `${API_URL}/v1/employees?status=active&per_page=100`,
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return { success: false, error: result.error };
|
||||
|
||||
const employees: EmployeeOption[] = result.data.data.map((item) => {
|
||||
const user = item.user as Record<string, unknown> | undefined;
|
||||
const department = item.department as Record<string, unknown> | undefined;
|
||||
return {
|
||||
success: false,
|
||||
error: result.message || '직원 목록 조회에 실패했습니다.',
|
||||
id: String(item.id),
|
||||
userId: (item.user_id as number) ?? (user?.id as number) ?? (item.id as number),
|
||||
name: (user?.name as string) ?? (item.name as string) ?? '',
|
||||
department: (department?.name as string) ?? '-',
|
||||
position: (item.position_label as string) ?? (item.position_key as string) ?? '-',
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[getActiveEmployees] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '직원 목록 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
});
|
||||
return { success: true, data: employees };
|
||||
}
|
||||
Reference in New Issue
Block a user