공지사항, csv 표출 작업1

This commit is contained in:
2025-11-29 13:41:13 +09:00
parent 39d21a475b
commit 91d14fdabf
5 changed files with 960 additions and 91 deletions

View File

@@ -47,6 +47,9 @@ export default function AdminLessonsPage() {
const [questionFileCount, setQuestionFileCount] = useState(0);
const [questionFileObject, setQuestionFileObject] = useState<File | null>(null);
const [questionFileKey, setQuestionFileKey] = useState<string | null>(null);
const [csvData, setCsvData] = useState<string[][]>([]);
const [csvHeaders, setCsvHeaders] = useState<string[]>([]);
const [csvRows, setCsvRows] = useState<string[][]>([]);
// 에러 상태
const [errors, setErrors] = useState<{
@@ -232,6 +235,9 @@ export default function AdminLessonsPage() {
setQuestionFileCount(0);
setQuestionFileObject(null);
setQuestionFileKey(null);
setCsvData([]);
setCsvHeaders([]);
setCsvRows([]);
setErrors({});
};
@@ -372,6 +378,9 @@ export default function AdminLessonsPage() {
setQuestionFileCount(0);
setQuestionFileObject(null);
setQuestionFileKey(null);
setCsvData([]);
setCsvHeaders([]);
setCsvRows([]);
// 토스트 팝업 표시
setShowToast(true);
@@ -881,6 +890,83 @@ export default function AdminLessonsPage() {
}
try {
// CSV 파일 파싱
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result as string;
if (!text) return;
try {
// CSV 파싱 함수
const parseCsv = (csvText: string): string[][] => {
const lines: string[][] = [];
let currentLine: string[] = [];
let currentField = '';
let inQuotes = false;
for (let i = 0; i < csvText.length; i++) {
const char = csvText[i];
const nextChar = csvText[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
currentField += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
currentLine.push(currentField.trim());
currentField = '';
} else if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && nextChar === '\n') {
i++;
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
currentLine = [];
currentField = '';
}
} else {
currentField += char;
}
}
if (currentField || currentLine.length > 0) {
currentLine.push(currentField.trim());
lines.push(currentLine);
}
return lines;
};
const parsed = parseCsv(text);
if (parsed.length === 0) {
alert('CSV 파일이 비어있습니다.');
return;
}
// 첫 번째 줄을 헤더로 사용
const headers = parsed[0];
const rows = parsed.slice(1);
setCsvHeaders(headers);
setCsvRows(rows);
setCsvData(parsed);
} catch (parseError) {
console.error('CSV 파싱 오류:', parseError);
alert('CSV 파일을 읽는 중 오류가 발생했습니다.');
}
};
reader.onerror = () => {
alert('파일을 읽는 중 오류가 발생했습니다.');
};
reader.readAsText(file, 'UTF-8');
// 단일 파일 업로드
const uploadResponse = await apiService.uploadFile(file);
@@ -923,10 +1009,94 @@ export default function AdminLessonsPage() {
</label>
</div>
</div>
<div className="h-[64px] border border-[#dee1e6] rounded-[8px] bg-gray-50 flex items-center justify-center">
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center">
.
</p>
<div className="border border-[#dee1e6] rounded-[8px] bg-gray-50">
{csvData.length === 0 ? (
<div className="h-[64px] flex items-center justify-center">
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center">
.
</p>
</div>
) : (
<div className="flex flex-col">
{/* 파일 정보 및 삭제 버튼 */}
{questionFileObject && (
<div className="h-[40px] px-[20px] py-[12px] flex items-center gap-[12px] bg-white border-b border-[#dee1e6]">
<p className="flex-1 text-[15px] font-normal leading-[1.5] text-[#333c47]">
{questionFileObject.name}
</p>
<button
type="button"
onClick={() => {
setQuestionFileObject(null);
setQuestionFileKey(null);
setQuestionFileCount(0);
setCsvData([]);
setCsvHeaders([]);
setCsvRows([]);
}}
className="size-[16px] flex items-center justify-center cursor-pointer hover:opacity-70 transition-opacity shrink-0"
aria-label="파일 삭제"
>
<CloseXOSvg />
</button>
</div>
)}
{/* CSV 표 */}
<div className="m-[24px] border border-[#dee1e6] border-solid relative bg-white max-h-[400px] overflow-y-auto">
<div className="content-stretch flex flex-col items-start justify-center relative size-full">
{/* 헤더 */}
<div className="bg-[#f1f8ff] content-stretch flex h-[48px] items-center overflow-clip relative shrink-0 w-full sticky top-0 z-10">
{csvHeaders.map((header, index) => {
const isLast = index === csvHeaders.length - 1;
return (
<div
key={index}
className={`border-[#dee1e6] ${
isLast ? '' : 'border-[0px_1px_0px_0px]'
} border-solid box-border content-stretch flex gap-[10px] h-full items-center justify-center px-[8px] py-[12px] relative shrink-0 ${
index === 0 ? 'w-[48px]' : index === 1 ? 'basis-0 grow min-h-px min-w-px' : 'w-[140px]'
}`}
>
<div className="flex flex-col font-['Pretendard:SemiBold',sans-serif] justify-center leading-[0] not-italic relative shrink-0 text-[#4c5561] text-[14px] text-nowrap">
<p className="leading-[1.5] whitespace-pre">{header || `${index + 1}`}</p>
</div>
</div>
);
})}
</div>
{/* 데이터 행 */}
{csvRows.map((row, rowIndex) => (
<div
key={rowIndex}
className="border-[#dee1e6] border-[1px_0px_0px] border-solid h-[48px] relative shrink-0 w-full"
>
<div className="content-stretch flex h-[48px] items-start overflow-clip relative rounded-[inherit] w-full">
{csvHeaders.map((_, colIndex) => {
const isLast = colIndex === csvHeaders.length - 1;
const cellValue = row[colIndex] || '';
return (
<div
key={colIndex}
className={`border-[#dee1e6] ${
isLast ? '' : 'border-[0px_1px_0px_0px]'
} border-solid box-border content-stretch flex flex-col gap-[4px] items-center justify-center px-[8px] py-[12px] relative shrink-0 ${
colIndex === 0 ? 'w-[48px]' : colIndex === 1 ? 'basis-0 grow min-h-px min-w-px' : 'w-[140px]'
}`}
>
<p className="font-['Pretendard:Medium',sans-serif] leading-[1.5] not-italic relative shrink-0 text-[#1b2027] text-[15px] text-nowrap whitespace-pre">
{cellValue}
</p>
</div>
);
})}
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>