From 364d4707fe4793e9dba0beb171658a9a4bd199e3 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Wed, 13 May 2026 16:44:26 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20=E2=80=94=20native=20=20=EA=B8=88=EC=A7=80=20+=20CompactFilterBar=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4=20+=20M-BOM=20=EC=8B=9C=EB=B2=94=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 영업관리에 이미 적용된 SmartSelect/CustomerSelect 패턴을 다른 메뉴(생산/개발/프로젝트) 의 native → SmartSelect 일괄 교체: - production/mbom/page.tsx 5건 (주문유형/제품구분/국내해외/고객사/유무상) - development/change-list/page.tsx 1건 (년도) - development/ebom-regist/page.tsx 1건 (상태) - development/ebom-search/page.tsx 1건 (표시레벨) - project/progress/page.tsx 3건 (년도/국내해외/유무상) - components/development/PartFormDialog.tsx — BasicSelect 가 내부적으로 SmartSelect 위임 - components/development/BomReportExcelImportDialog.tsx — E-BOM 복사 옵션 M-BOM 시범 마이그레이션: - 기존: 2행 grid 6×2 검색 폼 (h-9 큰 입력) - 변경: 안에 10개 (h-7 컴팩트) 원칙: - 향후 모든 신규/수정 페이지는 CompactFilterBar + SmartSelect/CustomerSelect 사용 필수 - native setFilter({ ...filter, Year: e.target.value })}> - - {YEAR_OPTIONS.map((y) => )} - + setFilter({ ...filter, Year: v })} + placeholder="전체" + /> - + onValueChange={(v) => setFilter({ ...filter, status: v })} + placeholder="전체" + /> {/* wace structureList.jsp 1:1 — select2-part 자동완성 (양방향 동기) */} diff --git a/frontend/app/(main)/COMPANY_16/development/ebom-search/page.tsx b/frontend/app/(main)/COMPANY_16/development/ebom-search/page.tsx index 567f27a6..1239e682 100644 --- a/frontend/app/(main)/COMPANY_16/development/ebom-search/page.tsx +++ b/frontend/app/(main)/COMPANY_16/development/ebom-search/page.tsx @@ -15,6 +15,15 @@ import { toast } from "sonner"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { devBomApi, BomTreeFilter, BomTreeRow } from "@/lib/api/devBom"; import { DevPartSelect } from "@/components/development/DevPartSelect"; +import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect"; + +const LEVEL_OPTIONS: SmartSelectOption[] = [ + { code: "1", label: "1레벨" }, + { code: "2", label: "2레벨" }, + { code: "3", label: "3레벨" }, + { code: "4", label: "4레벨" }, + { code: "5", label: "5레벨" }, +]; import { PartDetailDialog } from "@/components/development/PartDetailDialog"; type Direction = "ascending" | "descending"; @@ -203,18 +212,12 @@ export default function EbomSearchPage() { }))} /> - + onValueChange={(v) => setFilter({ ...filter, search_level: v })} + placeholder="전체" + />
diff --git a/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx b/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx index 115d8a66..42bdd7c0 100644 --- a/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/mbom/page.tsx @@ -9,12 +9,12 @@ // ※ BOM 복사 / 구매리스트 생성 / M-BOM 본 편집 — PR-B 분리. import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Search, Loader2, RotateCcw } from "lucide-react"; import { toast } from "sonner"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; +import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect"; +import { CustomerSelect } from "@/components/common/CustomerSelect"; +import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar"; import { apiClient } from "@/lib/api/client"; import { mbomApi, MbomListFilter, MbomRow } from "@/lib/api/mbom"; import { MbomDetailDialog } from "@/components/production/MbomDetailDialog"; @@ -23,8 +23,18 @@ const PARENT_CATEGORY = "0000167"; // 주문유형 comm_code parent_code_id const PARENT_PRODUCT = "0000001"; // 제품구분 comm_code parent_code_id const PARENT_PAID = "0001782"; // 유/무상 (참고: 빈 결과여도 raw paid/free 매칭으로 fallback) -interface CodeOpt { code: string; label: string; sort?: number | null } -interface CustomerOpt { id: number | string; customer_name: string | null; customer_code: string | null } +interface CodeOpt extends SmartSelectOption { sort?: number | null } + +const AREA_OPTS: SmartSelectOption[] = [ + { code: "국내", label: "국내" }, + { code: "해외", label: "해외" }, +]; + +// 운영판 1:1 — paid/free raw 매칭이 기본. comm_code 응답이 비어있을 때 사용. +const PAID_FALLBACK_OPTS: SmartSelectOption[] = [ + { code: "paid", label: "유상" }, + { code: "free", label: "무상" }, +]; const EMPTY_FILTER: MbomListFilter = { search_category_cd: "", @@ -71,7 +81,6 @@ export default function MbomMgmtPage() { const [categoryOpts, setCategoryOpts] = useState([]); const [productOpts, setProductOpts] = useState([]); const [paidOpts, setPaidOpts] = useState([]); - const [customerOpts, setCustomerOpts] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [dialogObjid, setDialogObjid] = useState(null); @@ -95,17 +104,15 @@ export default function MbomMgmtPage() { let dead = false; (async () => { try { - const [c1, c2, c3, cust] = await Promise.all([ + const [c1, c2, c3] = await Promise.all([ apiClient.get(`/sales/codes/${PARENT_CATEGORY}`), apiClient.get(`/sales/codes/${PARENT_PRODUCT}`), apiClient.get(`/sales/codes/${PARENT_PAID}`), - apiClient.get(`/sales/customers`), ]); if (dead) return; setCategoryOpts(c1.data?.data ?? []); setProductOpts(c2.data?.data ?? []); setPaidOpts(c3.data?.data ?? []); - setCustomerOpts(cust.data?.data ?? []); } catch { /* 옵션 로드 실패는 무시 — 그리드는 조회 가능 */ } @@ -132,133 +139,84 @@ export default function MbomMgmtPage() { }; return ( -
-
- {/* 운영판 wace mBomMgmtList.jsp 1:1 — 2행 검색 폼 */} -
-
- - setFilter({ ...filter, search_category_cd: v })} - /> - - - setFilter({ ...filter, search_product_cd: v })} - /> - - - - - - - - - - - - setFilter({ ...filter, search_serial_no: e.target.value })} - /> - -
-
- - setFilter({ ...filter, search_part_no: e.target.value })} - /> - - - setFilter({ ...filter, search_part_name: e.target.value })} - /> - - - setFilter({ ...filter, search_receipt_date_from: e.target.value })} - /> - - - setFilter({ ...filter, search_receipt_date_to: e.target.value })} - /> - - - setFilter({ ...filter, search_req_del_date_from: e.target.value })} - /> - - - setFilter({ ...filter, search_req_del_date_to: e.target.value })} - /> - -
-
+
+ 총 {total.toLocaleString()}건 · PROJECT_MGMT × CONTRACT_ITEM 펼침} + > + + setFilter({ ...filter, search_category_cd: v })} + /> + + + setFilter({ ...filter, search_product_cd: v })} + /> + + + setFilter({ ...filter, search_area_cd: v })} + /> + + + setFilter({ ...filter, search_customer_objid: v })} + /> + + + 0 ? paidOpts : PAID_FALLBACK_OPTS} + value={filter.search_paid_type ?? ""} + onValueChange={(v) => setFilter({ ...filter, search_paid_type: v })} + /> + + + setFilter({ ...filter, search_serial_no: e.target.value })} + /> + + + setFilter({ ...filter, search_part_no: e.target.value })} + /> + + + setFilter({ ...filter, search_part_name: e.target.value })} + /> + + + setFilter({ ...filter, search_receipt_date_from: v })} + to={filter.search_receipt_date_to ?? ""} + setTo={(v) => setFilter({ ...filter, search_receipt_date_to: v })} + /> + + + setFilter({ ...filter, search_req_del_date_from: v })} + to={filter.search_req_del_date_to ?? ""} + setTo={(v) => setFilter({ ...filter, search_req_del_date_to: v })} + /> + + -
-
- 총 {total.toLocaleString()}건 · PROJECT_MGMT × CONTRACT_ITEM 펼침 -
-
- - -
-
-
- -
+
- - {children} -
- ); -} - -function SelectBox({ - value, options, onChange, -}: { value: string; options: CodeOpt[]; onChange: (v: string) => void }) { - return ( - - ); -} diff --git a/frontend/app/(main)/COMPANY_16/project/progress/page.tsx b/frontend/app/(main)/COMPANY_16/project/progress/page.tsx index e7984bab..669a59f3 100644 --- a/frontend/app/(main)/COMPANY_16/project/progress/page.tsx +++ b/frontend/app/(main)/COMPANY_16/project/progress/page.tsx @@ -78,13 +78,23 @@ const CATEGORY_GROUP = "0000167"; // 주문유형 const PRODUCT_GROUP = "0000001"; // 제품구분 // wace L229: 시스템년도 ±4 (운영판은 sysYear-4 ~ sysYear). RPS는 sysYear±4로 여유. -const YEAR_OPTIONS = (() => { +const YEAR_OPTIONS: SmartSelectOption[] = (() => { const cur = new Date().getFullYear(); - const arr: string[] = []; - for (let y = cur + 4; y >= cur - 4; y--) arr.push(String(y)); + const arr: SmartSelectOption[] = []; + for (let y = cur + 4; y >= cur - 4; y--) arr.push({ code: String(y), label: String(y) }); return arr; })(); +const AREA_OPTIONS: SmartSelectOption[] = [ + { code: "국내", label: "국내" }, + { code: "해외", label: "해외" }, +]; + +const PAID_OPTIONS: SmartSelectOption[] = [ + { code: "유상", label: "유상" }, + { code: "무상", label: "무상" }, +]; + const EMPTY_FILTER: ProgressListFilter = { Year: "", project_nos: "", category_cd: "", customer_objid: "", product: "", contract_start_date: "", contract_end_date: "", @@ -143,14 +153,12 @@ export default function ProjectProgressPage() {
{/* 1행 */} - + onValueChange={(v) => setFilter({ ...filter, Year: v })} + placeholder="전체" + /> - + onValueChange={(v) => setFilter({ ...filter, area_cd: v })} + placeholder="전체" + /> - + onValueChange={(v) => setFilter({ ...filter, free_of_charge: v })} + placeholder="전체" + /> fetchList()} + * onReset={() => handleReset()} + * totalText={`총 ${total}건`} + * > + * + * + * + * + * + * + * + * + * 원칙: + * - 모든 RPS 메뉴의 검색 폼은 이 컴포넌트를 사용. 자체 검색 폼 구성 금지. + * - SmartSelect / CustomerSelect / CommCodeSelect / Input 모두 h-7 + text-xs 자동 적용. + */ + +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Search, Loader2, RotateCcw } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface CompactFilterBarProps { + children: React.ReactNode; + onSearch?: () => void; + onReset?: () => void; + /** 우측에 표시할 합계/통계 텍스트 (예: "총 12,345건 · 합계 12,000,000원") */ + totalText?: React.ReactNode; + loading?: boolean; + searchLabel?: string; + resetLabel?: string; + className?: string; +} + +export function CompactFilterBar({ + children, + onSearch, + onReset, + totalText, + loading, + searchLabel = "검색", + resetLabel = "초기화", + className, +}: CompactFilterBarProps) { + return ( +
+ {children} + {(onReset || onSearch) && ( +
+ {onReset && ( + + )} + {onSearch && ( + + )} +
+ )} + {totalText != null && ( + {totalText} + )} +
+ ); +} + +interface CompactFilterFieldProps { + label: string; + /** 컨트롤 박스 폭(px). 기본 120. */ + width?: number; + /** 폭 자동 (자식이 100% 폭을 차지하지 않게 할 때 유용) */ + flex?: boolean; + children: React.ReactNode; + className?: string; +} + +export function CompactFilterField({ + label, width = 120, flex, children, className, +}: CompactFilterFieldProps) { + return ( +
+ +
+ {children} +
+
+ ); +} + +/** + * 날짜 범위 입력 (CompactFilterField 자식으로 사용). + * + * + * + * + */ +export function CompactDateRange({ + from, setFrom, to, setTo, disabled, +}: { + from: string; + setFrom: (v: string) => void; + to: string; + setTo: (v: string) => void; + disabled?: boolean; +}) { + return ( +
+ setFrom(e.target.value)} + disabled={disabled} + /> + ~ + setTo(e.target.value)} + disabled={disabled} + /> +
+ ); +} diff --git a/frontend/components/development/BomReportExcelImportDialog.tsx b/frontend/components/development/BomReportExcelImportDialog.tsx index fae60795..7b6607e9 100644 --- a/frontend/components/development/BomReportExcelImportDialog.tsx +++ b/frontend/components/development/BomReportExcelImportDialog.tsx @@ -20,6 +20,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { CommCodeSelect } from "@/components/common/CommCodeSelect"; +import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect"; import { Download, Upload, Save, Loader2, FileX, Copy } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; @@ -261,18 +262,17 @@ export function BomReportExcelImportDialog({ open, onOpenChange, editObjid, init
- +
+ ((o) => ({ + code: o.objid, + label: `${o.part_no} / ${o.part_name}${o.revision ? ` (v${o.revision})` : ""}${o.regdate ? ` - ${o.regdate}` : ""}`, + }))} + value={copySelect} + onValueChange={setCopySelect} + placeholder="선택" + /> +