- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
'use server';
|
|
|
|
|
|
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
|
|
// ===== 타입 정의 =====
|
|
|
|
// API 응답 타입
|
|
interface ApiAttendanceSetting {
|
|
id: number;
|
|
tenant_id: number;
|
|
use_gps: boolean;
|
|
use_auto: boolean;
|
|
allowed_radius: number;
|
|
hq_address: string | null;
|
|
hq_latitude: number | null;
|
|
hq_longitude: number | null;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
// React 폼 데이터 타입
|
|
export interface AttendanceSettingFormData {
|
|
useGps: boolean;
|
|
useAuto: boolean;
|
|
allowedRadius: number;
|
|
hqAddress: string | null;
|
|
hqLatitude: number | null;
|
|
hqLongitude: number | null;
|
|
}
|
|
|
|
// 부서 타입 (트리 구조 지원)
|
|
export interface Department {
|
|
id: string;
|
|
name: string;
|
|
depth: number;
|
|
}
|
|
|
|
// API 부서 응답 타입
|
|
interface ApiDepartment {
|
|
id: number;
|
|
name: string;
|
|
parent_id: number | null;
|
|
children?: ApiDepartment[];
|
|
}
|
|
|
|
// ===== 데이터 변환 =====
|
|
|
|
/**
|
|
* API → React 변환
|
|
*/
|
|
function transformFromApi(data: ApiAttendanceSetting): AttendanceSettingFormData {
|
|
return {
|
|
useGps: data.use_gps,
|
|
useAuto: data.use_auto,
|
|
allowedRadius: data.allowed_radius,
|
|
hqAddress: data.hq_address,
|
|
hqLatitude: data.hq_latitude,
|
|
hqLongitude: data.hq_longitude,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* React → API 변환
|
|
*/
|
|
function transformToApi(data: Partial<AttendanceSettingFormData>): Record<string, unknown> {
|
|
const apiData: Record<string, unknown> = {};
|
|
if (data.useGps !== undefined) apiData.use_gps = data.useGps;
|
|
if (data.useAuto !== undefined) apiData.use_auto = data.useAuto;
|
|
if (data.allowedRadius !== undefined) apiData.allowed_radius = data.allowedRadius;
|
|
if (data.hqAddress !== undefined) apiData.hq_address = data.hqAddress;
|
|
if (data.hqLatitude !== undefined) apiData.hq_latitude = data.hqLatitude;
|
|
if (data.hqLongitude !== undefined) apiData.hq_longitude = data.hqLongitude;
|
|
return apiData;
|
|
}
|
|
|
|
/**
|
|
* 트리 구조를 평탄화 (재귀)
|
|
*/
|
|
function flattenDepartmentTree(departments: ApiDepartment[], depth: number = 0): Department[] {
|
|
const result: Department[] = [];
|
|
for (const dept of departments) {
|
|
result.push({ id: String(dept.id), name: dept.name, depth });
|
|
if (dept.children && dept.children.length > 0) {
|
|
result.push(...flattenDepartmentTree(dept.children, depth + 1));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ===== API 호출 =====
|
|
|
|
/**
|
|
* 출퇴근 설정 조회
|
|
*/
|
|
export async function getAttendanceSetting(): Promise<ActionResult<AttendanceSettingFormData>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/settings/attendance`,
|
|
transform: (data: ApiAttendanceSetting) => transformFromApi(data),
|
|
errorMessage: '출퇴근 설정을 불러오는데 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 출퇴근 설정 수정
|
|
*/
|
|
export async function updateAttendanceSetting(
|
|
data: Partial<AttendanceSettingFormData>
|
|
): Promise<ActionResult<AttendanceSettingFormData>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/settings/attendance`,
|
|
method: 'PUT',
|
|
body: transformToApi(data),
|
|
transform: (data: ApiAttendanceSetting) => transformFromApi(data),
|
|
errorMessage: '출퇴근 설정 저장에 실패했습니다.',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 부서 목록 조회 (트리 구조)
|
|
*/
|
|
export async function getDepartments(): Promise<ActionResult<Department[]>> {
|
|
return executeServerAction({
|
|
url: `${API_URL}/api/v1/departments/tree`,
|
|
transform: (data: ApiDepartment[]) => flattenDepartmentTree(data || []),
|
|
errorMessage: '부서 목록을 불러오는데 실패했습니다.',
|
|
});
|
|
} |