곰믹스 소스코드 수정

This commit is contained in:
2025-05-30 18:05:46 +09:00
parent 778ea2d3aa
commit b17a33e1d7
11 changed files with 126 additions and 97 deletions

BIN
00_DIC_2505A_TEST.xlsx Normal file

Binary file not shown.

View File

@@ -39,7 +39,7 @@ def copy_dic_subdirs(source_root, target_root_a, target_root_b, target_root_c, t
# 사용법 # 사용법
exam_round = "2505" exam_round = "2505"
source_directory = r"C:\Users\dra\project\GOM\DIC\회차별채점자료\2505\" # 원본 디렉토리 경로 source_directory = r"C:\Users\dra\project\data\2505회 정기\안파일" # 원본 디렉토리 경로
target_directory_a = f".\\output\\{exam_round}\\A" # '1교시'의 타겟 경로 target_directory_a = f".\\output\\{exam_round}\\A" # '1교시'의 타겟 경로
target_directory_b = f".\\output\\{exam_round}\\B" # '2교시'의 타겟 경로 target_directory_b = f".\\output\\{exam_round}\\B" # '2교시'의 타겟 경로

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -184,26 +184,36 @@
"videoStartTime": 170, "videoStartTime": 170,
"openingStartTime": 0, "openingStartTime": 0,
"1": { "1": {
"ele": "//CRClipArr/CRClip[position() = //CRTrackList[1]/CRTrackClip/@ClipIndex]/@Path", "ele": "//CRTrackList[@Name='비디오1']/CRTrackClip[not(@Length<='5' and @ClipLength='-1')]/@ClipIndex",
"type": "array", "type": "mediaOrder",
"value": [ "value": [
"동영상.mp4", "동영상.mp4",
"이미지3.jpg", "이미지3.jpg",
"이미지1.jpg", "이미지1.jpg",
"이미지2.jpg" "이미지2.jpg"
], ],
"point": 4 "point": 4,
"desc": "비디오1 트랙에 있는 클립의 ClipIndex값을 기준으로 CRClipArr에서 Path값을 가져와서 정답 채점, 클립의 ClipIndex값이 -1인 경우와 길이가 5이하인 경우는 제외한다."
}, },
"2": { "2": {
"ele": "/CROASTERP/CRTrackArr[1]/CRVideoTrackArr[1]/CRTrackList[1]/CRTrackClip[1][@Speed='130']", "ele": "/CROASTERP/CRTrackArr[1]/CRVideoTrackArr[1]/CRTrackList[1]/CRTrackClip[1]/@Speed",
"point": 2 "type": "oneAnswer",
"value": {
"speed": "130"
},
"point": 2,
"desc": "100당 1배속 / 130 = 1.3배속"
}, },
"3": { "3": {
"ele": "count(//CRClip[@Path='동영상.mp4']/preceding-sibling::*)", "ele": "//CRClipArr/CRClip[@Type='11']/CRCUnitArr/@Path | //CRClipArr/CRClip[not(@Type='11')]/@Path",
"type": "startend", "type": "startEnd",
"start": "0", "media": "동영상.mp4",
"end": "360", "value": {
"point": 2 "start": "0",
"end": "360"
},
"point": 2,
"desc": "시작시간과 재생시간 정답값 입력, 3번문항은 '동영상.mp4' 클립의 길이를 확인하는 문항이므로 media는 수정할 필요가 없다."
}, },
"4": { "4": {
"ele": "//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex=count(//CRClip[@Path='동영상.mp4']/preceding-sibling::*)]//CRFilter[@ID='168'][@VID100='0.80000001'][@VID102='10']", "ele": "//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex=count(//CRClip[@Path='동영상.mp4']/preceding-sibling::*)]//CRFilter[@ID='168'][@VID100='0.80000001'][@VID102='10']",

View File

@@ -15,15 +15,15 @@ const examRound = '2505';
const dic_or_dpi = 'DIC' const dic_or_dpi = 'DIC'
// const dic_or_dpi = 'DPI' // const dic_or_dpi = 'DPI'
const examTypes = [ const examTypes = [
// 'A', 'A',
'B', // 'B',
'C', // 'C',
// 'D' // 'D'
]; ];
// testMode가 true일 경우 TEST 폴더에 있는 답안 파일을 읽어옴 // testMode가 true일 경우 TEST 폴더에 있는 답안 파일을 읽어옴
// const testMode = true; // const testMode = false;
const testMode = false; const testMode = true;
const outputExcelFiles = []; const outputExcelFiles = [];
@@ -39,7 +39,6 @@ examTypes.forEach(type => {
// let outputExcelFile = `./${todayDate}_DIC_${examRound}${type}_채점결과.xlsx`; // let outputExcelFile = `./${todayDate}_DIC_${examRound}${type}_채점결과.xlsx`;
// if (testMode) { // if (testMode) {
// outputExcelFile = `./00_DIC_${examRound}${type}_TEST.xlsx`; // outputExcelFile = `./00_DIC_${examRound}${type}_TEST.xlsx`;
// }
// 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴 // 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴
@@ -81,7 +80,9 @@ examTypes.forEach(type => {
psdFiles.forEach((psdFile, index) => { psdFiles.forEach((psdFile, index) => {
const psdPath = path.join('./', studentDir, psdFile); const psdPath = path.join('./', studentDir, psdFile);
console.log(`Reading ${psdPath}...`);
console.log('');
console.log(`➡️ Reading ${psdPath}...`);
try { try {
const psdFileData = psd.fromFile(psdPath); const psdFileData = psd.fromFile(psdPath);
psdFileData.parse(); psdFileData.parse();
@@ -116,7 +117,9 @@ examTypes.forEach(type => {
else { else {
gmepFile.forEach((gmep, index) => { gmepFile.forEach((gmep, index) => {
const gmepPath = path.join('./', studentDir, gmep); const gmepPath = path.join('./', studentDir, gmep);
console.log(`Reading ${gmepPath}...`);
console.log('');
console.log(`➡️ Reading ${gmepPath}...`);
const xmlString = fs.readFileSync(gmepPath, 'utf8'); const xmlString = fs.readFileSync(gmepPath, 'utf8');
// XML 문자열을 파싱하여 XML 문서 객체로 변환 // XML 문자열을 파싱하여 XML 문서 객체로 변환
@@ -169,6 +172,30 @@ outputExcelFiles.forEach((outputFile, index) => {
// scoring.json 파일 내에 있는 type에 따라 비교하는 방식이 달라짐 // scoring.json 파일 내에 있는 type에 따라 비교하는 방식이 달라짐
// 채점 결과를 scoringResultList 배열에 저장 // 채점 결과를 scoringResultList 배열에 저장
function getGmepScore(gmepData, scoringJson, index) { function getGmepScore(gmepData, scoringJson, index) {
function compareAndScore(userAnswer, rightAnswer, point, key, scoringResult) {
let score = 0;
const isEqual = (typeof rightAnswer === "object" && typeof userAnswer === "object")
? JSON.stringify(userAnswer) === JSON.stringify(rightAnswer)
: userAnswer == rightAnswer;
if (isEqual) {
score = point;
console.log('작성답안: ', userAnswer);
console.log('>⭕ 정답: ', rightAnswer);
} else {
console.log('작성답안: ', userAnswer);
console.log('>❌ 오답: ', rightAnswer);
}
scoringResult[key] = score;
return score;
}
function getMediaOrderbyClipIndex(gmepXmlDoc, clipIndex) {
}
const gmepXmlDoc = gmepData; const gmepXmlDoc = gmepData;
const scoringResult = {}; const scoringResult = {};
@@ -188,6 +215,7 @@ function getGmepScore(gmepData, scoringJson, index) {
const point = scoringData[key].point; const point = scoringData[key].point;
const type = scoringData[key].type; const type = scoringData[key].type;
const search = scoringData[key].search; const search = scoringData[key].search;
const media = scoringData[key].media;
const videoStartTime = scoringData.videoStartTime; const videoStartTime = scoringData.videoStartTime;
const openingStartTime = scoringData.openingStartTime; const openingStartTime = scoringData.openingStartTime;
@@ -204,14 +232,14 @@ function getGmepScore(gmepData, scoringJson, index) {
const subtitleOrder = type === 'video' ? 2 : type === 'opening' ? 1 : null; const subtitleOrder = type === 'video' ? 2 : type === 'opening' ? 1 : null;
const startTime = type === 'video' ? videoStartTime : type === 'opening' ? openingStartTime : null; const startTime = type === 'video' ? videoStartTime : type === 'opening' ? openingStartTime : null;
let xpathList = [ele, ele2, ele3, existEle]; let xpathList = [ele, ele2, ele3, existEle];
xpathList = xpathList.map(e => e?e xpathList = xpathList.map(e => e ? e
.replace(/{subtitleIndex}/g, subtitleIndex) .replace(/{subtitleIndex}/g, subtitleIndex)
.replace(/{subtitleOrder}/g, subtitleOrder) .replace(/{subtitleOrder}/g, subtitleOrder)
.replace(/{startTime}/g, startTime) .replace(/{startTime}/g, startTime)
.replace(/{clipIndex}/g, clipIndex) .replace(/{clipIndex}/g, clipIndex)
.replace(/{image}/g, image) .replace(/{image}/g, image)
:e : e
); );
[ele, ele2, ele3, existEle] = xpathList; [ele, ele2, ele3, existEle] = xpathList;
@@ -253,92 +281,83 @@ function getGmepScore(gmepData, scoringJson, index) {
if (type == "boolean") { if (type == "boolean") {
scoringResult[key] = result.length > 0 ? point : 0; scoringResult[key] = result.length > 0 ? point : 0;
} }
else if (type == "array") {
// result: Path="동영상.mp4", Path="음악.mp3", Path="이미지2.jpg", Path="이미지1.jpg"
// value: 동영상.mp4,이미지1.jpg,이미지3.jpg,이미지2.jpg
// result 와 value를 순서대로 비교하여 모두 같으면 점수 point 부여
// XPath를 사용하여 CRTrackList Name="비디오1" 요소 찾기 else if (type == "oneAnswer") {
const trackListNode = xpath.select1('//CRVideoTrackArr/CRTrackList[@Name="비디오1"]', gmepXmlDoc); const result = xpath.select1(ele, gmepXmlDoc);
const values = [];
let isSame = true;
if (trackListNode) { let userAnswer = {};
// CRTrackClip 요소의 ClipIndex를 참조하여 CRClip 요소의 Path와 Type 출력 if ("speed" in rightAnswer) {
// @Length(클립재생길이) 5프레임 이하, @ClipLength -1인 항목은 제외 userAnswer = {
// 10프레임은 타임라인상 눈에 잘 보여서 5프레임으로 우선 수정 "speed": result ? result.value : null,
const clipIndexes = xpath.select('CRTrackClip[not(@Length<="5" and @ClipLength="-1")]/@ClipIndex' };
, trackListNode);
clipIndexes.forEach(indexNode => {
const clipIndex = parseInt(indexNode.value, 10) + 1; // XPath는 1-based index를 사용
console.log(`clipIndex: ${clipIndex}`);
if (clipIndex === 0) {
return;
}
const clipPathNode = xpath.select1(`//CRClipArr/CRClip[${clipIndex}]/@Path`, gmepXmlDoc);
const motionClipPathNode = xpath.select1(`//CRClipArr/CRClip[${clipIndex}]/CRCUnitArr/@Path`, gmepXmlDoc);
const notUndefinedClipNode = clipPathNode ?? motionClipPathNode;
if (notUndefinedClipNode === undefined) {
return;
}
values.push(notUndefinedClipNode.value);
});
// values에 값이 있는지 확인
if (values.length == 0 || values.length < 4) {
console.log('values length 0');
scoringResult[key] = 0;
continue;
}
values.forEach((v, i) => {
console.log(`values: ${v} value: ${rightAnswer[i]}`);
if (rightAnswer[i] !== v) {
isSame = false;
}
});
totalScore += isSame ? point : 0;
scoringResult[key] = isSame ? point : 0;
} else {
console.log('CRTrackList with Name="비디오1" not found.');
scoringResult[key] = 0;
} }
else {
userAnswer = result ? result.value : null;
}
totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
} }
else if (type == "startend") { // [3-1] 문항
console.log('type:', type);
const start = scoringData[key].start;
const end = scoringData[key].end;
// XPath를 사용하여 CRClip 요소 중 Path가 '동영상.mp4'인 요소의 위치 찾기
const clipIndexNode = xpath.select1(ele, gmepXmlDoc);
console.log(`clipIndexNode: ${clipIndexNode}`);
// XPath를 사용하여 해당 ClipIndex를 사용하는 CRTrackClip 요소 찾기 else if (type == "mediaOrder") {
const trackClipNodes = xpath.select1(`//CRTrackClip[@ClipIndex='${clipIndexNode}']`, gmepXmlDoc); // 미디어 순서를 저장할 배열
const mediaOrderList = [];
if (!trackClipNodes) { // 미디어의 인덱스 순서
const clipIndexOrder = xpath.select(ele, gmepXmlDoc);
clipIndexOrder.forEach((clipIndex) => {
CRClipIndex = parseInt(clipIndex.value, 10) + 1; // XPath는 1-based index를 사용
// 인덱스 순서에 따른 CRClip 요소의 Path를 찾기
const mediaPath = xpath.select1(`//CRClipArr/CRClip[${CRClipIndex}]/@Path`, gmepXmlDoc);
// 만약 CRClip 요소가 motion clip인 경우 CRCUnitArr의 Path를 찾기
if (mediaPath == null) {
const motionClipPath = xpath.select1(`//CRClipArr/CRClip[${CRClipIndex}]/CRCUnitArr/@Path`, gmepXmlDoc);
if (motionClipPath !== null) {
mediaOrderList.push(motionClipPath.value);
}
}
else if (mediaPath != null) {
mediaOrderList.push(mediaPath.value);
}
});
const userAnswer = mediaOrderList;
totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
}
else if (type == "startEnd") {
// CRClipArr/CRClip 요소의 Path속성 리스트를 구함
// 모션 클립 이미지도 고려해 처리
const mediaPathList = xpath.select("//CRClipArr/CRClip[@Type='11']/CRCUnitArr/@Path | //CRClipArr/CRClip[not(@Type='11')]/@Path", gmepXmlDoc);
// "동영상.mp4"의 clipIndex를 구함
let clipIndex = mediaPathList.findIndex(mediaPath => mediaPath.value === media);
// clipIndex가 -1이면 해당 미디어가 존재하지 않는 것
if (clipIndex === -1) {
scoringResult[key] = 0; scoringResult[key] = 0;
continue; continue;
} }
const posNode = xpath.select1('@Pos', trackClipNodes);
const lengthNode = xpath.select1('@Length', trackClipNodes);
console.log(`Pos: ${posNode.value}, Length: ${lengthNode.value}`);
scoringResult[key] = posNode.value === start && lengthNode.value === end ? point : 0; else {
totalScore += posNode.value === start && lengthNode.value === end ? point : 0; // //CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='동영상.mp4'] 요소를 찾음
const trackClipNode = xpath.select1(`//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='${clipIndex}']`, gmepXmlDoc);
if (!trackClipNode) {
scoringResult[key] = 0;
continue;
}
else {
// CRTrackClip 요소의 Pos(시작시간)과 Length(재생길이)를 구함
const pos = xpath.select1('@Pos', trackClipNode);
const length = xpath.select1('@Length', trackClipNode);
const userAnswer = { start: pos.value, end: length.value }
totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult);
}
}
} }
// else if (type == "video") {
// const result = xpath.select(ele, gmepXmlDoc);
// const length = scoringData[key].length;
// // 결과는 배열로 나오는데 2개 일 경우가 있음
// if (result.length !== length) {
// scoringResult[key] = 0;
// continue;
// }
// scoringResult[key] = point;
// totalScore += point;
// }
else if (type == "color") { else if (type == "color") {
const result = xpath.select(ele, gmepXmlDoc); const result = xpath.select(ele, gmepXmlDoc);

View File

@@ -1 +1 @@
[{"kind":2,"language":"xpath","value":"//Layer[Name[@value='Tracking']]/Effects/Item/Name/@value"},{"kind":2,"language":"xpath","value":"sum(//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[@ClipIndex=0]/preceding-sibling::CRTrackClip/@Length)"},{"kind":2,"language":"xpath","value":"//CROwneUnit/CRCUnitArr[@Name=\"아름다운 꽃 축제 (Happy Flower Festival)\"]/@Name"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='{layer}']]/Effects/Item[EffectData/{option}]/Name/@value | //Layer[Name[@value='{layer}']]/Effects/Item/EffectData/{option}/@value"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='{layer}']]/Effects/Item/Name/@value | //Layer[Name[@value='{layer}']]/Effects/Item/EffectData/{option}/@value\r\n"},{"kind":2,"language":"xpath","value":"//CRTransFilter[@ClipIndex=count(//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex=count(//CRClip[@Path='이미지3.jpg']/preceding-sibling::CRClip | //CRClip[@Type='11']/CRCUnitArr[@Path='이미지3.jpg']/../preceding-sibling::CRClip)][1]/preceding-sibling::CRTrackClip)][@Type='2']/@*[name()='ID' or name()='Range' or name()='Type']"},{"kind":2,"language":"xpath","value":"sum(//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[not(@ClipIndex='-1')][2]/preceding-sibling::CRTrackClip/@Length)"},{"kind":2,"language":"xpath","value":"sum(//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[not(@ClipIndex='-1')][@ClipIndex=0]/preceding-sibling::CRTrackClip/@Length)"}] [{"kind":2,"language":"xpath","value":"//Layer[Name[@value='Tracking']]/Effects/Item/Name/@value"},{"kind":2,"language":"xpath","value":"sum(//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[@ClipIndex=0]/preceding-sibling::CRTrackClip/@Length)"},{"kind":2,"language":"xpath","value":"//CROwneUnit/CRCUnitArr[@Name=\"아름다운 꽃 축제 (Happy Flower Festival)\"]/@Name"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='{layer}']]/Effects/Item[EffectData/{option}]/Name/@value | //Layer[Name[@value='{layer}']]/Effects/Item/EffectData/{option}/@value"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='{layer}']]/Effects/Item/Name/@value | //Layer[Name[@value='{layer}']]/Effects/Item/EffectData/{option}/@value\r\n"},{"kind":2,"language":"xpath","value":"//CRTransFilter[@ClipIndex=count(//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex=count(//CRClip[@Path='이미지3.jpg']/preceding-sibling::CRClip | //CRClip[@Type='11']/CRCUnitArr[@Path='이미지3.jpg']/../preceding-sibling::CRClip)][1]/preceding-sibling::CRTrackClip)][@Type='2']/@*[name()='ID' or name()='Range' or name()='Type']"},{"kind":2,"language":"xpath","value":"//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='1']"},{"kind":2,"language":"xpath","value":"//CRClipArr/CRClip[@Type='11']/CRCUnitArr/@Path | //CRClipArr/CRClip[not(@Type='11')]/@Path"}]