docs: [plans] 품목관리 검색 상태 보존 UX 개선 React 요청서 추가

This commit is contained in:
김보곤
2026-03-19 11:16:51 +09:00
parent 0b92442587
commit fb3dcb3e9b
2 changed files with 224 additions and 0 deletions

View File

@@ -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<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)
```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<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 업데이트
```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