From fb3dcb3e9bd8a72f58f3256d3e99fda7d3da27bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 19 Mar 2026 11:16:51 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[plans]=20=ED=92=88=EB=AA=A9=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=80=EC=83=89=20=EC=83=81=ED=83=9C=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B4=20UX=20=EA=B0=9C=EC=84=A0=20React=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INDEX.md | 1 + plans/item-list-search-state-preservation.md | 223 +++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 plans/item-list-search-state-preservation.md diff --git a/INDEX.md b/INDEX.md index af25056..c3040ac 100644 --- a/INDEX.md +++ b/INDEX.md @@ -328,6 +328,7 @@ DB 도메인별: | [usage-subscription-unification.md](plans/usage-subscription-unification.md) | 이용현황+구독관리 통합 계획 (AI 토큰 사용량, 저장공간, 메뉴 통합) | | [usage-react-request.md](plans/usage-react-request.md) | 이용현황(구독관리 통합) React 구현 요청서 (API 완료, 타입/컴포넌트/와이어프레임 포함) | | [bom-tree-3level-react-request.md](plans/bom-tree-3level-react-request.md) | BOM 트리 3단계 그룹 표시 React 구현 요청 (API 완료, 카테고리 접힘/펼침) | +| [item-list-search-state-preservation.md](plans/item-list-search-state-preservation.md) | 품목관리 검색 상태 보존 UX 개선 요청 (router.back + URL searchParams 동기화) | ### frontend/integration/ — 프론트엔드 개발 가이드 diff --git a/plans/item-list-search-state-preservation.md b/plans/item-list-search-state-preservation.md new file mode 100644 index 0000000..2ebceee --- /dev/null +++ b/plans/item-list-search-state-preservation.md @@ -0,0 +1,223 @@ +# 품목관리 검색 상태 보존 — 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에 반영되지 않으므로 페이지 이동 후 복귀하면 상태가 소멸된다. + +```typescript +// ItemListClient.tsx:76 — 항상 빈 문자열로 시작 +const [searchTerm, setSearchTerm] = useState(''); +const [selectedType, setSelectedType] = useState('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) + +```typescript +// Before +router.push('/production/screen-production'); +router.refresh(); + +// After +router.back(); +``` + +#### (2) `src/components/items/ItemDetailEdit.tsx` (라인 353, 368) + +```typescript +// Before (2곳 모두) +onClick={() => router.push('/production/screen-production')} + +// After (2곳 모두) +onClick={() => router.back()} +``` + +#### (3) `src/components/items/ItemForm/index.tsx` (라인 178) + +```typescript +// 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에서 초기값 읽기 + +```typescript +// Before (ItemListClient.tsx:76) +const [searchTerm, setSearchTerm] = useState(''); +const [selectedType, setSelectedType] = useState('all'); + +// After +import { useSearchParams } from 'next/navigation'; + +const searchParams = useSearchParams(); +const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || ''); +const [selectedType, setSelectedType] = useState(searchParams.get('type') || 'all'); +``` + +#### 변경 2: 검색/탭 변경 시 URL 업데이트 + +```typescript +// 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에서 검색 파라미터 전달 (선택) + +```typescript +// 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** 패턴을 사용할 수 있다: + +```typescript +// 안전한 뒤로가기 패턴 (선택) +const handleGoBack = () => { + if (window.history.length > 1) { + router.back(); + } else { + router.push('/production/screen-production'); + } +}; +``` + +### 5.2 `router.refresh()` 제거 여부 + +`DynamicItemForm/index.tsx:409`와 `ItemForm/index.tsx:179`에서 `router.refresh()`를 호출 중이다. `router.back()`으로 전환하면 `router.refresh()`는 불필요하다 (이전 페이지의 캐시된 상태로 복귀하므로). 단, 저장 후 목록 데이터 갱신이 필요하면 `ItemListClient`의 데이터 fetching이 마운트 시 자동 실행되는지 확인 필요. + +### 5.3 `UniversalListPage`의 `externalSearch` 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