const xpath = require('xpath'); const { DOMParser } = require('xmldom'); function parseColorToHex(colorString) { // 정규식을 사용하여 B, G, R, A 값 추출 const regex = /B:\s*(\d+),\s*G:\s*(\d+),\s*R:\s*(\d+),\s*A:\s*(\d+)/; const matches = colorString.match(regex); if (!matches) { throw new Error('Invalid color string format'); } // matches[1]은 B, matches[2]는 G, matches[3]은 R, matches[4]는 A const [_, b, g, r, a] = matches; // 각 값을 16진수로 변환하고 2자리로 패딩 const rHex = parseInt(r).toString(16).padStart(2, '0'); const gHex = parseInt(g).toString(16).padStart(2, '0'); const bHex = parseInt(b).toString(16).padStart(2, '0'); const aHex = parseInt(a).toString(16).padStart(2, '0'); // #RRGGBBAA 형식으로 반환 return `${rHex}${gHex}${bHex}`; } module.exports = getGpdpScore; /** * /Document/Layers/Layer/Shapes/Shape/draw_type 속성값 * > Interior: 내부 채우기 / Outline: 외곽선 * /Document/Layers/Layer/Shapes/Shape/interior_type 속성값 * > Fill: 채우기 / Gradient: 그라데이션 * @param {*} gpdpData * @param {*} scoringJson * @param {*} index * @returns */ // xml 형식의 GPDP 파일을 읽어서 점수를 계산 // scoring.json 파일 내에 있는 ele 요소는 xpath 형식으로 접근하여 요소를 탐색하고 나오는 값을 value와 비교하여 점수를 계산 // scoring.json 파일 내에 있는 type은 비교할 값의 타입을 의미하며, boolean, array 등이 있음 // scoring.json 파일 내에 있는 type에 따라 비교하는 방식이 달라짐 // 채점 결과를 scoringResultList 배열에 저장 function getGpdpScore(gpdpData, scoringJson, index) { const gpdpXmlDoc = gpdpData; const scoringResult = {}; const scoringData = scoringJson[index]; // console.log(scoringData); const layerList = xpath.select('/Document/Layers/Layer', gpdpXmlDoc); layerList.forEach((layer, layerIndex) => { const childNodes = Array.from(layer.childNodes); childNodes.forEach(child => { // console.log("🚀 ~ child node:", child); }); }); // gpdp 필요한 데이터 필터링 function getLayerData(gpdpXmlDoc) { /** * 3)레이어 마스크 여부 * 도형 * 5) 모양 (모서리가 둥근 사각형:ROUNDED_RECTANGLE / 사각형:RECTANGLE ) * 6) 크기 * 7) 그라데이션 (색상) * 텍스트 * 8) 어린이 과학관 * 9) 글꼴(돋움) * 10) 글꼴 스타일(기울임꼴) * 11) 크기(32pt) * 12) 채우기(색상 : F04DA5) * 13) 외곽선(두께 : 3.00px) * 14) 외곽선(색상 : FFF000) * 15) 클리핑 마스크 설정 * 16) 원형/타원형 * 17) 크기 : 150 × 150 * 18) 외곽선(두께 : 7.00px) * 19) 외곽선(색상 : 008878) * 20) 그림자(두께 : 5.00px, 거리 : 3.00px, 분산도 : 1px, 각도 : 320°) * */ } let totalScore = 0; // 채점기준표 문항별 분류 for (const key in scoringData) { let ele = scoringData[key].ele; let ele2 = scoringData[key].ele2; let existEle = scoringData[key].existEle; let rightAnswer = scoringData[key].value; let point = scoringData[key].point; let type = scoringData[key].type; let search = scoringData[key].search; const layer = scoringData[key].layer; const option = scoringData[key].option; const style = scoringData[key].style; ele = typeof ele === 'string' ? ele.replace(/{layer}/g, layer) : ele; ele = typeof ele === 'string' ? ele.replace(/{option}/g, option) : ele; ele = typeof ele === 'string' ? ele.replace(/{style}/g, style) : ele; if (search !== undefined) { let result = findSimilarString(gpdpXmlDoc, search, 0.8) // xpath 내부 "(큰따옴표) 필터링 if (result !== null) { result = result.replace(/"/g, "'"); } ele = ele.replace(/{search}/g, result); if (existEle !== undefined) { existEle = existEle.replace(/{search}/g, result); } } console.log(`example number: ${key}`) console.log("🚀 ~ getGpdpScore ~ ele:", ele) // if (type == "effects") { // // Layer/Effects/Item요소를 가져옴 // const items = xpath.select(ele, gpdpXmlDoc); // // let isRight = false; // // 각 Item 요소별 이름과 속성값을 구하고 정답과 비교 // items.forEach((item) => { // const name = xpath.select1('Name/@value', item)?.value; // const value = xpath.select1(`EffectData/${option?.replace(/"/g, '')}/@value`, item)?.value; // const resultArray = [name, value]; // // if (isRight) return; // if (JSON.stringify(resultArray) === JSON.stringify(rightAnswer)) { // totalScore += point; // scoringResult[key] = point; // console.log("🚀 ~ 정답 일치:", rightAnswer); // // isRight = true; // return; // } else { // scoringResult[key] = 0; // console.log("🚀 ~ 오답:", rightAnswer); // } // }); // } if (type == "boolean") { const items = xpath.select(ele, gpdpXmlDoc); // xpath 결과값을 반환하는 요소가 없을 경우 if (!items) { scoringResult[key] = 0; console.log("❌ 찾는 요소 없음"); } else { totalScore += point; scoringResult[key] = point; console.log("✅ 찾는 요소 존재함"); } } // 이펙트 효과의 이름과 속성값을 비교 else if (type == "effects") { const items = xpath.select(ele, gpdpXmlDoc); let matched = false; // 각 Item 요소별 이름과 속성값을 구하고 정답과 비교 for (const item of items) { const name = xpath.select1('Name/@value', item)?.value; const attr = xpath.select1(`EffectData/${option?.replace(/"/g, '')}/@value`, item)?.value; if (name === rightAnswer[0] && attr === rightAnswer[1]) { totalScore += point; scoringResult[key] = point; matched = true; console.log("✅ 정답 일치:", rightAnswer); break; } } if (!matched) { scoringResult[key] = 0; console.log("❌ 정답 없음:", rightAnswer); } } else if (type == "multiValue") { if (Array.isArray(rightAnswer)) { const result = ele ? xpath.select(ele, gpdpXmlDoc) : []; const resultValues = Array.isArray(result) ? result.map(r => (typeof r === 'object' ? r.value : r)) : [result]; console.log("🚀 ~ getGpdpScore ~ resultValues:", resultValues) const groupSize = rightAnswer.length; const groupedResult = []; for (let i = 0; i < resultValues.length; i += groupSize) { groupedResult.push(resultValues.slice(i, i + groupSize)); } console.log("🚀 ~ getGpdpScore ~ groupedResult:", groupedResult) // 배열 비교 함수 function arraysEqual(arr1, arr2) { if (arr1.length !== arr2.length) return false; return arr1.every((value, index) => value === arr2[index]); } // groupedResult 내부 배열에서 rightAnswer와 일치하는 배열이 있는지 확인 const isMatch = groupedResult.some(group => arraysEqual(group, rightAnswer)); if (isMatch) { totalScore += point; scoringResult[key] = point; console.log("🚀 ~ 정답 포함"); } else { scoringResult[key] = 0; console.log("🚀 ~ 오답"); } } } else if (type == "exists") { const result = xpath.select(ele, gpdpXmlDoc); const isMatch = result.some(v => { // 문자열 앞뒤 공백 제거 v.value = typeof v.value === 'string' ? v.value.trim() : v.value rightAnswer = typeof rightAnswer === 'string' ? rightAnswer.trim() : rightAnswer if (v.value === rightAnswer) { totalScore += point; scoringResult[key] = point; console.log(`✅ 정답 일치 > [작성답안]:${v.value} [정답]:${rightAnswer}`); return true; } return false; }); if (!isMatch) { scoringResult[key] = 0; console.log(`❌ 정답 없음 > [정답]:${rightAnswer}`); } } else if (type == "exact") { let result = xpath.select(ele, gpdpXmlDoc); if (result.length == 0) { scoringResult[key] = 0; console.log('ele not found'); continue; } if (result[0].value === rightAnswer) { totalScore += point; scoringResult[key] = point; } else { scoringResult[key] = 0; console.log('ele not matched, ' + result[0].value); } } else if (type === "color") { const items = xpath.select(ele, gpdpXmlDoc); let matched = false; for (const item of items) { const color = parseColorToHex(item.value); // console.log("🚀 ~ getGpdpScore ~ color:", color); // console.log("🚀 ~ getGpdpScore ~ rightColor:", rightAnswer); if (color === rightAnswer) { totalScore += point; scoringResult[key] = point; matched = true; console.log(`✅ 정답 일치 > [작성답안]:${color} [정답]:${rightAnswer}`); break; } } if (!matched) { scoringResult[key] = 0; console.log(`❌ 정답 없음 > [정답]:${rightAnswer}`); } } else if (type == "multi") { try { const result = xpath.select(ele, gpdpXmlDoc); let isSame = true; // console.log(`ele: ${ele}, value: ${value} result: ${result}`); if (result.length == 0) { console.log('result length 0'); scoringResult[key] = 0; continue; } result.forEach((v, i) => { // value[i] 값이 정수형인 경우에는 float로 변환하여 비교 // 정수형 v값을 float 형으로 변환하고 소수점 3자리까지 버림 let temp = v.value; let answer = rightAnswer[i]; if (Number.isFinite(rightAnswer[i]) && !Number.isInteger(rightAnswer[i])) { temp = parseFloat(v.value); answer = parseFloat(rightAnswer[i]); // 소수점 3자리까지 버림 temp = Math.floor(temp * 1000) / 1000; } // answer 문자열 중 : 가 포함되어 있다면 각각 분리하고 그 값의 차이를 구함 if (typeof answer == "string" && answer.indexOf(':') > -1) { const [answerStart, answerEnd] = answer.split(':').map(Number); const [tempStart, tempEnd] = temp.split(':').map(Number); answer = answerEnd - answerStart; temp = tempEnd - tempStart; } console.log(`temp: ${temp} answer: ${answer}`); if (answer !== temp) { console.log(`answer !== temp`); isSame = false; } }); totalScore += isSame ? point : 0; scoringResult[key] = isSame ? point : 0; } catch (e) { console.log('err :', e); scoringResult[key] = 0; } } else if (type == "size") { const items = xpath.select(ele, gpdpXmlDoc); let matched = false; // 각 Item 요소별 x,y 좌표 시작점과 끝점의 거리를 계산해 정답과 비교 for (const item of items) { const x1 = Number(xpath.select1('Item[1]/X/@value', item)?.value); const x2 = Number(xpath.select1('Item[last()]/X/@value', item)?.value); const y1 = Number(xpath.select1('Item[1]/Y/@value', item)?.value); const y2 = Number(xpath.select1('Item[last()]/Y/@value', item)?.value); const width = Math.round(Math.abs(x2 - x1)); const height = Math.round(Math.abs(y2 - y1)); if (width === rightAnswer["width"] && height === rightAnswer["height"]) { totalScore += point; scoringResult[key] = point; matched = true; console.log("✅ 정답 일치:", rightAnswer); break; } } if (!matched) { scoringResult[key] = 0; console.log("❌ 정답 없음:", rightAnswer); } } else if (type == "gradient") { const items = xpath.select(ele, gpdpXmlDoc); const startColorXpath = scoringData[key].startColor; const endColorXpath = scoringData[key].endColor; let matched = false; for (const item of items) { const startColor = parseColorToHex(xpath.select1(startColorXpath, item)?.value); const endColor = parseColorToHex(xpath.select1(endColorXpath, item)?.value); console.log(startColor + ":" + rightAnswer["startColor"], endColor + ":" + rightAnswer["endColor"]); if (startColor === rightAnswer["startColor"] && endColor === rightAnswer["endColor"]) { totalScore += point; scoringResult[key] = point; matched = true; console.log("✅ 정답 일치:", rightAnswer); break; } } if (!matched) { scoringResult[key] = 0; console.log("❌ 정답 없음:", rightAnswer); } } // 그림자 속성이 있는지 여부 파악해서 그림자 속성 별로 점수 1 점씩 부여 else if (type == "shadow") { const result = xpath.select(ele["shadow"], gpdpXmlDoc); let shadowScore = 0; if (result.length == 0) { scoringResult[key] = 0; console.log('shadow not found'); continue; } shadowScore += 1; const width = xpath.select(ele["width"], gpdpXmlDoc); const distance = xpath.select(ele["distance"], gpdpXmlDoc); const blur = xpath.select(ele["blur"], gpdpXmlDoc); const angle = xpath.select(ele["angle"], gpdpXmlDoc); if (width.length !== 0 && width[0].value == rightAnswer["width"]) { shadowScore += 1; console.log('width matched'); } if (distance.length !== 0 && distance[0].value == rightAnswer["distance"]) { shadowScore += 1; console.log('distance matched'); } if (blur.length !== 0 && blur[0].value == rightAnswer["blur"]) { shadowScore += 1; console.log('blur matched'); } if (angle.length !== 0 && angle[0].value == rightAnswer["angle"]) { shadowScore += 1; console.log('angle matched'); } totalScore += shadowScore; scoringResult[key] = shadowScore; } else { const result = xpath.select(ele, gpdpXmlDoc); const result2 = null; let isCheck = false; if (ele === 'none') { scoringResult[key] = "확인필요"; continue; } if (result.length == 0) { isCheck = true; } if (isCheck && ele2) { result2 = xpath.select(ele2, gpdpXmlDoc); if (result2.length == 0) { scoringResult[key] = 0; continue; } result = result2; // console.log(`1st isChecked: ${isCheck}, result: ${result}`) } // value와 result[0].value를 비교하여 같으면 점수 point 부여 // console.log(`${(value === result[0].value)}, ${result.length > 0 && value === result[0].value} `) // console.log(`2nd isChecked: ${isCheck}, result: ${result}`) totalScore += result.length > 0 ? point : 0; scoringResult[key] = result.length > 0 ? point : 0; } } scoringResult['총점'] = totalScore; return scoringResult; }