--- name: generate-import-modal description: Excel 가져오기 모달의 보일러플레이트를 자동 생성합니다. useExcelImport 훅 기반. 새 가져오기 기능 추가 시 사용. argument-hint: "[도메인 이름 (예: 'attendance', 'homework')]" --- ## Purpose 새로운 Excel 가져오기 모달을 프로젝트 표준 패턴에 맞춰 자동 생성합니다: 1. **모달 컴포넌트 생성** — `useExcelImport` 훅을 사용하는 가져오기 모달 2. **파서 함수 생성** — xlsx 필드 매핑 및 정규화 로직 3. **타입 정의** — 파싱된 행의 TypeScript 인터페이스 4. **부모 컴포넌트 연동 안내** — 모달 열기/닫기 및 onImport 콜백 연결 ## When to Run - 새 도메인에 Excel 가져오기 기능을 추가할 때 - 기존 가져오기 모달을 `useExcelImport` 훅 기반으로 리팩토링할 때 ## Related Files | File | Purpose | |------|---------| | `hooks/useExcelImport.ts` | Excel 가져오기 공통 훅 (파일 읽기, 상태 관리, 가져오기 실행) | | `components/Textbooks/TextbookImportModal.tsx` | 참조 구현: 교재 수납 가져오기 | | `components/Billing/BillingImportModal.tsx` | 참조 구현: 수납 데이터 가져오기 | | `components/StudentManagement/StudentMigrationModal.tsx` | 참조 구현: 학생 마이그레이션 | | `utils/studentMatching.ts` | 학생 매칭 유틸 (가져오기 시 학생 DB 매칭이 필요한 경우 사용) | ## Workflow ### Step 1: 사용자 입력 수집 `AskUserQuestion`을 사용하여 다음을 확인합니다: 1. **도메인 이름** (예: `attendance`, `homework`, `shuttle`) 2. **Excel 필드 매핑** — 어떤 열을 읽을지 (예: 이름, 학년, 학교, 점수) 3. **필터 조건** (선택) — 특정 행만 가져올지 (예: 구분 === '교재') 4. **학생 매칭 필요 여부** — `studentMatching.ts` 연동 필요 여부 5. **소속 탭** — 이 모달이 열리는 탭 컴포넌트 이름 ### Step 2: 타입 정의 생성 파싱된 행의 인터페이스를 모달 파일 상단에 정의합니다: ```typescript export interface ImportRow { // 사용자가 지정한 필드들 studentName: string; grade: string; // ... // 학생 매칭이 필요한 경우: matched: boolean; studentId?: string; } ``` ### Step 3: 모달 컴포넌트 생성 **파일:** `components//ImportModal.tsx` **프로젝트 표준 구조:** ```tsx import React, { useMemo } from 'react'; import { X, Upload, FileSpreadsheet, Loader2, AlertCircle, Check, RotateCcw, Eye } from 'lucide-react'; import { useExcelImport } from '../../hooks/useExcelImport'; // 1. 타입 정의 export interface ImportRow { // ...fields } // 2. Props 정의 interface ImportModalProps { isOpen: boolean; onClose: () => void; onImport: (rows: ImportRow[]) => Promise<{ added: number; skipped: number }>; // 학생 매칭이 필요한 경우: // studentIds?: Set; } // 3. 파서 함수 (또는 컴포넌트 내부 useCallback) function parseRows(rows: Record[]): ImportRow[] { return rows // .filter(row => ...) // 필터 조건이 있는 경우 .map(row => ({ // 필드 매핑 })); } // 4. 컴포넌트 export const ImportModal: React.FC<ImportModalProps> = ({ isOpen, onClose, onImport, }) => { const { fileInputRef, parsedData, fileName, isImporting, importResult, handleFileChange, handleImport, handleReset, openFileDialog, isParsed, isComplete, } = useExcelImport({ parser: parseRows, onImport, }); // 통계 요약 (useMemo) const summary = useMemo(() => { if (!isParsed) return null; return { totalRecords: parsedData.length, // ... 도메인별 통계 }; }, [parsedData, isParsed]); if (!isOpen) return null; return (
e.stopPropagation()}> {/* Header */}

<도메인명> 데이터 가져오기

{/* Body */}
{/* 파일 업로드 섹션 */}

파일 업로드

