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="선택"
+ />
+