2507회 채점자료 추가

This commit is contained in:
2025-08-01 17:28:24 +09:00
parent 0f961fd027
commit 1e4a625d85
11 changed files with 430 additions and 17 deletions

6
.gitignore vendored
View File

@@ -1,6 +1,12 @@
.venv/ .venv/
# 기본적으로 output 디렉토리 전체 무시
output/ output/
# 예외 처리: 2단계 하위 폴더의 .xlsx 파일은 추적
!output/*/
!output/*/*.xlsx
sample/ sample/
.DS_Store .DS_Store

View File

@@ -44,8 +44,8 @@ def copy_ent_files(source_root, target_root):
print(f"Copied {source_file_path} to {target_file_path}") print(f"Copied {source_file_path} to {target_file_path}")
# 사용법 # 사용법
source_directory = r"D:\project\data\CAS_제2506회 정기\답안파일\제2506회 코딩활용능력 2급 답안파일" # 원본 디렉토리 경로 source_directory = r"D:\project\data\CAT_제2507회 정기\채점의뢰" # 원본 디렉토리 경로
target_directory = r"./ent/2506_CAS_2_A" target_directory = r"./ent/2507_CAT_3_A"
target_directory_a = r"./output/A" # '1교시'의 타겟 경로 target_directory_a = r"./output/A" # '1교시'의 타겟 경로
target_directory_b = r"./output/B" # '2교시'의 타겟 경로 target_directory_b = r"./output/B" # '2교시'의 타겟 경로
target_directory_c = r"./output/C" # '3교시'의 타겟 경로 target_directory_c = r"./output/C" # '3교시'의 타겟 경로

View File

@@ -50,7 +50,7 @@ def process_ent_files(ent_dir, output_dir):
# 실행 예 # 실행 예
if __name__ == "__main__": if __name__ == "__main__":
test_name = "2506_CAS_2_A" test_name = "2507_CAT_3_A"
ent_dir = f".\\ent\\{test_name}" ent_dir = f".\\ent\\{test_name}"
output_dir = f".\\output\\{test_name}" output_dir = f".\\output\\{test_name}"
process_ent_files(ent_dir, output_dir) process_ent_files(ent_dir, output_dir)

Binary file not shown.

362
correct/2507_CAT_3_A.json Normal file
View File

