Files
sam-react-prod/src/components/attendance/actions.ts
유병철 55e0791e16 refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일)
- sanitize 유틸 추가 (XSS 방지)
- middleware CSP 헤더 추가 및 Open Redirect 방지
- 프록시 라우트 로깅 개발환경 한정으로 변경
- 프로덕션 불필요 console.log 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:14:06 +09:00

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: '근태 조회에 실패했습니다.',
});
}