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

@@ -28,6 +28,11 @@ import {
Loader2,
Play,
RotateCcw,
// 회계 아이콘
ArrowDownToLine, // 입금
ArrowUpFromLine, // 출금
Receipt, // 매입(지출결의서)
CreditCard, // 카드
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -35,6 +40,7 @@ import { useDevFillContext, type DevFillPageType } from './DevFillContext';
// 페이지 경로와 타입 매핑
const PAGE_PATTERNS: { pattern: RegExp; type: DevFillPageType; label: string }[] = [
// 판매/생산 플로우
{ pattern: /\/quote-management\/new/, type: 'quote', label: '견적' },
{ pattern: /\/quote-management\/\d+\/edit/, type: 'quote', label: '견적' },
{ pattern: /\/order-management-sales\/new/, type: 'order', label: '수주' },
@@ -44,6 +50,11 @@ const PAGE_PATTERNS: { pattern: RegExp; type: DevFillPageType; label: string }[]
{ pattern: /\/work-orders\/\d+$/, type: 'workOrderComplete', label: '작업완료' },
{ pattern: /\/shipments\/new/, type: 'shipment', label: '출하' },
{ pattern: /\/shipments\/\d+\/edit/, type: 'shipment', label: '출하' },
// 회계 플로우
{ pattern: /\/accounting\/deposits\/new/, type: 'deposit', label: '입금' },
{ pattern: /\/accounting\/withdrawals\/new/, type: 'withdrawal', label: '출금' },
{ pattern: /\/approval\/draft\/new/, type: 'purchaseApproval', label: '매입' },
{ pattern: /\/accounting\/card-transactions/, type: 'cardTransaction', label: '카드' },
];
// 플로우 단계 정의
@@ -55,6 +66,14 @@ const FLOW_STEPS: { type: DevFillPageType; label: string; icon: typeof FileText;
{ type: 'shipment', label: '출하', icon: Truck, path: '/outbound/shipments/new' },
];
// 회계 단계 정의
const ACCOUNTING_STEPS: { type: DevFillPageType; label: string; icon: typeof FileText; path: string; fillEnabled: boolean }[] = [
{ type: 'deposit', label: '입금', icon: ArrowDownToLine, path: '/accounting/deposits/new', fillEnabled: true },
{ type: 'withdrawal', label: '출금', icon: ArrowUpFromLine, path: '/accounting/withdrawals/new', fillEnabled: true },
{ type: 'purchaseApproval', label: '매입', icon: Receipt, path: '/approval/draft/new', fillEnabled: true },
{ type: 'cardTransaction', label: '카드', icon: CreditCard, path: '/accounting/card-transactions', fillEnabled: false }, // 이동만
];
export function DevToolbar() {
const pathname = usePathname();
const router = useRouter();
@@ -181,7 +200,7 @@ export function DevToolbar() {
</div>
</div>
{/* 버튼 영역 */}
{/* 판매/생산 플로우 버튼 영역 */}
{isExpanded && (
<div className="flex items-center gap-2 px-3 py-3">
{FLOW_STEPS.map((step, index) => {
@@ -255,11 +274,61 @@ export function DevToolbar() {
</div>
)}
{/* 회계 플로우 버튼 영역 */}
{isExpanded && (
<div className="flex items-center gap-2 px-3 pb-3 border-t border-yellow-300 pt-3">
<span className="text-xs text-yellow-600 font-medium mr-1">:</span>
{ACCOUNTING_STEPS.map((step) => {
const Icon = step.icon;
const isActive = activePage === step.type;
const isRegistered = hasRegisteredForm(step.type);
const isCurrentLoading = isLoading === step.type;
// 활성화된 페이지: 폼 채우기 (fillEnabled가 true인 경우만)
if (isActive && step.fillEnabled) {
return (
<Button
key={step.type}
size="sm"
variant="default"
disabled={!isRegistered || isCurrentLoading}
className="bg-blue-500 hover:bg-blue-600 text-white border-blue-600"
onClick={() => handleFillForm(step.type)}
title="폼 자동 채우기"
>
{isCurrentLoading ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
) : (
<Icon className="w-4 h-4 mr-1" />
)}
{step.label}
</Button>
);
}
// 비활성화된 페이지 또는 이동만 가능한 버튼: 해당 페이지로 이동
return (
<Button
key={step.type}
size="sm"
variant="outline"
className={`border-blue-300 text-blue-700 hover:bg-blue-100 hover:border-blue-500 ${isActive ? 'bg-blue-100 border-blue-500' : ''}`}
onClick={() => handleNavigate(step.path)}
title={`${step.label} 페이지로 이동`}
>
<Icon className="w-4 h-4 mr-1" />
{step.label}
</Button>
);
})}
</div>
)}
{/* 안내 메시지 */}
{isExpanded && !activePage && (
<div className="px-3 pb-3">
<p className="text-xs text-yellow-600">
///
//////
</p>
</div>
)}