diff --git a/src/components/settings/AttendanceSettingsManagement/actions.ts b/src/components/settings/AttendanceSettingsManagement/actions.ts index 3ef45697..78abc6a0 100644 --- a/src/components/settings/AttendanceSettingsManagement/actions.ts +++ b/src/components/settings/AttendanceSettingsManagement/actions.ts @@ -2,6 +2,8 @@ import { serverFetch } from '@/lib/api/fetch-wrapper'; +const API_URL = process.env.NEXT_PUBLIC_API_URL; + // ===== 타입 정의 ===== // API 응답 타입 @@ -26,6 +28,28 @@ export interface AttendanceSettingFormData { 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 응답 공통 타입 +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + // ===== 데이터 변환 ===== /** @@ -61,30 +85,26 @@ function transformToApi(data: Partial): Record { +export async function getAttendanceSetting(): Promise> { try { - const headers = await getAuthHeaders(); - - const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, { + const { response, error } = await serverFetch(`${API_URL}/api/v1/settings/attendance`, { method: 'GET', - headers, - cache: 'no-store', }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - return { - success: false, - error: errorData.message || `API 오류: ${response.status}`, - }; + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '출퇴근 설정 조회에 실패했습니다.' }; } const result = await response.json(); + if (!response.ok || !result.success) { + return { success: false, error: result.message || '출퇴근 설정 조회 실패' }; + } + return { success: true, data: transformFromApi(result.data), @@ -103,30 +123,27 @@ export async function getAttendanceSetting(): Promise<{ */ export async function updateAttendanceSetting( data: Partial -): Promise<{ - success: boolean; - data?: AttendanceSettingFormData; - error?: string; -}> { +): Promise> { try { - const headers = await getAuthHeaders(); - - const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, { + const { response, error } = await serverFetch(`${API_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: errorData.message || `API 오류: ${response.status}`, - }; + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '출퇴근 설정 저장에 실패했습니다.' }; } const result = await response.json(); + if (!response.ok || !result.success) { + return { success: false, error: result.message || '출퇴근 설정 저장 실패' }; + } + return { success: true, data: transformFromApi(result.data), @@ -138,4 +155,62 @@ export async function updateAttendanceSetting( error: '출퇴근 설정 저장에 실패했습니다.', }; } +} + +/** + * 트리 구조를 평탄화 (재귀) + */ +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; +} + +/** + * 부서 목록 조회 (트리 구조) + */ +export async function getDepartments(): Promise> { + try { + // 트리 API 사용 + const { response, error } = await serverFetch(`${API_URL}/api/v1/departments/tree`, { + method: 'GET', + }); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '부서 목록 조회에 실패했습니다.' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '부서 목록 조회 실패' }; + } + + // 트리를 평탄화하여 depth 포함된 배열로 변환 + const departments = flattenDepartmentTree(result.data || []); + + return { success: true, data: departments }; + } catch (error) { + console.error('getDepartments error:', error); + return { + success: false, + error: '부서 목록을 불러오는데 실패했습니다.', + }; + } } \ No newline at end of file diff --git a/src/components/settings/AttendanceSettingsManagement/index.tsx b/src/components/settings/AttendanceSettingsManagement/index.tsx index 1b99fc01..e77625d8 100644 --- a/src/components/settings/AttendanceSettingsManagement/index.tsx +++ b/src/components/settings/AttendanceSettingsManagement/index.tsx @@ -150,10 +150,11 @@ export function AttendanceSettingsManagement() { } }; - // 부서 옵션 변환 + // 부서 옵션 변환 (depth 포함) const departmentOptions = departments.map(dept => ({ value: dept.id, label: dept.name, + depth: dept.depth, })); // 선택된 부서 표시 텍스트 diff --git a/src/components/ui/multi-select-combobox.tsx b/src/components/ui/multi-select-combobox.tsx index 33fb6564..7712243a 100644 --- a/src/components/ui/multi-select-combobox.tsx +++ b/src/components/ui/multi-select-combobox.tsx @@ -17,6 +17,7 @@ import { cn } from './utils'; export interface MultiSelectOption { value: string; label: string; + depth?: number; // 트리 구조 들여쓰기용 } interface MultiSelectComboboxProps { @@ -107,14 +108,15 @@ export function MultiSelectCombobox({ value={option.label} onSelect={() => handleSelect(option.value)} className="cursor-pointer" + style={{ paddingLeft: option.depth ? `${(option.depth * 16) + 8}px` : undefined }} > - {option.label} + {option.label} ))}