Files
sam-react-prod/claudedocs/guides/mobile/[PLAN-2026-01-20] mobile-card-infinity-scroll.md
유병철 07374c826c refactor(WEB): claudedocs 재정리 및 AuthContext/Zustand/유틸 코드 개선
- 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>
2026-02-23 17:17:13 +09:00

13 KiB

MobileCard 통합 및 모바일 인피니티 스크롤 계획서

개요

두 가지 연관된 개선 작업:

  1. MobileCard 통합: 2개 버전 → 1개 통합 컴포넌트
  2. 모바일 인피니티 스크롤: 전체 로드 → 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(...) → 페이지네이션된 데이터만

문제점:

  • mobileDisplayCount props는 있지만 실제 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 ListMobileCard export 별칭으로 하위 호환성 유지
  • onToggleSelectiononToggle 별칭 동작 확인

Step 1.4: 정리

  • molecules/MobileCard.tsx 삭제
  • organisms/ListMobileCard.tsx 삭제
  • organisms/index.ts export 정리

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 - 스크롤 감지로 자동 로드
  • "더 보기" 버튼 + 로딩 인디케이터 + 진행률 표시
  • enableMobileInfinityScroll prop (기본: true)
  • isMobileLoading prop - 추가 로딩 상태

Step 3.2: UniversalListPage 연동

  • isMobileLoading 상태 추가
  • fetchData(isMobileAppend) - 모바일 추가 로드 시 별도 로딩 상태
  • prevPage 추적 - 페이지 증가 감지하여 isMobileAppend 결정
  • isMobileLoading prop 전달

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에서 allData dependency 제거 (페이지 변경 시 리셋 방지)
  • "더 보기" 버튼 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 통합

  1. 하위 호환성: 기존 사용처가 그대로 동작해야 함
  2. actions 타입 분기: ReactNode vs Array 런타임 체크 필요
  3. 테스트 범위: 61개 파일 모두 확인 필요

인피니티 스크롤

  1. 메모리: 계속 추가되면 DOM 노드 증가 → 성능 영향
  2. 상태 초기화: 탭/필터 변경 시 displayCount 리셋 필요
  3. 로딩 UX: 추가 로드 중 표시 필요

의사결정 필요 사항

  1. MobileCard actions 기본 방식: ReactNode vs Array 중 어느 쪽을 권장?
  2. 인피니티 스크롤 트리거: Intersection Observer vs 버튼("더 보기")?
  3. 한 번에 로드할 개수: 20개 고정? 설정 가능?
  4. 최대 로드 개수: 제한 없이 전체? 아니면 100개 제한?

다음 단계

  1. 이 계획서 검토 및 승인
  2. Phase 1 (MobileCard 통합) 진행
  3. Phase 2 (인피니티 스크롤) 진행
  4. 통합 테스트

참고: 사용처 목록

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