diff --git a/250407_DIC_2503A_TEST.xlsx b/250407_DIC_2503A_TEST.xlsx deleted file mode 100644 index 3c89f57..0000000 Binary files a/250407_DIC_2503A_TEST.xlsx and /dev/null differ diff --git a/250408_DIC_2503A_채점결과.xlsx b/250408_DIC_2503A_채점결과.xlsx new file mode 100644 index 0000000..d491d0e Binary files /dev/null and b/250408_DIC_2503A_채점결과.xlsx differ diff --git a/250408_DIC_2503B_채점결과.xlsx b/250408_DIC_2503B_채점결과.xlsx new file mode 100644 index 0000000..45b9bae Binary files /dev/null and b/250408_DIC_2503B_채점결과.xlsx differ diff --git a/250408_DIC_2503C_채점결과.xlsx b/250408_DIC_2503C_채점결과.xlsx new file mode 100644 index 0000000..2dc7495 Binary files /dev/null and b/250408_DIC_2503C_채점결과.xlsx differ diff --git a/psdExport_2.js b/psdExport_2.js index 9aaad1d..41cad1e 100644 --- a/psdExport_2.js +++ b/psdExport_2.js @@ -11,266 +11,123 @@ const getGpdpScore = require('./gpdpScoring.js'); const getToday = require('./getToday.js'); const todayDate = getToday(); -// -------------------------------------------------------- -// const scoringJson = require('./DIC_2503A.json'); -const scoringJson = require('./DIC_2503B.json'); -// const scoringJson = require('./DIC_2503C.json'); -// const scoringJson = require('./DIC_2503D.json'); +const examType = [ + 'A', + 'B', + 'C', + // 'D' +]; +const examRound = '2503'; -// -------------------------------------------------------- -// const answerFilesDir = './output/A/DIC'; -const answerFilesDir = './output/B/DIC'; -// const answerFilesDir = './output/C/DIC'; -// const answerFilesDir = './output/D/DIC'; +// testMode가 true일 경우 TEST 폴더에 있는 답안 파일을 읽어옴 +// const testMode = true; +const testMode = false; -// TEST -// const answerFilesDir = './output/A/TEST'; -// const answerFilesDir = './output/B/TEST'; -// const answerFilesDir = './output/C/TEST'; -// const answerFilesDir = './output/D/TEST'; +const outputExcelFiles = []; -// -------------------------------------------------------- -// const outputExcelFile = './'+todayDate+'_DIC_2503A_채점결과.xlsx'; -const outputExcelFile = './'+todayDate+'_DIC_2503B_채점결과.xlsx'; -// const outputExcelFile = './'+todayDate+'_DIC_2503C_채점결과.xlsx'; -// const outputExcelFile = './'+todayDate+'_DIC_2503D_채점결과.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'}`; -// 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'; - -// -------------------------------------------------------- - - -// 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴 -const studentDirs = fs.readdirSync(answerFilesDir).filter(file => { - const filePath = path.join(answerFilesDir, file); - return fs.statSync(filePath).isDirectory(); -}); - -// 채점 결과 리스트 -const scoringResultList = []; -const psdData = []; - -studentDirs.forEach(student => { - // 맥에서 한글 디렉토리 이름을 읽어서 엑셀에 저장 할 시 자소 분리가 되어 저장되는 문제 노말라이즈해서 해결 - const name = student.normalize('NFC'); - const studentDir = path.join(answerFilesDir, student); - const psdFiles = fs.readdirSync(studentDir).filter(file => file.endsWith('.psd')); - // DIAT시험 프로젝트로 생성시 gmep확장자로 - // 교육용 프로젝프로 생성시 gmdp확장자로 생성됨 - // 두 경우 모두 처리 - const gmepFile = fs.readdirSync(studentDir).filter( - file => file.endsWith('.gmep') || file.endsWith('.gmdp') - ); - - // 곰픽 파일 gpdp 파일 이거나 xml 파일 - const gpdpFiles = fs.readdirSync(studentDir).filter( - file => file.endsWith('.xml') - ); - - // 학생 이름을 key로 하는 객체 생성 - // 채점결과 - const scoringResult = { - 0: name - }; - - psdFiles.forEach((psdFile, index) => { - const psdPath = path.join('./', studentDir, psdFile); - console.log(`Reading ${psdPath}...`); - try { - const psdFileData = psd.fromFile(psdPath); - psdFileData.parse(); - psdData[index] = psdFileData; - scoringResult[index + 1] = getScore(psdData, scoringJson, index); - } catch (error) { - console.error(`Error reading PSD file: ${psdPath}`, error); - } + // 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴 + const studentDirs = fs.readdirSync(answerFilesDir).filter(file => { + const filePath = path.join(answerFilesDir, file); + return fs.statSync(filePath).isDirectory(); }); - gpdpFiles.forEach((gpdpFile, index) => { - const gpdpPath = path.join('./', studentDir, gpdpFile); - console.log(`Reading ${gpdpPath}...`); - const xmlString = fs.readFileSync(gpdpPath, 'utf8'); - // XML 문자열을 파싱하여 XML 문서 객체로 변환 - const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml'); - // console.log('xmlDocument:', xmlDocument); + // 채점 결과 리스트 + const scoringResultList = []; + const psdData = []; - scoringResult[index + 1] = getGpdpScore(xmlDocument, scoringJson, index + 4); - }); - gmepFile.forEach((gmep, index) => { - const gmepPath = path.join('./', studentDir, gmep); - console.log(`Reading ${gmepPath}...`); + studentDirs.forEach(student => { + // 맥에서 한글 디렉토리 이름을 읽어서 엑셀에 저장 할 시 자소 분리가 되어 저장되는 문제 노말라이즈해서 해결 + const name = student.normalize('NFC'); + const studentDir = path.join(answerFilesDir, student); + const psdFiles = fs.readdirSync(studentDir).filter(file => file.endsWith('.psd')); + // DIAT시험 프로젝트로 생성시 gmep확장자로 + // 교육용 프로젝프로 생성시 gmdp확장자로 생성됨 + // 두 경우 모두 처리 + const gmepFile = fs.readdirSync(studentDir).filter( + file => file.endsWith('.gmep') || file.endsWith('.gmdp') + ); - const xmlString = fs.readFileSync(gmepPath, 'utf8'); - // XML 문자열을 파싱하여 XML 문서 객체로 변환 - const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml'); - // console.log('xmlDocument:', xmlDocument); + // 곰픽 파일 gpdp 파일 이거나 xml 파일 + const gpdpFiles = fs.readdirSync(studentDir).filter( + file => file.endsWith('.xml') + ); - scoringResult[3] = getGmepScore(xmlDocument, scoringJson, 2); - }); - scoringResultList.push(scoringResult); -}); + // 학생 이름을 key로 하는 객체 생성 + // 채점결과 + const scoringResult = { + 0: name + }; -// // 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]; + psdFiles.forEach((psdFile, index) => { + const psdPath = path.join('./', studentDir, psdFile); + console.log(`Reading ${psdPath}...`); + try { + const psdFileData = psd.fromFile(psdPath); + psdFileData.parse(); + psdData[index] = psdFileData; + scoringResult[index + 1] = getScore(psdData, scoringJson, index); + } catch (error) { + console.error(`Error reading PSD file: ${psdPath}`, error); } }); + gpdpFiles.forEach((gpdpFile, index) => { + const gpdpPath = path.join('./', studentDir, gpdpFile); + console.log(`Reading ${gpdpPath}...`); - return flattened; - }); -} -// console.log(flattenedData); -const flattenedData = prepareExcelData(scoringResultList); + const xmlString = fs.readFileSync(gpdpPath, 'utf8'); + // XML 문자열을 파싱하여 XML 문서 객체로 변환 + const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml'); + // console.log('xmlDocument:', xmlDocument); -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]; // 각 학생의 데이터를 열로 추가 + scoringResult[index + 1] = getGpdpScore(xmlDocument, scoringJson, index + 4); }); - return row; + gmepFile.forEach((gmep, index) => { + const gmepPath = path.join('./', studentDir, gmep); + console.log(`Reading ${gmepPath}...`); + + const xmlString = fs.readFileSync(gmepPath, 'utf8'); + // XML 문자열을 파싱하여 XML 문서 객체로 변환 + const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml'); + // console.log('xmlDocument:', xmlDocument); + + scoringResult[3] = getGmepScore(xmlDocument, scoringJson, 2); + }); + scoringResultList.push(scoringResult); }); - return transposed; -} + const flattenedData = prepareExcelData(scoringResultList); + 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 workbook = XLSX.utils.book_new(); -// 엑셀 파일 생성 -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( + // key.length, // 열 제목의 길이 + // ...transposedData.map(row => (row[key] ? row[key].toString().length : 0)) // 각 셀의 데이터 길이 + 4 // 고정 너비 + ); + return { wch: maxLength + 1 }; // 여유 공간 추가 + }); -// 열 너비 계산 -const columnWidths = Object.keys(transposedData[0]).map(key => { - const maxLength = Math.max( - // key.length, // 열 제목의 길이 - // ...transposedData.map(row => (row[key] ? row[key].toString().length : 0)) // 각 셀의 데이터 길이 - 4 // 고정 너비 - ); - 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); }); - -// 열 너비 설정 -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; -} +console.log('채점 결과가 ' + outputExcelFiles + ' 파일에 저장되었습니다.'); // xml 형식의 gmep 파일을 읽어서 점수를 계산 // scoring.json 파일 내에 있는 ele 요소는 xpath 형식으로 접근하여 요소를 탐색하고 나오는 값을 value와 비교하여 점수를 계산 @@ -772,3 +629,134 @@ function getScore(psdData, scoring, index) { scoringResult['총점'] = totalScore; 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; +} +