feat(WEB): FCM 푸시 알림, 입금 등록, 견적 저장 개선

- 수주 상세 페이지에서 수주확정 시 FCM 푸시 알림 발송 추가
- FCM 프리셋 함수 추가: 계약완료, 발주완료 알림
- 입금 등록 시 입금일, 입금계좌, 입금자명, 입금금액 입력 가능
- 견적 저장 시 토스트 메시지 정상 표시 수정
- ShipmentCreate SelectItem key prop 경고 수정
- DevToolbar 문법 오류 수정
This commit is contained in:
2026-01-22 19:31:19 +09:00
parent 5a00828568
commit 92af11c787
12 changed files with 446 additions and 68 deletions

View File

@@ -67,6 +67,7 @@ import {
type Order,
type OrderStatus,
} from "@/components/orders";
import { sendSalesOrderNotification } from "@/lib/actions/fcm";
// 상태 뱃지 헬퍼
@@ -214,6 +215,20 @@ export default function OrderDetailPage() {
setOrder(result.data);
toast.success("수주가 확정되었습니다.");
setIsConfirmDialogOpen(false);
// FCM 푸시 알림 발송
try {
const fcmResult = await sendSalesOrderNotification({
body: `${order.client} - ${order.siteName} 수주가 확정되었습니다.`,
});
if (fcmResult.success) {
console.log(`[FCM] 수주확정 알림 발송 성공 (${fcmResult.sentCount || 0}건)`);
} else {
console.warn('[FCM] 수주확정 알림 발송 실패:', fcmResult.error);
}
} catch (fcmError) {
console.error('[FCM] 수주확정 알림 발송 오류:', fcmError);
}
} else {
toast.error(result.error || "수주 확정에 실패했습니다.");
}

View File

@@ -151,8 +151,8 @@ export default function QuoteDetailPage() {
};
// V2 패턴: 수정 저장 핸들러
const handleSave = async (formData: QuoteFormData) => {
if (isSaving) return;
const handleSave = async (formData: QuoteFormData): Promise<{ success: boolean; error?: string }> => {
if (isSaving) return { success: false, error: '저장 중입니다.' };
setIsSaving(true);
try {
@@ -160,13 +160,14 @@ export default function QuoteDetailPage() {
const result = await updateQuote(quoteId, apiData as any);
if (result.success) {
toast.success("견적이 수정되었습니다.");
// toast는 IntegratedDetailTemplate에서 처리
router.push(`/sales/quote-management/${quoteId}`);
return { success: true };
} else {
toast.error(result.error || "견적 수정에 실패했습니다.");
return { success: false, error: result.error || "견적 수정에 실패했습니다." };
}
} catch (error) {
toast.error("견적 수정에 실패했습니다.");
return { success: false, error: "견적 수정에 실패했습니다." };
} finally {
setIsSaving(false);
}

View File

@@ -18,8 +18,8 @@ export default function QuoteNewPage() {
router.push("/sales/quote-management");
};
const handleSave = async (formData: QuoteFormData) => {
if (isSaving) return;
const handleSave = async (formData: QuoteFormData): Promise<{ success: boolean; error?: string }> => {
if (isSaving) return { success: false, error: '저장 중입니다.' };
setIsSaving(true);
try {
@@ -45,13 +45,14 @@ export default function QuoteNewPage() {
const result = await createQuote(apiData as any);
if (result.success && result.data) {
toast.success("견적이 등록되었습니다.");
// toast는 IntegratedDetailTemplate에서 처리
router.push(`/sales/quote-management/${result.data.id}`);
return { success: true };
} else {
toast.error(result.error || "견적 등록에 실패했습니다.");
return { success: false, error: result.error || "견적 등록에 실패했습니다." };
}
} catch (error) {
toast.error("견적 등록에 실패했습니다.");
return { success: false, error: "견적 등록에 실패했습니다." };
} finally {
setIsSaving(false);
}