- 범용 컴포넌트 3종 개발 및 레지스트리 등록: * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트 * EntitySearchInput: 엔티티 검색 모달 컴포넌트 * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트 - 수주등록 전용 컴포넌트: * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼) * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼) * OrderRegistrationModal: 수주등록 메인 모달 - 백엔드 API: * Entity 검색 API (멀티테넌시 지원) * 수주 등록 API (자동 채번) - 화면 편집기 통합: * 컴포넌트 레지스트리에 등록 * ConfigPanel을 통한 설정 기능 * 드래그앤드롭으로 배치 가능 - 개발 문서: * 수주등록_화면_개발_계획서.md (상세 설계 문서)
127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Search, X } from "lucide-react";
|
|
import { EntitySearchModal } from "./EntitySearchModal";
|
|
import { EntitySearchInputProps, EntitySearchResult } from "./types";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function EntitySearchInputComponent({
|
|
tableName,
|
|
displayField,
|
|
valueField,
|
|
searchFields = [displayField],
|
|
mode = "combo",
|
|
placeholder = "검색...",
|
|
disabled = false,
|
|
filterCondition = {},
|
|
value,
|
|
onChange,
|
|
modalTitle = "검색",
|
|
modalColumns = [],
|
|
showAdditionalInfo = false,
|
|
additionalFields = [],
|
|
className,
|
|
}: EntitySearchInputProps) {
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [displayValue, setDisplayValue] = useState("");
|
|
const [selectedData, setSelectedData] = useState<EntitySearchResult | null>(null);
|
|
|
|
// value가 변경되면 표시값 업데이트
|
|
useEffect(() => {
|
|
if (value && selectedData) {
|
|
setDisplayValue(selectedData[displayField] || "");
|
|
} else {
|
|
setDisplayValue("");
|
|
setSelectedData(null);
|
|
}
|
|
}, [value, displayField]);
|
|
|
|
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
|
setSelectedData(fullData);
|
|
setDisplayValue(fullData[displayField] || "");
|
|
onChange?.(newValue, fullData);
|
|
};
|
|
|
|
const handleClear = () => {
|
|
setDisplayValue("");
|
|
setSelectedData(null);
|
|
onChange?.(null, null);
|
|
};
|
|
|
|
const handleOpenModal = () => {
|
|
if (!disabled) {
|
|
setModalOpen(true);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={cn("space-y-2", className)}>
|
|
{/* 입력 필드 */}
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Input
|
|
value={displayValue}
|
|
onChange={(e) => setDisplayValue(e.target.value)}
|
|
placeholder={placeholder}
|
|
disabled={disabled}
|
|
readOnly={mode === "modal" || mode === "combo"}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm pr-8"
|
|
/>
|
|
{displayValue && !disabled && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClear}
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{(mode === "modal" || mode === "combo") && (
|
|
<Button
|
|
type="button"
|
|
onClick={handleOpenModal}
|
|
disabled={disabled}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
>
|
|
<Search className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 추가 정보 표시 */}
|
|
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
|
|
<div className="text-xs text-muted-foreground space-y-1 px-2">
|
|
{additionalFields.map((field) => (
|
|
<div key={field} className="flex gap-2">
|
|
<span className="font-medium">{field}:</span>
|
|
<span>{selectedData[field] || "-"}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 검색 모달 */}
|
|
<EntitySearchModal
|
|
open={modalOpen}
|
|
onOpenChange={setModalOpen}
|
|
tableName={tableName}
|
|
displayField={displayField}
|
|
valueField={valueField}
|
|
searchFields={searchFields}
|
|
filterCondition={filterCondition}
|
|
modalTitle={modalTitle}
|
|
modalColumns={modalColumns}
|
|
onSelect={handleSelect}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|