feat(WEB): 근태 설정 및 관리 시스템 개선

- AttendanceSettingsManagement: 근무시간/휴식시간 설정 API 연동
- AttendanceManagement: 출퇴근 기록 조회/수정 기능 강화
- 근태 상태 필터링 및 검색 기능 개선
- 근태 actions 공통 로직 정리
This commit is contained in:
2025-12-30 17:20:04 +09:00
parent a45ff9af28
commit 2443c0dc63
8 changed files with 150 additions and 113 deletions

View File

@@ -65,30 +65,21 @@ export async function getAttendanceSetting(): Promise<{
success: boolean;
data?: AttendanceSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/attendance`,
{
method: 'GET',
cache: 'no-store',
}
);
const headers = await getAuthHeaders();
if (error) {
const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
error: errorData.message || `API 오류: ${response.status}`,
};
}
@@ -116,30 +107,21 @@ export async function updateAttendanceSetting(
success: boolean;
data?: AttendanceSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/attendance`,
{
method: 'PUT',
body: JSON.stringify(transformToApi(data)),
}
);
const headers = await getAuthHeaders();
if (error) {
const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, {
method: 'PUT',
headers,
body: JSON.stringify(transformToApi(data)),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
error: errorData.message || `API 오류: ${response.status}`,
};
}

View File

@@ -25,7 +25,12 @@ import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { MapPin, Save, Loader2 } from 'lucide-react';
import { getAttendanceSetting, updateAttendanceSetting } from './actions';
import {
getAttendanceSetting,
updateAttendanceSetting,
getDepartments,
type Department,
} from './actions';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
@@ -37,38 +42,47 @@ import {
SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner';
import type { AttendanceSettings, AllowedRadius, Department } from './types';
import {
DEFAULT_ATTENDANCE_SETTINGS,
ALLOWED_RADIUS_OPTIONS,
MOCK_DEPARTMENTS,
} from './types';
import type { AttendanceSettings, AllowedRadius } from './types';
import { DEFAULT_ATTENDANCE_SETTINGS, ALLOWED_RADIUS_OPTIONS } from './types';
import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox';
export function AttendanceSettingsManagement() {
const [settings, setSettings] = useState<AttendanceSettings>(DEFAULT_ATTENDANCE_SETTINGS);
const [departments] = useState<Department[]>(MOCK_DEPARTMENTS);
const [departments, setDepartments] = useState<Department[]>([]);
// 로딩 상태
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
// API에서 설정 로드
// API에서 설정 및 부서 로드
const loadData = useCallback(async () => {
setIsLoading(true);
try {
const result = await getAttendanceSetting();
if (result.success && result.data) {
// 설정과 부서를 병렬로 조회
const [settingResult, deptResult] = await Promise.all([
getAttendanceSetting(),
getDepartments(),
]);
// 설정 로드
if (settingResult.success && settingResult.data) {
setSettings(prev => ({
...prev,
gpsEnabled: result.data!.useGps,
allowedRadius: result.data!.allowedRadius as AllowedRadius,
gpsEnabled: settingResult.data!.useGps,
allowedRadius: settingResult.data!.allowedRadius as AllowedRadius,
}));
} else if (result.error) {
toast.error(result.error);
} else if (settingResult.error) {
toast.error(settingResult.error);
}
// 부서 로드
if (deptResult.success && deptResult.data) {
setDepartments(deptResult.data);
} else if (deptResult.error) {
toast.error(deptResult.error);
}
} catch {
toast.error('설정을 불러오는데 실패했습니다.');
toast.error('데이터를 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}

View File

@@ -30,26 +30,4 @@ export const DEFAULT_ATTENDANCE_SETTINGS: AttendanceSettings = {
autoDepartments: [],
};
// 부서 타입 (임시 - API 연동 시 교체)
export interface Department {
id: string;
name: string;
}
// Mock 부서 데이터 (API 연동 전까지 사용)
export const MOCK_DEPARTMENTS: Department[] = [
{ id: '1', name: 'M사장님' },
{ id: '2', name: '부사장님' },
{ id: '3', name: '영업부' },
{ id: '4', name: '개발부' },
{ id: '5', name: '인사부' },
{ id: '6', name: '경영지원부' },
{ id: '7', name: '마케팅부' },
{ id: '8', name: '재무부' },
{ id: '9', name: '생산부' },
{ id: '10', name: '품질관리부' },
{ id: '11', name: '물류부' },
{ id: '12', name: '고객지원부' },
{ id: '13', name: '연구개발부' },
{ id: '14', name: '기획부' },
];
// 부서 타입은 actions.ts에서 export됨 (API 연동)