공용 — 검색·초기화 버튼을 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:
hjjeong
2026-05-13 17:17:24 +09:00
parent 16ae6088d4
commit a136867f52
14 changed files with 102 additions and 104 deletions
@@ -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>
)}
+38 -3
View File
@@ -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>
);
}