""" 유저스토리를 엑셀로 변환하는 스크립트 """ 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()