Files
pbl02/scripts/convert_md_to_json.py
2026-10-08 16:43:48 +09:00

694 lines
32 KiB
Python

import os
import re
import sys
import json
import glob
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
def parse_markdown_table(text):
"""Parse markdown tables from text and return a list of rows with robust multi-line cell support."""
lines = text.splitlines()
merged_lines = []
current_line = ''
for l in lines:
stripped = l.strip()
if not stripped:
continue
if re.match(r'^\|[\s\-:|]+\|$', stripped):
continue
if stripped.startswith('|'):
if current_line:
merged_lines.append(current_line)
current_line = stripped
else:
if current_line:
current_line += ' ' + stripped
else:
current_line = stripped
if current_line:
merged_lines.append(current_line)
rows = []
for l in merged_lines:
if not l.startswith('|'):
continue
parts = l.split('|')
if parts and parts[0].strip() == '':
parts = parts[1:]
if parts and parts[-1].strip() == '':
parts = parts[:-1]
cells = [p.strip().replace('<br>', '\n') for p in parts]
if any(cells):
rows.append(cells)
return rows
def clean_html_tags(s):
"""Clean markdown formatting for plaintext usage."""
s = re.sub(r'\*\*(.*?)\*\*', r'\1', s)
s = re.sub(r'\*(.*?)\*', r'\1', s)
s = re.sub(r'`(.*?)`', r'\1', s)
return s.strip()
def load_metadata():
"""Load metadata from projectlist02.md and missions02.md."""
projects = {}
with open('data/projectlist02.md', 'r', encoding='utf-8') as f:
rows = parse_markdown_table(f.read())
if rows:
headers = rows[0]
for r in rows[1:]:
if len(r) >= 11:
pid = r[2]
projects[pid] = dict(zip(headers, r))
missions = {}
with open('data/missions02.md', 'r', encoding='utf-8') as f:
rows = parse_markdown_table(f.read())
if rows:
headers = rows[0]
for r in rows[1:]:
if len(r) >= 22:
pid = r[1]
try:
seq = int(r[8])
except ValueError:
seq = 1
mid = r[9]
missions[(pid, seq)] = dict(zip(headers, r))
return projects, missions
def parse_scenario_md(file_path):
"""Parse project main scenario md file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
data = {
'title': '',
'difficulty': '',
'target': '',
'precautions': '',
'missions': {}
}
# Extract Title and Difficulty
lines = content.splitlines()
for l in lines[:10]:
clean_l = clean_html_tags(l)
if clean_l and not data['title'] and '난이도' not in clean_l and not clean_l.startswith('|'):
data['title'] = clean_l
if '난이도' in clean_l or '난 이 도' in clean_l:
data['difficulty'] = clean_l.replace('난이도', '').replace('난 이 도', '').strip()
# Extract Project Target and Precautions
# Split tables by empty lines / double newlines
raw_tables = re.split(r'\n\s*\n', content)
for tbl_text in raw_tables:
if '프로젝트 목표' in tbl_text or '과제 목표' in tbl_text:
rows = parse_markdown_table(tbl_text)
for r in rows:
if len(r) >= 2 and ('목표' in r[0] or '과제' in r[0]):
data['target'] = r[1].strip()
if '유의사항' in tbl_text or '주의사항' in tbl_text:
rows = parse_markdown_table(tbl_text)
for r in rows:
if len(r) >= 2 and ('유의' in r[0] or '주의' in r[0]):
data['precautions'] = r[1].strip()
# Find all mission tables
# Mission tables typically start with "| **미션 X." or have "미션 X" in header cell
for tbl_text in raw_tables:
if not tbl_text.strip().startswith('|'):
continue
rows = parse_markdown_table(tbl_text)
if not rows:
continue
header_text = " ".join(rows[0])
m_match = re.search(r'미션\s*(\d+)', header_text)
if not m_match:
# check if any cell mentions mission number
for r in rows:
m_match = re.search(r'미션\s*(\d+)', " ".join(r))
if m_match:
break
if m_match:
seq = int(m_match.group(1))
m_info = {'title': '', 'target': '', 'exec': '', 'deliverable': '', 'eval': ''}
for r in rows:
if len(r) >= 2:
k = clean_html_tags(r[0])
v = r[1].strip()
if '목표' in k:
m_info['target'] = v
elif '수행' in k or '내용' in k:
m_info['exec'] = v
elif '산출물' in k:
m_info['deliverable'] = v
elif '평가' in k:
m_info['eval'] = v
elif len(r) == 1 and '미션' in r[0]:
m_info['title'] = clean_html_tags(r[0])
data['missions'][seq] = m_info
return data
def parse_guide_md(file_path):
"""Parse project guide md file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
data = {
'overview': '',
'prep': '',
'steps_summary': [],
'missions': {}
}
# Overview
m_ov = re.search(r'\*{2}1\.\s*실습 개요\*{2}(.*?)(?=\*{2}2\.|\n#|\Z)', content, re.DOTALL)
if m_ov:
data['overview'] = m_ov.group(1).strip()
# Split by Mission sections
m_sections = re.split(r'\n(?=\*{0,2}미션\s*\d+)', content)
for sec in m_sections:
m_head = re.search(r'미션\s*(\d+)\.?\s*([^\n\r]+)', sec)
if m_head:
seq = int(m_head.group(1))
m_title = clean_html_tags(m_head.group(2)).strip()
rows = parse_markdown_table(sec)
m_detail = {
'title': m_title,
'difficulty_time': '',
'steps': '',
'expected': '',
'challenge': '',
'deliverables': ''
}
for r in rows:
if len(r) >= 2:
k = clean_html_tags(r[0])
v = r[1].strip()
if '난이도' in k or '시간' in k:
m_detail['difficulty_time'] = v
elif '진행' in k or '순서' in k or '활동' in k:
m_detail['steps'] = v
elif '확인' in k or '결과' in k or '지표' in k:
m_detail['expected'] = v
elif '도전' in k or '과제' in k:
m_detail['challenge'] = v
elif '제출' in k or '산출물' in k:
m_detail['deliverables'] = v
data['missions'][seq] = m_detail
return data
def format_exec_action_phrase(exec_text, m_title):
"""Refine raw objectives text into a concise, natural Korean action phrase (no copy-paste)."""
clean = clean_html_tags(exec_text).strip()
sentences = [s.strip().rstrip('.') for s in re.split(r'\.\s+', clean) if s.strip()]
if not sentences:
return f"{m_title} 관련 핵심 기능을 설계하고 검증하는"
first = sentences[0]
# Replace common terminal patterns into natural modifying clauses or noun phrases
phrase = re.sub(r'(?:을\s*작성한다|를\s*작성한다|작성한다)$', ' 설계하고 작성하는', first)
phrase = re.sub(r'(?:을\s*구현한다|를\s*구현한다|구현한다)$', ' 구현하고 동작을 검증하는', phrase)
phrase = re.sub(r'(?:을\s*설계한다|를\s*설계한다|설계한다)$', ' 설계하고 워크플로우를 구성하는', phrase)
phrase = re.sub(r'(?:을\s*분석한다|를\s*분석한다|분석한다)$', ' 분석하고 원인을 규명하는', phrase)
phrase = re.sub(r'(?:을\s*평가한다|를\s*평가한다|평가한다)$', ' 평가하고 성능을 비교하는', phrase)
phrase = re.sub(r'(?:을\s*통합한다|를\s*통합한다|통합한다)$', ' 파이프라인으로 통합하고 브리핑을 완성하는', phrase)
phrase = re.sub(r'(?:을\s*계산한다|를\s*계산한다|계산한다)$', ' 산출하고 모델에 적용하는', phrase)
phrase = re.sub(r'(?:을\s*확인한다|를\s*확인한다|확인한다)$', ' 확인하고 유효성을 점검하는', phrase)
phrase = re.sub(r'(?:을\s*수행한다|를\s*수행한다|수행한다)$', ' 수행하고 분석하는', phrase)
phrase = re.sub(r'(?:을\s*정리한다|를\s*정리한다|정리한다)$', ' 정리하고 규격화하는', phrase)
phrase = re.sub(r'(?:을\s*정의한다|를\s*정의한다|정의한다)$', ' 정의하고 검증하는', phrase)
phrase = re.sub(r'(?:을\s*도출한다|를\s*도출한다|도출한다)$', ' 도출하고 검증하는', phrase)
phrase = re.sub(r'(?:을\s*구축한다|를\s*구축한다|구축한다)$', ' 구축하고 안정성을 검증하는', phrase)
phrase = re.sub(r'(?:을\s*조합한다|를\s*조합한다|조합한다)$', ' 조합하여 최적화하는', phrase)
phrase = re.sub(r'(?:을\s*판정한다|를\s*판정한다|판정한다)$', ' 판정하고 정확도를 측정하는', phrase)
phrase = re.sub(r'(?:을\s*학습하고|를\s*학습하고|학습하고)\s*,?\s*(?:점수에\s*따라\s*)?.*(?:부여한다)$', ' 학습하여 등급별 신뢰도를 판정하는', phrase)
phrase = re.sub(r'(?:을\s*주입한다|를\s*주입한다|주입한다)$', ' 주입하여 처리 지연 및 복원력을 시험하는', phrase)
phrase = re.sub(r'(?:을\s*만든다|를\s*만든다|만든다)$', ' 작성하여 원인을 분석하는', phrase)
phrase = re.sub(r'한다$', ' 수행하고 검증하는', phrase)
# If the sentence didn't end with "는", append " 핵심 과제를 진행하는"
if not phrase.endswith('는'):
phrase += ' 과정을 진행하는'
phrase = re.sub(r'\s+', ' ', phrase).strip().rstrip('.')
return phrase
def build_guide_steps(seq, m_meta, m_scen, m_guide):
"""Build 1~5 detailed guide steps for a mission referencing _실습가이드.md."""
raw_steps = m_guide.get('steps', '') if m_guide else ''
# Split by 1), 2), 3), 1., 2. or newlines
step_items = []
if raw_steps:
step_items = [s.strip() for s in re.split(r'(?:\n|^|\s+)\d+[\)\.]\s*|(?:\n|^)\s*[\-\*]\s*', raw_steps) if s.strip()]
if not step_items:
step_items = [s.strip() for s in raw_steps.splitlines() if s.strip()]
if not step_items:
m_exec = m_scen.get('exec', '') if m_scen else ''
if m_exec:
step_items = [s.strip() for s in re.split(r'\.\s+', m_exec) if len(s.strip()) > 10]
else:
step_items = [m_meta.get('미션 설명', f'미션 {seq} 실습을 수행한다.')]
expected_text = m_guide.get('expected', '') if m_guide else ''
if not expected_text:
expected_text = m_meta.get('정량적 평가', '정량 기준치 100% 충족')
challenge_text = m_guide.get('challenge', '') if m_guide else ''
if not challenge_text:
challenge_text = m_meta.get('개선 및 피드백 방안', '파라미터 튜닝 및 최적화')
guide_steps = []
for idx, s_text in enumerate(step_items[:4], start=1):
clean_text = clean_html_tags(s_text)
problem_title = f"{clean_text[:50]}..." if len(clean_text) > 50 else clean_text
if not problem_title:
problem_title = f"단계 {idx}. 세부 기능 구현 및 실행 검증"
activity_html = (
f"<h4>이 단계의 역할</h4>"
f"<p>{clean_text}</p>"
f"<h4>해야 할 일</h4>"
f"<ol>"
f"<li><p>{clean_text}에 필요한 데이터셋과 입력 파라미터를 준비한다.</p></li>"
f"<li><p>정답 코드 및 모듈을 실행하여 중간 출력 결과를 도출한다.</p></li>"
f"<li><p>출력된 지표와 로그가 기준 조건을 만족하는지 대조·검증한다.</p></li>"
f"</ol>"
f"<h4>핵심 확인 지표</h4>"
f"<blockquote><p>{clean_html_tags(expected_text)}</p></blockquote>"
f"<h4>학습자가 제출해야 하는 내용</h4>"
f"<ul><li><p>{clean_text} 실행 결과 및 검증 로그</p></li></ul>"
)
eval_html = (
f"<h4>PASS 기준</h4>"
f"<ul>"
f"<li><p>{clean_text} 과정이 정상적으로 수행되고 오류 없이 완료되었다.</p></li>"
f"<li><p>요구된 핵심 성능 지표({clean_html_tags(expected_text)})를 충족하였다.</p></li>"
f"</ul>"
f"<h4>보완이 필요한 경우</h4>"
f"<ul><li><p>실행 중 오류가 발생했거나 필수 검증 지표가 누락된 경우 재실행하여 보완한다.</p></li></ul>"
)
guide_steps.append({
"step": idx,
"problem": problem_title,
"activity": activity_html,
"evalText": eval_html,
"attachments": []
})
# Add challenge step if available
if challenge_text and len(guide_steps) < 5:
idx = len(guide_steps) + 1
guide_steps.append({
"step": idx,
"problem": "도전 과제 및 파라미터 튜닝 최적화",
"activity": (
f"<h4>이 단계의 역할</h4>"
f"<p>기본 파이프라인 수행 후 심화 튜닝 및 조건 변경을 통해 최적화 역량을 다진다.</p>"
f"<h4>해야 할 일</h4>"
f"<p>{clean_html_tags(challenge_text)}</p>"
f"<h4>학습자가 제출해야 하는 내용</h4>"
f"<ul><li><p>파라미터 변경 전후 비교 분석 메모 및 최종 최적화 결과</p></li></ul>"
),
"evalText": (
f"<h4>PASS 기준</h4>"
f"<ul><li><p>도전 과제 변경 사항이 논리적으로 타당하며 결과 변화 이유를 명확히 기술하였다.</p></li></ul>"
),
"attachments": []
})
return guide_steps
def build_project_json(pid, p_meta, scen_data, guide_data, missions_meta):
"""Construct standard Project JSON object matching A13.json schema."""
title = p_meta.get('프로젝트명', scen_data.get('title', f'프로젝트 {pid}'))
category = p_meta.get('군 특화/군수 업무 분야', p_meta.get('분야', '군사AI'))
raw_diff = p_meta.get('난이도', '고급')
level_map = {'초급': 1, '중급': 2, '고급': 3, '최고급': 4}
level = level_map.get(raw_diff, 3)
# Calculate required hours
req_hours = 0.0
for seq in range(1, 11):
m_meta = missions_meta.get((pid, seq), {})
try:
req_hours += float(m_meta.get('예상 소요시간(h)', '2.5'))
except ValueError:
req_hours += 2.5
# Target / Objectives mapping (프로젝트 목표 -> objectives)
target_text = scen_data.get('target', '')
if not target_text:
target_text = f"{title}에 대한 군 특화 AI 실무 해결 역량을 학습하는 프로젝트입니다."
objectives_html = f"<p>{target_text}</p>"
# Project Deliverables
p_deliverables = []
for seq in range(1, 11):
m_meta = missions_meta.get((pid, seq), {})
d_raw = m_meta.get('산출물 및 저장 위치', '')
if d_raw:
clean_d = clean_html_tags(d_raw)
for item in clean_d.split(' 및 '):
item = item.strip()
if item and item not in p_deliverables and len(p_deliverables) < 6:
p_deliverables.append(f"{item} (필수)")
if not p_deliverables:
p_deliverables = ["프로젝트 실습 소스코드 및 데이터셋 (필수)", "최종 분석 및 검증 보고서 (필수)"]
# Project Constraints
precautions_text = scen_data.get('precautions', '')
if not precautions_text:
precautions_text = (
"모든 부대·장비·작전 데이터는 교육용 가상 데이터셋을 사용하며 실제 군사 2·3급 비밀 및 개인정보는 입력하지 않는다. "
"AI 분석 결과는 의사결정 보조 수단이며 최종 판정 및 지휘 결심은 지휘관/실무자가 수행한다."
)
constraints_html = (
f"<h4>부대 보안 및 개인정보 보호</h4>"
f"<ul>"
f"<li><p>실제 부대 편성, 위치, 작전 일정, 군번·성명 등 민감 군사보안 및 개인식별정보는 절대 외부 AI에 입력하지 않는다.</p></li>"
f"<li><p>모든 실습에는 제공된 교육용 가상 데이터셋 및 시뮬레이션 표본만을 활용한다.</p></li>"
f"</ul>"
f"<h4>AI 결과와 인간 최종 판단의 구분 (HITL 원칙)</h4>"
f"<ul>"
f"<li><p>AI 모델이 제안한 분석값이나 탐지 결과는 참고 지표이며, 최종 승인·보류·교전·배치 명령은 사람이 확정한다.</p></li>"
f"<li><p>{precautions_text}</p></li>"
f"</ul>"
)
eval_summary_html = (
f"<h4>공통 PASS/FAIL 판정 원칙</h4>"
f"<ul>"
f"<li><p><strong>수행 증거:</strong> 요구된 데이터 전처리, 모델 구현, 검증 지표 산출, HITL 워크플로우 증거가 명확한가?</p></li>"
f"<li><p><strong>판단 근거:</strong> 오경보/미탐 방지, 신뢰도 등급화, 파라미터 튜닝의 군사적·논리적 근거가 타당한가?</p></li>"
f"<li><p><strong>앞뒤 연결성:</strong> 앞 미션의 스키마·전처리가 뒤 미션의 예측·대시보드·AAR 재보정에 정상 연계되는가?</p></li>"
f"<li><p><strong>안전성:</strong> 보안 규정 준수, 극단 과부하 상황에서의 Fail-safe 복원력이 확보되었는가?</p></li>"
f"</ul>"
f"<h4>프로젝트 완료 조건</h4>"
f"<p>10개 미션 전체에서 요구되는 필수([필수]) 평가 기준을 모두 PASS해야 프로젝트 최종 PASS로 판정한다.</p>"
)
# Project Skills
project_skills = [
{
"skillId": f"competency_군특화문제해결_{category}_문제분석",
"level": level,
"category": "군특화문제해결",
"subCategory": f"{category} 문제분석"
},
{
"skillId": f"competency_군특화문제해결_{category}_대안도출",
"level": max(1, level - 1),
"category": "군특화문제해결",
"subCategory": f"{category} 대안도출"
},
{
"skillId": "competency_데이터분석검증_결과검증",
"level": level,
"category": "데이터분석검증",
"subCategory": "AI 결과 검증"
},
{
"skillId": "competency_AI활용역량_도구활용",
"level": max(1, level - 1),
"category": "AI활용역량",
"subCategory": "AI 도구 및 모델 활용"
},
{
"skillId": "competency_전술의사결정_인간검토",
"level": level,
"category": "전술의사결정",
"subCategory": "HITL 인간 검토 및 책임분계"
}
]
# Build 10 Missions
missions_list = []
for seq in range(1, 11):
m_meta = missions_meta.get((pid, seq), {})
m_scen = scen_data.get('missions', {}).get(seq, {})
m_guide = guide_data.get('missions', {}).get(seq, {})
m_id = f"{pid}_M{seq:02d}"
m_title = m_meta.get('미션명', m_guide.get('title', f"미션 {seq}"))
try:
m_duration = float(m_meta.get('예상 소요시간(h)', '2.5'))
except ValueError:
m_duration = 2.5
m_desc = m_meta.get('미션 설명', m_scen.get('target', ''))
m_scen_target = m_scen.get('target', m_desc)
m_scen_exec = m_scen.get('exec', m_desc)
m_quant_eval = m_meta.get('정량적 평가', '')
m_qual_eval = m_meta.get('정성적 평가', '')
m_track = m_meta.get('진행 추적 및 완료 기준', '')
m_verify = m_meta.get('검증 방법', '')
m_feedback = m_meta.get('개선 및 피드백 방안', '')
m_deliverables_raw = m_meta.get('산출물 및 저장 위치', m_scen.get('deliverable', ''))
# Overview 매핑: 미션 - '목표'
m_overview = f"<p>{m_scen_target}</p>"
# Objectives 매핑: 미션 - '수행'
m_objectives = f"<p>{m_scen_exec}</p>"
# Guide Steps (미리 빌드하여 scenario 연계)
g_steps = build_guide_steps(seq, m_meta, m_scen, m_guide)
step_count = len(g_steps)
# Scenario (Why 스토리 2~3문장 + objectives 간략 정리 + 실습 흐름 + 최종 산출물)
desc_clean = clean_html_tags(m_desc).strip()
target_clean = clean_html_tags(m_scen_target).strip()
exec_clean = clean_html_tags(m_scen_exec).strip()
deliv_clean = clean_html_tags(m_deliverables_raw).strip()
if desc_clean and not desc_clean.endswith('.'):
desc_clean += '.'
# objectives 수행 내용을 자연스러운 1개의 완결된 행동 문구로 정제
exec_phrase = format_exec_action_phrase(exec_clean, m_title)
m_scenario = (
f"<h4>미션 배경 및 당위성</h4>"
f"<p>{desc_clean} 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, "
f"<strong>{target_clean}</strong> 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.</p>"
f"<p>이 문제를 해결하기 위해 본 미션에서는 <strong>{exec_phrase}</strong> 실습을 체계적으로 수행합니다.</p>"
f"<p>학습자는 실습가이드에 제시된 총 {step_count}개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.</p>"
f"<p><strong>이번 결과물:</strong> <code>{deliv_clean}</code> 형태의 결과물을 완성한다.</p>"
)
# Deliverables list
m_deliv_list = []
for d in clean_html_tags(m_deliverables_raw).split(' 및 '):
d = d.strip()
if d:
m_deliv_list.append(f"{d} (필수)")
if not m_deliv_list:
m_deliv_list = [f"M{seq} 산출물 및 검증 결과 (필수)"]
# Tech Stack
tech_words = [category, p_meta.get('학습 구현/수행 유형', '코딩형'), "데이터 전처리", "알고리즘 구현", "HITL 검증"]
# Evaluation Keys
eval_keys = [
(
f"<h4>평가 질문 (PASS/FAIL)</h4>"
f"<p><strong>[{clean_html_tags(m_title)}] 요구된 핵심 성능 기준({clean_html_tags(m_quant_eval)})을 완벽히 달성하였는가?</strong></p>"
f"<h4>PASS 증거</h4>"
f"<p>{clean_html_tags(m_quant_eval)} 기준을 충족하고 {clean_html_tags(m_track)}을 만족하는 실행 결과 및 로그가 제출되었다.</p>"
f"<h4>보완이 필요한 상태</h4>"
f"<p>정량적 지표가 기준치에 미달하거나 실행 로그에서 예외/결측 오류가 발생한 경우.</p>"
f"<h4>보완 방법</h4>"
f"<p>{clean_html_tags(m_feedback)}을 적용하여 파라미터를 보정하고 단위 테스트를 재수행한다.</p>"
),
(
f"<h4>평가 질문 (PASS/FAIL)</h4>"
f"<p><strong>[정성적 타당성] 군사적 현실성 및 {clean_html_tags(m_qual_eval)} 관점의 타당성이 확보되었는가?</strong></p>"
f"<h4>PASS 증거</h4>"
f"<p>{clean_html_tags(m_qual_eval)}이 논리적으로 검증되었으며 {clean_html_tags(m_verify)}를 통한 무결성이 확인되었다.</p>"
f"<h4>보완이 필요한 상태</h4>"
f"<p>비현실적인 가정을 적용했거나 오류 원인에 대한 정성적 설명 근거가 부족한 경우.</p>"
f"<h4>보완 방법</h4>"
f"<p>교차 검증 데이터를 보강하고 지휘관/실무자 관점의 해석 근거를 명시한다.</p>"
)
]
# Evaluation Text
eval_text = (
f"<h4>미션 판정</h4>"
f"<p><strong>평가 구분:</strong> 필수</p>"
f"<p><strong>핵심 평가영역:</strong> {clean_html_tags(m_qual_eval)}</p>"
f"<h4>미션 PASS 조건</h4>"
f"<ul>"
f"<li><p>정량 기준: {clean_html_tags(m_quant_eval)}</p></li>"
f"<li><p>진행 기준: {clean_html_tags(m_track)}</p></li>"
f"<li><p>검증 방법: {clean_html_tags(m_verify)}</p></li>"
f"</ul>"
f"<h4>채점자 판정 메모</h4>"
f"<blockquote><p>단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.</p></blockquote>"
)
# Mission Object Assembly
mission_obj = {
"id": m_id,
"sequence": seq,
"title": m_title,
"durationHours": m_duration,
"overview": m_overview,
"objectives": m_objectives,
"scenario": m_scenario,
"attachments": [],
"materials": [
f"/uploads/{pid}/{pid.lower()}_m{seq:02d}_dataset.json",
f"/uploads/{pid}/{pid.lower()}_m{seq:02d}_sample.md"
],
"materialText": (
f"<p><strong>미션 실습 자료</strong></p>"
f"<ul>"
f"<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"/uploads/{pid}/{pid.lower()}_m{seq:02d}_dataset.json\">가상 데이터셋 샘플 (JSON)</a></li>"
f"<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"/uploads/{pid}/{pid.lower()}_m{seq:02d}_sample.md\">실습 및 검증 가이드 (MD)</a></li>"
f"</ul>"
f"<p><strong>활용 주의</strong>: 제공된 교육용 가상 데이터만 사용하며 실제 군사 비밀 및 개인정보는 입력하지 않습니다.</p>"
),
"techStack": tech_words,
"deliverables": m_deliv_list,
"evaluationKeys": eval_keys,
"evaluationTypes": ["REQUIRED", "REQUIRED"],
"evaluationText": eval_text,
"aiGuidelines": {
"allow": [
"데이터 전처리 알고리즘 및 정규화 서식 추천받기",
"이상치 탐지 및 모델 파라미터 튜닝 아이디어 제안받기",
"대시보드 UI 컴포넌트 코드 및 시각화 서식 생성하기"
],
"rules": [
"최종 판정 및 지휘 결심은 학습자가 직접 수행한다.",
"실제 군사 비밀이나 개인 식별 정보는 입력하지 않는다.",
"AI의 출력 결과를 맹신하지 않고 반드시 단위 테스트로 검증한다."
],
"forbid": [
"실제 군사 작전/인사 비밀 데이터를 AI에 입력하기",
"검증 없이 AI가 생성한 코드를 그대로 제출하기",
"단정적인 자동 결심 결과를 인간 검토 없이 확정하기"
]
},
"guideSteps": g_steps
}
missions_list.append(mission_obj)
project_json = {
"version": "1.0.0",
"exportedAt": "2026-10-08T00:00:00.000Z",
"id": pid,
"title": title,
"category": category,
"level": level,
"durationDays": 5,
"requiredHours": req_hours,
"status": "COMPLETED",
"isRecommended": False,
"summary": "",
"objectives": objectives_html,
"deliverables": p_deliverables,
"constraints": constraints_html,
"evalSummary": eval_summary_html,
"prerequisites": f"기본 프로그래밍 지식 및 {category} 데이터 분석 기초",
"refFiles": [],
"refText": "",
"projectSkills": project_skills,
"prerequisiteVods": [],
"missions": missions_list
}
return project_json
def run_conversion(target_project_id=None):
"""Main function to convert md files to json (supports single project or all)."""
output_dir = Path("scenario_json")
output_dir.mkdir(parents=True, exist_ok=True)
projects_meta, missions_meta = load_metadata()
print(f"Loaded metadata for {len(projects_meta)} projects and {len(missions_meta)} missions.")
md_dirs = [d for d in os.listdir('scenario_md') if os.path.isdir(os.path.join('scenario_md', d))]
print(f"Found {len(md_dirs)} project directories in scenario_md.")
if target_project_id:
print(f"Filtering for single project: {target_project_id}")
success_count = 0
error_count = 0
for d in md_dirs:
m = re.match(r'^([A-Z0-9]+)-', d)
if not m:
continue
pid = m.group(1)
if target_project_id and pid != target_project_id:
continue
dir_path = os.path.join('scenario_md', d)
# Locate files
files = os.listdir(dir_path)
main_scen_file = None
guide_file = None
for f in files:
if f.endswith('.md'):
if '실습가이드' in f:
guide_file = os.path.join(dir_path, f)
elif '코드' not in f and '실습자료' not in f:
main_scen_file = os.path.join(dir_path, f)
if not main_scen_file:
for f in files:
if f.endswith('.md') and '실습가이드' not in f and '코드' not in f:
main_scen_file = os.path.join(dir_path, f)
break
scen_data = parse_scenario_md(main_scen_file) if main_scen_file else {}
guide_data = parse_guide_md(guide_file) if guide_file else {}
p_meta = projects_meta.get(pid, {})
try:
p_json = build_project_json(pid, p_meta, scen_data, guide_data, missions_meta)
out_file = output_dir / f"{pid}.json"
with open(out_file, 'w', encoding='utf-8') as fp:
json.dump(p_json, fp, ensure_ascii=False, indent=2)
print(f" [SUCCESS] Generated {out_file.name}")
success_count += 1
except Exception as e:
print(f" [ERROR] Failed to build JSON for {pid}: {e}")
error_count += 1
print("=" * 60)
print("Conversion Completed:")
print(f" Successfully generated: {success_count} project JSON files")
print(f" Errors: {error_count}")
print(f" Output directory: {output_dir.resolve()}")
print("=" * 60)
if __name__ == '__main__':
target = sys.argv[1] if len(sys.argv) > 1 else None
run_conversion(target)