feat(WEB): 견적서 V2 컴포넌트 개선 및 미리보기 모달 패턴 적용
- LocationDetailPanel: 6개 탭 구현 (본체, 가이드레일, 케이스, 하단마감재, 모터&제어기, 부자재) - 각 탭별 다른 테이블 컬럼 구조 적용 - QuoteSummaryPanel: 개소별/상세별 합계 패널 개선 - QuotePreviewModal: EstimateDocumentModal 패턴 적용 (헤더+버튼 영역 분리) - Input value → defaultValue 변경으로 React 경고 해결 - 팩스/카카오톡 버튼 제거 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
128
src/components/quotes/ItemSearchModal.tsx
Normal file
128
src/components/quotes/ItemSearchModal.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 품목 검색 모달
|
||||
*
|
||||
* - 품목 코드로 검색
|
||||
* - 품목 목록에서 선택
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { Search, X } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
// =============================================================================
|
||||
// 목데이터 - 품목 목록
|
||||
// =============================================================================
|
||||
|
||||
const MOCK_ITEMS = [
|
||||
{ code: "KSS01", name: "스크린", description: "방화스크린 기본형" },
|
||||
{ code: "KSS02", name: "스크린", description: "방화스크린 고급형" },
|
||||
{ code: "KSS03", name: "슬랫", description: "방화슬랫 기본형" },
|
||||
{ code: "KSS04", name: "스크린", description: "방화스크린 특수형" },
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// Props
|
||||
// =============================================================================
|
||||
|
||||
interface ItemSearchModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelectItem: (item: { code: string; name: string }) => void;
|
||||
tabLabel?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 컴포넌트
|
||||
// =============================================================================
|
||||
|
||||
export function ItemSearchModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelectItem,
|
||||
tabLabel,
|
||||
}: ItemSearchModalProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// 검색 필터링
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!searchQuery) return MOCK_ITEMS;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return MOCK_ITEMS.filter(
|
||||
(item) =>
|
||||
item.code.toLowerCase().includes(query) ||
|
||||
item.name.toLowerCase().includes(query) ||
|
||||
item.description.toLowerCase().includes(query)
|
||||
);
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleSelect = (item: (typeof MOCK_ITEMS)[0]) => {
|
||||
onSelectItem({ code: item.code, name: item.name });
|
||||
onOpenChange(false);
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 검색</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 검색 입력 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="원하는 검색어..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<X className="h-4 w-4 text-gray-400 hover:text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 품목 목록 */}
|
||||
<div className="max-h-[300px] overflow-y-auto border rounded-lg divide-y">
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="p-4 text-center text-gray-500 text-sm">
|
||||
검색 결과가 없습니다
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => (
|
||||
<div
|
||||
key={item.code}
|
||||
onClick={() => handleSelect(item)}
|
||||
className="p-3 hover:bg-blue-50 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="font-semibold text-gray-900">{item.code}</span>
|
||||
<span className="ml-2 text-sm text-gray-500">{item.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="text-xs text-gray-400 mt-1">{item.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user