- SearchableSelectionModal<T> 제네릭 컴포넌트 추출 (organisms) - 검색 모달 5개 리팩토링: SupplierSearch, QuotationSelect, SalesOrderSelect, OrderSelect, ItemSearch - shared-lookups API 유틸 추가 (거래처/품목/수주 등 공통 조회) - create-crud-service 확장 (lookup, search 메서드) - actions.ts 20+개 파일 lookup 패턴 통일 - 공통 페이지 패턴 가이드 문서 추가 - CLAUDE.md Common Component Usage Rules 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
635 lines
23 KiB
TypeScript
635 lines
23 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 { executeServerAction } from '@/lib/api/execute-server-action';
|
|
import type { PaginatedApiResponse } from '@/lib/api/types';
|
|
|
|
// ============================================
|
|
// 타입 정의
|
|
// ============================================
|
|
|
|
/**
|
|
* 휴가 유형
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// 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 함수
|
|
// ============================================
|
|
|
|
/** 휴가 목록 조회 */
|
|
export async function getLeaves(params?: GetLeavesParams): Promise<{
|
|
success: boolean;
|
|
data?: { items: LeaveRecord[]; total: number; currentPage: number; lastPage: number };
|
|
error?: string;
|
|
}> {
|
|
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<PaginatedApiResponse<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,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 휴가 상세 조회 */
|
|
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 };
|
|
}
|
|
|
|
/** 휴가 신청 */
|
|
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 };
|
|
}
|
|
|
|
/** 휴가 승인 */
|
|
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 };
|
|
}
|
|
|
|
/** 휴가 반려 */
|
|
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 };
|
|
}
|
|
|
|
/** 휴가 취소 */
|
|
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 };
|
|
}
|
|
|
|
/** 내 잔여 휴가 조회 */
|
|
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 };
|
|
}
|
|
|
|
/** 특정 사용자 잔여 휴가 조회 */
|
|
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 };
|
|
}
|
|
|
|
/** 잔여 휴가 설정 (부여) */
|
|
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 };
|
|
}
|
|
|
|
/** 휴가 삭제 */
|
|
export async function deleteLeave(id: number): Promise<{ success: boolean; error?: string }> {
|
|
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;
|
|
}> {
|
|
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;
|
|
}> {
|
|
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 : '일부 휴가 반려에 실패했습니다.' };
|
|
}
|
|
|
|
/** 전체 직원 휴가 사용현황 조회 */
|
|
export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise<{
|
|
success: boolean;
|
|
data?: { items: LeaveBalanceRecord[]; total: number; currentPage: number; lastPage: number };
|
|
error?: string;
|
|
}> {
|
|
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<PaginatedApiResponse<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,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 휴가 사용현황 레코드 응답 변환
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/** 휴가 부여 이력 조회 */
|
|
export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
|
success: boolean;
|
|
data?: { items: LeaveGrantRecord[]; total: number; currentPage: number; lastPage: number };
|
|
error?: string;
|
|
}> {
|
|
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<PaginatedApiResponse<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,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 휴가 부여 */
|
|
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 };
|
|
}
|
|
|
|
/** 휴가 부여 삭제 */
|
|
export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error?: string }> {
|
|
const result = await executeServerAction({
|
|
url: `${API_URL}/v1/leaves/grants/${id}`,
|
|
method: 'DELETE',
|
|
errorMessage: '휴가 부여 삭제에 실패했습니다.',
|
|
});
|
|
return { success: result.success, error: result.error };
|
|
}
|
|
|
|
/**
|
|
* 휴가 부여 이력 레코드 응답 변환
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/** 활성 직원 목록 조회 (휴가 신청/부여용) */
|
|
export async function getActiveEmployees(): Promise<{ success: boolean; data?: EmployeeOption[]; error?: string }> {
|
|
interface EmployeePaginatedApiResponse { data: Record<string, unknown>[]; total: number }
|
|
const result = await executeServerAction<EmployeePaginatedApiResponse>({
|
|
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 {
|
|
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) ?? '-',
|
|
};
|
|
});
|
|
return { success: true, data: employees };
|
|
} |