mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 12:36:23 +00:00
- 회의 자료 섹션 삭제 - Todo undefined 문제 해결 (네임스페이스 충돌 수정) - JavaScript 디버깅 로그 추가 - 기존 prototype 디렉토리 삭제 - prototype-gappa 디렉토리 추가 - 유저스토리 gappa 버전 추가 - 엑셀 변환 스크립트 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
215 lines
7.3 KiB
Python
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()
|