공용 — 검색·초기화 버튼을 PageHeader 우측 액션 영역으로 이전
사용자 보고: "초기화, 검색 버튼은 상단의 메뉴이름 쪽에 다른 버튼들이랑 같이 있으면 될거같아"
CompactFilterBar 안에 있던 [초기화][검색] 버튼이 자리 차지 + 시선 분산.
PageHeader 의 actions 슬롯 옆으로 통합하면서 11개 페이지 일괄 적용.
PageHeader 확장:
- onSearch / onReset / loading / searchLabel / resetLabel prop 추가
- actions 뒤에 [초기화][검색] 버튼 자동 렌더 (h-8 / text-xs)
CompactFilterBar 단순화:
- onSearch / onReset / loading / searchLabel / resetLabel prop 제거
- children + totalText 만 유지 (필드 컨테이너 + 합계 텍스트)
11개 페이지: <CompactFilterBar onSearch onReset loading> 3 prop 을
<PageHeader onSearch onReset loading> 로 이동
메모리: feedback_compact_search_pattern.md 에 "검색·초기화 위치 = PageHeader" 박제
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,14 +92,13 @@ export default function EoHistoryPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-2">
|
||||
<PageHeader />
|
||||
|
||||
<CompactFilterBar
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
totalText={<>총 {total.toLocaleString()}건 (read-only)</>}
|
||||
>
|
||||
/>
|
||||
|
||||
<CompactFilterBar totalText={<>총 {total.toLocaleString()}건 (read-only)</>}>
|
||||
<CompactFilterField label="년도" width={100}>
|
||||
<SmartSelect
|
||||
options={YEAR_OPTIONS}
|
||||
|
||||
@@ -115,7 +115,11 @@ export default function EbomRegistPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-2">
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" onClick={() => setExcelOpen(true)}
|
||||
className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs">
|
||||
@@ -132,12 +136,7 @@ export default function EbomRegistPage() {
|
||||
</>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
totalText={<>총 {total.toLocaleString()}건</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {total.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="제품구분" width={160}>
|
||||
<CommCodeSelect
|
||||
groupId={PRODUCT_GROUP}
|
||||
|
||||
@@ -185,7 +185,9 @@ export default function EbomSearchPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-2">
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
onReset={() => { setFilter(EMPTY_FILTER); setRows([]); setMaxLevel(0); }}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" onClick={() => runQuery("ascending")} disabled={loading}
|
||||
variant={direction === "ascending" ? "default" : "secondary"}
|
||||
@@ -217,8 +219,6 @@ export default function EbomSearchPage() {
|
||||
{/* 운영판 wace structureAscendingList.jsp 1:1 — 노출 검색 필드 3개
|
||||
(고객사/프로젝트번호/유닛명 은 운영판에서도 주석 처리되어 노출 안 됨) */}
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); setRows([]); setMaxLevel(0); }}
|
||||
totalText={<>모드: {direction === "ascending" ? "정전개 (루트 → 리프)" : "역전개 (리프 → 부모)"} · {rows.length.toLocaleString()}행 · MAX_LEVEL = {maxLevel}</>}
|
||||
>
|
||||
<CompactFilterField label="품번" width={200}>
|
||||
|
||||
@@ -156,7 +156,11 @@ export default function PartRegistPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-2">
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" className="h-8 gap-1 text-xs" onClick={handleCreate}>
|
||||
<Plus className="h-3.5 w-3.5" />등록
|
||||
@@ -184,12 +188,7 @@ export default function PartRegistPage() {
|
||||
</>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
totalText={<>총 {total.toLocaleString()}건 (M1: status ≠ 'release')</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {total.toLocaleString()}건 (M1: status ≠ 'release')</>}>
|
||||
<CompactFilterField label="품번" width={220}>
|
||||
<DevPartSelect mode="partNo"
|
||||
value={filter.search_part_no ?? ""}
|
||||
|
||||
@@ -125,7 +125,11 @@ export default function PartSearchPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-2">
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" className="h-8 gap-1 text-xs" onClick={handleCreate}>
|
||||
<Plus className="h-3.5 w-3.5" />등록
|
||||
@@ -146,12 +150,7 @@ export default function PartSearchPage() {
|
||||
</>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={() => fetchList()}
|
||||
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
|
||||
totalText={<>총 {total.toLocaleString()}건 (M2: status = 'release')</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {total.toLocaleString()}건 (M2: status = 'release')</>}>
|
||||
<CompactFilterField label="품번" width={220}>
|
||||
<DevPartSelect mode="partNo"
|
||||
value={filter.search_part_no ?? ""}
|
||||
|
||||
@@ -141,11 +141,12 @@ export default function MbomMgmtPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-2">
|
||||
<PageHeader />
|
||||
<CompactFilterBar
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={handleSearch}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
<CompactFilterBar
|
||||
totalText={<>총 {total.toLocaleString()}건 · PROJECT_MGMT × CONTRACT_ITEM 펼침</>}
|
||||
>
|
||||
<CompactFilterField label="주문유형" width={130}>
|
||||
|
||||
@@ -147,14 +147,13 @@ export default function ProjectProgressPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2 p-2">
|
||||
<PageHeader />
|
||||
|
||||
<CompactFilterBar
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
totalText={<>총 {rows.length.toLocaleString()}건</>}
|
||||
>
|
||||
/>
|
||||
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="년도" width={100}>
|
||||
<SmartSelect
|
||||
options={YEAR_OPTIONS}
|
||||
|
||||
@@ -112,7 +112,11 @@ export default function WbsTemplatePage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2 p-2">
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={handleSearch}
|
||||
onReset={handleReset}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" className="h-8 gap-1 text-xs" onClick={handleRegist}>
|
||||
<Plus className="h-3.5 w-3.5" />등록
|
||||
@@ -123,12 +127,7 @@ export default function WbsTemplatePage() {
|
||||
</>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={handleSearch}
|
||||
onReset={handleReset}
|
||||
totalText={<>총 {rows.length.toLocaleString()}건</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="제품구분" width={200}>
|
||||
<CommCodeSelect
|
||||
groupId={PRODUCT_GROUP}
|
||||
|
||||
@@ -494,7 +494,11 @@ export default function SalesEstimatePage() {
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
{ConfirmDialogComponent}
|
||||
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete} disabled={!selected}>
|
||||
<Trash2 className="h-3.5 w-3.5" />삭제
|
||||
@@ -517,9 +521,6 @@ export default function SalesEstimatePage() {
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
totalText={<>총 {rows.length.toLocaleString()}건</>}
|
||||
>
|
||||
<CompactFilterField label="주문유형" width={130}>
|
||||
|
||||
@@ -537,7 +537,11 @@ export default function SalesOrderPage() {
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
{ConfirmDialogComponent}
|
||||
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" className="h-8 gap-1 text-xs" onClick={() => { if (selected) openEdit(); else openCreate(); }}>
|
||||
{selected ? <Pencil className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
|
||||
@@ -561,12 +565,7 @@ export default function SalesOrderPage() {
|
||||
</>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
totalText={<>총 {rows.length.toLocaleString()}건</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건</>}>
|
||||
<CompactFilterField label="주문유형" width={130}>
|
||||
<CommCodeSelect groupId="0000167"
|
||||
value={searchForm.category_cd}
|
||||
|
||||
@@ -191,7 +191,11 @@ export default function SalesRevenuePage() {
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
{ConfirmDialogComponent}
|
||||
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" className="h-8 gap-1 bg-blue-600 hover:bg-blue-700 text-white text-xs" onClick={openDeadline} disabled={!selected}>
|
||||
<FileCheck2 className="h-3.5 w-3.5" />마감정보입력
|
||||
@@ -202,12 +206,7 @@ export default function SalesRevenuePage() {
|
||||
</>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
totalText={<>총 {rows.length.toLocaleString()}건 (출하/매출 이력)</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건 (출하/매출 이력)</>}>
|
||||
<CompactFilterField label="주문유형" width={130}>
|
||||
<CommCodeSelect groupId="0000167"
|
||||
value={searchForm.orderType}
|
||||
|
||||
@@ -185,18 +185,17 @@ export default function SalesSalePage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
<PageHeader actions={
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
actions={
|
||||
<Button size="sm" className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs" onClick={openRegister} disabled={!selected}>
|
||||
<Truck className="h-3.5 w-3.5" />출하지시/판매등록
|
||||
</Button>
|
||||
} />
|
||||
|
||||
<CompactFilterBar
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
onReset={handleReset}
|
||||
totalText={<>총 {rows.length.toLocaleString()}건 (라인 단위)</>}
|
||||
>
|
||||
<CompactFilterBar totalText={<>총 {rows.length.toLocaleString()}건 (라인 단위)</>}>
|
||||
<CompactFilterField label="주문유형" width={130}>
|
||||
<CommCodeSelect groupId="0000167"
|
||||
value={searchForm.orderType}
|
||||
|
||||
@@ -28,33 +28,19 @@
|
||||
*/
|
||||
|
||||
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) {
|
||||
export function CompactFilterBar({ children, totalText, className }: CompactFilterBarProps) {
|
||||
// 검색/초기화 버튼은 PageHeader 의 우측 액션 영역으로 통합.
|
||||
// CompactFilterBar 는 필드 컨테이너 + 합계 텍스트만 담당.
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -63,22 +49,6 @@ export function CompactFilterBar({
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{(onReset || onSearch) && (
|
||||
<div className="flex items-end gap-1">
|
||||
{onReset && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 px-2 text-xs" onClick={onReset}>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
<span>{resetLabel}</span>
|
||||
</Button>
|
||||
)}
|
||||
{onSearch && (
|
||||
<Button size="sm" className="h-7 gap-1 px-2 text-xs" onClick={onSearch} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Search className="h-3 w-3" />}
|
||||
<span>{searchLabel}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{totalText != null && (
|
||||
<span className="ml-auto text-[11px] text-muted-foreground">{totalText}</span>
|
||||
)}
|
||||
|
||||
@@ -23,12 +23,23 @@ import { usePathname } from "next/navigation";
|
||||
import { useMenu } from "@/contexts/MenuContext";
|
||||
import { useTabStore, selectTabs, selectActiveTabId } from "@/stores/tabStore";
|
||||
import type { MenuItem } from "@/lib/api/menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, Loader2, RotateCcw } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PageHeaderProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
/** 업무 액션 슬롯 (등록/삭제/상신 등). 검색·초기화는 onSearch/onReset 로 전달. */
|
||||
actions?: React.ReactNode;
|
||||
/** 검색 핸들러. 지정 시 우측에 검색 버튼 자동 렌더. */
|
||||
onSearch?: () => void;
|
||||
/** 초기화 핸들러. 지정 시 우측에 초기화 버튼 자동 렌더. */
|
||||
onReset?: () => void;
|
||||
/** 검색 중 로딩 표시 */
|
||||
loading?: boolean;
|
||||
searchLabel?: string;
|
||||
resetLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -56,7 +67,10 @@ function findByUrl(menus: MenuItem[], strippedUrl: string): MenuItem | null {
|
||||
return best;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
||||
export function PageHeader({
|
||||
title, description, actions, onSearch, onReset, loading,
|
||||
searchLabel = "검색", resetLabel = "초기화", className,
|
||||
}: PageHeaderProps) {
|
||||
const pathname = usePathname() ?? "";
|
||||
const tabs = useTabStore(selectTabs);
|
||||
const activeTabId = useTabStore(selectActiveTabId);
|
||||
@@ -81,7 +95,8 @@ export function PageHeader({ title, description, actions, className }: PageHeade
|
||||
const resolvedTitle = title ?? menu?.menu_name_kor ?? "";
|
||||
const resolvedDesc = description ?? menu?.menu_desc ?? "";
|
||||
|
||||
if (!resolvedTitle && !resolvedDesc && !actions) return null;
|
||||
const hasSearchButtons = !!(onSearch || onReset);
|
||||
if (!resolvedTitle && !resolvedDesc && !actions && !hasSearchButtons) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-shrink-0 items-end justify-between gap-3 border-b pb-3", className)}>
|
||||
@@ -93,7 +108,27 @@ export function PageHeader({ title, description, actions, className }: PageHeade
|
||||
<p className="text-xs text-muted-foreground">{resolvedDesc}</p>
|
||||
)}
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-1.5">{actions}</div>}
|
||||
{(actions || hasSearchButtons) && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{actions}
|
||||
{hasSearchButtons && (
|
||||
<>
|
||||
{onReset && (
|
||||
<Button size="sm" variant="outline" className="h-8 gap-1 px-2 text-xs" onClick={onReset}>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
{resetLabel}
|
||||
</Button>
|
||||
)}
|
||||
{onSearch && (
|
||||
<Button size="sm" className="h-8 gap-1 px-2 text-xs" onClick={onSearch} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Search className="h-3.5 w-3.5" />}
|
||||
{searchLabel}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user