Files
sam-react-prod/src/components/hr/VacationManagement/actions.ts
byeongcheolryu 0d539628f3 chore(WEB): actions.ts 에러 핸들링 및 CEO 대시보드 개선
- 전체 모듈 actions.ts redirect 에러 핸들링 추가
- CEODashboard DetailModal 추가
- MonthlyExpenseSection 개선
- fetch-wrapper redirect 에러 처리
- redirect-error 유틸 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 18:41:15 +09:00

1126 lines
32 KiB
TypeScript

/**
* 휴가관리 서버 액션
*
* API Endpoints:
* - GET /api/v1/leaves - 휴가 목록 조회
* - GET /api/v1/leaves/{id} - 휴가 상세 조회
* - POST /api/v1/leaves - 휴가 신청
* - PATCH /api/v1/leaves/{id} - 휴가 수정
* - DELETE /api/v1/leaves/{id} - 휴가 삭제
* - POST /api/v1/leaves/{id}/approve - 휴가 승인
* - POST /api/v1/leaves/{id}/reject - 휴가 반려
* - POST /api/v1/leaves/{id}/cancel - 휴가 취소
* - GET /api/v1/leaves/balances - 전체 직원 휴가 사용현황 조회
* - GET /api/v1/leaves/balance - 내 잔여 휴가 조회
* - GET /api/v1/leaves/balance/{userId} - 특정 사용자 잔여 휴가 조회
* - PUT /api/v1/leaves/balance - 잔여 휴가 설정
* - GET /api/v1/leaves/grants - 휴가 부여 이력 조회
* - POST /api/v1/leaves/grants - 휴가 부여
* - DELETE /api/v1/leaves/grants/{id} - 휴가 부여 삭제
*/
'use server';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ============================================
// 타입 정의
// ============================================
/**
* 휴가 유형
*/
export type LeaveType = 'annual' | 'half_am' | 'half_pm' | 'sick' | 'family' | 'maternity' | 'parental';
/**
* 휴가 상태
*/
export type LeaveStatus = 'pending' | 'approved' | 'rejected' | 'cancelled';
/**
* 휴가 기록 응답 타입
*/
export interface LeaveRecord {
id: number;
userId: number;
leaveType: LeaveType;
startDate: string;
endDate: string;
days: number;
reason: string | null;
status: LeaveStatus;
approvedBy: number | null;
approvedAt: string | null;
rejectedReason: string | null;
createdAt: string;
updatedAt: string;
// 관계 데이터
user?: {
id: number;
name: string;
email: string;
};
// 사용자 프로필 (부서/직책/직급)
userProfile?: {
id: number;
department_id: number | null;
position_key: string | null;
job_title_key: string | null;
position_label: string | null;
job_title_label: string | null;
rank: string | null;
department?: {
id: number;
name: string;
} | null;
} | null;
}
/**
* 휴가 목록 조회 파라미터
*/
export interface GetLeavesParams {
userId?: number;
status?: LeaveStatus;
leaveType?: LeaveType;
dateFrom?: string;
dateTo?: string;
year?: number;
departmentId?: number;
sortBy?: 'created_at' | 'start_date' | 'end_date' | 'days' | 'status';
sortDir?: 'asc' | 'desc';
perPage?: number;
page?: number;
}
/**
* 휴가 신청 요청 데이터
*/
export interface CreateLeaveRequest {
userId?: number;
leaveType: LeaveType;
startDate: string;
endDate: string;
days: number;
reason?: string;
}
/**
* 잔여 휴가 정보
*/
export interface LeaveBalance {
userId: number;
year: number;
totalDays: number;
usedDays: number;
remainingDays: number;
}
/**
* 잔여 휴가 설정 요청 데이터
*/
export interface SetLeaveBalanceRequest {
userId: number;
year: number;
totalDays: number;
}
/**
* 전체 직원 휴가 사용현황 조회 파라미터
*/
export interface GetLeaveBalancesParams {
year?: number;
departmentId?: number;
search?: string;
sortBy?: 'user_id' | 'department';
sortDir?: 'asc' | 'desc';
perPage?: number;
page?: number;
}
/**
* 전체 직원 휴가 사용현황 레코드
*/
export interface LeaveBalanceRecord {
id: number;
userId: number;
employeeName: string;
email: string;
department: string;
position: string; // position_label (부장, 과장 등)
jobTitle: string; // job_title_label (팀장, 팀원 등) → 직책으로 사용
rank: string | null; // json_extra.rank → 직급으로 사용
hireDate: string | null;
displayName: string | null;
totalDays: number;
usedDays: number;
remainingDays: number;
}
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
interface PaginatedResponse<T> {
current_page: number;
data: T[];
total: number;
per_page: number;
last_page: number;
}
// API URL
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
/**
* API 응답에서 프론트엔드 형식으로 변환
*/
function transformApiToFrontend(apiData: Record<string, unknown>): LeaveRecord {
const user = apiData.user as Record<string, unknown> | undefined;
const userProfile = apiData.user_profile as Record<string, unknown> | undefined;
const department = userProfile?.department as Record<string, unknown> | undefined;
return {
id: apiData.id as number,
userId: apiData.user_id as number,
leaveType: apiData.leave_type as LeaveType,
startDate: apiData.start_date as string,
endDate: apiData.end_date as string,
days: apiData.days as number,
reason: apiData.reason as string | null,
status: apiData.status as LeaveStatus,
approvedBy: apiData.approved_by as number | null,
approvedAt: apiData.approved_at as string | null,
rejectedReason: apiData.rejected_reason as string | null,
createdAt: apiData.created_at as string,
updatedAt: apiData.updated_at as string,
user: user
? {
id: user.id as number,
name: user.name as string,
email: user.email as string,
}
: undefined,
userProfile: userProfile
? {
id: userProfile.id as number,
department_id: userProfile.department_id as number | null,
position_key: userProfile.position_key as string | null,
job_title_key: userProfile.job_title_key as string | null,
position_label: userProfile.position_label as string | null,
job_title_label: userProfile.job_title_label as string | null,
rank: userProfile.rank as string | null,
department: department
? { id: department.id as number, name: department.name as string }
: null,
}
: null,
};
}
/**
* 잔여 휴가 응답 변환
*/
function transformBalanceToFrontend(apiData: Record<string, unknown>): LeaveBalance {
return {
userId: apiData.user_id as number,
year: apiData.year as number,
totalDays: apiData.total_days as number,
usedDays: apiData.used_days as number,
remainingDays: apiData.remaining_days as number,
};
}
// ============================================
// 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 : '휴가 목록 조회에 실패했습니다.',
};
}
}
/**
* 휴가 상세 조회
* 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 : '휴가 조회에 실패했습니다.',
};
}
}
/**
* 휴가 신청
* 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 : '휴가 신청에 실패했습니다.',
};
}
}
/**
* 휴가 승인
* 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 : '휴가 승인에 실패했습니다.',
};
}
}
/**
* 휴가 반려
* 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 : '휴가 반려에 실패했습니다.',
};
}
}
/**
* 휴가 취소
* 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 : '휴가 취소에 실패했습니다.',
};
}
}
/**
* 내 잔여 휴가 조회
* 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 : '잔여 휴가 조회에 실패했습니다.',
};
}
}
/**
* 특정 사용자 잔여 휴가 조회
* 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 : '잔여 휴가 조회에 실패했습니다.',
};
}
}
/**
* 잔여 휴가 설정 (부여)
* 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 : '잔여 휴가 설정에 실패했습니다.',
};
}
}
/**
* 휴가 삭제
* 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 : '휴가 삭제에 실패했습니다.',
};
}
}
/**
* 여러 휴가 일괄 승인
*/
export async function approveLeavesMany(ids: number[]): 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 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 : '휴가 일괄 승인에 실패했습니다.',
};
}
}
/**
* 여러 휴가 일괄 반려
*/
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 : '휴가 일괄 반려에 실패했습니다.',
};
}
}
/**
* 전체 직원 휴가 사용현황 조회
* 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 : '휴가 사용현황 조회에 실패했습니다.',
};
}
}
/**
* 휴가 사용현황 레코드 응답 변환
*/
function transformBalanceRecordToFrontend(apiData: Record<string, unknown>): LeaveBalanceRecord {
const user = apiData.user as Record<string, unknown> | undefined;
const department = apiData.department as Record<string, unknown> | undefined;
const totalDays = (apiData.leave_balance_total as number | null) ?? 15; // 기본 15일
const usedDays = (apiData.leave_balance_used as number | null) ?? 0;
return {
id: apiData.id as number,
userId: apiData.user_id as number,
employeeName: (user?.name as string) ?? '',
email: (user?.email as string) ?? '',
department: (department?.name as string) ?? '-',
position: (apiData.position_label as string) ?? '-',
jobTitle: (apiData.job_title_label as string) ?? '-',
rank: (apiData.rank as string | null) ?? null, // json_extra.rank
hireDate: (apiData.hire_date as string | null) ?? null,
displayName: (apiData.display_name as string | null) ?? null,
totalDays,
usedDays,
remainingDays: totalDays - usedDays,
};
}
// ============================================
// 휴가 부여 관련 타입 및 API
// ============================================
/**
* 휴가 부여 유형
*/
export type LeaveGrantType = 'annual' | 'monthly' | 'reward' | 'condolence' | 'other';
/**
* 휴가 부여 이력 조회 파라미터
*/
export interface GetLeaveGrantsParams {
userId?: number;
grantType?: LeaveGrantType;
dateFrom?: string;
dateTo?: string;
year?: number;
departmentId?: number;
search?: string;
sortBy?: 'grant_date' | 'grant_days' | 'created_at';
sortDir?: 'asc' | 'desc';
perPage?: number;
page?: number;
}
/**
* 휴가 부여 레코드
*/
export interface LeaveGrantRecord {
id: number;
userId: number;
employeeName: string;
email: string;
department: string;
position: string; // position_label
jobTitle: string; // job_title_label → 직책
rank: string | null; // json_extra.rank → 직급
grantType: LeaveGrantType;
grantDate: string;
grantDays: number;
reason: string | null;
createdBy: number | null;
creatorName: string | null;
createdAt: string;
}
/**
* 휴가 부여 요청 데이터
*/
export interface CreateLeaveGrantRequest {
userId: number;
grantType: LeaveGrantType;
grantDate: string;
grantDays: number;
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 : '휴가 부여 이력 조회에 실패했습니다.',
};
}
}
/**
* 휴가 부여
* 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 : '휴가 부여에 실패했습니다.',
};
}
}
/**
* 휴가 부여 삭제
* 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 : '휴가 부여 삭제에 실패했습니다.',
};
}
}
/**
* 휴가 부여 이력 레코드 응답 변환
*/
function transformGrantRecordToFrontend(apiData: Record<string, unknown>): LeaveGrantRecord {
const user = apiData.user as Record<string, unknown> | undefined;
const tenantProfile = user?.tenant_profile as Record<string, unknown> | undefined;
const department = tenantProfile?.department as Record<string, unknown> | undefined;
const creator = apiData.creator as Record<string, unknown> | undefined;
return {
id: apiData.id as number,
userId: apiData.user_id as number,
employeeName: (user?.name as string) ?? '',
email: (user?.email as string) ?? '',
department: (department?.name as string) ?? '-',
position: (tenantProfile?.position_label as string) ?? '-',
jobTitle: (tenantProfile?.job_title_label as string) ?? '-',
rank: (tenantProfile?.rank as string | null) ?? null, // json_extra.rank
grantType: apiData.grant_type as LeaveGrantType,
grantDate: apiData.grant_date as string,
grantDays: apiData.grant_days as number,
reason: apiData.reason as string | null,
createdBy: apiData.created_by as number | null,
creatorName: (creator?.name as string) ?? null,
createdAt: apiData.created_at as string,
};
}
// ============================================
// 직원 목록 조회 (다이얼로그용)
// ============================================
/**
* 직원 선택 옵션 타입
*/
export interface EmployeeOption {
id: string;
userId: number;
name: string;
department: string;
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 };
}
return {
success: false,
error: result.message || '직원 목록 조회에 실패했습니다.',
};
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[getActiveEmployees] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '직원 목록 조회에 실패했습니다.',
};
}
}