# 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
```typescript
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
```typescript
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 모바일 처리

```typescript
// 현재 props (이미 존재)
allData?: T[];                    // 전체 데이터
mobileDisplayCount?: number;      // 표시 개수
onLoadMore?: () => void;          // 더보기 콜백
infinityScrollSentinelRef?: RefObject<HTMLDivElement>;  // sentinel
```

**현재 동작:**
- 모바일: `(allData || data).map(...)` → 전체 데이터 한번에 렌더링
- 데스크톱: `data.map(...)` → 페이지네이션된 데이터만

**문제점:**
- `mobileDisplayCount` props는 있지만 실제 slice 로직 미구현
- 인피니티 스크롤 sentinel은 있지만 observer 연결 없음

---

## 통합 설계

### 1. UnifiedMobileCard 인터페이스

```typescript
// 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

```typescript
// 내부 상태 추가
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: 통합 컴포넌트 작성 ✅
- [x] `organisms/MobileCard.tsx` 통합 버전으로 재작성
- [x] 기존 두 버전의 모든 기능 포함
- [x] 하위 호환성 별칭 지원 (`fields`, `onCardClick`, `onToggleSelection`)

#### Step 1.2: molecules → organisms 마이그레이션 ✅
- [x] 28개 파일의 import 경로 변경
  ```typescript
  // Before
  import { MobileCard } from '@/components/molecules/MobileCard';
  // After
  import { MobileCard } from '@/components/organisms/MobileCard';
  ```
- [x] props 변경 없이 그대로 동작 확인

#### Step 1.3: ListMobileCard 사용처 마이그레이션 ✅
- [x] 33개 파일 import 경로 변경
- [x] `MobileCard as ListMobileCard` export 별칭으로 하위 호환성 유지
- [x] `onToggleSelection` → `onToggle` 별칭 동작 확인

#### Step 1.4: 정리 ✅
- [x] `molecules/MobileCard.tsx` 삭제
- [x] `organisms/ListMobileCard.tsx` 삭제
- [x] `organisms/index.ts` export 정리

### Phase 2: 모바일 인피니티 스크롤 ✅

#### Step 2.1: IntegratedListTemplateV2 수정 ✅
- [x] displayCount 내부 상태 추가
- [x] Intersection Observer 구현
- [x] 모바일 렌더링에 slice 적용 (`mobileData = allData?.slice(0, displayCount) || data`)
- [x] 로딩 인디케이터 추가 ("스크롤하여 더 보기 (N/M)")

#### Step 2.2: UniversalListPage 연동
- [x] allData prop 이미 지원됨
- [x] infinityScrollSentinelRef 이미 지원됨

#### Step 2.3: 테스트 ✅ (2026-01-21 Chrome DevTools MCP로 검증 완료)
- [x] 모바일에서 20개 초기 로드 확인
- [x] 스크롤 시 추가 로드 확인 (20개씩 추가 로드)
- [x] 전체 데이터 로드 완료 시 메시지 변경 ("모든 항목을 불러왔습니다")

**테스트 결과 (기성청구관리 페이지 - 50건)**:
| 단계 | 표시 항목 | 메시지 |
|------|----------|--------|
| 초기 로드 | 20/50 | "스크롤하여 더 보기 (20/50)" |
| 1차 스크롤 | 40/50 | "스크롤하여 더 보기 (40/50)" |
| 2차 스크롤 | 50/50 | "모든 항목을 불러왔습니다 (50개)" |

### Phase 3: 서버 사이드 모바일 인피니티 스크롤 ✅

> **배경**: 품목관리 등 대용량 데이터(10,000건 이상)는 서버에서 페이지당 20개씩만 반환
> 클라이언트 사이드 인피니티는 allData가 없으면 동작하지 않음
> → 서버 사이드 페이지네이션 기반의 모바일 인피니티 스크롤 필요

#### Step 3.1: IntegratedListTemplateV2 서버 사이드 인피니티 ✅
- [x] `accumulatedMobileData` 상태 추가 - 페이지별 데이터 누적
- [x] `lastAccumulatedPage` 추적 - 페이지 1이면 리셋, 이전+1이면 누적
- [x] `handleLoadMoreMobile()` - 다음 페이지 요청 (`pagination.onPageChange(currentPage + 1)`)
- [x] Intersection Observer - 스크롤 감지로 자동 로드
- [x] "더 보기" 버튼 + 로딩 인디케이터 + 진행률 표시
- [x] `enableMobileInfinityScroll` prop (기본: true)
- [x] `isMobileLoading` prop - 추가 로딩 상태

#### Step 3.2: UniversalListPage 연동 ✅
- [x] `isMobileLoading` 상태 추가
- [x] `fetchData(isMobileAppend)` - 모바일 추가 로드 시 별도 로딩 상태
- [x] `prevPage` 추적 - 페이지 증가 감지하여 isMobileAppend 결정
- [x] `isMobileLoading` prop 전달

#### Step 3.3: 테스트 ✅ (2026-01-21)
- [x] 로컬 환경 테스트 (품목관리, 거래처관리, 사원관리)
- [x] 데이터 20개 미만: "모든 항목을 불러왔습니다 (N개)" 정상 표시
- [x] 총 개수 표시 정상 동작
- [x] **품목관리 (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`를 전달하는 경우,
> 서버 사이드 페이지네이션임에도 클라이언트 사이드로 판단되는 문제 발생

**수정 사항**:
- [x] `isServerSidePagination` 판단 로직 개선: `!allData || pagination.totalItems > allData.length`
- [x] 탭 변경 감지 useEffect에서 `allData` dependency 제거 (페이지 변경 시 리셋 방지)
- [x] "더 보기" 버튼 onClick: `isServerSidePagination` 기준으로 핸들러 선택
- [x] Intersection Observer: 동일하게 `isServerSidePagination` 기준 적용

**구현 로직**:
```typescript
// 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
