diff --git a/gpdpScoring.js b/gpdpScoring.js new file mode 100644 index 0000000..2c4bfb4 --- /dev/null +++ b/gpdpScoring.js @@ -0,0 +1,274 @@ +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; +// 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); + + 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; + + // search 값이 undefined 아니면 ele의 {search}부분을 search로 치환 + /** + * JSON파일 곰믹스 5번문항/22번 문항 + * type : "subtitle" 인 항목들 + * GPString태그 VID7속성 찾는 xpath구문 + * CRCUnitArr태그 Name속성 찾는 구문으로 변환 + * > 멀티라인 텍스트 유사도 판별하기 어려움 + */ + 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}`) + 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") { + let result = xpath.select(ele, gpdpXmlDoc); + if (result.length == 0) { + scoringResult[key] = 0; + console.log('ele not found'); + continue; + } + const hexColor = parseColorToHex(result[0].value); + + if (hexColor === rightAnswer) { + totalScore += point; + scoringResult[key] = point; + console.log('color matched, ' + hexColor); + } else { + scoringResult[key] = 0; + console.log('color not matched, ' + hexColor); + } + } + 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") { + let posX = scoringData[key].posX; + let posY = scoringData[key].posY; + + let answerWidth = rightAnswer["width"]; + let answerHeight = rightAnswer["height"]; + + let width = xpath.select(posX, gpdpXmlDoc); + let height = xpath.select(posY, gpdpXmlDoc); + width = Math.round(width); + height = Math.round(height); + + console.log(`width:${answerWidth},${width}, height: ${answerHeight},${height}`); + if (answerWidth === width && answerHeight === height) { + totalScore += point; + scoringResult[key] = point; + console.log("same size"); + } + else { + scoringResult[key] = 0; + console.log("different size"); + } + } + else if (type == "gradient") { + let startColor = scoringData[key].startColor; + let endColor = scoringData[key].endColor; + + let answerStartColor = rightAnswer["startColor"]; + let answerEndColor = rightAnswer["endColor"]; + + let start = xpath.select(startColor, gpdpXmlDoc); + let end = xpath.select(endColor, gpdpXmlDoc); + + // console.log(start[0].value, end[0].value); + if (start.length == 0 || end.length == 0) { + console.log("gradient color not found"); + scoringResult[key] = 0; + continue; + } + + const startHexColor = parseColorToHex(start[0].value); + const endHexColor = parseColorToHex(end[0].value); + console.log(startHexColor + ":" + answerStartColor, endHexColor + ":" + answerEndColor); + + if (startHexColor === answerStartColor && endHexColor === answerEndColor) { + totalScore += point; + scoringResult[key] = point; + console.log("same color"); + } + else { + scoringResult[key] = 0; + console.log("different color"); + } + } + // 그림자 속성이 있는지 여부 파악해서 그림자 속성 별로 점수 1 점씩 부여 + else if(type == "shadow"){ + + let result = xpath.select(ele["shadow"], gpdpXmlDoc); + let shadowScore = 0; + if (result.length == 0) { + scoringResult[key] = 0; + console.log('shadow not found'); + continue; + } + + shadowScore += 1; + let width = xpath.select(ele["width"], gpdpXmlDoc); + let distance = xpath.select(ele["distance"], gpdpXmlDoc); + let blur = xpath.select(ele["blur"], gpdpXmlDoc); + let 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('distence 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 { + let result = xpath.select(ele, gpdpXmlDoc); + let 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; + } + \ No newline at end of file diff --git a/psdExport_2.js b/psdExport_2.js index 0ca472c..200afa5 100644 --- a/psdExport_2.js +++ b/psdExport_2.js @@ -7,14 +7,16 @@ const xpath = require('xpath'); const { DOMParser } = require('xmldom'); const findSimilarString = require('./findSimilarString'); +const getGpdpScore = require('./gpdpScoring.js'); const getToday = require('./getToday.js'); const todayDate = getToday(); // -------------------------------------------------------- // const scoringJson = require('./DIC_2502A.json'); // const scoringJson = require('./DIC_2502B.json'); -const scoringJson = require('./DIC_2502C.json'); +// const scoringJson = require('./DIC_2502C.json'); // const scoringJson = require('./DIC_2502D.json'); +const scoringJson = require('./samples/DIC(gompic).json'); // TEST // const scoringJson = require('./DIC_2502A_TEST.json'); @@ -26,11 +28,11 @@ const scoringJson = require('./DIC_2502C.json'); // const answerFilesDir = './output/B/DIC'; // const answerFilesDir = './output/C/DIC'; // const answerFilesDir = './output/D/DIC'; - +const answerFilesDir = './samples/'; // TEST // const answerFilesDir = './output/A/TEST'; // const answerFilesDir = './output/B/TEST'; -const answerFilesDir = './output/C/TEST'; +// const answerFilesDir = './output/C/TEST'; // const answerFilesDir = './output/D/TEST'; // -------------------------------------------------------- @@ -42,10 +44,12 @@ const answerFilesDir = './output/C/TEST'; // TEST // const outputExcelFile = './'+todayDate+'_DIC_2502A_TEST.xlsx'; // const outputExcelFile = './'+todayDate+'_DIC_2502B_TEST.xlsx'; -const outputExcelFile = './' + todayDate + '_DIC_2502C_TEST.xlsx'; +// const outputExcelFile = './' + todayDate + '_DIC_2502C_TEST.xlsx'; // const outputExcelFile = './'+todayDate+'_DIC_2502D_TEST.xlsx'; +const outputExcelFile = './'+todayDate+'_gompic.xlsx'; // -------------------------------------------------------- + // 답안 폴더 내부에 디렉토리가 아닌 일반 파일이 있을 경우 디렉토리만 필터링 해서 불러옴 const studentDirs = fs.readdirSync(answerFilesDir).filter(file => { const filePath = path.join(answerFilesDir, file); @@ -68,6 +72,11 @@ studentDirs.forEach(student => { file => file.endsWith('.gmep') || file.endsWith('.gmdp') ); + // 곰픽 파일 gpdp 파일 이거나 xml 파일 + const gpdpFile = fs.readdirSync(studentDir).filter( + file => file.endsWith('.xml') + ); + // 학생 이름을 key로 하는 객체 생성 // 채점결과 const scoringResult = { @@ -86,6 +95,17 @@ studentDirs.forEach(student => { console.error(`Error reading PSD file: ${psdPath}`, error); } }); + gpdpFile.forEach((gpdp, index) => { + const gpdpPath = path.join('./', studentDir, gpdp); + console.log(`Reading ${gpdpPath}...`); + + const xmlString = fs.readFileSync(gpdpPath, 'utf8'); + // XML 문자열을 파싱하여 XML 문서 객체로 변환 + const xmlDocument = new DOMParser().parseFromString(xmlString, 'application/xml'); + // console.log('xmlDocument:', xmlDocument); + + scoringResult[index + 1] = getGpdpScore(xmlDocument, scoringJson, index + 3); + }); gmepFile.forEach((gmep, index) => { const gmepPath = path.join('./', studentDir, gmep); console.log(`Reading ${gmepPath}...`); diff --git a/samples/DIC(gompic).json b/samples/DIC(gompic).json new file mode 100644 index 0000000..0524501 --- /dev/null +++ b/samples/DIC(gompic).json @@ -0,0 +1,149 @@ +{ + "4": { + "1": { + "type": "multi", + "ele": "//Document/Width/@value | //Document/Height/@value", + "value": [ + "650", + "450" + ], + "point": 5, + "desc": "캔버스 사이즈 650*450" + }, + "2": { + "ele": "none", + "point": 5, + "desc": "배경색 문항은 채점 불가" + }, + "3": { + "ele": "//Layer[contains(Name/@value, 'Layer 2')][MaskOpType/@value='Layering']", + "point": 6 + }, + "4": { + "ele": "none", + "point": 5, + "desc": "가로방향 흐릿하게 문항은 채점 불가" + }, + "5": { + "ele": "//Layer[contains(Name/@value, 'Layer 3')]//shape_type/@value", + "answer": "ROUNDED_RECTANGLE", + "point": 6 + }, + "6": { + "type": "size", + "posX": "//Layer[contains(Name/@value, 'Layer 3')]//op_points[Item]/Item[last()]/X/@value - //Layer[contains(Name/@value, 'Layer 3')]//op_points[Item]/Item[1]/X/@value", + "posY": "//Layer[contains(Name/@value, 'Layer 3')]//op_points[Item]/Item[last()]/Y/@value - //Layer[contains(Name/@value, 'Layer 3')]//op_points[Item]/Item[1]/Y/@value", + "value": { + "width": 370, + "height": 60 + }, + "point": 3, + "desc": "레이어 쉐이프 X, Y 좌표를 가지고 너비, 높이 계산하여 정답 채점" + }, + "7": { + "type": "gradient", + "startColor": "//Layer[contains(Name/@value, 'Layer 3')]//gradient_start_color/@value", + "endColor": "//Layer[contains(Name/@value, 'Layer 3')]//gradient_end_color/@value", + "value": { + "startColor": "347813", + "endColor": "041177" + }, + "point": 6 + }, + "8": { + "type": "exact", + "ele": "//Layer//Shape[shape_type/@value='TEXT']/lines/Item/@value", + "value": "어린이 과학관", + "point": 5 + }, + "9": { + "type": "exact", + "ele": "//Layer//Shape[shape_type/@value='TEXT']/font/Name/@value", + "value": "돋움", + "point": 3 + }, + "10": { + "type": "exact", + "ele": "//Layer//Shape[shape_type/@value='TEXT']/font/Italic/@value", + "value": "True", + "point": 3 + }, + "11": { + "type": "exact", + "ele": "//Layer//Shape[shape_type/@value='TEXT']/font/Size/@value", + "value": "32", + "point": 3 + }, + "12": { + "type": "color", + "ele": "//Layer//Shape[shape_type/@value='TEXT'][contains(draw_type/@value, 'Interior')]/secondary_color/@value", + "value": "f04da5", + "point": 3, + "desc": "색상 코드 비교 시 소문자로 입력할 것" + }, + "13": { + "type": "exact", + "ele": "//Layer//Shape[shape_type/@value='TEXT']/outline_peninfo/Width/@value", + "value": "3", + "point": 3 + }, + "14": { + "type": "color", + "ele": "//Layer//Shape[shape_type/@value='TEXT'][contains(draw_type/@value, 'Outline')]/primary_color/@value", + "value": "ffe000", + "point": 3, + "desc": "색상 코드 비교 시 소문자로 입력할 것" + }, + "15": { + "ele": "//Layer[MaskOpType/@value='Clipping'][last()]", + "point": 6, + "desc": "클리핑 마스크 항목은 별도 레이어로 추가되고 해당 속성을 추가해놓은 레이어가 있는지 여부 체크 함" + }, + "16": { + "ele": "//Layer[contains(Name/@value, 'Layer 3')]//shape_type/@value", + "value": "RECTANGLE", + "point": 3 + }, + "17": { + "type": "size", + "posX": "//Layer[contains(Name/@value, 'Layer 5')]//op_points[Item]/Item[last()]/X/@value - //Layer[contains(Name/@value, 'Layer 5')]//op_points[Item]/Item[1]/X/@value", + "posY": "//Layer[contains(Name/@value, 'Layer 5')]//op_points[Item]/Item[last()]/Y/@value - //Layer[contains(Name/@value, 'Layer 5')]//op_points[Item]/Item[1]/Y/@value", + "value": { + "width": 150, + "height": 150 + }, + "point": 3, + "desc": "레이어 쉐이프 X, Y 좌표를 가지고 너비, 높이 계산하여 정답 채점" + }, + "18": { + "ele": "//Layer[contains(Name/@value, 'Layer 5')]//outline_peninfo/Width/@value", + "value": "7", + "point": 3 + }, + "19": { + "type": "color", + "ele": "//Layer//Shape[shape_type/@value='RECTANGLE'][contains(draw_type/@value, 'Outline')]/primary_color/@value", + "value": "008878", + "point": 3, + "desc": "색상 코드 비교 시 소문자로 입력할 것(채우기:secondary_color, 외곽선:primary_color)" + }, + "20": { + "type": "shadow", + "ele": { + "shadow": "//Layer//Shape[shape_type/@value='RECTANGLE'][contains(draw_type/@value, 'Shadow')]", + "width": "//Layer//Shape[shape_type/@value='RECTANGLE'][contains(draw_type/@value, 'Shadow')]/shadow_width/@value", + "distance": "//Layer//Shape[shape_type/@value='RECTANGLE'][contains(draw_type/@value, 'Shadow')]/shadow_distance/@value", + "blur": "//Layer//Shape[shape_type/@value='RECTANGLE'][contains(draw_type/@value, 'Shadow')]/shadow_blur/@value", + "angle": "//Layer//Shape[shape_type/@value='RECTANGLE'][contains(draw_type/@value, 'Shadow')]/shadow_angle/@value" + }, + "value": { + "width": "5", + "distance": "3", + "blur": "1", + "angle": "320" + }, + "point": 5, + "desc": "그림자 속성이 있는 경우 그림자 속성의 너비, 거리, 흐림 정도, 각도를 비교하여 정답 채점" + } + } +} \ No newline at end of file