feat(WEB): 근태 설정 및 관리 시스템 개선
- AttendanceSettingsManagement: 근무시간/휴식시간 설정 API 연동 - AttendanceManagement: 출퇴근 기록 조회/수정 기능 강화 - 근태 상태 필터링 및 검색 기능 개선 - 근태 actions 공통 로직 정리
This commit is contained in:
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 연동)
|
||||
|
||||
Reference in New Issue
Block a user