시나리오 내용 반영
This commit is contained in:
1 parent
d9287de729
commit
350bb92bf9
270 files changed
+198867
No files matched your search
@@ -0,0 +1,693 @@
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user