{fileName && {fileName}}
{/* 데이터 미리보기 섹션 */} {summary && (

데이터 미리보기

{/* 통계 카드 */}
{/* ... 도메인별 통계 카드 */}
{/* 미리보기 테이블 */}

처음 15건 미리보기

{/* 도메인별 컬럼 헤더 */} {parsedData.slice(0, 15).map((row, i) => ( {/* 도메인별 컬럼 데이터 */} ))}
{parsedData.length > 15 && (

... 외 {parsedData.length - 15}건 더 있음

)}
)} {/* 결과 섹션 */} {importResult && (
{importResult.success ? : }

{importResult.success ? '가져오기 완료' : '가져오기 실패'}

{importResult.success ? (
{importResult.added.toLocaleString()}건 추가 완료 {importResult.skipped > 0 && ( (중복 {importResult.skipped.toLocaleString()}건 건너뜀) )}
) : (
데이터 가져오기에 실패했습니다.
)}
)}
{/* Footer */}
{isComplete ? ( <> ) : ( <> )}
); }; ``` ### Step 4: barrel export 업데이트 해당 도메인의 `index.ts`에 export를 추가합니다: ```typescript export { ImportModal } from './ImportModal'; ``` ### Step 5: 부모 컴포넌트 연동 안내 가져오기 모달을 호출하는 부모 컴포넌트에서: ```tsx // 1. state 추가 const [isImportOpen, setIsImportOpen] = useState(false); // 2. onImport 콜백 (훅에서 mutation 함수를 가져와 사용) const handleImport = async (rows: ImportRow[]) => { // Firebase batch write or mutation const batch = writeBatch(db); // ... batch.set/update await batch.commit(); return { added: rows.length, skipped: 0 }; }; // 3. 버튼 추가 // 4. 모달 렌더 <ImportModal isOpen={isImportOpen} onClose={() => setIsImportOpen(false)} onImport={handleImport} /> ``` ### Step 6: 검증 1. 컴포넌트 파일 존재 확인 2. `useExcelImport` import 확인 3. barrel export 존재 확인 4. TypeScript 타입 오류 없는지 확인 ## Output Format ```markdown ## 가져오기 모달 생성 완료 | 항목 | 상태 | 파일 | |------|------|------| | 타입 정의 | 생성됨 | `components//ImportModal.tsx` (상단) | | 모달 컴포넌트 | 생성됨 | `components//ImportModal.tsx` | | barrel export | 업데이트 | `components//index.ts` | | useExcelImport | 연동됨 | `hooks/useExcelImport.ts` | 필드 매핑: - `이름` → studentName (string) - `학년` → grade (string) - ... 다음 단계: 1. 부모 컴포넌트에서 모달 열기/닫기 state 추가 2. `onImport` 콜백에서 Firebase batch write 구현 3. 통계 카드와 미리보기 테이블의 컬럼을 도메인에 맞게 커스터마이즈 ``` ## Design Decisions ### useExcelImport 훅 사용 이유 프로젝트에 이미 7개의 가져오기 모달이 존재하며, 모두 동일한 상태 관리 패턴을 반복합니다: - `fileInputRef`, `parsedData`, `fileName`, `isImporting`, `importResult` state - `handleFileChange` (xlsx 파싱), `handleImport`, `handleReset` 핸들러 `useExcelImport` 훅은 이 공통 로직을 캡슐화하여: - 새 모달 작성 시 코드량 60% 감소 - 상태 관리 버그 방지 (검증된 패턴 재사용) - 일관된 UX (파일 선택 → 미리보기 → 가져오기 → 결과) ### 모달 UI 표준 모든 가져오기 모달은 동일한 4섹션 구조를 따릅니다: 1. **헤더** — FileSpreadsheet 아이콘 + 도메인명 + 닫기 버튼 2. **파일 업로드** — 점선 border 버튼 + hidden input 3. **미리보기** — 통계 카드(grid) + 테이블(처음 15건) 4. **푸터** — 완료 전: 취소/가져오기 | 완료 후: 다른 파일/닫기 색상 체계: emerald(성공), red(실패), gray(기본), blue(통계) ## Exceptions 다음은 **문제가 아닙니다**: 1. **통계 카드/테이블 컬럼이 TODO** — 도메인별 커스터마이즈가 필요한 부분이므로 생성 후 수동 조정 2. **onImport에 Firebase 로직이 없는 것** — 가져오기 모달은 UI만 담당하고, 저장 로직은 부모/훅에서 구현 3. **학생 매칭 미포함** — 학생 매칭이 필요한 경우에만 `studentIds` prop과 `studentMatching.ts` 유틸 연동