refactor(WEB): 직급/직책 관리 Server Actions 전환

- 클라이언트 직접 API 호출 → Server Actions 방식으로 변경
- RankManagement/actions.ts 신규 생성
- TitleManagement/actions.ts 신규 생성
- API_KEY 환경변수를 서버에서만 사용하도록 변경 (보안 강화)
- 기존 lib/api/positions.ts 삭제
This commit is contained in:
2025-12-30 21:04:14 +09:00
parent 5ab1354bcc
commit 258c8e4179
5 changed files with 623 additions and 369 deletions

View File

@@ -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<T> {
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<PositionApiData[]> = 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<PositionApiData> = 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<PositionApiData> = 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: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -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() {
</AlertDialog>
</PageLayout>
);
}
}

View File

@@ -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<T> {
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<PositionApiData[]> = 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<PositionApiData> = 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<PositionApiData> = 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: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -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('순서 변경에 실패했습니다.');

View File

@@ -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<T> {
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<string, string> = {
'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<T>(response: Response): Promise<T> {
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<Position[]> {
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<ApiResponse<Position[]>>(response);
return result.data;
}
/**
* 직급/직책 단건 조회
*
* @param id - Position ID
*/
export async function fetchPosition(id: number): Promise<Position> {
const response = await fetch(
`${API_URL}/v1/positions/${id}`,
createFetchOptions()
);
const result = await handleApiResponse<ApiResponse<Position>>(response);
return result.data;
}
/**
* 직급/직책 생성
*
* @param data - 생성 데이터
* @example
* const newRank = await createPosition({
* type: 'rank',
* name: '차장',
* });
*/
export async function createPosition(
data: PositionCreateRequest
): Promise<Position> {
const response = await fetch(
`${API_URL}/v1/positions`,
createFetchOptions({
method: 'POST',
body: JSON.stringify(data),
})
);
const result = await handleApiResponse<ApiResponse<Position>>(response);
return result.data;
}
/**
* 직급/직책 수정
*
* @param id - Position ID
* @param data - 수정 데이터
*/
export async function updatePosition(
id: number,
data: PositionUpdateRequest
): Promise<Position> {
const response = await fetch(
`${API_URL}/v1/positions/${id}`,
createFetchOptions({
method: 'PUT',
body: JSON.stringify(data),
})
);
const result = await handleApiResponse<ApiResponse<Position>>(response);
return result.data;
}
/**
* 직급/직책 삭제
*
* @param id - Position ID
*/
export async function deletePosition(id: number): Promise<void> {
const response = await fetch(
`${API_URL}/v1/positions/${id}`,
createFetchOptions({
method: 'DELETE',
})
);
await handleApiResponse<ApiResponse<{ id: number; deleted_at: string }>>(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<ApiResponse<{ success: boolean; updated: number }>>(response);
return result.data;
}
// ===== 헬퍼 함수 =====
/**
* 직급 목록 조회 (헬퍼)
*/
export async function fetchRanks(params?: Omit<PositionListParams, 'type'>): Promise<Position[]> {
return fetchPositions({ ...params, type: 'rank' });
}
/**
* 직책 목록 조회 (헬퍼)
*/
export async function fetchTitles(params?: Omit<PositionListParams, 'type'>): Promise<Position[]> {
return fetchPositions({ ...params, type: 'title' });
}
/**
* 직급 생성 (헬퍼)
*/
export async function createRank(
data: Omit<PositionCreateRequest, 'type'>
): Promise<Position> {
return createPosition({ ...data, type: 'rank' });
}
/**
* 직책 생성 (헬퍼)
*/
export async function createTitle(
data: Omit<PositionCreateRequest, 'type'>
): Promise<Position> {
return createPosition({ ...data, type: 'title' });
}