feat(WEB): UniversalListPage 전체 마이그레이션 및 코드 정리

- UniversalListPage/IntegratedListTemplateV2 컴포넌트 기능 개선
- 회계, HR, 건설, 고객센터, 결재, 설정 등 전체 리스트 컴포넌트 마이그레이션
- 테스트 페이지 및 미사용 API 라우트 정리 (board-test, order-management-test 등)
- 미들웨어 토큰 갱신 로직 개선
- AuthenticatedLayout 구조 개선
- claudedocs 문서 업데이트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-16 15:19:09 +09:00
parent 8639eee5df
commit ad493bcea6
90 changed files with 19864 additions and 20305 deletions

View File

@@ -1,8 +1,94 @@
# UniversalListPage 컴포넌트 통합 작업
> **목표**: 59개 리스트 페이지를 1개의 공통 컴포넌트로 통합
> **목표**: 55개 리스트 페이지를 1개의 공통 컴포넌트로 통합
> **시작일**: 2026-01-14
> **원칙**: 기존 기능 100% 유지, 테이블 영역만 공통화
> **상태**: ✅ 전체 완료 (55/55 페이지, 100%)
---
## 📊 페이지 수 산정 (2026-01-16 확정)
### 최종 페이지 수: 55개
| 항목 | 개수 | 설명 |
|------|------|------|
| UniversalListPage 사용 파일 | 62개 | 전체 import 기준 |
| 템플릿 export 파일 | -1개 | `templates/index.ts` (export only) |
| 중복 파일 쌍 | -6개 | wrapper + client 패턴 |
| **실제 페이지 수** | **55개** | |
### 중복 파일 쌍 목록 (6쌍)
동일한 페이지인데 wrapper(index.tsx)와 client 컴포넌트가 분리된 경우:
| # | 페이지 | 파일 1 (wrapper) | 파일 2 (client) |
|---|--------|------------------|-----------------|
| 1 | 거래처관리(회계) | `VendorManagement/index.tsx` | `VendorManagementClient.tsx` |
| 2 | 어음관리 | `BillManagement/index.tsx` | `BillManagementClient.tsx` |
| 3 | 결제내역 | `PaymentHistoryManagement/index.tsx` | `PaymentHistoryClient.tsx` |
| 4 | 카드관리 | `CardManagement/index.tsx` | `CardManagementUnified.tsx` |
| 5 | 게시판목록 | `BoardList/index.tsx` | `BoardListUnified.tsx` |
| 6 | 발주관리 | `OrderManagementListClient.tsx` | `OrderManagementUnified.tsx` |
### 마이그레이션 제외 페이지
| 파일 | 제외 사유 |
|------|----------|
| `construction/projects/ProjectListClient.tsx` | PageLayout 직접 사용 (IntegratedListTemplateV2 미사용) |
| `settings/PermissionManagement/index.tsx` | IntegratedListTemplateV2 미사용 |
| `customer-center/FAQManagement/FAQList.tsx` | IntegratedListTemplateV2 미사용 |
| `pricing/PricingListClient.tsx` (일반) | IntegratedListTemplateV2 미사용 |
| 영업 도메인 3개 | 별도 구조 사용 (추후 검토) |
> **Note**: 수량이 변동되는 원인은 중복 파일(wrapper/client 패턴)과 제외 대상 파일 때문입니다.
---
## 🎯 핵심 목적 (절대 잊지 말 것!)
**이 공통화 작업의 근본적인 목적은 모바일에서 필터를 바텀시트로 보여주기 위함이다.**
### filterConfig 사용 규칙
- `filterConfig`를 사용하면 **자동으로** PC/모바일 분기 처리됨
- **PC (1280px+)**: 인라인 필터 (테이블 헤더 영역)
- **모바일 (~1279px)**: 바텀시트 필터 (MobileFilter 컴포넌트)
- **새로운 모바일 필터 기능을 만들지 말 것!** 이미 공통화되어 있음
- 정렬, 상태 필터 등 모든 필터는 `filterConfig`로 정의
### 예시
```typescript
// ✅ 올바른 방식 - filterConfig 사용
filterConfig: [
{
key: 'sort',
label: '정렬',
type: 'single',
options: [{ value: 'latest', label: '최신순' }, ...],
},
],
// ❌ 잘못된 방식 - 별도 모바일 필터 구현
mobileTableHeaderActions: ... // 이런 거 만들지 말 것!
```
---
## 🚨 작업 정책 (필독!)
### 본 페이지 직접 작업 원칙
- **테스트 페이지 생성 금지**: `-test` 접미사 페이지 만들지 말 것
- **feature 브랜치 활용**: 이미 `feature/universal-list-component` 브랜치에서 작업 중
- **본 페이지에 바로 적용**: 마이그레이션은 원본 파일에 직접 수행
- **롤백 가능**: 문제 발생 시 `git checkout` 또는 브랜치 전환으로 복구
### ❌ 삭제된 테스트 페이지 (2026-01-14)
| 삭제된 테스트 페이지 | 본 페이지 |
|---------------------|----------|
| `/board-test/` | `/board/` |
| `/construction/order/order-management-test/` | `/construction/order/order-management/` |
| `/hr/card-management-test/` | `/hr/card-management/` |
| `/customer-center/notices-test/` | `/customer-center/notices/` |
---
@@ -36,12 +122,271 @@
---
## Phase 2.5: 공통 옵션화 리팩토링 ✅ 완료
> **목적**: headerActions의 달력/버튼을 config 옵션으로 통합하여 위치/스타일 공통 관리
### 2.5.1 DateRangeSelector 옵션화
- [x] `UniversalListConfig``dateRangeSelector` 옵션 추가 ✅
- [x] IntegratedListTemplateV2에서 달력 렌더링 위치 통합 ✅
- [x] 기존 페이지 headerActions → config 옵션으로 마이그레이션 ✅
```typescript
// config 옵션 정의
dateRangeSelector?: {
enabled: boolean;
showPresets?: boolean; // 당월, 전월, 오늘 등 프리셋 버튼
startDate?: string;
endDate?: string;
onStartDateChange?: (date: string) => void;
onEndDateChange?: (date: string) => void;
};
```
### 2.5.2 등록 버튼 옵션화
- [x] `UniversalListConfig``createButton` 옵션 추가 ✅
- [x] 버튼 위치 오른쪽 끝 고정 (공통) ✅
- [x] 기존 페이지 headerActions → config 옵션으로 마이그레이션 ✅
```typescript
// config 옵션 정의
createButton?: {
label: string; // '등록', '공정 등록' 등
onClick: () => void;
icon?: LucideIcon; // 기본값: Plus
};
```
### 2.5.3 레이아웃 규칙
```
[달력 (왼쪽)] -------------- [등록 버튼 (오른쪽 끝)]
```
### 마이그레이션 완료 파일 (Level 1)
| 파일 | 달력 | 등록버튼 | 상태 |
|-----|------|---------|------|
| InquiryList.tsx | ✅ | ✅ | ✅ 완료 |
| NoticeList.tsx | ✅ | ❌ | ✅ 완료 |
| EventList.tsx | ✅ | ❌ | ✅ 완료 |
| PopupList.tsx | ❌ | ✅ | ✅ 완료 |
> **Note**: 나머지 페이지들은 Level 2+ 마이그레이션 시 적용 예정
---
## Phase 3: 파일럿 마이그레이션
- [ ] 기본 케이스 1개 선정 및 마이그레이션
- [ ] 특이 케이스 1개 선정 및 마이그레이션
- [ ] 기능 테스트 (데스크톱 + 모바일)
- [ ] 스크린샷 비교 검증
> ⚠️ **2026-01-14 수정**: 이전 세션에서 완료 표시했으나 실제 코드 미적용 확인됨. 파일럿은 건너뛰고 Level 1부터 순차 진행.
- [ ] ~~기본 케이스 - 카드관리(HR)~~ → Level 3으로 이동 (복잡한 상태)
- [ ] ~~특이 케이스 - 게시판목록~~ → Level 4로 이동 (동적 탭)
- [ ] ~~특이 케이스 - 발주관리~~ → Level 2로 이동 (ScheduleCalendar)
### Level 1 마이그레이션 진행 상황 (15/15 완료 ✅)
| # | 파일 | 상태 | 완료일 |
|---|-----|------|--------|
| 1 | `production/WorkOrders/WorkOrderList.tsx` | ✅ 완료 | 2026-01-14 |
| 2 | `production/WorkResults/WorkResultList.tsx` | ✅ 완료 | 2026-01-14 |
| 3 | `outbound/ShipmentManagement/ShipmentList.tsx` | ✅ 완료 | 2026-01-14 |
| 4 | `material/StockStatus/StockStatusList.tsx` | ✅ 완료 | 2026-01-14 |
| 5 | `material/ReceivingManagement/ReceivingList.tsx` | ✅ 완료 | 2026-01-14 |
| 6 | `quality/InspectionManagement/InspectionList.tsx` | ✅ 완료 | 2026-01-14 |
| 7 | `items/ItemListClient.tsx` | ✅ 완료 | 2026-01-14 |
| 8 | `settings/PaymentHistoryManagement/PaymentHistoryClient.tsx` | ✅ 완료 | 2026-01-14 |
| 9 | `settings/PopupManagement/PopupList.tsx` | ✅ 완료 | 2026-01-14 |
| 10 | `customer-center/EventManagement/EventList.tsx` | ✅ 완료 | 2026-01-14 |
| 11 | `customer-center/InquiryManagement/InquiryList.tsx` | ✅ 완료 | 2026-01-14 |
| 12 | `customer-center/NoticeManagement/NoticeList.tsx` | ✅ 완료 | 2026-01-14 |
| 13 | `quotes/QuoteManagementClient.tsx` | ✅ 완료 | 2026-01-14 |
| 14 | `process-management/ProcessListClient.tsx` | ✅ 완료 | 2026-01-14 |
| 15 | `settings/AccountManagement/index.tsx` | ✅ 완료 | 2026-01-14 |
### Level 2 마이그레이션 진행 상황 (건설 17개 + 회계 13개 = 총 30개)
> **Note**: ProjectListClient는 PageLayout 직접 사용 (IntegratedListTemplateV2 미사용)으로 마이그레이션 대상에서 제외
#### 건설 도메인 (17개 대상, 17개 완료 ✅)
| # | 파일 | 특이사항 | 상태 | 완료일 |
|---|-----|---------|------|--------|
| 1 | `construction/estimates/EstimateListClient.tsx` | 거래처(다중), 견적자(다중), 상태, 정렬 | ✅ 완료 | 2026-01-14 |
| 2 | `construction/bidding/BiddingListClient.tsx` | 거래처(다중), 입찰자(다중), 상태, 정렬 | ✅ 완료 | 2026-01-14 |
| 3 | `construction/site-briefings/SiteBriefingListClient.tsx` | 거래처(다중), 타입, 상태, 정렬 | ✅ 완료 | 2026-01-14 |
| 4 | `construction/contract/ContractListClient.tsx` | 거래처(다중), 계약담당자(다중), 공사PM(다중) | ✅ 완료 | 2026-01-14 |
| 5 | `construction/partners/PartnerListClient.tsx` | 탭(전체/신규), 악성채권, 정렬 | ✅ 완료 | 2026-01-14 |
| 6 | `construction/handover-report/HandoverReportListClient.tsx` | 거래처, 계약담당자, 공사PM | ✅ 완료 | 2026-01-15 |
| 7 | `construction/worker-status/WorkerStatusListClient.tsx` | 거래처, 현장, 구분, 부서, 이름 | ✅ 완료 | 2026-01-15 |
| 8 | `construction/utility-management/UtilityManagementListClient.tsx` | 7개 필터, AlertDialog | ✅ 완료 | 2026-01-15 |
| 9 | `construction/progress-billing/ProgressBillingManagementListClient.tsx` | showQuickButtons | ✅ 완료 | 2026-01-15 |
| 10 | `construction/structure-review/StructureReviewListClient.tsx` | AlertDialog, createButton | ✅ 완료 | 2026-01-15 |
| 11 | `construction/site-management/SiteManagementListClient.tsx` | AlertDialog | ✅ 완료 | 2026-01-15 |
| 12 | `construction/pricing-management/PricingListClient.tsx` | **renderCustomTableHeader (동적 컬럼)** | ✅ 완료 | 2026-01-15 |
| 13 | `construction/issue-management/IssueManagementListClient.tsx` | bulkActions (회수 기능) | ✅ 완료 | 2026-01-15 |
| 14 | `construction/order-management/OrderManagementListClient.tsx` | ScheduleCalendar (beforeTableContent) | ✅ 완료 | 2026-01-15 |
| 15 | `construction/management/ConstructionManagementListClient.tsx` | ScheduleCalendar (beforeTableContent) | ✅ 완료 | 2026-01-15 |
| 16 | `construction/labor-management/LaborManagementClient.tsx` | 노무 관리 필터 | ✅ 완료 | 2026-01-15 |
| 17 | `construction/item-management/ItemManagementClient.tsx` | 품목 분류 필터 | ✅ 완료 | 2026-01-15 |
#### 회계 도메인 (13개 대상, 13개 완료 ✅)
| # | 파일 | 특이사항 | 상태 | 완료일 |
|---|-----|---------|------|--------|
| 1 | `accounting/VendorManagement/index.tsx` | 5개 single 필터, Stats 카드 | ✅ 완료 | 2026-01-15 |
| 2 | `accounting/SalesManagement/index.tsx` | Switch, beforeTableContent, tableHeaderActions, tableFooter | ✅ 완료 | 2026-01-15 |
| 3 | `accounting/PurchaseManagement/index.tsx` | Switch, beforeTableContent, tableHeaderActions, tableFooter | ✅ 완료 | 2026-01-15 |
| 4 | `accounting/DepositManagement/index.tsx` | beforeTableContent (새로고침), tableHeaderActions | ✅ 완료 | 2026-01-15 |
| 5 | `accounting/WithdrawalManagement/index.tsx` | 계정과목명 저장, beforeTableContent, tableHeaderActions | ✅ 완료 | 2026-01-15 |
| 6 | `accounting/BillManagement/index.tsx` | 어음관리 필터, RadioGroup | ✅ 완료 | 2026-01-15 |
| 7 | `accounting/BadDebtCollection/index.tsx` | 부실채권 관리, Switch 토글, 3개 필터 | ✅ 완료 | 2026-01-15 |
| 8 | `accounting/BankTransactionInquiry/index.tsx` | 서버사이드 페이지네이션, tableFooter, 3개 필터 | ✅ 완료 | 2026-01-15 |
| 9 | `accounting/CardTransactionInquiry/index.tsx` | 상세 모달, 계정과목명 일괄 저장, 2개 필터 | ✅ 완료 | 2026-01-15 |
| 10 | `accounting/VendorLedger/index.tsx` | 서버사이드 페이지네이션, 엑셀 다운로드, tableFooter | ✅ 완료 | 2026-01-15 |
| 11 | `accounting/ExpectedExpenseManagement/index.tsx` | **매우 복잡** (월별 그룹핑, 폼 다이얼로그, externalPagination/externalSelection) | ✅ 완료 | 2026-01-15 |
| 12 | `accounting/BillManagement/BillManagementClient.tsx` | dateRangeSelector, beforeTableContent (상태+저장+라디오), externalPagination/Selection | ✅ 완료 | 2026-01-15 |
| 13 | `accounting/VendorManagement/VendorManagementClient.tsx` | computeStats, 5개 필터, 클라이언트 필터링, externalPagination/Selection | ✅ 완료 | 2026-01-15 |
---
## 📋 마이그레이션 페이지별 테스트 체크리스트
### 데스크톱 기능 테스트
- [ ] 테이블 렌더링 (데이터 표시, 컬럼 정렬)
- [ ] 행 선택 (체크박스 동작, 선택 카운터)
- [ ] 수정/삭제 버튼 (선택 시 표시, 페이지 이동)
- [ ] 필터 동작 (검색, 필터 적용, 초기화)
- [ ] 페이지네이션 (페이지 이동, 개수 변경)
- [ ] 탭 동작 (탭 전환, 데이터 필터링)
### 📱 모바일 반응형 테스트
> **최소 지원 너비**: 280px (모바일 최소 사이즈 기준)
- [ ] **레이아웃 깨짐 확인**: 280px까지 축소 시 요소 겹침/튀어나감 없음
- [ ] **줄바꿈 정상**: 긴 텍스트 줄바꿈 처리 확인
- [ ] **버튼/뱃지**: 영역 밖으로 튀어나가지 않음
- [ ] **모바일 필터**: 하단에서 슬라이드업 정상 동작
- [ ] **필터 적용/초기화**: 모바일 필터 버튼 정상 작동
- [ ] **모바일 카드 뷰**: renderMobileCard 정상 표시
- [ ] **터치 동작**: 체크박스, 버튼 터치 반응 정상
### 스크린샷 비교
- [ ] 데스크톱: 기존 페이지 vs 마이그레이션 페이지 비교
- [ ] 모바일: 기존 페이지 vs 마이그레이션 페이지 비교
---
## 📊 복잡도별 분류 (마이그레이션 우선순위)
> **통합 후 이점**: 새 기능 추가/버그 수정 시 55개 파일 → **1개 파일**만 수정
### Level 1 (기본) - 15개 ⭐ 1순위
단순 테이블 + 기본 탭 필터만 있는 경우
| 파일 | 설명 |
|-----|------|
| `production/WorkOrders/WorkOrderList.tsx` | 탭 기반 상태 필터링 |
| `production/WorkResults/WorkResultList.tsx` | 기본 리스트 |
| `outbound/ShipmentManagement/ShipmentList.tsx` | 상태별 통계, 탭 필터 |
| `material/StockStatus/StockStatusList.tsx` | 재고 현황 |
| `material/ReceivingManagement/ReceivingList.tsx` | 기본 수입 목록 |
| `quality/InspectionManagement/InspectionList.tsx` | 검사 상태별 탭 |
| `items/ItemListClient.tsx` | 품목 유형별 탭 |
| `settings/PaymentHistoryManagement/PaymentHistoryClient.tsx` | 결제 이력 |
| `settings/PopupManagement/PopupList.tsx` | 팝업 관리 |
| `customer-center/EventManagement/EventList.tsx` | 이벤트 관리 |
| `customer-center/InquiryManagement/InquiryList.tsx` | 문의 관리 |
| `customer-center/NoticeManagement/NoticeList.tsx` | 공지사항 |
| `quotes/QuoteManagementClient.tsx` | 견적 관리 |
| `process-management/ProcessListClient.tsx` | 프로세스 관리 |
| `settings/AccountManagement/index.tsx` | 계정 관리 |
### Level 2 (필터 복잡) - 30개 ⭐ 2순위
FilterFieldConfig 기반 다중 필터, 정렬 옵션 (주류 패턴)
#### 건설 도메인 (17개)
| 파일 | 특이사항 |
|-----|---------|
| `construction/site-briefings/SiteBriefingListClient.tsx` | 거래처(다중), 타입, 상태, 정렬 |
| `construction/estimates/EstimateListClient.tsx` | 거래처(다중), 견적자(다중), 상태, 정렬 |
| `construction/bidding/BiddingListClient.tsx` | 입찰 정보 필터 |
| `construction/contract/ContractListClient.tsx` | 계약 정보 필터 |
| `construction/partners/PartnerListClient.tsx` | 협력업체 필터 |
| `construction/handover-report/HandoverReportListClient.tsx` | 준공 보고 필터 |
| `construction/worker-status/WorkerStatusListClient.tsx` | 근로자 상태 필터 |
| `construction/utility-management/UtilityManagementListClient.tsx` | 유틸리티 관리 필터 |
| `construction/progress-billing/ProgressBillingManagementListClient.tsx` | 기성 청구 필터 |
| `construction/structure-review/StructureReviewListClient.tsx` | 구조 검토 필터 |
| `construction/site-management/SiteManagementListClient.tsx` | 현장 정보 필터 |
| `construction/pricing-management/PricingListClient.tsx` | **동적 컬럼 (renderCustomTableHeader)** |
| `construction/issue-management/IssueManagementListClient.tsx` | 거래처, 현장, 구분, 중요도, 상태 |
| `construction/order-management/OrderManagementListClient.tsx` | ScheduleCalendar (beforeTableContent) |
| `construction/management/ConstructionManagementListClient.tsx` | ScheduleCalendar (beforeTableContent) |
| `construction/labor-management/LaborManagementClient.tsx` | 노무 관리 필터 |
| `construction/item-management/ItemManagementClient.tsx` | 품목 분류 필터 |
#### 회계 도메인 (13개)
| 파일 | 특이사항 |
|-----|---------|
| `accounting/VendorManagement/VendorManagementClient.tsx` | 거래처 분류(다중), 신용등급, 거래등급 |
| `accounting/VendorManagement/index.tsx` | VendorManagementClient wrapper |
| `accounting/PurchaseManagement/index.tsx` | 구매 관리 필터 |
| `accounting/SalesManagement/index.tsx` | 판매 관리 필터 |
| `accounting/DepositManagement/index.tsx` | 입금 관리 필터 |
| `accounting/WithdrawalManagement/index.tsx` | 출금 관리 필터 |
| `accounting/BadDebtCollection/index.tsx` | 부실채권 관리 필터 |
| `accounting/ExpectedExpenseManagement/index.tsx` | 예상 지출 관리 필터 |
| `accounting/BillManagement/index.tsx` | 청구서 관리 wrapper |
| `accounting/BillManagement/BillManagementClient.tsx` | 청구서 관리 필터 |
| `accounting/BankTransactionInquiry/index.tsx` | 입출금계좌조회 |
| `accounting/CardTransactionInquiry/index.tsx` | 카드내역조회 |
| `accounting/VendorLedger/index.tsx` | 거래처원장 |
### Level 3~5 마이그레이션 (2026-01-15) ✅ 완료
> **결론 변경**: Level 3~5 컴포넌트(10개)도 **UniversalListPage로 마이그레이션 진행**
> **이유**: 장기적 유지보수 및 모바일 대응 일원화를 위해 모든 리스트 페이지 통합
#### Phase 3-1: UniversalListPage 기능 확장 ✅ 완료
| 기능 | 설명 | 상태 |
|------|------|------|
| `renderDialogs` | 커스텀 다이얼로그 슬롯 | [x] 완료 |
| `dynamicHeaderActions` | 선택 상태 기반 동적 헤더 액션 | [x] 완료 (tableHeaderActions에 selectedItems 전달) |
| `fetchTabs` | API 기반 동적 탭 생성 | [x] 완료 (이미 구현되어 있었음) |
| `columnsPerTab` | 탭별 다른 컬럼 구조 지원 | [x] 완료 |
| `extraFilters` | 추가 필터 슬롯 | [x] 완료 (이미 구현되어 있었음) |
#### Phase 3-2: 게시판 도메인 마이그레이션 (2개) ✅ 완료
| # | 파일 | 특이사항 | 상태 |
|---|------|---------|------|
| 1 | `board/BoardManagement/index.tsx` | AlertDialog + 선택 기반 수정/삭제 | [x] 완료 |
| 2 | `board/BoardList/index.tsx` | API 동적 탭 (fetchTabs) + 서버사이드 페이지네이션 | [x] 완료 |
#### Phase 3-3: 전자결재 도메인 마이그레이션 (3개) ✅ 완료
| # | 파일 | 특이사항 | 상태 |
|---|------|---------|------|
| 1 | `approval/DraftBox/index.tsx` | DocumentDetailModal + 상신/삭제 + 동적 헤더 | [x] 완료 |
| 2 | `approval/ApprovalBox/index.tsx` | DocumentDetailModal + 승인/반려 다이얼로그 | [x] 완료 |
| 3 | `approval/ReferenceBox/index.tsx` | DocumentDetailModal + 열람/미열람 처리 | [x] 완료 |
#### Phase 3-4: HR 도메인 마이그레이션 (5개) ✅ 완료
| # | 파일 | 특이사항 | 상태 |
|---|------|---------|------|
| 1 | `hr/CardManagement/index.tsx` | 3개 탭 + AlertDialog (가장 단순) | [x] 완료 |
| 2 | `hr/SalaryManagement/index.tsx` | SalaryDetailDialog + 선택 기반 동적 버튼 | [x] 완료 |
| 3 | `hr/AttendanceManagement/index.tsx` | 9개 탭 + 2개 다이얼로그 + extraFilters | [x] 완료 |
| 4 | `hr/EmployeeManagement/index.tsx` | 4개 탭 + 복수 다이얼로그 + DateRangeSelector | [x] 완료 |
| 5 | `hr/VacationManagement/index.tsx` | 3개 탭(탭별 상이한 컬럼) + 4개 다이얼로그 | [x] 완료 |
### 최종 분류 통계 ✅ 완료
| 레벨 | 개수 | 상태 | 비고 |
|-----|-----|------|-----|
| Level 1 (기본) | 15개 | ✅ 완료 | UniversalListPage 마이그레이션 |
| Level 2 (필터) | 30개 | ✅ 완료 | UniversalListPage 마이그레이션 |
| Level 3~5 (복잡) | 10개 | ✅ 완료 | UniversalListPage 마이그레이션 |
| **합계** | **55개** | ✅ **완료** | **전체 통합 완료!** |
---
@@ -124,12 +469,160 @@
---
## Phase 5: 최종 검증
## Phase 5: 최종 검증 (QA 체크리스트)
- [ ] 전체 59개 페이지 기능 테스트
- [ ] 스크린샷 비교 검증 (변경 전 vs 후)
- [ ] 모바일 뷰 테스트
- [ ] 성능 테스트 (빌드 사이즈, 로딩 속도)
### QA 검수 기준
-**PC**: 테이블 렌더링, 필터, 페이지네이션, 행 선택, 수정/삭제
-**모바일**: 카드 뷰, 바텀시트 필터, 터치 동작
-**공통**: API 연동, 데이터 표시, 에러 처리
---
### Level 1 - 기본 페이지 (15개) ✅ QA 완료
| # | 페이지 | 경로 | PC | 모바일 | 비고 |
|---|--------|------|:--:|:------:|------|
| 1 | 작업지시관리 | `/production/work-orders` | [x] | [x] | |
| 2 | 작업실적조회 | `/production/work-results` | [x] | [x] | |
| 3 | 출하관리 | `/outbound/shipment` | [x] | [x] | |
| 4 | 재고현황 | `/material/stock-status` | [x] | [x] | |
| 5 | 입고관리 | `/material/receiving` | [x] | [x] | |
| 6 | 검사관리 | `/quality/inspection` | [x] | [x] | |
| 7 | 품목기준관리 | `/items` | [x] | [x] | |
| 8 | 결제내역 | `/settings/payment-history` | [x] | [x] | |
| 9 | 팝업관리 | `/settings/popup` | [x] | [x] | |
| 10 | 이벤트관리 | `/customer-center/events` | [x] | [x] | 모바일 탭 이슈 해결 완료 |
| 11 | 1:1문의 | `/customer-center/inquiries` | [x] | [x] | 필터 동작 검증 완료 |
| 12 | 공지사항 | `/customer-center/notices` | [x] | [x] | |
| 13 | 견적관리 | `/quotes` | [x] | [x] | |
| 14 | 공정관리 | `/process-management` | [x] | [x] | |
| 15 | 계좌관리 | `/settings/accounts` | [x] | [x] | |
---
### Level 2 - 건설 도메인 (17개) ✅ QA 완료
| # | 페이지 | 경로 | PC | 모바일 | 비고 |
|---|--------|------|:--:|:------:|------|
| 1 | 견적관리 | `/construction/estimates` | [x] | [x] | |
| 2 | 입찰관리 | `/construction/bidding` | [x] | [x] | |
| 3 | 현장설명회 | `/construction/site-briefings` | [x] | [x] | |
| 4 | 계약관리 | `/construction/contracts` | [x] | [x] | |
| 5 | 협력업체 | `/construction/partners` | [x] | [x] | |
| 6 | 인수인계보고서 | `/construction/handover-report` | [x] | [x] | |
| 7 | 작업인력현황 | `/construction/worker-status` | [x] | [x] | |
| 8 | 공과관리 | `/construction/utility` | [x] | [x] | |
| 9 | 기성청구관리 | `/construction/progress-billing` | [x] | [x] | |
| 10 | 구조검토관리 | `/construction/structure-review` | [x] | [x] | |
| 11 | 현장관리 | `/construction/sites` | [x] | [x] | |
| 12 | 단가관리 | `/construction/pricing` | [x] | [x] | 동적 컬럼 |
| 13 | 이슈관리 | `/construction/issues` | [x] | [x] | |
| 14 | 발주관리 | `/construction/order/order-management` | [x] | [x] | ScheduleCalendar |
| 15 | 시공관리 | `/construction/management` | [x] | [x] | ScheduleCalendar |
| 16 | 노임관리 | `/construction/labor` | [x] | [x] | |
| 17 | 품목관리 | `/construction/items` | [x] | [x] | |
---
### Level 2 - 회계 도메인 (13개) ✅ QA 완료
| # | 페이지 | 경로 | PC | 모바일 | 비고 |
|---|--------|------|:--:|:------:|------|
| 1 | 거래처관리 | `/accounting/vendors` | [x] | [x] | |
| 2 | 매출관리 | `/accounting/sales` | [x] | [x] | filterConfig 추가 |
| 3 | 매입관리 | `/accounting/purchases` | [x] | [x] | filterConfig 추가 |
| 4 | 입금관리 | `/accounting/deposits` | [x] | [x] | |
| 5 | 출금관리 | `/accounting/withdrawals` | [x] | [x] | |
| 6 | 어음관리 | `/accounting/bills` | [x] | [x] | |
| 7 | 악성채권추심 | `/accounting/bad-debt` | [x] | [x] | filterConfig 추가 |
| 8 | 입출금계좌조회 | `/accounting/bank-transactions` | [x] | [x] | filterConfig 추가 |
| 9 | 카드내역조회 | `/accounting/card-transactions` | [x] | [x] | |
| 10 | 거래처원장 | `/accounting/vendor-ledger` | [x] | [x] | |
| 11 | 지출예상내역서 | `/accounting/expected-expenses` | [x] | [x] | |
| 12 | 어음관리Client | `/accounting/bills` | [x] | [x] | |
| 13 | 거래처관리Client | `/accounting/vendors` | [x] | [x] | |
---
### Level 3~5 - 복잡 페이지 (10개) ✅ QA 완료
| # | 페이지 | 경로 | PC | 모바일 | 비고 |
|---|--------|------|:--:|:------:|------|
| 1 | 게시판관리 | `/board/management` | [x] | [x] | |
| 2 | 게시판목록 | `/board` | [x] | [x] | 동적 탭 |
| 3 | 기안함 | `/approval/draft` | [x] | [x] | 문서 모달, 모바일 필터 추가 |
| 4 | 결재함 | `/approval/approval` | [x] | [x] | 승인/반려, 모바일 필터 추가 |
| 5 | 참조함 | `/approval/reference` | [x] | [x] | 모바일 필터 추가 |
| 6 | 카드관리 | `/hr/card-management` | [x] | [x] | 탭 필터만 (PC필터 없음) |
| 7 | 급여관리 | `/hr/salary-management` | [x] | [x] | 정렬 필터 |
| 8 | 근태관리 | `/hr/attendance-management` | [x] | [x] | 9개 탭, 필터+정렬 |
| 9 | 사원관리 | `/hr/employee-management` | [x] | [x] | 필터+정렬 |
| 10 | 휴가관리 | `/hr/vacation-management` | [x] | [x] | 필터+정렬 |
---
### QA 진행 현황
| 레벨 | 전체 | PC 완료 | 모바일 완료 | 진행률 |
|-----|-----|---------|------------|--------|
| Level 1 | 15 | 15 | 15 | **100%** ✅ |
| Level 2 건설 | 17 | 17 | 17 | **100%** ✅ |
| Level 2 회계 | 13 | 13 | 13 | **100%** ✅ |
| Level 3~5 | 10 | 10 | 10 | **100%** ✅ |
| **합계** | **55** | **55** | **55** | **100%** ✅ |
### 🚨 알려진 이슈
| 이슈 | 영향 범위 | 상태 | 비고 |
|------|----------|------|------|
| 모바일 탭 미표시 | 탭 사용 페이지 전체 | ✅ 해결 완료 | IntegratedListTemplateV2 수정 (hidden → block) |
---
## Phase 6: 다음 개선 사항 (Next Steps)
### 6.1 모바일 인피니티 스크롤 (Infinite Scroll)
> **목적**: 모바일 카드 뷰에서 페이지네이션 대신 무한 스크롤로 UX 개선
#### 구현 계획
**config 옵션 추가**:
```typescript
// UniversalListConfig에 추가
infiniteScroll?: {
enabled: boolean;
mobileOnly?: boolean; // 모바일에서만 적용 (기본: true)
pageSize?: number; // 한 번에 로드할 개수 (기본: 20)
threshold?: number; // 트리거 위치 (기본: 0.8 = 80% 스크롤)
};
```
**동작 방식**:
```
모바일 + infiniteScroll.enabled?
├─ Yes → 페이지네이션 숨김 + IntersectionObserver로 무한스크롤
└─ No → 기존 페이지네이션 유지
```
**기술 스택**:
- `IntersectionObserver` (네이티브) 또는 `react-intersection-observer`
- 스크롤 80% 도달 시 다음 pageSize개 로드
- 로딩 스피너 표시 → 데이터 append → 스피너 제거
**적용 효과**:
- config 한 줄 추가로 55개 페이지 자동 적용
- PC는 기존 페이지네이션 유지
- 모바일만 무한스크롤 적용
#### 구현 체크리스트
- [ ] `IntersectionObserver` 훅 구현 (`useInfiniteScroll`)
- [ ] `UniversalListConfig``infiniteScroll` 옵션 추가
- [ ] `IntegratedListTemplateV2`에 무한스크롤 로직 추가
- [ ] 모바일 감지 시 페이지네이션 → 무한스크롤 전환
- [ ] 로딩 스피너 컴포넌트 추가
- [ ] 파일럿 페이지 테스트
- [ ] 전체 페이지 적용
---
@@ -149,7 +642,48 @@
| 날짜 | 작업 내용 |
|------|----------|
| 2026-01-14 | 체크리스트 문서 생성, 작업 시작 |
| 2026-01-14 | 영업 도메인 3개 추가 (총 56개 → 59개) |
| 2026-01-14 | 영업 도메인 3개 발견 (마이그레이션 대상 검토 필요) |
| 2026-01-14 | ~~파일럿 3개 완료~~**실제 미적용 확인됨** |
| 2026-01-14 | 복잡도별 분류 완료 (Level 1~5, 55개 파일) |
| 2026-01-14 | 모바일 반응형 테스트 체크리스트 추가 |
| 2026-01-14 | "본 페이지 직접 작업" 정책 추가, 테스트 페이지 4개 삭제 |
| 2026-01-14 | Level 1: NoticeList, PopupList, EventList, InquiryList 마이그레이션 완료 (4/15) |
| 2026-01-14 | **체크리스트 정합성 수정** - 파일럿 3개 미완료 확인, Level 1 진행 상황 테이블 추가 |
| 2026-01-14 | Level 1 마이그레이션 진행: WorkOrderList, WorkResultList, ShipmentList, StockStatusList, ReceivingList, InspectionList 완료 (6개) |
| 2026-01-14 | Level 1 마이그레이션 진행: ItemListClient, PaymentHistoryClient, AccountManagement 완료 (3개 추가, 총 13/15) |
| 2026-01-14 | **Level 1 완료!** QuoteManagementClient, ProcessListClient 마이그레이션 완료 (15/15) |
| 2026-01-14 | Level 1 검수: 탭 기본값(ShipmentList, StockStatusList), optional chaining(UniversalListPage), 필터 중복(InquiryList), "총 N건" 위치 수정 |
| 2026-01-14 | **Phase 2.5 추가**: 달력/버튼 공통 옵션화 리팩토링 계획 문서화 |
| 2026-01-14 | **Phase 2.5 완료**: dateRangeSelector/createButton config 옵션 구현, Level 1 페이지 4개 마이그레이션 (InquiryList, NoticeList, EventList, PopupList) |
| 2026-01-14 | **Level 2 시작**: 건설 도메인 5개 완료 (EstimateListClient, BiddingListClient, SiteBriefingListClient, ContractListClient, PartnerListClient) |
| 2026-01-14 | ProjectListClient 제외 (PageLayout 직접 사용, IntegratedListTemplateV2 미사용) |
| 2026-01-15 | 건설 도메인 6개 추가 완료 (HandoverReport, WorkerStatus, Utility, ProgressBilling, StructureReview, SiteManagement) |
| 2026-01-15 | **UniversalListPage에 renderCustomTableHeader 지원 추가** (동적 컬럼용) |
| 2026-01-15 | 건설 도메인 6개 추가 완료 (PricingList, IssueManagement, OrderManagement, ConstructionManagement, LaborManagement, ItemManagement) |
| 2026-01-15 | **건설 도메인 17개 모두 완료!** ✅ |
| 2026-01-15 | 회계 도메인 5개 완료 (VendorManagement, SalesManagement, PurchaseManagement, DepositManagement, WithdrawalManagement) |
| 2026-01-15 | 회계 도메인 6개 추가 완료 (BillManagement, BadDebtCollection, BankTransactionInquiry, CardTransactionInquiry, VendorLedger, ExpectedExpenseManagement) |
| 2026-01-15 | **UniversalListPage에 externalPagination, externalSelection 지원 추가** (복잡한 외부 상태 관리용) |
| 2026-01-15 | 회계 도메인 11/13 완료 (남은 2개: BillManagementClient, VendorManagementClient 확인 필요) |
| 2026-01-15 | **회계 도메인 13/13 완료!** ✅ BillManagementClient, VendorManagementClient 마이그레이션 완료 |
| 2026-01-15 | **Level 3~5 분석 완료** - 전자결재(3), HR(5), 게시판(2) 총 10개 파일 분석 |
| 2026-01-15 | **마이그레이션 최종 결론 변경**: Level 3~5도 UniversalListPage로 마이그레이션 진행 결정 |
| 2026-01-15 | **Phase 3-1 완료**: UniversalListPage 기능 확장 (renderDialogs, headerActions with selectedItems) |
| 2026-01-15 | **Phase 3-2 완료**: 게시판 도메인 마이그레이션 2개 (BoardManagement, BoardList) |
| 2026-01-15 | **Phase 3-3 완료**: 전자결재 도메인 마이그레이션 3개 (DraftBox, ApprovalBox, ReferenceBox) |
| 2026-01-15 | **Phase 3-4 완료**: HR 도메인 마이그레이션 5개 (CardManagement, SalaryManagement, AttendanceManagement, EmployeeManagement, VacationManagement) |
| 2026-01-15 | **🎉 프로젝트 완료!** Level 1~5 (55개) 전체 마이그레이션 완료 |
| 2026-01-15 | **Level 1 QA 완료!** 15개 페이지 PC/모바일 검수, 이벤트관리 모바일 탭 이슈 발견 (공통 수정 예정) |
| 2026-01-15 | **Level 2 건설 QA 완료!** 17개 페이지 PC/모바일 검수 완료 |
| 2026-01-15 | **Level 2 회계 모바일 필터 추가!** 매출관리, 매입관리, 악성채권추심, 은행거래조회 4개 페이지 |
| 2026-01-15 | **결재 도메인 모바일 필터 추가!** 기안함, 결재함, 참조함 3개 페이지에 filterConfig + onFilterChange 추가 |
| 2026-01-15 | **Level 3~5 QA 완료!** HR 도메인 5개 페이지 (카드/급여/근태/사원/휴가) 전체 검수 완료 |
| 2026-01-15 | **🎉 전체 QA 완료!** 55개 페이지 PC/모바일 검수 100% 완료 |
| 2026-01-16 | **📊 페이지 수 최종 확정!** 62개 파일 중 중복(6쌍) 및 제외 대상 정리 → 55개 페이지 확정 |
| 2026-01-16 | **Phase 5 기능 검수 시작** - 수동 QA 진행, 오류 발견 및 수정 |
| 2026-01-16 | **휴가관리 탭 카운트 수정** - config.tabs 변경 감지 useEffect 추가 (UniversalListPage) |
| 2026-01-16 | **휴가관리 기능 검증** - 휴가신청/승인/거절 버튼 정상 동작 확인 |
| 2026-01-16 | **휴가관리 승인/거절 건수 표시 수정** - handleApproveClick/handleRejectClick에서 selected를 내부 state로 복사 |
---
@@ -160,4 +694,125 @@
| `~/Desktop/test-urls_리스트 게시판 스샷/` | 34개 | 일반 도메인 |
| `~/Desktop/construction-test-urls_리스트 게시판 스샷/` | 18개 | 건설 도메인 |
| `~/Desktop/추가_리스트_스샷/` | 7개 | 누락 페이지 |
| **합계** | **59개** | |
| **합계** | **59개** | 스크린샷 기준 (제외 대상 포함) |
> **Note**: 스크린샷은 59개지만, 실제 마이그레이션 대상 페이지는 55개입니다. (제외 대상 4개, 중복 제거)
---
## 🔍 Phase 5: 전체 기능 검수 (2026-01-16)
> **목적**: UniversalListPage 적용 후 모든 핵심 기능 정상 동작 확인
> **검수 항목**: 검색 / 탭 / 필터 / 체크박스 / 상세이동 / 등록버튼
### 검수 항목 설명
| 항목 | 설명 |
|------|------|
| 🔍 검색 | 검색창 입력 후 필터링 동작 |
| 📑 탭 | 탭 버튼 클릭 시 데이터 전환 |
| 🎛️ 필터 | 필터 선택/적용/초기화 동작 |
| ☑️ 체크박스 | 테이블 행 체크박스 선택 동작 |
| 👁️ 상세 | 테이블 로우 클릭 → 상세페이지/모달 이동 |
| 등록 | 등록 버튼 클릭 → 등록페이지 이동 |
### HR 도메인 (5개)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 사원관리 | `/hr/employee-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 2 | 카드관리 | `/hr/card-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 3 | 근태관리 | `/hr/attendance-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 4 | 급여관리 | `/hr/salary-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 5 | 휴가관리 | `/hr/vacation-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
### 전자결재 도메인 (3개)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 기안함 | `/approval/draft-box` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 2 | 결재함 | `/approval/approval-box` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 3 | 참조함 | `/approval/reference-box` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
### 게시판 도메인 (2개)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 게시판관리 | `/settings/board-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 2 | 게시판목록 | `/boards/[boardCode]` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
### 건설 도메인 (17개)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 견적관리 | `/construction/estimates` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 2 | 입찰관리 | `/construction/bidding` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 3 | 현장설명회 | `/construction/site-briefings` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 4 | 계약관리 | `/construction/contracts` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 5 | 협력업체 | `/construction/partners` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 6 | 준공보고 | `/construction/handover-reports` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 7 | 근로자현황 | `/construction/worker-status` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 8 | 유틸리티관리 | `/construction/utility-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 9 | 기성관리 | `/construction/progress-billing` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 10 | 구조검토 | `/construction/structure-review` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 11 | 현장관리 | `/construction/sites` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 12 | 단가관리 | `/construction/pricing` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 13 | 이슈관리 | `/construction/issues` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 14 | 발주관리 | `/construction/order-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 15 | 공사관리 | `/construction/management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 16 | 노무관리 | `/construction/labor-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 17 | 품목관리 | `/construction/item-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
### 회계 도메인 (13개)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 거래처관리 | `/accounting/vendor-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 2 | 매출관리 | `/accounting/sales-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 3 | 매입관리 | `/accounting/purchase-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 4 | 입금관리 | `/accounting/deposit-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 5 | 출금관리 | `/accounting/withdrawal-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 6 | 어음관리 | `/accounting/bill-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 7 | 부실채권 | `/accounting/bad-debt-collection` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 8 | 입출금조회 | `/accounting/bank-transactions` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 9 | 카드조회 | `/accounting/card-transactions` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 10 | 거래처원장 | `/accounting/vendor-ledger` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 11 | 예상지출 | `/accounting/expected-expenses` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
### 기타 도메인 (15개)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 작업지시 | `/production/work-orders` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 2 | 작업실적 | `/production/work-results` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 3 | 출하관리 | `/outbound/shipment-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 4 | 재고현황 | `/material/stock-status` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 5 | 입고관리 | `/material/receiving` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 6 | 검사관리 | `/quality/inspection` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 7 | 품목관리 | `/items` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 8 | 결제이력 | `/settings/payment-history` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 9 | 팝업관리 | `/settings/popup-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 10 | 이벤트관리 | `/customer-center/events` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 11 | 문의관리 | `/customer-center/inquiries` | [ ] | [ ] | [ ] | [ ] | [ ] | N/A | 🔄 |
| 12 | 공지관리 | `/customer-center/notices` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 13 | 견적관리 | `/quotes` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 14 | 공정관리 | `/process-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
| 15 | 계정관리 | `/settings/account-management` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
### 영업 도메인 (추가)
| # | 페이지 | URL | 🔍 검색 | 📑 탭 | 🎛️ 필터 | ☑️ 체크 | 👁️ 상세 | 등록 | 상태 |
|---|--------|-----|---------|-------|---------|---------|---------|---------|------|
| 1 | 거래처관리 | `/sales/client-management-sales-admin` | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | 🔄 |
---
### 🐛 발견된 오류 목록
| # | 페이지 | 오류 내용 | 원인 | 해결 상태 |
|---|--------|----------|------|----------|
| 1 | 급여관리 | 달력 아이콘 짤림 (375px) | Input width 너무 좁음 | ✅ 수정 |
| 2 | 급여관리 | DateRangeSelector 미사용 | 직접 Input 사용 | ✅ 수정 |
| 3 | 거래처관리(영업) | `headerActions.call is not a function` | headerActions 함수 아님 | ✅ 수정 |
| 4 | 거래처관리(영업) | NaN globalIndex | externalPagination 형태 불일치 | ✅ 수정 |
| 5 | 휴가관리 | `externalSelection.onToggleSelection is not a function` | externalSelection 형태 불일치 | ✅ 수정 |
| 6 | 휴가관리 | 탭 변경 시 `Invalid time value` | 날짜 필드 null 체크 없음 + 오타 | ✅ 수정 |
| 7 | 근태관리 | 프리셋 버튼 미표시 | showPresets: false | ✅ 수정 |
| 8 | 휴가관리 | 탭 카운트 모두 동일하게 표시 | tabFilter 제거 후 count 동기화 안됨 | ✅ 수정 |
| 9 | UniversalListPage | config.tabs 변경 시 내부 상태 미동기화 | useEffect 누락 | ✅ 수정 |
| 10 | 사원관리 | 프리셋 버튼 미표시 | showPresets: false | ✅ 수정 |
| 11 | 휴가관리 | 승인/거절 팝업 미표시 | selectedItems 상태 불일치 | ✅ 수정 |
| 12 | 휴가관리 | 승인/거절 팝업에 선택 건수 0으로 표시 | headerActions의 selected가 내부 state로 복사 안됨 | ✅ 수정 |
| 13 | 단가관리(판매) | `headerActions.call is not a function` | headerActions가 함수 아닌 ReactNode | ✅ 수정 |

View File

@@ -1,131 +1,91 @@
# UniversalListPage 파일럿 마이그레이션 세션 컨텍스트
# UniversalListPage 마이그레이션 세션 컨텍스트
## 세션 요약 (2026-01-14)
## 🎉 프로젝트 완료 (2026-01-15)
### 완료된 작업
- [x] UniversalListPage 컴포넌트 생성 (Phase 1, 2)
- [x] 카드관리 (HR) 파일럿 마이그레이션 - 기본 케이스
- [x] 게시판목록 (BoardList) 파일럿 마이그레이션 - 동적 탭 (fetchTabs)
- [x] 발주관리 (OrderManagement) 파일럿 마이그레이션 - ScheduleCalendar (beforeTableContent)
- [x] 3개 파일럿 페이지 기능 검증 완료
### 최종 결과
| 레벨 | 개수 | 상태 | 처리 방식 |
|-----|-----|------|----------|
| Level 1 (기본) | 15개 | ✅ 완료 | UniversalListPage 마이그레이션 |
| Level 2 (필터) | 30개 | ✅ 완료 | UniversalListPage 마이그레이션 |
| Level 3~5 (복잡) | 10개 | ✅ 분석 완료 | 마이그레이션 제외 (현상 유지) |
| **합계** | **55개** | ✅ | 45개 마이그레이션 + 10개 현상 유지 |
### 프로젝트 목표
59개 리스트 페이지를 하나의 `UniversalListPage` 컴포넌트로 통합
- **핵심 원칙**: 기존 기능 100% 유지, 테이블 영역만 공통화
- **접근 방식**: Config 기반 커스터마이징
### Level 3~5 마이그레이션 제외 사유
#### 전자결재 도메인 (3개) - 제외
| 파일 | 제외 사유 |
|------|----------|
| DraftBox | DocumentDetailModal (커스텀 인터페이스), 선택 기반 동적 헤더, 상신/삭제 액션 |
| ApprovalBox | DocumentDetailModal, 승인/반려/재상신 다이얼로그, 4개 탭 |
| ReferenceBox | DocumentDetailModal, 열람/미열람 처리 다이얼로그, 3개 탭 |
#### HR 도메인 (5개) - 제외
| 파일 | 제외 사유 |
|------|----------|
| SalaryManagement | SalaryDetailDialog, 급여 상태 변경, 선택 기반 동적 버튼 |
| AttendanceManagement | 9개 탭, 2개 다이얼로그, extraFilters, 클라이언트 필터링 |
| VacationManagement | 3개 탭(탭별 상이한 컬럼), 2개 다이얼로그 + 2개 AlertDialog |
| EmployeeManagement | 4개 탭, FieldSettingsDialog + UserInviteDialog + DateRangeSelector |
| CardManagement | 3개 탭, AlertDialog, 클라이언트 필터링 |
#### 게시판 도메인 (2개) - 제외
| 파일 | 제외 사유 |
|------|----------|
| BoardManagement | AlertDialog, 선택 기반 수정/삭제, 클라이언트 페이지네이션 |
| BoardList | API 기반 동적 탭 (getBoards), "나의 게시글" 특수 탭, 서버사이드 페이지네이션 |
### 핵심 결론
> **UniversalListPage는 Level 1~2 (단순~중간 복잡도) 컴포넌트에 적합**
> **Level 3~5 (복잡) 컴포넌트는 IntegratedListTemplateV2 직접 사용이 더 효율적**
---
## 파일럿 검증 결과
## 🎯 핵심 목적 (절대 잊지 말 것!)
**이 공통화 작업의 근본적인 목적은 모바일에서 필터를 바텀시트로 보여주기 위함이다.**
| 파일럿 | 특이 케이스 | 테스트 URL | 상태 |
|--------|------------|-----------|------|
| **카드관리** | 기본 케이스 | `/ko/hr/card-management-test` | ✅ 완료 |
| **게시판목록** | 동적 탭 (fetchTabs) | `/ko/board-test` | ✅ 완료 |
| **발주관리** | ScheduleCalendar | `/ko/construction/order/order-management-test` | ✅ 완료 |
### 검증된 기능
1. ✅ 테이블 렌더링 (20개 행, 페이지네이션)
2. ✅ 행 선택 (체크박스, 카운터, 삭제 버튼)
3. ✅ 수정/삭제 버튼 (선택 시 표시, 페이지 이동)
4. ✅ 클라이언트 사이드 필터링 (customFilterFn, customSortFn)
5. ✅ 커스텀 콘텐츠 슬롯 (beforeTableContent, tableHeaderActions)
6. ✅ 동적 탭 (fetchTabs API 호출)
7. ✅ 필터 설정 (filterConfig 기반 다중 필터)
- `filterConfig` 사용 → 자동으로 PC/모바일 분기
- PC (1280px+): 인라인 필터
- 모바일 (~1279px): 바텀시트 필터 (MobileFilter)
- **새로운 모바일 필터 기능 만들지 말 것!**
---
## 생성/수정된 파일
## 완료된 마이그레이션 목록
### UniversalListPage 핵심 파일
```
src/components/templates/UniversalListPage/
├── index.tsx # 메인 컴포넌트
└── types.ts # 타입 정의
```
### Level 1 (15/15) ✅
| # | 파일 | 완료일 |
|---|------|--------|
| 1 | WorkOrderList | 2026-01-14 |
| 2 | WorkResultList | 2026-01-14 |
| 3 | ShipmentList | 2026-01-14 |
| 4 | StockStatusList | 2026-01-14 |
| 5 | ReceivingList | 2026-01-14 |
| 6 | InspectionList | 2026-01-14 |
| 7 | ItemListClient | 2026-01-14 |
| 8 | PaymentHistoryClient | 2026-01-14 |
| 9 | PopupList | 2026-01-14 |
| 10 | EventList | 2026-01-14 |
| 11 | InquiryList | 2026-01-14 |
| 12 | NoticeList | 2026-01-14 |
| 13 | QuoteManagementClient | 2026-01-14 |
| 14 | ProcessListClient | 2026-01-14 |
| 15 | AccountManagement | 2026-01-14 |
### 파일럿 Unified 컴포넌트
```
src/components/business/hr/CardManagement/CardManagementUnified.tsx
src/components/business/construction/order-management/OrderManagementUnified.tsx
src/components/board/BoardList/BoardListUnified.tsx
```
### Level 2 - 건설 도메인 (17/17) ✅
| # | 파일 | 완료일 |
|---|------|--------|
| 1-5 | Estimate, Bidding, SiteBriefing, Contract, Partner | 2026-01-14 |
| 6-11 | HandoverReport, WorkerStatus, Utility, ProgressBilling, StructureReview, SiteManagement | 2026-01-15 |
| 12-17 | Pricing, IssueManagement, OrderManagement, ConstructionManagement, LaborManagement, ItemManagement | 2026-01-15 |
### 테스트 페이지
```
src/app/[locale]/(protected)/hr/card-management-test/page.tsx
src/app/[locale]/(protected)/board-test/page.tsx
src/app/[locale]/(protected)/construction/order/order-management-test/page.tsx
```
### Level 2 - 회계 도메인 (13/13) ✅
| # | 파일 | 완료일 |
|---|------|--------|
| 1-5 | VendorManagement, SalesManagement, PurchaseManagement, DepositManagement, WithdrawalManagement | 2026-01-15 |
| 6-10 | BillManagement, BadDebtCollection, BankTransactionInquiry, CardTransactionInquiry, VendorLedger | 2026-01-15 |
| 11-13 | ExpectedExpenseManagement, BillManagementClient, VendorManagementClient | 2026-01-15 |
---
## 핵심 수정 사항
### types.ts에 추가된 Config 옵션
```typescript
// 클라이언트 사이드 필터링 확장
customFilterFn?: (items: T[], filterValues: Record<string, string | string[]>) => T[];
customSortFn?: (items: T[], filterValues: Record<string, string | string[]>) => T[];
// 테이블 헤더 액션
tableHeaderActions?: ReactNode;
```
### index.tsx 주요 수정
1. **탭 기본값 수정**: `activeTab` 기본값을 `'default'`로 변경 (IntegratedListTemplateV2와 일치)
2. **빈 탭 배열 처리**: `tabs={computedTabs.length > 0 ? computedTabs : undefined}`
3. **customFilterFn/customSortFn 적용**: filteredData useMemo에서 사용
---
## 다음 세션 TODO
### Phase 4: 본격 마이그레이션
- [ ] 나머지 56개 페이지 우선순위 정리
- [ ] 복잡도별 그룹핑 (기본 / 필터 복잡 / 커스텀 슬롯)
- [ ] 그룹별 순차 마이그레이션
### 고려사항
- 기존 페이지와 테스트 페이지 비교 검증 후 교체
- 마이그레이션 완료 후 기존 컴포넌트 정리
---
## 참고 사항
### UniversalListConfig 주요 속성
```typescript
interface UniversalListConfig<T> {
title: string;
basePath: string;
idField: keyof T | ((item: T) => string);
actions: ListActions<T>;
columns: TableColumn[];
// 렌더링
renderTableRow: (item, index, globalIndex, handlers) => ReactNode;
renderMobileCard: (item, index, globalIndex, handlers) => ReactNode;
// 필터링
filterConfig?: FilterFieldConfig[];
clientSideFiltering?: boolean;
customFilterFn?: (items, filterValues) => T[];
// 탭
tabs?: TabOption[];
fetchTabs?: () => Promise<TabOption[]>;
// 슬롯
beforeTableContent?: ReactNode;
tableHeaderActions?: ReactNode;
headerActions?: (params) => ReactNode;
}
```
### 기존 vs 테스트 URL 비교
| 기능 | 기존 | 테스트 (UniversalListPage) |
|------|------|---------------------------|
| 카드관리 | `/ko/hr/card-management` | `/ko/hr/card-management-test` |
| 게시판 | `/ko/board` | `/ko/board-test` |
| 발주관리 | `/ko/construction/order/order-management` | `/ko/construction/order/order-management-test` |
## 참고 문서
- `claudedocs/[IMPL-2026-01-14] universal-list-component-checklist.md` - 메인 체크리스트

View File

@@ -0,0 +1,96 @@
# 레이아웃 구조 변경 계획
> **상태**: 📋 대기 (기능 검수 완료 후 진행)
> **작성일**: 2026-01-16
> **적용 대상**: IntegratedListTemplateV2.tsx (55개 페이지 일괄 적용)
---
## 현재 구조
```
1. 타이틀
2. 달력 / 버튼들 (등록 버튼 여기)
3. 통계 카드
4. 검색창 (Card로 감싸짐)
5. 테이블 Card
└─ 탭 버튼들 / 필터 / 삭제 버튼
└─ 테이블
```
---
## 변경 후 구조
```
1. 타이틀
2. 달력 / 달력버튼 / 검색창 (한 줄)
3. 카드섹션 (한 줄, 줄넘김 없음)
4. [탭 버튼들] ─────────────── [등록] [CSV] 버튼들 ← Card 밖
5. 테이블 Card
├─ 총 N건 / 선택건 / 필터
└─ 테이블
```
---
## 시각화
```
┌─ 페이지 ─────────────────────────────────────────────────┐
│ 휴가관리 │
│ 직원들의 휴가 현황을 관리합니다 │
├──────────────────────────────────────────────────────────┤
│ [📅 2025-12-01] ~ [📅 2025-12-31] [당월][전월] [🔍검색] │
├──────────────────────────────────────────────────────────┤
│ [승인대기 1명] [연차 4명] [경조사 0명] [사용률 4.3%] │ ← 카드 (줄넘김X)
├──────────────────────────────────────────────────────────┤
│ [사용현황 4] [부여현황 2] [신청현황 3] [등록] [CSV] │ ← Card 밖
├──────────────────────────────────────────────────────────┤
│ ┌─ 테이블 Card ────────────────────────────────────────┐ │
│ │ 총 55건 | 3개 선택됨 [필터1] [필터2] │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ □ | 번호 | 부서 | 이름 | ... │ │
│ │ □ | 1 | 개발 | 홍길동 | ... │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
---
## 주요 변경점
| 항목 | 현재 | 변경 후 |
|------|------|---------|
| 검색창 | Card로 감싸짐, 별도 영역 | 달력 옆 한 줄에 배치 |
| 카드섹션 | flex-wrap (줄넘김) | flex-nowrap + overflow-x-auto |
| 탭 버튼 | 테이블 Card 내부 | 테이블 Card 위 (밖) |
| 등록/액션 버튼 | 헤더 영역 | 탭 버튼 오른쪽 |
| 총 N건/선택건 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 |
| 필터 | 탭과 같은 줄 | 테이블 Card 내부 첫 줄 |
---
## 수정 대상 파일
1. **IntegratedListTemplateV2.tsx** - 전체 레이아웃 구조 변경
2. **UniversalListPage/index.tsx** - prop 전달 방식 조정 (필요시)
---
## 체크리스트
- [ ] 검색창 위치 이동 (달력 옆)
- [ ] 카드섹션 줄넘김 방지 (flex-nowrap)
- [ ] 탭 버튼 테이블 Card 밖으로 이동
- [ ] 등록/액션 버튼 탭 옆으로 이동
- [ ] 총 N건/선택건/필터 테이블 Card 내부로 이동
- [ ] PC/모바일 반응형 확인
- [ ] 55개 페이지 일괄 테스트
---
## 진행 조건
**기능 검수 완료 후 진행**
- 현재 화면과 비교 검수가 필요하므로 레이아웃 변경은 기능 검수 이후에 진행

