- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage) - 페이지(app/[locale]) 타입 호환성 수정 (80개) - 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement) - 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults) - 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders) - 견적/단가 모듈 타입 수정 (Quotes, Pricing) - 건설 모듈 타입 수정 (49개, 17개 하위 모듈) - HR 모듈 타입 수정 (CardManagement, VacationManagement 등) - 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등) - 게시판 모듈 타입 수정 (BoardManagement, BoardList 등) - 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등) - 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등) - 유틸/훅/API 타입 수정 (hooks, contexts, lib) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
6.6 KiB
TypeScript
264 lines
6.6 KiB
TypeScript
/**
|
|
* 근태관리 서버 액션
|
|
*
|
|
* API Endpoints:
|
|
* - POST /api/v1/attendances/check-in - 출근 체크인
|
|
* - POST /api/v1/attendances/check-out - 퇴근 체크아웃
|
|
* - GET /api/v1/attendances - 근태 목록 조회
|
|
*/
|
|
|
|
'use server';
|
|
|
|
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper';
|
|
import { getTodayString } from '@/utils/date';
|
|
|
|
// ============================================
|
|
// 타입 정의
|
|
// ============================================
|
|
|
|
/**
|
|
* GPS 데이터 타입
|
|
*/
|
|
export interface GpsData {
|
|
latitude: number;
|
|
longitude: number;
|
|
accuracy?: number;
|
|
}
|
|
|
|
/**
|
|
* 출근 체크인 요청 데이터
|
|
*/
|
|
export interface CheckInRequest {
|
|
userId?: number;
|
|
checkIn?: string; // HH:mm:ss format
|
|
gpsData?: GpsData;
|
|
}
|
|
|
|
/**
|
|
* 퇴근 체크아웃 요청 데이터
|
|
*/
|
|
export interface CheckOutRequest {
|
|
userId?: number;
|
|
checkOut?: string; // HH:mm:ss format
|
|
gpsData?: GpsData;
|
|
}
|
|
|
|
/**
|
|
* 근태 기록 응답 타입
|
|
*/
|
|
export interface AttendanceRecord {
|
|
id: number;
|
|
userId: number;
|
|
date: string;
|
|
checkIn: string | null;
|
|
checkOut: string | null;
|
|
status: string;
|
|
gpsData?: GpsData;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
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 응답에서 프론트엔드 형식으로 변환
|
|
*/
|
|
function transformApiToFrontend(apiData: Record<string, unknown>): AttendanceRecord {
|
|
return {
|
|
id: apiData.id as number,
|
|
userId: apiData.user_id as number,
|
|
date: apiData.date as string,
|
|
checkIn: apiData.check_in as string | null,
|
|
checkOut: apiData.check_out as string | null,
|
|
status: apiData.status as string,
|
|
gpsData: apiData.gps_data as GpsData | undefined,
|
|
createdAt: apiData.created_at as string,
|
|
updatedAt: apiData.updated_at as string,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// API 함수
|
|
// ============================================
|
|
|
|
/**
|
|
* 출근 체크인
|
|
* POST /v1/attendances/check-in
|
|
*/
|
|
export async function checkIn(
|
|
data: CheckInRequest
|
|
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/check-in`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
user_id: data.userId,
|
|
check_in: data.checkIn,
|
|
gps_data: data.gpsData
|
|
? {
|
|
latitude: data.gpsData.latitude,
|
|
longitude: data.gpsData.longitude,
|
|
accuracy: data.gpsData.accuracy,
|
|
}
|
|
: undefined,
|
|
}),
|
|
});
|
|
|
|
if (error) {
|
|
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
|
}
|
|
|
|
if (!response) {
|
|
return { success: false, error: '출근 기록에 실패했습니다.' };
|
|
}
|
|
|
|
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('[checkIn] Error:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : '출근 기록에 실패했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 퇴근 체크아웃
|
|
* POST /v1/attendances/check-out
|
|
*/
|
|
export async function checkOut(
|
|
data: CheckOutRequest
|
|
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const { response, error } = await serverFetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/check-out`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
user_id: data.userId,
|
|
check_out: data.checkOut,
|
|
gps_data: data.gpsData
|
|
? {
|
|
latitude: data.gpsData.latitude,
|
|
longitude: data.gpsData.longitude,
|
|
accuracy: data.gpsData.accuracy,
|
|
}
|
|
: undefined,
|
|
}),
|
|
});
|
|
|
|
if (error) {
|
|
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
|
}
|
|
|
|
if (!response) {
|
|
return { success: false, error: '퇴근 기록에 실패했습니다.' };
|
|
}
|
|
|
|
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 (err) {
|
|
if (isNextRedirectError(err)) throw err;
|
|
console.error('[checkOut] Error:', err);
|
|
return {
|
|
success: false,
|
|
error: err instanceof Error ? err.message : '퇴근 기록에 실패했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 오늘 근태 상태 조회
|
|
* GET /v1/attendances?date=YYYY-MM-DD
|
|
*/
|
|
export async function getTodayAttendance(): Promise<{
|
|
success: boolean;
|
|
data?: AttendanceRecord;
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
try {
|
|
const today = getTodayString();
|
|
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances?date=${today}&per_page=1`,
|
|
{
|
|
method: 'GET',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
|
}
|
|
|
|
if (!response) {
|
|
return { success: false, error: '근태 조회에 실패했습니다.' };
|
|
}
|
|
|
|
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
|
|
|
|
if (result.success && result.data) {
|
|
const items = result.data.data || [];
|
|
if (items.length > 0) {
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(items[0]),
|
|
};
|
|
}
|
|
// 오늘 기록이 없으면 undefined 반환
|
|
return { success: true, data: undefined };
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: result.message || '근태 조회에 실패했습니다.',
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[getTodayAttendance] Error:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : '근태 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
} |