공지사항, csv 표출 작업1
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user