View File

@@ -0,0 +1,200 @@
# UniversalListPage 마이그레이션 검수 체크리스트
> **검수일**: 2026-01-15
> **검수 방법**: Chrome DevTools MCP를 사용한 페이지별 UI 검증
> **총 대상**: 63개 페이지
---
## 검수 목표
> **기존 페이지 기능이 UniversalListPage 통합 후에도 정상 작동하는가**
---
## 검수 기준
### 자동 검수 항목 (Claude)
| 항목 | 설명 |
|------|------|
| 데이터 표출 | 테이블/카드에 데이터가 정상적으로 표시되는가 |
| 검색 기능 | 검색어 입력 시 필터링이 작동하는가 |
| 탭 전환 | 탭 클릭 시 데이터가 올바르게 필터링되는가 |
| 커스텀 버튼 | 페이지별 고유 버튼(등록/수정/삭제 등)이 표시되는가 |
| 필터 동작 | 날짜/상태/유형 등 필터가 작동하는가 |
| 콘솔 에러 | JavaScript 에러가 없는가 |
| 모바일 뷰 | 모바일 사이즈에서 카드 형태로 표시되는가 |
### 수동 검수 항목 (사용자)
| 항목 | 설명 |
|------|------|
| 모바일 바텀시트 | 모바일에서 필터 버튼 클릭 시 바텀시트가 정상 작동하는가 |
---
## 검수 상태 범례
- ✅ 통과
- ❌ 실패
- ⚠️ 부분 통과 (이슈 있음)
- 🔍 검수 중
- ⏳ 미확인
---
## Level 1 검수 (15개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 작업지시관리 | `/ko/production/work-orders` | | | | | | ⏳ |
| 2 | 작업실적조회 | `/ko/production/work-results` | | | | | | ⏳ |
| 3 | 출하관리 | `/ko/outbound/shipment-management` | | | | | | ⏳ |
| 4 | 재고현황 | `/ko/material/stock-status` | | | | | | ⏳ |
| 5 | 입고관리 | `/ko/material/receiving` | | | | | | ⏳ |
| 6 | 검사관리 | `/ko/quality/inspection-management` | | | | | | ⏳ |
| 7 | 품목관리 | `/ko/items` | | | | | | ⏳ |
| 8 | 결제내역 | `/ko/payment-history` | | | | | | ⏳ |
| 9 | 팝업관리 | `/ko/settings/popup-management` | | | | | | ⏳ |
| 10 | 이벤트관리 | `/ko/customer-center/events` | | | | | | ⏳ |
| 11 | 문의관리 | `/ko/customer-center/inquiries` | | | | | | ⏳ |
| 12 | 공지사항 | `/ko/customer-center/notices` | | | | | | ⏳ |
| 13 | 견적관리 | `/ko/quotes` | | | | | | ⏳ |
| 14 | 공정관리 | `/ko/process-management` | | | | | | ⏳ |
| 15 | 계좌관리 | `/ko/settings/account-management` | | | | | | ⏳ |
---
## Level 2 건설 도메인 검수 (17개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 견적관리 | `/ko/construction/estimates` | | | | | | ⏳ |
| 2 | 입찰관리 | `/ko/construction/bidding` | | | | | | ⏳ |
| 3 | 현장설명회 | `/ko/construction/site-briefings` | | | | | | ⏳ |
| 4 | 계약관리 | `/ko/construction/contract` | | | | | | ⏳ |
| 5 | 협력업체 | `/ko/construction/partners` | | | | | | ⏳ |
| 6 | 인수인계보고서 | `/ko/construction/handover-report` | | | | | | ⏳ |
| 7 | 작업인력현황 | `/ko/construction/worker-status` | | | | | | ⏳ |
| 8 | 공과관리 | `/ko/construction/utility-management` | | | | | | ⏳ |
| 9 | 기성청구관리 | `/ko/construction/progress-billing` | | | | | | ⏳ |
| 10 | 구조검토 | `/ko/construction/structure-review` | | | | | | ⏳ |
| 11 | 현장관리 | `/ko/construction/site-management` | | | | | | ⏳ |
| 12 | 단가관리 | `/ko/construction/pricing` | | | | | | ⏳ |
| 13 | 이슈관리 | `/ko/construction/issue-management` | | | | | | ⏳ |
| 14 | 발주관리 | `/ko/construction/order/order-management` | | | | | | ⏳ |
| 15 | 시공관리 | `/ko/construction/management` | | | | | | ⏳ |
| 16 | 노임관리 | `/ko/construction/labor-management` | | | | | | ⏳ |
| 17 | 품목관리(건설) | `/ko/construction/order/base-info/items` | | | | | | ⏳ |
---
## Level 2 회계 도메인 검수 (11개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 거래처관리 | `/ko/accounting/vendor-management` | | | | | | ⏳ |
| 2 | 매출관리 | `/ko/accounting/sales-management` | | | | | | ⏳ |
| 3 | 매입관리 | `/ko/accounting/purchase-management` | | | | | | ⏳ |
| 4 | 입금관리 | `/ko/accounting/deposit-management` | | | | | | ⏳ |
| 5 | 출금관리 | `/ko/accounting/withdrawal-management` | | | | | | ⏳ |
| 6 | 어음관리 | `/ko/accounting/bill-management` | | | | | | ⏳ |
| 7 | 악성채권추심 | `/ko/accounting/bad-debt-collection` | | | | | | ⏳ |
| 8 | 입출금계좌조회 | `/ko/accounting/bank-transaction-inquiry` | | | | | | ⏳ |
| 9 | 카드내역조회 | `/ko/accounting/card-transaction-inquiry` | | | | | | ⏳ |
| 10 | 거래처원장 | `/ko/accounting/vendor-ledger` | | | | | | ⏳ |
| 11 | 지출예상내역서 | `/ko/accounting/expected-expense-management` | | | | | | ⏳ |
---
## Level 2 영업 도메인 검수 (4개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 수주관리 | `/ko/sales/order-management-sales` | | | | | | ⏳ |
| 2 | 생산발주 | `/ko/sales/order-management-sales/production-orders` | | | | | | ⏳ |
| 3 | 거래처관리(영업) | `/ko/sales/client-management-sales-admin` | | | | | | ⏳ |
| 4 | 단가관리(영업) | `/ko/sales/pricing-management` | | | | | | ⏳ |
---
## Level 3~5 복잡 페이지 검수 (10개)
### 게시판 도메인 (3개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 게시판관리 | `/ko/board/management` | | | | | | ⏳ |
| 2 | 게시판목록 | `/ko/board/list` | | | | | | ⏳ |
| 3 | 동적게시판 | `/ko/boards/[boardCode]` | | | | | | ⏳ |
### 전자결재 도메인 (3개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 기안함 | `/ko/approval/draft-box` | | | | | | ⏳ |
| 2 | 결재함 | `/ko/approval/approval-box` | | | | | | ⏳ |
| 3 | 참조함 | `/ko/approval/reference-box` | | | | | | ⏳ |
### HR 도메인 (5개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 카드관리 | `/ko/hr/card-management` | | | | | | ⏳ |
| 2 | 급여관리 | `/ko/hr/salary-management` | | | | | | ⏳ |
| 3 | 근태관리 | `/ko/hr/attendance-management` | | | | | | ⏳ |
| 4 | 사원관리 | `/ko/hr/employee-management` | | | | | | ⏳ |
| 5 | 휴가관리 | `/ko/hr/vacation-management` | | | | | | ⏳ |
---
## 추가 발견 페이지 검수 (2개)
| # | 페이지 | URL | 데이터 | 검색 | 필터 | 에러 | 모바일 | 상태 |
|---|--------|-----|--------|------|------|------|--------|------|
| 1 | 권한관리 | `/ko/settings/permissions` | | | | | | ⏳ |
| 2 | 결제내역(별도) | `/ko/settings/payment-history` | | | | | | ⏳ |
---
## 검수 결과 요약
| 레벨 | 총 개수 | 통과 | 실패 | 미확인 |
|------|--------|------|------|--------|
| Level 1 | 15 | 0 | 0 | 15 |
| Level 2 건설 | 17 | 0 | 0 | 17 |
| Level 2 회계 | 11 | 0 | 0 | 11 |
| Level 2 영업 | 4 | 0 | 0 | 4 |
| Level 3~5 | 11 | 0 | 0 | 11 |
| 추가 발견 | 2 | 0 | 0 | 2 |
| **합계** | **60** | **0** | **0** | **60** |
> **참고**: HR/전자결재/게시판 일부는 UniversalListPage가 아닌 별도 구조 사용 가능
---
## 발견된 이슈
### Critical (즉시 수정 필요)
_없음_
### Major (수정 권장)
_없음_
### Minor (개선 권장)
_없음_
---
## 수동 검수 필요 항목
| 항목 | 상태 | 비고 |
|------|------|------|
| 모바일 바텀시트 필터 동작 | ⏳ | 사용자 수동 확인 필요 |
---
## 변경 이력
| 일시 | 작업 내용 |
|------|----------|
| 2026-01-15 | 검수 체크리스트 문서 생성 |
| 2026-01-15 | 검수 기준 업데이트 (데이터/검색/필터/모바일 세분화) |
| 2026-01-15 | 추가 발견 페이지 5개 포함 (총 63개 → 60개 검수 대상) |
| 2026-01-15 | URL 오류 수정 (결제내역, 품목관리-건설) |

