diff --git a/DIC_2505A.json b/DIC_2505A.json index dc79026..6dfc21a 100644 --- a/DIC_2505A.json +++ b/DIC_2505A.json @@ -229,8 +229,9 @@ "desc": "value값의 키값(VID___)은 이펙트의 속성종류에 따라 변경되므로 채점기준표작성시 확인 필요" }, "5": { - "ele": "//CRCUnitArr[@Name='{search}']/@Name", - "ele2": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr/@Name", + "ele": "//CROwneUnit[{index}]/CRCUnitArr/@Name", + "ele2": "//CRCUnitArr[@Name='{search}']/@Name", + "ele3": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr/@Name", "type": "video", "value": "모래 촉감 놀이", "search": "모래 촉감 놀이", diff --git a/DIC_2505B.json b/DIC_2505B.json index cf1e9e2..bd26536 100644 --- a/DIC_2505B.json +++ b/DIC_2505B.json @@ -211,7 +211,7 @@ "media": "동영상.mp4", "value": { "start": "0", - "end": "360" + "end": "380" }, "point": 2, "desc": "시작시간과 재생시간 정답값 입력, 3번문항은 '동영상.mp4' 클립의 길이를 확인하는 문항이므로 media는 수정할 필요가 없다." @@ -229,59 +229,56 @@ "desc": "value값의 키값(VID___)은 이펙트의 속성종류에 따라 변경되므로 채점기준표작성시 확인 필요" }, "5": { - "ele": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr/@Name", - "ele2": "//CRCUnitArr[@Name='{search}']/@Name", - "type": "video", - "value": "청량하고 시원한 폭포", + "ele": "//CROwneUnit[{index}]/CRCUnitArr/@Name", "search": "청량하고 시원한 폭포", + "type": "video.Text", + "value": "청량하고 시원한 폭포", "point": 3 }, "6": { - "ele": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr//GCUnitPool[@Type='1']/GCUnit/@VID102", - "ele2": "//CRCUnitArr[@Name='{search}']//GCUnitPool[@Type='1']/GCUnit/@VID102", + "ele": "//CROwneUnit[{index}]/CRCUnitArr//GCUnitPool[@Type='1']/GCUnit/@VID102", "search": "청량하고 시원한 폭포", - "type": "video", + "type": "video.Text", "value": "돋움체", "point": 2 }, "7": { - "ele": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr//GCUnitPool[@Type='1']/GCUnit/@VID101", - "ele2": "//CRCUnitArr[@Name='{search}']//GCUnitPool[@Type='1']/GCUnit/@VID101", + "ele": "//CROwneUnit[{index}]/CRCUnitArr//GCUnitPool[@Type='1']/GCUnit/@VID101", "search": "청량하고 시원한 폭포", - "type": "video", + "type": "video.Text", "value": "120", "point": 2 }, "8": { - "ele": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr//GCUnitPool/GCUnit[@Type='4']/@VID100", - "ele2": "//CRCUnitArr[@Name='{search}']//GCUnitPool/GCUnit[@Type='4']/@VID100", + "ele": "//CROwneUnit[{index}]/CRCUnitArr//GCUnitPool/GCUnit[@Type='4']/@VID100", "search": "청량하고 시원한 폭포", - "type": "video", - "value": "-937955", - "point": 2 + "type": "video.Text.Color", + "value": "1db0f1", + "point": 2, + "desc": "컬러값은 RGB로 입력한다, [대소문자, #]허용 (#FFFFFF, ffffff 두 값 모두 허용)" }, "9": { - "ele": "//CROwneUnit[{subtitleIndex}]/CRCUnitArr/@*[name()='VID600' or name()='VID601']", - "ele2": "//CRCUnitArr[@Name='{search}']/@*[name()='VID600' or name()='VID601']", - "type": "video", + "ele": "//CROwneUnit[{index}]/CRCUnitArr/@*[name()='VID600' or name()='VID601']", "search": "청량하고 시원한 폭포", + "type": "video.Location", "value": [ - 0.27395833, - 0.9222222 + "0.27395833", + "0.9222222" ], - "point": 2 + "point": 2, + "desc": "정답 파일의 자막 좌표를 기준으로 프로그램 내부적으로 0.1까지 오차를 허용한다" }, "10": { - "ele": "{search}", + "ele": "", "search": "청량하고 시원한 폭포", - "type": "videoStartTime", + "type": "video.StartTime", "value": 170, "point": 2 }, "11": { - "ele": "{search}", + "ele": "", "search": "청량하고 시원한 폭포", - "type": "videoLength", + "type": "video.Length", "value": 150, "point": 2 }, diff --git a/psdExport_2.js b/psdExport_2.js index 8b05282..9db6f7d 100644 --- a/psdExport_2.js +++ b/psdExport_2.js @@ -172,12 +172,24 @@ outputExcelFiles.forEach((outputFile, index) => { // scoring.json 파일 내에 있는 type에 따라 비교하는 방식이 달라짐 // 채점 결과를 scoringResultList 배열에 저장 function getGmepScore(gmepData, scoringJson, index) { - function compareAndScore(userAnswer, rightAnswer, point, key, scoringResult) { + function compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, tolerance = 0) { let score = 0; - const isEqual = (typeof rightAnswer === "object" && typeof userAnswer === "object") - ? JSON.stringify(userAnswer) === JSON.stringify(rightAnswer) - : userAnswer == rightAnswer; + let isEqual; + + if (Array.isArray(rightAnswer) && Array.isArray(userAnswer)) { + // 배열 길이 같아야 비교 가능 + if (rightAnswer.length === userAnswer.length) { + isEqual = rightAnswer.every((val, idx) => Math.abs(val - userAnswer[idx]) <= tolerance); + } else { + isEqual = false; + } + } else if (typeof rightAnswer === "object" && typeof userAnswer === "object") { + // 객체일 때는 기존 방식 유지 (원하면 별도 로직 추가 가능) + isEqual = JSON.stringify(userAnswer) === JSON.stringify(rightAnswer); + } else { + isEqual = userAnswer == rightAnswer; + } if (isEqual) { score = point; @@ -192,6 +204,25 @@ function getGmepScore(gmepData, scoringJson, index) { return score; } + function convertColorToHex(colorValue) { + // 문자열이면 정수로 변환 + const intValue = typeof colorValue === 'string' ? parseInt(colorValue, 10) : colorValue; + + // 부호 없는 32비트 정수로 변환 → 8자리 16진수 문자열로 + const hex = (intValue >>> 0).toString(16).padStart(8, '0'); + + // 하위 6자리 추출 (BGR 순서) + const bgr = hex.slice(2); // 예: "fff1b01d" → "f1b01d" + + // BGR → RGB로 재배열 + const b = bgr.slice(0, 2); + const g = bgr.slice(2, 4); + const r = bgr.slice(4, 6); + + // RGB 순서로 합치고 소문자로 반환 + return (r + g + b).toLowerCase(); // e.g., "fd5721" + } + const gmepXmlDoc = gmepData; const scoringResult = {}; @@ -320,7 +351,7 @@ function getGmepScore(gmepData, scoringJson, index) { const rightAnswer = scoringData[key].value; const point = scoringData[key].point; const type = scoringData[key].type; - const search = scoringData[key].search; + let search = scoringData[key].search; const media = scoringData[key].media; const videoStartTime = scoringData.videoStartTime; const openingStartTime = scoringData.openingStartTime; @@ -333,15 +364,18 @@ function getGmepScore(gmepData, scoringJson, index) { const trackClipNode = getTrackClipNode(gmepXmlDoc, type, videoStartTime, openingStartTime); const subtitleIndex = trackClipNode ? parseInt(trackClipNode.getAttribute('ClipIndex'), 10) + 1 : null; const textClipIndex = getTextClipIndex(gmepXmlDoc, search); - const typeToOrderMap = { - opening: 1, - openingStartTime: 1, - openingLength: 1, - video: 2, - videoStartTime: 2, - videoLength: 2, - }; - const subtitleOrder = typeToOrderMap[type] ?? null; + // const typeToOrderMap = { + // opening: 1, + // openingStartTime: 1, + // openingLength: 1, + // openingText: 1, + // video: 2, + // videoStartTime: 2, + // videoLength: 2, + // videoText: 2, + // }; + // const subtitleOrder = typeToOrderMap[type] ?? null; + const subtitleOrder = type.includes('opening') ? 1 : type.includes('video') ? 2 : null; const startTime = type === 'video' ? videoStartTime : type === 'opening' ? openingStartTime : null; let xpathList = [ele, ele2, ele3, existEle]; @@ -367,7 +401,8 @@ function getGmepScore(gmepData, scoringJson, index) { let result = findSimilarString(gmepXmlDoc, search, 0.8); if (result !== null) { result = result.replace(/"/g, "'"); - [ele, ele2, ele3, existEle] = [ele, ele2, ele3, existEle].map(e => e?.replace(/{search}/g, result)); + search = result; + [ele, ele2, ele3, existEle] = [ele, ele2, ele3, existEle].map(e => e?.replace(/{search}/g, search)); } else { [ele, ele2, ele3] = [ele, ele2, ele3].map(e => e?.includes('{search}') ? null : e); } @@ -488,13 +523,21 @@ function getGmepScore(gmepData, scoringJson, index) { } } - else if (type === 'openingStartTime' || type === 'openingLength' - || type === 'videoStartTime' || type === 'videoLength') { + // 자막관련 type검사하는 구문을 opening포함 video포함으로 변경 + // 6/16(월)시작 지점 + // 1. JSON파일 10,11번 처럼 변경 + else if (type.includes('opening') || type.includes('video')) { + function toHexColor(value) { + + } + // else if (type === 'openingStartTime' || type === 'openingLength' + // || type === 'videoStartTime' || type === 'videoLength') { + // 자막의 정보를 이용해 CROwneUnit의 인덱스를 구함 // 1. 텍스트 // 2. 순서 // 3. 시작시간 - const indexByText = getClipIndexByText(ele); + const indexByText = getClipIndexByText(search); const indexByOrder = getClipIndexByOrder(subtitleOrder); if (type.includes('opening')) time = openingStartTime; @@ -505,29 +548,62 @@ function getGmepScore(gmepData, scoringJson, index) { // 1, 2, 3순으로 자막을 찾음 const index = indexByText ?? indexByOrder ?? indexByStartTime; if (index != null) { - // 자막 시작시간 + // 자막 시작시간 [2-10] [2-28] if (type.includes('StartTime')) { const crtrackClipIndex = getCrtrackClipIndex(index) const startTimeList = getSubtitleStartTime(); const startTime = startTimeList[crtrackClipIndex]; - userAnswer = startTime; - } - // 자막 길이 + userAnswer = startTime; + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + } + // 자막 길이 [2-11] [2-29] else if (type.includes('Length')) { const crtrackClipIndex = getCrtrackClipIndex(index) + 1 // XML 1-based index const clipLength = xpath.select1(`//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[${crtrackClipIndex}]/@Length`, gmepXmlDoc); userAnswer = parseInt(clipLength.value, 10); + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + } + // 자막 텍스트(글자, 폰트, 크기, 색상) [2-5, 6, 7, 8] [2-22, 23, 24, 25] + else if (type.includes('Text') || type.includes('Color')) { + const xmlIndex = index + 1 // XML 1-based index + const subtitleXpath = ele?.replace(/{index}/g, xmlIndex); + const subtitleResult = xpath.select1(subtitleXpath, gmepXmlDoc); + + if (subtitleResult) { + if (type.includes('Color')) { + const hex = convertColorToHex(subtitleResult.value); + userAnswer = hex; + } + else { + userAnswer = subtitleResult.value; + } + } else { + userAnswer = null; + } + + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + } + + // 자막 위치 [2-9] (화면 정가운데 아래) + // 정답 좌표를 기준으로 + else if (type.includes('Location')) { + const xmlIndex = index + 1 // XML 1-based index + const subtitleXpath = ele?.replace(/{index}/g, xmlIndex); + const subtitleResult = xpath.select(subtitleXpath, gmepXmlDoc); + + userAnswer = subtitleResult.map(r => r.value); + const errorRange = 0.1; + + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, tolerance = errorRange); } } else { userAnswer = null; } - - totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); } - + else if (type == "color") { const result = xpath.select(ele, gmepXmlDoc); diff --git a/z.xbook b/z.xbook index 80492a9..ebe66f0 100644 --- a/z.xbook +++ b/z.xbook @@ -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":"//CRTrackList[@Name='텍스트' or @Name='비디오2']/CRTrackClip[1]/@Length"},{"kind":2,"language":"xpath","value":"//CRClipArr/CRClip[@Type='11']/CRCUnitArr/@Path | //CRClipArr/CRClip[not(@Type='11')]/@Path"}] \ No newline at end of file +[{"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":"//CROwneUnit[1]/CRCUnitArr/@*[name()='VID600' or name()='VID601']"},{"kind":2,"language":"xpath","value":"//CRClipArr/CRClip[@Type='11']/CRCUnitArr/@Path | //CRClipArr/CRClip[not(@Type='11')]/@Path"}] \ No newline at end of file