diwScoring.py 문제1 적용완료

This commit is contained in:
2025-04-25 17:57:04 +09:00
parent 5d3ff211ea
commit c16af0063d
5 changed files with 213 additions and 9 deletions

View File

@@ -181,22 +181,22 @@
"item": "문구 (■ 행사안내 ■)/② 정렬 (가운데 정렬)" "item": "문구 (■ 행사안내 ■)/② 정렬 (가운데 정렬)"
}, },
"17": { "17": {
"path": "boolean(//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape][BOLD])", "path": "boolean(//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape][ITALIC])",
"path2": null, "path2": null,
"searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회", "searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수",
"value": true, "value": true,
"points": 1, "points": 1,
"category": "글꼴 속성", "category": "글꼴 속성",
"item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/① 진하게" "item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/① 진하게"
}, },
"18": { "18": {
"path": "boolean(//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape][UNDERLINE])", "path": "boolean(//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape][UNDERLINE])",
"path2": null, "path2": null,
"searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회", "searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수",
"value": true, "value": true,
"points": 1, "points": 1,
"category": "글꼴 속성", "category": "글꼴 속성",
"item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/② 밑줄" "item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/② 밑줄"
}, },
"19": { "19": {
"path": "boolean(//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Left=3000 and //PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Indent=-2400)", "path": "boolean(//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Left=3000 and //PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Indent=-2400)",

View File

@@ -153,12 +153,151 @@
}, },
"13": { "13": {
"path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]", "path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]",
"path2": null,
"searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회", "searchValue": "한옥에 대한 체험과 교육이 준비된 사생대회",
"value": "UNDERLINE", "value": "UNDERLINE",
"points": 2, "points": 2,
"category": "FontAttribute", "category": "FontAttribute",
"item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/② 밑줄" "item": "문구 (한옥에 대한 체험과 교육이 준비된 사생대회)/② 밑줄"
},
"14": {
"path": "//CHAR[contains(text(),'{char1}')]",
"path2": "//CHAR[contains(text(),'{char2}')]",
"char1": "■",
"char2": "※",
"value": 3,
"points": 3,
"category": "SpecialChar",
"item": "① ■, ② ■, ③ ※"
},
"15": {
"path": "//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape",
"searchValue": "■ 행사안내 ■",
"value": "돋움",
"points": 1,
"category": "FontName",
"item": "문구 (■ 행사안내 ■)/① 글씨체 (돋움)"
},
"16": {
"path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align",
"searchValue": "■ 행사안내 ■",
"value": "Center",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (■ 행사안내 ■)/② 정렬 (가운데 정렬)"
},
"17": {
"path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]",
"searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수",
"value": "ITALIC",
"points": 1,
"category": "FontAttribute",
"item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/① 기울임"
},
"18": {
"path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]",
"searchValue": "홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수",
"value": "UNDERLINE",
"points": 1,
"category": "FontAttribute",
"item": "문구 (홈페이지(http://www.ihd.or.kr)에서 개별 신청, 선착순 접수)/② 밑줄"
},
"19": {
"path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN",
"searchValue": "기타사항",
"value": {
"Left": 15,
"Indent": 12
},
"points": 2,
"category": "ParaShape",
"item": "문구 (※ 기타… 이하 문단)/왼쪽여백 (15pt), 내어쓰기 (12pt)",
"desc": "내부적으로 내어쓰기는 음수값"
},
"20": {
"path": "//CHARSHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/parent::TEXT/@CharShape]/@Height",
"searchValue": "2025. 03. 22.",
"value": "1300",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (2025. 03. 22.)/① 크기 (13pt)"
},
"21": {
"path": "//PARASHAPE[@Id=//CHAR[contains(text(),'{searchValue}')]/ancestor::P/@ParaShape]/@Align",
"searchValue": "2025. 03. 22.",
"value": "Center",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (2025. 03. 22.)/② 정렬 (가운데 정렬)"
},
"22": {
"path": "//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape",
"searchValue": "한국고건축협회",
"value": "궁서",
"points": 1,
"category": "FontName",
"item": "문구 (한국고건축협회)/① 글씨체 (궁서)"
},
"23": {
"path": "//CHARSHAPE[@Id=//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]/@Height",
"searchValue": "한국고건축협회",
"value": "2400",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (한국고건축협회)/② 크기 (24pt)"
},
"24": {
"path": "//PARASHAPE[@Id=//CHAR[text()='{searchValue}']/ancestor::P/@ParaShape]/@Align",
"searchValue": "한국고건축협회",
"value": "Center",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (한국고건축협회)/③ 정렬 (가운데 정렬)"
},
"25": {
"path": "//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape",
"searchValue": "DIAT",
"value": "굴림",
"points": 1,
"category": "FontName",
"item": "문구 (DIAT)/① 글꼴 (굴림)"
},
"26": {
"path": "//CHARSHAPE[@Id=//SECTION[1]//CHAR[text()='{searchValue}']/parent::TEXT/@CharShape]/@Height",
"searchValue": "DIAT",
"value": "900",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (DIAT)/② 크기 (9pt)"
},
"27": {
"path": "//PARASHAPE[@Id=//SECTION[1]//CHAR[text()='{searchValue}']/parent::TEXT/parent::P/@ParaShape]/@Align",
"searchValue": "DIAT",
"value": "Right",
"points": 1,
"category": "SingleAnswer",
"item": "문구 (DIAT)/③ 정렬 (오른쪽 정렬)"
},
"28": {
"path": "//PAGENUM/@FormatType",
"value": "HangulSyllable",
"points": 2,
"category": "SingleAnswer",
"item": "① 쪽 번호 매기기 (가,나,다 순으로)"
},
"29": {
"path": "//PAGENUM/@Pos",
"value": "BottomCenter",
"points": 2,
"category": "SingleAnswer",
"item": "② 가운데 아래"
},
"30": {
"path": "not(//PARASHAPE[@Id=//SECTION[1]/P/@ParaShape]/PARAMARGIN[@LineSpacing!='180'])",
"value": true,
"points": 2,
"category": "Boolean",
"item": "문제 1 줄간격 180% 설정",
"desc": "1페이지 문단의 줄간격이 180%가 아닌 문단이 있으면 False(감점)"
} }
} }
} }

