- 이전 잘 작동하던 코드 로직을 현재 프로세서에 적용 - LuckyExcel 우선 시도 + SheetJS Fallback 패턴 도입 - CSV, XLS, XLSX 모든 형식에 대한 안정적 처리 - 한글 시트명 정규화 및 워크북 구조 검증 강화 - 복잡한 SheetJS 옵션 단순화로 안정성 향상 - 에러 발생 시 빈 시트 생성으로 앱 중단 방지 - 테스트 환경 및 Cursor 규칙 업데이트 Technical improvements: - convertSheetJSToLuckyExcel 함수로 안정적 데이터 변환 - UTF-8 codepage 설정으로 한글 지원 강화 - validateWorkbook 함수로 방어적 프로그래밍 적용
154 lines
4.1 KiB
TypeScript
154 lines
4.1 KiB
TypeScript
import React from "react";
|
|
import { Button } from "./button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
|
import { cn } from "../../lib/utils";
|
|
|
|
interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* 기본 모달 컴포넌트
|
|
* - 배경 클릭으로 닫기
|
|
* - ESC 키로 닫기
|
|
* - 접근성 지원 (ARIA)
|
|
*/
|
|
export function Modal({
|
|
isOpen,
|
|
onClose,
|
|
title,
|
|
children,
|
|
className,
|
|
}: ModalProps) {
|
|
// ESC 키 이벤트 처리
|
|
React.useEffect(() => {
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "hidden";
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "unset";
|
|
};
|
|
}, [isOpen, onClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title"
|
|
>
|
|
{/* 배경 오버레이 */}
|
|
<div
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
{/* 모달 콘텐츠 */}
|
|
<Card className={cn("relative w-full max-w-md", className)}>
|
|
<CardHeader className="pb-4">
|
|
<CardTitle id="modal-title" className="text-lg font-semibold">
|
|
{title}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">{children}</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FileErrorModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
errors: Array<{ fileName: string; error: string }>;
|
|
}
|
|
|
|
/**
|
|
* 파일 업로드 에러 전용 모달
|
|
*/
|
|
export function FileErrorModal({
|
|
isOpen,
|
|
onClose,
|
|
errors,
|
|
}: FileErrorModalProps) {
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={onClose}
|
|
title="파일 업로드 오류"
|
|
className="max-w-lg"
|
|
>
|
|
<div className="space-y-4">
|
|
<div className="flex items-start space-x-3">
|
|
<div className="flex-shrink-0">
|
|
<svg
|
|
className="h-6 w-6 text-red-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.864-.833-2.634 0L3.197 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-gray-900 mb-3">
|
|
다음 파일들을 업로드할 수 없습니다:
|
|
</p>
|
|
<div className="space-y-2">
|
|
{errors.map((error, index) => (
|
|
<div
|
|
key={index}
|
|
className="p-3 bg-red-50 border border-red-200 rounded-md"
|
|
>
|
|
<p className="text-sm font-medium text-red-800">
|
|
{error.fileName}
|
|
</p>
|
|
<p className="text-xs text-red-600 mt-1">{error.error}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
|
<h4 className="text-sm font-medium text-blue-800 mb-2">
|
|
지원되는 파일 형식:
|
|
</h4>
|
|
<ul className="text-xs text-blue-700 space-y-1">
|
|
<li>• Excel 파일 (.xlsx, .xls)</li>
|
|
<li>• 최대 파일 크기: 50MB</li>
|
|
<li>• 한글 파일명 및 시트명 지원</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-2 pt-2">
|
|
<Button onClick={onClose} variant="default">
|
|
확인
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|