- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
'use server';
|
|
|
|
|
|
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
|
import { getTodayString } from '@/utils/date';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
|
|
// ===== 타입 정의 =====
|
|
export interface GpsData {
|
|
latitude: number;
|
|
longitude: number;
|
|
accuracy?: number;
|
|
}
|
|
|
|
export interface CheckInRequest {
|
|
userId?: number;
|
|
checkIn?: string;
|
|
gpsData?: GpsData;
|
|
}
|
|
|
|
export interface CheckOutRequest {
|
|
userId?: number;
|
|
checkOut?: string;
|
|
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 AttendancePaginatedResponse {
|
|
current_page: number;
|
|
data: Record<string, unknown>[];
|
|
total: number;
|
|
per_page: number;
|
|
last_page: number;
|
|
}
|
|
|
|
// ===== 변환 =====
|
|
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,
|
|
};
|
|
}
|
|
|
|
function transformGpsBody(gpsData?: GpsData) {
|
|
return gpsData ? { latitude: gpsData.latitude, longitude: gpsData.longitude, accuracy: gpsData.accuracy } : undefined;
|
|
}
|
|
|
|
// ===== 출근 체크인 =====
|
|
export async function checkIn(data: CheckInRequest): Promise<ActionResult<AttendanceRecord>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/attendances/check-in`,
|
|
method: 'POST',
|
|
body: { user_id: data.userId, check_in: data.checkIn, gps_data: transformGpsBody(data.gpsData) },
|
|
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
|
errorMessage: '출근 기록에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 퇴근 체크아웃 =====
|
|
export async function checkOut(data: CheckOutRequest): Promise<ActionResult<AttendanceRecord>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/attendances/check-out`,
|
|
method: 'POST',
|
|
body: { user_id: data.userId, check_out: data.checkOut, gps_data: transformGpsBody(data.gpsData) },
|
|
transform: (d: Record<string, unknown>) => transformApiToFrontend(d),
|
|
errorMessage: '퇴근 기록에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
// ===== 오늘 근태 상태 조회 =====
|
|
export async function getTodayAttendance(): Promise<ActionResult<AttendanceRecord | undefined>> {
|
|
const today = getTodayString();
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/attendances?date=${today}&per_page=1`,
|
|
transform: (data: AttendancePaginatedResponse) => {
|
|
const items = data.data || [];
|
|
return items.length > 0 ? transformApiToFrontend(items[0]) : undefined;
|
|
},
|
|
errorMessage: '근태 조회에 실패했습니다.',
|
|
});
|
|
} |