View File

@@ -139,6 +139,10 @@ class XMLScorer:
is_correct = abs(user_answer - right_answer) <= tolerance is_correct = abs(user_answer - right_answer) <= tolerance
elif method == "in": elif method == "in":
is_correct = user_answer in right_answer 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: else:
raise ValueError(f"Unknown comparison method: {method}") raise ValueError(f"Unknown comparison method: {method}")
@@ -196,6 +200,7 @@ class XMLScorer:
# search_value를 포함하는 텍스트 찾기 # search_value를 포함하는 텍스트 찾기
similar_text = self.find_similar_text(root, search_value) similar_text = self.find_similar_text(root, search_value)
xpath = xpath.replace('{searchValue}', similar_text) xpath = xpath.replace('{searchValue}', similar_text)
# xpath2 = xpath2.replace('{searchValue}', similar_text)
# 문항 별 채점 결과 저장 # 문항 별 채점 결과 저장
scoring = { scoring = {
@@ -273,6 +278,21 @@ class XMLScorer:
if scoring['points'] > 0: if scoring['points'] > 0:
break break
elif "ParaShape" in (category or ""):
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 타입 정답인 경우 # Boolean 타입 정답인 경우
elif "Boolean" in (category or ""): elif "Boolean" in (category or ""):
items = root.xpath(xpath) items = root.xpath(xpath)
@@ -296,6 +316,7 @@ class XMLScorer:
if scoring['points'] > 0: if scoring['points'] > 0:
break break
# 문단 첫글자 장식 채점
elif "TwoLineSize" in (category or ""): elif "TwoLineSize" in (category or ""):
items = root.xpath(xpath) items = root.xpath(xpath)
error_range = criterion.get('tolerance', 0) error_range = criterion.get('tolerance', 0)
@@ -309,6 +330,7 @@ class XMLScorer:
if scoring['points'] > 0: if scoring['points'] > 0:
break break
# 폰트명
elif "FontName" in (category or ""): elif "FontName" in (category or ""):
charshape_id = root.xpath(xpath) charshape_id = root.xpath(xpath)
if not charshape_id: if not charshape_id:
@@ -321,6 +343,7 @@ class XMLScorer:
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
# 폰트 속성
elif "FontAttribute" in (category or ""): elif "FontAttribute" in (category or ""):
charshape = root.xpath(xpath) charshape = root.xpath(xpath)
if not charshape: if not charshape:
@@ -330,9 +353,51 @@ class XMLScorer:
font_attribute = charshape[0].find(right_answer) font_attribute = charshape[0].find(right_answer)
if font_attribute is not None: if font_attribute is not None:
user_answer = font_attribute.tag user_answer = font_attribute.tag
else:
user_answer = None
self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal") self.evaluate_answer(scoring, user_answer, right_answer, points, method="equal")
# 특수문자 갯수 채점
elif "SpecialChar" in (category or ""):
ch1 = criterion.get('char1', None)
ch2 = criterion.get('char2', None)
xpath = xpath.replace('{char1}', ch1)
xpath2 = xpath2.replace('{char2}', ch2)
char1_ele = root.xpath(xpath)
char2_ele = root.xpath(xpath2)
sum_char = 0
# char1 요소에서 특수문자 갯수 세기 (최대 2점)
if not char1_ele:
user_answer = 0
else:
for item in char1_ele:
count_char1 = item.text.count(ch1)
sum_char += count_char1
if sum_char >= 2:
sum_char = 2
break
# char2 요소에서 특수문자 갯수 세기 (최대 1점)
if not char2_ele:
user_answer = 0
else:
count_char2 = char2_ele[0].text.count(ch2)
if count_char2 > 1:
count_char2 = 1
sum_char += count_char2
user_answer = sum_char
self.evaluate_answer(scoring, user_answer, right_answer, points, method="partial_score")
# 줄간격
elif "LineSpacing" in (category or ""):
break
onePersonResult['score_results'].append(scoring) onePersonResult['score_results'].append(scoring)
print(f'scoring: {scoring}') print(f'scoring: {scoring}')
onePersonResult['partial_scores'].append({ onePersonResult['partial_scores'].append({

View File

@@ -1 +1 @@
[{"kind":2,"language":"xpath","value":"boolean(//PARASHAPE[@Id=//CHAR[contains(text(),'기타')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Left=3000 and //PARASHAPE[@Id=//CHAR[contains(text(),'기타')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Indent=-2400)"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE/FONTID/@Hangul][@Name='바탕']"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Name='바탕']/@Id"},{"kind":2,"language":"xpath","value":"//CHARSHAPE[@Id='6']/FONTID/@Hangul"},{"kind":2,"language":"xpath","value":"//PARASHAPE[@Id='0']/@Align"},{"kind":2,"language":"xpath","value":"boolean(//RECTANGLE[.//CHAR[text()='지']][.//SIZE[(@Height >= 2600 and @Height <= 2800)and(@Width >= 2600 and @Width <= 2800)]])"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE[@Id=//TEXT[CHAR[text()='지']]/@CharShape]/FONTID/@Hangul]/@Name"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE[@Id={charshape_id}]/FONTID/@Hangul]/@Name"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id={font_id}]/@Name"},{"kind":2,"language":"xpath","value":"//CHARSHAPE[@Id=//CHAR[contains(text()[1],'전 세계적으로 차량의 수는 약 13억 대가 있고 국내는 약 2,500만 대')]/parent::TEXT/@CharShape][ITALIC]"},{"kind":2,"language":"xpath","value":"//CHAR[contains(text(),'전 세계적으로 차량의 수는 약 13억 대가 있고 국내는 약 2,500만 대')]/parent::TEXT/@CharShape"},{"kind":2,"language":"xpath","value":"//CHARSHAPE[@Id=//CHAR[contains(text(),'한옥에 대한 체험과 교육이 준비된 사생대회')]/parent::TEXT/@CharShape]"}] [{"kind":2,"language":"xpath","value":"boolean(//PARASHAPE[@Id=//CHAR[contains(text(),'기타')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Left=3000 and //PARASHAPE[@Id=//CHAR[contains(text(),'기타')]/ancestor::P/following-sibling::P[1]/@ParaShape]/PARAMARGIN/@Indent=-2400)"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE/FONTID/@Hangul][@Name='바탕']"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Name='바탕']/@Id"},{"kind":2,"language":"xpath","value":"//CHARSHAPE[@Id='6']/FONTID/@Hangul"},{"kind":2,"language":"xpath","value":"//PARASHAPE[@Id='0']/@Align"},{"kind":2,"language":"xpath","value":"boolean(//RECTANGLE[.//CHAR[text()='지']][.//SIZE[(@Height >= 2600 and @Height <= 2800)and(@Width >= 2600 and @Width <= 2800)]])"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE[@Id=//TEXT[CHAR[text()='지']]/@CharShape]/FONTID/@Hangul]/@Name"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id=//CHARSHAPE[@Id={charshape_id}]/FONTID/@Hangul]/@Name"},{"kind":2,"language":"xpath","value":"//FONTFACE[@Lang='Hangul']/FONT[@Id={font_id}]/@Name"},{"kind":2,"language":"xpath","value":"//CHARSHAPE[@Id=//CHAR[contains(text()[1],'전 세계적으로 차량의 수는 약 13억 대가 있고 국내는 약 2,500만 대')]/parent::TEXT/@CharShape][ITALIC]"},{"kind":2,"language":"xpath","value":"//PARASHAPE[@Id=//SECTION[1]/P/@ParaShape]/PARAMARGIN/@LineSpacing"},{"kind":2,"language":"xpath","value":"not(//PARASHAPE[@Id=//SECTION[1]/P/@ParaShape]/PARAMARGIN[@LineSpacing!='180'])"}]

View File

@@ -523,7 +523,7 @@
<PARABORDER BorderFill="2" Connect="false" IgnoreMargin="false"/> <PARABORDER BorderFill="2" Connect="false" IgnoreMargin="false"/>
</PARASHAPE> </PARASHAPE>
<PARASHAPE Align="Justify" AutoSpaceEAsianEng="false" AutoSpaceEAsianNum="false" BreakLatinWord="KeepWord" BreakNonLatinWord="true" Condense="0" FontLineHeight="false" HeadingType="None" Id="15" KeepLines="false" KeepWithNext="false" Level="0" LineWrap="Break" PageBreakBefore="false" SnapToGrid="true" TabDef="0" VerAlign="Baseline" WidowOrphan="false"> <PARASHAPE Align="Justify" AutoSpaceEAsianEng="false" AutoSpaceEAsianNum="false" BreakLatinWord="KeepWord" BreakNonLatinWord="true" Condense="0" FontLineHeight="false" HeadingType="None" Id="15" KeepLines="false" KeepWithNext="false" Level="0" LineWrap="Break" PageBreakBefore="false" SnapToGrid="true" TabDef="0" VerAlign="Baseline" WidowOrphan="false">
<PARAMARGIN Indent="0" Left="0" LineSpacing="180" LineSpacingType="Percent" Next="0" Prev="0" Right="0"/> <PARAMARGIN Indent="0" Left="0" LineSpacing="170" LineSpacingType="Percent" Next="0" Prev="0" Right="0"/>
<PARABORDER BorderFill="2" Connect="false" IgnoreMargin="false"/> <PARABORDER BorderFill="2" Connect="false" IgnoreMargin="false"/>
</PARASHAPE> </PARASHAPE>
<PARASHAPE Align="Center" AutoSpaceEAsianEng="false" AutoSpaceEAsianNum="false" BreakLatinWord="KeepWord" BreakNonLatinWord="false" Condense="0" FontLineHeight="false" HeadingType="None" Id="16" KeepLines="false" KeepWithNext="false" Level="0" LineWrap="Break" PageBreakBefore="false" SnapToGrid="true" TabDef="0" VerAlign="Baseline" WidowOrphan="false"> <PARASHAPE Align="Center" AutoSpaceEAsianEng="false" AutoSpaceEAsianNum="false" BreakLatinWord="KeepWord" BreakNonLatinWord="false" Condense="0" FontLineHeight="false" HeadingType="None" Id="16" KeepLines="false" KeepWithNext="false" Level="0" LineWrap="Break" PageBreakBefore="false" SnapToGrid="true" TabDef="0" VerAlign="Baseline" WidowOrphan="false">