View File

@@ -0,0 +1,313 @@
# UniversalListPage 검수 패턴 가이드
> **목적**: 55개 페이지 검수 시 발생하는 공통 에러 패턴과 해결책 정리
> **작성일**: 2026-01-16
> **기준**: 지금까지 검수 중 발견된 13개 이상의 에러 분석
---
## 검수 항목 체크리스트
| 항목 | 아이콘 | 설명 |
|------|--------|------|
| 검색 | 🔍 | 검색창 입력 시 필터링 동작 |
| 탭 | 📑 | 탭 버튼 클릭 시 데이터 전환 |
| 필터 | 🎛️ | 필터 선택/적용/초기화 동작 |
| 체크박스 | ☑️ | 테이블 행 체크박스 선택 동작 |
| 상세 | 👁️ | 테이블 로우 클릭 → 상세페이지/모달 이동 |
| 등록 | | 등록 버튼 클릭 → 등록페이지 이동 |
---
## 🚨 공통 에러 패턴 및 해결책
### 1. `headerActions.call is not a function`
**증상**: 페이지 로드 시 에러 발생, 콘솔에 에러 메시지 표시
**원인**: `headerActions`가 ReactNode로 정의되어 있음 (함수가 아님)
**잘못된 코드**:
```typescript
// ❌ ReactNode로 정의
const headerActions = (
<Button onClick={() => console.log('click')}>
버튼
</Button>
);
```
**올바른 코드**:
```typescript
// ✅ 함수로 정의
const headerActions = () => (
<Button onClick={() => console.log('click')}>
버튼
</Button>
);
```
---
### 2. 탭 클릭해도 데이터가 변경되지 않음
**증상**: 탭 버튼 클릭은 되지만 테이블 데이터가 그대로 유지됨
**원인 A (클라이언트 사이드 필터링)**:
- `filteredData`(이미 필터링된 데이터)를 `initialData`에 전달
- UniversalListPage 내부 상태가 외부 데이터 변경을 감지 못함
**해결책 A**:
```typescript
// ✅ 전체 데이터 전달 + tabFilter 함수 추가
const config = {
// ...
clientSideFiltering: true,
tabFilter: (item, activeTab) => {
if (activeTab === 'all') return true;
return item.type === activeTab;
},
searchFilter: (item, search) => {
return item.name.toLowerCase().includes(search.toLowerCase());
},
};
<UniversalListPage
config={config}
initialData={data} // ✅ 전체 데이터
onTabChange={setActiveTab}
/>
```
**원인 B (서버 사이드 필터링)**:
- `onTabChange` prop이 누락됨
**해결책 B**:
```typescript
// ✅ onTabChange prop 추가
<UniversalListPage
config={config}
initialData={items}
onTabChange={handleTypeChange} // ✅ 추가
externalPagination={{...}}
/>
```
---
### 3. 승인/거절 팝업에 선택 건수가 0으로 표시
**증상**: 체크박스 선택 후 버튼 클릭하면 팝업에 "0건" 표시
**원인**: `headerActions`에서 받는 `selected`와 컴포넌트 내부 `selectedItems` 상태가 동기화되지 않음
**잘못된 코드**:
```typescript
// ❌ selected를 내부 상태로 복사하지 않음
const handleApproveClick = useCallback(() => {
setApproveDialogOpen(true);
}, []);
// headerActions에서
<Button onClick={() => handleApproveClick()}>승인</Button>
```
**올바른 코드**:
```typescript
// ✅ selected를 받아서 내부 상태로 복사
const handleApproveClick = useCallback((selected: Set<string>) => {
setSelectedItems(selected); // 복사!
setApproveDialogOpen(true);
}, []);
// headerActions에서
headerActions: ({ selected }) => (
<Button onClick={() => handleApproveClick(selected)}>승인</Button>
)
```
---
### 4. `externalSelection.onToggleSelection is not a function`
**증상**: 체크박스 클릭 시 에러 발생
**원인**: `externalSelection` 프로퍼티 이름이 타입과 불일치
**잘못된 코드**:
```typescript
// ❌ 잘못된 프로퍼티 이름
externalSelection={{
selectedItems,
setSelectedItems, // ❌
toggleSelection, // ❌
toggleSelectAll, // ❌
}}
```
**올바른 코드**:
```typescript
// ✅ 올바른 프로퍼티 이름
externalSelection={{
selectedItems,
onToggleSelection: toggleSelection, // ✅
onToggleSelectAll: toggleSelectAll, // ✅
getItemId: (item) => item.id,
}}
```
---
### 5. `externalPagination` NaN 또는 globalIndex 오류
**증상**: 번호 컬럼에 NaN 표시, 페이지네이션 동작 안함
**원인**: `externalPagination` 프로퍼티 형태 불일치
**올바른 형태**:
```typescript
externalPagination={{
currentPage: pagination.currentPage,
totalPages: pagination.totalPages,
totalItems: pagination.totalItems,
itemsPerPage: pagination.perPage, // ✅ itemsPerPage (perPage 아님)
onPageChange: handlePageChange,
}}
```
---
### 6. 프리셋 버튼 (당월/전월/오늘) 미표시
**증상**: DateRangeSelector는 표시되지만 프리셋 버튼 없음
**원인**: `showPresets: false` 설정
**해결책**:
```typescript
dateRangeSelector: {
enabled: true,
showPresets: true, // ✅ true로 설정
startDate,
endDate,
onStartDateChange,
onEndDateChange,
},
```
---
### 7. 탭 카운트가 모두 동일하게 표시
**증상**: 모든 탭에 같은 숫자가 표시됨
**원인**: `config.tabs` 변경 시 UniversalListPage 내부 상태가 업데이트되지 않음
**해결책** (이미 UniversalListPage에 적용됨):
```typescript
// UniversalListPage/index.tsx에서
useEffect(() => {
if (config.tabs) {
setTabs(config.tabs);
}
}, [config.tabs]);
```
---
## 📋 검수 순서 권장
### Step 1: 페이지 로드 확인
- [ ] 에러 없이 페이지 로드되는가?
- [ ] 콘솔에 에러 메시지 없는가?
### Step 2: 기본 UI 확인
- [ ] 테이블/카드 목록 정상 표시되는가?
- [ ] 통계 카드 (있는 경우) 정상 표시되는가?
- [ ] 탭 버튼 (있는 경우) 정상 표시되는가?
### Step 3: 탭 기능 (있는 경우)
- [ ] 탭 클릭 시 데이터가 변경되는가?
- [ ] 탭별 건수가 정확하게 표시되는가?
- [ ] 탭 변경 후 검색/필터가 유지되는가?
### Step 4: 검색 기능
- [ ] 검색창에 입력 시 필터링되는가?
- [ ] 검색어 삭제 시 전체 목록 표시되는가?
### Step 5: 필터 기능 (있는 경우)
- [ ] PC에서 필터 선택 시 데이터 필터링되는가?
- [ ] 모바일에서 필터 바텀시트 열리는가?
- [ ] 필터 적용/초기화 정상 동작하는가?
### Step 6: 체크박스 선택
- [ ] 개별 체크박스 선택/해제 되는가?
- [ ] 전체 선택 체크박스 동작하는가?
- [ ] 선택 건수가 정확히 표시되는가?
### Step 7: 상세 이동
- [ ] 행 클릭 또는 상세 버튼 클릭 시 이동하는가?
- [ ] URL 파라미터 올바르게 전달되는가?
### Step 8: 등록 버튼 (있는 경우)
- [ ] 등록 버튼 표시되는가?
- [ ] 클릭 시 등록 페이지로 이동하는가?
### Step 9: 커스텀 액션 (승인/거절/삭제 등)
- [ ] 버튼이 올바른 위치에 표시되는가?
- [ ] 선택된 항목 수가 정확히 팝업에 표시되는가?
- [ ] 액션 실행 후 데이터가 갱신되는가?
---
## 🔧 데이터 흐름 패턴
### 패턴 A: 클라이언트 사이드 필터링
```
initialData={전체데이터}
config.tabFilter() → 탭 필터링
config.searchFilter() → 검색 필터링
내부 페이지네이션 → displayData
```
**적합한 경우**:
- 데이터량 적음 (500개 이하)
- 전체 데이터를 한번에 로드 가능
### 패턴 B: 서버 사이드 필터링
```
initialData={API로 받은 데이터}
onTabChange → 외부 상태 변경 → API 재호출
onSearchChange → 외부 상태 변경 → API 재호출
externalPagination으로 페이지 제어
```
**적합한 경우**:
- 데이터량 많음 (1000개 이상)
- 페이지네이션된 API 사용
---
## 발견된 에러 통계
| 에러 유형 | 발생 횟수 | 패턴 |
|----------|----------|------|
| headerActions 함수 아님 | 2회 | 거래처관리(영업), 단가관리(판매) |
| 탭 데이터 미갱신 | 2회 | 단가관리(판매), 품목관리 |
| 선택 건수 0 표시 | 1회 | 휴가관리 |
| externalSelection 형태 불일치 | 1회 | 휴가관리 |
| showPresets 누락 | 2회 | 근태관리, 사원관리 |
| 탭 카운트 동기화 | 1회 | 휴가관리 |
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-01-16 | 문서 초안 작성 (13개 에러 패턴 분석) |

