diff --git a/250903_DIW_2508C_TEST.xlsx b/250903_DIW_2508C_TEST.xlsx new file mode 100644 index 0000000..2083ffd Binary files /dev/null and b/250903_DIW_2508C_TEST.xlsx differ diff --git a/250903_DIW_2508C_채점결과.xlsx b/250903_DIW_2508C_채점결과.xlsx new file mode 100644 index 0000000..ab16312 Binary files /dev/null and b/250903_DIW_2508C_채점결과.xlsx differ diff --git a/diwScoring2.py b/diwScoring2.py index 56381f1..eb306a1 100644 --- a/diwScoring2.py +++ b/diwScoring2.py @@ -218,7 +218,6 @@ class XMLScorer: category = criterion.get('category', None) item = criterion.get('item', None) option = criterion.get('option', None) - page = criterion.get('page', None) similar_text = None # search_value가 있는 경우 @@ -1321,8 +1320,8 @@ def main(): exam_types = [ # 'A', # 'B', - # 'C', - 'D', + 'C', + # 'D', ] # test_mode = False diff --git a/diwScoring2_new.py b/diwScoring2_new.py new file mode 100644 index 0000000..31d9e8a --- /dev/null +++ b/diwScoring2_new.py @@ -0,0 +1,1445 @@ +from datetime import datetime +import difflib +import json +from pathlib import Path +import os +from lxml import etree as ET +import re +from difflib import SequenceMatcher +import pandas as pd +import base64 +import math +from itertools import chain + + +# from xpathSearch import XMLPathHandler + +class XMLScorer: + # 채점 기준 경로 초기화 + def __init__(self, scoring_criteria_path): + # 채점 기준 로드 + self.scoring_criteria = self._load_scoring_criteria(scoring_criteria_path) + self.total_score = 0 + self.partial_score = 0 + self.typo_score = 0 + + def set_typo_score(self, score): + self.typo_score = score + + def get_typo_score(self): + return self.typo_score + + # 채점 기준파일 로드(JSON 파일) + def _load_scoring_criteria(self, file_path): + 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_scale = 100 + pt = math.trunc(mm * one_mm_per_pt * hwp_scale) + return pt + + def convert_pt_to_mm(self, pt): + one_mm_per_pt = 2.83465 + hwp_scale = 100 + mm = round(pt / (one_mm_per_pt * hwp_scale), 1) + return mm + + # 유사한 텍스트 찾기 + def find_similar_text(self, root, target_text, xml_type, threshold=0.7): + """ + 전체 문서에서 유사한 텍스트를 찾아 반환 + + Args: + root (_type_): xml root element 객체 + target_text (_type_): 찾을 텍스트 + threshold (float, optional): 유사도 설정 Defaults to 0.3. + + Returns: + str: 유사도 기준을 만족하는 텍스트 + """ + # 전체 텍스트 추출 + # all_text = root.xpath(f"//CHAR/text()") + # all_text.append(root.xpath(f"//TEXTART/@text")) + + namespaces = { + 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main', + 'c': 'http://schemas.openxmlformats.org/drawingml/2006/chart' + } + + if xml_type == "hml": + all_text = root.xpath(f"//BODY//text() | //TEXTART/@Text") if root is not None else [] + + elif xml_type == "chart": + all_text = root.xpath(f"//c:chart//text()", namespaces=namespaces) if root is not None else [] + + else: + all_text = [] + + # 유사도 비교 + max_score = 0 + similar_text = '' + + for text in all_text: + score = SequenceMatcher(None, target_text, text).ratio() + + if score > max_score: + max_score = score + similar_text = text + + if max_score >= threshold: + return similar_text + else: + return target_text + + # 정답 비교 및 점수 계산 + def evaluate_answer(self, scoring, user_answer, right_answer, points, + method="equal", tolerance=0): + + scoring['user_answer'] = user_answer + + is_correct = False + + # 일치 여부 확인 + if method == "equal": + is_correct = (user_answer == right_answer) + + # 정답이 오차범위가 필요한 경우 + elif method == "tolerance": + if isinstance(user_answer, dict) and isinstance(right_answer, dict): + is_correct = all(abs(user_answer[k] - right_answer[k]) <= tolerance for k in right_answer) + else: + is_correct = abs(user_answer - right_answer) <= tolerance + + # 정답이 포함되어 있는 경우 + elif method == "in": + is_correct = user_answer in right_answer + + # 정답을 부분점수로 계산(특수문자, 한자) + elif method == "partial_score": + # 부분 점수 계산 + is_correct = isinstance(user_answer, (int, float)) and user_answer <= right_answer + points = min(points, user_answer) + else: + raise ValueError(f"Unknown comparison method: {method}") + + if is_correct: + scoring['points'] = points + self.total_score += points + self.partial_score += points + else: + scoring['points'] = 0 + + # 하나의 XML 파일 채점 + def _score_xml_file(self, xml_file, chart_xml): + + def parse_pages_by_bookmark(root): + """ + P/TEXT/BOOKMARK 구조를 가진 XML에서 페이지 구간별 전체 XML을 저장 + """ + pages = {} + all_p_tags = root.xpath('//P') + + current_page = None + page_start_index = None + + for i, p in enumerate(all_p_tags): + bookmark = p.xpath('./TEXT/BOOKMARK') + if bookmark: + name = bookmark[0].get('Name') + if name and name.endswith('_start'): + current_page = name.replace('_start', '') + page_start_index = i + elif name and name.endswith('_end') and current_page is not None: + page_end_index = i + # 페이지 전체 XML fragment 만들기 + page_root = ET.Element("Page") + for ptag in all_p_tags[page_start_index:page_end_index + 1]: + page_root.append(ptag) + + # dict에 저장 (ElementTree 자체 저장) + pages[current_page] = page_root + + current_page = None + page_start_index = None + + return pages + + def extract_char_text_from_p(p_element): + """ + 주어진

