From 258c8e417912057ad0e50fba20a39194e0a12e48 Mon Sep 17 00:00:00 2001 From: kent Date: Tue, 30 Dec 2025 21:04:14 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20=EC=A7=81=EA=B8=89/=EC=A7=81?= =?UTF-8?q?=EC=B1=85=20=EA=B4=80=EB=A6=AC=20Server=20Actions=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 클라이언트 직접 API 호출 → Server Actions 방식으로 변경 - RankManagement/actions.ts 신규 생성 - TitleManagement/actions.ts 신규 생성 - API_KEY 환경변수를 서버에서만 사용하도록 변경 (보안 강화) - 기존 lib/api/positions.ts 삭제 --- .../settings/RankManagement/actions.ts | 269 ++++++++++++++++ .../settings/RankManagement/index.tsx | 80 ++--- .../settings/TitleManagement/actions.ts | 269 ++++++++++++++++ .../settings/TitleManagement/index.tsx | 78 ++--- src/lib/api/positions.ts | 296 ------------------ 5 files changed, 623 insertions(+), 369 deletions(-) create mode 100644 src/components/settings/RankManagement/actions.ts create mode 100644 src/components/settings/TitleManagement/actions.ts delete mode 100644 src/lib/api/positions.ts diff --git a/src/components/settings/RankManagement/actions.ts b/src/components/settings/RankManagement/actions.ts new file mode 100644 index 00000000..808d882b --- /dev/null +++ b/src/components/settings/RankManagement/actions.ts @@ -0,0 +1,269 @@ +'use server'; + +import { serverFetch } from '@/lib/api/fetch-wrapper'; +import type { Rank } from './types'; + +// ===== API 응답 타입 ===== +interface PositionApiData { + id: number; + tenant_id: number; + type: 'rank' | 'title'; + name: string; + sort_order: number; + is_active: boolean; + created_at?: string; + updated_at?: string; +} + +interface ApiResponse { + success: boolean; + message?: string; + data: T; +} + +// ===== 데이터 변환: API → Frontend ===== +function transformApiToFrontend(apiData: PositionApiData): Rank { + return { + id: apiData.id, + name: apiData.name, + order: apiData.sort_order, + isActive: apiData.is_active, + createdAt: apiData.created_at, + updatedAt: apiData.updated_at, + }; +} + +// ===== 직급 목록 조회 ===== +export async function getRanks(params?: { + is_active?: boolean; + q?: string; +}): Promise<{ + success: boolean; + data?: Rank[]; + error?: string; + __authError?: boolean; +}> { + try { + const searchParams = new URLSearchParams(); + searchParams.set('type', 'rank'); + + if (params?.is_active !== undefined) { + searchParams.set('is_active', params.is_active.toString()); + } + if (params?.q) { + searchParams.set('q', params.q); + } + + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions?${searchParams.toString()}`; + + const { response, error } = await serverFetch(url, { + method: 'GET', + cache: 'no-store', + }); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + if (!response) { + return { success: false, error: '직급 목록 조회에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '직급 목록 조회에 실패했습니다.' }; + } + + const ranks = result.data.map(transformApiToFrontend); + return { success: true, data: ranks }; + } catch (error) { + console.error('[getRanks] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직급 생성 ===== +export async function createRank(data: { + name: string; + sort_order?: number; + is_active?: boolean; +}): Promise<{ + success: boolean; + data?: Rank; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions`, + { + method: 'POST', + body: JSON.stringify({ + type: 'rank', + name: data.name, + sort_order: data.sort_order, + is_active: data.is_active ?? true, + }), + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + if (!response) { + return { success: false, error: '직급 생성에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '직급 생성에 실패했습니다.' }; + } + + const rank = transformApiToFrontend(result.data); + return { success: true, data: rank }; + } catch (error) { + console.error('[createRank] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직급 수정 ===== +export async function updateRank( + id: number, + data: { + name?: string; + sort_order?: number; + is_active?: boolean; + } +): Promise<{ + success: boolean; + data?: Rank; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`, + { + method: 'PUT', + body: JSON.stringify(data), + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + if (!response) { + return { success: false, error: '직급 수정에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '직급 수정에 실패했습니다.' }; + } + + const rank = transformApiToFrontend(result.data); + return { success: true, data: rank }; + } catch (error) { + console.error('[updateRank] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직급 삭제 ===== +export async function deleteRank(id: number): Promise<{ + success: boolean; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`, + { + method: 'DELETE', + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + 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 }; + } catch (error) { + console.error('[deleteRank] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직급 순서 변경 ===== +export async function reorderRanks( + items: { id: number; sort_order: number }[] +): Promise<{ + success: boolean; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/reorder`, + { + method: 'PUT', + body: JSON.stringify({ items }), + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + 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 }; + } catch (error) { + console.error('[reorderRanks] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} \ No newline at end of file diff --git a/src/components/settings/RankManagement/index.tsx b/src/components/settings/RankManagement/index.tsx index 0c949fe1..b0d2ae17 100644 --- a/src/components/settings/RankManagement/index.tsx +++ b/src/components/settings/RankManagement/index.tsx @@ -21,27 +21,12 @@ import { import { toast } from 'sonner'; import type { Rank } from './types'; import { - fetchRanks, + getRanks, createRank, - updatePosition, - deletePosition, - reorderPositions, - type Position, -} from '@/lib/api/positions'; - -/** - * Position → Rank 변환 - */ -function positionToRank(position: Position): Rank { - return { - id: position.id, - name: position.name, - order: position.sort_order, - isActive: position.is_active, - createdAt: position.created_at, - updatedAt: position.updated_at, - }; -} + updateRank, + deleteRank, + reorderRanks, +} from './actions'; export function RankManagement() { // 직급 데이터 @@ -68,8 +53,12 @@ export function RankManagement() { const loadRanks = useCallback(async () => { try { setIsLoading(true); - const positions = await fetchRanks(); - setRanks(positions.map(positionToRank)); + const result = await getRanks(); + if (result.success && result.data) { + setRanks(result.data); + } else { + toast.error(result.error || '직급 목록을 불러오는데 실패했습니다.'); + } } catch (error) { console.error('직급 목록 조회 실패:', error); toast.error('직급 목록을 불러오는데 실패했습니다.'); @@ -89,10 +78,14 @@ export function RankManagement() { try { setIsSubmitting(true); - const newPosition = await createRank({ name: newRankName.trim() }); - setRanks(prev => [...prev, positionToRank(newPosition)]); - setNewRankName(''); - toast.success('직급이 추가되었습니다.'); + const result = await createRank({ name: newRankName.trim() }); + if (result.success && result.data) { + setRanks(prev => [...prev, result.data!]); + setNewRankName(''); + toast.success('직급이 추가되었습니다.'); + } else { + toast.error(result.error || '직급 추가에 실패했습니다.'); + } } catch (error) { console.error('직급 추가 실패:', error); toast.error('직급 추가에 실패했습니다.'); @@ -120,9 +113,13 @@ export function RankManagement() { try { setIsSubmitting(true); - await deletePosition(rankToDelete.id); - setRanks(prev => prev.filter(r => r.id !== rankToDelete.id)); - toast.success('직급이 삭제되었습니다.'); + const result = await deleteRank(rankToDelete.id); + if (result.success) { + setRanks(prev => prev.filter(r => r.id !== rankToDelete.id)); + toast.success('직급이 삭제되었습니다.'); + } else { + toast.error(result.error || '직급 삭제에 실패했습니다.'); + } } catch (error) { console.error('직급 삭제 실패:', error); toast.error('직급 삭제에 실패했습니다.'); @@ -138,11 +135,15 @@ export function RankManagement() { if (dialogMode === 'edit' && selectedRank) { try { setIsSubmitting(true); - await updatePosition(selectedRank.id, { name }); - setRanks(prev => prev.map(r => - r.id === selectedRank.id ? { ...r, name } : r - )); - toast.success('직급이 수정되었습니다.'); + const result = await updateRank(selectedRank.id, { name }); + if (result.success) { + setRanks(prev => prev.map(r => + r.id === selectedRank.id ? { ...r, name } : r + )); + toast.success('직급이 수정되었습니다.'); + } else { + toast.error(result.error || '직급 수정에 실패했습니다.'); + } } catch (error) { console.error('직급 수정 실패:', error); toast.error('직급 수정에 실패했습니다.'); @@ -171,8 +172,13 @@ export function RankManagement() { id: rank.id, sort_order: idx + 1, })); - await reorderPositions(items); - toast.success('순서가 변경되었습니다.'); + const result = await reorderRanks(items); + if (result.success) { + toast.success('순서가 변경되었습니다.'); + } else { + toast.error(result.error || '순서 변경에 실패했습니다.'); + loadRanks(); + } } catch (error) { console.error('순서 변경 실패:', error); toast.error('순서 변경에 실패했습니다.'); @@ -358,4 +364,4 @@ export function RankManagement() { ); -} +} \ No newline at end of file diff --git a/src/components/settings/TitleManagement/actions.ts b/src/components/settings/TitleManagement/actions.ts new file mode 100644 index 00000000..c59b53cb --- /dev/null +++ b/src/components/settings/TitleManagement/actions.ts @@ -0,0 +1,269 @@ +'use server'; + +import { serverFetch } from '@/lib/api/fetch-wrapper'; +import type { Title } from './types'; + +// ===== API 응답 타입 ===== +interface PositionApiData { + id: number; + tenant_id: number; + type: 'rank' | 'title'; + name: string; + sort_order: number; + is_active: boolean; + created_at?: string; + updated_at?: string; +} + +interface ApiResponse { + success: boolean; + message?: string; + data: T; +} + +// ===== 데이터 변환: API → Frontend ===== +function transformApiToFrontend(apiData: PositionApiData): Title { + return { + id: apiData.id, + name: apiData.name, + order: apiData.sort_order, + isActive: apiData.is_active, + createdAt: apiData.created_at, + updatedAt: apiData.updated_at, + }; +} + +// ===== 직책 목록 조회 ===== +export async function getTitles(params?: { + is_active?: boolean; + q?: string; +}): Promise<{ + success: boolean; + data?: Title[]; + error?: string; + __authError?: boolean; +}> { + try { + const searchParams = new URLSearchParams(); + searchParams.set('type', 'title'); + + if (params?.is_active !== undefined) { + searchParams.set('is_active', params.is_active.toString()); + } + if (params?.q) { + searchParams.set('q', params.q); + } + + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions?${searchParams.toString()}`; + + const { response, error } = await serverFetch(url, { + method: 'GET', + cache: 'no-store', + }); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + if (!response) { + return { success: false, error: '직책 목록 조회에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '직책 목록 조회에 실패했습니다.' }; + } + + const titles = result.data.map(transformApiToFrontend); + return { success: true, data: titles }; + } catch (error) { + console.error('[getTitles] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직책 생성 ===== +export async function createTitle(data: { + name: string; + sort_order?: number; + is_active?: boolean; +}): Promise<{ + success: boolean; + data?: Title; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions`, + { + method: 'POST', + body: JSON.stringify({ + type: 'title', + name: data.name, + sort_order: data.sort_order, + is_active: data.is_active ?? true, + }), + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + if (!response) { + return { success: false, error: '직책 생성에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '직책 생성에 실패했습니다.' }; + } + + const title = transformApiToFrontend(result.data); + return { success: true, data: title }; + } catch (error) { + console.error('[createTitle] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직책 수정 ===== +export async function updateTitle( + id: number, + data: { + name?: string; + sort_order?: number; + is_active?: boolean; + } +): Promise<{ + success: boolean; + data?: Title; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`, + { + method: 'PUT', + body: JSON.stringify(data), + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + if (!response) { + return { success: false, error: '직책 수정에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '직책 수정에 실패했습니다.' }; + } + + const title = transformApiToFrontend(result.data); + return { success: true, data: title }; + } catch (error) { + console.error('[updateTitle] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직책 삭제 ===== +export async function deleteTitle(id: number): Promise<{ + success: boolean; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/${id}`, + { + method: 'DELETE', + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + 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 }; + } catch (error) { + console.error('[deleteTitle] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 직책 순서 변경 ===== +export async function reorderTitles( + items: { id: number; sort_order: number }[] +): Promise<{ + success: boolean; + error?: string; + __authError?: boolean; +}> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/positions/reorder`, + { + method: 'PUT', + body: JSON.stringify({ items }), + } + ); + + if (error) { + return { + success: false, + error: error.message, + __authError: error.code === 'UNAUTHORIZED', + }; + } + + 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 }; + } catch (error) { + console.error('[reorderTitles] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} \ No newline at end of file diff --git a/src/components/settings/TitleManagement/index.tsx b/src/components/settings/TitleManagement/index.tsx index c8dbfb7d..ce8cd98c 100644 --- a/src/components/settings/TitleManagement/index.tsx +++ b/src/components/settings/TitleManagement/index.tsx @@ -21,27 +21,12 @@ import { import { toast } from 'sonner'; import type { Title } from './types'; import { - fetchTitles, + getTitles, createTitle, - updatePosition, - deletePosition, - reorderPositions, - type Position, -} from '@/lib/api/positions'; - -/** - * Position → Title 변환 - */ -function positionToTitle(position: Position): Title { - return { - id: position.id, - name: position.name, - order: position.sort_order, - isActive: position.is_active, - createdAt: position.created_at, - updatedAt: position.updated_at, - }; -} + updateTitle, + deleteTitle, + reorderTitles, +} from './actions'; export function TitleManagement() { // 직책 데이터 @@ -68,8 +53,12 @@ export function TitleManagement() { const loadTitles = useCallback(async () => { try { setIsLoading(true); - const positions = await fetchTitles(); - setTitles(positions.map(positionToTitle)); + const result = await getTitles(); + if (result.success && result.data) { + setTitles(result.data); + } else { + toast.error(result.error || '직책 목록을 불러오는데 실패했습니다.'); + } } catch (error) { console.error('직책 목록 조회 실패:', error); toast.error('직책 목록을 불러오는데 실패했습니다.'); @@ -89,10 +78,14 @@ export function TitleManagement() { try { setIsSubmitting(true); - const newPosition = await createTitle({ name: newTitleName.trim() }); - setTitles(prev => [...prev, positionToTitle(newPosition)]); - setNewTitleName(''); - toast.success('직책이 추가되었습니다.'); + const result = await createTitle({ name: newTitleName.trim() }); + if (result.success && result.data) { + setTitles(prev => [...prev, result.data!]); + setNewTitleName(''); + toast.success('직책이 추가되었습니다.'); + } else { + toast.error(result.error || '직책 추가에 실패했습니다.'); + } } catch (error) { console.error('직책 추가 실패:', error); toast.error('직책 추가에 실패했습니다.'); @@ -120,9 +113,13 @@ export function TitleManagement() { try { setIsSubmitting(true); - await deletePosition(titleToDelete.id); - setTitles(prev => prev.filter(t => t.id !== titleToDelete.id)); - toast.success('직책이 삭제되었습니다.'); + const result = await deleteTitle(titleToDelete.id); + if (result.success) { + setTitles(prev => prev.filter(t => t.id !== titleToDelete.id)); + toast.success('직책이 삭제되었습니다.'); + } else { + toast.error(result.error || '직책 삭제에 실패했습니다.'); + } } catch (error) { console.error('직책 삭제 실패:', error); toast.error('직책 삭제에 실패했습니다.'); @@ -138,11 +135,15 @@ export function TitleManagement() { if (dialogMode === 'edit' && selectedTitle) { try { setIsSubmitting(true); - await updatePosition(selectedTitle.id, { name }); - setTitles(prev => prev.map(t => - t.id === selectedTitle.id ? { ...t, name } : t - )); - toast.success('직책이 수정되었습니다.'); + const result = await updateTitle(selectedTitle.id, { name }); + if (result.success) { + setTitles(prev => prev.map(t => + t.id === selectedTitle.id ? { ...t, name } : t + )); + toast.success('직책이 수정되었습니다.'); + } else { + toast.error(result.error || '직책 수정에 실패했습니다.'); + } } catch (error) { console.error('직책 수정 실패:', error); toast.error('직책 수정에 실패했습니다.'); @@ -171,8 +172,13 @@ export function TitleManagement() { id: title.id, sort_order: idx + 1, })); - await reorderPositions(items); - toast.success('순서가 변경되었습니다.'); + const result = await reorderTitles(items); + if (result.success) { + toast.success('순서가 변경되었습니다.'); + } else { + toast.error(result.error || '순서 변경에 실패했습니다.'); + loadTitles(); + } } catch (error) { console.error('순서 변경 실패:', error); toast.error('순서 변경에 실패했습니다.'); diff --git a/src/lib/api/positions.ts b/src/lib/api/positions.ts deleted file mode 100644 index 77619c80..00000000 --- a/src/lib/api/positions.ts +++ /dev/null @@ -1,296 +0,0 @@ -/** - * 직급/직책 통합 API 클라이언트 - * - * Laravel 백엔드 positions 테이블과 통신 - * type: 'rank' = 직급, 'title' = 직책 - */ - -// ===== 타입 정의 ===== - -export type PositionType = 'rank' | 'title'; - -export interface Position { - id: number; - tenant_id: number; - type: PositionType; - name: string; - sort_order: number; - is_active: boolean; - created_at?: string; - updated_at?: string; -} - -export interface PositionCreateRequest { - type: PositionType; - name: string; - sort_order?: number; - is_active?: boolean; -} - -export interface PositionUpdateRequest { - name?: string; - sort_order?: number; - is_active?: boolean; -} - -export interface PositionReorderItem { - id: number; - sort_order: number; -} - -export interface PositionListParams { - type?: PositionType; - is_active?: boolean; - q?: string; - per_page?: number; - page?: number; -} - -interface ApiResponse { - success: boolean; - message: string; - data: T; -} - -// ===== 환경 변수 ===== - -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://api.sam.kr'; - -// ===== 유틸리티 함수 ===== - -/** - * 인증 토큰 가져오기 - */ -function getAuthToken(): string | null { - if (typeof window !== 'undefined') { - return localStorage.getItem('auth_token'); - } - return null; -} - -/** - * API Key 가져오기 - */ -function getApiKey(): string { - return process.env.NEXT_PUBLIC_API_KEY || ''; -} - -/** - * Fetch 옵션 생성 - */ -function createFetchOptions(options: RequestInit = {}): RequestInit { - const token = getAuthToken(); - const apiKey = getApiKey(); - - const headers: Record = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }; - - // API Key 추가 - if (apiKey) { - headers['X-API-KEY'] = apiKey; - } - - // Bearer 토큰 추가 - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - // Merge existing headers - if (options.headers && typeof options.headers === 'object' && !Array.isArray(options.headers)) { - Object.assign(headers, options.headers); - } - - return { - ...options, - headers, - credentials: 'include', - }; -} - -/** - * API 에러 처리 - */ -async function handleApiResponse(response: Response): Promise { - if (!response.ok) { - const error = await response.json().catch(() => ({ - message: 'API 요청 실패', - })); - - throw new Error(error.message || `HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - return data; -} - -// ===== Position CRUD API ===== - -/** - * 직급/직책 목록 조회 - * - * @param params - 필터 파라미터 - * @example - * // 직급만 조회 - * const ranks = await fetchPositions({ type: 'rank' }); - * // 직책만 조회 - * const titles = await fetchPositions({ type: 'title' }); - */ -export async function fetchPositions( - params?: PositionListParams -): Promise { - const queryParams = new URLSearchParams(); - - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - queryParams.append(key, String(value)); - } - }); - } - - const url = `${API_URL}/v1/positions${queryParams.toString() ? `?${queryParams}` : ''}`; - - const response = await fetch(url, createFetchOptions()); - const result = await handleApiResponse>(response); - - return result.data; -} - -/** - * 직급/직책 단건 조회 - * - * @param id - Position ID - */ -export async function fetchPosition(id: number): Promise { - const response = await fetch( - `${API_URL}/v1/positions/${id}`, - createFetchOptions() - ); - - const result = await handleApiResponse>(response); - return result.data; -} - -/** - * 직급/직책 생성 - * - * @param data - 생성 데이터 - * @example - * const newRank = await createPosition({ - * type: 'rank', - * name: '차장', - * }); - */ -export async function createPosition( - data: PositionCreateRequest -): Promise { - const response = await fetch( - `${API_URL}/v1/positions`, - createFetchOptions({ - method: 'POST', - body: JSON.stringify(data), - }) - ); - - const result = await handleApiResponse>(response); - return result.data; -} - -/** - * 직급/직책 수정 - * - * @param id - Position ID - * @param data - 수정 데이터 - */ -export async function updatePosition( - id: number, - data: PositionUpdateRequest -): Promise { - const response = await fetch( - `${API_URL}/v1/positions/${id}`, - createFetchOptions({ - method: 'PUT', - body: JSON.stringify(data), - }) - ); - - const result = await handleApiResponse>(response); - return result.data; -} - -/** - * 직급/직책 삭제 - * - * @param id - Position ID - */ -export async function deletePosition(id: number): Promise { - const response = await fetch( - `${API_URL}/v1/positions/${id}`, - createFetchOptions({ - method: 'DELETE', - }) - ); - - await handleApiResponse>(response); -} - -/** - * 직급/직책 순서 일괄 변경 - * - * @param items - 정렬할 아이템 목록 - * @example - * await reorderPositions([ - * { id: 1, sort_order: 1 }, - * { id: 2, sort_order: 2 }, - * ]); - */ -export async function reorderPositions( - items: PositionReorderItem[] -): Promise<{ success: boolean; updated: number }> { - const response = await fetch( - `${API_URL}/v1/positions/reorder`, - createFetchOptions({ - method: 'PUT', - body: JSON.stringify({ items }), - }) - ); - - const result = await handleApiResponse>(response); - return result.data; -} - -// ===== 헬퍼 함수 ===== - -/** - * 직급 목록 조회 (헬퍼) - */ -export async function fetchRanks(params?: Omit): Promise { - return fetchPositions({ ...params, type: 'rank' }); -} - -/** - * 직책 목록 조회 (헬퍼) - */ -export async function fetchTitles(params?: Omit): Promise { - return fetchPositions({ ...params, type: 'title' }); -} - -/** - * 직급 생성 (헬퍼) - */ -export async function createRank( - data: Omit -): Promise { - return createPosition({ ...data, type: 'rank' }); -} - -/** - * 직책 생성 (헬퍼) - */ -export async function createTitle( - data: Omit -): Promise { - return createPosition({ ...data, type: 'title' }); -} \ No newline at end of file