Files
wace_rps/frontend/components/common/CustomerSearchDialog.tsx
T
hjjeong 489fa50d11 영업관리 4개 메뉴 검색폼 wace 일치 + 공통 UX(초기화·date input) 정비
- 검색 폼 정합성: wace JSP `<!-- 주석처리된 검색필터 -->` 블록까지 잘못 이식했던 부분 정정
  - 견적: 11→7개 (제품구분/국내해외/유무상/요청납기 제거)
  - 주문: 13→9개 (제품구분/국내해외/유무상/견적환종 제거)
  - 매출: 10→11개 (출하지시상태 제거 + 제품구분·국내/해외 추가, JSP 순서로 재배치)
  - 판매: 변경 없음 (원본 그대로 일치)
- 매출 백엔드: SaleListFilter에 productType/nation 추가, getRevenueList에 partObjId/serialNo/orderDate/productType/nation 5개 필터 처리
- 공통 UX
  - 초기화 버튼을 4개 메뉴 동일하게 통일 (variant=ghost, 버튼 영역 끝)
  - <Input type="date">는 빈 값 placeholder 숨김 + 캘린더 아이콘 숨김 + 영역 클릭으로 picker 자동(showPicker)
- 신규 공통 컴포넌트: CommCodeSelect/CustomerSelect/CustomerSearchDialog/PartSelect/ItemSearchDialog + backend salesCommonRoutes
- 문서: 01/02/04 검색 폼 표를 활성/비활성 분리 형식으로 정정, README에 8. 공통 UX 규칙 섹션 신설

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:42:16 +09:00

151 lines
5.2 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { apiClient } from "@/lib/api/client";
import { toast } from "sonner";
export interface CustomerRow {
objid?: string | number;
id?: string | number;
customer_name?: string;
contact_person?: string;
business_number?: string;
contact_phone?: string;
address?: string;
customer_code?: string;
[key: string]: any;
}
/**
* customer_mng.id (정수) → contract_mgmt.customer_objid 'C_xxxxxxxxxx' (10자리 padded)
* 영업관리(contract_mgmt) 테이블이 사용하는 포맷.
*/
export const toContractCustomerObjid = (id: number | string | null | undefined) => {
if (id === null || id === undefined || id === "") return "";
return `C_${String(id).padStart(10, "0")}`;
};
interface CustomerSearchDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSelect: (customer: CustomerRow) => void;
title?: string;
description?: string;
}
export function CustomerSearchDialog({
open, onOpenChange, onSelect,
title = "거래처 검색",
description = "거래처를 검색하여 선택하세요.",
}: CustomerSearchDialogProps) {
const [keyword, setKeyword] = useState("");
const [results, setResults] = useState<CustomerRow[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (open) {
setKeyword("");
setResults([]);
void search("");
}
}, [open]);
const search = async (kw?: string) => {
const k = kw ?? keyword;
setLoading(true);
try {
const filters: any[] = [];
if (k) filters.push({ columnName: "customer_name", operator: "contains", value: k });
const res = await apiClient.post("/table-management/tables/customer_mng/data", {
page: 1, size: 50,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
setResults(resData?.data || resData?.rows || []);
} catch {
toast.error("거래처 조회 실패");
} finally {
setLoading(false);
}
};
const handleSelect = (cust: CustomerRow) => {
onSelect(cust);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div className="flex gap-2">
<Input
placeholder="거래처명 검색"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && search()}
className="text-sm"
/>
<Button onClick={() => search()} disabled={loading} className="shrink-0 gap-1">
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "검색"}
</Button>
</div>
<div className="flex-1 overflow-auto rounded border">
<table className="w-full text-xs">
<thead className="sticky top-0 bg-gray-50">
<tr>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-left"></th>
<th className="px-3 py-2 text-left"></th>
<th className="w-16 px-2 py-2 text-center"></th>
</tr>
</thead>
<tbody>
{results.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-gray-400">
{loading ? "검색 중..." : "검색 결과가 없습니다."}
</td>
</tr>
) : results.map((row, i) => (
<tr
key={row.objid ?? row.id ?? i}
className="cursor-pointer border-t hover:bg-blue-50"
onClick={() => handleSelect(row)}
>
<td className="px-3 py-2 font-medium">{row.customer_name || "-"}</td>
<td className="px-3 py-2">{row.contact_person || "-"}</td>
<td className="px-3 py-2">{row.business_number || "-"}</td>
<td className="px-3 py-2">{row.contact_phone || "-"}</td>
<td className="px-2 py-2 text-center">
<Button
size="sm"
variant="outline"
className="h-6 text-xs"
onClick={(e) => { e.stopPropagation(); handleSelect(row); }}
></Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}