refactor(WEB): 전체 actions.ts에 공통 API 유틸 적용
- buildApiUrl / executePaginatedAction 패턴으로 전환 (40+ actions 파일) - 직접 URLSearchParams 조립 → buildApiUrl 유틸 사용 - 수동 페이지네이션 메타 변환 → executePaginatedAction 자동 처리 - HandoverReportDocumentModal, OrderDocumentModal 개선 - 급여관리 SalaryManagement 코드 개선 - CLAUDE.md Server Action 공통 유틸 규칙 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
@@ -31,9 +32,6 @@ import type {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답 데이터를 프론트엔드 형식으로 변환
|
||||
*/
|
||||
@@ -161,7 +159,7 @@ interface EmployeeApiData {
|
||||
|
||||
export async function getEmployeesForAttendance(): Promise<EmployeeOption[]> {
|
||||
const result = await executeServerAction<PaginatedApiResponse<EmployeeApiData>>({
|
||||
url: `${API_URL}/v1/employees?per_page=100&status=active`,
|
||||
url: buildApiUrl('/api/v1/employees', { per_page: 100, status: 'active' }),
|
||||
errorMessage: '사원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -185,20 +183,19 @@ export async function getAttendances(params?: {
|
||||
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?.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 result = await executeServerAction<PaginatedApiResponse<AttendanceApiData>>({
|
||||
url: `${API_URL}/v1/attendances?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/attendances', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
user_id: params?.user_id,
|
||||
date: params?.date,
|
||||
date_from: params?.date_from,
|
||||
date_to: params?.date_to,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
department_id: params?.department_id,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '근태 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -213,7 +210,7 @@ export async function getAttendances(params?: {
|
||||
|
||||
export async function getAttendanceById(id: string): Promise<AttendanceRecord | null> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
url: buildApiUrl(`/api/v1/attendances/${id}`),
|
||||
transform: (data: AttendanceApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '근태 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -225,7 +222,7 @@ export async function createAttendance(
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances`,
|
||||
url: buildApiUrl('/api/v1/attendances'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: AttendanceApiData) => transformApiToFrontend(d),
|
||||
@@ -240,7 +237,7 @@ export async function updateAttendance(
|
||||
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
url: buildApiUrl(`/api/v1/attendances/${id}`),
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
transform: (d: AttendanceApiData) => transformApiToFrontend(d),
|
||||
@@ -251,7 +248,7 @@ export async function updateAttendance(
|
||||
|
||||
export async function deleteAttendance(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/${id}`,
|
||||
url: buildApiUrl(`/api/v1/attendances/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '근태 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -260,7 +257,7 @@ export async function deleteAttendance(id: string): Promise<{ success: boolean;
|
||||
|
||||
export async function deleteAttendances(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/attendances/bulk-delete`,
|
||||
url: buildApiUrl('/api/v1/attendances/bulk-delete'),
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '근태 일괄 삭제에 실패했습니다.',
|
||||
@@ -271,11 +268,6 @@ export async function deleteAttendances(ids: string[]): Promise<{ success: boole
|
||||
export async function getMonthlyStats(params: {
|
||||
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);
|
||||
|
||||
interface MonthlyStatsApiData {
|
||||
year: number; month: number; total_days: number;
|
||||
by_status: {
|
||||
@@ -286,7 +278,11 @@ export async function getMonthlyStats(params: {
|
||||
}
|
||||
|
||||
const result = await executeServerAction<MonthlyStatsApiData>({
|
||||
url: `${API_URL}/v1/attendances/monthly-stats?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/attendances/monthly-stats', {
|
||||
year: params.year,
|
||||
month: params.month,
|
||||
user_id: params.user_id,
|
||||
}),
|
||||
errorMessage: '월간 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -322,15 +318,13 @@ export async function exportAttendanceExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
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?.department_id) searchParams.set('department_id', params.department_id);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_URL}/v1/attendances/export${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl('/api/v1/attendances/export', {
|
||||
date_from: params?.date_from,
|
||||
date_to: params?.date_to,
|
||||
user_id: params?.user_id,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
department_id: params?.department_id,
|
||||
});
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import type { Card, CardFormData, CardStatus } from './types';
|
||||
|
||||
@@ -43,9 +44,6 @@ interface CardPaginationData {
|
||||
total: number;
|
||||
}
|
||||
|
||||
// API URL (without double /api)
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
// 상태 매핑: API → Frontend
|
||||
function mapApiStatusToFrontend(apiStatus: 'active' | 'inactive'): CardStatus {
|
||||
return apiStatus === 'active' ? 'active' : 'suspended';
|
||||
@@ -114,15 +112,13 @@ function transformFrontendToApi(data: CardFormData): Record<string, unknown> {
|
||||
export async function getCards(params?: {
|
||||
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 result = await executeServerAction<CardPaginationData>({
|
||||
url: `${API_URL}/v1/cards${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/cards', {
|
||||
search: params?.search,
|
||||
status: params?.status && params.status !== 'all' ? mapFrontendStatusToApi(params.status as CardStatus) : undefined,
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
}),
|
||||
errorMessage: '카드 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -144,7 +140,7 @@ export async function getCards(params?: {
|
||||
// ===== 카드 상세 조회 =====
|
||||
export async function getCard(id: string): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}`),
|
||||
transform: (data: CardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '카드 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
@@ -153,7 +149,7 @@ export async function getCard(id: string): Promise<ActionResult<Card>> {
|
||||
// ===== 카드 등록 =====
|
||||
export async function createCard(data: CardFormData): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards`,
|
||||
url: buildApiUrl('/api/v1/cards'),
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: CardApiData) => transformApiToFrontend(d),
|
||||
@@ -164,7 +160,7 @@ export async function createCard(data: CardFormData): Promise<ActionResult<Card>
|
||||
// ===== 카드 수정 =====
|
||||
export async function updateCard(id: string, data: CardFormData): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}`),
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: CardApiData) => transformApiToFrontend(d),
|
||||
@@ -175,7 +171,7 @@ export async function updateCard(id: string, data: CardFormData): Promise<Action
|
||||
// ===== 카드 삭제 =====
|
||||
export async function deleteCard(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '카드 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -199,7 +195,7 @@ export async function deleteCards(ids: string[]): Promise<{ success: boolean; er
|
||||
// ===== 카드 상태 토글 =====
|
||||
export async function toggleCardStatus(id: string): Promise<ActionResult<Card>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/cards/${id}/toggle`,
|
||||
url: buildApiUrl(`/api/v1/cards/${id}/toggle`),
|
||||
method: 'PATCH',
|
||||
transform: (data: CardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '상태 변경에 실패했습니다.',
|
||||
@@ -219,7 +215,10 @@ export async function getActiveEmployees(): Promise<{ success: boolean; data?: A
|
||||
}
|
||||
|
||||
const result = await executeServerAction<EmployeePaginationData>({
|
||||
url: `${API_URL}/v1/employees?status=active&per_page=50`,
|
||||
url: buildApiUrl('/api/v1/employees', {
|
||||
status: 'active',
|
||||
per_page: 50,
|
||||
}),
|
||||
errorMessage: '직원 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -233,4 +232,4 @@ export async function getActiveEmployees(): Promise<{ success: boolean; data?: A
|
||||
}));
|
||||
|
||||
return { success: true, data: employees };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
|
||||
// ============================================
|
||||
// 타입 정의
|
||||
@@ -87,8 +88,6 @@ export interface UpdateDepartmentRequest {
|
||||
// 헬퍼 함수
|
||||
// ============================================
|
||||
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답을 프론트엔드 형식으로 변환 (재귀)
|
||||
*/
|
||||
@@ -124,14 +123,10 @@ function transformApiToFrontend(apiData: ApiDepartment, depth: number = 0): Depa
|
||||
export async function getDepartmentTree(params?: {
|
||||
withUsers?: boolean;
|
||||
}): Promise<ActionResult<DepartmentRecord[]>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.withUsers) {
|
||||
queryParams.append('with_users', '1');
|
||||
}
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/departments/tree', {
|
||||
with_users: params?.withUsers ? '1' : undefined,
|
||||
}),
|
||||
transform: (data: ApiDepartment[]) => data.map((dept) => transformApiToFrontend(dept, 0)),
|
||||
errorMessage: '부서 트리 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -145,7 +140,7 @@ export async function getDepartmentById(
|
||||
id: number
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/departments/${id}`),
|
||||
transform: (data: ApiDepartment) => transformApiToFrontend(data),
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -159,7 +154,7 @@ export async function createDepartment(
|
||||
data: CreateDepartmentRequest
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments`,
|
||||
url: buildApiUrl('/api/v1/departments'),
|
||||
method: 'POST',
|
||||
body: {
|
||||
parent_id: data.parentId,
|
||||
@@ -183,7 +178,7 @@ export async function updateDepartment(
|
||||
data: UpdateDepartmentRequest
|
||||
): Promise<ActionResult<DepartmentRecord>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/departments/${id}`),
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
parent_id: data.parentId === null ? 0 : data.parentId,
|
||||
@@ -206,7 +201,7 @@ export async function deleteDepartment(
|
||||
id: number
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/v1/departments/${id}`,
|
||||
url: buildApiUrl(`/api/v1/departments/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '부서 삭제에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
@@ -44,18 +45,17 @@ export async function getEmployees(params?: {
|
||||
department_id?: string; has_account?: boolean;
|
||||
sort_by?: string; sort_dir?: 'asc' | 'desc';
|
||||
}): Promise<{ data: Employee[]; total: number; lastPage: number; __authError?: boolean }> {
|
||||
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);
|
||||
|
||||
const result = await executeServerAction<PaginatedApiResponse<EmployeeApiData>>({
|
||||
url: `${API_URL}/api/v1/employees?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/employees', {
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
q: params?.q,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
department_id: params?.department_id,
|
||||
has_account: params?.has_account,
|
||||
sort_by: params?.sort_by,
|
||||
sort_dir: params?.sort_dir,
|
||||
}),
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function getEmployees(params?: {
|
||||
|
||||
export async function getEmployeeById(id: string): Promise<Employee | null | { __authError: true }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
url: buildApiUrl(`/api/v1/employees/${id}`),
|
||||
transform: (data: EmployeeApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '직원 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -88,7 +88,7 @@ export async function createEmployee(
|
||||
}> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees`,
|
||||
url: buildApiUrl('/api/v1/employees'),
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: EmployeeApiData) => transformApiToFrontend(d),
|
||||
@@ -105,7 +105,7 @@ export async function updateEmployee(
|
||||
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
url: buildApiUrl(`/api/v1/employees/${id}`),
|
||||
method: 'PATCH',
|
||||
body: apiData,
|
||||
transform: (d: EmployeeApiData) => transformApiToFrontend(d),
|
||||
@@ -118,7 +118,7 @@ export async function updateEmployee(
|
||||
|
||||
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/${id}`,
|
||||
url: buildApiUrl(`/api/v1/employees/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '직원 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export async function deleteEmployee(id: string): Promise<{ success: boolean; er
|
||||
|
||||
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/employees/bulk-delete`,
|
||||
url: buildApiUrl('/api/v1/employees/bulk-delete'),
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)) },
|
||||
errorMessage: '직원 일괄 삭제에 실패했습니다.',
|
||||
@@ -148,7 +148,7 @@ interface EmployeeStatsApiData {
|
||||
|
||||
export async function getEmployeeStats(): Promise<EmployeeStats | null | { __authError: true }> {
|
||||
const result = await executeServerAction<EmployeeStatsApiData>({
|
||||
url: `${API_URL}/api/v1/employees/stats`,
|
||||
url: buildApiUrl('/api/v1/employees/stats'),
|
||||
errorMessage: '직원 통계 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -177,11 +177,10 @@ export interface PositionItem {
|
||||
}
|
||||
|
||||
export async function getPositions(type?: 'rank' | 'title'): Promise<PositionItem[]> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (type) searchParams.set('type', type);
|
||||
|
||||
const result = await executeServerAction<PositionItem[]>({
|
||||
url: `${API_URL}/api/v1/positions?${searchParams.toString()}`,
|
||||
url: buildApiUrl('/api/v1/positions', {
|
||||
type,
|
||||
}),
|
||||
errorMessage: '직급/직책 조회에 실패했습니다.',
|
||||
});
|
||||
return result.data || [];
|
||||
@@ -201,7 +200,7 @@ export interface DepartmentItem {
|
||||
|
||||
export async function getDepartments(): Promise<DepartmentItem[]> {
|
||||
const result = await executeServerAction<DepartmentItem[] | { data: DepartmentItem[] }>({
|
||||
url: `${API_URL}/api/v1/departments`,
|
||||
url: buildApiUrl('/api/v1/departments'),
|
||||
errorMessage: '부서 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.data) return [];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { cookies } from 'next/headers';
|
||||
import type { SalaryRecord, SalaryDetail, PaymentStatus } from './types';
|
||||
@@ -66,9 +67,6 @@ interface StatisticsApiData {
|
||||
completed_count: number;
|
||||
}
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
// API → Frontend 변환 (목록용)
|
||||
function transformApiToFrontend(apiData: SalaryApiData): SalaryRecord {
|
||||
const profile = apiData.employee_profile;
|
||||
@@ -144,20 +142,18 @@ export async function getSalaries(params?: {
|
||||
pagination?: { total: number; currentPage: number; lastPage: number };
|
||||
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));
|
||||
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
|
||||
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
|
||||
if (params?.start_date) searchParams.set('start_date', params.start_date);
|
||||
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 result = await executeServerAction<SalaryPaginationData>({
|
||||
url: `${API_URL}/v1/salaries${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/salaries', {
|
||||
search: params?.search,
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
employee_id: params?.employee_id,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
page: params?.page,
|
||||
per_page: params?.per_page,
|
||||
}),
|
||||
errorMessage: '급여 목록을 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -179,7 +175,7 @@ export async function getSalary(id: string): Promise<{
|
||||
success: boolean; data?: SalaryDetail; error?: string
|
||||
}> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}`,
|
||||
url: buildApiUrl(`/api/v1/salaries/${id}`),
|
||||
transform: (data: SalaryApiData) => transformApiToDetail(data),
|
||||
errorMessage: '급여 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
@@ -192,7 +188,7 @@ export async function updateSalaryStatus(
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; data?: SalaryRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}/status`,
|
||||
url: buildApiUrl(`/api/v1/salaries/${id}/status`),
|
||||
method: 'PATCH',
|
||||
body: { status },
|
||||
transform: (data: SalaryApiData) => transformApiToFrontend(data),
|
||||
@@ -207,7 +203,7 @@ export async function bulkUpdateSalaryStatus(
|
||||
status: PaymentStatus
|
||||
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
|
||||
const result = await executeServerAction<{ updated_count: number }>({
|
||||
url: `${API_URL}/v1/salaries/bulk-update-status`,
|
||||
url: buildApiUrl('/api/v1/salaries/bulk-update-status'),
|
||||
method: 'POST',
|
||||
body: { ids: ids.map(id => parseInt(id, 10)), status },
|
||||
errorMessage: '일괄 상태 변경에 실패했습니다.',
|
||||
@@ -228,7 +224,7 @@ export async function updateSalary(
|
||||
}
|
||||
): Promise<{ success: boolean; data?: SalaryDetail; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/salaries/${id}`,
|
||||
url: buildApiUrl(`/api/v1/salaries/${id}`),
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (d: SalaryApiData) => transformApiToDetail(d),
|
||||
@@ -249,15 +245,13 @@ export async function getSalaryStatistics(params?: {
|
||||
};
|
||||
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 result = await executeServerAction<StatisticsApiData>({
|
||||
url: `${API_URL}/v1/salaries/statistics${queryString ? `?${queryString}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/salaries/statistics', {
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
}),
|
||||
errorMessage: '통계 정보를 불러오는데 실패했습니다.',
|
||||
});
|
||||
|
||||
@@ -303,18 +297,14 @@ export async function exportSalaryExcel(params?: {
|
||||
'X-API-KEY': process.env.API_KEY || '',
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.year) searchParams.set('year', String(params.year));
|
||||
if (params?.month) searchParams.set('month', String(params.month));
|
||||
if (params?.status && params.status !== 'all') {
|
||||
searchParams.set('status', params.status);
|
||||
}
|
||||
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
|
||||
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/export${queryString ? `?${queryString}` : ''}`;
|
||||
const url = buildApiUrl('/api/v1/salaries/export', {
|
||||
year: params?.year,
|
||||
month: params?.month,
|
||||
status: params?.status && params.status !== 'all' ? params.status : undefined,
|
||||
employee_id: params?.employee_id,
|
||||
start_date: params?.start_date,
|
||||
end_date: params?.end_date,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Download,
|
||||
DollarSign,
|
||||
Check,
|
||||
Clock,
|
||||
@@ -382,6 +381,26 @@ export function SalaryManagement() {
|
||||
|
||||
itemsPerPage: itemsPerPage,
|
||||
|
||||
// 엑셀 다운로드 설정
|
||||
excelDownload: {
|
||||
columns: [
|
||||
{ header: '부서', key: 'department' },
|
||||
{ header: '직책', key: 'position' },
|
||||
{ header: '이름', key: 'employeeName' },
|
||||
{ header: '직급', key: 'rank' },
|
||||
{ header: '기본급', key: 'baseSalary' },
|
||||
{ header: '수당', key: 'allowance' },
|
||||
{ header: '초과근무', key: 'overtime' },
|
||||
{ header: '상여', key: 'bonus' },
|
||||
{ header: '공제', key: 'deduction' },
|
||||
{ header: '실지급액', key: 'netPayment' },
|
||||
{ header: '지급일', key: 'paymentDate' },
|
||||
{ header: '상태', key: 'status', transform: (value: unknown) => value === 'completed' ? '지급완료' : '지급예정' },
|
||||
],
|
||||
filename: '급여명세',
|
||||
sheetName: '급여',
|
||||
},
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
@@ -428,16 +447,7 @@ export function SalaryManagement() {
|
||||
</>
|
||||
),
|
||||
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{canExport && (
|
||||
<Button variant="outline" onClick={() => toast.info('엑셀 다운로드 기능은 준비 중입니다.')}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
headerActions: () => null,
|
||||
|
||||
renderTableRow: (item, index, globalIndex, handlers) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import { buildApiUrl } from '@/lib/api/query-params';
|
||||
import type { PaginatedApiResponse } from '@/lib/api/types';
|
||||
|
||||
// ============================================
|
||||
@@ -165,9 +166,6 @@ interface ApiResponse<T> {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// API URL
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
|
||||
|
||||
/**
|
||||
* API 응답에서 프론트엔드 형식으로 변환
|
||||
*/
|
||||
@@ -237,23 +235,20 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
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}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/leaves', {
|
||||
user_id: params?.userId,
|
||||
status: params?.status,
|
||||
leave_type: params?.leaveType,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
year: params?.year,
|
||||
department_id: params?.departmentId,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
per_page: params?.perPage,
|
||||
page: params?.page,
|
||||
}),
|
||||
errorMessage: '휴가 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -269,7 +264,7 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
|
||||
/** 휴가 상세 조회 */
|
||||
export async function getLeaveById(id: number): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}`),
|
||||
transform: (data: Record<string, unknown>) => transformApiToFrontend(data),
|
||||
errorMessage: '휴가 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -279,7 +274,7 @@ export async function getLeaveById(id: number): Promise<{ success: boolean; data
|
||||
/** 휴가 신청 */
|
||||
export async function createLeave(data: CreateLeaveRequest): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves`,
|
||||
url: buildApiUrl('/api/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),
|
||||
@@ -291,7 +286,7 @@ export async function createLeave(data: CreateLeaveRequest): Promise<{ success:
|
||||
/** 휴가 승인 */
|
||||
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`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}/approve`),
|
||||
method: 'POST',
|
||||
body: { comment },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -303,7 +298,7 @@ export async function approveLeave(id: number, comment?: string): Promise<{ succ
|
||||
/** 휴가 반려 */
|
||||
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`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}/reject`),
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -315,7 +310,7 @@ export async function rejectLeave(id: number, reason: string): Promise<{ success
|
||||
/** 휴가 취소 */
|
||||
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`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}/cancel`),
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
||||
@@ -326,9 +321,8 @@ export async function cancelLeave(id: number, reason?: string): Promise<{ succes
|
||||
|
||||
/** 내 잔여 휴가 조회 */
|
||||
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,
|
||||
url: buildApiUrl('/api/v1/leaves/balance', { year }),
|
||||
transform: (data: Record<string, unknown>) => transformBalanceToFrontend(data),
|
||||
errorMessage: '잔여 휴가 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -337,9 +331,8 @@ export async function getMyLeaveBalance(year?: number): Promise<{ success: boole
|
||||
|
||||
/** 특정 사용자 잔여 휴가 조회 */
|
||||
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,
|
||||
url: buildApiUrl(`/api/v1/leaves/balance/${userId}`, { year }),
|
||||
transform: (data: Record<string, unknown>) => transformBalanceToFrontend(data),
|
||||
errorMessage: '잔여 휴가 조회에 실패했습니다.',
|
||||
});
|
||||
@@ -349,7 +342,7 @@ export async function getUserLeaveBalance(userId: number, year?: number): Promis
|
||||
/** 잔여 휴가 설정 (부여) */
|
||||
export async function setLeaveBalance(data: SetLeaveBalanceRequest): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/balance`,
|
||||
url: buildApiUrl('/api/v1/leaves/balance'),
|
||||
method: 'PUT',
|
||||
body: { user_id: data.userId, year: data.year, total_days: data.totalDays },
|
||||
transform: (d: Record<string, unknown>) => transformBalanceToFrontend(d),
|
||||
@@ -361,7 +354,7 @@ export async function setLeaveBalance(data: SetLeaveBalanceRequest): Promise<{ s
|
||||
/** 휴가 삭제 */
|
||||
export async function deleteLeave(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/${id}`,
|
||||
url: buildApiUrl(`/api/v1/leaves/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '휴가 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -402,19 +395,16 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise
|
||||
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}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/leaves/balances', {
|
||||
year: params?.year,
|
||||
department_id: params?.departmentId,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
per_page: params?.perPage,
|
||||
page: params?.page,
|
||||
}),
|
||||
errorMessage: '휴가 사용현황 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -517,23 +507,20 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
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}` : ''}`,
|
||||
url: buildApiUrl('/api/v1/leaves/grants', {
|
||||
user_id: params?.userId,
|
||||
grant_type: params?.grantType,
|
||||
date_from: params?.dateFrom,
|
||||
date_to: params?.dateTo,
|
||||
year: params?.year,
|
||||
department_id: params?.departmentId,
|
||||
search: params?.search,
|
||||
sort_by: params?.sortBy,
|
||||
sort_dir: params?.sortDir,
|
||||
per_page: params?.perPage,
|
||||
page: params?.page,
|
||||
}),
|
||||
errorMessage: '휴가 부여 이력 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
@@ -549,7 +536,7 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
|
||||
/** 휴가 부여 */
|
||||
export async function createLeaveGrant(data: CreateLeaveGrantRequest): Promise<{ success: boolean; data?: LeaveGrantRecord; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/grants`,
|
||||
url: buildApiUrl('/api/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),
|
||||
@@ -561,7 +548,7 @@ export async function createLeaveGrant(data: CreateLeaveGrantRequest): Promise<{
|
||||
/** 휴가 부여 삭제 */
|
||||
export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/v1/leaves/grants/${id}`,
|
||||
url: buildApiUrl(`/api/v1/leaves/grants/${id}`),
|
||||
method: 'DELETE',
|
||||
errorMessage: '휴가 부여 삭제에 실패했습니다.',
|
||||
});
|
||||
@@ -615,7 +602,7 @@ export interface EmployeeOption {
|
||||
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`,
|
||||
url: buildApiUrl('/api/v1/employees', { status: 'active', per_page: 100 }),
|
||||
errorMessage: '직원 목록 조회에 실패했습니다.',
|
||||
});
|
||||
if (!result.success || !result.data?.data) return { success: false, error: result.error };
|
||||
|
||||
Reference in New Issue
Block a user