feat(WEB): 견적 시스템 개선, 엑셀 다운로드, PDF 생성 기능 추가
견적 시스템: - QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가 - DiscountModal: 할인율/할인금액 상호 계산 모달 - QuoteTransactionModal: 거래명세서 미리보기 모달 - LocationDetailPanel, LocationListPanel 개선 템플릿 기능: - UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드) - DocumentViewer: PDF 생성 기능 개선 신규 API: - /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트 UI 개선: - 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%) - 각종 리스트 컴포넌트 정렬/필터링 개선 패키지 추가: - html2canvas, jspdf, puppeteer, dom-to-image-more Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,61 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { Package, Save, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Package } from 'lucide-react';
|
||||
|
||||
export interface FormHeaderProps {
|
||||
mode: 'create' | 'edit';
|
||||
selectedItemType: string;
|
||||
isSubmitting: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더 컴포넌트 - 기존 FormHeader와 동일한 디자인
|
||||
* 헤더 컴포넌트 - 타이틀만 표시 (버튼은 하단 sticky로 이동)
|
||||
*/
|
||||
export function FormHeader({
|
||||
mode,
|
||||
selectedItemType,
|
||||
isSubmitting,
|
||||
onCancel,
|
||||
}: FormHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg hidden md:block">
|
||||
<Package className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl">
|
||||
{mode === 'create' ? '품목 등록' : '품목 수정'}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
품목 정보를 입력하세요
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg hidden md:block">
|
||||
<Package className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 sm:gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
className="gap-1 sm:gap-2"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">취소</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={!selectedItemType || isSubmitting}
|
||||
className="gap-1 sm:gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{isSubmitting ? '저장 중...' : '저장'}</span>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl">
|
||||
{mode === 'create' ? '품목 등록' : '품목 수정'}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
품목 정보를 입력하세요
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { Save, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FormSectionSkeleton } from '@/components/ui/skeleton';
|
||||
@@ -45,6 +48,7 @@ export default function DynamicItemForm({
|
||||
onSubmit,
|
||||
}: DynamicItemFormProps) {
|
||||
const router = useRouter();
|
||||
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
||||
|
||||
// 품목 유형 상태 (변경 가능)
|
||||
const [selectedItemType, setSelectedItemType] = useState<ItemType | ''>(initialItemType || '');
|
||||
@@ -658,17 +662,12 @@ export default function DynamicItemForm({
|
||||
: [];
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="space-y-6">
|
||||
<form onSubmit={handleFormSubmit} className="space-y-6 pb-24">
|
||||
{/* Validation 에러 Alert */}
|
||||
<ValidationAlert errors={errors} />
|
||||
|
||||
{/* 헤더 */}
|
||||
<FormHeader
|
||||
mode={mode}
|
||||
selectedItemType={selectedItemType}
|
||||
isSubmitting={isSubmitting}
|
||||
onCancel={() => router.back()}
|
||||
/>
|
||||
<FormHeader mode={mode} />
|
||||
|
||||
{/* 기본 정보 - 목업과 동일한 레이아웃 (모든 필드 한 줄씩) */}
|
||||
<Card>
|
||||
@@ -1045,6 +1044,26 @@ export default function DynamicItemForm({
|
||||
onCancel={handleCancelDuplicate}
|
||||
onGoToEdit={handleGoToEditDuplicate}
|
||||
/>
|
||||
|
||||
{/* 하단 액션 버튼 (sticky) */}
|
||||
<div className={`fixed bottom-6 ${sidebarCollapsed ? 'left-[156px]' : 'left-[316px]'} right-[48px] px-6 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 flex items-center justify-between`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!selectedItemType || isSubmitting}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{isSubmitting ? '저장 중...' : '저장'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user