feat(WEB): FCM 푸시 알림 시스템 구현
- FCMProvider 컨텍스트 및 useFCM 훅 추가 - Capacitor FCM 플러그인 통합 - 알림 사운드 파일 추가 (default.wav, push_notification.wav) - Firebase 메시징 패키지 의존성 추가
This commit is contained in:
296
src/lib/api/positions.ts
Normal file
296
src/lib/api/positions.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 직급/직책 통합 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' });
|
||||
}
|
||||
Reference in New Issue
Block a user