@@ -0,0 +1,362 @@
{
"1-1": {
"type": "scene",
"ele": "$..objects[?(@.name=='실험실')]",
"points": 2,
"desc": "문제 1/교실 뒤(3)/[배경] 이름 설정/이름을 '실험실'으로 변경하기"
},
"1-2": {
"type": "scene",
"ele": "$..objects[?(@.name=='새로운 약')]",
"points": 2,
"desc": "문제 1/물약(빨강)/[개체] 이름 설정 1/이름을 '새로운 약'으로 변경하기"
},
"1-4": {
"type": "scene",
"ele": "$..objects[?(@.name=='불')]",
"points": 2,
"desc": "문제 1/불(2)/[개체] 이름 설정 2/이름을 '불'로 변경하기"
},
"1-5": {
"type": "scene",
"ele": "$..objects[?(@.name=='마법사')]",
"points": 2,
"desc": "문제 1/꼬마 마법사/[개체] 이름 설정 3/이름을 '마법사'로 변경하기"
},
"1-6": {
"type": "scene",
"ele": "$..objects[?(@.name=='마법의 약')]",
"points": 2,
"desc": "문제 1/마법의 약/[개체] 이름 설정 4/이름 변경 없음"
},
"2-0": {
"type": "script",
"ele": "$.objects[?(@.name=~'새로운 약|물약')].script",
"blocks": [
{
"ele": "$[0][0].type",
"answer": "when_run_button_click",
"points": 2.43,
"desc": "문제 2/새로운 약/시작/시작하기 버튼을 클릭했을 때"
},
{
"ele": [
"$[0][1].type",
"$[0][1].params[0].params[0]",
"$[0][1].params[1].params[0]"
],
"answer": ["locate_xy", "50", "5"],
"points": 2.43,
"desc": "문제 2/새로운 약/[시작]의 세부 동작 1/x: '50' y: '5' 위치로 이동하기",
"type": "list"
},
{
"ele": "$[0][2].params[0].params[0]",
"answer": "60",
"points": 2.43,
"desc": "문제 2/새로운 약/[시작]의 세부 동작 2/크기를 '60' 으로 정하기"
},
{
"ele": "$[1][0].type",
"answer": "when_object_click",
"points": 2.43,
"desc": "문제 2/새로운 약/오브젝트/오브젝트를 클릭했을 때"
},
{
"ele": ["$[1][1].type", "$[1][1].params[0]"],
"answer": ["repeat_inf", null],
"points": 2.43,
"desc": "문제 2/새로운 약/반복/계속 반복하기",
"type": "list"
},
{
"ele": "$[1][1].statements[0][0].type",
"answer": "locate",
"points": 2.43,
"desc": "문제 2/새로운 약/[반복]의 세부 동작/'마우스 포인터' 위치로 이동하기"
}
]
},
"3-0": {
"type": "script",
"ele": "$.objects[?(@.name=~'불')].script",
"blocks": [
{
"ele": "$[0][0].type",
"answer": "when_run_button_click",
"points": 2.43,
"desc": "문제 2/불/시작/시작하기 버튼을 클릭했을 때"
},
{
"ele": [
"$[0][1].type",
"$[0][1].params[0].params[0]",
"$[0][1].params[1].params[0]"
],
"answer": ["locate_xy", "-150", "-30"],
"points": 2.43,
"desc": "문제 2/불/[시작]의 세부 동작 1/x: '-150' y: '-30' 위치로 이동하기",
"type": "list"
},
{
"ele": "$[0][2].params[0].params[0]",
"answer": "90",
"points": 2.43,
"desc": "문제 2/불/[시작]의 세부 동작 2/크기를 '90' 으로 정하기"
},
{
"ele": ["$[0][3].type", "$[0][3].params[0]"],
"answer": ["repeat_inf", null],
"points": 2.43,
"desc": "문제 2/불/반복/계속 반복하기",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][0].type",
"$[0][3].statements[0][0].params[0].params[1].params[0]",
"$[0][3].statements[0][0].params[0].params[3].params[0]",
"$[0][3].statements[0][0].params[1].params[1].params[0]",
"$[0][3].statements[0][0].params[1].params[3].params[0]"
],
"answer": ["locate_xy", "-150", "0", "-10", "40"],
"points": 2.43,
"desc": "문제 2/불/[반복]의 세부 동작 1/x: '-150 부터 0 사이의 무작위 수' y: '-10 부터 40 사이의 무작위 수' 위치로 이동하기",
"type": "list"
},
{
"ele": "$[0][3].statements[0][1].type",
"answer": "hide",
"points": 2.43,
"desc": "문제 2/불/[반복]의 세부 동작 2/모양 숨기기"
},
{
"ele": [
"$[0][3].statements[0][2].type",
"$[0][3].statements[0][2].params[0].type",
"$[0][3].statements[0][2].params[0].params[1].params[0]",
"$[0][3].statements[0][2].params[0].params[3].params[0]"
],
"answer": ["wait_second", "calc_rand", "1", "2"],
"points": 2.43,
"desc": "문제 2/불/[반복]의 세부 동작 3/'1 부터 2 사이의 무작위 수' 초 기다리기",
"type": "list"
},
{
"ele": "$[0][3].statements[0][3].type",
"answer": "show",
"points": 2.43,
"desc": "문제 2/불/[반복]의 세부 동작 4/모양 보이기"
},
{
"ele": [
"$[0][3].statements[0][4].type",
"$[0][3].statements[0][4].params[0].params[0]"
],
"answer": ["wait_second", "0.5"],
"points": 2.43,
"desc": "문제 2/불/[반복]의 세부 동작 5/'0.5' 초 기다리기",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][5].type",
"$[0][3].statements[0][5].params[0].type"
],
"answer": ["_if", "reach_something"],
"points": 2.43,
"desc": "문제 2/불/만일/만일 '새로운 약' 에 닿았는가? 라면",
"type": "list"
},
{
"ele": "$[0][3].statements[0][5].statements[0][0].params[0].params[0]",
"answer": "300",
"points": 2.43,
"desc": "문제 2/불/[만일]의 세부 동작 1/크기를 '300' 으로 정하기"
},
{
"ele": [
"$[0][3].statements[0][5].statements[0][1].type",
"$[0][3].statements[0][5].statements[0][1].params[0].params[0]",
"$[0][3].statements[0][5].statements[0][1].params[1]"
],
"answer": ["dialog", "마법 실패", "speak"],
"points": 2.43,
"desc": "문제 2/불/[만일]의 세부 동작 2/'마법 실패' 를 '말하기'",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][5].statements[0][2].type",
"$[0][3].statements[0][5].statements[0][2].params[0]"
],
"answer": ["stop_object", "all"],
"points": 2.43,
"desc": "문제 2/불/[만일]의 세부 동작 3/'모든' 코드 멈추기",
"type": "list"
}
]
},
"4-0": {
"type": "script",
"ele": "$.objects[?(@.name=~'마법사')].script",
"blocks": [
{
"ele": "$[0][0].type",
"answer": "when_run_button_click",
"points": 2.43,
"desc": "문제 2/마법사/시작/시작하기 버튼을 클릭했을 때"
},
{
"ele": [
"$[0][1].type",
"$[0][1].params[0].params[0]",
"$[0][1].params[1].params[0]"
],
"answer": ["locate_xy", "135", "-20"],
"points": 2.43,
"desc": "문제 2/마법사/[시작]의 세부 동작 1/x: '135' y: '-20' 위치로 이동하기",
"type": "list"
},
{
"ele": "$[0][2].params[0].params[0]",
"answer": "180",
"points": 2.43,
"desc": "문제 2/마법사/[시작]의 세부 동작 2/크기를 '180' 으로 정하기"
},
{
"ele": "$[0][3].params[*].params",
"answer": ["마법 실험을 해봐야지!", "2"],
"points": 2.43,
"desc": "문제 2/마법사/[시작]의 세부 동작 3/'마법 실험을 해봐야지!' 를 '2' 초 동안 '말하기'"
}
]
},
"5-0": {
"type": "script",
"ele": "$.objects[?(@.name=~'마법의 약')].script",
"blocks": [
{
"ele": "$[0][0].type",
"answer": "when_run_button_click",
"points": 2.43,
"desc": "문제 2/마법의 약/시작/시작하기 버튼을 클릭했을 때"
},
{
"ele": [
"$[0][1].type",
"$[0][1].params[0].params[0]",
"$[0][1].params[1].params[0]"
],
"answer": ["locate_xy", "-40", "10"],
"points": 2.43,
"desc": "문제 2/마법의 약/[시작]의 세부 동작 1/x: '-40' y: '10' 위치로 이동하기",
"type": "list"
},
{
"ele": "$[0][2].params[0].params[0]",
"answer": "50",
"points": 2.43,
"desc": "문제 2/마법의 약/[시작]의 세부 동작 2/크기를 '50' 으로 정하기"
},
{
"ele": ["$[0][3].type", "$[0][3].params[0]"],
"answer": ["repeat_inf", null],
"points": 2.43,
"desc": "문제 2/마법의 약/반복/계속 반복하기",
"type": "list"
},
{
"ele": "$[0][3].statements[0][0].type",
"answer": "hide",
"points": 2.43,
"desc": "문제 2/마법의 약/[반복]의 세부 동작 1/모양 숨기기"
},
{
"ele": [
"$[0][3].statements[0][1].type",
"$[0][3].statements[0][1].params[0].params[1].params[0]",
"$[0][3].statements[0][1].params[0].params[3].params[0]"
],
"answer": ["locate_x", "-160", "60"],
"points": 2.43,
"desc": "문제 2/마법의 약/[반복]의 세부 동작 2/x: '-160 부터 60 사이의 무작위 수' 위치로 이동하기",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][2].type",
"$[0][3].statements[0][2].params[0].params[0]"
],
"answer": ["wait_second", "2"],
"points": 2.43,
"desc": "문제 2/마법의 약/[반복]의 세부 동작 3/ '2' 초 기다리기",
"type": "list"
},
{
"ele": "$[0][3].statements[0][3].type",
"answer": "show",
"points": 2.43,
"desc": "문제 2/마법의 약/[반복]의 세부 동작 4/모양 보이기"
},
{
"ele": [
"$[0][3].statements[0][4].type",
"$[0][3].statements[0][4].params[0].params[0]"
],
"answer": ["wait_second", "0.5"],
"points": 2.43,
"desc": "문제 2/마법의 약/[반복]의 세부 동작 5/'0.5' 초 기다리기",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][5].type",
"$[0][3].statements[0][5].params[0].type"
],
"answer": ["_if", "reach_something"],
"points": 2.43,
"desc": "문제 2/마법의 약/만일/만일 '새로운 약' 에 닿았는가? 라면",
"type": "list"
},
{
"ele": "$[0][3].statements[0][5].statements[0][0].params[0].params[0]",
"answer": "100",
"points": 2.43,
"desc": "문제 2/마법의 약/[만일]의 세부 동작 1/크기를 '100' 으로 정하기"
},
{
"ele": [
"$[0][3].statements[0][5].statements[0][1].type",
"$[0][3].statements[0][5].statements[0][1].params[0]",
"$[0][3].statements[0][5].statements[0][1].params[1].params[0]"
],
"answer": ["add_effect_amount", "color", "70"],
"points": 2.43,
"desc": "문제 2/마법의 약/[만일]의 세부 동작 2/'색깔' 효과를 '70' 만큼 주기",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][5].statements[0][2].type",
"$[0][3].statements[0][5].statements[0][2].params[0].params[0]",
"$[0][3].statements[0][5].statements[0][2].params[1]"
],
"answer": ["dialog", "마법 성공", "speak"],
"points": 2.43,
"desc": "문제 2/마법의 약/[만일]의 세부 동작 3/'마법 성공' 을 '말하기'",
"type": "list"
},
{
"ele": [
"$[0][3].statements[0][5].statements[0][3].type",
"$[0][3].statements[0][5].statements[0][3].params[0]"
],
"answer": ["stop_object", "all"],
"points": 2.43,
"desc": "문제 2/마법의 약/[만일]의 세부 동작 4/'모든' 코드 멈추기",
"type": "list"
}
]
}
}

