feat: 수주등록 모달 및 범용 컴포넌트 개발

- 범용 컴포넌트 3종 개발 및 레지스트리 등록:
  * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트
  * EntitySearchInput: 엔티티 검색 모달 컴포넌트
  * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트

- 수주등록 전용 컴포넌트:
  * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼)
  * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼)
  * OrderRegistrationModal: 수주등록 메인 모달

- 백엔드 API:
  * Entity 검색 API (멀티테넌시 지원)
  * 수주 등록 API (자동 채번)

- 화면 편집기 통합:
  * 컴포넌트 레지스트리에 등록
  * ConfigPanel을 통한 설정 기능
  * 드래그앤드롭으로 배치 가능

- 개발 문서:
  * 수주등록_화면_개발_계획서.md (상세 설계 문서)
This commit is contained in:
kjs
2025-11-14 14:43:53 +09:00
parent 075869c89c
commit 64e6fd1920
46 changed files with 6086 additions and 8 deletions

View File

@@ -0,0 +1,138 @@
"use client";
import React, { useState } from "react";
import { EntitySearchInputComponent } from "@/lib/registry/components/entity-search-input";
import { AutocompleteSearchInputComponent } from "@/lib/registry/components/autocomplete-search-input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
export default function TestEntitySearchPage() {
const [customerCode, setCustomerCode] = useState<string>("");
const [customerData, setCustomerData] = useState<any>(null);
const [itemCode, setItemCode] = useState<string>("");
const [itemData, setItemData] = useState<any>(null);
return (
<div className="container mx-auto p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold">EntitySearchInput </h1>
<p className="text-muted-foreground mt-2">
</p>
</div>
{/* 거래처 검색 테스트 - 자동완성 방식 */}
<Card>
<CardHeader>
<CardTitle> ( ) NEW</CardTitle>
<CardDescription>
-
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<AutocompleteSearchInputComponent
tableName="customer_mng"
displayField="customer_name"
valueField="customer_code"
searchFields={["customer_name", "customer_code", "business_number"]}
placeholder="거래처명 입력하여 검색"
showAdditionalInfo
additionalFields={["customer_code", "address", "contact_phone"]}
value={customerCode}
onChange={(code, fullData) => {
setCustomerCode(code || "");
setCustomerData(fullData);
}}
/>
</div>
{customerData && (
<div className="mt-4 p-4 bg-muted rounded-md">
<h3 className="font-semibold mb-2"> :</h3>
<pre className="text-xs">
{JSON.stringify(customerData, null, 2)}
</pre>
</div>
)}
</CardContent>
</Card>
{/* 거래처 검색 테스트 - 모달 방식 */}
<Card>
<CardHeader>
<CardTitle> ( )</CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<EntitySearchInputComponent
tableName="customer_mng"
displayField="customer_name"
valueField="customer_code"
searchFields={["customer_name", "customer_code", "business_number"]}
mode="combo"
placeholder="거래처를 검색하세요"
modalTitle="거래처 검색 및 선택"
modalColumns={["customer_code", "customer_name", "address", "contact_phone"]}
showAdditionalInfo
additionalFields={["address", "contact_phone", "business_number"]}
value={customerCode}
onChange={(code, fullData) => {
setCustomerCode(code || "");
setCustomerData(fullData);
}}
/>
</div>
</CardContent>
</Card>
{/* 품목 검색 테스트 */}
<Card>
<CardHeader>
<CardTitle> (Modal )</CardTitle>
<CardDescription>
item_info
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<EntitySearchInputComponent
tableName="item_info"
displayField="item_name"
valueField="id"
searchFields={["item_name", "id", "item_number"]}
mode="modal"
placeholder="품목 선택"
modalTitle="품목 검색"
modalColumns={["id", "item_name", "item_number", "unit", "selling_price"]}
showAdditionalInfo
additionalFields={["item_number", "unit", "selling_price"]}
value={itemCode}
onChange={(code, fullData) => {
setItemCode(code || "");
setItemData(fullData);
}}
/>
</div>
{itemData && (
<div className="mt-4 p-4 bg-muted rounded-md">
<h3 className="font-semibold mb-2"> :</h3>
<pre className="text-xs">
{JSON.stringify(itemData, null, 2)}
</pre>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import React, { useState } from "react";
import { OrderRegistrationModal } from "@/components/order/OrderRegistrationModal";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function TestOrderRegistrationPage() {
const [modalOpen, setModalOpen] = useState(false);
const handleSuccess = () => {
console.log("수주 등록 성공!");
};
return (
<div className="container mx-auto p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold"> </h1>
<p className="text-muted-foreground mt-2">
EntitySearchInput + ModalRepeaterTable을
</p>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={() => setModalOpen(true)}>
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span>EntitySearchInput: 거래처 ( )</span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span>ModalRepeaterTable: 품목 </span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span> 계산: 수량 × = </span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span> 편집: 수량, , , </span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span> 방지: 이미 </span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span> 삭제: 추가된 </span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span> 표시: 모든 </span>
</div>
<div className="flex items-start gap-2">
<span className="text-primary"></span>
<span> 전환: 거래처 / / </span>
</div>
</CardContent>
</Card>
{/* 수주 등록 모달 */}
<OrderRegistrationModal
open={modalOpen}
onOpenChange={setModalOpen}
onSuccess={handleSuccess}
/>
</div>
);
}