feat(WEB): FCM 푸시 알림 공통 모듈 및 기안함 연동
- FCM 공통 모듈 생성 (src/lib/actions/fcm.ts) - sendFcmNotification: 기본 FCM 발송 함수 - sendApprovalNotification: 결재 알림 프리셋 - sendWorkOrderNotification: 작업지시 알림 프리셋 - sendNoticeNotification: 공지사항 알림 프리셋 - 기안함 페이지에 '문서완료' 버튼 추가 - Bell 아이콘 + FCM 발송 기능 - 발송 결과 토스트 메시지 표시
This commit is contained in:
@@ -457,4 +457,4 @@ export async function cancelDraft(id: string): Promise<{ success: boolean; error
|
|||||||
error: '서버 오류가 발생했습니다.',
|
error: '서버 오류가 발생했습니다.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Plus,
|
Plus,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
Bell,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
submitDraft,
|
submitDraft,
|
||||||
submitDrafts,
|
submitDrafts,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
import { sendApprovalNotification } from '@/lib/actions/fcm';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -243,6 +245,24 @@ export function DraftBox() {
|
|||||||
router.push('/ko/approval/draft/new');
|
router.push('/ko/approval/draft/new');
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
// ===== FCM 알림 발송 핸들러 =====
|
||||||
|
const handleSendNotification = useCallback(async () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
const result = await sendApprovalNotification();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(`결재 알림을 발송했습니다. (${result.sentCount || 0}건)`);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || '알림 발송에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isNextRedirectError(error)) throw error;
|
||||||
|
console.error('Notification error:', error);
|
||||||
|
toast.error('알림 발송 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ===== 문서 클릭/수정 핸들러 (조건부 로직) =====
|
// ===== 문서 클릭/수정 핸들러 (조건부 로직) =====
|
||||||
// 임시저장 → 문서 작성 페이지 (수정 모드)
|
// 임시저장 → 문서 작성 페이지 (수정 모드)
|
||||||
// 그 외 → 문서 상세 모달 (상세 API 호출하여 content 포함된 데이터 가져옴)
|
// 그 외 → 문서 상세 모달 (상세 API 호출하여 content 포함된 데이터 가져옴)
|
||||||
@@ -597,6 +617,10 @@ export function DraftBox() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Button variant="outline" onClick={handleSendNotification}>
|
||||||
|
<Bell className="h-4 w-4 mr-2" />
|
||||||
|
문서완료
|
||||||
|
</Button>
|
||||||
<Button onClick={handleNewDocument}>
|
<Button onClick={handleNewDocument}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
문서 작성
|
문서 작성
|
||||||
|
|||||||
148
src/lib/actions/fcm.ts
Normal file
148
src/lib/actions/fcm.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* FCM 푸시 알림 공통 서버 액션
|
||||||
|
*
|
||||||
|
* 사용 예시:
|
||||||
|
* import { sendFcmNotification, sendApprovalNotification } from '@/lib/actions/fcm';
|
||||||
|
*
|
||||||
|
* // 기본 알림 발송
|
||||||
|
* const result = await sendFcmNotification({ title: '알림', body: '내용' });
|
||||||
|
*
|
||||||
|
* // 결재 알림 발송 (프리셋)
|
||||||
|
* const result = await sendApprovalNotification();
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||||
|
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 타입 정의
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface FcmNotificationParams {
|
||||||
|
/** 알림 제목 (필수) */
|
||||||
|
title: string;
|
||||||
|
/** 알림 본문 (필수) */
|
||||||
|
body: string;
|
||||||
|
/** 특정 테넌트에게만 발송 */
|
||||||
|
tenant_id?: number;
|
||||||
|
/** 특정 사용자에게만 발송 */
|
||||||
|
user_id?: number;
|
||||||
|
/** 플랫폼 필터 (android, ios, web) */
|
||||||
|
platform?: 'android' | 'ios' | 'web';
|
||||||
|
/** 알림 채널 ID (Android) */
|
||||||
|
channel_id?: string;
|
||||||
|
/** 알림 타입 (앱에서 분기 처리용) */
|
||||||
|
type?: string;
|
||||||
|
/** 클릭 시 이동할 URL */
|
||||||
|
url?: string;
|
||||||
|
/** 알림 사운드 키 */
|
||||||
|
sound_key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FcmResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
sentCount?: number;
|
||||||
|
__authError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 기본 FCM 발송 함수
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FCM 푸시 알림 발송
|
||||||
|
*/
|
||||||
|
export async function sendFcmNotification(
|
||||||
|
params: FcmNotificationParams
|
||||||
|
): Promise<FcmResult> {
|
||||||
|
try {
|
||||||
|
const { response, error } = await serverFetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/admin/fcm/send`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error?.__authError) {
|
||||||
|
return { success: false, error: '인증이 필요합니다.', __authError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return { success: false, error: error?.message || 'FCM 발송에 실패했습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: result.message || 'FCM 발송에 실패했습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
sentCount: result.data?.success || 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (isNextRedirectError(error)) throw error;
|
||||||
|
console.error('[FCM] sendFcmNotification error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '서버 오류가 발생했습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 프리셋 함수들 (자주 사용하는 알림 타입)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 결재 알림 발송 (프리셋)
|
||||||
|
*/
|
||||||
|
export async function sendApprovalNotification(
|
||||||
|
customParams?: Partial<FcmNotificationParams>
|
||||||
|
): Promise<FcmResult> {
|
||||||
|
return sendFcmNotification({
|
||||||
|
title: '결재 알림',
|
||||||
|
body: '결재 문서가 완료되었습니다.',
|
||||||
|
type: 'approval',
|
||||||
|
channel_id: 'approval',
|
||||||
|
...customParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 작업지시 알림 발송 (프리셋)
|
||||||
|
*/
|
||||||
|
export async function sendWorkOrderNotification(
|
||||||
|
customParams?: Partial<FcmNotificationParams>
|
||||||
|
): Promise<FcmResult> {
|
||||||
|
return sendFcmNotification({
|
||||||
|
title: '작업지시 알림',
|
||||||
|
body: '새로운 작업지시가 있습니다.',
|
||||||
|
type: 'work_order',
|
||||||
|
channel_id: 'work_order',
|
||||||
|
...customParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일반 공지 알림 발송 (프리셋)
|
||||||
|
*/
|
||||||
|
export async function sendNoticeNotification(
|
||||||
|
customParams?: Partial<FcmNotificationParams>
|
||||||
|
): Promise<FcmResult> {
|
||||||
|
return sendFcmNotification({
|
||||||
|
title: '공지사항',
|
||||||
|
body: '새로운 공지사항이 있습니다.',
|
||||||
|
type: 'notice',
|
||||||
|
channel_id: 'notice',
|
||||||
|
...customParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user