diff --git a/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx b/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx index 405f5d0a..c175b976 100644 --- a/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx +++ b/frontend/app/(main)/COMPANY_16/development/change-list/page.tsx @@ -13,6 +13,7 @@ import { Search, Loader2, RotateCcw } from "lucide-react"; import { toast } from "sonner"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { CommCodeSelect } from "@/components/common/CommCodeSelect"; +import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect"; import { devEoHistoryApi, EoHistoryListFilter, EoHistoryRow } from "@/lib/api/devEoHistory"; import { PartHisDetailDialog } from "@/components/development/PartHisDetailDialog"; @@ -21,10 +22,10 @@ const GROUP_PART_TYPE = "0000062"; // change_type/change_option은 wace 운영판 그룹 ID가 명확하지 않으므로 text input으로 우선 운영. // (시드 후 그룹 ID 확인되면 SmartSelect 전환) -const YEAR_OPTIONS = (() => { +const YEAR_OPTIONS: SmartSelectOption[] = (() => { const cur = new Date().getFullYear(); - const arr: string[] = []; - for (let y = cur + 4; y >= cur - 8; y--) arr.push(String(y)); + const arr: SmartSelectOption[] = []; + for (let y = cur + 4; y >= cur - 8; y--) arr.push({ code: String(y), label: String(y) }); return arr; })(); @@ -95,12 +96,12 @@ export default function EoHistoryPage() {
- + 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="선택" + /> +