diff --git a/DIC_2504B.json b/DIC_2504B.json index 32b2c1e..fa2cdb4 100644 --- a/DIC_2504B.json +++ b/DIC_2504B.json @@ -498,8 +498,8 @@ }, "4": { "type": "layer.Effects", - "ele": "//Layer[Name[@value='{layer}']]/Effects/Item", - "layer": "Flower", + "ele": "//Layer[Name[@value='{search}']]/Effects/Item", + "search": "Flower", "value": { "name": "세피아", "option": { @@ -557,19 +557,21 @@ "point": 6 }, "11": { - "ele": "none", + "type": "none", + "ele": "", "point": 0, "desc": "기본설정" }, "12": { - "ele": "none", + "type": "none", + "ele": "", "point": 0, "desc": "파일명 확인" } }, "5": { "1": { - "type": "multi", + "type": "canvas.Size", "ele": "//Document/Width/@value | //Document/Height/@value", "value": [ "650", @@ -579,7 +581,8 @@ "desc": "캔버스 사이즈 650*450" }, "2": { - "ele": "none", + "type": "none", + "ele": "", "point": 5, "desc": "배경색 문항은 채점 불가" }, @@ -587,10 +590,12 @@ "type": "exists", "ele": "//Layer/MaskOpType/@value", "value": "Layering", - "point": 6 + "point": 6, + "desc": "레이어 마스크 설정 확인" }, "4": { - "ele": "none", + "type": "none", + "ele": "", "point": 6, "desc": "가로방향 흐릿하게 문항은 채점 불가" }, @@ -601,7 +606,7 @@ "point": 3 }, "6": { - "type": "size", + "type": "shape.size", "ele": "//Layer//op_points", "value": { "width": 400, @@ -611,13 +616,13 @@ "desc": "레이어 쉐이프 X, Y 좌표를 가지고 너비, 높이 계산하여 정답 채점" }, "7": { - "type": "gradient", + "type": "gradient.color", "ele": "//Layer/Shapes/Shape", "startColor": "gradient_start_color/@value", "endColor": "gradient_end_color/@value", "value": { "startColor": "ffe000", - "endColor": "34a159" + "endColor": "34A159" }, "point": 6 }, @@ -647,11 +652,10 @@ "point": 3 }, "12": { - "type": "color", + "type": "text.color", "ele": "//Layer//Shape[shape_type/@value='TEXT'][contains(draw_type/@value, 'Interior')]/secondary_color/@value", - "value": "b46ef8", - "point": 3, - "desc": "색상 코드 비교 시 소문자로 입력할 것" + "value": "b46Ef8", + "point": 3 }, "13": { "type": "exists", @@ -660,14 +664,15 @@ "point": 3 }, "14": { - "type": "color", + "type": "text.color", "ele": "//Layer//Shape[shape_type/@value='TEXT'][contains(draw_type/@value, 'Outline')]/primary_color/@value", "value": "ffffff", - "point": 3, - "desc": "색상 코드 비교 시 소문자로 입력할 것" + "point": 3 }, "15": { - "ele": "//Layer[MaskOpType/@value='Clipping'][last()]", + "type": "exists", + "ele": "//Layer/MaskOpType/@value", + "value": "Clipping", "point": 6, "desc": "클리핑 마스크 항목은 별도 레이어로 추가되고 해당 속성을 추가해놓은 레이어가 있는지 여부 체크 함" }, @@ -675,11 +680,15 @@ "type": "exists", "ele": "//Layer/Shapes/Shape/shape_type/@value", "value": "RECTANGLE", - "point": 3 + "point": 3, + "desc": { + "사각형": "RECTANGLE" + } }, "17": { - "type": "size", - "ele": "//Layer//op_points", + "type": "clipping.size", + "ele": "//Layer//Shape[shape_type/@value='{option}']//op_points", + "option": "RECTANGLE", "value": { "width": 150, "height": 150 @@ -689,27 +698,32 @@ }, "18": { "type": "exists", - "ele": "//Layer//outline_peninfo/Width/@value", + "ele": "//Layer//Shape[shape_type/@value='{option}']/outline_peninfo/Width/@value", + "option": "RECTANGLE", "value": "7", "point": 3 }, "19": { - "type": "color", - "ele": "//Layer//Shape[contains(draw_type/@value, 'Outline')]/primary_color/@value", + "type": "clipping.color", + "ele": "//Layer//Shape[shape_type/@value='{option}' and contains(draw_type/@value, 'Outline')]/primary_color/@value", + "option": "RECTANGLE", "value": "e8e88e", "point": 3, - "desc": "색상 코드 비교 시 소문자로 입력할 것(채우기:secondary_color, 외곽선:primary_color)" + "desc": "채우기:secondary_color, 외곽선:primary_color" }, "20": { "type": "shadow", - "ele": { + "ele": "//Layer//Shape[shape_type/@value='{option}']", + "ele2": { "shadow": "//Layer//Shape[contains(draw_type/@value, 'Shadow')]", "width": "//Layer//Shape[contains(draw_type/@value, 'Shadow')]/shadow_width/@value", "distance": "//Layer//Shape[contains(draw_type/@value, 'Shadow')]/shadow_distance/@value", "blur": "//Layer//Shape[contains(draw_type/@value, 'Shadow')]/shadow_blur/@value", "angle": "//Layer//Shape[contains(draw_type/@value, 'Shadow')]/shadow_angle/@value" }, + "option": "RECTANGLE", "value": { + "shadow": true, "width": "3", "distance": "5", "blur": "1", @@ -719,12 +733,14 @@ "desc": "그림자 속성이 있는 경우 그림자 속성의 너비, 거리, 흐림 정도, 각도를 비교하여 정답 채점" }, "21": { - "ele": "none", + "type": "none", + "ele": "", "point": 0, "desc": "기본설정" }, "22": { - "ele": "none", + "type": "none", + "ele": "", "point": 0, "desc": "파일명 확인" } diff --git a/findSimilarString.js b/findSimilarString.js index f94f966..ddbe3b9 100644 --- a/findSimilarString.js +++ b/findSimilarString.js @@ -11,16 +11,24 @@ const stringSimilarity = require("string-similarity"); */ function findSimilarString(xmlDoc, targetString, threshold = 0.8) { // XML 내부의 비교 대상 텍스트 리스트 찾기 - function getTextNodes(xmlDoc, stringList = []) { - const stringNodes = xpath.select("//CRCUnitArr/@Name", xmlDoc); - stringNodes.forEach(stringNode => { - stringList.push(stringNode.value); + function getTextNodes(xmlDoc, paths = ["//CRCUnitArr/@Name"]) { + const stringList = []; + + paths.forEach(path => { + const nodes = xpath.select(path, xmlDoc); + nodes.forEach(node => { + stringList.push(node.value); + }); }); + return stringList; } // XML에서 모든 텍스트 추출 - const stringList = getTextNodes(xmlDoc); + const stringList = getTextNodes(xmlDoc, [ + "//CRCUnitArr/@Name", + "//Layer/Name/@value", + ]); // 유사도 비교하여 가장 유사한 문자열 찾기 let bestMatch = null; diff --git a/gpdpScoring.js b/gpdpScoring.js index 8fb3345..eefc831 100644 --- a/gpdpScoring.js +++ b/gpdpScoring.js @@ -1,5 +1,8 @@ const xpath = require('xpath'); const { DOMParser } = require('xmldom'); +const _ = require('lodash'); + +const findSimilarString = require('./findSimilarString'); function parseColorToHex(colorString) { // 정규식을 사용하여 B, G, R, A 값 추출 @@ -49,47 +52,69 @@ function getGpdpScore(gpdpData, scoringJson, index) { const { tolerance = 0, // 숫자 비교 시 허용 오차 type = 'auto', // 'auto' | 'number[]' | 'string[]' | 'object' | 'primitive' + partial = false, } = options; if (type === 'force-correct') { - isEqual = true; + score = point; } - else if (Array.isArray(right) && Array.isArray(user)) { - if (right.length === user.length) { - if - (type === 'number[]' || - (type === 'auto' && typeof right[0] === 'number')) { - isEqual = right.every((val, idx) => Math.abs(val - user[idx]) <= tolerance); + // 객체 비교 + else if ( + (type === 'object' || (type === 'auto' && typeof right === 'object' && typeof user === 'object')) + && !Array.isArray(right) + ) { + if (partial) { + const keys = Object.keys(right); + const partialPoint = point / keys.length; + for (const k of keys) { + if (_.isEqual(user[k], right[k])) { + score += partialPoint; + } } - else if - (type === 'string[]' || - (type === 'auto' && typeof right[0] === 'string')) { - isEqual = right.every((val, idx) => val === user[idx]); + } else { + if (_.isEqual(user, right)) { + score = point; } } } - else if - (type === 'object' || - (type === 'auto' && typeof right === 'object' && typeof user === 'object')) { - isEqual = JSON.stringify(user) === JSON.stringify(right); - } - else { - isEqual = user == right; // primitive 비교 + + // 배열 비교 + else if (Array.isArray(right) && Array.isArray(user)) { + if (right.length === user.length) { + if (type === 'number[]' || (type === 'auto' && typeof right[0] === 'number')) { + if (right.every((val, idx) => val === user[idx])) { + score = point; + } + } else if (type === 'string[]' || (type === 'auto' && typeof right[0] === 'string')) { + if (right.every((val, idx) => val === user[idx])) { + score = point; + } + } + } } - if (isEqual) { - score = point; - console.log('작성답안: ', user); - console.log('>⭕ 정답: ', right); - } else { - console.log('작성답안: ', user); - console.log('>❌ 오답: ', right); + // 원시값 비교 + else { + if (user == right) { + score = point; + } + } + + console.log(`▶ 작성답안:`, user); + if (score > 0) { + if (partial) + console.log('⭕부분정답:', right, `(${score}/${point})`); + else + console.log('⭕ 정답 : ', right, `(${score}/${point})`); + } + else { + console.log(`❌ 오답:`, right); } scoringResult[key] = score; - // totalScore += score; return score; } + const gpdpXmlDoc = gpdpData; const scoringResult = {}; @@ -109,9 +134,10 @@ function getGpdpScore(gpdpData, scoringJson, index) { // 채점기준표 문항별 분류 for (const key in scoringData) { + console.log(`[❔]문제번호 : [${index}-${key}]`) + let ele = scoringData[key].ele; let ele2 = scoringData[key].ele2; - let existEle = scoringData[key].existEle; const rightAnswer = scoringData[key].value; const point = scoringData[key].point; const type = scoringData[key].type; @@ -127,19 +153,26 @@ function getGpdpScore(gpdpData, scoringJson, index) { ele = typeof ele === 'string' ? ele.replace(/{option}/g, option) : ele; ele = typeof ele === 'string' ? ele.replace(/{style}/g, style) : ele; + // search 값이 undefined 아니면 ele의 {search}부분을 search로 치환 + /** + * JSON파일 곰믹스 5번문항/22번 문항 + * type : "video" 인 항목들 + * GPString태그 VID7속성 찾는 xpath구문 + * CRCUnitArr태그 Name속성 찾는 구문으로 변환 + * > 멀티라인 텍스트 유사도 판별하기 어려움 + */ if (search !== undefined) { - let result = findSimilarString(gpdpXmlDoc, search, 0.8) - // xpath 내부 "(큰따옴표) 필터링 + let result = findSimilarString(gpdpXmlDoc, search, 0.8); if (result !== null) { result = result.replace(/"/g, "'"); + search = result; + ele = ele?.replace(/{search}/g, search); } - ele = ele.replace(/{search}/g, result); - if (existEle !== undefined) { - existEle = existEle.replace(/{search}/g, result); + else { + ele = ele?.includes('{search}') ? null : ele; } } - console.log(`❔문제번호 : [4-${key}]`) if (type === "none") { console.log("❌ 채점하지 않음"); @@ -162,407 +195,534 @@ function getGpdpScore(gpdpData, scoringJson, index) { else if (type === "layer.Exists") { const layerNameList = xpath.select(ele, gpdpXmlDoc); const layerNames = layerNameList.map(layer => layer.value); - console.log("🚀 ~ getGpdpScore ~ layerNames:", layerNames); + let isMatched = false - // userAnswer = layerNames.find(name => name === rightAnswer); - for (const layerName of layerNames) { - if (layerName.trim().toLowerCase() === rightAnswer.trim().toLowerCase()) { - userAnswer = layerName; - break; - } + // for (const layerName of layerNames) { + // if (layerName.trim().toLowerCase() === rightAnswer.trim().toLowerCase()) { + // userAnswer = layerName; + // isMatched = true; + // break; + // } + // } + + // if (isMatched) { + // totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + // } + + let result = findSimilarString(gpdpXmlDoc, rightAnswer, 0.8); + if (result !== null) { + userAnswer = result; + isMatched = true; + } + + if (isMatched) { + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, { + type: 'force-correct' + }); + } + else { + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); } - totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); - continue; } // [1-4] 사진1 > 조정 else if (type === "layer.Effects") { - const effects = xpath.select(ele, gpdpXmlDoc); + const effects = xpath.select(ele, gpdpXmlDoc); - let isMatched = false; - for (const item of effects) { - const name = xpath.select1('Name/@value', item)?.value; - const effectData = xpath.select1(`EffectData`, item); + let isMatched = false; + for (const item of effects) { + const name = xpath.select1('Name/@value', item)?.value; + const effectData = xpath.select1(`EffectData`, item); - // 동일한 이펙트 요소만 검사 - if (rightAnswer['name'] !== name) { - continue; + // 동일한 이펙트 요소만 검사 + if (rightAnswer['name'] !== name) { + continue; + } + + userAnswer = { + name: name, + option: {}, + } + if (name === '흑백') { + const Intensity = xpath.select1('Intensity/@value', effectData)?.value; + + const optionKeys = Object.keys(rightAnswer['option']); + if (optionKeys.includes('강도')) userAnswer['option']['강도'] = Intensity; + } + else if (name === '밝기/대비') { + const brightness = xpath.select1('brightness/@value', effectData)?.value; + const contrast = xpath.select1('contrast/@value', effectData)?.value; + + const optionKeys = Object.keys(rightAnswer['option']); + if (optionKeys.includes('밝기')) userAnswer['option']['밝기'] = brightness; + if (optionKeys.includes('대비')) userAnswer['option']['대비'] = contrast; + } + else if (name === '노출') { + const ExposureValue = xpath.select1('ExposureValue/@value', effectData)?.value; + + const optionKeys = Object.keys(rightAnswer['option']); + if (optionKeys.includes('노출')) userAnswer['option']['노출'] = ExposureValue; + } + else if (name === '색조/채도') { + const hue = xpath.select1('hue/@value', effectData)?.value; + const saturation = xpath.select1('saturation/@value', effectData)?.value; + const lightness = xpath.select1('lightness/@value', effectData)?.value; + + const optionKeys = Object.keys(rightAnswer['option']); + if (optionKeys.includes('색조')) userAnswer['option']['색조'] = hue; + if (optionKeys.includes('채도')) userAnswer['option']['채도'] = saturation; + if (optionKeys.includes('명도')) userAnswer['option']['명도'] = lightness; + } + else if (name === '감마') { + const lift = xpath.select1('Lift/@value', effectData)?.value; + const gamma = xpath.select1('Gamma/@value', effectData)?.value; + const gain = xpath.select1('Gain/@value', effectData)?.value; + + const optionKeys = Object.keys(rightAnswer['option']); + if (optionKeys.includes('리프트')) userAnswer['option']['리프트'] = lift; + if (optionKeys.includes('감마')) userAnswer['option']['감마'] = gamma; + if (optionKeys.includes('게인')) userAnswer['option']['게인'] = gain; + } + else if (name === '세피아') { + const u = xpath.select1('U/@value', effectData)?.value; + const v = xpath.select1('V/@value', effectData)?.value; + + const optionKeys = Object.keys(rightAnswer['option']).map(key => key.toUpperCase()); + if (optionKeys.includes('U')) userAnswer['option']['U'] = u; + if (optionKeys.includes('V')) userAnswer['option']['V'] = v; + } + else if (name === '생동감') { + const vibranceValue = xpath.select1('VibranceValue/@value', effectData)?.value; + // 생동감 옵션값이 프로그램에서 적용한 값에 오차가 발생하는 경우가 있음 + // 곰픽>XML / 30>29 / 40>39 + // 설정한 값 그대로 적용되는 경우도 있어서 오차범위 2로 설정 + const userValue = parseInt(vibranceValue, 10); + const rightValue = parseInt(rightAnswer.option['생동감'], 10); + + if (Math.abs(rightValue - userValue) <= 2) { + const optionKeys = Object.keys(rightAnswer['option']); + if (optionKeys.includes('생동감')) { + userAnswer['option']['생동감'] = rightValue.toString(); + } } + } + + for (const key in rightAnswer.option) { + // 속성값이 정답과 다른 경우가 있으면 오답처리 + if (rightAnswer.option[key] !== userAnswer.option[key]) { + isMatched = false; + break; + } + else { + isMatched = true; + } + } + + // 속성값이 하나라도 일치하지 않으면 오답 + if (isMatched === false) { + break; + } + } + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + continue; + } + + // + else if (type === "exists") { + const existsValues = xpath.select(ele, gpdpXmlDoc); + let isMatched = false; + + for (const v of existsValues) { + // 하나라도 일치하면 정답 + if (v.value === rightAnswer) { + userAnswer = v.value; + isMatched = true; + break; + } + } + // if (isMatched) { + + // } + // else { + + // } + totalScore = compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + } + + + // else if (type === "shape.size") { + else if (type.includes("size")) { + const items = xpath.select(ele, gpdpXmlDoc); + let isMatched = false; + + // 각 Item 요소별 x,y 좌표 시작점과 끝점의 거리를 계산해 정답과 비교 + for (const item of items) { + const x1 = Number(xpath.select1('Item[1]/X/@value', item)?.value); + const y1 = Number(xpath.select1('Item[1]/Y/@value', item)?.value); + + const x2 = Number(xpath.select1('Item[last()]/X/@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)); + + userAnswer = { + width: width, + height: height, + }; + + // 하나라도 일치하면 정답 + if (JSON.stringify(userAnswer) == JSON.stringify(rightAnswer)) { + isMatched = true; + break; + } + } + + totalScore = compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + continue; + } + + // [1-8] + else if (type.includes("color")) { + const items = xpath.select(ele, gpdpXmlDoc); + let normalizedAnswer = null; + let isMatched = false; + + for (const item of items) { + if (type.includes('gradient')) { + const startColorXpath = scoringData[key].startColor; + const endColorXpath = scoringData[key].endColor; + + const startColorRGB = xpath.select1(startColorXpath, item).value; + const endColorRGB = xpath.select1(endColorXpath, item).value; + + const startColor = parseColorToHex(startColorRGB); + const endColor = parseColorToHex(endColorRGB); userAnswer = { - name: name, - option: {}, - } - if (name === '흑백') { - const Intensity = xpath.select1('Intensity/@value', effectData)?.value; - - const optionKeys = Object.keys(rightAnswer['option']); - if (optionKeys.includes('강도')) userAnswer['option']['강도'] = Intensity; - } - else if (name === '밝기/대비') { - const brightness = xpath.select1('brightness/@value', effectData)?.value; - const contrast = xpath.select1('contrast/@value', effectData)?.value; - - const optionKeys = Object.keys(rightAnswer['option']); - if (optionKeys.includes('밝기')) userAnswer['option']['밝기'] = brightness; - if (optionKeys.includes('대비')) userAnswer['option']['대비'] = contrast; - } - else if (name === '노출') { - const ExposureValue = xpath.select1('ExposureValue/@value', effectData)?.value; - - const optionKeys = Object.keys(rightAnswer['option']); - if (optionKeys.includes('노출')) userAnswer['option']['노출'] = ExposureValue; - } - else if (name === '색조/채도') { - const hue = xpath.select1('hue/@value', effectData)?.value; - const saturation = xpath.select1('saturation/@value', effectData)?.value; - const lightness = xpath.select1('lightness/@value', effectData)?.value; - - const optionKeys = Object.keys(rightAnswer['option']); - if (optionKeys.includes('색조')) userAnswer['option']['색조'] = hue; - if (optionKeys.includes('채도')) userAnswer['option']['채도'] = saturation; - if (optionKeys.includes('명도')) userAnswer['option']['명도'] = lightness; - } - else if (name === '감마') { - const lift = xpath.select1('Lift/@value', effectData)?.value; - const gamma = xpath.select1('Gamma/@value', effectData)?.value; - const gain = xpath.select1('Gain/@value', effectData)?.value; - - const optionKeys = Object.keys(rightAnswer['option']); - if (optionKeys.includes('리프트')) userAnswer['option']['리프트'] = lift; - if (optionKeys.includes('감마')) userAnswer['option']['감마'] = gamma; - if (optionKeys.includes('게인')) userAnswer['option']['게인'] = gain; - } - else if (name === '세피아') { - const u = xpath.select1('U/@value', effectData)?.value; - const v = xpath.select1('V/@value', effectData)?.value; - - const optionKeys = Object.keys(rightAnswer['option']).map(key => key.toUpperCase()); - if (optionKeys.includes('U')) userAnswer['option']['U'] = u; - if (optionKeys.includes('V')) userAnswer['option']['V'] = v; - } - else if (name === '생동감') { - const vibranceValue = xpath.select1('VibranceValue/@value', effectData)?.value; - // 생동감 옵션값이 프로그램에서 적용한 값에 오차가 발생하는 경우가 있음 - // 곰픽>XML / 30>29 / 40>39 - // 설정한 값 그대로 적용되는 경우도 있어서 오차범위를 설정 - const userValue = parseInt(vibranceValue, 10); - const rightValue = parseInt(rightAnswer.option['생동감'], 10); - - if (Math.abs(rightValue - userValue) <= 2) { - const optionKeys = Object.keys(rightAnswer['option']); - if (optionKeys.includes('생동감')) { - userAnswer['option']['생동감'] = rightValue.toString(); - } - } + startColor: startColor, + endColor: endColor, } - for (const key in rightAnswer.option) { - // 속성값이 정답과 다른 경우가 있으면 오답처리 - if (rightAnswer.option[key] !== userAnswer.option[key]) { - isMatched = false; - break; - } - else { - isMatched = true; - } + // JSON파일에서 대문자로 입력된 경우 소문자로 변환 + normalizedAnswer = { + startColor: rightAnswer.startColor.toLowerCase(), + endColor: rightAnswer.endColor.toLowerCase(), } - // 속성값이 하나라도 일치하지 않으면 오답 - if (isMatched === false) { + // 하나라도 일치하면 정답 + if (JSON.stringify(userAnswer) == JSON.stringify(normalizedAnswer)) { + isMatched = true; break; } } - totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + + // else if (type.includes('shape') || type.includes('text') || type.includes('clipping')) { + else { + const color = parseColorToHex(item.value); + userAnswer = color; + normalizedAnswer = rightAnswer.toLowerCase?.(); + + // 하나라도 일치하면 정답 + if (userAnswer === normalizedAnswer) { + isMatched = true; + break; + } + } + } + totalScore = compareAndScore(userAnswer, normalizedAnswer, point, key, scoringResult); + } + + else if (type === 'layer.blend.opacity') { + const layers = xpath.select(ele, gpdpXmlDoc); + let isMatched = false; + + for (const layer of layers) { + const blendop = xpath.select1('BlendOp/@value', layer).value; + const opacity = xpath.select1('Opacity/@value', layer).value; + + userAnswer = { + BlendOp: blendop, + Opacity: opacity, + } + + // 하나라도 일치하면 정답 + if (JSON.stringify(userAnswer) == JSON.stringify(rightAnswer)) { + isMatched = true; + break; + } + } + + totalScore = compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); + continue; + } + + // [5-20] + else if (type === 'shadow') { + const shapes = xpath.select(ele, gpdpXmlDoc); + + for (const shape of shapes) { + // 그림자 설정 여부 + const shadowExists = xpath.select1('contains(draw_type/@value, "Shadow")', shape); + // Shadow 옵션이 있다면 + if (shadowExists) { + // 두께 + const width = xpath.select1('shadow_width/@value', shape).value; + // 거리 + const distance = xpath.select1('shadow_distance/@value', shape).value; + // 분산도 + const blur = xpath.select1('shadow_blur/@value', shape).value; + // 각도 + const angle = xpath.select1('shadow_angle/@value', shape).value; + + userAnswer = { + shadow: shadowExists, + width: width, + distance: distance, + blur: blur, + angle: angle, + } + } + else { + userAnswer = { + shadow: shadowExists, + width: null, + distance: null, + blur: null, + angle: null, + } + + } + } + console.log("🚀 ~ userAnswer:", userAnswer); + console.log("🚀 ~ rightAnswer : ", rightAnswer) + totalScore += compareAndScore(userAnswer, rightAnswer, point, key, scoringResult, { + partial: true + }) + continue; + } + + else 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 == "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 == "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 == "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); - // [1-6][1-7] - else if (type === "exists") { - const existsValues = xpath.select(ele, gpdpXmlDoc); - - for (const v of existsValues) { - if (v.value === rightAnswer) { - userAnswer = v.value; - break; - } - } - totalScore = compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); - } - - - else if (type === "shape.size") { - const items = xpath.select(ele, gpdpXmlDoc); - - // 각 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)); - - userAnswer = { - width: width, - height: height, - }; - } - - totalScore = compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); - } - - // [1-8] - else if (type === "shape.color") { - const items = xpath.select(ele, gpdpXmlDoc); - - for (const item of items) { - const color = parseColorToHex(item.value); - userAnswer = color; - } - - const normalizedRA = rightAnswer.toLowerCase?.(); - totalScore = compareAndScore(userAnswer, normalizedRA, point, key, scoringResult); - } - - else if (type === 'layer.blend.opacity') { - const layers = xpath.select(ele, gpdpXmlDoc); - - for (const layer of layers) { - const blendop = xpath.select1('BlendOp/@value', layer).value; - const opacity = xpath.select1('Opacity/@value', layer).value; - - userAnswer = { - BlendOp: blendop, - Opacity: opacity, - } - } - - totalScore = compareAndScore(userAnswer, rightAnswer, point, key, scoringResult); - } - else 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 == "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 == "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 == "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; - } - + if (width.length !== 0 && width[0].value == rightAnswer["width"]) { 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; + console.log('width matched'); } - else { - const result = xpath.select(ele, gpdpXmlDoc); - const result2 = null; - let isCheck = false; - - 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; + 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 (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; +} +scoringResult['총점'] = totalScore; +return scoringResult; } diff --git a/package-lock.json b/package-lock.json index 747de00..0f99bf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "jsonpath": "^1.1.1", + "loadsh": "^0.0.4", "psd": "^3.4.0", "string-similarity": "^4.0.4", "xlsx": "^0.18.5", @@ -209,6 +210,13 @@ "node": ">= 0.8.0" } }, + "node_modules/loadsh": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/loadsh/-/loadsh-0.0.4.tgz", + "integrity": "sha512-U+wLL8InpfRalWrr+0SuhWgGt10M4OyAk6G8xCYo2rwpiHtxZkWiFpjei0vO463ghW8LPCdhqQxXlMy2qicAEw==", + "deprecated": "This is a typosquat on the popular Lodash package. This is not maintained nor is the original Lodash package.", + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", diff --git a/package.json b/package.json index ea43b59..bfd640c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "license": "ISC", "dependencies": { "jsonpath": "^1.1.1", + "loadsh": "^0.0.4", "psd": "^3.4.0", "string-similarity": "^4.0.4", "xlsx": "^0.18.5", diff --git a/psdExport_2.js b/psdExport_2.js index d98594c..d4660d8 100644 --- a/psdExport_2.js +++ b/psdExport_2.js @@ -9,8 +9,8 @@ const { DOMParser } = require('xmldom'); const findSimilarString = require('./findSimilarString'); const getGpdpScore = require('./gpdpScoring.js'); const getToday = require('./getToday.js'); -const { userInfo } = require('os'); -const { get } = require('http'); +// const { userInfo } = require('os'); +// const { get } = require('http'); const todayDate = getToday(); const examRound = '2504'; diff --git a/z.xbook b/z.xbook index 9010b61..4745597 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":"//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='3']/@Length"},{"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":"//Document/Width/@value | //Document/Height/@value"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='Flower']]/Effects/Item[EffectData/VibranceValue]/(Name/@value | EffectData/VibranceValue/@value)"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='Flower']]/Effects/Item[EffectData/VibranceValue]/Name/@value | //Layer[Name[@value='Flower']]/Effects/Item/EffectData/VibranceValue/@value"}] \ 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":"//CRTrackList[@Name='비디오1']/CRTrackClip[@ClipIndex='3']/@Length"},{"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":"//Layer[MaskOpType/@value='Clipping']"},{"kind":2,"language":"xpath","value":"//Layer//Shape[shape_type/@value='RECTANGLE' and contains(draw_type/@value, 'Outline')]/primary_color/@value"},{"kind":2,"language":"xpath","value":"//Layer[Name[@value='Flower']]/Effects/Item[EffectData/VibranceValue]/Name/@value | //Layer[Name[@value='Flower']]/Effects/Item/EffectData/VibranceValue/@value"}] \ No newline at end of file diff --git a/회차별채점자료/2504/excel_채점기준표/DPI_2504B.xlsx b/회차별채점자료/2504/excel_채점기준표/DPI_2504B.xlsx index d17604a..b69d3d2 100644 Binary files a/회차별채점자료/2504/excel_채점기준표/DPI_2504B.xlsx and b/회차별채점자료/2504/excel_채점기준표/DPI_2504B.xlsx differ