# -*- coding: utf-8 -*- """ sources.py — 산출물 생성의 단일 데이터 소스 로더. docs/PLANNING.md · docs/IMPLEMENTATION_BACKLOG.md · docs/FEATURE_BACKLOG_100.md · docs/architecture/* 를 파싱하여 구조화 데이터(모듈·Phase/WBS·기능/요구사항)를 반환한다. 소스 문서가 갱신되면 _gen 재실행만으로 전 산출물이 갱신된다. 파싱 실패 시 임베디드 폴백을 사용해 결정적 산출을 보장한다. """ import os, re DOCS = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) # kintex/docs ROOT = os.path.abspath(os.path.join(DOCS, "..")) # kintex META = { "project": "킨텍스 자동전시시스템", "project_en": "Exhibition Automation Platform", "title": "킨텍스 자동전시시스템 구축 사업수행계획서", "version": "v1.0", "date": "2026-07-11", "owner": "지오정보기술(주) — GUARDiA 플랫폼 사업본부", "client": "킨텍스(KINTEX, 한국국제전시장)", "dev_domain": "kintex.zioinfo.co.kr(개발)", "std": "행정안전부 정보화사업 산출물 표준 목차 준용", } def _read(rel): p = os.path.join(DOCS, rel) try: with open(p, "r", encoding="utf-8") as f: return f.read() except Exception: return "" def _table_rows(text, min_cols=2): """마크다운 표의 데이터 행(| a | b |)을 셀 리스트로 반환(헤더/구분선 제외).""" out = [] for line in text.splitlines(): s = line.strip() if not s.startswith("|"): continue if re.match(r"^\|[\s:\-\|]+\|?$", s): # separator continue cells = [c.strip() for c in s.strip("|").split("|")] if len(cells) >= min_cols: out.append(cells) return out # ---------------- 모듈(M1~M18 + §5B) : PLANNING 기능 우선순위 총괄표 ---------------- _MOD_FALLBACK = [ ("M2","플로어플랜 스튜디오(부스 배치)","P0"),("M3","부스 설계 스튜디오","P0"), ("M4","유틸리티 설계 자동화(전기·조명/네트워크·급배수)","P0"),("M5","나노바나나 시각화","P0"), ("M1","홀 배정·자동 견적","P1"),("M6","서류·마일스톤 워크플로","P1"), ("M7","등록업체 매칭·견적","P1"),("M9","정산·결제","P1"), ("M15","공사/장치 옥션(역경매·견적서·낙찰)","P1"),("M10","관람객 등록·배지·체크인·리드캡처","P1"), ("M12","마케팅·EDM·공개 홍보 사이트","P1"),("M16","경영분석 BI","P1"), ("M17","CMS(콘텐츠·마이크로사이트·다국어)","P1"),("M18","관리자 백오피스","P1"), ("§5B","공통/시스템관리 레이어 + JWT+2FA","P1"), ("M8","반입/반출 물류","P2"),("M11","비즈니스 매칭","P2"), ("M13","wayfinding·실내 내비","P2"),("M14","현장운영(혼잡·안전·주차·에너지)","P2"), ] def modules(): text = _read("PLANNING.md") mods, seen = [], set() for cells in _table_rows(text, min_cols=4): mid = cells[0].replace("**","").strip() if not re.match(r"^(M\d+|§5B)$", mid): continue if mid in seen: continue name = re.sub(r"\*\*|`", "", cells[1]).strip() pri = "" for c in cells: m = re.search(r"P0|P1|P2", c) if m: pri = m.group(0); break if not name or not pri: continue seen.add(mid); mods.append((mid, name, pri)) if len(mods) < 15: return list(_MOD_FALLBACK) order = {"P0":0,"P1":1,"P2":2} mods.sort(key=lambda x: order.get(x[2],9)) return mods # ---------------- Phase / WBS : IMPLEMENTATION_BACKLOG Phase 표 ---------------- _PHASE_FALLBACK = [ ("A","아키텍처·거버넌스",[ ("A-1","애플리케이션 아키텍처(모듈 경계·API 표준)","아키텍트(AA)","docs/architecture/app.md"), ("A-2","시스템 아키텍처·NFR(확장성·HA·성능·보안영역)","아키텍트(SA)","docs/architecture/system.md"), ("A-3","기술 표준(스택·빌드/배포·관측성·AiTextRouter)","아키텍트(TA)","docs/architecture/tech.md"), ("A-4","데이터 아키텍처(전사 ERD·공간·마스터·BI마트)","아키텍트(DA)","ERD·data.md"), ("A-5","네트워크 아키텍처(DMZ/내부망·방화벽·부하분산)","아키텍트(NA)","network.md"), ("A-6","아키텍처↔PLANNING 정합 검증","reviewer","불일치 0/티켓화"), ]), ("B","공통/시스템관리 레이어(전 모듈 선행)",[ ("B-0","스캐폴드(백엔드·프론트·DB·마이그레이션)","BE·FE·DB","빌드·마이그레이션 통과"), ("B-1","인증 JWT+RBAC+2차인증(OTP)+실패잠금+admin env","COM·BE","2FA 등록/검증/초기화 왕복"), ("B-2","시스템관리(사용자·역할·공통코드·메뉴·감사·설정)","COM·ADM·DB","CRUD·RBAC 가드·감사"), ("B-3","공통 업무기능(일지·일정·쪽지·통계·공지·회의·알림)","COM·DB","각 모듈 API 왕복"), ("B-4","공통 컴포넌트·프론트 공통(2FA·그리드·달력·모달)","COM·FE","아키텍처 표준 정합"), ("B-5","점진 QA(2FA·RBAC·PII·경계면)","QA","통과까지 반려"), ]), ("C","P0 부스 시공 코어",[ ("C-M2","플로어플랜(배치 3안·병합·규정검증 PostGIS)","DB·BE·AI·FE·QA","3안 생성·병합·규정 재검증"), ("C-M3","부스 설계(조립/독립·규정 사전검증·3안)","BE·AI·FE·QA","위반 사전 플래깅·컨펌"), ("C-M4","유틸리티 배선(전기/네트워크/급배수·견적·위치표시도)","DB·BE·FE·QA","최단 배선·견적·위치표시도"), ("C-M5","나노바나나 시각화(RenderJob·워커·워터마크)","BE·VIZ·FE·QA","실생성/미승인 시 목·워터마크"), ("C-C","역할별 공통 화면(로그인·대시보드·현장 모바일)","FE·QA","역할별 진입·상태 처리"), ]), ("D","P1 도메인(역할별 포털)",[ ("D-M15","공사/장치 옥션(견적서 제출·역경매·낙찰)","BID·BE·DB·FE·QA","견적서·역경매·낙찰 왕복"), ("D-M10","관람객·현장(등록·티켓·배지·체크인·리드)","VIS·BE·DB·FE·QA","등록→배지→체크인→리드"), ("D-M12/17","마케팅·공개사이트·CMS(SEO·다국어·EDM)","CMS·BE·FE·QA","공개 조회·게시 워크플로"), ("D-M16","경영분석 BI(가동률·P&L·ROI·수요예측·KPI)","BI·DA·BE·FE·QA","지표 집계·드릴다운"), ("D-M18","관리자 백오피스(마스터데이터·룰셋 버전)","ADM·BE·FE·QA","RBAC·마스터 CRUD·룰셋"), ("D-M1/6/7/9","배정·서류·매칭·정산","BE·FE·DB·QA","견적·마일스톤·정산"), ]), ("E","P2 + 빌드·배포",[ ("E-M11","비즈니스 매칭(추천·미팅예약)","VIS·AI","추천·미팅예약"), ("E-M13","wayfinding·실내 내비(M2 재사용)","VIS·FE","부스 검색·경로"), ("E-M14","현장운영(혼잡·안전·주차·에너지)","BE·FE","대시보드·경보"), ("E-DEP","역할별 번들·jar·워커 빌드·CI/CD·systemd","DEV","push→배포"), ]), ] def phases(): """[(letter, title, [(id, task, owner, done), ...]), ...]""" text = _read("IMPLEMENTATION_BACKLOG.md") if not text: return _PHASE_FALLBACK out = [] cur = None lines = text.splitlines() hdr_re = re.compile(r"^##\s+Phase\s+([A-E])\s*[—\-–]\s*(.+)$") for line in lines: hm = hdr_re.match(line.strip()) if hm: if cur: out.append(cur) title = re.sub(r"\([^)]*\)", "", hm.group(2)) title = re.sub(r"[★((].*$", "", title).strip() cur = (hm.group(1), title, []) continue s = line.strip() if cur and s.startswith("|"): if re.match(r"^\|[\s:\-\|]+\|?$", s): continue cells = [c.strip() for c in s.strip("|").split("|")] if not cells or cells[0] in ("ID","Phase") or not re.match(r"^[A-E]-", cells[0]): continue # 표 형태 2종: (ID,작업,담당,완료) 또는 (ID,모듈,작업,담당,완료) if len(cells) >= 5: _id, task, owner, done = cells[0], cells[2], cells[3], cells[4] elif len(cells) == 4: _id, task, owner, done = cells[0], cells[1], cells[2], cells[3] else: continue cur[2].append((_id, re.sub(r"\*\*|`","",task), re.sub(r"\*\*","",owner), re.sub(r"\*\*","",done))) if cur: out.append(cur) ok = [p for p in out if p[2]] return ok if len(ok) >= 4 else _PHASE_FALLBACK # 마스터 일정(착수 상대 주차) — 큐레이션(마일스톤) SCHEDULE = [ ("A","아키텍처·거버넌스","W1","W3","M1 아키텍처 베이스라인 승인(W3)"), ("B","공통/시스템관리 레이어","W3","W8","M2 공통 레이어·인증(2FA) 완료(W8)"), ("C","P0 부스 시공 코어","W8","W16","M3 P0 부스 코어 시연·첫 상용 행사 적용(W16)"), ("D","P1 도메인","W16","W24","M4 옥션·관람객·BI·관리자 통합(W24)"), ("E","P2 + 빌드·배포","W24","W28","M5 통합 QA 통과·운영 배포(W28)"), ] # ---------------- 기능/요구사항 : FEATURE_BACKLOG_100 C1~C12 ---------------- def features(): """[(fid, name, category, priority, ai, module), ...] — 100대 기능(RTM/요구사항 원천).""" text = _read("FEATURE_BACKLOG_100.md") out, seen = [], set() cur_cat = "" cat_re = re.compile(r"^###\s+(C\d+)\.\s*(.+?)\s*(?:\(\d+\)|—|$)") stop = False for line in text.splitlines(): s = line.strip() cm = cat_re.match(s) if cm: cur_cat = cm.group(1) + " " + re.sub(r"\s*[—-].*$","",cm.group(2)).strip() stop = False continue # C 표가 끝나고 'AI 자동화 맵' 등 다른 섹션이면 중단 표시 if s.startswith("##") and "카테고리" not in s and not s.startswith("###"): stop = True if stop: continue m = re.match(r"^\|\s*(F\d{3})\s*\|(.+)$", s) if m and cur_cat: fid = m.group(1) if fid in seen: continue cells = [c.strip() for c in m.group(2).strip("|").split("|")] name = re.sub(r"\*\*|`","",cells[0]).strip() if cells else fid pri = next((re.search(r"P[012]",c).group(0) for c in cells if re.search(r"P[012]",c)), "") ai = "" for c in cells: cc = c.strip() if cc in ("Y","N","부분"): ai = cc; break module = next((re.sub(r"\*\*|`","",c) for c in cells if re.search(r"(M\d+|§5B|§1A|§6)", c)), "") seen.add(fid) out.append((fid, name, cur_cat, pri, ai, module)) return out # ---------------- 산출물 목록(표준 단계별) ---------------- DELIVERABLES = [ # (단계, 산출물명, 형식, 담당, 상태, 비고) ("착수(계획)","사업수행계획서","DOCX","PMO","작성완료","본 산출물(gen_docx)"), ("착수(계획)","WBS 및 마스터 일정","XLSX","PMO","작성완료","gen_xlsx(WBS/일정/투입/산출물)"), ("착수(계획)","산출물 목록","XLSX","PMO","작성완료","gen_xlsx(산출물목록 시트)"), ("착수(계획)","착수보고 자료","PPTX","PMO","작성완료","gen_pptx(착수보고)"), ("분석","요구사항 정의서","DOCX","기획(planner)","작성완료","gen_reqspec(100대 기능 원천)"), ("분석","요구사항 추적표(RTM)","XLSX","QA/기획","작성완료","gen_xlsx(RTM 시트)"), ("분석","기획서(PLANNING)","MD","기획(planner)","기보유","docs/PLANNING.md v3.0"), ("설계","개발계획서","DOCX","PMO/개발리드","작성완료","gen_devplan"), ("설계","화면설계서·UI 스펙(design)","MD/DOCX","디자이너","기보유","docs/design.md"), ("설계","아키텍처 설계서(앱/시스템/기술/데이터/네트워크)","MD","아키텍트","기보유","docs/architecture/*"), ("설계","데이터 모델(전사 ERD·BI 데이터마트)","MD/SQL","DA/DB","진행","설계 트랙"), ("설계","API 설계서(계약)","MD","AA/백엔드","기보유","docs/API_GUIDE.md"), ("구현","소스코드(백엔드·프론트·워커)","Git","개발","진행","src/, tools/nanobanana"), ("구현","DB 스키마·마이그레이션","SQL","DB","진행","Flyway"), ("시험","시험계획서","DOCX","QA","작성완료","gen_testplan"), ("시험","시험케이스·시험결과서","XLSX","QA","작성완료","gen_xlsx(시험케이스 시트)"), ("이관","배포/운영 런북·이관 확인서","MD","DevOps","기보유","docs/OPS_RUNBOOK.md"), ("교육","사용자 지침서·운영자 지침서","DOCX/PPTX","기술문서","예정","완성+QA 후"), ("종료","완료(준공) 보고서","DOCX","PMO","예정","사업 종료 시"), ] # ---------------- 투입 인력 계획(M/M) ---------------- STAFFING = [ ("사업 PM/PMO","1","특급","A~E 상시","6.5"), ("아키텍트(AA/SA/TA)","2","특급","A 집중","3.0"), ("데이터 아키텍트(DA)","1","고급","A·D","2.0"), ("네트워크 아키텍트(NA)","1","고급","A·E","1.0"), ("공통/시스템관리 개발","2","중급","B 집중","4.0"), ("백엔드 개발","3","중·고급","B~D","12.0"), ("프론트엔드 개발","3","중·고급","B~E","12.0"), ("DB 엔지니어(PostGIS)","1","고급","B~D","4.5"), ("AI 개발","1","고급","C~D","3.0"), ("시각화(나노바나나)","1","고급","C","2.0"), ("도메인 개발(옥션/관람객/CMS/BI/관리자)","3","중·고급","D 집중","9.0"), ("QA","2","중급","B~E 상시","8.0"), ("DevOps","1","고급","B·E","2.5"), ("디자이너","1","고급","A~E","3.0"), ] RISKS = [ ("R1","홀 실측 CAD 원본 미확보","배치 정밀도","공개 규격 근사 도면으로 진행, 확보 시 대체"), ("R2","이미지 생성(외부) 승인 게이트","M5 시각화","미승인 시 목/degraded, 승인 후 라이브"), ("R3","외부 시스템(작업신고/PG/KT) 연동 미공개","M6/M9/M4b","제출용 파일·릴레이 방식, 협의 후 연동"), ("R4","등록업체 옥션 참여율·응찰 품질","M15 실효성","등록업체 게이트·AI 자료 공정 비교 유인"), ("R5","개인정보(리드·배지) 규정 위반","법적 리스크","동의·보존정책·감사·최소 수집"), ("R6","AI 시각화 오용(계약 서류)","분쟁 리스크","워터마크·고지 강제(UI·응답·PDF)"), ("R7","공통 레이어 지연","일정","Phase B 인력 선집중·병행 착수"), ("R8","규정·요율 개정","정합성","룰셋 버전 관리·과거 버전 보존"), ] if __name__ == "__main__": print("modules:", len(modules())) ph = phases(); print("phases:", [(p[0], len(p[2])) for p in ph]) print("features:", len(features())) print("sample feature:", features()[0] if features() else None)