View File

@@ -0,0 +1,165 @@
# 모바일 핀치 줌(Pinch Zoom) 이슈 해결 가이드
> **작성일**: 2026-01-15
> **상태**: 해결 완료
> **적용 범위**: iOS Safari, Android Chrome
---
## 1. 문제 현상
### 1-1. 초기 증상
- 모바일에서 핀치 줌(손가락 확대)이 **특정 화면에서만** 동작
- 확대 시 **아래에서 회색/어두운 영역**이 올라와 화면을 가림
- Android / iOS 모두 동일한 현상
### 1-2. 영향 범위
| 화면 | 줌 가능 | 회색 영역 |
|------|---------|----------|
| 로그인 페이지 | ✅ 정상 | ❌ 없음 |
| 인증된 내부 페이지 | ❌ 불가 → ✅ 수정 후 가능 | ✅ 발생 → ❌ 수정 후 해결 |
---
## 2. 원인 분석
### 2-1. 핀치 줌 차단 원인
**파일**: `src/layouts/AuthenticatedLayout.tsx`
```tsx
// 문제 코드 - touch-pan-y가 핀치 줌 차단
<main className="... touch-pan-y" style={{ WebkitOverflowScrolling: 'touch' }}>
```
| touch-action 값 | 세로 스크롤 | 핀치 줌 |
|----------------|------------|--------|
| `pan-y` | ✅ | ❌ 차단 |
| `pan-y pinch-zoom` | ✅ | ✅ |
| `manipulation` | ✅ | ❌ 더블탭만 |
### 2-2. 회색 영역 발생 원인
**원인 1**: `body`에 추가된 safe-area 패딩
```css
/* globals.css - 문제 코드 */
body {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
```
- 확대 시 body가 확장되면서 패딩 영역이 화면에 노출
**원인 2**: 모바일 레이아웃 wrapper에 배경색 미지정
```tsx
// 문제 코드 - 배경색 없음
<div className="flex flex-col overflow-hidden" style={{ height: 'var(--app-height)' }}>
```
- 배경색이 없어서 확대 시 뒤에 있는 요소(어두운 배경)가 투과되어 보임
**원인 3**: `overflow-hidden`으로 인한 콘텐츠 클리핑
- 고정 높이 + overflow-hidden = 확대 시 콘텐츠가 잘림
---
## 3. 해결 방법
### 3-1. 핀치 줌 활성화
**파일**: `src/layouts/AuthenticatedLayout.tsx` (Line 615)
```tsx
// 변경 전
<main className="flex-1 overflow-y-auto px-3 overscroll-contain touch-pan-y"
style={{ WebkitOverflowScrolling: 'touch' }}>
// 변경 후
<main className="flex-1 overflow-y-auto px-3 overscroll-contain"
style={{ WebkitOverflowScrolling: 'touch', touchAction: 'pan-y pinch-zoom' }}>
```
### 3-2. body 패딩 제거
**파일**: `src/app/globals.css`
```css
/* 변경 전 */
body {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* 변경 후 - 해당 코드 제거 */
/* safe-area 변수는 유지, body 패딩만 제거 */
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-right: env(safe-area-inset-right, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-inset-left: env(safe-area-inset-left, 0px);
}
```
### 3-3. 모바일 레이아웃 배경색 및 높이 수정
**파일**: `src/layouts/AuthenticatedLayout.tsx` (Line 370)
```tsx
// 변경 전
<div className="flex flex-col overflow-hidden" style={{ height: 'var(--app-height)' }}>
// 변경 후
<div className="flex flex-col bg-background min-h-screen" style={{ height: 'var(--app-height)' }}>
```
| 변경 항목 | 효과 |
|----------|------|
| `bg-background` | 배경색 명시적 지정 → 어두운 영역 가림 |
| `min-h-screen` | 최소 높이 보장 → 확대 시에도 배경 커버 |
| `overflow-hidden` 제거 | 확대 시 콘텐츠 클리핑 방지 |
---
## 4. Viewport 설정 (참고)
**파일**: `src/app/[locale]/layout.tsx`
```tsx
// 현재 설정 - 줌 허용 + iOS safe-area 지원
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
minimumScale: 1, // 최소 100%
maximumScale: 5, // 최대 500%까지 확대 가능
userScalable: true, // 손가락 확대 허용
viewportFit: 'cover', // 아이폰 노치/다이나믹 아일랜드/하단 홈바 영역 커버
};
```
---
## 5. 최종 변경 파일 목록
| 파일 | 변경 내용 |
|------|----------|
| `src/layouts/AuthenticatedLayout.tsx` | touch-action 수정, 배경색/높이 추가 |
| `src/app/globals.css` | body padding-bottom 제거 |
| `src/app/[locale]/layout.tsx` | viewport 설정 (이전에 적용됨) |
---
## 6. 테스트 체크리스트
- [x] iOS Safari 핀치 줌 동작
- [x] Android Chrome 핀치 줌 동작
- [x] 확대 시 회색 영역 미노출
- [x] 로그인 페이지 정상 동작
- [x] 내부 페이지(AuthenticatedLayout) 정상 동작
- [x] 세로 스크롤 정상 동작
---
## 7. 관련 문서
- `[REF] mobile-zoom-prevention-guide.md` - 줌 방지가 필요할 때 적용 가이드
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-01-15 | 문서 작성, 이슈 해결 완료 |