24
logging_config.py Normal file
View File

@@ -0,0 +1,24 @@
import logging
import os
def setup_logging():
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "cat.log")
log_format = "[%(asctime)s] [%(levelname)s] [%(module)s:%(lineno)d] %(message)s"
date_format = "%Y-%m-%d %H:%M:%S"
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(logging.Formatter(log_format, date_format))
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(log_format, date_format))
logger.addHandler(console_handler)
logger.addHandler(file_handler)

12
logs/cat.log Normal file
View File

@@ -0,0 +1,12 @@
[2025-07-31 15:50:20] [ERROR] [main:381] Error processing ./output/2507_CAT_3_A/2507회코딩활용능력3급A형정답\project.json: cannot access local variable 'student_id' where it is not associated with a value
Traceback (most recent call last):
File "D:\project\Entry\Entry-Scoring\main.py", line 375, in main
points.insert(0, student_id)
^^^^^^^^^^
UnboundLocalError: cannot access local variable 'student_id' where it is not associated with a value
[2025-07-31 15:52:50] [ERROR] [main:380] 🚫Error processing ./output/2507_CAT_3_A/2507회코딩활용능력3급A형정답\project.json: cannot access local variable 'student_id' where it is not associated with a value
Traceback (most recent call last):
File "D:\project\Entry\Entry-Scoring\main.py", line 375, in main
points.insert(0, student_id)
^^^^^^^^^^
UnboundLocalError: cannot access local variable 'student_id' where it is not associated with a value

