feat: 수주등록 모달 및 범용 컴포넌트 개발
- 범용 컴포넌트 3종 개발 및 레지스트리 등록: * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트 * EntitySearchInput: 엔티티 검색 모달 컴포넌트 * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트 - 수주등록 전용 컴포넌트: * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼) * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼) * OrderRegistrationModal: 수주등록 메인 모달 - 백엔드 API: * Entity 검색 API (멀티테넌시 지원) * 수주 등록 API (자동 채번) - 화면 편집기 통합: * 컴포넌트 레지스트리에 등록 * ConfigPanel을 통한 설정 기능 * 드래그앤드롭으로 배치 가능 - 개발 문서: * 수주등록_화면_개발_계획서.md (상세 설계 문서)
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { ItemSelectionModal } from "./ItemSelectionModal";
|
||||
import { RepeaterTable } from "./RepeaterTable";
|
||||
import { ModalRepeaterTableProps } from "./types";
|
||||
import { useCalculation } from "./useCalculation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ModalRepeaterTableComponentProps extends Partial<ModalRepeaterTableProps> {
|
||||
config?: ModalRepeaterTableProps;
|
||||
}
|
||||
|
||||
export function ModalRepeaterTableComponent({
|
||||
config,
|
||||
sourceTable: propSourceTable,
|
||||
sourceColumns: propSourceColumns,
|
||||
sourceSearchFields: propSourceSearchFields,
|
||||
modalTitle: propModalTitle,
|
||||
modalButtonText: propModalButtonText,
|
||||
multiSelect: propMultiSelect,
|
||||
columns: propColumns,
|
||||
calculationRules: propCalculationRules,
|
||||
value: propValue,
|
||||
onChange: propOnChange,
|
||||
uniqueField: propUniqueField,
|
||||
filterCondition: propFilterCondition,
|
||||
companyCode: propCompanyCode,
|
||||
className,
|
||||
}: ModalRepeaterTableComponentProps) {
|
||||
// config prop 우선, 없으면 개별 prop 사용
|
||||
const sourceTable = config?.sourceTable || propSourceTable || "";
|
||||
const sourceColumns = config?.sourceColumns || propSourceColumns || [];
|
||||
const sourceSearchFields = config?.sourceSearchFields || propSourceSearchFields || [];
|
||||
const modalTitle = config?.modalTitle || propModalTitle || "항목 검색";
|
||||
const modalButtonText = config?.modalButtonText || propModalButtonText || "품목 검색";
|
||||
const multiSelect = config?.multiSelect ?? propMultiSelect ?? true;
|
||||
const columns = config?.columns || propColumns || [];
|
||||
const calculationRules = config?.calculationRules || propCalculationRules || [];
|
||||
const value = config?.value || propValue || [];
|
||||
const onChange = config?.onChange || propOnChange || (() => {});
|
||||
const uniqueField = config?.uniqueField || propUniqueField;
|
||||
const filterCondition = config?.filterCondition || propFilterCondition || {};
|
||||
const companyCode = config?.companyCode || propCompanyCode;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
||||
|
||||
// 초기 데이터에 계산 필드 적용
|
||||
useEffect(() => {
|
||||
if (value.length > 0 && calculationRules.length > 0) {
|
||||
const calculated = calculateAll(value);
|
||||
// 값이 실제로 변경된 경우만 업데이트
|
||||
if (JSON.stringify(calculated) !== JSON.stringify(value)) {
|
||||
onChange(calculated);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAddItems = (items: any[]) => {
|
||||
// 기본값 적용
|
||||
const itemsWithDefaults = items.map((item) => {
|
||||
const newItem = { ...item };
|
||||
columns.forEach((col) => {
|
||||
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
|
||||
newItem[col.field] = col.defaultValue;
|
||||
}
|
||||
});
|
||||
return newItem;
|
||||
});
|
||||
|
||||
// 계산 필드 업데이트
|
||||
const calculatedItems = calculateAll(itemsWithDefaults);
|
||||
|
||||
// 기존 데이터에 추가
|
||||
onChange([...value, ...calculatedItems]);
|
||||
};
|
||||
|
||||
const handleRowChange = (index: number, newRow: any) => {
|
||||
// 계산 필드 업데이트
|
||||
const calculatedRow = calculateRow(newRow);
|
||||
|
||||
// 데이터 업데이트
|
||||
const newData = [...value];
|
||||
newData[index] = calculatedRow;
|
||||
onChange(newData);
|
||||
};
|
||||
|
||||
const handleRowDelete = (index: number) => {
|
||||
const newData = value.filter((_, i) => i !== index);
|
||||
onChange(newData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* 추가 버튼 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{value.length > 0 && `${value.length}개 항목`}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{modalButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Repeater 테이블 */}
|
||||
<RepeaterTable
|
||||
columns={columns}
|
||||
data={value}
|
||||
onDataChange={onChange}
|
||||
onRowChange={handleRowChange}
|
||||
onRowDelete={handleRowDelete}
|
||||
/>
|
||||
|
||||
{/* 항목 선택 모달 */}
|
||||
<ItemSelectionModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
sourceTable={sourceTable}
|
||||
sourceColumns={sourceColumns}
|
||||
sourceSearchFields={sourceSearchFields}
|
||||
multiSelect={multiSelect}
|
||||
filterCondition={filterCondition}
|
||||
modalTitle={modalTitle}
|
||||
alreadySelected={value}
|
||||
uniqueField={uniqueField}
|
||||
onSelect={handleAddItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user