View File

@@ -0,0 +1,101 @@
# 모바일 확대 방지 설정 가이드
> **목적**: 모바일 웹에서 손가락 핀치/더블탭 확대 방지
> **상태**: 미적용 (사용자 접근성 우선)
> **적용 시점**: 필요 시 아래 설정 적용
---
## 1. Viewport 설정 (Next.js 15)
**파일**: `src/app/[locale]/layout.tsx`
### 1-1. import 추가
```tsx
import type { Metadata, Viewport } from "next";
```
### 1-2. viewport export 추가 (metadata 아래)
```tsx
// 📱 Viewport 설정 - 모바일 확대 방지 + 100% 스케일 고정
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
minimumScale: 1,
maximumScale: 1,
userScalable: false, // 손가락 확대 방지 (Android + iOS)
viewportFit: 'cover', // 아이폰 노치/다이나믹 아일랜드 대응
};
```
---
## 2. iOS Safari 자동 확대 방지 (CSS)
**파일**: `src/app/globals.css`
iOS Safari는 `font-size`가 16px 미만인 input에 포커스하면 자동으로 확대함.
viewport 설정만으로는 방지 안 됨.
### 2-1. @variant 아래에 추가
```css
/* 📱 iOS Safari 자동 확대 방지
- iOS는 font-size 16px 미만 input 포커스 시 자동 확대
- 16px 이상으로 설정하면 확대 방지됨
*/
input,
select,
textarea {
font-size: 16px !important;
}
/* 터치 동작 최적화 - 더블탭 확대 방지 */
html {
touch-action: manipulation;
}
```
---
## 3. 설정별 효과
| 설정 | 효과 | 적용 위치 |
|------|------|-----------|
| `userScalable: false` | 핀치 확대 방지 | layout.tsx |
| `maximumScale: 1` | 최대 100% 고정 | layout.tsx |
| `minimumScale: 1` | 최소 100% 고정 | layout.tsx |
| `viewportFit: 'cover'` | 노치 영역 커버 | layout.tsx |
| `font-size: 16px` | iOS input 확대 방지 | globals.css |
| `touch-action: manipulation` | 더블탭 확대 방지 | globals.css |
---
## 4. 적용 여부 결정 기준
### 적용 권장 상황
- 키오스크/POS 앱처럼 고정 레이아웃 필수
- 특정 인터랙션에서 확대가 UX를 방해하는 경우
### 미적용 권장 상황 (현재)
- 사용자 연령대가 높아 확대 기능 필요
- 접근성(A11y) 가이드라인 준수 필요
- 텍스트가 작은 영역이 있는 경우
---
## 5. 참고사항
- **iOS Safari**: viewport 설정만으로는 input 포커스 확대 방지 안 됨, CSS 필수
- **Android Chrome**: viewport 설정만으로 대부분 방지됨
- **Next.js 15**: `viewport`는 별도 export로 분리 (metadata와 별개)
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-01-15 | 문서 작성, 설정 롤백 (접근성 우선) |

