diff --git a/.gitignore b/.gitignore index 137430d..800b1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ _old_excel_files/ output/ input/ -*.hwp + +~$*.xlsx diff --git a/250512_DIW_2504회_A형_TEST.xlsx b/250512_DIW_2504회_A형_TEST.xlsx new file mode 100644 index 0000000..8eb06b5 Binary files /dev/null and b/250512_DIW_2504회_A형_TEST.xlsx differ diff --git a/DIW_2504A_new.json b/DIW_2504A_new.json new file mode 100644 index 0000000..f004b2e --- /dev/null +++ b/DIW_2504A_new.json @@ -0,0 +1,319 @@ +{ + "0": { + "0": { + "path": "", + "path2": "", + "points": 0, + "category": "파일저장", + "item": "파일명 (수검번호.hwp/hwpx)" + }, + "1": { + "path": "//PAGEMARGIN", + "value": { + "Bottom": 5669, + "Footer": 2834, + "Gutter": 0, + "Header": 2834, + "Left": 5669, + "Right": 5669, + "Top": 5669 + }, + "tolerance": 1, + "points": 4, + "category": "PageSetting", + "item": "A4용지, 왼쪽/오른쪽/위쪽/아래쪽 (각20mm), 머리말/꼬리말 (10mm), 제본(0mm)" + }, + "2": { + "path": "//STYLE[@Name='바탕글']", + "value": { + "FontName": "바탕", + "FontSize": "1000", + "Alignment": "Justify", + "LineSpacing": "160" + }, + "points": 4, + "category": "BasicSetting", + "item": "글꼴 (바탕, 10pt), 양쪽정렬, 줄간격 (160%)" + }, + "3": { + "path": "", + "searchValue": null, + "value": null, + "points": 40, + "category": "오타감점", + "item": "오타 1개 -1점 / 2503회부터 오타 1개 -1점으로 변경" + } + }, + "1": { + "1": { + "path": "//TEXTART[@Text='{searchValue}']/TEXTARTSHAPE/@FontName", + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": "맑은 고딕", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/① 글씨체 (맑은 고딕)" + }, + "2": { + "path": "//TEXTART[@Text='{searchValue}']/descendant::WINDOWBRUSH/@FaceColor", + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": "28,61,98", + "points": 2, + "category": "Color", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/② 채우기 : 색상(RGB:100,170,92)" + }, + "3": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Width", + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": "110", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/③ 크기-너비 (110mm)" + }, + "4": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Height", + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": "20", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/④ 크기-높이 (20mm)" + }, + "5": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/POSITION/@TreatAsChar", + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": "true", + "points": 2, + "category": "SingleAnswer", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/⑤ 위치 (글자처럼 취급)" + }, + "6": { + "path": "//PARASHAPE[@Id=//TEXTART[@Text='{searchValue}']/ancestor::P/@ParaShape]/@Align", + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": "Center", + "points": 2, + "category": "SingleAnswer", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/⑥ 정렬 (가운데 정렬)" + }, + "7": { + "path": "//TEXTART[@Text='{searchValue}']", + "path2": null, + "searchValue": "클라우드컴퓨팅컨퍼런스", + "value": true, + "points": 2, + "category": "Boolean", + "item": "문구 (클라우드컴퓨팅컨퍼런스)/⑦ 글맵시모양 (육안확인)" + }, + "8": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/SIZE", + "path2": null, + "searchValue": "전", + "value": { + "Height": 2800, + "Width": 2800 + }, + "tolerance": 200, + "points": 1, + "category": "TwoLineSize", + "item": "전/① 모양 (2줄)" + }, + "9": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "path2": null, + "searchValue": "전", + "value": "궁서체", + "points": 1, + "category": "FontName", + "item": "전/② 글씨체 (궁서체)" + }, + "10": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor", + "path2": null, + "searchValue": "전", + "value": "255,132,58", + "points": 2, + "category": "Color", + "item": "전/③ 면색 : 색상(RGB:255,132,58)" + }, + "11": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//OUTSIDEMARGIN/@Right", + "path2": null, + "searchValue": "전", + "value": "850", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "전/④ 본문과의 간격 : 3.0mm" + }, + "12": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", + "path2": null, + "searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회", + "value": "BOLD", + "points": 2, + "category": "FontAttribute", + "item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/① 진하게" + }, + "13": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", + "searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회", + "value": "UNDERLINE", + "points": 2, + "category": "FontAttribute", + "item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/② 밑줄" + }, + "14": { + "path": "//CHAR[contains(text(),'{char1}')]", + "path2": "//CHAR[contains(text(),'{char2}')]", + "path3": "//CHAR[contains(text(),'{char3}')]", + "char1": "●", + "char2": "●", + "char3": "※", + "value": 3, + "points": 3, + "category": "SpecialChar", + "item": "① ●, ② ●, ③ ※" + }, + "15": { + "path": "//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape", + "searchValue": "■ 행사안내 ■", + "value": "돋움", + "points": 1, + "category": "FontName", + "item": "문구 (■ 행사안내 ■)/① 글씨체 (돋움)" + }, + "16": { + "path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align", + "searchValue": "■ 행사안내 ■", + "value": "Center", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (■ 행사안내 ■)/② 정렬 (가운데 정렬)" + }, + "17": { + "path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]", + "searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수", + "value": "ITALIC", + "points": 1, + "category": "FontAttribute", + "item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/① 기울임" + }, + "18": { + "path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]", + "searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수", + "value": "UNDERLINE", + "points": 1, + "category": "FontAttribute", + "item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/② 밑줄" + }, + "19": { + "path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN", + "searchValue": "기타사항", + "value": { + "Left": 15, + "Indent": 12 + }, + "points": 2, + "category": "ParaShape", + "item": "문구 (※ 기타… 이하 문단)/왼쪽여백 (15pt), 내어쓰기 (12pt)", + "desc": "내부적으로 내어쓰기는 음수값" + }, + "20": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]/@Height", + "searchValue": "2025. 03. 22.", + "value": "1300", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (2025. 03. 22.)/① 크기 (13pt)" + }, + "21": { + "path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align", + "searchValue": "2025. 03. 22.", + "value": "Center", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (2025. 03. 22.)/② 정렬 (가운데 정렬)" + }, + "22": { + "path": "//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape", + "searchValue": "한국고건축협회", + "value": "궁서", + "points": 1, + "category": "FontName", + "item": "문구 (한국고건축협회)/① 글씨체 (궁서)" + }, + "23": { + "path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]/@Height", + "searchValue": "한국고건축협회", + "value": "2400", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (한국고건축협회)/② 크기 (24pt)" + }, + "24": { + "path": "//PARASHAPE[@Id=//CHAR[text()='{searchValue}']/ancestor::P/@ParaShape]/@Align", + "searchValue": "한국고건축협회", + "value": "Center", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (한국고건축협회)/③ 정렬 (가운데 정렬)" + }, + "25": { + "path": "//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape", + "searchValue": "DIAT", + "value": "굴림", + "points": 1, + "category": "FontName", + "item": "문구 (DIAT)/① 글꼴 (굴림)" + }, + "26": { + "path": "//CHARSHAPE[@Id=//SECTION[1]//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]/@Height", + "searchValue": "DIAT", + "value": "900", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (DIAT)/② 크기 (9pt)" + }, + "27": { + "path": "//PARASHAPE[@Id=//SECTION[1]//CHAR[text()='{searchValue}']/parent::TEXT/parent::P/@ParaShape]/@Align", + "searchValue": "DIAT", + "value": "Right", + "points": 1, + "category": "SingleAnswer", + "item": "문구 (DIAT)/③ 정렬 (오른쪽 정렬)" + }, + "28": { + "path": "//PAGENUM/@FormatType", + "value": "HangulSyllable", + "points": 2, + "category": "SingleAnswer", + "item": "① 쪽 번호 매기기 (가,나,다 순으로)" + }, + "29": { + "path": "//PAGENUM/@Pos", + "value": "BottomCenter", + "points": 2, + "category": "SingleAnswer", + "item": "② 가운데 아래" + }, + "30": { + "path": "not(//PARASHAPE[@Id=//SECTION[1]/P/@ParaShape]/PARAMARGIN[@LineSpacing!='180'])", + "value": true, + "points": 2, + "category": "Boolean", + "item": "문제 1 줄간격 180% 설정", + "desc": "1페이지 문단의 줄간격이 180%가 아닌 문단이 있으면 False(감점)" + } + }, + "2": { + "1": { + "path": "//SECTION[2]//PAGEBORDERFILL[@Type='Both' or @Type='Even']", + "path2": "boolean(//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@HeaderInside='true' and //BORDERFILL[@Id=//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@BorferFill]/*[contains(local-name(), 'BORDER')]/@Type='DoubleSlim')", + "value": true, + "points": 4, + "category": "PageBorder", + "item": "문제2 쪽테두리(이중 실선, 머리말 포함) 설정" + }, + "61": {} + } +} \ No newline at end of file diff --git a/diwScoring.py b/diwScoring.py index 064d246..2836095 100644 --- a/diwScoring.py +++ b/diwScoring.py @@ -42,6 +42,7 @@ class XMLScorer: hyperlink_xpath = self.scoring_criteria["1"]["17"]["hyperlink_xpath"] right_text = self.scoring_criteria["1"]["17"]["searchValue"].replace(" ","") try: + # 하이퍼링크가 포함된 p태그 인지 확인 p_elements = root.xpath(is_hyperlink) for p in p_elements: diff --git a/diwScoring2.py b/diwScoring2.py index cf93207..6528393 100644 --- a/diwScoring2.py +++ b/diwScoring2.py @@ -10,6 +10,7 @@ import re from difflib import SequenceMatcher import pandas as pd import base64 +import math # from xpathSearch import XMLPathHandler class XMLScorer: @@ -32,6 +33,13 @@ class XMLScorer: with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) + # mm to pt + def convert_mm_to_pt(self, mm): + one_mm_per_pt = 2.83465 + hwp_internal_conversion_method = 100 + pt = math.trunc(mm * one_mm_per_pt * hwp_internal_conversion_method) + return pt + # XML 파일에서 element의 값을 찾아 반환 def query_xml(self, root, *args): first_xpath = args[0] @@ -188,6 +196,7 @@ class XMLScorer: id = criterion_id xpath = criterion.get('path', None) xpath2 = criterion.get('path2', None) + xpath3 = criterion.get('path3', None) search_value = criterion.get('searchValue', None) right_answer = criterion.get('value', None) points = criterion.get('points', 0) @@ -277,7 +286,20 @@ class XMLScorer: if scoring['points'] > 0: break - + + elif "mmSize" in (category or ""): + items = root.xpath(xpath) + error_range = criterion.get('tolerance', 0) + + for item in items: + user_answer = int(item) + right_answer = self.convert_mm_to_pt(int(right_answer)) + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance") + + if scoring['points'] > 0: + break + elif "ParaShape" in (category or ""): items = root.xpath(xpath) @@ -362,31 +384,36 @@ class XMLScorer: elif "SpecialChar" in (category or ""): ch1 = criterion.get('char1', None) ch2 = criterion.get('char2', None) + ch3 = criterion.get('char3', None) xpath = xpath.replace('{char1}', ch1) xpath2 = xpath2.replace('{char2}', ch2) + xpath3 = xpath3.replace('{char3}', ch3) char1_ele = root.xpath(xpath) char2_ele = root.xpath(xpath2) + char3_ele = root.xpath(xpath3) sum_char = 0 # char1 요소에서 특수문자 갯수 세기 (최대 2점) - if not char1_ele: - user_answer = 0 - else: - for item in char1_ele: - count_char1 = item.text.count(ch1) - sum_char += count_char1 - if sum_char >= 2: - sum_char = 2 - break + for item in char1_ele or []: + count_char1 = item.text.count(ch1) + sum_char += count_char1 + if sum_char >= 2: + sum_char = 2 + break # char2 요소에서 특수문자 갯수 세기 (최대 1점) - if not char2_ele: - user_answer = 0 - else: + if (ch1 != ch2) and char2_ele: count_char2 = char2_ele[0].text.count(ch2) if count_char2 > 1: count_char2 = 1 sum_char += count_char2 + + # char2 요소에서 특수문자 갯수 세기 (최대 1점) + if char3_ele: + count_char3 = char3_ele[0].text.count(ch3) + if count_char3 > 1: + count_char3 = 1 + sum_char += count_char3 user_answer = sum_char @@ -691,7 +718,7 @@ class XMLScorer: def main(): # 시험회차 및 유형 - exam_round = '2504_2' + exam_round = '2504' exam_types = [ 'A', # 'B', @@ -704,7 +731,7 @@ def main(): for exam_type in exam_types: # JSON 채점기준표 파일 (예시:DIW_2503A.json) # scoring_criteria_path = f'./DIW_{exam_round}.json' - scoring_criteria_path = f'./DIW_{exam_round}_{exam_type}_new.json' + scoring_criteria_path = f'./DIW_{exam_round}{exam_type}_new.json' # xml(hml)파일 디렉토리 경로 (예시:./output/A/DIW) # xml_directory = f'./output/{exam_type}/{"TEST" if test_mode else "DIW"}' diff --git a/회차별채점자료/2502/hwp_정답/DIW_2502A.hwp b/회차별채점자료/2502/hwp_정답/DIW_2502A.hwp new file mode 100644 index 0000000..02d2ff6 Binary files /dev/null and b/회차별채점자료/2502/hwp_정답/DIW_2502A.hwp differ diff --git a/회차별채점자료/2502/hwp_정답/DIW_2502B.hwp b/회차별채점자료/2502/hwp_정답/DIW_2502B.hwp new file mode 100644 index 0000000..6086f7e Binary files /dev/null and b/회차별채점자료/2502/hwp_정답/DIW_2502B.hwp differ diff --git a/회차별채점자료/2502/hwp_정답/DIW_2502C.hwp b/회차별채점자료/2502/hwp_정답/DIW_2502C.hwp new file mode 100644 index 0000000..d20638f Binary files /dev/null and b/회차별채점자료/2502/hwp_정답/DIW_2502C.hwp differ diff --git a/회차별채점자료/2502/hwp_정답/DIW_2502D.hwp b/회차별채점자료/2502/hwp_정답/DIW_2502D.hwp new file mode 100644 index 0000000..92147e7 Binary files /dev/null and b/회차별채점자료/2502/hwp_정답/DIW_2502D.hwp differ diff --git a/회차별채점자료/2502/hwp_정답/DIW_2502E.hwp b/회차별채점자료/2502/hwp_정답/DIW_2502E.hwp new file mode 100644 index 0000000..c22ba01 Binary files /dev/null and b/회차별채점자료/2502/hwp_정답/DIW_2502E.hwp differ diff --git a/회차별채점자료/2503/hwp_정답/DIW_2503C.hwp b/회차별채점자료/2503/hwp_정답/DIW_2503C.hwp new file mode 100644 index 0000000..154e9b5 Binary files /dev/null and b/회차별채점자료/2503/hwp_정답/DIW_2503C.hwp differ diff --git a/회차별채점자료/2503/hwp_정답/문제2503C.hwp b/회차별채점자료/2503/hwp_정답/문제2503C.hwp new file mode 100644 index 0000000..17045da Binary files /dev/null and b/회차별채점자료/2503/hwp_정답/문제2503C.hwp differ diff --git a/회차별채점자료/2504_2/hwp_정답/DIW_2504A.hwp b/회차별채점자료/2504_2/hwp_정답/DIW_2504A.hwp new file mode 100644 index 0000000..a7a8263 Binary files /dev/null and b/회차별채점자료/2504_2/hwp_정답/DIW_2504A.hwp differ diff --git a/회차별채점자료/2504_2/hwp_정답/DIW_2504A_question.hwp b/회차별채점자료/2504_2/hwp_정답/DIW_2504A_question.hwp new file mode 100644 index 0000000..a3a599e Binary files /dev/null and b/회차별채점자료/2504_2/hwp_정답/DIW_2504A_question.hwp differ diff --git a/회차별채점자료/2504_3/hwp_정답/DIW_2504A.hwp b/회차별채점자료/2504_3/hwp_정답/DIW_2504A.hwp new file mode 100644 index 0000000..a7a8263 Binary files /dev/null and b/회차별채점자료/2504_3/hwp_정답/DIW_2504A.hwp differ diff --git a/회차별채점자료/2504_3/hwp_정답/DIW_2504A_question.hwp b/회차별채점자료/2504_3/hwp_정답/DIW_2504A_question.hwp new file mode 100644 index 0000000..a3a599e Binary files /dev/null and b/회차별채점자료/2504_3/hwp_정답/DIW_2504A_question.hwp differ diff --git a/회차별채점자료/_2521/hwp_정답/DIW_2521A.hwp b/회차별채점자료/_2521/hwp_정답/DIW_2521A.hwp new file mode 100644 index 0000000..155fbb0 Binary files /dev/null and b/회차별채점자료/_2521/hwp_정답/DIW_2521A.hwp differ diff --git a/회차별채점자료/_2521/hwp_정답/DIW_2521B.hwp b/회차별채점자료/_2521/hwp_정답/DIW_2521B.hwp new file mode 100644 index 0000000..0605045 Binary files /dev/null and b/회차별채점자료/_2521/hwp_정답/DIW_2521B.hwp differ diff --git a/회차별채점자료/_2521/hwp_정답/DIW_2521C.hwp b/회차별채점자료/_2521/hwp_정답/DIW_2521C.hwp new file mode 100644 index 0000000..cd125ab Binary files /dev/null and b/회차별채점자료/_2521/hwp_정답/DIW_2521C.hwp differ diff --git a/회차별채점자료/_2521/hwp_정답/DIW_2521D.hwp b/회차별채점자료/_2521/hwp_정답/DIW_2521D.hwp new file mode 100644 index 0000000..b91b34f Binary files /dev/null and b/회차별채점자료/_2521/hwp_정답/DIW_2521D.hwp differ