Files
sam-react-prod/src/components/settings/AttendanceSettingsManagement/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

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: '부서 목록을 불러오는데 실패했습니다.',
});
}