View File

@@ -339,6 +339,16 @@ Safari에서 `SameSite=Strict` + `Secure` 조합이 localhost에서 쿠키 저
## 10. 업데이트 이력
### 10.0 [2026-01-15] 미들웨어 사전 갱신 기능 추가
**관련 문서:** `[IMPL-2026-01-15] middleware-pre-refresh.md`
Request Coalescing 패턴만으로는 auth/check + serverFetch 동시 호출 시 Race Condition이 완전히 해결되지 않아, **미들웨어에서 페이지 렌더링 전 토큰을 미리 갱신**하는 기능 추가.
두 기능은 상호 보완적:
- **미들웨어 사전 갱신**: 페이지 로드 전 토큰 준비 (1차 방어)
- **Request Coalescing**: API 호출 시 401 발생 시 중복 갱신 방지 (2차 방어)
### 10.1 [2026-01-08] 누락된 API 라우트 통합
**문제 발견:**

View File

@@ -0,0 +1,424 @@
# 미들웨어 토큰 사전 갱신 (Pre-Refresh) 구현 문서
> 작성일: 2026-01-15
> 상태: 완료
## 1. 문제 상황
### 1.1 기존 Request Coalescing 패턴의 한계
`refresh-token.ts`의 5초 캐싱 패턴으로 동시 API 호출 시 중복 갱신은 방지했지만, **auth/check + serverFetch 동시 호출** 문제가 완전히 해결되지 않았음.
### 1.2 Race Condition 시나리오
```
페이지 로드 시 (access_token 만료, refresh_token만 있는 상태)
시간 →
────────────────────────────────────────────────────────────────────
[페이지 렌더링 시작]
[useEffect] → auth/check 호출 ─────┐
[Server Component] → serverFetch ──┼─→ 둘 다 refresh_token 필요
첫 번째가 갱신하면 두 번째는?
(캐시 공유해도 타이밍 문제 발생 가능)
────────────────────────────────────────────────────────────────────
```
### 1.3 증상
- 페이지 로드 시 간헐적으로 401 에러
- 토큰 만료 직후 첫 페이지 접속 시 로그인 페이지로 튕김
- 콘솔에 `Token refresh failed` 로그
---
## 2. 해결 방법: 미들웨어 사전 갱신 (Pre-Refresh)
### 2.1 핵심 아이디어
**페이지 렌더링 전에 미들웨어에서 토큰을 미리 갱신**하여, 페이지 로드 시 모든 API 호출이 이미 갱신된 access_token을 사용하도록 함.
```
시간 →
────────────────────────────────────────────────────────────────────
[브라우저 요청] → [미들웨어 7.5단계]
access_token 없고 refresh_token만 있음?
↓ YES
백엔드 /api/v1/refresh 호출 (1회)
Set-Cookie: access_token, refresh_token
[페이지 렌더링] → auth/check, serverFetch 모두 새 access_token 사용
✅ Race Condition 없음
────────────────────────────────────────────────────────────────────
```
### 2.2 기존 패턴과의 관계
| 기능 | 목적 | 실행 시점 | 파일 |
|------|------|----------|------|
| **Request Coalescing** | 동시 API 호출 시 refresh 중복 방지 | API 호출 시 401 응답 후 | `refresh-token.ts` |
| **미들웨어 사전 갱신** | 페이지 로드 전 토큰 준비 | 미들웨어 실행 시 | `middleware.ts` |
두 기능은 **상호 보완적**:
- 미들웨어가 사전 갱신하면 대부분의 경우 API 호출 시 401이 발생하지 않음
- 만약 미들웨어 이후 토큰이 만료되면 Request Coalescing이 백업으로 동작
---
## 3. 구현 코드
### 3.1 파일 위치
```
src/middleware.ts
```
### 3.2 추가된 코드 구조
```typescript
// 1. 캐시 객체 (모듈 레벨)
let middlewareRefreshCache: {
promise: Promise<RefreshResult> | null;
timestamp: number;
result: RefreshResult | null;
} = { promise: null, timestamp: 0, result: null };
const MIDDLEWARE_REFRESH_CACHE_TTL = 5000; // 5초
// 2. checkAuthentication() 확장
function checkAuthentication(request: NextRequest): {
isAuthenticated: boolean;
authMode: 'sanctum' | 'bearer' | 'api-key' | null;
needsRefresh: boolean; // 🆕 access_token 없고 refresh_token만 있음
refreshToken: string | null; // 🆕 갱신에 사용할 토큰
}
// 3. refreshTokenInMiddleware() 함수
async function refreshTokenInMiddleware(refreshToken: string): Promise<RefreshResult>
// 4. middleware() 함수 내 7.5단계
export async function middleware(request: NextRequest) {
// ... 기존 1~7단계 ...
// 7.5단계: 토큰 사전 갱신
if (needsRefresh && refreshToken) {
const refreshResult = await refreshTokenInMiddleware(refreshToken);
// Set-Cookie로 새 토큰 설정
}
// ... 기존 8~10단계 ...
}
```
### 3.3 checkAuthentication() 반환값 변경
**변경 전:**
```typescript
return {
isAuthenticated: boolean;
authMode: 'sanctum' | 'bearer' | 'api-key' | null;
}
```
**변경 후:**
```typescript
return {
isAuthenticated: boolean;
authMode: 'sanctum' | 'bearer' | 'api-key' | null;
needsRefresh: boolean; // access_token 없고 refresh_token만 있으면 true
refreshToken: string | null; // 갱신에 사용할 refresh_token 값
}
```
### 3.4 7.5단계 사전 갱신 로직
```typescript
// 7⃣.5️⃣ 🔄 토큰 사전 갱신 (Race Condition 방지)
if (needsRefresh && refreshToken) {
console.log(`🔄 [Middleware] Pre-refreshing token before page render: ${pathname}`);
const refreshResult = await refreshTokenInMiddleware(refreshToken);
if (refreshResult.success && refreshResult.accessToken) {
const isProduction = process.env.NODE_ENV === 'production';
const intlResponse = intlMiddleware(request);
// Set-Cookie 헤더로 새 토큰 전송
const accessTokenCookie = [
`access_token=${refreshResult.accessToken}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
`Max-Age=${refreshResult.expiresIn || 7200}`,
].join('; ');
const refreshTokenCookie = [
`refresh_token=${refreshResult.refreshToken}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
'Max-Age=604800', // 7 days (하드코딩)
].join('; ');
intlResponse.headers.append('Set-Cookie', accessTokenCookie);
intlResponse.headers.append('Set-Cookie', refreshTokenCookie);
// ... 기타 쿠키 ...
return intlResponse;
} else {
// 갱신 실패 시 로그인 페이지로
return NextResponse.redirect(new URL('/login', request.url));
}
}
```
---
## 4. 동작 흐름도
### 4.1 정상 흐름 (access_token 유효)
```
브라우저 → 미들웨어 → checkAuthentication()
needsRefresh = false (access_token 있음)
7.5단계 스킵 → 페이지 렌더링
```
### 4.2 사전 갱신 흐름 (access_token 만료, refresh_token 유효)
```
브라우저 → 미들웨어 → checkAuthentication()
needsRefresh = true (access_token 없음, refresh_token 있음)
7.5단계: refreshTokenInMiddleware() 호출
백엔드 /api/v1/refresh → 새 토큰 발급
Set-Cookie: access_token, refresh_token
페이지 렌더링 (새 토큰으로)
```
### 4.3 갱신 실패 흐름 (refresh_token도 만료)
```
브라우저 → 미들웨어 → checkAuthentication()
needsRefresh = true
7.5단계: refreshTokenInMiddleware() 호출
백엔드 → 401 (refresh_token 만료)
redirect('/login')
```
---
## 5. 설정 값
| 항목 | 값 | 설명 |
|------|-----|------|
| MIDDLEWARE_REFRESH_CACHE_TTL | 5초 | 미들웨어 캐시 유지 시간 |
| access_token Max-Age | 7200초 (2시간) | 백엔드 expires_in 값 또는 기본값 |
| refresh_token Max-Age | 604800초 (7일) | 하드코딩 (백엔드에서 미제공) |
---
## 6. 로그 메시지
### 6.1 사전 갱신 시작
```
🔄 [Middleware] Pre-refreshing token before page render: /dashboard
```
### 6.2 캐시 히트
```
🔵 [Middleware] Using cached refresh result (age: 1234ms)
```
### 6.3 진행 중인 갱신 대기
```
🔵 [Middleware] Waiting for ongoing refresh...
```
### 6.4 갱신 성공
```
✅ [Middleware] Pre-refresh successful
✅ [Middleware] Pre-refresh complete, new tokens set in cookies
```
### 6.5 갱신 실패
```
🔴 [Middleware] Pre-refresh failed: 401
🔴 [Middleware] Pre-refresh failed, redirecting to login
```
---
## 7. Edge Runtime 고려사항
### 7.1 모듈 레벨 캐시의 한계
Edge Runtime에서는 모듈 레벨 변수가 **요청 간 공유되지 않을 수 있음**.
따라서 `middlewareRefreshCache`는 **같은 요청 내 중복 갱신 방지**에만 효과적.
### 7.2 5초 캐시의 역할
- 같은 요청 처리 중 여러 번 호출되는 경우 방지
- Edge 인스턴스 간 캐시 공유는 불가능
- 충분히 짧아서 토큰 갱신 지연 문제 없음
---
## 8. 관련 파일
| 파일 | 역할 |
|------|------|
| `src/middleware.ts` | 미들웨어 사전 갱신 로직 |
| `src/lib/api/refresh-token.ts` | Request Coalescing 패턴 (백업) |
| `src/app/api/auth/check/route.ts` | 인증 확인 API |
| `src/app/api/auth/refresh/route.ts` | 토큰 갱신 프록시 |
---
## 9. 관련 문서
- `[IMPL-2025-12-30] token-refresh-caching.md` - Request Coalescing 패턴 문서
- `[IMPL-2025-11-07] middleware-issue-resolution.md` - 미들웨어 기본 구조
---
## 10. 업데이트 이력
### 10.1 [2026-01-15] 초기 구현
**배경:**
- auth/check와 serverFetch 동시 호출 시 Race Condition 발생
- 기존 Request Coalescing만으로는 완전히 해결되지 않음
**구현 내용:**
1. `middlewareRefreshCache` 캐시 객체 추가
2. `refreshTokenInMiddleware()` 함수 구현
3. `checkAuthentication()``needsRefresh`, `refreshToken` 반환 추가
4. 7.5단계 사전 갱신 로직 추가
**결과:**
- 페이지 렌더링 전 토큰 갱신 완료
- 이후 API 호출들은 새 access_token 사용
- Race Condition 완전 해결
### 10.2 [2026-01-15] 파편화된 API route 통합
**배경:**
- `/api/menus` 등 별도 route에서 refresh 로직 없이 바로 401 반환
- 1~2시간 방치 후 로그인 페이지로 튕기는 문제 발생
**수행 내용:**
1. 클라이언트 호출 경로 변경:
- `/api/menus``/api/proxy/menus` (menuRefresh.ts)
- `/api/files/${id}/download``/api/proxy/files/${id}/download` (DocumentCreate, DraftBox)
2. 파편화된 API route 삭제:
- `src/app/api/menus/` - 삭제
- `src/app/api/files/` - 삭제
- `src/app/api/tenants/` - 삭제 (미사용)
- `src/lib/api/php-proxy.ts` - 삭제 (중복 유틸)
**결과:**
- 모든 API 호출이 `/api/proxy`를 통해 refresh 로직 적용
- 토큰 만료 시 자동 갱신 후 재시도
### 10.3 [2026-01-15] 인증 흐름 전면 재설계
**배경:**
- pre-refresh 실패 시 무한 리다이렉트 루프 발생
- 5⃣ 게스트 전용 라우트에서 `needsRefresh` 상태를 고려하지 않음
- `refresh_token`만 있는 상태를 "로그인됨"으로 섣부르게 판정
**문제의 무한 루프 시나리오:**
```
/login 접근 (refresh_token만 있음)
5⃣ isAuthenticated=true (refresh_token 있으니까) → /dashboard로 리다이렉트
7.5️⃣ pre-refresh 시도 → 401 실패 → /login으로 리다이렉트
무한 반복!
```
**핵심 원인:**
- `refresh_token`만 있는 상태 = "로그인됨"이 아니라 "로그인 가능성 있음"
- 실제로 refresh 성공해야 "진짜 로그인"
- 5⃣에서 이걸 확인 안 하고 바로 /dashboard로 보냄
**수정 내용 (5⃣ 게스트 전용 라우트):**
```typescript
if (isGuestOnlyRoute(pathnameWithoutLocale)) {
// needsRefresh인 경우: 먼저 refresh 시도해서 "진짜 로그인"인지 확인
if (needsRefresh && refreshToken) {
const refreshResult = await refreshTokenInMiddleware(refreshToken);
if (refreshResult.success) {
// ✅ 진짜 로그인됨 → /dashboard로 (쿠키 설정)
return redirectToDashboard(with new cookies);
} else {
// ❌ 로그인 안 됨 → 쿠키 삭제 후 로그인 페이지 표시 (리다이렉트 없이!)
return showLoginPage(with cleared cookies);
}
}
// access_token 있음 = 확실히 로그인됨 → /dashboard로
if (isAuthenticated) {
return redirectToDashboard();
}
// 쿠키 없음 = 비로그인 → 로그인 페이지 표시
return showLoginPage();
}
```
**수정 후 흐름:**
```
/login 접근 (refresh_token만 있음)
5⃣ needsRefresh=true → refresh 먼저 시도
├─ 성공 → "진짜 로그인" → /dashboard (왕복 1회)
└─ 실패 → "로그인 안 됨" → 쿠키 삭제 → 로그인 페이지 (왕복 0회!)
```
**결과:**
- 무한 리다이렉트 루프 완전 해결
- 불필요한 /dashboard → /login 왕복 제거
- refresh 실패 시 바로 로그인 페이지 표시
---
## 11. TODO (Phase 2)
### 쿠키 설정 공통 모듈화
현재 쿠키 설정 코드가 6곳에 중복:
- `/api/proxy/[...path]/route.ts`
- `/api/auth/login/route.ts`
- `/api/auth/check/route.ts`
- `/api/auth/refresh/route.ts`
- `middleware.ts`
- `fetch-wrapper.ts`
**계획:**
```typescript
// src/lib/api/cookie-utils.ts (신규)
export function createTokenCookies(tokens: TokenSet): string[]
export function clearTokenCookies(): string[]
```
**효과:** 유지보수성 향상 (쿠키 설정 변경 시 1곳만 수정)