요소에서 모든 자손 의 텍스트를 추출해 문자열 리스트로 반환합니다. + """ + full_text = [] + for p in p_element: + char_elements = p.xpath('.//CHAR') + combined_text = ''.join([char.text for char in char_elements if char.text]) + no_space_text = re.sub(r'\s+', '', combined_text) # 공백 문자 제거 + full_text.append(no_space_text) + return full_text + + def has_elements(ptags, xpath): + for p in ptags: + element_list = p.xpath(xpath) if xpath else [] + if element_list: + return True + return False + + def get_items(xpath, pages, root, use_page2=False): + if not xpath: + return [] + if use_page2: + return pages["Page_2"].xpath(xpath) + return root.xpath(xpath) + + try: + tree = ET.parse(xml_file) + root = tree.getroot() + + # XML문서 페이지 파싱 전처리 + pages = parse_pages_by_bookmark(root) + # print("🚩Pages : ", pages) + + # 네임스페이스 정의 + namespaces = { + 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main', + 'c': 'http://schemas.openxmlformats.org/drawingml/2006/chart' + } + + # 차트 XML 파일이 없는 경우 0점 채점을 위헤 빈 XML 생성 + if chart_xml is None: + chart_tree = ET.fromstring('') + else: + chart_tree = ET.fromstring(chart_xml) + + # 결과값을 Dictionary로 저장 + # 하나의 xml파일 = 수험생 한명의 답안지 + onePersonResult = { + 'filename': os.path.basename(xml_file), + 'score_results': [], + 'total_score': 0, + 'partial_scores': [] + } + print(f"🔜File name: {onePersonResult['filename']}") + + self.total_score = 0 + for section_id, section in self.scoring_criteria.items(): + self.partial_score = 0 + for criterion_id, criterion in section.items(): + id = criterion_id + xpath = criterion.get('path', None) + xpath2 = criterion.get('path2', None) + xpath3 = criterion.get('path3', None) + chart_xpath = criterion.get('chart_xpath', None) + search_value = criterion.get('searchValue', None) + right_answer = criterion.get('value', None) + points = criterion.get('points', 0) + category = criterion.get('category', None) + item = criterion.get('item', None) + option = criterion.get('option', None) + page = criterion.get('page', None) + similar_text = None + + # search_value가 있는 경우 + if search_value is not None: + if xpath or xpath2: + similar_text = self.find_similar_text(root, search_value, xml_type="hml") + xpath = xpath.replace('{searchValue}', similar_text) if xpath else "" + xpath2 = xpath2.replace('{searchValue}', similar_text) if xpath2 else "" + if chart_xpath: + similar_text = self.find_similar_text(chart_tree, search_value, xml_type="chart") + chart_xpath = chart_xpath.replace('{searchValue}', similar_text) if chart_xpath else "" + + if option: + xpath = xpath.replace('{option}', option) if xpath else "" + xpath2 = xpath2.replace('{option}', option) if xpath2 else "" + chart_xpath = chart_xpath.replace('{option}', option) if chart_xpath else "" + + + # 문항 별 채점 결과 저장 + scoring = { + 'section': section_id, + 'id': id, + 'category': category, # 채점 분류 + 'item': item, # 채점 항목 + 'right_answer': right_answer, # 정답 + 'user_answer': None, # 실제 작성 답안 + 'points': 0, # 점수 + } + + try: + if (category or "") == "PageSetting": + items = root.xpath(xpath) + error_range = criterion.get('tolerance', 0) + + right_answer = { + 'Top' : float(right_answer.get("Top", 0)), + 'Bottom' : float(right_answer.get("Bottom", 0)), + 'Left' : float(right_answer.get("Left", 0)), + 'Right' : float(right_answer.get("Right", 0)), + 'Header' : float(right_answer.get("Header", 0)), + 'Footer' : float(right_answer.get("Footer", 0)), + 'Gutter' : float(right_answer.get("Gutter", 0)), + } + right_answer = { + k: self.convert_mm_to_pt(v) + for k, v in right_answer.items() + } + + for item in items: + user_answer = { + 'Top' : float(item.get("Top", 0)), + 'Bottom' : float(item.get("Bottom", 0)), + 'Left' : float(item.get("Left", 0)), + 'Right' : float(item.get("Right", 0)), + 'Header' : float(item.get("Header", 0)), + 'Footer' : float(item.get("Footer", 0)), + 'Gutter' : float(item.get("Gutter", 0)), + } + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance", tolerance=error_range) + + if scoring['points'] > 0: + break + + elif (category or "") == "BasicSetting": + # FontName, FontSize, Alignment, LineSpacing + # 해당 속성의 요소(텍스트)가 문서 내부에 존재하면 정답처리 + + matches = set() + + # P 태그 순회 + for p_tag in root.xpath(".//P"): + parashape = p_tag.get("ParaShape") + + for text_tag in p_tag.xpath(".//TEXT"): + charshape = text_tag.get("CharShape") + + if parashape is not None and charshape is not None: + matches.add((parashape, charshape)) + + # 출력 + for para, char in matches: + # print(f"ParaShape = {para}, CharShape = {char}") + font_id = root.xpath(f"//CHARSHAPE[@Id='{char}']/FONTID/@Hangul") + font_name = root.xpath(f"//FONTFACE[@Lang='Hangul']/FONT[@Id='{font_id[0]}']/@Name") + + user_answer = { + 'FontName': font_name[0], + 'FontSize': root.xpath(f"//CHARSHAPE[@Id='{char}']/@Height")[0], + 'Alignment': root.xpath(f"//PARASHAPE[@Id='{para}']/@Align")[0], + 'LineSpacing': root.xpath(f"//PARASHAPE[@Id='{para}']/PARAMARGIN/@LineSpacing")[0] + } + + # 정답과 수험자 답안 비교 + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + + if scoring['points'] > 0: + break + + # 1, 2페이지 모두 정답이어야 함 + elif (category or "") == "PageNumber": + items = root.xpath(xpath) if xpath else [] + + all_match = True + for item in chain(items): + user_answer = item + if right_answer != user_answer: + all_match = False + break + + if all_match: + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + else: + self.evaluate_answer(scoring, user_answer, right_answer, 0, method="equal") + + + # 오타 감점 부분은 미리 계산 하고, 이후 점수만 계산 + elif (category or "") == "오타감점": + points = self.get_typo_score() + self.total_score += points + self.partial_score += points + scoring['points'] = points + + # 테이블의 경우 모든 셀에 요구사항이 적용되어야 정답처리 + elif (category or "") == "TableAnswer": + items = root.xpath(xpath) if xpath else [] + items2 = root.xpath(xpath2) if xpath2 else [] + + def is_all_match(item_list): + return item_list and all(item == right_answer for item in item_list) + ## 위 코드와 동일한 기능(풀어서 설명) + # 리스트가 비어 있으면 False 반환 + # if not item_list: + # return False + + # # 리스트의 모든 항목이 right_answer와 같은지 검사 + # for item in item_list: + # if item != right_answer: + # return False # 하나라도 다르면 False 반환 + + # return True # 전부 일치하면 True 반환 + + if is_all_match(items): + user_answer = right_answer + elif is_all_match(items2): + user_answer = right_answer + else: + user_answer = "" + points = 0 + + self.evaluate_answer(scoring, user_answer, right_answer, points) + + # [1-16] ◈ 행사안내 ◈ + # 특수문자와 글자의 속성이 같고 문서 내부에 '행사안내'와 같은 문자가 있을 경우 + # 유사도 문제로 의도치 않은 다른 부분의 텍스트 속성이 채점되는것을 방지하고자 + # 해당 문자를 포함하는 모든 문단의 속성을 판단해 + # 정렬값이 정답과 일치하는 경우 정답으로 채점 + elif (category or "") == "Align": + match_str = criterion.get('match_str', None) + + xpath = xpath.replace('{match_str}', match_str) + items = root.xpath(xpath) + + for item in items: + user_answer = item + self.evaluate_answer(scoring, user_answer, right_answer, points) + if scoring['points'] > 0: + break + + elif (category or "") == "majorGridlines": + # 줄/칸 전환여부 확인 + # table_col_count = root.xpath("//SECTION[2]//TABLE/@ColCount") + table_col_count = root.xpath("//TABLE/@ColCount") + + # print("🟡테이블 열 개수: ", int(table_col_count[0]) if table_col_count else 0) + + chart_ser_count = chart_tree.xpath("count(//c:ser)", namespaces=namespaces) if chart_xpath else 0 + + # print("🟡차트 데이터 개수: ", int(chart_ser_count) if isinstance(chart_ser_count, (int, float)) else 0) + + isXYtransposed = False + if table_col_count and chart_ser_count: + if int(chart_ser_count) > int(table_col_count[0])-1: + isXYtransposed = True + + # 값 축 주눈금선 존재하는지 여부 확인 + items = chart_tree.xpath(chart_xpath, namespaces=namespaces) if chart_xpath else [] + + for item in items: + # item이 존재하면 True, 없으면 False + user_answer = (item is not None) and isXYtransposed + + # 정답과 수험자 답안 비교 + self.evaluate_answer(scoring, user_answer, right_answer, points) + if scoring['points'] > 0: + break + + + # 정답이 하나인 경우 + # elif (category or "") in ["OneAnswer", "ChartOneAnswer"]: + elif "OneAnswer" in (category or ""): + if "Header" in category: + page1_ptags = pages.get('Page_1', []) + page2_ptags = pages.get('Page_2', []) + header_xpath = "//HEADER//P" + has_page1_element = has_elements(page1_ptags, header_xpath) + has_page2_element = has_elements(page2_ptags, header_xpath) + + if not has_page1_element or not has_page2_element: + user_answer = None + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + + # 2페이지 글상자인지 확인 + is_page2_rectangle = False + if "Rectangle" in category: + page2_ptags = pages.get('Page_2', []) + rectangle_xpath = "//RECTANGLE" + has_page2_element = has_elements(page2_ptags, rectangle_xpath) + + if not has_page2_element: + user_answer = None + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + else: + is_page2_rectangle = True + + # items = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + # items2 = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + + items = root.xpath(xpath) if xpath else [] + items2 = root.xpath(xpath2) if xpath2 else [] + + # 차트 XML에서 정답을 찾는 경우 + # 차트 종류가 + # 세로막대형이면 x축이 카테고리(catAx) y축이 값(valAx) + # 가로막대형이면 x축이 값(valAx) y축이 카테고리(catAx) + if "ChartOneAnswer" in category: + # 하드코딩이라 [2-45문항] 변경시 수정 필요 + # chart_type = self.scoring_criteria["2"]["45"]["chart_type"].replace(" ","") + + # chart_type 변수의 경우 45번 문항을 먼저 채점하므로 + # xy축의 변경이 필요한 53~58번 문항 채점시에 chart_type변수에 차트모양의 정보는 입력 되어있음 + + # 가로 차트일 경우에만 x축과 y축을 바꿔줌 + # 세로, 꺾은선, 원형 차트의 경우 그대로 사용 + if "가로" in chart_type: + if "catAx" in chart_xpath: + chart_xpath = chart_xpath.replace("catAx", "valAx") + elif "valAx" in chart_xpath: + chart_xpath = chart_xpath.replace("valAx", "catAx") + + chart_items = chart_tree.xpath(chart_xpath, namespaces=namespaces) if chart_xpath else [] + + for item in chain(items, items2, chart_items): + user_answer = item.replace(" ", "") if isinstance(item, str) else item + right_answer = right_answer.replace(" ", "") + + self.evaluate_answer(scoring, user_answer, right_answer, points) + if scoring['points'] > 0: + break + + # [2-6] 테두리 이중실선 1.00mm + # elif (category or "") == "LineShape": + elif "LineShape" in (category or ""): + user_answer = { + 'Style': None, + 'Width': None + } + + # 2페이지 글상자인지 확인 + is_page2_rectangle = False + if "Rectangle" in category: + page2_ptags = pages.get('Page_2', []) + rectangle_xpath = "//RECTANGLE" + has_page2_element = has_elements(page2_ptags, rectangle_xpath) + + if not has_page2_element: + user_answer = None + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + else: + is_page2_rectangle = True + + line_shapes = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + + # line_shapes = root.xpath(xpath) if xpath else [] + + for line_shape in line_shapes: + style = line_shape.get("Style") + width = line_shape.get("Width") + + user_answer['Style'] = style + user_answer['Width'] = width + + self.evaluate_answer(scoring, user_answer, right_answer, points) + if scoring['points'] > 0: + break + + # 사용자 입력값이 mm단위인 경우 + # elif (category or "") == "mmSize": + elif "mmSize" in (category or ""): + # 2페이지 글상자인지 확인 + is_page2_rectangle = False + if "Rectangle" in category: + page2_ptags = pages.get('Page_2', []) + rectangle_xpath = "//RECTANGLE" + has_page2_element = has_elements(page2_ptags, rectangle_xpath) + + if not has_page2_element: + user_answer = None + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + else: + is_page2_rectangle = True + + items = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + + # 오차범위 설정 + # 한글 프로그램 내부에서 드물게 0mm이지만 1pt로 저장되는 경우가 있음 + # + # XML파일의 요소 옵션값은 내부적으로 1=0.01pt + # 이 경우를 대비하여 tolerance를 10으로 설정 (1pt=약0.04mm 만큼의 오차 혀용) + error_range = criterion.get('tolerance', 10) + + # JSON 파일 value키값에 mm나 공백이 입력될 경우 제거 + # 예) "80.2 mm" >> 80.2 로 변환 + float_string = right_answer.strip().replace("mm", "") + right_answer = self.convert_mm_to_pt(float(float_string)) + + if not items: + scoring['points'] = 0 + else: + for item in items: + user_answer = float(item) + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance", tolerance=error_range) + + if scoring['points'] > 0: + break + + elif (category or "") == "ParaShape": + items = root.xpath(xpath) + + for item in items: + user_answer = { + 'Left': float(item.get('Left', 0)) / 200, + 'Indent': float(item.get('Indent', 0)) / -200, + } + + # 정답과 수험자 답안 비교 + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + + if scoring['points'] > 0: + break + + # Boolean 타입 정답인 경우 + elif (category or "") == "Boolean": + items = root.xpath(xpath) if xpath else False + items2 = root.xpath(xpath2) if xpath2 else False + chart_items = chart_tree.xpath(chart_xpath, namespaces=namespaces) if chart_xpath else False + + user_answer = bool( items or items2 or chart_items ) + + self.evaluate_answer(scoring, user_answer, right_answer, points) + + # 채점기준표 파일에 작성된 rgb값을 그대로 읽어와 HML파일 요소의 int형 rgb값과 비교 + # elif (category or "") == "Color": + elif "Color" in (category or ""): + is_page2_rectangle = False + if "Rectangle" in category: + page2_ptags = pages.get('Page_2', []) + rectangle_xpath = "//RECTANGLE" + has_page2_element = has_elements(page2_ptags, rectangle_xpath) + + if not has_page2_element: + user_answer = None + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + else: + is_page2_rectangle = True + + items = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + items2 = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + + rgb_text = right_answer + + # 정규식을 이용해 숫자만 리스트로 추출 + numbers = re.findall(r'\d+', rgb_text) + r, g, b = map(int, numbers) if len(numbers) == 3 else None + + # 콤마(,)로 구분된 문자열을 정수형으로 변환 + # r, g, b = map(int, rgb_text.split(',')) + + rgb_int = (b << 16) + (g << 8) + r + + # items, items2를 순차적으로 순회 + for item in chain(items, items2): + user_answer = int(item) + self.evaluate_answer(scoring, user_answer, rgb_int, points, method="equal") + if scoring['points'] > 0: + break + + # 문단 첫글자 장식 채점 + elif (category or "") == "TwoLineSize": + items = root.xpath(xpath) + error_range = criterion.get('tolerance', 0) + for item in items: + user_answer = { + "Height": int(item.get('Height', 0)), + "Width": int(item.get('Width', 0)) + } + self.evaluate_answer(scoring, user_answer, right_answer, points, method="tolerance", tolerance=error_range) + + if scoring['points'] > 0: + break + + # 폰트명 + elif "FontName" in (category or ""): + # 'DIAT' 머릿말 문항 1,2페이지 둘 중 하나라도 없으면 0점 처리 + if "Header" in category: + page1_ptags = pages.get('Page_1', []) + page2_ptags = pages.get('Page_2', []) + header_xpath = "//HEADER//P" + has_page1_element = has_elements(page1_ptags, header_xpath) + has_page2_element = has_elements(page2_ptags, header_xpath) + + if not has_page1_element or not has_page2_element: + user_answer = "" + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + + # 2페이지 글상자인지 확인 + is_page2_rectangle = False + if "Rectangle" in category: + page2_ptags = pages.get('Page_2', []) + rectangle_xpath = "//RECTANGLE" + has_page2_element = has_elements(page2_ptags, rectangle_xpath) + + if not has_page2_element: + user_answer = None + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + continue + else: + is_page2_rectangle = True + + charshape_list = get_items(xpath, pages, root, use_page2=is_page2_rectangle) + + # 문자속성이 없는 경우 + if not charshape_list: + user_answer = "" + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + else: + require_all_match = ("TableFontName" in category) + any_match = False + all_match = True + matched_user_answer = None # 일치하는 user_answer를 기억 + + for charshape_id in charshape_list: + font_id = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']/FONTID/@Hangul") + if not font_id: + all_match = False + continue + + font_name = root.xpath(f"//FONTFACE[@Lang='Hangul']/FONT[@Id='{font_id[0]}']/@Name") + + if not font_name: + all_match = False + continue + + # 공백 제거 + user_answer = font_name[0].replace(" ", "") + right_answer = right_answer.replace(" ","") + + # 접두어 제거 + if right_answer in ["견고딕", "중고딕"]: + user_answer = user_answer.replace("한양", "") + + if user_answer == right_answer: + any_match = True + matched_user_answer = user_answer + else: + all_match = False + if require_all_match: + break + + if require_all_match: + score = points if all_match else 0 + self.evaluate_answer(scoring, user_answer, right_answer, score) + else: + score = points if any_match else 0 + self.evaluate_answer(scoring, matched_user_answer if any_match else "", right_answer, score) + + # 폰트 속성 + elif (category or "") == "FontAttribute": + # 하이퍼링크 처리 + + # 1. 하이퍼링크를 포함하는 P요소를 가져옴 + # 2. 그 P요소의 자손 CHAR태그에 있는 텍스트를 하나의 문자열로 변환 + # 3. P요소의 문자열과 채점하려는 문자열이 일치하는지 확인 + hyperlink_xpath = criterion.get('hyperlink_ptag', None) + hyperlink_ptag = root.xpath(hyperlink_xpath) if hyperlink_xpath else None + + p_tag_text_list = extract_char_text_from_p(hyperlink_ptag) if hyperlink_ptag else [] + hyperlink_text = search_value.replace(" ", "") if search_value else "" + + # search_value가 hyperlink문자열에 포함되어 있는지 확인 + # search_value가 hyperlink인 경우와 아닌경우를 구분해 채점 + search_in_hyperlink = False + if hyperlink_text and any(hyperlink_text in text for text in p_tag_text_list): + search_in_hyperlink = True + else: + search_in_hyperlink = False + + # hyperlink가 아닌 경우(일반적인 텍스트 일 경우) + # 하이퍼링크를 포함한 P태그가 없거나 search_value값이 하이퍼링크텍스트에 포함되어 있지 않을 경우 + if not hyperlink_ptag or not search_in_hyperlink: + charshape_list = root.xpath(xpath) + if not charshape_list: + charshape = None + user_answer = None + else: + for charshape in charshape_list: + font_attribute = charshape.find(right_answer) + if font_attribute is not None: + user_answer = font_attribute.tag + else: + user_answer = None + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + + if scoring['points'] > 0: + break + + # 하이퍼링크인 경우 + # elif hyperlink_ptag and search_in_hyperlink: + else: + p_elements = hyperlink_ptag + + for p in p_elements: + # 수험자가 입력한 텍스트 중 하이퍼링크가 들어간 문단의 모든 텍스트를 가져와 + # 채점하고자 하는 (정답) 하이퍼링크 텍스트와 시작 위치를 비교 + # (예시) + # [수험자입력] 1. 사전등록 : 서울 국제 도서 박람회 흠페이지(http://www.ind.or.kr) 참조 + # [정답] 서울 국제 도서 박람회 흠페이지(http://www.ind.or.kr) 참조 + # 수험자 텍스트의 "1. 사전등록" 부분을 제외하고 난 뒤 + # 남은 "서울 국제 도서 박람회 흠페이지(http://www.ind.or.kr) 참조"의 정답 부분과 유사도를 비교 + + text_list = p.xpath(".//CHAR/text()") + full_text = ''.join(text_list).replace(" ", "") + # print("full_text: ", full_text) + + # 채점하고자 하는 문자열 (search_value)의 첫 문자 + first_char = search_value[0] + + # 수험자 답안에서 첫 문자 인덱스 위치 + user_answer_first_index = full_text.find(first_char) + + if user_answer_first_index != -1: + # 수험자 답안에서 첫 문자 인덱스 위치부터 search_value 길이만큼 잘라서 비교 + trimmed_full_text = full_text[user_answer_first_index:] + else: + trimmed_full_text = full_text + + # 두 문자열의 유사도 계산 + similarity = difflib.SequenceMatcher(None, trimmed_full_text, hyperlink_text).ratio() + + # 두 문자열의 유사도에 따라 하이퍼링크 확인 + # 유사도가 낮은 경우 오답처리 + if similarity < 0.7: + self.evaluate_answer(scoring, user_answer, right_answer, 0, method="equal") + + # 유사도가 높은 경우 + else: + inside_field = False + charshape_list = [] + + for elem in p.iter(): + # 시작 지점 확인 + # FIELDBEGIN태그와 FIELDEND태그 사이 + if elem.tag == "FIELDBEGIN": + inside_field = True + elif elem.tag == "FIELDEND": + inside_field = False + + # 하이퍼링크 텍스트가 CharShape 속성값이 앞의 텍스트와 다른 경우 + # http://www.ihd.or.kr 주소가 TEXT 부모태그를 가지는 경우 + # [예시] + # + # http://www.ihd.or.kr) + # + # 해당 부모 TEXT태그의 CharShape속성을 확인 + elif inside_field and elem.tag == "TEXT": + charshape = elem.get("CharShape") + print('charshape : ', charshape) + if charshape: + charshape_list.append(charshape) + + # 하이퍼링크 텍스트가 CharShape 속성값이 앞의 텍스트와 같은 경우 + # http://www.ihd.or.kr 주소가 TEXT부모태그 없이 CHAR로만 있는경우 + # [예시] + # http://www.ihd.or.kr) + # FIELDBEGIN밖의 TEXT태그의 CharShape속성을 확인해야 한다 + elif inside_field and elem.tag == "CHAR": + parent = elem.getparent() + + charshape = parent.get("CharShape") + print('charshape : ', charshape) + if charshape: + charshape_list.append(charshape) + + + # 하이퍼링크에 해당하는 P태그 내 존재하는 charshape ID값 모두를 비교해 해당 속성(ITALIC, BOLD, UNDERLINE) 확인 + # 모든 charshape ID값이 정답과 일치하는 경우에만 점수 부여 + all_attributes_match = True + if charshape_list: + for charshape_id in charshape_list: + charshape = root.xpath(f"//CHARSHAPE[@Id='{charshape_id}']") + + # 속성 태그가 존재하는지 확인 + font_attribute = charshape[0].find(right_answer) + if font_attribute is None: + user_answer = None + all_attributes_match = False + break + + else: + user_answer = font_attribute.tag + + if all_attributes_match: + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + else: + self.evaluate_answer(scoring, user_answer, right_answer, 0, method="equal") + + elif (category or "") == "LineSpacing": + page1_ptags = pages.get('Page_1', []) + + # 줄간격이 하나라도 일치하지 않을 경우 오답처리 + linespacing_match = True + for p in page1_ptags: + parashape_id = p.get('ParaShape') + xpath = xpath.replace('{parashape_id}', parashape_id) + linespacing = root.xpath(xpath) + user_answer = linespacing[0] + + # print("🟡줄간격: ", user_answer) + if user_answer != right_answer: + linespacing_match = False + break + + # 문단 첫 글자 크기에 따라 채점 기준 추가 (050624) + # 1. 기본 줄간격 160% 일 때 26pt + # 2. 해당 문제의 정답 줄간격 (180% = 28pt / 200% = 30pt ) + # 두 경우의 글자 크기가 아니라면 오답처리 + firstword = criterion.get('first_word', None) + result = root.xpath(f"//CHARSHAPE[@Id=//RECTANGLE//TEXT[CHAR[text()='{firstword}']]/@CharShape]/@Height") + firstword_size = result[0] if result else None + + if (right_answer == '180' and firstword_size not in ['2600', '2800', None]) or (right_answer == '200' and firstword_size not in ['2600', '3000', None]): + linespacing_match = False + + if linespacing_match is True: + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + else: + self.evaluate_answer(scoring, user_answer, right_answer, 0, method="equal") + + + # 특수문자 갯수 채점 + elif (category or "") == "SpecialChar": + 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) + ch1_str = root.xpath(xpath) + ch2_str = root.xpath(xpath2) + ch3_str = root.xpath(xpath3) + sum_char = 0 + + # char1 요소에서 특수문자 갯수 세기 (최대 2점) + for text in ch1_str or []: + ch1_count = text.count(ch1) + sum_char += ch1_count + if sum_char >= 2: + sum_char = 2 + break + + # char2 요소에서 특수문자 갯수 세기 (최대 1점) + # char1과 char2가 다른 경우 (예: ▶ 행사안내 ◀) + if (ch1 != ch2) and ch2_str: + for text in ch2_str or []: + ch2_count = text.count(ch2) + if ch2_count > 1: + ch2_count = 1 + sum_char += ch2_count + + # char3 요소에서 특수문자 갯수 세기 (최대 1점) + if ch3_str: + for text in ch3_str or []: + ch3_count = text.count(ch3) + if ch3_count > 1: + ch3_count = 1 + sum_char += ch3_count + + user_answer = sum_char + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="partial_score") + + # 쪽 테두리 (이중 실선, 머리말 포함) 설정 + elif (category or "") == "PageBorder": + user_answer = { + "header_inside": False, + "all_double_slim": False + } + + # 머릿말 포함 객체가 하나라도 있으면 정답 + header_inside_elements = root.xpath(xpath) + for header_inside in header_inside_elements: + # print("머릿말포함: ",header_inside) + if "true" in header_inside: + user_answer["header_inside"] = True + break + + # BORDERFILL요소의 자녀 + # LEFTBORDER, RIGHTBORDER, TOPBORDER, BOTTOMBORDER 요소의 Type속성이 + # 모두 DoubleSlim이면 정답 + border_tags = ["LEFTBORDER", "RIGHTBORDER", "TOPBORDER", "BOTTOMBORDER"] + + borderfill_elements = root.xpath(xpath2) + for borderfill in borderfill_elements: + all_double_slim = True + + for tag in border_tags: + element = borderfill.find(tag) + + if (element is None) or (element.get("Type") != "DoubleSlim"): + all_double_slim = False + break + + #모든 BORDER 태그의 Type 속성이 'DoubleSlim'인 객체가 있다면 반복문 탈출 + if all_double_slim: + user_answer["all_double_slim"] = True + break + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + + # 다단 확인 [2-3]문항 + elif (category or "") == "TwoColumn": + page2_ptags = pages.get('Page_2', []) + + for p in page2_ptags: + column_count = p.xpath(xpath) + user_answer = column_count[0] if column_count else '0' + + if user_answer == right_answer: + self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") + + # P태그들 중 하나라도 다단이 존재할 경우 정답처리 + if scoring['points'] > 0: + break + + # 한자 + elif (category or "") == "Hanja": + # 점수 계산 + score = 0 + max_score = points + + word_list = criterion.get('word', []) + # 부분점수 (최대점수에서 한자 갯수만큼 나눈 몫) + score_per_pair = max_score // len(word_list) + + # 한자가 5개 고정일 경우 + # score_per_pair = 2 + + for kor, chn in word_list: + # XPath 구문 구성 및 실행 + exec_xpath = xpath.replace('{kor}', kor).replace('{chn}', chn) + matched = root.xpath(exec_xpath) + + if matched: + score += score_per_pair + + # 최대 점수 초과 방지 + user_answer = min(score, max_score) + + self.evaluate_answer(scoring, user_answer, right_answer, points, method="partial_score") + + elif (category or "") == "ChartType": + chart_type_list = { + '꺾은선형': "//c:lineChart[c:grouping[@val='standard']]", + '묶은가로막대형': "//c:barChart[c:barDir[@val='bar'] and c:grouping[@val='clustered']]", + '누적가로막대형': "//c:barChart[c:barDir[@val='bar'] and c:grouping[@val='stacked']]", + '묶은세로막대형': "//c:barChart[c:barDir[@val='col'] and c:grouping[@val='clustered']]", + '누적세로막대형': "//c:barChart[c:barDir[@val='col'] and c:grouping[@val='stacked']]", + '원형': "//c:pieChart", + '분산형': "//c:scatterChart" + } + chart_type = criterion.get('chart_type').replace(" ","") + + # 입력한 chart_type에 해당하는 xpath를 가져옴 + chart_xpath = chart_type_list[chart_type] + + # xpath를 사용하여 차트 요소가 있는지 확인 + user_answer = bool(chart_tree.xpath(chart_xpath, namespaces=namespaces)) + self.evaluate_answer(scoring, user_answer, right_answer, points) + + finally: + # 문항 채점 결과를 리스트에 입력 + onePersonResult['score_results'].append(scoring) + print(f'scoring: {scoring}') + + onePersonResult['partial_scores'].append({ + 'section': section_id, + 'score': self.partial_score + }) + onePersonResult['total_score'] = self.total_score + return onePersonResult + + except ET.ParseError as e: + return { + 'filename': os.path.basename(xml_file), + 'error': f"XML 파싱 오류: {str(e)}", + 'total_score': 0 + } + + def binary_to_chartxml(self, xml_path): + tree = ET.parse(xml_path) + root = tree.getroot() + + binary_data = root.xpath('//BINDATA[@Id=//BINITEM[@Format="OLE"]/@BinData]/text()') + if not binary_data: + return None + binary_data = binary_data[0].encode('utf-8') + + # 태그와 그 내부 내용을 삭제합니다. + encoded_data = re.sub(b'', b'', binary_data) + encoded_data = encoded_data.replace(b'', b'') + encoded_data = encoded_data.replace(b'\r\n', b'') + + # base64 디코딩을 수행합니다. + decoded_data = base64.b64decode(encoded_data+b'==') + + # 디코딩된 데이터 내용 중 xml 형식만 추출할 때 , 사이의 데이터만 추출. + start = decoded_data.find(b'') + print(end) + xml_data = decoded_data[start:end+len(b'')] + + # xml 데이터가 없는 경우 None을 반환합니다. + if -1 in [start, end]: + return None + + # 디코딩된 데이터를 파일로 저장합니다. + base_filename = os.path.splitext(xml_path)[0] + new_filename = f'{base_filename}.xml' + with open(new_filename, 'wb') as file: + file.write(xml_data) + + return xml_data + + def typo_check(self, correct_answer_file, user_answer_file, chart_xml): + + # 문자열 리스트를 필터링 + def clean_text_list(text_list, ignore_words=None): + result = [] + for text in text_list: + if ignore_words: + text = text.replace(ignore_words, '') + text = text.replace(' ', '') # 공백 제거 + text = re.sub(r'\d+\.\s*|-', '', text) # 숫자. / - 제거 + result.append(text) + return result + + # 1. 텍스트 추출 + # 2. 공백제거, 특정 형식 제거 + # 3. 리스트를 문자열로 변환 + + user_answer_root = ET.parse(user_answer_file).getroot() + correct_answer_root = ET.parse(correct_answer_file).getroot() + + # xpath로 바이너리 부분추출 + user_input_text = user_answer_root.xpath('//CHAR//text()[not(ancestor::HEADER) and not(ancestor::TABLE)]') + correct_input_text = correct_answer_root.xpath('//CHAR//text()[not(ancestor::HEADER) and not(ancestor::TABLE)]') + + # 테이블 구간 추출 + user_table_text = user_answer_root.xpath('//TABLE//CHAR//text()') + correct_table_text = correct_answer_root.xpath('//TABLE//CHAR//text()') + + user_chart_title = "" + correct_chart_title = self.scoring_criteria["2"]["50"]["searchValue"] + + # 차트 XML에서 차트제목 추출 + if chart_xml is not None: + chart_xml_tree = ET.fromstring(chart_xml) + ns = {'c': 'http://schemas.openxmlformats.org/drawingml/2006/chart', + 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'} + xpath_expr = '/c:chartSpace/c:chart/c:title/c:tx/c:rich/a:p/a:r/a:t' + + # 차트 제목 추출 + chart_title = chart_xml_tree.xpath(xpath_expr, namespaces=ns) + + # 차트 제목이 존재하는 경우 + user_chart_title = chart_title[0].text if chart_title else "" + + try : + ignore_word = self.scoring_criteria["2"]["29"]["ignoreWord"] + # 특정 단어 제거 + # 오타와 누락의 경우만 판단하면 정상작동하지만 + # 추가 된 단어의 경우를 채점기준에 추가하면 정확하게 채점 되지 않을 수 있음 + # [정답] Hybrid [실제작성] + user_input_text = [text.replace(ignore_word, '') for text in user_input_text] + correct_input_text = [text.replace(ignore_word, '') for text in correct_input_text] + except (KeyError, IndexError, AttributeError): + ignore_word = None + + # print(f"ignore_word: {ignore_word}") + + # 문자열 필터링 + correct_input_text = clean_text_list(correct_input_text, ignore_word) + user_input_text = clean_text_list(user_input_text, ignore_word) + + correct_table_text = clean_text_list(correct_table_text) + user_table_text = clean_text_list(user_table_text) + + correct_chart_title = clean_text_list(correct_chart_title) + user_chart_title = clean_text_list(user_chart_title) + + # 리스트를 하나의 문자열로 변경 + correct_input_text_str = ''.join(correct_input_text) + user_input_text_str = ''.join(user_input_text) + + correct_table_text_str = ''.join(correct_table_text) + user_table_text_str = ''.join(user_table_text) + + correct_chart_title_str = ''.join(correct_chart_title) + user_chart_title_str = ''.join(user_chart_title) + + print("user_input_text as string:") + print(user_input_text_str) + print("\n") + print("correct_input_text_answer as string:") + print(correct_input_text_str) + + # 문자열의 차이를 비교 + text_diff = difflib.ndiff(correct_input_text_str, user_input_text_str) + table_text_diff = difflib.ndiff(correct_table_text_str, user_table_text_str) + chart_title_diff = difflib.ndiff(correct_chart_title_str, user_chart_title_str) + + # text_diff = difflib.ndiff(correct_input_text, user_input_text) + # table_text_diff = difflib.ndiff(correct_table_text, user_table_text) + # chart_title_diff = difflib.ndiff(correct_chart_title, user_chart_title) + # diff_list = list(diff) + text_list = list(text_diff) + table_list = list(table_text_diff) + chart_list = list(chart_title_diff) + + diff_list = text_list + table_list + chart_list + # diff_list = text_list + table_list + + # 차이점을 정리하여 result_diff에 저장 + result_diff = [] + + # 누락 된 단어만 따로 리스트로 저장 + missing_list = [] + + # 오타와 누락된 단어 리스트 저장 + error_missing_list = [] + + skip_next = False + + for i, line in enumerate(diff_list): + if skip_next: + skip_next = False + continue + # diff_list의 line 시작이 '-'이면서 다음 line이 '+'이면 두 line을 붙여서 맞춤법이 틀린 단어로 판단 + if line.startswith('- '): + # 오타 + if i + 1 < len(diff_list) and diff_list[i + 1].startswith('+ '): + line = line.replace('- ', '-') + next = diff_list[i + 1].replace('+ ', '') + result_diff.append(line+'=>'+next) + error_missing_list.append(line+'=>'+next) + skip_next = True + # 누락 + else: + line = line.replace('- ', '-') + result_diff.append(line) + missing_list.append(line) + error_missing_list.append(line) + # 없어도 되는 글자가 있는 경우 (추가) + elif line.startswith('+ '): + line = line.replace('+ ', '+') + result_diff.append(line) + + # result_diff 출력 + # print("\nResult Differences:") + # for diff in result_diff: + # print(diff) + + # result_diff 배열의 길이를 맨 앞에 저장 + + # 모든 차이를 계산해 점수 차감 + # temp = 40 - min(len(result_diff)*2, 40) + + # 누락된 텍스트만 계산해 점수 차감 + # temp = 40 - min(len(missing_list)*2, 40) + + # 2503회 기준 오타 1개당 [2점]->[1점] 차감 + temp = 40 - min(len(error_missing_list)*1, 40) + + self.set_typo_score(temp) + + result_diff.insert(0, temp) + return result_diff + + # XML 파일 채점 + def score_directory(self, xml_directory, correct_answer_file): + # xml 파일 불러오기 + xml_files = Path(xml_directory).glob('*.hml') + + # 채점결과 저장할 리스트 + score_results = [] + + for user_answer_file in xml_files: + score_result = {} + chart_xml = self.binary_to_chartxml(user_answer_file) + score_result['typo'] = self.typo_check(correct_answer_file, user_answer_file, chart_xml) + score_result['score'] = self._score_xml_file(user_answer_file, chart_xml) + # score_result['score']['score_results'][2]['points'] = score_result['typo'][0] + score_results.append(score_result) + return score_results + + def parse_filename(self, filename): + if isinstance(filename, dict): + filename = filename.get('파일명', '') + match = re.match(r'.*-(\d+)-(.+)\.hml', filename) + if match: + number = match.group(1) + name = match.group(2) + return number, name + + return None, None + + def export_to_excel(self, results, output_path=None): + if output_path is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") #연월일_시분초 + # timestamp = datetime.now().strftime("%Y%m%d") #연월일 + output_path = f"scoring_results_{timestamp}.xlsx" + + summary_data = [] + detail_data = [] + typo_data = [] + + for temp in results: + # 요약 정보 + result = temp['score'] + summary_row = { + '파일명': result['filename'], + '총점': result.get('total_score', 0) + } + if 'error' in result: + summary_row['오류'] = result['error'] + + summary_data.append(summary_row) + + # 상세 정보 + if 'score_results' in result: + filename = {'파일명': result['filename']} + number, name = self.parse_filename(filename) + if (number or name) is None: + detail_row = {'채점항목': result['filename'] } + else: + detail_row = {'채점항목':f"{number}-{name}"} + + section_num = None + row_index = [] + for i, score_result in enumerate(result['score_results']): + current_section = score_result['section'] + + if section_num is None: + section_num = current_section + + # 다음 섹션(문제0 => 문제1)로 넘어갔을 경우 or 마지막 문제일 경우 + if current_section != section_num: + # 이전 섹션의 부분합을 출력 + detail_row[f'문제{section_num}'] = result['partial_scores'][int(section_num)]['score'] + row_index.append(f'문제{section_num}') + section_num = current_section + + detail_row[f'{i+1}'] = score_result['points'] + row_index.append(score_result['id']) + + # 마지막 섹션(문제2)부분합 점수를 출력 + if i == len(result['score_results']) - 1: + detail_row[f'문제{current_section}'] = result['partial_scores'][int(current_section)]['score'] + row_index.append(f'문제{current_section}') + + detail_row['총점'] = result.get('total_score', 0) + row_index.append('총점') + detail_data.append(detail_row) + + summary_df = pd.DataFrame(summary_data) + detail_df = pd.DataFrame(detail_data).transpose() + detail_df.columns = detail_df.iloc[0] + detail_df = detail_df[1:] + + detail_df.index = row_index + # detail_df = pd.DataFrame(detail_data) + + for one_result in results: + total_typo_err_score = one_result['typo'][0] + typo_err_list = one_result['typo'][1:] + + typo_row = { + '파일명': one_result['score']['filename'], + '오타점수': total_typo_err_score, + } + typo_row.update({f'오타{i+1}': typo_err for i, typo_err in enumerate(typo_err_list)}) + + typo_data.append(typo_row) + + typo_df = pd.DataFrame(typo_data) + typo_df = typo_df.transpose() + # transpose 후 행 -> 열 변환했을 때의 인덱스 제거 (기본 인덱스 제거) + typo_df.reset_index(drop=True, inplace=True) + + # transpose 했으므로 첫 행을 컬럼명으로 지정 + typo_df.columns = typo_df.iloc[0] # 첫 행을 컬럼명으로 지정 + typo_df = typo_df.drop(typo_df.index[0]) # 첫 행 제거 + + + # ExcelWriter 객체 생성 + with pd.ExcelWriter(output_path, engine='openpyxl') as writer: + detail_df.to_excel(writer, sheet_name='채점상세내역', index=True) + typo_df.to_excel(writer, sheet_name='오타내역', index=False) + summary_df.to_excel(writer, sheet_name='채점결과요약', index=False) + + # 열 너비 자동 조정 + # for sheet_name in writer.sheets: + # worksheet = writer.sheets[sheet_name] + # for column_cells in worksheet.columns: + # max_length = 0 + # column = column_cells[0].column_letter # 열의 문자 + # for cell in column_cells: + # try: + # if cell.value: + # max_length = max(max_length, len(str(cell.value))) + # except: + # pass + # adjusted_width = (max_length + 2) + # worksheet.column_dimensions[column].width = adjusted_width + + return output_path + +def main(): + + # 시험회차 및 유형 + # exam_round = '2507' + exam_round = '2508' + + # 채점하고자 하는 유형은 주석 해제 + exam_types = [ + # 'A', + # 'B', + 'C', + # 'D', + ] + + # test_mode = False + test_mode = True #/TEST 폴더 채점시 + + output_excel_paths = [] + for exam_type in exam_types: + # JSON 채점기준표 파일 (예시:DIW_2503A.json) + scoring_criteria_path = f'./DIW_{exam_round}{exam_type}.json' + + # xml(hml)파일 디렉토리 경로 (예시:./output/2503/A/DIW) + xml_directory = f'./output/{exam_round}/{exam_type}/{"TEST" if test_mode else "DIW"}' + # 오탈자 체크를 위한 정답 파일 경로 (예시:./output/A/DIW/DIW_2503A.hml) + # correct_answer_file = f'./output/{exam_type}/DIW/DIW_{exam_round}{exam_type}.hml' + correct_answer_file = f'./output/{exam_round}/{exam_type}/DIW/DIW_{exam_round}{exam_type}.hml' + + # 엑셀 파일명 (비어있으면 자동생성) (예시:241001_DIW_2503A_채점결과.xlsx) + timestamp = datetime.now().strftime("%y%m%d") + output_path = f'{timestamp}_DIW_{exam_round}{exam_type}_{"TEST" if test_mode else "채점결과"}.xlsx' + + # 채점 클래스 초기화 + scorer = XMLScorer(scoring_criteria_path) + + # 폴더 내 모든 xml 파일 채점 + results = scorer.score_directory(xml_directory, correct_answer_file) + if not results: + print(f"❌ 채점 결과가 없습니다. {xml_directory} 폴더에 답안파일이 존재하는지 확인하세요.") + continue + # 채점 결과 엑셀로 저장 + output_excel_paths.append(scorer.export_to_excel(results, output_path)) + + if output_excel_paths: + print(f"채점 결과 엑셀 파일: {output_excel_paths}") + +if __name__ == '__main__': + main() diff --git a/DIW_2508A.json b/회차별채점자료/2508/JSON/DIW_2508A.json similarity index 100% rename from DIW_2508A.json rename to 회차별채점자료/2508/JSON/DIW_2508A.json diff --git a/DIW_2508B.json b/회차별채점자료/2508/JSON/DIW_2508B.json similarity index 100% rename from DIW_2508B.json rename to 회차별채점자료/2508/JSON/DIW_2508B.json diff --git a/DIW_2508C.json b/회차별채점자료/2508/JSON/DIW_2508C.json similarity index 100% rename from DIW_2508C.json rename to 회차별채점자료/2508/JSON/DIW_2508C.json diff --git a/DIW_2508D.json b/회차별채점자료/2508/JSON/DIW_2508D.json similarity index 100% rename from DIW_2508D.json rename to 회차별채점자료/2508/JSON/DIW_2508D.json diff --git a/DIW_2508_2A.json b/회차별채점자료/2522/A/DIW_2522A (2).json similarity index 100% rename from DIW_2508_2A.json rename to 회차별채점자료/2522/A/DIW_2522A (2).json diff --git a/회차별채점자료/2522/A/DIW_2522A.hwpx b/회차별채점자료/2522/A/DIW_2522A.hwpx new file mode 100644 index 0000000..95f1183 Binary files /dev/null and b/회차별채점자료/2522/A/DIW_2522A.hwpx differ diff --git a/회차별채점자료/2522/A/DIW_2522A.json b/회차별채점자료/2522/A/DIW_2522A.json new file mode 100644 index 0000000..d8f0f4d --- /dev/null +++ b/회차별채점자료/2522/A/DIW_2522A.json @@ -0,0 +1,840 @@ +{ + "0": { + "0": { + "path": "", + "path2": "", + "points": 0, + "category": "파일저장", + "item": "파일명 (수검번호.hwp/hwpx)" + }, + "1": { + "path": "//PAGEMARGIN", + "value": { + "Top": 20, + "Bottom": 20, + "Left": 20, + "Right": 20, + "Header": 10, + "Footer": 10, + "Gutter": 0 + }, + "tolerance": 1, + "points": 4, + "category": "PageSetting", + "item": "A4용지, 왼쪽/오른쪽/위쪽/아래쪽 (각20mm), 머리말/꼬리말 (10mm), 제본(0mm)" + }, + "2": { + "path": "", + "value": { + "FontName": "바탕", + "FontSize": "1000", + "Alignment": "Justify", + "LineSpacing": "160" + }, + "points": 4, + "category": "BasicSetting", + "item": "글꼴 (바탕, 10pt), 양쪽정렬, 줄간격 (160%)" + }, + "3": { + "path": "", + "value": null, + "points": 40, + "category": "오타감점", + "item": "오타 1개 -1점 / 2503회부터 오타 1개 -1점으로 변경" + } + }, + "1": { + "1": { + "path": "//TEXTART[@Text='{searchValue}']/TEXTARTSHAPE/@FontName", + "searchValue": "노인일자리참여자모집", + "value": "휴먼옛체", + "points": 1, + "category": "OneAnswer", + "item": "문구 (노인일자리참여자모집)/① 글씨체 (휴먼옛체)" + }, + "2": { + "path": "//TEXTART[@Text='{searchValue}']/descendant::WINDOWBRUSH/@FaceColor", + "searchValue": "노인일자리참여자모집", + "value": "199,58,82", + "points": 2, + "category": "Color", + "item": "문구 (노인일자리참여자모집)/② 채우기 : 색상(RGB:199,58,82)" + }, + "3": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Width", + "searchValue": "노인일자리참여자모집", + "value": "120", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "문구 (노인일자리참여자모집)/③ 크기-너비 (120 mm)" + }, + "4": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Height", + "searchValue": "노인일자리참여자모집", + "value": "20", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "문구 (노인일자리참여자모집)/④ 크기-높이 (20 mm)" + }, + "5": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/POSITION/@TreatAsChar", + "searchValue": "노인일자리참여자모집", + "value": "true", + "points": 2, + "category": "OneAnswer", + "item": "문구 (노인일자리참여자모집)/⑤ 위치 (글자처럼 취급)" + }, + "6": { + "path": "//PARASHAPE[@Id=//P[.//TEXTART[@Text='{searchValue}']]/@ParaShape]/@Align", + "searchValue": "노인일자리참여자모집", + "value": "Center", + "points": 2, + "category": "OneAnswer", + "item": "문구 (노인일자리참여자모집)/⑥ 정렬 (가운데 정렬)" + }, + "7": { + "path": "//TEXTART[@Text='{searchValue}']", + "searchValue": "노인일자리참여자모집", + "value": true, + "points": 2, + "category": "Boolean", + "item": "문구 (노인일자리참여자모집)/⑦ 글맵시모양 (육안확인)" + }, + "8": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/SIZE", + "searchValue": "어", + "value": { + "Height": 2800, + "Width": 2800 + }, + "tolerance": 200, + "points": 1, + "category": "TwoLineSize", + "item": "어/① 모양 (2줄)" + }, + "9": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "어", + "value": "중고딕", + "points": 1, + "category": "FontName", + "item": "어/② 글씨체 (중고딕)" + }, + "10": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor", + "searchValue": "어", + "value": "137,221,202", + "points": 2, + "category": "Color", + "item": "어/③ 면색 : 색상(RGB:137,221,202)" + }, + "11": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//OUTSIDEMARGIN/@Right", + "searchValue": "어", + "value": "3.0", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "어/④ 본문과의 간격 : 3.0mm" + }, + "12": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", + "searchValue": "어르신들에게 다양한 일자리와 봉사활동 기회", + "value": "BOLD", + "points": 2, + "category": "FontAttribute", + "item": "문구 (어르신들에게 다양한 일자리와 봉사활동 기회)/① BOLD" + }, + "13": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", + "searchValue": "어르신들에게 다양한 일자리와 봉사활동 기회", + "value": "UNDERLINE", + "points": 2, + "category": "FontAttribute", + "item": "문구 (어르신들에게 다양한 일자리와 봉사활동 기회)/② UNDERLINE" + }, + "14": { + "path": "//CHAR[contains(string(.),'{char1}')]/text()", + "path2": "//CHAR[contains(string(.),'{char2}')]/text()", + "path3": "//CHAR[contains(string(.),'{char3}')]/text()", + "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(),'{match_str}')]/ancestor::P/@ParaShape]/@Align", + "match_str": "모집안내", + "value": "Center", + "points": 1, + "category": "Align", + "item": "문구 (▶ 모집안내 ◀)/② 정렬 (가운데 정렬)" + }, + "17": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "hyperlink_ptag": "//P[.//FIELDBEGIN[@Type='Hyperlink']]", + "searchValue": "공익활동형, 사회서비스형, 공동체사업단", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "문구 (공익활동형, 사회서비스형, 공동체사업단)/① BOLD" + }, + "18": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "hyperlink_ptag": "//P[.//FIELDBEGIN[@Type='Hyperlink']]", + "searchValue": "공익활동형, 사회서비스형, 공동체사업단", + "value": "ITALIC", + "points": 1, + "category": "FontAttribute", + "item": "문구 (공익활동형, 사회서비스형, 공동체사업단)/② ITALIC" + }, + "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": "문구 (※ 기타… 이하 문단)/왼쪽여백 (15), 내어쓰기 (12)", + "desc": "내부적으로 내어쓰기는 음수값 / JSON value값은 양수로 입력" + }, + "20": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]/@Height", + "searchValue": "2025. 08. 23", + "value": "1300", + "points": 1, + "category": "OneAnswer", + "item": "문구 (2025. 08. 23)/① 크기 (1300)", + "desc": "1pt당 100" + }, + "21": { + "path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align", + "searchValue": "2025. 08. 23", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (2025. 08. 23)/② 정렬 (가운데 정렬)" + }, + "22": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "시니어클럽", + "value": "궁서", + "points": 1, + "category": "FontName", + "item": "문구 (시니어클럽)/① 글씨체 (궁서)" + }, + "23": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "시니어클럽", + "value": "2400", + "points": 1, + "category": "OneAnswer", + "item": "문구 (시니어클럽)/② 크기 (2400)" + }, + "24": { + "path": "//PARASHAPE[@Id=//CHAR[text()='{searchValue}']/ancestor::P/@ParaShape]/@Align", + "searchValue": "시니어클럽", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (시니어클럽)/③ 정렬 (가운데 정렬)" + }, + "25": { + "path": "//SECTION[1]//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "DIAT", + "value": "굴림", + "points": 1, + "category": "FontName", + "item": "문구 (DIAT)/① 글꼴 (굴림)" + }, + "26": { + "path": "//CHARSHAPE[@Id=//SECTION[1]//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "DIAT", + "value": "900", + "points": 1, + "category": "OneAnswer", + "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": "OneAnswer", + "item": "문구 (DIAT)/③ 정렬 (오른쪽 정렬)" + }, + "28": { + "path": "//PAGENUM/@FormatType", + "value": "HangulSyllable", + "points": 2, + "category": "PageNumber", + "item": "① 쪽 번호 매기기 (가,나,다 순으로)", + "desc1": { + "가,나,다": "HangulSyllable", + "1,2,3": "Digit", + "갑,을,병": "DecagonCircle", + "A,B,C": "LatinCapital", + "a,b,c": "LatinSmall", + "①,②,③": "CircledDigit", + "一,二,三": "Ideograph", + "㉠,㉡,㉢": "CircledHangulJamo", + "ⓐ,ⓑ,ⓒ": "CircledLatinSmall", + "i,ii,iii": "RomanSmall", + "I,II,III": "RomanCapital", + "desc": "정답에 맞는 값 value에 입력" + }, + "desc2": "1, 2페이지 모두 정답이어야 점수 부여" + }, + "29": { + "path": "//PAGENUM/@Pos", + "value": "BottomCenter", + "points": 2, + "category": "PageNumber", + "item": "가운데 아래", + "desc": "1, 2페이지 모두 정답이어야 점수 부여", + "desc2": { + "가운데 아래": "BottomCenter", + "오른쪽 아래": "BottomRight" + } + }, + "30": { + "path": "//PARASHAPE[@Id='{parashape_id}']/PARAMARGIN/@LineSpacing", + "value": "180", + "first_word": "어", + "points": 2, + "category": "LineSpacing", + "item": "문제 1 줄간격 180% 설정", + "desc": "1페이지 문단의 줄간격이 정답이 아닌 문단이 있으면 False(감점), first_word 속성에 [문단 첫글자 장식]에 해당하는 글자를 입력해준다." + } + }, + "2": { + "1": { + "path": "//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@HeaderInside", + "path2": "//BORDERFILL[@Id=//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@BorferFill]", + "value": { + "header_inside": true, + "all_double_slim": true + }, + "points": 4, + "category": "PageBorder", + "item": "문제2 쪽테두리(이중 실선, 머리말 포함) 설정" + }, + "2": { + "path": "count(//SECTION)>1", + "value": true, + "points": 3, + "category": "Boolean", + "item": "① 구역나누기", + "desc": "섹션이 1개 이상이면 점수부여" + }, + "3": { + "path": "TEXT/COLDEF/@Count", + "value": "2", + "points": 3, + "category": "TwoColumn", + "item": "② 다단 2단" + }, + "4": { + "path": "//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Width", + "searchValue": "노인일자리", + "value": "60", + "points": 2, + "category": "mmSize", + "item": "문구 (노인일자리)/① 크기-너비 (60 mm)" + }, + "5": { + "path": "//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Height", + "searchValue": "노인일자리", + "value": "12", + "points": 2, + "category": "mmSize", + "item": "문구 (노인일자리)/② 크기-높이 (12 mm)" + }, + "6": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//LINESHAPE", + "searchValue": "노인일자리", + "value": { + "Style": "DoubleSlim", + "Width": "283" + }, + "points": 2, + "category": "LineShape", + "item": "문구 (노인일자리)/③ 테두리 : 이중 실선(1.00mm)", + "desc": "1mm = 283pt value['Width']에 pt값 입력" + }, + "7": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/@Ratio", + "searchValue": "노인일자리", + "value": "50", + "points": 2, + "category": "OneAnswer", + "item": "문구 (노인일자리)/④ 글상자 모서리 (반원)", + "desc": "모서리 비율 반원:50 / 둥근모양:20" + }, + "8": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor", + "searchValue": "노인일자리", + "value": "211,251,193", + "points": 2, + "category": "Color", + "item": "문구 (노인일자리)/⑤ 채우기 : 색상(RGB:211,251,193)" + }, + "9": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/POSITION/@TreatAsChar", + "searchValue": "노인일자리", + "value": "true", + "points": 1, + "category": "OneAnswer", + "item": "문구 (노인일자리)/⑥ 글상자 위치 (글자처럼 취급)" + }, + "10": { + "path": "//PARASHAPE[@Id=//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::P[last()]/@ParaShape]/@Align", + "searchValue": "노인일자리", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (노인일자리)/⑦ 글상자 정렬 (가운데 정렬)" + }, + "11": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "노인일자리", + "value": "맑은고딕", + "points": 1, + "category": "FontName", + "item": "문구 (노인일자리)/⑧ 글씨체 (맑은고딕)" + }, + "12": { + "path": "//CHARSHAPE[@Id=//RECTANGLE//TEXT[./CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "노인일자리", + "value": "2200", + "points": 1, + "category": "OneAnswer", + "item": "문구 (노인일자리)/⑨ 글씨크기 (2200)", + "desc": "1pt당 100" + }, + "13": { + "path": "//PARASHAPE[@Id=//RECTANGLE//P[.//CHAR[text()='{searchValue}']]/@ParaShape]/@Align", + "searchValue": "노인일자리", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (노인일자리)/⑩ 정렬 (가운데 정렬)" + }, + "14": { + "path": "//BINITEM[@BinData=//PICTURE/IMAGE/@BinItem][@Format='JPG' or @Format='JPEG']", + "value": true, + "points": 2, + "category": "Boolean", + "item": "① 파일명 \"그림A.jpg\" 삽입", + "desc": "첨부 이미지 파일명 손상으로 정상적인 채점이 불가한 경우가 발견되어서 이미지 첨부 여부로 채점 방식 변경 (7/3)" + }, + "15": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/SIZE/@Width", + "value": "85", + "points": 2, + "category": "mmSize", + "item": "② 크기-너비 (85 mm)" + }, + "16": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/SIZE/@Height", + "value": "40", + "points": 2, + "category": "mmSize", + "item": "③ 크기-높이 (40 mm)" + }, + "17": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/POSITION[not(@TreatAsChar='true') and @HorzRelTo='Page']/@HorzOffset", + "value": "0", + "points": 2, + "category": "mmSize", + "item": "④ 위치 (어울림 : 가로-쪽의 왼쪽 0.0mm)" + }, + "18": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/POSITION[not(@TreatAsChar='true') and @HorzRelTo='Page']/@VertOffset", + "value": "22", + "points": 2, + "category": "mmSize", + "item": "⑤ 위치 (어울림 : 세로-쪽의 위 22 mm)" + }, + "19": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "1. 필요성", + "value": "돋움", + "points": 1, + "category": "FontName", + "item": "문구① (1. 필요성)/① 글씨체 (돋움)" + }, + "20": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "1. 필요성", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "문구① (1. 필요성)/② 크기 (1200)" + }, + "21": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "searchValue": "1. 필요성", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "문구① (1. 필요성)/③ 진하게" + }, + "22": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "2.주요 노인일자리사업", + "value": "돋움", + "points": 1, + "category": "FontName", + "item": "문구② (2.주요 노인일자리사업)/① 글씨체 (돋움)" + }, + "23": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "2.주요 노인일자리사업", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "문구② (2.주요 노인일자리사업)/② 크기 (1200)" + }, + "24": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "searchValue": "2.주요 노인일자리사업", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "문구② (2.주요 노인일자리사업)/③ 진하게" + }, + "25": { + "path": "boolean(//TEXT[CHAR[contains(text(),'{option}')]]/FOOTNOTE)", + "path2": "boolean(//CHAR[substring(., string-length(.) - string-length('{option}') + 1) = '{option}']/following-sibling::FOOTNOTE/descendant::CHAR)", + "option": "세계보건기구", + "value": true, + "points": 2, + "category": "Boolean", + "item": "문구 (세계보건기구)/① 각주 설정 및 문구 입력" + }, + "26": { + "path": "//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape", + "searchValue": "국제 공중보건을 책임지는 유엔 전문 기구", + "value": "궁서", + "points": 1, + "category": "FontName", + "item": "문구 (세계보건기구)/② 글씨체 (궁서)" + }, + "27": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[contains(text(),'{searchValue}')]]/@CharShape]/@Height", + "searchValue": "국제 공중보건을 책임지는 유엔 전문 기구", + "value": "900", + "points": 1, + "category": "OneAnswer", + "item": "문구 (세계보건기구)/③ 크기 (9pt)" + }, + "28": { + "path": "//P[TEXT[CHAR[contains(text(), '{searchValue}')]]]//AUTONUMFORMAT/@Type", + "searchValue": "국제 공중보건을 책임지는 유엔 전문 기구", + "value": "CircledDigit", + "points": 2, + "category": "OneAnswer", + "item": "문구 (전당)/④ 각주 번호모양", + "desc": { + "가,나,다": "HangulSyllable", + "1,2,3": "Digit", + "갑,을,병": "DecagonCircle", + "A,B,C": "LatinCapital", + "a,b,c": "LatinSmall", + "①,②,③": "CircledDigit", + "一,二,三": "Ideograph", + "㉠,㉡,㉢": "CircledHangulJamo", + "ⓐ,ⓑ,ⓒ": "CircledLatinSmall", + "i,ii,iii": "RomanSmall", + "I,II,III": "RomanCapital", + "甲,乙,丙": "DecagonCircleHanja", + "+,++,+++": "UserChar", + "정답에 맞는 값 value에 입력": "" + } + }, + "29": { + "path": "boolean(//CHAR[contains(text(),'Improvement')])", + "ignoreWord": "Improvement", + "value": true, + "points": 3, + "category": "Boolean", + "item": "Improvement/영단어 미입력, 대소문자/오타 시 전체 감점", + "desc": "유사도 검사를 진행하지 않고 영단어가 모두 일치해야 하므로 xpath구문 내 단어도 수정필요" + }, + "30": { + "path": "//CHAR[contains(text(),'{kor}')][contains(text(),'{chn}')]", + "word": [ + ["영위", "營爲"], + ["소득", "所得"], + ["필수적", "必須的"], + ["창출", "創出"], + ["증진", "增進"] + ], + "value": 10, + "points": 10, + "category": "Hanja", + "item": "① 영위(營爲), ② 소득(所得), ③ 필수적(必須的), ④ 창출(創出), ⑤ 증진(增進)" + }, + "31": { + "path": "boolean(//CHAR[contains(translate(text(), ' ', ''),'계를유지')])", + "value": true, + "points": 3, + "category": "Boolean", + "item": "문구 (…사회적 고립을 예방하고 대인관계는 유지하며…)>'는' → '를' 글자바꿈" + }, + "32": { + "path": "boolean(//CHAR[contains(translate(text(), ' ', ''),'존감회복')])", + "value": true, + "points": 3, + "category": "Boolean", + "item": "문구 (…어르신들의 회복과 자존감 행복한 노후생활을…)>'회복과 / 자존감' 순서바꿈" + }, + "33": { + "path": "//TEXT[CHAR[contains(text(),'{searchValue}')]]/@CharShape", + "searchValue": "노인일자리 창출 현황(예산:십억원)", + "value": "굴림체", + "points": 1, + "category": "FontName", + "item": "제목 문구 (노인일자리 창출 현황(예산:십억원))/① 글씨체 (굴림체)" + }, + "34": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "노인일자리 창출 현황(예산:십억원)", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (노인일자리 창출 현황(예산:십억원))/② 크기 (1200)" + }, + "35": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "searchValue": "노인일자리 창출 현황(예산:십억원)", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "제목 문구 (노인일자리 창출 현황(예산:십억원))/③ 진하게" + }, + "36": { + "path": "//PARASHAPE[@Id=//P[.//CHAR[text()='{searchValue}']]/@ParaShape]/@Align", + "searchValue": "노인일자리 창출 현황(예산:십억원)", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (노인일자리 창출 현황(예산:십억원))/④ 정렬 (가운데 정렬)" + }, + "37": { + "path": "//BORDERFILL[@Id=//TABLE/ROW[1]/CELL/@BorderFill]/FILLBRUSH/WINDOWBRUSH/@FaceColor", + "path2": "//BORDERFILL[@Id=//CELLZONE[@StartRowAddr='0' and @EndRowAddr='0' and @StartColAddr='0' and @EndColAddr=(ancestor::TABLE[1]/@ColCount)-1]/@BorderFill]/FILLBRUSH/WINDOWBRUSH/@FaceColor", + "value": "233,215,77", + "points": 2, + "category": "Color", + "item": "위쪽 제목 셀/① 색상(RGB:233,215,77)" + }, + "38": { + "path": "//CHARSHAPE[@Id=//TABLE/ROW[1]/descendant::TEXT/@CharShape]", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "위쪽 제목 셀/② 진하게", + "desc": "글자 속성이라 CELLZONE으로 적용 되지 않음" + }, + "39": { + "path": "//BORDERFILL[@Id=//TABLE/ROW[1]/CELL/@BorderFill]/BOTTOMBORDER/@Type", + "path2": "//BORDERFILL[@Id=//CELLZONE[@StartRowAddr='0' and @EndRowAddr='0' and @StartColAddr='0' and @EndColAddr=(ancestor::TABLE[1]/@ColCount)-1]/@BorderFill]/BOTTOMBORDER/@Type", + "value": "DoubleSlim", + "points": 2, + "category": "TableAnswer", + "item": "제목 셀 아래선/① 이중실선" + }, + "40": { + "path": "//BORDERFILL[@Id=//TABLE/ROW[1]/CELL/@BorderFill]/BOTTOMBORDER/@Width", + "path2": "//BORDERFILL[@Id=//CELLZONE[@StartRowAddr='0' and @EndRowAddr='0' and @StartColAddr='0' and @EndColAddr=(ancestor::TABLE[1]/@ColCount)-1]/@BorderFill]/BOTTOMBORDER/@Width", + "value": "0.5mm", + "points": 2, + "category": "TableAnswer", + "item": "제목 셀 아래선/② 0.5mm" + }, + "41": { + "path": "//TABLE//TEXT/@CharShape", + "path2": "//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE[@Id=//TABLE/ROW/descendant::TEXT/@CharShape]/FONTID/@Hangul]/@Name", + "value": "맑은 고딕", + "points": 1, + "category": "TableFontName", + "category_tmp": "FontName", + "item": "글자모양/① 글씨체 (맑은 고딕)", + "desc": "테이블 폰트명 문항은 테이블의 모든 셀이 정답폰트와 일치해야 함, 하나만 일치해도 정답으로 채점할 경우 category값을 FontName으로 변경" + }, + "42": { + "path": "//CHARSHAPE[@Id=//TABLE//TEXT/@CharShape]/@Height", + "value": "1000", + "points": 1, + "category": "TableAnswer", + "item": "글자모양/② 크기 (1000)" + }, + "43": { + "path": "//PARASHAPE[@Id=//TABLE/ROW//P/@ParaShape]/@Align", + "value": "Center", + "points": 1, + "category": "TableAnswer", + "item": "글자모양/③ 정렬 (가운데 정렬)" + }, + "44": { + "path": "boolean(//TABLE[1]/ROW[last()]/CELL[position()=last()]//FIELDBEGIN[starts-with(@Command, '={option}')]) and boolean(//TABLE[1]/ROW[last()]/CELL[position()=last()-1]//FIELDBEGIN[starts-with(@Command, '={option}')])", + "option": "SUM", + "value": true, + "points": 4, + "category": "Boolean", + "item": "블록 계산식/합계", + "desc": "option값에 합계는 SUM / 평균은 AVG" + }, + "45": { + "chart_xpath": "", + "chart_type": "묶은 가로 막대형", + "value": true, + "points": 2, + "category": "ChartType", + "item": "① 종류 (묶은 가로 막대형)", + "desc": "chart_type을 입력받아 차트타입에 맞는 xml요소가 있는지 내부적으로 검사, chart_type만 한글로 입력해주면 된다. (공백무시)" + }, + "46": { + "chart_xpath": "//c:valAx/c:majorTickMark/@val", + "value": "out", + "points": 2, + "category": "ChartOneAnswer", + "item": "② 값 축 주 눈금선", + "desc": "chart xml파일에서 답안을 가져오는 문항은 path키값 대신 chart_xpath키값을 이용해 xapth구문을 작성한다" + }, + "47": { + "path": "//OLE[@BinItem=//BINITEM[@Format='OLE']/@BinData]//SIZE/@Width", + "value": "80", + "points": 2, + "category": "mmSize", + "item": "③ 크기-너비 (80 mm)" + }, + "48": { + "path": "//OLE[@BinItem=//BINITEM[@Format='OLE']/@BinData]//SIZE/@Height", + "value": "90", + "points": 2, + "category": "mmSize", + "item": "④ 크기-높이 (90 mm)" + }, + "49": { + "chart_xpath": "boolean(//c:chart and not(//c:pt[not(ancestor::c:tx)]/c:v[text()='합계' or text()='평균']))", + "value": true, + "points": 2, + "category": "Boolean", + "item": "⑤ 차트 데이터(표에서 블록계산식을 제외한 나머지 값만 이용)", + "desc": "차트가 존재하고 블록계산식(합계, 평균) 데이터가 없는 경우 정답 처리" + }, + "50": { + "chart_xpath": "//a:t[text()='{searchValue}']/ancestor::a:r//a:ea/@typeface", + "searchValue": "노인 일자리 창출", + "value": "궁서체", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (노인 일자리 창출)/① 글씨체 (궁서체)" + }, + "51": { + "chart_xpath": "//a:t[text()='{searchValue}']/ancestor::a:r/a:rPr/@sz", + "searchValue": "노인 일자리 창출", + "value": "1300", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (노인 일자리 창출)/② 크기 (1300)" + }, + "52": { + "chart_xpath": "//a:t[text()='{searchValue}']/ancestor::a:r/a:rPr/@{option}", + "option": "b", + "searchValue": "노인 일자리 창출", + "value": "1", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (노인 일자리 창출)/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + }, + "53": { + "chart_xpath": "//c:catAx/c:txPr//a:ea/@typeface", + "value": "굴림", + "points": 1, + "category": "ChartOneAnswer", + "item": "X축/① 글꼴 (굴림)" + }, + "54": { + "chart_xpath": "//c:catAx/c:txPr//a:defRPr/@sz", + "value": "900", + "points": 1, + "category": "ChartOneAnswer", + "item": "X축/② 크기 (9pt)" + }, + "55": { + "chart_xpath": "//c:catAx/c:txPr//a:defRPr/@{option}", + "option": "i", + "value": "1", + "points": 1, + "category": "ChartOneAnswer", + "item": "X축/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + }, + "56": { + "chart_xpath": "//c:valAx/c:txPr//a:ea/@typeface", + "value": "굴림", + "points": 1, + "category": "ChartOneAnswer", + "item": "Y축/① 글꼴 (굴림)" + }, + "57": { + "chart_xpath": "//c:valAx/c:txPr//a:defRPr/@sz", + "value": "900", + "points": 1, + "category": "ChartOneAnswer", + "item": "Y축/② 크기 (9pt)" + }, + "58": { + "chart_xpath": "//c:valAx/c:txPr//a:defRPr/@{option}", + "option": "i", + "value": "1", + "points": 1, + "category": "ChartOneAnswer", + "item": "Y축/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + }, + "59": { + "chart_xpath": "//c:legend//a:ea/@typeface", + "value": "굴림", + "points": 1, + "category": "OneAnswer", + "item": "범례/① 글꼴 (굴림)" + }, + "60": { + "chart_xpath": "//c:legend//a:defRPr/@sz", + "value": "900", + "points": 1, + "category": "OneAnswer", + "item": "범례/② 크기 (9pt)" + }, + "61": { + "chart_xpath": "//c:legend//a:defRPr/@{option}", + "option": "i", + "value": "1", + "points": 1, + "category": "OneAnswer", + "item": "범례/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + } + } +} diff --git a/회차별채점자료/2522/A/DIW_2522A.xlsx b/회차별채점자료/2522/A/DIW_2522A.xlsx new file mode 100644 index 0000000..784eb28 Binary files /dev/null and b/회차별채점자료/2522/A/DIW_2522A.xlsx differ diff --git a/회차별채점자료/2522/A/DIW_2522A_문제.hwpx b/회차별채점자료/2522/A/DIW_2522A_문제.hwpx new file mode 100644 index 0000000..22f0d0c Binary files /dev/null and b/회차별채점자료/2522/A/DIW_2522A_문제.hwpx differ diff --git a/회차별채점자료/2522/B/DIW_2522B.hwpx b/회차별채점자료/2522/B/DIW_2522B.hwpx new file mode 100644 index 0000000..149fb61 Binary files /dev/null and b/회차별채점자료/2522/B/DIW_2522B.hwpx differ diff --git a/회차별채점자료/2522/B/DIW_2522B.json b/회차별채점자료/2522/B/DIW_2522B.json new file mode 100644 index 0000000..92e2e53 --- /dev/null +++ b/회차별채점자료/2522/B/DIW_2522B.json @@ -0,0 +1,841 @@ +{ + "0": { + "0": { + "path": "", + "path2": "", + "points": 0, + "category": "파일저장", + "item": "파일명 (수검번호.hwp/hwpx)" + }, + "1": { + "path": "//PAGEMARGIN", + "value": { + "Top": 20, + "Bottom": 20, + "Left": 20, + "Right": 20, + "Header": 10, + "Footer": 10, + "Gutter": 0 + }, + "tolerance": 1, + "points": 4, + "category": "PageSetting", + "item": "A4용지, 왼쪽/오른쪽/위쪽/아래쪽 (각20mm), 머리말/꼬리말 (10mm), 제본(0mm)" + }, + "2": { + "path": "", + "value": { + "FontName": "바탕", + "FontSize": "1000", + "Alignment": "Justify", + "LineSpacing": "160" + }, + "points": 4, + "category": "BasicSetting", + "item": "글꼴 (바탕, 10pt), 양쪽정렬, 줄간격 (160%)" + }, + "3": { + "path": "", + "value": null, + "points": 40, + "category": "오타감점", + "item": "오타 1개 -1점 / 2503회부터 오타 1개 -1점으로 변경" + } + }, + "1": { + "1": { + "path": "//TEXTART[@Text='{searchValue}']/TEXTARTSHAPE/@FontName", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": "돋움체", + "points": 1, + "category": "OneAnswer", + "item": "문구 (소셜네트워킹전략컨퍼런스)/① 글씨체 (돋움체)" + }, + "2": { + "path": "//TEXTART[@Text='{searchValue}']/descendant::WINDOWBRUSH/@FaceColor", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": "201,102,248", + "points": 2, + "category": "Color", + "item": "문구 (소셜네트워킹전략컨퍼런스)/② 채우기 : 색상(RGB:201,102,248)" + }, + "3": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Width", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": "120", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "문구 (소셜네트워킹전략컨퍼런스)/③ 크기-너비 (120 mm)" + }, + "4": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/SIZE/@Height", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": "20", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "문구 (소셜네트워킹전략컨퍼런스)/④ 크기-높이 (20 mm)" + }, + "5": { + "path": "//TEXTART[@Text='{searchValue}']/SHAPEOBJECT/POSITION/@TreatAsChar", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": "true", + "points": 2, + "category": "OneAnswer", + "item": "문구 (소셜네트워킹전략컨퍼런스)/⑤ 위치 (글자처럼 취급)" + }, + "6": { + "path": "//PARASHAPE[@Id=//P[.//TEXTART[@Text='{searchValue}']]/@ParaShape]/@Align", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": "Center", + "points": 2, + "category": "OneAnswer", + "item": "문구 (소셜네트워킹전략컨퍼런스)/⑥ 정렬 (가운데 정렬)" + }, + "7": { + "path": "//TEXTART[@Text='{searchValue}']", + "searchValue": "소셜네트워킹전략컨퍼런스", + "value": true, + "points": 2, + "category": "Boolean", + "item": "문구 (소셜네트워킹전략컨퍼런스)/⑦ 글맵시모양 (육안확인)" + }, + "8": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/SIZE", + "searchValue": "최", + "value": { + "Height": 2800, + "Width": 2800 + }, + "tolerance": 200, + "points": 1, + "category": "TwoLineSize", + "item": "2/① 모양 (2줄)" + }, + "9": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "최", + "value": "궁서", + "points": 1, + "category": "FontName", + "item": "2/② 글씨체 (궁서)" + }, + "10": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor", + "searchValue": "최", + "value": "218,202,48", + "points": 2, + "category": "Color", + "item": "2/③ 면색 : 색상(RGB:218,202,48)" + }, + "11": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//OUTSIDEMARGIN/@Right", + "searchValue": "최", + "value": "3.0", + "tolerance": 1, + "points": 2, + "category": "mmSize", + "item": "2/④ 본문과의 간격 : 3.0mm" + }, + "12": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", + "searchValue": "소셜 네트워킹 서비스", + "value": "BOLD", + "points": 2, + "category": "FontAttribute", + "item": "문구 (소셜 네트워킹 서비스)/① BOLD" + }, + "13": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", + "searchValue": "소셜 네트워킹 서비스", + "value": "ITALIC", + "points": 2, + "category": "FontAttribute", + "item": "문구 (소셜 네트워킹 서비스)/② ITALIC" + }, + "14": { + "path": "//CHAR[contains(string(.),'{char1}')]/text()", + "path2": "//CHAR[contains(string(.),'{char2}')]/text()", + "path3": "//CHAR[contains(string(.),'{char3}')]/text()", + "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(),'{match_str}')]/ancestor::P/@ParaShape]/@Align", + "match_str": "행사안내", + "value": "Center", + "points": 1, + "category": "Align", + "item": "문구 (□ 행사안내 □)/② 정렬 (가운데 정렬)" + }, + "17": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "hyperlink_ptag": "//P[.//FIELDBEGIN[@Type='Hyperlink']]", + "searchValue": "서울 강남구 한국정보기술협력센터 3층 대회의장", + "value": "ITALIC", + "points": 1, + "category": "FontAttribute", + "item": "문구 (서울 강남구 한국정보기술협력센터 3층 대회의장)/① ITALIC" + }, + "18": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "hyperlink_ptag": "//P[.//FIELDBEGIN[@Type='Hyperlink']]", + "searchValue": "서울 강남구 한국정보기술협력센터 3층 대회의장", + "value": "UNDERLINE", + "points": 1, + "category": "FontAttribute", + "item": "문구 (서울 강남구 한국정보기술협력센터 3층 대회의장)/② UNDERLINE" + }, + "19": { + "path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN", + "searchValue": "기타사항", + "value": { + "Left": 10, + "Indent": 12 + }, + "points": 2, + "category": "ParaShape", + "item": "문구 (※ 기타… 이하 문단)/왼쪽여백 (10pt), 내어쓰기 (12pt)", + "desc": "내부적으로 내어쓰기는 음수값 / JSON value값은 양수로 입력" + }, + "20": { + "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]/@Height", + "searchValue": "2025. 08. 30.", + "value": "1400", + "points": 1, + "category": "OneAnswer", + "item": "문구 (2025. 08. 30.)/① 크기 (1400)", + "desc": "1pt당 100" + }, + "21": { + "path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align", + "searchValue": "2025. 08. 30.", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (2025. 08. 30.)/② 정렬 (가운데 정렬)" + }, + "22": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "한국정보서비스학회장", + "value": "궁서체", + "points": 1, + "category": "FontName", + "item": "문구 (한국정보서비스학회장)/① 글씨체 (궁서체)" + }, + "23": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "한국정보서비스학회장", + "value": "2000", + "points": 1, + "category": "OneAnswer", + "item": "문구 (한국정보서비스학회장)/② 크기 (2000)" + }, + "24": { + "path": "//PARASHAPE[@Id=//CHAR[text()='{searchValue}']/ancestor::P/@ParaShape]/@Align", + "searchValue": "한국정보서비스학회장", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (기능경진대회운영위원회)/③ 정렬 (가운데 정렬)" + }, + "25": { + "path": "//SECTION[1]//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "DIAT", + "value": "돋움", + "points": 1, + "category": "FontName", + "item": "문구 (DIAT)/① 글꼴 (돋움)" + }, + "26": { + "path": "//CHARSHAPE[@Id=//SECTION[1]//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "DIAT", + "value": "900", + "points": 1, + "category": "OneAnswer", + "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": "OneAnswer", + "item": "문구 (DIAT)/③ 정렬 (오른쪽 정렬)" + }, + "28": { + "path": "//PAGENUM/@FormatType", + "value": "Digit", + "points": 2, + "category": "PageNumber", + "item": "① 쪽 번호 매기기 (가,나,다 순으로)", + "desc1": { + "가,나,다": "HangulSyllable", + "1,2,3": "Digit", + "갑,을,병": "DecagonCircle", + "A,B,C": "LatinCapital", + "a,b,c": "LatinSmall", + "①,②,③": "CircledDigit", + "一,二,三": "Ideograph", + "㉠,㉡,㉢": "CircledHangulJamo", + "ⓐ,ⓑ,ⓒ": "CircledLatinSmall", + "i,ii,iii": "RomanSmall", + "I,II,III": "RomanCapital", + "desc": "정답에 맞는 값 value에 입력" + }, + "desc2": "1, 2페이지 모두 정답이어야 점수 부여" + }, + "29": { + "path": "//PAGENUM/@Pos", + "value": "BottomCenter", + "points": 2, + "category": "PageNumber", + "item": "가운데 아래", + "desc": "1, 2페이지 모두 정답이어야 점수 부여", + "desc2": { + "가운데 아래": "BottomCenter", + "오른쪽 아래": "BottomRight" + } + }, + "30": { + "path": "//PARASHAPE[@Id='{parashape_id}']/PARAMARGIN/@LineSpacing", + "value": "190", + "first_word": "최", + "points": 2, + "category": "LineSpacing", + "item": "문제 1 줄간격 190% 설정", + "desc": "1페이지 문단의 줄간격이 정답이 아닌 문단이 있으면 False(감점), first_word 속성에 [문단 첫글자 장식]에 해당하는 글자를 입력해준다." + } + }, + "2": { + "1": { + "path": "//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@HeaderInside", + "path2": "//BORDERFILL[@Id=//PAGEBORDERFILL[@Type='Both' or @Type='Even']/@BorferFill]", + "value": { + "header_inside": true, + "all_double_slim": true + }, + "points": 4, + "category": "PageBorder", + "item": "문제2 쪽테두리(이중 실선, 머리말 포함) 설정" + }, + "2": { + "path": "count(//SECTION)>1", + "value": true, + "points": 3, + "category": "Boolean", + "item": "① 구역나누기", + "desc": "섹션이 1개 이상이면 점수부여" + }, + "3": { + "path": "TEXT/COLDEF/@Count", + "value": "2", + "points": 3, + "category": "TwoColumn", + "item": "② 다단 2단" + }, + "4": { + "path": "//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Width", + "searchValue": "소셜 네트워킹 서비스", + "value": "70", + "points": 2, + "category": "mmSize", + "item": "문구 (소셜 네트워킹 서비스)/① 크기-너비 (60 mm)" + }, + "5": { + "path": "//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::RECTANGLE/SHAPEOBJECT/SIZE/@Height", + "searchValue": "소셜 네트워킹 서비스", + "value": "12", + "points": 2, + "category": "mmSize", + "item": "문구 (소셜 네트워킹 서비스)/② 크기-높이 (12 mm)" + }, + "6": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//LINESHAPE", + "searchValue": "소셜 네트워킹 서비스", + "value": { + "Style": "DoubleSlim", + "Width": "283" + }, + "points": 2, + "category": "LineShape", + "item": "문구 (소셜 네트워킹 서비스)/③ 테두리 : 이중 실선(1.00mm)", + "desc": "1mm = 283pt value['Width']에 pt값 입력" + }, + "7": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/@Ratio", + "searchValue": "소셜 네트워킹 서비스", + "value": "50", + "points": 2, + "category": "OneAnswer", + "item": "문구 (소셜 네트워킹 서비스)/④ 글상자 모서리 (반원)", + "desc": "모서리 비율 반원:50 / 둥근모양:20" + }, + "8": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]//WINDOWBRUSH/@FaceColor", + "searchValue": "소셜 네트워킹 서비스", + "value": "90,233,53", + "points": 2, + "category": "Color", + "item": "문구 (소셜 네트워킹 서비스)/⑤ 채우기 : 색상(RGB:90,233,53)" + }, + "9": { + "path": "//RECTANGLE[.//CHAR[text()='{searchValue}']]/SHAPEOBJECT/POSITION/@TreatAsChar", + "searchValue": "소셜 네트워킹 서비스", + "value": "true", + "points": 1, + "category": "OneAnswer", + "item": "문구 (소셜 네트워킹 서비스)/⑥ 글상자 위치 (글자처럼 취급)" + }, + "10": { + "path": "//PARASHAPE[@Id=//RECTANGLE//CHAR[text()='{searchValue}']/ancestor::P[last()]/@ParaShape]/@Align", + "searchValue": "소셜 네트워킹 서비스", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (소셜 네트워킹 서비스)/⑦ 글상자 정렬 (가운데 정렬)" + }, + "11": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "소셜 네트워킹 서비스", + "value": "궁서체", + "points": 1, + "category": "FontName", + "item": "문구 (소셜 네트워킹 서비스)/⑧ 글씨체 (궁서체)" + }, + "12": { + "path": "//CHARSHAPE[@Id=//RECTANGLE//TEXT[./CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "소셜 네트워킹 서비스", + "value": "1800", + "points": 1, + "category": "OneAnswer", + "item": "문구 (소셜 네트워킹 서비스)/⑨ 글씨크기 (1800)", + "desc": "1pt당 100" + }, + "13": { + "path": "//PARASHAPE[@Id=//RECTANGLE//P[.//CHAR[text()='{searchValue}']]/@ParaShape]/@Align", + "searchValue": "소셜 네트워킹 서비스", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "문구 (소셜 네트워킹 서비스)/⑩ 정렬 (가운데 정렬)" + }, + "14": { + "path": "//BINITEM[@BinData=//PICTURE/IMAGE/@BinItem][@Format='JPG' or @Format='JPEG']", + "value": true, + "points": 2, + "category": "Boolean", + "item": "① 파일명 \"그림B.jpg\" 삽입", + "desc": "첨부 이미지 파일명 손상으로 정상적인 채점이 불가한 경우가 발견되어서 이미지 첨부 여부로 채점 방식 변경 (7/3)" + }, + "15": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/SIZE/@Width", + "value": "80", + "points": 2, + "category": "mmSize", + "item": "② 크기-너비 (80 mm)" + }, + "16": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/SIZE/@Height", + "value": "45", + "points": 2, + "category": "mmSize", + "item": "③ 크기-높이 (45 mm)" + }, + "17": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/POSITION[not(@TreatAsChar='true') and @HorzRelTo='Page']/@HorzOffset", + "value": "0", + "points": 2, + "category": "mmSize", + "item": "④ 위치 (어울림 : 가로-쪽의 왼쪽 0.0mm)" + }, + "18": { + "path": "//PICTURE[./IMAGE[@BinItem=//BINITEM[@Format='JPG' or @Format='JPEG']/@BinData]]/SHAPEOBJECT/POSITION[not(@TreatAsChar='true') and @HorzRelTo='Page']/@VertOffset", + "value": "22", + "points": 2, + "category": "mmSize", + "item": "⑤ 위치 (어울림 : 세로-쪽의 위 22 mm)" + }, + "19": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "1. 소셜 네트워킹 서비스", + "value": "굴림체", + "points": 1, + "category": "FontName", + "item": "문구① (1. 소셜 네트워킹 서비스)/① 글씨체 (굴림체)" + }, + "20": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "1. 소셜 네트워킹 서비스", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "문구① (1. 소셜 네트워킹 서비스)/② 크기 (12pt)" + }, + "21": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "searchValue": "1. 소셜 네트워킹 서비스", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "문구① (1. 소셜 네트워킹 서비스)/③ 진하게" + }, + "22": { + "path": "//TEXT[CHAR[text()='{searchValue}']]/@CharShape", + "searchValue": "2. 소셜 네트워킹 서비스 활용", + "value": "굴림체", + "points": 1, + "category": "FontName", + "item": "문구② (2. 소셜 네트워킹 서비스 활용)/① 글씨체 (굴림체)" + }, + "23": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "2. 소셜 네트워킹 서비스 활용", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "문구② (2. 소셜 네트워킹 서비스 활용)/② 크기 (1200)" + }, + "24": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "searchValue": "2. 소셜 네트워킹 서비스 활용", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "문구② (2. 소셜 네트워킹 서비스 활용)/③ 진하게" + }, + "25": { + "path": "boolean(//TEXT[CHAR[contains(text(),'{option}')]]/FOOTNOTE)", + "path2": "boolean(//CHAR[substring(., string-length(.) - string-length('{option}') + 1) = '{option}']/following-sibling::FOOTNOTE/descendant::CHAR)", + "option": "마이크로블로깅", + "value": true, + "points": 2, + "category": "Boolean", + "item": "문구 (마이크로블로깅)/① 각주 설정 및 문구 입력" + }, + "26": { + "path": "//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape", + "searchValue": "블로거가 한두 문장 정도의 단편적 정보를 관심이 있는 개인들에게 전달하는 통신방식", + "value": "돋움", + "points": 1, + "category": "FontName", + "item": "문구 (마이크로블로깅)/② 글씨체 (돋움)" + }, + "27": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[contains(text(),'{searchValue}')]]/@CharShape]/@Height", + "searchValue": "블로거가 한두 문장 정도의 단편적 정보를 관심이 있는 개인들에게 전달하는 통신방식", + "value": "900", + "points": 1, + "category": "OneAnswer", + "item": "문구 (마이크로블로깅)/③ 크기 (9pt)" + }, + "28": { + "path": "//P[TEXT[CHAR[contains(text(), '{searchValue}')]]]//AUTONUMFORMAT/@Type", + "searchValue": "블로거가 한두 문장 정도의 단편적 정보를 관심이 있는 개인들에게 전달하는 통신방식", + "value": "CircledLatinCapital", + "points": 2, + "category": "OneAnswer", + "item": "문구 (전당)/④ 각주 번호모양", + "desc": { + "가,나,다": "HangulSyllable", + "1,2,3": "Digit", + "갑,을,병": "DecagonCircle", + "A,B,C": "LatinCapital", + "a,b,c": "LatinSmall", + "①,②,③": "CircledDigit", + "一,二,三": "Ideograph", + "㉠,㉡,㉢": "CircledHangulJamo", + "Ⓐ,Ⓑ,Ⓒ": "CircledLatinCapital", + "ⓐ,ⓑ,ⓒ": "CircledLatinSmall", + "i,ii,iii": "RomanSmall", + "I,II,III": "RomanCapital", + "甲,乙,丙": "DecagonCircleHanja", + "+,++,+++": "UserChar", + "정답에 맞는 값 value에 입력": "" + } + }, + "29": { + "path": "boolean(//CHAR[contains(text(),'Marketing')])", + "ignoreWord": "Marketing", + "value": true, + "points": 3, + "category": "Boolean", + "item": "Marketing/영단어 미입력, 대소문자/오타 시 전체 감점", + "desc": "유사도 검사를 진행하지 않고 영단어가 모두 일치해야 하므로 xpath구문 내 단어도 수정필요" + }, + "30": { + "path": "//CHAR[contains(text(),'{kor}')][contains(text(),'{chn}')]", + "word": [ + ["방식", "方式"], + ["활성화", "活性化"], + ["획득", "獲得"], + ["교류", "交流"], + ["절감", "節減"] + ], + "value": 10, + "points": 10, + "category": "Hanja", + "item": "① 방식(方式), ② 활성화(活性化), ③ 획득(獲得), ④ 교류(交流), ⑤ 절감(節減)" + }, + "31": { + "path": "boolean(//CHAR[contains(translate(text(), ' ', ''),'적인매체')])", + "value": true, + "points": 3, + "category": "Boolean", + "item": "문구 (…대표적의 매체로…)→'의' → '인' 글자바꿈" + }, + "32": { + "path": "boolean(//CHAR[contains(translate(text(), ' ', ''),'준의상향')])", + "value": true, + "points": 3, + "category": "Boolean", + "item": "문구 (…필요한 개인이 정보를…)→'필요한' / '개인이' 순서바꿈" + }, + "33": { + "path": "//TEXT[CHAR[contains(text(),'{searchValue}')]]/@CharShape", + "searchValue": "스마트폰 가입자 수(단위 : 만 명)", + "value": "굴림체", + "points": 1, + "category": "FontName", + "item": "제목 문구 (스마트폰 가입자 수(단위 : 만 명))/① 글씨체 (굴림체)" + }, + "34": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]/@Height", + "searchValue": "스마트폰 가입자 수(단위 : 만 명)", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (스마트폰 가입자 수(단위 : 만 명))/② 크기 (1200)" + }, + "35": { + "path": "//CHARSHAPE[@Id=//TEXT[CHAR[text()='{searchValue}']]/@CharShape]", + "searchValue": "스마트폰 가입자 수(단위 : 만 명)", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "제목 문구 (스마트폰 가입자 수(단위 : 만 명))/③ 진하게" + }, + "36": { + "path": "//PARASHAPE[@Id=//P[.//CHAR[text()='{searchValue}']]/@ParaShape]/@Align", + "searchValue": "스마트폰 가입자 수(단위 : 만 명)", + "value": "Center", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (스마트폰 가입자 수(단위 : 만 명))/④ 정렬 (가운데 정렬)" + }, + "37": { + "path": "//BORDERFILL[@Id=//TABLE/ROW[1]/CELL/@BorderFill]/FILLBRUSH/WINDOWBRUSH/@FaceColor", + "path2": "//BORDERFILL[@Id=//CELLZONE[@StartRowAddr='0' and @EndRowAddr='0' and @StartColAddr='0' and @EndColAddr=(ancestor::TABLE[1]/@ColCount)-1]/@BorderFill]/FILLBRUSH/WINDOWBRUSH/@FaceColor", + "value": "248,203,169", + "points": 2, + "category": "Color", + "item": "위쪽 제목 셀/① 색상(RGB:248,203,169)" + }, + "38": { + "path": "//CHARSHAPE[@Id=//TABLE/ROW[1]/descendant::TEXT/@CharShape]", + "value": "BOLD", + "points": 1, + "category": "FontAttribute", + "item": "위쪽 제목 셀/② 진하게", + "desc": "글자 속성이라 CELLZONE으로 적용 되지 않음" + }, + "39": { + "path": "//BORDERFILL[@Id=//TABLE/ROW[1]/CELL/@BorderFill]/BOTTOMBORDER/@Type", + "path2": "//BORDERFILL[@Id=//CELLZONE[@StartRowAddr='0' and @EndRowAddr='0' and @StartColAddr='0' and @EndColAddr=(ancestor::TABLE[1]/@ColCount)-1]/@BorderFill]/BOTTOMBORDER/@Type", + "value": "DoubleSlim", + "points": 2, + "category": "TableAnswer", + "item": "제목 셀 아래선/① 이중실선" + }, + "40": { + "path": "//BORDERFILL[@Id=//TABLE/ROW[1]/CELL/@BorderFill]/BOTTOMBORDER/@Width", + "path2": "//BORDERFILL[@Id=//CELLZONE[@StartRowAddr='0' and @EndRowAddr='0' and @StartColAddr='0' and @EndColAddr=(ancestor::TABLE[1]/@ColCount)-1]/@BorderFill]/BOTTOMBORDER/@Width", + "value": "0.5mm", + "points": 2, + "category": "TableAnswer", + "item": "제목 셀 아래선/② 0.5mm" + }, + "41": { + "path": "//TABLE//TEXT/@CharShape", + "path2": "//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE[@Id=//TABLE/ROW/descendant::TEXT/@CharShape]/FONTID/@Hangul]/@Name", + "value": "굴림", + "points": 1, + "category": "TableFontName", + "category_tmp": "FontName", + "item": "글자모양/① 글씨체 (굴림)", + "desc": "테이블 폰트명 문항은 테이블의 모든 셀이 정답폰트와 일치해야 함, 하나만 일치해도 정답으로 채점할 경우 category값을 FontName으로 변경" + }, + "42": { + "path": "//CHARSHAPE[@Id=//TABLE//TEXT/@CharShape]/@Height", + "value": "1000", + "points": 1, + "category": "TableAnswer", + "item": "글자모양/② 크기 (1000)" + }, + "43": { + "path": "//PARASHAPE[@Id=//TABLE/ROW//P/@ParaShape]/@Align", + "value": "Center", + "points": 1, + "category": "TableAnswer", + "item": "글자모양/③ 정렬 (가운데 정렬)" + }, + "44": { + "path": "boolean(//TABLE[1]/ROW[last()]/CELL[position()=last()]//FIELDBEGIN[starts-with(@Command, '={option}')]) and boolean(//TABLE[1]/ROW[last()]/CELL[position()=last()-1]//FIELDBEGIN[starts-with(@Command, '={option}')])", + "option": "AVG", + "value": true, + "points": 4, + "category": "Boolean", + "item": "블록 계산식/합계", + "desc": "option값에 합계는 SUM / 평균은 AVG" + }, + "45": { + "chart_xpath": "", + "chart_type": "꺾은선형", + "value": true, + "points": 2, + "category": "ChartType", + "item": "① 종류 (꺾은선형)", + "desc": "chart_type을 입력받아 차트타입에 맞는 xml요소가 있는지 내부적으로 검사, chart_type만 한글로 입력해주면 된다. (공백무시)" + }, + "46": { + "chart_xpath": "//c:valAx/c:majorTickMark/@val", + "value": "out", + "points": 2, + "category": "ChartOneAnswer", + "item": "② 값 축 주 눈금선", + "desc": "chart xml파일에서 답안을 가져오는 문항은 path키값 대신 chart_xpath키값을 이용해 xapth구문을 작성한다" + }, + "47": { + "path": "//OLE[@BinItem=//BINITEM[@Format='OLE']/@BinData]//SIZE/@Width", + "value": "80", + "points": 2, + "category": "mmSize", + "item": "③ 크기-너비 (80 mm)" + }, + "48": { + "path": "//OLE[@BinItem=//BINITEM[@Format='OLE']/@BinData]//SIZE/@Height", + "value": "90", + "points": 2, + "category": "mmSize", + "item": "④ 크기-높이 (90 mm)" + }, + "49": { + "chart_xpath": "boolean(//c:chart and not(//c:pt[not(ancestor::c:tx)]/c:v[text()='합계' or text()='평균']))", + "value": true, + "points": 2, + "category": "Boolean", + "item": "⑤ 차트 데이터(표에서 블록계산식을 제외한 나머지 값만 이용)", + "desc": "차트가 존재하고 블록계산식(합계, 평균) 데이터가 없는 경우 정답 처리" + }, + "50": { + "chart_xpath": "//a:t[text()='{searchValue}']/ancestor::a:r//a:ea/@typeface", + "searchValue": "스마트폰 가입자 수", + "value": "궁서체", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (스마트폰 가입자 수)/① 글씨체 (궁서체)" + }, + "51": { + "chart_xpath": "//a:t[text()='{searchValue}']/ancestor::a:r/a:rPr/@sz", + "searchValue": "스마트폰 가입자 수", + "value": "1200", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (스마트폰 가입자 수)/② 크기 (1200)" + }, + "52": { + "chart_xpath": "//a:t[text()='{searchValue}']/ancestor::a:r/a:rPr/@{option}", + "option": "b", + "searchValue": "스마트폰 가입자 수", + "value": "1", + "points": 1, + "category": "OneAnswer", + "item": "제목 문구 (스마트폰 가입자 수)/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + }, + "53": { + "chart_xpath": "//c:catAx/c:txPr//a:ea/@typeface", + "value": "돋움체", + "points": 1, + "category": "ChartOneAnswer", + "item": "X축/① 글꼴 (돋움체)" + }, + "54": { + "chart_xpath": "//c:catAx/c:txPr//a:defRPr/@sz", + "value": "900", + "points": 1, + "category": "ChartOneAnswer", + "item": "X축/② 크기 (9pt)" + }, + "55": { + "chart_xpath": "//c:catAx/c:txPr//a:defRPr/@{option}", + "option": "i", + "value": "1", + "points": 1, + "category": "ChartOneAnswer", + "item": "X축/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + }, + "56": { + "chart_xpath": "//c:valAx/c:txPr//a:ea/@typeface", + "value": "돋움체", + "points": 1, + "category": "ChartOneAnswer", + "item": "Y축/① 글꼴 (돋움체)" + }, + "57": { + "chart_xpath": "//c:valAx/c:txPr//a:defRPr/@sz", + "value": "900", + "points": 1, + "category": "ChartOneAnswer", + "item": "Y축/② 크기 (9pt)" + }, + "58": { + "chart_xpath": "//c:valAx/c:txPr//a:defRPr/@{option}", + "option": "i", + "value": "1", + "points": 1, + "category": "ChartOneAnswer", + "item": "Y축/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + }, + "59": { + "chart_xpath": "//c:legend//a:ea/@typeface", + "value": "돋움체", + "points": 1, + "category": "OneAnswer", + "item": "범례/① 글꼴 (돋움체)" + }, + "60": { + "chart_xpath": "//c:legend//a:defRPr/@sz", + "value": "900", + "points": 1, + "category": "OneAnswer", + "item": "범례/② 크기 (9pt)" + }, + "61": { + "chart_xpath": "//c:legend//a:defRPr/@{option}", + "option": "i", + "value": "1", + "points": 1, + "category": "OneAnswer", + "item": "범례/③ 기울임", + "desc": "option값 - 기울임(Italic):i / 굵게(Bold):b" + } + } +} diff --git a/회차별채점자료/2522/B/DIW_2522B.xlsx b/회차별채점자료/2522/B/DIW_2522B.xlsx new file mode 100644 index 0000000..8b74740 Binary files /dev/null and b/회차별채점자료/2522/B/DIW_2522B.xlsx differ diff --git a/회차별채점자료/2522/B/DIW_2522B_문제.hwpx b/회차별채점자료/2522/B/DIW_2522B_문제.hwpx new file mode 100644 index 0000000..2878ee5 Binary files /dev/null and b/회차별채점자료/2522/B/DIW_2522B_문제.hwpx differ diff --git a/회차별채점자료/2522/C/DIW_2522C.hml b/회차별채점자료/2522/C/DIW_2522C.hml new file mode 100644 index 0000000..7dc25d3 --- /dev/null +++ b/회차별채점자료/2522/C/DIW_2522C.hml @@ -0,0 +1,3115 @@ +user2025년 4월 17일 목요일 오전 9:52:45^1.^2.^3)^4)(^5)(^6)^7^8^1.^2.^3)^4)(^5)(^6)^7^8