- claudedocs 폴더 구조 재정리: archive/sessions, guides/migration·mobile·universal-list, refactoring 분류 - 오래된 세션 컨텍스트/체크리스트 문서 정리 (아카이브 이동 또는 삭제) - AuthContext → authStore(Zustand) 전환 시작, RootProvider 간소화 - GenericCRUDDialog 공통 다이얼로그 컴포넌트 추가 - PermissionDialog 삭제 → GenericCRUDDialog로 대체 - RankDialog/TitleDialog GenericCRUDDialog 기반으로 리팩토링 - toast-utils.ts 삭제 (미사용) - fileDownload.ts 개선, excel-download.ts 정리 - menuStore/themeStore Zustand 셀렉터 최적화 - useColumnSettings/useTableColumnStore 기능 보강 - 세금계산서/견적/작업자화면/결재 등 소규모 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
13 KiB
MobileCard 통합 및 모바일 인피니티 스크롤 계획서
개요
두 가지 연관된 개선 작업:
- MobileCard 통합: 2개 버전 → 1개 통합 컴포넌트
- 모바일 인피니티 스크롤: 전체 로드 → 20개씩 점진적 로드
현재 상태 분석
1. MobileCard 현황
| 위치 | 사용처 | 특징 |
|---|---|---|
organisms/MobileCard |
33개 파일 | icon, fields[], actions[] 배열 |
molecules/MobileCard |
28개 파일 | checkbox, details[], actions ReactNode |
organisms/MobileCard Props
interface MobileCardProps {
title: string;
subtitle?: string;
icon?: ReactNode;
badge?: { label: string; variant?: BadgeVariant };
fields: Array<{ label: string; value: string; badge?: boolean }>;
actions?: Array<{ label: string; onClick: () => void; icon?: LucideIcon; variant?: ButtonVariant }>;
onCardClick?: () => void;
}
- ✅ icon 지원
- ✅ fields 배열로 key-value 표시
- ✅ actions 배열로 버튼 자동 생성
- ❌ checkbox 미지원
- ❌ className 미지원
molecules/MobileCard Props
interface MobileCardProps {
title: string;
subtitle?: string;
description?: string;
badge?: string;
badgeVariant?: BadgeVariant;
badgeClassName?: string;
isSelected?: boolean;
onToggle?: () => void;
onClick?: () => void;
details?: Array<{ label: string; value: string | ReactNode }>;
actions?: ReactNode;
className?: string;
}
- ✅ checkbox 내장 (isSelected, onToggle)
- ✅ description 필드
- ✅ className 지원
- ✅ actions를 ReactNode로 유연하게
- ❌ icon 미지원
- ❌ actions 배열 방식 미지원
2. IntegratedListTemplateV2 모바일 처리
// 현재 props (이미 존재)
allData?: T[]; // 전체 데이터
mobileDisplayCount?: number; // 표시 개수
onLoadMore?: () => void; // 더보기 콜백
infinityScrollSentinelRef?: RefObject<HTMLDivElement>; // sentinel
현재 동작:
- 모바일:
(allData || data).map(...)→ 전체 데이터 한번에 렌더링 - 데스크톱:
data.map(...)→ 페이지네이션된 데이터만
문제점:
mobileDisplayCountprops는 있지만 실제 slice 로직 미구현- 인피니티 스크롤 sentinel은 있지만 observer 연결 없음
통합 설계
1. UnifiedMobileCard 인터페이스
// src/components/organisms/MobileCard.tsx (통합 버전)
interface MobileCardProps {
// === 공통 (필수) ===
title: string;
// === 공통 (선택) ===
subtitle?: string;
description?: string;
icon?: ReactNode;
onClick?: () => void;
onCardClick?: () => void; // 🔄 onClick 별칭 (하위 호환성)
className?: string;
// === Badge (두 가지 형식 지원) ===
badge?: string | { label: string; variant?: BadgeVariant };
badgeVariant?: BadgeVariant; // badge가 string일 때 사용
badgeClassName?: string;
// === Checkbox Selection ===
isSelected?: boolean;
onToggle?: () => void;
showCheckbox?: boolean; // 기본값: onToggle이 있으면 true
// === Details/Fields (두 이름 지원) ===
details?: Array<{
label: string;
value: string | ReactNode;
badge?: boolean;
badgeVariant?: string;
colSpan?: number; // grid에서 차지할 컬럼 수
}>;
fields?: Array<{ // 🔄 details 별칭 (하위 호환성)
label: string;
value: string;
badge?: boolean;
badgeVariant?: string;
}>;
// === Actions (두 가지 방식 지원) ===
// 방식 1: ReactNode (완전 커스텀) - molecules 방식
// 방식 2: 배열 (자동 버튼 생성) - organisms 방식
actions?: ReactNode | Array<{
label: string;
onClick: () => void;
icon?: LucideIcon | ComponentType<any>;
variant?: 'default' | 'outline' | 'destructive';
}>;
// === Layout ===
detailsColumns?: 1 | 2 | 3; // details 그리드 컬럼 수 (기본: 2)
}
2. 하위 호환성 별칭 정리
| 기존 (organisms) | 기존 (molecules) | 통합 버전 | 처리 방식 |
|---|---|---|---|
onCardClick |
onClick |
둘 다 지원 | onClick || onCardClick |
fields |
details |
둘 다 지원 | details || fields |
badge: { label, variant } |
badge: string |
둘 다 지원 | typeof 체크 |
| - | isSelected, onToggle |
지원 | 그대로 |
icon |
- | 지원 | 그대로 |
| - | description |
지원 | 그대로 |
| - | className |
지원 | 그대로 |
actions: Array |
actions: ReactNode |
둘 다 지원 | Array.isArray 체크 |
2. 모바일 인피니티 스크롤
구현 위치: IntegratedListTemplateV2
// 내부 상태 추가
const [displayCount, setDisplayCount] = useState(mobileDisplayCount || 20);
// Intersection Observer 설정
useEffect(() => {
if (!infinityScrollSentinelRef?.current) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && allData && displayCount < allData.length) {
setDisplayCount(prev => Math.min(prev + 20, allData.length));
onLoadMore?.();
}
},
{ threshold: 0.1 }
);
observer.observe(infinityScrollSentinelRef.current);
return () => observer.disconnect();
}, [allData, displayCount, onLoadMore]);
// 모바일 렌더링 수정
const mobileData = allData?.slice(0, displayCount) || data;
마이그레이션 계획
상태: ✅ 완료 (2026-01-21) - Phase 1~3 모두 완료, 브라우저 테스트 검증 완료
Phase 1: MobileCard 통합 (영향: 61개 파일) ✅
Step 1.1: 통합 컴포넌트 작성 ✅
organisms/MobileCard.tsx통합 버전으로 재작성- 기존 두 버전의 모든 기능 포함
- 하위 호환성 별칭 지원 (
fields,onCardClick,onToggleSelection)
Step 1.2: molecules → organisms 마이그레이션 ✅
- 28개 파일의 import 경로 변경
// Before import { MobileCard } from '@/components/molecules/MobileCard'; // After import { MobileCard } from '@/components/organisms/MobileCard'; - props 변경 없이 그대로 동작 확인
Step 1.3: ListMobileCard 사용처 마이그레이션 ✅
- 33개 파일 import 경로 변경
MobileCard as ListMobileCardexport 별칭으로 하위 호환성 유지onToggleSelection→onToggle별칭 동작 확인
Step 1.4: 정리 ✅
molecules/MobileCard.tsx삭제organisms/ListMobileCard.tsx삭제organisms/index.tsexport 정리
Phase 2: 모바일 인피니티 스크롤 ✅
Step 2.1: IntegratedListTemplateV2 수정 ✅
- displayCount 내부 상태 추가
- Intersection Observer 구현
- 모바일 렌더링에 slice 적용 (
mobileData = allData?.slice(0, displayCount) || data) - 로딩 인디케이터 추가 ("스크롤하여 더 보기 (N/M)")
Step 2.2: UniversalListPage 연동
- allData prop 이미 지원됨
- infinityScrollSentinelRef 이미 지원됨
Step 2.3: 테스트 ✅ (2026-01-21 Chrome DevTools MCP로 검증 완료)
- 모바일에서 20개 초기 로드 확인
- 스크롤 시 추가 로드 확인 (20개씩 추가 로드)
- 전체 데이터 로드 완료 시 메시지 변경 ("모든 항목을 불러왔습니다")
테스트 결과 (기성청구관리 페이지 - 50건):
| 단계 | 표시 항목 | 메시지 |
|---|---|---|
| 초기 로드 | 20/50 | "스크롤하여 더 보기 (20/50)" |
| 1차 스크롤 | 40/50 | "스크롤하여 더 보기 (40/50)" |
| 2차 스크롤 | 50/50 | "모든 항목을 불러왔습니다 (50개)" |
Phase 3: 서버 사이드 모바일 인피니티 스크롤 ✅
배경: 품목관리 등 대용량 데이터(10,000건 이상)는 서버에서 페이지당 20개씩만 반환 클라이언트 사이드 인피니티는 allData가 없으면 동작하지 않음 → 서버 사이드 페이지네이션 기반의 모바일 인피니티 스크롤 필요
Step 3.1: IntegratedListTemplateV2 서버 사이드 인피니티 ✅
accumulatedMobileData상태 추가 - 페이지별 데이터 누적lastAccumulatedPage추적 - 페이지 1이면 리셋, 이전+1이면 누적handleLoadMoreMobile()- 다음 페이지 요청 (pagination.onPageChange(currentPage + 1))- Intersection Observer - 스크롤 감지로 자동 로드
- "더 보기" 버튼 + 로딩 인디케이터 + 진행률 표시
enableMobileInfinityScrollprop (기본: true)isMobileLoadingprop - 추가 로딩 상태
Step 3.2: UniversalListPage 연동 ✅
isMobileLoading상태 추가fetchData(isMobileAppend)- 모바일 추가 로드 시 별도 로딩 상태prevPage추적 - 페이지 증가 감지하여 isMobileAppend 결정isMobileLoadingprop 전달
Step 3.3: 테스트 ✅ (2026-01-21)
- 로컬 환경 테스트 (품목관리, 거래처관리, 사원관리)
- 데이터 20개 미만: "모든 항목을 불러왔습니다 (N개)" 정상 표시
- 총 개수 표시 정상 동작
- 품목관리 (10,429건) 대용량 테스트 완료 (2026-01-21)
- 초기 로드: 20개 표시, "20 / 10,429" 진행률
- "더 보기" 클릭: 페이지 1+2 누적 → 40개, "40 / 10,429"
- 페이지 1 데이터 유지됨 (소모품 테스트 4 → CS-000985 → CS-000984...)
- 참고: 실제 "더 보기" 버튼은 데이터 > 20개일 때만 표시됨
Step 3.4: 외부 훅 연동 수정 ✅ (2026-01-21)
문제: ItemListClient처럼 외부 훅(
useItemList)으로 데이터를 관리하면서allData를 전달하는 경우, 서버 사이드 페이지네이션임에도 클라이언트 사이드로 판단되는 문제 발생
수정 사항:
isServerSidePagination판단 로직 개선:!allData || pagination.totalItems > allData.length- 탭 변경 감지 useEffect에서
allDatadependency 제거 (페이지 변경 시 리셋 방지) - "더 보기" 버튼 onClick:
isServerSidePagination기준으로 핸들러 선택 - Intersection Observer: 동일하게
isServerSidePagination기준 적용
구현 로직:
// IntegratedListTemplateV2.tsx
// 1. 누적 데이터 관리
const [accumulatedMobileData, setAccumulatedMobileData] = useState<T[]>([]);
// 2. 페이지 변경 감지 → 데이터 누적
useEffect(() => {
if (pagination.currentPage === 1) {
setAccumulatedMobileData(data); // 리셋
} else if (pagination.currentPage === lastAccumulatedPage + 1) {
setAccumulatedMobileData(prev => [...prev, ...data]); // 누적
}
}, [data, pagination.currentPage]);
// 3. 모바일 데이터 결정
const mobileData = allData
? allData.slice(0, clientDisplayCount) // 클라이언트 사이드
: (enableMobileInfinityScroll ? accumulatedMobileData : data); // 서버 사이드
// 4. 진행 상황
const hasMoreData = pagination.currentPage < pagination.totalPages;
const loadedCount = accumulatedMobileData.length;
const totalDataCount = pagination.totalItems;
예상 작업량
| Phase | 작업 | 파일 수 | 난이도 |
|---|---|---|---|
| 1.1 | MobileCard 통합 작성 | 1 | 중 |
| 1.2 | molecules 사용처 마이그레이션 | 28 | 단순 반복 |
| 1.3 | organisms 사용처 확인 | 33 | 검증 |
| 1.4 | 정리 | 2 | 단순 |
| 2.1 | IntegratedListTemplateV2 수정 | 1 | 중 |
| 2.2 | UniversalListPage 연동 | 1 | 단순 |
| 2.3 | 테스트 | - | 검증 |
총 예상: 61개 파일 import 변경 + 핵심 로직 3개 파일
리스크 및 고려사항
MobileCard 통합
- 하위 호환성: 기존 사용처가 그대로 동작해야 함
- actions 타입 분기: ReactNode vs Array 런타임 체크 필요
- 테스트 범위: 61개 파일 모두 확인 필요
인피니티 스크롤
- 메모리: 계속 추가되면 DOM 노드 증가 → 성능 영향
- 상태 초기화: 탭/필터 변경 시 displayCount 리셋 필요
- 로딩 UX: 추가 로드 중 표시 필요
의사결정 필요 사항
- MobileCard actions 기본 방식: ReactNode vs Array 중 어느 쪽을 권장?
- 인피니티 스크롤 트리거: Intersection Observer vs 버튼("더 보기")?
- 한 번에 로드할 개수: 20개 고정? 설정 가능?
- 최대 로드 개수: 제한 없이 전체? 아니면 100개 제한?
다음 단계
- 이 계획서 검토 및 승인
- Phase 1 (MobileCard 통합) 진행
- Phase 2 (인피니티 스크롤) 진행
- 통합 테스트
참고: 사용처 목록
molecules/MobileCard 사용처 (28개)
- accounting: DepositManagement, WithdrawalManagement, BadDebtCollection, SalesManagement, CardTransactionInquiry, BankTransactionInquiry, BillManagement, VendorLedger, PurchaseManagement, VendorManagement
- business/construction: progress-billing, labor, handover-report, site-management, site-briefings, order-management, management, utility, bidding, item-management, project-list, contract, worker-status, pricing, estimates, structure-review, partners, issue-management
organisms/MobileCard 사용처 (33개)
- approval: ApprovalBox, ReferenceBox, DraftBox
- settings: PermissionManagement, PaymentHistoryManagement, AccountManagement
- quotes, board, quality, material, process, production, pricing, hr, items, outbound
- sales pages