5.7 KiB
5.7 KiB
11. 페이지 패턴 가이드
대상: 프론트엔드 개발자 최종 업데이트: 2026-03-20
목차
| 번호 | 항목 |
|---|---|
| 11.1 | 리스트 페이지 |
| 11.2 | 상세/폼 페이지 |
| 11.3 | 검색 모달 |
| 11.4 | 공통 기능은 공통 컴포넌트에서 |
11.1 리스트 페이지
UniversalListPage (권장)
대부분의 리스트 페이지는 UniversalListPage로 구성합니다.
const config: UniversalListConfig<MyItem> = {
// 검색 (클라이언트 사이드 필터링)
clientSideFiltering: true,
searchFilter: (item, searchValue) => {
const q = searchValue.toLowerCase();
return item.name.toLowerCase().includes(q) || item.code.toLowerCase().includes(q);
},
searchPlaceholder: '이름, 코드 검색...',
// 컬럼 정의
columns: [...],
// 모바일 카드
renderMobileCard: (item) => <MobileCard item={item} />,
};
IntegratedListTemplateV2 필수 적용 항목
IntegratedListTemplateV2 사용 시 다음 10가지를 반드시 확인:
| # | 항목 | 필수 |
|---|---|---|
| 1 | useColumnSettings + ColumnSettingsPopover |
O |
| 2 | searchValue + onSearchChange (or clientSideFiltering) |
O |
| 3 | 체크박스 Set<string> 패턴 |
O |
| 4 | 페이지네이션 | O |
| 5 | renderMobileCard |
O |
| 6 | 테이블 행 클릭 -> 상세 이동 | O |
| 7 | 헤더 레이아웃 (StatCards, 필터) | O |
| 8 | tableHeaderActions (테이블 내 필터) |
선택 |
| 9 | filterConfig (필터 설정) |
선택 |
| 10 | 탭 구성 | 선택 |
검색 패턴 (필수)
// ✅ 올바른 패턴 -- UniversalListPage + clientSideFiltering
const config: UniversalListConfig<MyItem> = {
clientSideFiltering: true,
searchFilter: (item, searchValue) => {
const q = searchValue.toLowerCase();
return item.name.toLowerCase().includes(q);
},
};
// 데이터를 한 번 API로 로드 -> 검색은 메모리에서 즉시 필터링 -> 깜빡임 없음
// ❌ 금지 패턴 -- IntegratedListTemplateV2에 onSearchChange 직접 연결
<IntegratedListTemplateV2
searchValue={searchTerm}
onSearchChange={(q) => { setSearchTerm(q); }}
/>
// 키입력마다 state 변경 -> re-render -> 화면 깜빡임, 한글 조합 불가
11.2 상세/폼 페이지
라우팅 규칙
- 별도
/new경로 금지 ->?mode=new쿼리파라미터 사용 - 별도
/edit경로 금지 ->?mode=edit쿼리파라미터 사용
// ✅ 올바른 패턴
router.push('/some-page?mode=new');
router.push('/some-page/123?mode=edit');
// ❌ 금지 패턴
router.push('/some-page/new');
router.push('/some-page/123/edit');
페이지 구조
export default function SomePage() {
const searchParams = useSearchParams();
const mode = searchParams.get('mode');
if (mode === 'new') return <SomeForm />;
return <SomeList />;
}
헤더 표준
| 위치 | 요소 |
|---|---|
| 상단 좌측 | 페이지 제목 (<h1>) |
| 상단 우측 | <- 목록으로 링크 |
하단 Sticky 액션 바 (필수)
<div className="sticky bottom-0 bg-white border-t shadow-sm">
<div className="px-3 py-3 md:px-6 md:py-4 flex items-center justify-between">
<Button variant="outline" onClick={() => router.push(listPath)}>
<X className="h-4 w-4 mr-1" /> 취소
</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
<Save className="h-4 w-4 mr-1" /> 저장
</Button>
</div>
</div>
| 모드 | 좌측 | 우측 |
|---|---|---|
| 등록 (new) | 취소 | 저장 |
| 상세 (view) | 취소 (목록으로) | 수정 |
| 수정 (edit) | 취소 | 저장 |
폼 레이아웃
- Card 내부에 버튼 넣지 않음 -> sticky 하단 바 사용
- 아이콘 포함: 취소(
X), 저장(Save), 수정(Pencil)
11.3 검색 모달
SearchableSelectionModal (필수)
검색+선택 기능이 필요한 모달은 반드시 SearchableSelectionModal<T>을 사용합니다.
// ✅ 올바른 패턴
<SearchableSelectionModal<VendorItem>
open={open}
onOpenChange={setOpen}
title="거래처 선택"
fetchData={async () => {
const result = await getVendors();
return result.success ? result.data : [];
}}
columns={[
{ key: 'vendorName', label: '거래처명' },
{ key: 'businessNumber', label: '사업자번호' },
]}
onSelect={(vendor) => {
setSelectedVendor(vendor);
}}
searchFilter={(item, query) =>
item.vendorName.includes(query) || item.businessNumber?.includes(query)
}
/>
// ❌ 금지 패턴 -- Dialog + Table 직접 조합
<Dialog>
<Input onChange={...} />
<Table>...</Table>
</Dialog>
11.4 공통 기능은 공통 컴포넌트에서
리스트 페이지 전체에 적용해야 하는 기능은 개별 페이지 수정 금지. 반드시 공통 레이어에서 처리.
| 기능 | 수정 위치 | 개별 페이지 수정 |
|---|---|---|
| 검색 상태 보존 | UniversalListPage |
금지 |
| 검색 X(클리어) 버튼 | SearchFilter + IntegratedListTemplateV2 |
금지 |
| 검색 디바운스 | UniversalListPage 내부 300ms |
금지 |
| 체크박스 선택 | IntegratedListTemplateV2 |
금지 |
| 페이지네이션 | IntegratedListTemplateV2 |
금지 |
| 모바일 카드/인피니티 | IntegratedListTemplateV2 |
금지 |
| 컬럼 설정 | useColumnSettings + ColumnSettingsPopover |
금지 |
원칙: "26개 페이지에 하나씩 적용" -> 잘못된 접근. "공통 1곳 수정 -> 전체 자동 적용" -> 올바른 접근.