hgzero/convert_userstory_to_excel.py
hiondal 25efe243f4 대시보드 프로토타입 개선 및 프로젝트 정리
- 회의 자료 섹션 삭제
- Todo undefined 문제 해결 (네임스페이스 충돌 수정)
- JavaScript 디버깅 로그 추가
- 기존 prototype 디렉토리 삭제
- prototype-gappa 디렉토리 추가
- 유저스토리 gappa 버전 추가
- 엑셀 변환 스크립트 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 16:29:21 +09:00

215 lines
7.3 KiB
Python

"""
유저스토리를 엑셀로 변환하는 스크립트
"""
import re
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
def parse_userstory(file_path):
"""유저스토리 파일 파싱"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 유저스토리 섹션 추출 (``` 사이의 내용)
match = re.search(r'```\n(.*?)\n```', content, re.DOTALL)
if not match:
raise ValueError("유저스토리 섹션을 찾을 수 없습니다.")
userstory_content = match.group(1)
# 유저스토리 파싱
stories = []
current_service = None
current_epic = None
lines = userstory_content.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
# 서비스 감지 (숫자. 서비스명 또는 숫자. 서비스명 서비스)
service_match = re.match(r'^(\d+)\.\s+(.+?)$', line)
if service_match and not line.startswith('UFR-') and not line.startswith('AFR-') and not re.match(r'^\d+\)\s+', line):
service_name = service_match.group(2).strip()
# "서비스" 문자열 제거
if service_name.endswith(' 서비스'):
service_name = service_name[:-3]
elif service_name.endswith('서비스'):
service_name = service_name[:-2]
current_service = f"{service_match.group(1)}. {service_name}"
current_epic = None
i += 1
continue
# Epic 감지 (숫자) Epic명)
epic_match = re.match(r'^(\d+)\)\s+(.+)$', line)
if epic_match:
current_epic = f"{epic_match.group(1)}. {epic_match.group(2).strip()}"
i += 1
continue
# 유저스토리 ID 감지 (UFR-XXX-YYY: 또는 AFR-XXX-YYY:)
story_match = re.match(r'^([UA]FR-[A-Z]+-\d+):\s+\[(.+?)\]\s*(.*)$', line)
if story_match:
story_id = story_match.group(1)
story_title = story_match.group(2)
# 콜론 다음에 유저스토리 내용이 있는 경우
remaining = story_match.group(3).strip()
# 다음 줄이 유저스토리 내용인지 확인
i += 1
story_desc = ""
# 남은 부분이 있으면 사용, 없으면 다음 줄 확인
if remaining and remaining.startswith(':'):
story_desc = remaining[1:].strip()
elif i < len(lines):
next_line = lines[i].strip()
# 다음 줄이 시나리오가 아니면 유저스토리 내용으로 간주
if not next_line.startswith('-') and not next_line.startswith('[') and next_line:
story_desc = next_line
i += 1
# 인수시나리오와 점수 추출
acceptance_criteria = []
biz_score = ""
while i < len(lines):
next_line = lines[i]
# 다음 유저스토리나 서비스 시작
if (re.match(r'^[UA]FR-[A-Z]+-\d+:', next_line.strip()) or
re.match(r'^\d+\.\s+[A-Z]', next_line.strip()) or
re.match(r'^\d+\)\s+', next_line.strip()) or
next_line.strip() == '---'):
break
# Biz중요도/Score 추출
score_match = re.match(r'^-\s+([A-Z])/(\d+)$', next_line.strip())
if score_match:
biz_score = f"{score_match.group(1)}/{score_match.group(2)}"
i += 1
break
# 인수시나리오 수집 (빈 줄 제외)
if next_line.strip() and not next_line.strip().startswith('---'):
acceptance_criteria.append(next_line)
i += 1
# 인수시나리오를 하나의 문자열로 합치기
acceptance_text = '\n'.join(acceptance_criteria)
stories.append({
'service': current_service or '',
'epic': current_epic or '',
'story_id': story_id,
'story_title': story_title,
'story_desc': story_desc,
'acceptance': acceptance_text,
'biz_score': biz_score
})
continue
i += 1
return stories
def create_excel(stories, output_path):
"""엑셀 파일 생성"""
wb = Workbook()
ws = wb.active
ws.title = "유저스토리"
# 헤더 정의
headers = [
'서비스',
'Epic',
'유저스토리 ID',
'유저스토리 제목',
'유저스토리',
'인수시나리오',
'Biz중요도/Score'
]
# 헤더 스타일
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=11)
header_alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
# 테두리 스타일
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# 헤더 작성
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num)
cell.value = header
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
cell.border = thin_border
# 데이터 작성
for row_num, story in enumerate(stories, 2):
ws.cell(row=row_num, column=1).value = story['service']
ws.cell(row=row_num, column=2).value = story['epic']
ws.cell(row=row_num, column=3).value = story['story_id']
ws.cell(row=row_num, column=4).value = story['story_title']
ws.cell(row=row_num, column=5).value = story['story_desc']
ws.cell(row=row_num, column=6).value = story['acceptance']
ws.cell(row=row_num, column=7).value = story['biz_score']
# 데이터 스타일 적용
for col_num in range(1, 8):
cell = ws.cell(row=row_num, column=col_num)
cell.alignment = Alignment(vertical="top", wrap_text=True)
cell.border = thin_border
# 컬럼 너비 조정
column_widths = {
'A': 20, # 서비스
'B': 25, # Epic
'C': 15, # 유저스토리 ID
'D': 25, # 유저스토리 제목
'E': 50, # 유저스토리
'F': 60, # 인수시나리오
'G': 15 # Biz중요도/Score
}
for col_letter, width in column_widths.items():
ws.column_dimensions[col_letter].width = width
# 헤더 행 높이
ws.row_dimensions[1].height = 30
# 저장
wb.save(output_path)
print(f"[OK] Excel file created: {output_path}")
print(f"[INFO] Total {len(stories)} user stories converted.")
if __name__ == "__main__":
input_file = "design/userstory.md"
output_file = "design/userstory.xlsx"
try:
print("[INFO] Parsing user story file...")
stories = parse_userstory(input_file)
print(f"[OK] Parsed {len(stories)} user stories.")
print("[INFO] Creating Excel file...")
create_excel(stories, output_file)
except Exception as e:
print(f"[ERROR] {e}")
import traceback
traceback.print_exc()