37
main.py
View File

@@ -6,6 +6,11 @@ import pandas as pd # 추가된 import
import unicodedata # 상단에 import 추가 import unicodedata # 상단에 import 추가
import re # 상단에 추가 import re # 상단에 추가
from datetime import datetime from datetime import datetime
import logging
from logging_config import setup_logging # logging 설정을 위한 import
import traceback
setup_logging() # logging 설정 호출
# JSON 파일 읽기 # JSON 파일 읽기
def read_json(file_path): def read_json(file_path):
@@ -262,14 +267,11 @@ def process_project(project_data, scoring_data):
script_data_1 = json.loads(script_raw) if script_raw else None script_data_1 = json.loads(script_raw) if script_raw else None
script_data = swap_script(script_data_1) if script_data_1 else None script_data = swap_script(script_data_1) if script_data_1 else None
if script_data != script_data_1:
print(f"⬜ Script data 순서 변경 ")
block_index = 1 block_index = 1
for block in block_list: for block in block_list:
block_type = block.get('type') block_type = block.get('type')
block_path = block.get('ele') block_path = block.get('ele')
block_expected_answer = block.get('answer', None) block_answer = block.get('answer', None)
block_points = block.get('points') block_points = block.get('points')
if script_data is None: if script_data is None:
@@ -285,23 +287,23 @@ def process_project(project_data, scoring_data):
block_elements = find_element(script_data, block_path) block_elements = find_element(script_data, block_path)
# 결과값 정리 # 결과값 정리
if block_elements and isinstance(block_expected_answer, list): if block_elements and isinstance(block_answer, list):
found_values = [convert_to_str(x) for x in chain.from_iterable(block_elements)] found_values = [convert_to_str(x) for x in chain.from_iterable(block_elements)]
else: else:
found_values = convert_to_str(block_elements[0]) if block_elements else None found_values = convert_to_str(block_elements[0]) if block_elements else None
expected_str = convert_to_str(block_expected_answer) if block_expected_answer is not None else None expected_str = convert_to_str(block_answer) if block_answer is not None else None
# 비교 및 점수 처리 # 비교 및 점수 처리
if block_elements: if block_elements:
if block_expected_answer is not None and expected_str != found_values: if block_answer is not None and expected_str != found_values:
print(f"{question_key}-{block_index}: ❌ {expected_str} != {found_values}") print(f"{question_key}-{block_index}: ❌ {expected_str} != {found_values}")
score_list.append(0) score_list.append(0)
elif block_expected_answer is not None and expected_str == found_values: elif block_answer is not None and expected_str == found_values:
print(f"{question_key}-{block_index}: ✅ {expected_str} == {found_values}") print(f"{question_key}-{block_index}: ✅ {expected_str} == {found_values}")
total_points += block_points total_points += block_points
score_list.append(block_points) score_list.append(block_points)
elif block_expected_answer is None: elif block_answer is None:
total_points += block_points total_points += block_points
score_list.append(block_points) score_list.append(block_points)
print(f"{question_key}-{block_index}: Element Exists") print(f"{question_key}-{block_index}: Element Exists")
@@ -311,6 +313,7 @@ def process_project(project_data, scoring_data):
block_index += 1 block_index += 1
# total_points = round(total_points, ndigits=0) # 총점 반올림
score_list.append(total_points) score_list.append(total_points)
return score_list return score_list
@@ -321,9 +324,9 @@ def normalize_path(path):
def main(): def main():
# 파일 경로 설정 # 파일 경로 설정
# project_json_path = './output/2506_CAS_2_A/' project_json_path = './output/2507_CAT_3_A/'
project_json_path = './output/00_test/' # project_json_path = './output/00_test/'
scoring_json_path = './correct/2506_CAS_2_A.json' scoring_json_path = './correct/2507_CAT_3_A.json'
scoring_data = read_json(scoring_json_path) scoring_data = read_json(scoring_json_path)
student_score_list = [] student_score_list = []
@@ -363,9 +366,14 @@ def main():
# 디렉토리 패스 내에 학생 이름만 뽑아서 엑셀 컬럼 명으로 추가 # 디렉토리 패스 내에 학생 이름만 뽑아서 엑셀 컬럼 명으로 추가
# output/cas-000040-이지원/temp/project.json # output/cas-000040-이지원/temp/project.json
# student_id = normalize_path(full_path.split('/')[3]) # student_id = normalize_path(full_path.split('/')[3])
match = re.search(r'(\d{6}-[^\\/]+)[\\/]', full_path) match = re.search(r'(\d{6}[-_][^\\/]+)[\\/]', full_path)
if match: if match:
student_id = match.group(1) student_id = match.group(1)
else:
if '정답' in full_path:
student_id = '정답'
else:
student_id = '000000'
# project.json 파일 내용 # project.json 파일 내용
project_data = read_json(full_path) project_data = read_json(full_path)
@@ -374,7 +382,8 @@ def main():
student_score_list.append(points) student_score_list.append(points)
print(f"Total Points for {points}") print(f"Total Points for {points}")
except Exception as e: except Exception as e:
print(f"Error processing {full_path}: {str(e)}") # print(traceback.format_exc())
logging.exception(f"🚫Error processing {full_path}: {str(e)}")
continue continue
# DataFrame 생성 및 엑셀 저장 # DataFrame 생성 및 엑셀 저장