메인소스코드 정리 및 시험타입별 채점 한번에 처리 가능하도록 변경

This commit is contained in:
2025-04-08 16:11:38 +09:00
parent 98700d073a
commit ddd919c207
5 changed files with 227 additions and 239 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -11,50 +11,36 @@ const getGpdpScore = require('./gpdpScoring.js');
const getToday = require('./getToday.js'); const getToday = require('./getToday.js');
const todayDate = getToday(); const todayDate = getToday();
// -------------------------------------------------------- const examType = [
// const scoringJson = require('./DIC_2503A.json'); 'A',
const scoringJson = require('./DIC_2503B.json'); 'B',
// const scoringJson = require('./DIC_2503C.json'); 'C',
// const scoringJson = require('./DIC_2503D.json'); // 'D'
];
const examRound = '2503';
// -------------------------------------------------------- // testMode가 true일 경우 TEST 폴더에 있는 답안 파일을 읽어옴
// const answerFilesDir = './output/A/DIC'; // const testMode = true;
const answerFilesDir = './output/B/DIC'; const testMode = false;
// const answerFilesDir = './output/C/DIC';
// const answerFilesDir = './output/D/DIC';
// TEST const outputExcelFiles = [];
// const answerFilesDir = './output/A/TEST';
// const answerFilesDir = './output/B/TEST';
// const answerFilesDir = './output/C/TEST';
// const answerFilesDir = './output/D/TEST';
// -------------------------------------------------------- examType.forEach(type => {
// const outputExcelFile = './'+todayDate+'_DIC_2503A_채점결과.xlsx'; const scoringJson = require(`./DIC_${examRound}${type}.json`);
const outputExcelFile = './'+todayDate+'_DIC_2503B_채점결과.xlsx'; const answerFilesDir = `./output/${type}/${testMode ? 'TEST' : 'DIC'}`;
// const outputExcelFile = './'+todayDate+'_DIC_2503C_채점결과.xlsx'; const outputExcelFile = `./${todayDate}_DIC_${examRound}${type}_${testMode ? 'TEST.xlsx' : '채점결과.xlsx'}`;
// const outputExcelFile = './'+todayDate+'_DIC_2503D_채점결과.xlsx';
// TEST // 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴
// const outputExcelFile = './'+todayDate+'_DIC_2503A_TEST.xlsx'; const studentDirs = fs.readdirSync(answerFilesDir).filter(file => {
// const outputExcelFile = './'+todayDate+'_DIC_2503B_TEST.xlsx';
// const outputExcelFile = './'+todayDate+'_DIC_2503C_TEST.xlsx';
// const outputExcelFile = './'+todayDate+'_DIC_2503D_TEST.xlsx';
// --------------------------------------------------------
// 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴
const studentDirs = fs.readdirSync(answerFilesDir).filter(file => {
const filePath = path.join(answerFilesDir, file); const filePath = path.join(answerFilesDir, file);
return fs.statSync(filePath).isDirectory(); return fs.statSync(filePath).isDirectory();
}); });
// 채점 결과 리스트 // 채점 결과 리스트
const scoringResultList = []; const scoringResultList = [];
const psdData = []; const psdData = [];
studentDirs.forEach(student => { studentDirs.forEach(student => {
// 맥에서 한글 디렉토리 이름을 읽어서 엑셀에 저장 할 시 자소 분리가 되어 저장되는 문제 노말라이즈해서 해결 // 맥에서 한글 디렉토리 이름을 읽어서 엑셀에 저장 할 시 자소 분리가 되어 저장되는 문제 노말라이즈해서 해결
const name = student.normalize('NFC'); const name = student.normalize('NFC');
const studentDir = path.join(answerFilesDir, student); const studentDir = path.join(answerFilesDir, student);
@@ -112,165 +98,36 @@ studentDirs.forEach(student => {
scoringResult[3] = getGmepScore(xmlDocument, scoringJson, 2); scoringResult[3] = getGmepScore(xmlDocument, scoringJson, 2);
}); });
scoringResultList.push(scoringResult); scoringResultList.push(scoringResult);
});
// // Flatten the resultData for better representation in Excel
// const flattenedData = scoringResultList.map(student => {
// // const name = student["0"];
// const flattened = { "학생": student["0"] };
// // excel에 표시하지 않을 key값들
// const exceptKeys = [
// "0", // 학생 이름 항상 제외
// // "1", // psd1
// // "2", // psd2
// ]
// const exceptSubkeys = [
// "videoStartTime",
// "openingStartTime",
// ];
// Object.keys(student).forEach(key => {
// if (exceptKeys.includes(key)) {
// return;
// }
// Object.keys(student[key]).forEach(subKey => {
// if (exceptSubkeys.includes(subKey)) {
// return;
// }
// flattened[`${key}-${subKey}`] = student[key][subKey];
// });
// });
// return flattened;
// });
/**
* scoringResultList 배열을 엑셀에 출력하기 위한 데이터 정리 함수
* @param {Array} scoringResultList - 학생별 채점 결과 리스트
* @returns {Array} - 엑셀에 출력할 데이터 배열
*/
function prepareExcelData(scoringResultList) {
return scoringResultList.map(student => {
// const flattened = { "학생": student["0"] }; // 학생 이름을 첫 번째 열로 설정
const flattened = { "문항": student["0"] }; // 행열을 변환 할 경우 첫 행의 제목을 "문항"으로 설정
// 제외할 키와 서브키 정의
const exceptKeys = [
"0", // 학생 이름 제외
// "1", // psd1
// "2", // psd2
];
const exceptSubkeys = ["videoStartTime", "openingStartTime"]; // 제외할 서브키
// 학생 데이터 순회
Object.keys(student).forEach(key => {
if (exceptKeys.includes(key)) {
return; // 제외할 키는 건너뜀
}
// 서브키 순회
if (typeof student[key] === "object") {
Object.keys(student[key]).forEach(subKey => {
if (exceptSubkeys.includes(subKey)) {
return; // 제외할 서브키는 건너뜀
}
flattened[`${key}-${subKey}`] = student[key][subKey];
});
} else {
// 서브키가 없는 경우
flattened[key] = student[key];
}
}); });
return flattened; const flattenedData = prepareExcelData(scoringResultList);
}); const transposedData = transposeData(flattenedData);
}
// console.log(flattenedData);
const flattenedData = prepareExcelData(scoringResultList);
function transposeData(data) { // 엑셀 파일 생성
// 데이터가 없으면 빈 배열 반환 const worksheet = XLSX.utils.json_to_sheet(transposedData, { skipHeader: true });
if (data.length === 0) return []; const workbook = XLSX.utils.book_new();
// 첫 번째 객체의 키(열 제목) 가져오기 // 열 너비 계산
const keys = Object.keys(data[0]); const columnWidths = Object.keys(transposedData[0]).map(key => {
// 각 열의 최대 길이를 계산
// 행과 열을 변환
const transposed = keys.map(key => {
const row = { "항목": key }; // 각 열 제목을 "항목"으로 설정
data.forEach((item, index) => {
//console.log(data[index]['문항']);
row[data[index]['문항']] = item[key]; // 각 학생의 데이터를 열로 추가
});
return row;
});
return transposed;
}
const transposedData = transposeData(flattenedData);
// const transposedData = transposeData(flattenedData).slice(1)
// 엑셀 파일 생성
const worksheet = XLSX.utils.json_to_sheet(transposedData, {skipHeader: true});
const workbook = XLSX.utils.book_new();
// 열 너비 계산
const columnWidths = Object.keys(transposedData[0]).map(key => {
const maxLength = Math.max( const maxLength = Math.max(
// key.length, // 열 제목의 길이 // key.length, // 열 제목의 길이
// ...transposedData.map(row => (row[key] ? row[key].toString().length : 0)) // 각 셀의 데이터 길이 // ...transposedData.map(row => (row[key] ? row[key].toString().length : 0)) // 각 셀의 데이터 길이
4 // 고정 너비 4 // 고정 너비
); );
return { wch: maxLength + 1 }; // 여유 공간 추가 return { wch: maxLength + 1 }; // 여유 공간 추가
});
// 열 너비 설정
worksheet['!cols'] = columnWidths;
// Add the worksheet to the workbook
XLSX.utils.book_append_sheet(workbook, worksheet, '채점 결과');
// 엑셀 파일 저장
XLSX.writeFile(workbook, outputExcelFile);
outputExcelFiles.push(outputExcelFile);
}); });
console.log('채점 결과가 ' + outputExcelFiles + ' 파일에 저장되었습니다.');
// 열 너비 설정
worksheet['!cols'] = columnWidths;
// Add the worksheet to the workbook
XLSX.utils.book_append_sheet(workbook, worksheet, '채점 결과');
// 엑셀 파일 저장
XLSX.writeFile(workbook, outputExcelFile);
console.log('채점 결과가 ' + outputExcelFile + ' 파일에 저장되었습니다.');
/**
* 자막 태그의 인덱스를 구할 때 사용
* 1. CRTrackClip 요소의 순서에 따라 그 요소에 해당하는 CROwneUnit 태그의 순서를 구함
* 2. CRTrackClip 요소의 시작시간에 따라 그 요소에 해당하는 CROwneUnit 태그의 순서를 구함
*/
function getTrackClipNode(xmlDoc, type, videoStartTime, openingStartTime) {
let trackClipNode = null;
// 동영상 자막이면 2, 오프닝 자막이면 1, 그 외는 0
const subtitleOrder = type === 'video' ? 2 : type === 'opening' ? 1 : null;
const startTime = type === 'video' ? videoStartTime : openingStartTime;
// xpath 구문을 통해 CRTrackClip 요소의 ClipIndex를 찾음
const trackClipNode1 = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[not(@ClipIndex='-1')][${subtitleOrder}]`, xmlDoc);
const trackClipNode2 = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[sum(preceding-sibling::CRTrackClip/@Length) = ${startTime}]`, xmlDoc);
return trackClipNode = trackClipNode1 ?? trackClipNode2;
}
/**
* 자막텍스트를 이용해 자막 태그의 인덱스를 구할 때 사용
* 1. 자막 텍스트의 유사도를 판별
* 2. 자막텍스트와 일치하는 자막요소(CROWneUnit)의 순서를 구함
*/
function getClipIndexBySubtitle(xmlDoc, search) {
// 1. search값이 일치하지 않는 경우 : count가 0이 되어 @ClipIndex = 0 / CROwneUnit[1]을 가리킴 [오류]
// 2. search값이 일치하는 경우
// 1) search값이 CROwneUnit[1]이면 : preceding-sibling::CROwneUnit이 없어서 @ClipIndex = 0 / CROwneUnit[1]을 가리킴 [정상]
// 2) search값이 CROwneUnit[2]이면 : preceding-sibling::CROwneUnit이 한개 있으므로 @ClipIndex = 1 / CROwneUnit[2]을 가리킴 [정상] ...
if (!search) {
return null;
}
const searchResult = search ? findSimilarString(xmlDoc, search, 0.8) : null;
const cROwneUnitPreceding = searchResult ? xpath.select(`//CROwneUnit[CRCUnitArr[@Name='${searchResult}']]/preceding-sibling::CROwneUnit`, xmlDoc) : null;
const clipIndex = cROwneUnitPreceding ? cROwneUnitPreceding.length : null;
return clipIndex;
}
// xml 형식의 gmep 파일을 읽어서 점수를 계산 // xml 형식의 gmep 파일을 읽어서 점수를 계산
// scoring.json 파일 내에 있는 ele 요소는 xpath 형식으로 접근하여 요소를 탐색하고 나오는 값을 value와 비교하여 점수를 계산 // scoring.json 파일 내에 있는 ele 요소는 xpath 형식으로 접근하여 요소를 탐색하고 나오는 값을 value와 비교하여 점수를 계산
@@ -772,3 +629,134 @@ function getScore(psdData, scoring, index) {
scoringResult['총점'] = totalScore; scoringResult['총점'] = totalScore;
return scoringResult; return scoringResult;
} }
/**
* 자막 태그의 인덱스를 구할 때 사용
* 1. CRTrackClip 요소의 순서에 따라 그 요소에 해당하는 CROwneUnit 태그의 순서를 구함
* 2. CRTrackClip 요소의 시작시간에 따라 그 요소에 해당하는 CROwneUnit 태그의 순서를 구함
*/
function getTrackClipNode(xmlDoc, type, videoStartTime, openingStartTime) {
let trackClipNode = null;
// 동영상 자막이면 2, 오프닝 자막이면 1, 그 외는 0
const subtitleOrder = type === 'video' ? 2 : type === 'opening' ? 1 : null;
const startTime = type === 'video' ? videoStartTime : openingStartTime;
// xpath 구문을 통해 CRTrackClip 요소의 ClipIndex를 찾음
const trackClipNode1 = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[not(@ClipIndex='-1')][${subtitleOrder}]`, xmlDoc);
const trackClipNode2 = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[sum(preceding-sibling::CRTrackClip/@Length) = ${startTime}]`, xmlDoc);
return trackClipNode = trackClipNode1 ?? trackClipNode2;
}
/**
* 자막텍스트를 이용해 자막 태그의 인덱스를 구할 때 사용
* 1. 자막 텍스트의 유사도를 판별
* 2. 자막텍스트와 일치하는 자막요소(CROWneUnit)의 순서를 구함
*/
function getClipIndexBySubtitle(xmlDoc, search) {
// 1. search값이 일치하지 않는 경우 : count가 0이 되어 @ClipIndex = 0 / CROwneUnit[1]을 가리킴 [오류]
// 2. search값이 일치하는 경우
// 1) search값이 CROwneUnit[1]이면 : preceding-sibling::CROwneUnit이 없어서 @ClipIndex = 0 / CROwneUnit[1]을 가리킴 [정상]
// 2) search값이 CROwneUnit[2]이면 : preceding-sibling::CROwneUnit이 한개 있으므로 @ClipIndex = 1 / CROwneUnit[2]을 가리킴 [정상] ...
if (!search) {
return null;
}
const searchResult = search ? findSimilarString(xmlDoc, search, 0.8) : null;
const cROwneUnitPreceding = searchResult ? xpath.select(`//CROwneUnit[CRCUnitArr[@Name='${searchResult}']]/preceding-sibling::CROwneUnit`, xmlDoc) : null;
const clipIndex = cROwneUnitPreceding ? cROwneUnitPreceding.length : null;
return clipIndex;
}
// // Flatten the resultData for better representation in Excel
// const flattenedData = scoringResultList.map(student => {
// // const name = student["0"];
// const flattened = { "학생": student["0"] };
// // excel에 표시하지 않을 key값들
// const exceptKeys = [
// "0", // 학생 이름 항상 제외
// // "1", // psd1
// // "2", // psd2
// ]
// const exceptSubkeys = [
// "videoStartTime",
// "openingStartTime",
// ];
// Object.keys(student).forEach(key => {
// if (exceptKeys.includes(key)) {
// return;
// }
// Object.keys(student[key]).forEach(subKey => {
// if (exceptSubkeys.includes(subKey)) {
// return;
// }
// flattened[`${key}-${subKey}`] = student[key][subKey];
// });
// });
// return flattened;
// });
/**
* scoringResultList 배열을 엑셀에 출력하기 위한 데이터 정리 함수
* @param {Array} scoringResultList - 학생별 채점 결과 리스트
* @returns {Array} - 엑셀에 출력할 데이터 배열
*/
function prepareExcelData(scoringResultList) {
return scoringResultList.map(student => {
// const flattened = { "학생": student["0"] }; // 학생 이름을 첫 번째 열로 설정
const flattened = { "문항": student["0"] }; // 행열을 변환 할 경우 첫 행의 제목을 "문항"으로 설정
// 제외할 키와 서브키 정의
const exceptKeys = [
"0", // 학생 이름 제외
// "1", // psd1
// "2", // psd2
];
const exceptSubkeys = ["videoStartTime", "openingStartTime"]; // 제외할 서브키
// 학생 데이터 순회
Object.keys(student).forEach(key => {
if (exceptKeys.includes(key)) {
return; // 제외할 키는 건너뜀
}
// 서브키 순회
if (typeof student[key] === "object") {
Object.keys(student[key]).forEach(subKey => {
if (exceptSubkeys.includes(subKey)) {
return; // 제외할 서브키는 건너뜀
}
flattened[`${key}-${subKey}`] = student[key][subKey];
});
} else {
// 서브키가 없는 경우
flattened[key] = student[key];
}
});
return flattened;
});
}
function transposeData(data) {
// 데이터가 없으면 빈 배열 반환
if (data.length === 0) return [];
// 첫 번째 객체의 키(열 제목) 가져오기
const keys = Object.keys(data[0]);
// 행과 열을 변환
const transposed = keys.map(key => {
const row = { "항목": key }; // 각 열 제목을 "항목"으로 설정
data.forEach((item, index) => {
//console.log(data[index]['문항']);
row[data[index]['문항']] = item[key]; // 각 학생의 데이터를 열로 추가
});
return row;
});
return transposed;
}