메인소스코드 정리 및 시험타입별 채점 한번에 처리 가능하도록 변경
This commit is contained in:
Binary file not shown.
BIN
250408_DIC_2503A_채점결과.xlsx
Normal file
BIN
250408_DIC_2503A_채점결과.xlsx
Normal file
Binary file not shown.
BIN
250408_DIC_2503B_채점결과.xlsx
Normal file
BIN
250408_DIC_2503B_채점결과.xlsx
Normal file
Binary file not shown.
BIN
250408_DIC_2503C_채점결과.xlsx
Normal file
BIN
250408_DIC_2503C_채점결과.xlsx
Normal file
Binary file not shown.
312
psdExport_2.js
312
psdExport_2.js
@@ -11,38 +11,24 @@ 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';
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503A_채점결과.xlsx';
|
|
||||||
const outputExcelFile = './'+todayDate+'_DIC_2503B_채점결과.xlsx';
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503C_채점결과.xlsx';
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503D_채점결과.xlsx';
|
|
||||||
|
|
||||||
// TEST
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503A_TEST.xlsx';
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503B_TEST.xlsx';
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503C_TEST.xlsx';
|
|
||||||
// const outputExcelFile = './'+todayDate+'_DIC_2503D_TEST.xlsx';
|
|
||||||
|
|
||||||
// --------------------------------------------------------
|
|
||||||
|
|
||||||
|
examType.forEach(type => {
|
||||||
|
const scoringJson = require(`./DIC_${examRound}${type}.json`);
|
||||||
|
const answerFilesDir = `./output/${type}/${testMode ? 'TEST' : 'DIC'}`;
|
||||||
|
const outputExcelFile = `./${todayDate}_DIC_${examRound}${type}_${testMode ? 'TEST.xlsx' : '채점결과.xlsx'}`;
|
||||||
|
|
||||||
// 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴
|
// 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴
|
||||||
const studentDirs = fs.readdirSync(answerFilesDir).filter(file => {
|
const studentDirs = fs.readdirSync(answerFilesDir).filter(file => {
|
||||||
@@ -114,101 +100,8 @@ studentDirs.forEach(student => {
|
|||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// console.log(flattenedData);
|
|
||||||
const flattenedData = prepareExcelData(scoringResultList);
|
const flattenedData = prepareExcelData(scoringResultList);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transposedData = transposeData(flattenedData);
|
const transposedData = transposeData(flattenedData);
|
||||||
// const transposedData = transposeData(flattenedData).slice(1)
|
|
||||||
|
|
||||||
// 엑셀 파일 생성
|
// 엑셀 파일 생성
|
||||||
const worksheet = XLSX.utils.json_to_sheet(transposedData, { skipHeader: true });
|
const worksheet = XLSX.utils.json_to_sheet(transposedData, { skipHeader: true });
|
||||||
@@ -216,6 +109,7 @@ const workbook = XLSX.utils.book_new();
|
|||||||
|
|
||||||
// 열 너비 계산
|
// 열 너비 계산
|
||||||
const columnWidths = Object.keys(transposedData[0]).map(key => {
|
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)) // 각 셀의 데이터 길이
|
||||||
@@ -231,46 +125,9 @@ XLSX.utils.book_append_sheet(workbook, worksheet, '채점 결과');
|
|||||||
|
|
||||||
// 엑셀 파일 저장
|
// 엑셀 파일 저장
|
||||||
XLSX.writeFile(workbook, outputExcelFile);
|
XLSX.writeFile(workbook, outputExcelFile);
|
||||||
console.log('채점 결과가 ' + outputExcelFile + ' 파일에 저장되었습니다.');
|
outputExcelFiles.push(outputExcelFile);
|
||||||
|
});
|
||||||
/**
|
console.log('채점 결과가 ' + outputExcelFiles + ' 파일에 저장되었습니다.');
|
||||||
* 자막 태그의 인덱스를 구할 때 사용
|
|
||||||
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user