Files
sam-docs/plans/item-list-search-state-preservation.md

7.5 KiB

품목관리 검색 상태 보존 — React UX 개선 요청서

작성일: 2026-03-19 요청자: R&D실 대상: React 프론트엔드 개발자 우선순위: 중간 (UX 개선) 영향 범위: React만 수정 (API 변경 없음)


1. 문제 설명

1.1 현상

품목관리 목록(/production/screen-production 또는 /items)에서:

  1. 검색어 "실리카" 입력 → 4건 필터링
  2. 품목 클릭 → 상세 페이지 이동
  3. 수정 후 저장 → 목록으로 복귀
  4. 검색어가 초기화되어 전체 1,175건이 다시 표시됨
[목록] 검색: "실리카" (4건)
    ↓ 클릭
[상세] 품목 조회
    ↓ 수정 → 저장
[목록] 검색: "" (초기화됨, 1,175건) ← 문제!

1.2 기대 동작

수정/조회 후 목록으로 돌아오면 이전 검색어와 탭 필터가 그대로 유지되어야 한다.


2. 원인 분석

2.1 근본 원인: 검색 상태가 컴포넌트 state에만 존재

ItemListClient.tsx:76에서 검색어가 useState('')로 초기화된다. URL에 반영되지 않으므로 페이지 이동 후 복귀하면 상태가 소멸된다.

// ItemListClient.tsx:76 — 항상 빈 문자열로 시작
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState<string>('all');

2.2 직접 원인: 목록 복귀 시 router.push 사용

수정/등록 완료 후 검색 파라미터 없이 하드코딩된 경로로 이동한다:

파일 라인 현재 코드
DynamicItemForm/index.tsx 408 router.push('/production/screen-production')
ItemDetailEdit.tsx 353, 368 router.push('/production/screen-production')
ItemForm/index.tsx 178 router.push('/production/screen-production')

참고: ItemDetailClient.tsx:568의 "목록으로" 버튼은 이미 router.back()을 사용 중 (정상).


3. 수정 방안

2단계 조합으로 해결한다:

3.1 Step 1: 목록 복귀 시 router.back() 사용

수정/등록 완료 후 router.push(...) 대신 router.back()을 사용한다. 브라우저 히스토리의 이전 페이지로 복귀하므로 검색 상태가 자연스럽게 보존된다.

수정 대상 3개 파일:

(1) src/components/items/DynamicItemForm/index.tsx (라인 408)

// Before
router.push('/production/screen-production');
router.refresh();

// After
router.back();

(2) src/components/items/ItemDetailEdit.tsx (라인 353, 368)

// Before (2곳 모두)
onClick={() => router.push('/production/screen-production')}

// After (2곳 모두)
onClick={() => router.back()}

(3) src/components/items/ItemForm/index.tsx (라인 178)

// Before
router.push('/production/screen-production');
router.refresh();

// After
router.back();

3.2 Step 2: URL searchParams 동기화 (선택사항 — 권장)

Step 1만으로도 기본 동작은 해결된다. Step 2는 새로고침/URL 공유 시에도 검색 상태를 보존하기 위한 추가 개선이다.

ItemListClient.tsx에서 검색어와 탭 필터를 URL 쿼리 파라미터에 동기화한다.

변경 1: URL에서 초기값 읽기

// Before (ItemListClient.tsx:76)
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState<string>('all');

// After
import { useSearchParams } from 'next/navigation';

const searchParams = useSearchParams();
const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || '');
const [selectedType, setSelectedType] = useState<string>(searchParams.get('type') || 'all');

변경 2: 검색/탭 변경 시 URL 업데이트

// ItemListClient.tsx — 새로 추가할 useEffect
import { useRouter, useSearchParams, usePathname } from 'next/navigation';

const pathname = usePathname();

useEffect(() => {
  const params = new URLSearchParams();
  if (debouncedSearchTerm) params.set('search', debouncedSearchTerm);
  if (selectedType !== 'all') params.set('type', selectedType);

  const queryString = params.toString();
  const newUrl = queryString ? `${pathname}?${queryString}` : pathname;

  // 히스토리 교체 (뒤로가기 스택 오염 방지)
  window.history.replaceState(null, '', newUrl);
}, [debouncedSearchTerm, selectedType, pathname]);

변경 3: handleView에서 검색 파라미터 전달 (선택)

// ItemListClient.tsx:133 — 상세 페이지 이동 시 returnUrl 전달
const handleView = (itemCode: string, itemType: string, itemId: string) => {
  const returnParams = new URLSearchParams();
  if (searchTerm) returnParams.set('search', searchTerm);
  if (selectedType !== 'all') returnParams.set('type', selectedType);

  router.push(
    `/production/screen-production/${encodeURIComponent(itemCode)}?mode=view&type=${itemType}&id=${itemId}`
  );
};

Step 2를 적용하면 URL이 /items?search=실리카&type=all 형태가 되어 새로고침/공유에도 상태가 유지된다.


4. 수정 파일 요약

파일 수정 내용 필수/선택
src/components/items/DynamicItemForm/index.tsx router.push(...)router.back() 필수
src/components/items/ItemDetailEdit.tsx router.push(...)router.back() (2곳) 필수
src/components/items/ItemForm/index.tsx router.push(...)router.back() 필수
src/components/items/ItemListClient.tsx URL searchParams 동기화 권장

5. 주의사항

5.1 router.back() 사용 시 고려점

  • 사용자가 URL 직접 입력으로 수정 페이지에 진입한 경우, router.back()은 품목 목록이 아닌 이전 페이지(브라우저 탭의 히스토리)로 이동할 수 있다
  • 이 경우를 대비하려면 router.back() 대신 referrer 체크 후 fallback 패턴을 사용할 수 있다:
// 안전한 뒤로가기 패턴 (선택)
const handleGoBack = () => {
  if (window.history.length > 1) {
    router.back();
  } else {
    router.push('/production/screen-production');
  }
};

5.2 router.refresh() 제거 여부

DynamicItemForm/index.tsx:409ItemForm/index.tsx:179에서 router.refresh()를 호출 중이다. router.back()으로 전환하면 router.refresh()는 불필요하다 (이전 페이지의 캐시된 상태로 복귀하므로). 단, 저장 후 목록 데이터 갱신이 필요하면 ItemListClient의 데이터 fetching이 마운트 시 자동 실행되는지 확인 필요.

5.3 UniversalListPageexternalSearch prop

UniversalListPage에 이미 externalSearch prop이 존재한다. Step 2 구현 시 이 prop을 활용하면 UniversalListPage 내부의 검색 input 값도 URL과 동기화할 수 있다.


6. 테스트 체크리스트

  • 검색어 입력 후 품목 클릭 → 상세 조회 → "목록으로" → 검색어 유지 확인
  • 검색어 입력 후 품목 클릭 → 수정 → 저장 → 검색어 유지 확인
  • 검색어 입력 후 품목 등록(?mode=new) → 저장 → 검색어 유지 확인
  • 탭 필터(완제품/부품 등) 선택 후 상세 → 복귀 → 탭 유지 확인
  • (Step 2) 검색 후 페이지 새로고침 → 검색어 유지 확인
  • (Step 2) 검색 상태 URL 복사 → 새 탭에서 열기 → 동일 결과 확인
  • 에러 상태에서 "품목 목록으로 돌아가기" 클릭 → 목록 정상 표시 확인

관련 문서

  • 품목 정책: rules/item-policy.md

최종 업데이트: 2026-03-19