feat: [common] 검색 X 클리어 버튼 + 검색 상태 sessionStorage 보존
- SearchFilter, IntegratedListTemplateV2: 검색 입력 시 X(클리어) 버튼 표시 - useListSearchState 훅 신규: URL + sessionStorage 이중 저장으로 상세→목록 복귀 시 검색 유지 - UniversalListPage: useListSearchState 연동
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search } from "lucide-react";
|
||||
import { Search, XCircle } from "lucide-react";
|
||||
import { ReactNode, useState, useEffect } from "react";
|
||||
|
||||
interface SearchFilterProps {
|
||||
@@ -45,8 +45,17 @@ export function SearchFilter({
|
||||
placeholder={isMobile ? "내용을 검색해주세요." : searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10 text-[14px]"
|
||||
className="pl-10 pr-8 text-[14px]"
|
||||
/>
|
||||
{searchValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSearchChange('')}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/60 hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{extraActions && (
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:flex-wrap">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, Fragment, useState, useEffect, useRef, useCallback, Children, isValidElement, cloneElement } from "react";
|
||||
import { LucideIcon, Trash2, Plus, Loader2, ArrowUpDown, ArrowUp, ArrowDown, Search } from "lucide-react";
|
||||
import { LucideIcon, Trash2, Plus, Loader2, ArrowUpDown, ArrowUp, ArrowDown, Search, XCircle } from "lucide-react";
|
||||
import { DateRangeSelector } from "@/components/molecules/DateRangeSelector";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
@@ -691,8 +691,17 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue || ''}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-9 w-full bg-gray-50 border-gray-200"
|
||||
className="pl-9 pr-8 w-full bg-gray-50 border-gray-200"
|
||||
/>
|
||||
{searchValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSearchChange('')}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/60 hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 기존 extraActions (추가 버튼 등) */}
|
||||
@@ -710,8 +719,17 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue || ''}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-9 w-full bg-gray-50 border-gray-200"
|
||||
className="pl-9 pr-8 w-full bg-gray-50 border-gray-200"
|
||||
/>
|
||||
{searchValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSearchChange('')}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground/60 hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { useColumnSettings } from '@/hooks/useColumnSettings';
|
||||
import { useListSearchState } from '@/hooks/useListSearchState';
|
||||
import { ColumnSettingsPopover } from '@/components/molecules/ColumnSettingsPopover';
|
||||
import { toast } from 'sonner';
|
||||
import { Download, Loader2 } from 'lucide-react';
|
||||
@@ -48,14 +49,21 @@ export function UniversalListPage<T>({
|
||||
const locale = (params.locale as string) || 'ko';
|
||||
const { canCreate: permCanCreate, canDelete: permCanDelete, canExport } = usePermission();
|
||||
|
||||
// ===== 검색 상태 보존 (sessionStorage + URL) =====
|
||||
const {
|
||||
getValue: getSearchStateValue,
|
||||
setValue: setSearchStateValue,
|
||||
} = useListSearchState({ debounceMs: 500 });
|
||||
const restoredSearch = getSearchStateValue('search');
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
// 원본 데이터 (클라이언트 사이드 필터링용)
|
||||
const [rawData, setRawData] = useState<T[]>(initialData || []);
|
||||
// UI 상태
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(!initialData);
|
||||
const [searchValue, setSearchValue] = useState(''); // UI 입력용 (즉시 반영)
|
||||
const [debouncedSearchValue, setDebouncedSearchValue] = useState(''); // API 호출용 (debounced)
|
||||
const [searchValue, setSearchValue] = useState(restoredSearch); // UI 입력용 (즉시 반영, sessionStorage에서 복원)
|
||||
const [debouncedSearchValue, setDebouncedSearchValue] = useState(restoredSearch); // API 호출용 (debounced)
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
// 탭이 없으면 IntegratedListTemplateV2의 기본값 'default'와 일치해야 함
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
@@ -536,7 +544,7 @@ export function UniversalListPage<T>({
|
||||
// ===== 검색 핸들러 =====
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value); // UI 즉시 반영
|
||||
// 페이지 초기화와 외부 콜백은 debounce 후 실행됨 (아래 useEffect에서 처리)
|
||||
setSearchStateValue('search', value); // sessionStorage + URL 동기화
|
||||
}, []);
|
||||
|
||||
// 외부 콜백에 debounced 값 전달 (자체 loadData 관리하는 컴포넌트용)
|
||||
|
||||
Reference in New Issue
Block a user