install 스크립트 개선: JDK 21 자동 설치, 경로 문제 수정, 문서 업데이트
- install.bat/install.sh: JDK 미설치 시 자동 설치 (winget/brew/apt/yum) - install.bat: ERRORLEVEL 지연 확장 통일, gradlew 경로 수정, CRLF/이스케이프 수정 - install.sh: set -euo pipefail 제거, PROJECT_ROOT 기준 경로로 변경 - Lombok @Builder.Default 경고 3건 수정 (WbxUserRole, WbxAuditLog, RolePermission) - 개발자가이드/설치가이드 PDF: JDK 자동 설치 기능 반영 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
바이너리 파일은 표시되지 않습니다.
바이너리 파일은 표시되지 않습니다.
838
docs/generate_system_design.py
일반 파일
838
docs/generate_system_design.py
일반 파일
@@ -0,0 +1,838 @@
|
||||
"""
|
||||
한화오션 EPU WTM 시스템 설계서 PPT 생성
|
||||
요구사항정의 → To-Be → 시스템구성도 → 기능정의 → 시스템설계
|
||||
"""
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
import os
|
||||
|
||||
OUTPUT = os.path.join(os.path.dirname(__file__), "한화오션_EPU_WTM_시스템설계서.pptx")
|
||||
|
||||
# 색상
|
||||
PRIMARY = RGBColor(30, 60, 120)
|
||||
ACCENT = RGBColor(0, 120, 200)
|
||||
DARK = RGBColor(40, 40, 40)
|
||||
BODY = RGBColor(80, 80, 80)
|
||||
WHITE = RGBColor(255, 255, 255)
|
||||
LIGHT_BG = RGBColor(240, 245, 255)
|
||||
GREEN_BG = RGBColor(220, 240, 220)
|
||||
ORANGE_BG = RGBColor(255, 240, 220)
|
||||
RED_ACCENT = RGBColor(200, 60, 60)
|
||||
|
||||
|
||||
def add_box(slide, left, top, width, height, text, fill_color=LIGHT_BG, font_color=PRIMARY, font_size=10, bold=True):
|
||||
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height)
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = fill_color
|
||||
shape.line.color.rgb = PRIMARY
|
||||
shape.line.width = Pt(1)
|
||||
tf = shape.text_frame
|
||||
tf.word_wrap = True
|
||||
p = tf.paragraphs[0]
|
||||
p.text = text
|
||||
p.font.size = Pt(font_size)
|
||||
p.font.color.rgb = font_color
|
||||
p.font.bold = bold
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
tf.paragraphs[0].space_before = Pt(2)
|
||||
return shape
|
||||
|
||||
|
||||
def add_textbox(slide, left, top, width, height, text, font_size=10, color=BODY, bold=False, align=PP_ALIGN.LEFT):
|
||||
txBox = slide.shapes.add_textbox(left, top, width, height)
|
||||
tf = txBox.text_frame
|
||||
tf.word_wrap = True
|
||||
p = tf.paragraphs[0]
|
||||
p.text = text
|
||||
p.font.size = Pt(font_size)
|
||||
p.font.color.rgb = color
|
||||
p.font.bold = bold
|
||||
p.alignment = align
|
||||
return txBox
|
||||
|
||||
|
||||
def add_multiline_box(slide, left, top, width, height, title, items, fill_color=LIGHT_BG):
|
||||
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height)
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = fill_color
|
||||
shape.line.color.rgb = PRIMARY
|
||||
shape.line.width = Pt(1)
|
||||
tf = shape.text_frame
|
||||
tf.word_wrap = True
|
||||
p = tf.paragraphs[0]
|
||||
p.text = title
|
||||
p.font.size = Pt(9)
|
||||
p.font.color.rgb = PRIMARY
|
||||
p.font.bold = True
|
||||
for item in items:
|
||||
p2 = tf.add_paragraph()
|
||||
p2.text = f"• {item}"
|
||||
p2.font.size = Pt(7)
|
||||
p2.font.color.rgb = BODY
|
||||
p2.space_before = Pt(1)
|
||||
return shape
|
||||
|
||||
|
||||
def add_arrow(slide, start_left, start_top, end_left, end_top):
|
||||
connector = slide.shapes.add_connector(
|
||||
1, start_left, start_top, end_left, end_top) # 1 = straight
|
||||
connector.line.color.rgb = PRIMARY
|
||||
connector.line.width = Pt(1.5)
|
||||
return connector
|
||||
|
||||
|
||||
def add_table(slide, left, top, width, rows_data, col_widths=None):
|
||||
rows = len(rows_data)
|
||||
cols = len(rows_data[0])
|
||||
table_shape = slide.shapes.add_table(rows, cols, left, top, width, Inches(0.3 * rows))
|
||||
table = table_shape.table
|
||||
|
||||
if col_widths:
|
||||
for i, w in enumerate(col_widths):
|
||||
table.columns[i].width = w
|
||||
|
||||
for ri, row in enumerate(rows_data):
|
||||
for ci, val in enumerate(row):
|
||||
cell = table.cell(ri, ci)
|
||||
cell.text = str(val)
|
||||
p = cell.text_frame.paragraphs[0]
|
||||
p.font.size = Pt(8)
|
||||
if ri == 0:
|
||||
p.font.bold = True
|
||||
p.font.color.rgb = WHITE
|
||||
cell.fill.solid()
|
||||
cell.fill.fore_color.rgb = PRIMARY
|
||||
else:
|
||||
p.font.color.rgb = BODY
|
||||
if ri % 2 == 0:
|
||||
cell.fill.solid()
|
||||
cell.fill.fore_color.rgb = LIGHT_BG
|
||||
p.alignment = PP_ALIGN.CENTER if ci > 0 else PP_ALIGN.LEFT
|
||||
|
||||
|
||||
def title_slide(prs, title, subtitle):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
|
||||
# 배경
|
||||
bg = slide.background.fill
|
||||
bg.solid()
|
||||
bg.fore_color.rgb = PRIMARY
|
||||
# 타이틀
|
||||
add_textbox(slide, Inches(1), Inches(2), Inches(8), Inches(1),
|
||||
title, font_size=32, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
|
||||
add_textbox(slide, Inches(1), Inches(3.2), Inches(8), Inches(0.8),
|
||||
subtitle, font_size=16, color=RGBColor(180, 200, 230), align=PP_ALIGN.CENTER)
|
||||
add_textbox(slide, Inches(1), Inches(5.5), Inches(8), Inches(0.5),
|
||||
"아큐라시스템 | 2026년 3월", font_size=12, color=RGBColor(150, 170, 200), align=PP_ALIGN.CENTER)
|
||||
return slide
|
||||
|
||||
|
||||
def section_slide(prs, num, title, subtitle=""):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
bg = slide.background.fill
|
||||
bg.solid()
|
||||
bg.fore_color.rgb = RGBColor(25, 50, 100)
|
||||
# 큰 번호
|
||||
add_textbox(slide, Inches(0.8), Inches(1.5), Inches(2), Inches(1.2),
|
||||
f"{num:02d}", font_size=72, color=ACCENT, bold=True)
|
||||
# 구분선
|
||||
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
|
||||
Inches(0.8), Inches(3.0), Inches(8), Pt(3))
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = ACCENT
|
||||
shape.line.fill.background()
|
||||
# 타이틀
|
||||
add_textbox(slide, Inches(0.8), Inches(3.3), Inches(9), Inches(0.8),
|
||||
title, font_size=28, color=WHITE, bold=True)
|
||||
# 서브타이틀
|
||||
if subtitle:
|
||||
add_textbox(slide, Inches(0.8), Inches(4.3), Inches(8), Inches(0.5),
|
||||
subtitle, font_size=12, color=RGBColor(150, 180, 220))
|
||||
return slide
|
||||
|
||||
|
||||
def content_slide(prs, title):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
# 상단 바
|
||||
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, Inches(10), Inches(0.6))
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = PRIMARY
|
||||
shape.line.fill.background()
|
||||
add_textbox(slide, Inches(0.3), Inches(0.05), Inches(9), Inches(0.5),
|
||||
title, font_size=16, color=WHITE, bold=True)
|
||||
return slide
|
||||
|
||||
|
||||
def build():
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(10)
|
||||
prs.slide_height = Inches(7.5)
|
||||
|
||||
# ============================================================
|
||||
# 표지
|
||||
# ============================================================
|
||||
title_slide(prs,
|
||||
"한화오션 EPU\nWork Time Manager (WTM)",
|
||||
"시스템 설계서 — 요구사항정의 · To-Be · 시스템구성 · 기능정의 · 설계")
|
||||
|
||||
# ============================================================
|
||||
# 목차
|
||||
# ============================================================
|
||||
slide = content_slide(prs, "목차")
|
||||
toc = [
|
||||
"01. 프로젝트 개요",
|
||||
"02. 요구사항 정의서",
|
||||
"03. As-Is / To-Be 분석",
|
||||
"04. 시스템 구성도",
|
||||
"05. 기능 정의서",
|
||||
"06. 데이터베이스 설계",
|
||||
"07. API 설계",
|
||||
"08. 화면 구성",
|
||||
"09. 인터페이스 설계 (SAP/P6/Cognite)",
|
||||
"10. 보안 설계",
|
||||
"11. 단계별 추진 일정",
|
||||
]
|
||||
y = 1.0
|
||||
for item in toc:
|
||||
add_textbox(slide, Inches(1.5), Inches(y), Inches(7), Inches(0.35),
|
||||
item, font_size=14, color=DARK)
|
||||
y += 0.4
|
||||
|
||||
# ============================================================
|
||||
# 01. 프로젝트 개요
|
||||
# ============================================================
|
||||
section_slide(prs, 1, "프로젝트 개요", "WTM 시스템 범위, 기술 스택, 추진 구조")
|
||||
|
||||
slide = content_slide(prs, "01. 프로젝트 개요")
|
||||
overview_data = [
|
||||
["항목", "내용"],
|
||||
["프로젝트명", "WTM (Work Time Manager)"],
|
||||
["고객", "한화오션 EPU (Engineering Procurement Unit)"],
|
||||
["목적", "EPC 프로젝트 인력 시수 관리, WBS 연동, 결재, 리포트"],
|
||||
["기능 요구사항", "86개 (PH1 Y=62 / PH2 이관=24)"],
|
||||
["비기능 요구사항", "17개 (NF.1~17)"],
|
||||
["화면 수", "49개 (PH1-1차 26 / PH1-2차 11 / PH2 12)"],
|
||||
["API 수", "79개 (PH1-1: 67 / PH1-2: 6 / PH2: 6)"],
|
||||
["기술 스택", "Spring Boot 3.5.0 + Java 21 (wbx-spring 프레임워크)"],
|
||||
["DB", "Azure SQL (프로필 전환: Oracle/MSSQL/MySQL/PG)"],
|
||||
["인증", "Azure Entra ID SSO + MFA/TOTP"],
|
||||
]
|
||||
add_table(slide, Inches(0.5), Inches(1.0), Inches(9),
|
||||
overview_data, col_widths=[Inches(2.5), Inches(6.5)])
|
||||
|
||||
# ============================================================
|
||||
# 02. 요구사항 정의서
|
||||
# ============================================================
|
||||
section_slide(prs, 2, "요구사항 정의서", "기능 86개 (Y=62/N=24) + 비기능 17개 + 확인필요사항 7건")
|
||||
|
||||
# 기능 요구사항 요약
|
||||
slide = content_slide(prs, "02-1. 기능 요구사항 (86개) — 영역별 분류")
|
||||
req_data = [
|
||||
["영역", "PH1 (Y)", "PH2 이관 (N)", "합계"],
|
||||
["User Registration (No.1~7)", "5", "2", "7"],
|
||||
["Login / 인증 (No.8~16)", "8", "1", "9"],
|
||||
["User Home / 권한 (No.17~24)", "6", "2", "8"],
|
||||
["Project / WBS (No.25~41)", "14", "3", "17"],
|
||||
["Resource Assignment (No.42~54)", "6", "7", "13"],
|
||||
["Time Sheet (No.55~69)", "13", "2", "15"],
|
||||
["Approval (No.70~75)", "6", "0", "6"],
|
||||
["Reporting (No.76~86)", "2", "9", "11"],
|
||||
["합계", "62", "24", "86"],
|
||||
]
|
||||
add_table(slide, Inches(0.5), Inches(1.0), Inches(9),
|
||||
req_data, col_widths=[Inches(4), Inches(2), Inches(2), Inches(1)])
|
||||
|
||||
# 비기능 요구사항
|
||||
slide = content_slide(prs, "02-2. 비기능 요구사항 (17개)")
|
||||
nf_data = [
|
||||
["No.", "카테고리", "요구사항", "비고"],
|
||||
["NF.1~2", "클라우드", "Azure Hybrid Security Zone 내 구성", "MS Azure"],
|
||||
["NF.3", "보안(서버)", "HIWARE, V3, Secuver TOS", "한화 표준"],
|
||||
["NF.4", "보안(DB)", "Cubeone(암호화), Dbsafer(접근제어)", "한화 표준"],
|
||||
["NF.5", "보안(Cloud)", "Defender + Analytics", "Native"],
|
||||
["NF.6~7", "정보보호", "보안성 심의, 컴플라이언스", "그룹 표준"],
|
||||
["NF.8~10", "모니터링", "onTune(SMS), Maxguage(DB), Jennifer(WAS)", "한화 표준"],
|
||||
["NF.11", "인증(내부)", "Azure Entra ID SSO", ""],
|
||||
["NF.12", "인증(외부)", "2-Way 인증 (MFA)", "TOTP"],
|
||||
["NF.13", "I/F(HR)", "SAP BTP 경유 SuccessFactors 연동", ""],
|
||||
["NF.14", "I/F(P6)", "파일 기반 (물리적 I/F 없음)", ""],
|
||||
["NF.15", "I/F(Cognite)", "Extractor 서버 구성", "PH2"],
|
||||
["NF.16~17", "아키텍처", "이중화, 백업 전략", ""],
|
||||
]
|
||||
add_table(slide, Inches(0.3), Inches(1.0), Inches(9.4),
|
||||
nf_data, col_widths=[Inches(1), Inches(1.5), Inches(4.5), Inches(1.5)])
|
||||
|
||||
# 확인필요사항
|
||||
slide = content_slide(prs, "02-3. 확인필요사항 (7건)")
|
||||
confirm_data = [
|
||||
["#", "영역", "내용", "현재 상태"],
|
||||
["1", "User", "파트너사/외주 인력 관리 정책", "파트너 마스터 미연계"],
|
||||
["2", "권한", "사용자 관리규정 및 권한정책", "No.18 확정 (기능 기반 역할)"],
|
||||
["3", "WBS", "WBS별 Resource Plan 방식", "TT에서 투입시수 세팅"],
|
||||
["4", "배정", "인력 배치 시 스케줄 확인 절차", "Discipline별 인력 선정 후 제출"],
|
||||
["5", "EPC", "C단계 시수 입력 방안", "C단계 대상 제외 (EPU만)"],
|
||||
["6", "결재", "업무별 승인 절차", "User→DL→PM 3단계 확정"],
|
||||
["7", "배정", "Project Assignment 프로세스", "SA 생성→PM 배정→확정"],
|
||||
]
|
||||
add_table(slide, Inches(0.3), Inches(1.0), Inches(9.4),
|
||||
confirm_data, col_widths=[Inches(0.5), Inches(1), Inches(3.5), Inches(3.5)])
|
||||
|
||||
# ============================================================
|
||||
# 03. As-Is / To-Be
|
||||
# ============================================================
|
||||
section_slide(prs, 3, "As-Is / To-Be 분석", "Excel 수작업 → WTM 시스템 전환 효과")
|
||||
|
||||
slide = content_slide(prs, "03. As-Is → To-Be 전환")
|
||||
|
||||
# As-Is 박스들
|
||||
add_textbox(slide, Inches(0.5), Inches(0.9), Inches(4), Inches(0.4),
|
||||
"As-Is (현재)", font_size=14, color=RED_ACCENT, bold=True)
|
||||
|
||||
as_is = [
|
||||
("Excel 기반 시수 관리", "수작업 입력, 오류 빈번"),
|
||||
("이메일 기반 결재", "추적 불가, 지연"),
|
||||
("P6 WBS 수동 매핑", "버전 관리 어려움"),
|
||||
("리포트 수동 집계", "실시간 분석 불가"),
|
||||
("인력 배정 구두 전달", "이력 관리 없음"),
|
||||
]
|
||||
y = 1.4
|
||||
for title, desc in as_is:
|
||||
add_multiline_box(slide, Inches(0.5), Inches(y), Inches(4), Inches(0.55),
|
||||
title, [desc], fill_color=RGBColor(255, 235, 235))
|
||||
y += 0.6
|
||||
|
||||
# 화살표 영역
|
||||
add_textbox(slide, Inches(4.5), Inches(2.5), Inches(1), Inches(0.5),
|
||||
"→", font_size=36, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
# To-Be 박스들
|
||||
add_textbox(slide, Inches(5.5), Inches(0.9), Inches(4), Inches(0.4),
|
||||
"To-Be (WTM 시스템)", font_size=14, color=ACCENT, bold=True)
|
||||
|
||||
to_be = [
|
||||
("통합 시수 입력 (3종)", "Non-Project/Other/EPC 탭 전환, 규칙 엔진"),
|
||||
("온라인 결재 워크플로우", "User→DL→PM 3단계, 일괄 승인, 실시간 알림"),
|
||||
("P6 WBS 자동 파싱", "Canonical WBS 매핑, 버전 관리, 비교 UI"),
|
||||
("실시간 리포트", "프로젝트별/WBS별 시수 분석, Excel Export"),
|
||||
("체계적 인력 배정", "프로젝트-Discipline-인력 매핑, 이력 관리"),
|
||||
]
|
||||
y = 1.4
|
||||
for title, desc in to_be:
|
||||
add_multiline_box(slide, Inches(5.5), Inches(y), Inches(4), Inches(0.55),
|
||||
title, [desc], fill_color=RGBColor(230, 245, 255))
|
||||
y += 0.6
|
||||
|
||||
# 하단 기대효과
|
||||
add_textbox(slide, Inches(0.5), Inches(5.0), Inches(9), Inches(0.3),
|
||||
"기대효과", font_size=12, color=PRIMARY, bold=True)
|
||||
effects = "• 시수 입력 오류 90% 감소 • 결재 처리 시간 70% 단축 • 실시간 프로젝트 시수 현황 파악 • WBS 변경 추적 자동화"
|
||||
add_textbox(slide, Inches(0.5), Inches(5.3), Inches(9), Inches(0.5),
|
||||
effects, font_size=10, color=BODY)
|
||||
|
||||
# ============================================================
|
||||
# 04. 시스템 구성도
|
||||
# ============================================================
|
||||
section_slide(prs, 4, "시스템 구성도", "전체 아키텍처, 기술 스택, 네트워크 구성")
|
||||
|
||||
# 전체 아키텍처
|
||||
slide = content_slide(prs, "04-1. 전체 시스템 아키텍처")
|
||||
|
||||
# 클라이언트 영역
|
||||
add_textbox(slide, Inches(0.3), Inches(0.9), Inches(2), Inches(0.3),
|
||||
"클라이언트", font_size=10, color=BODY, bold=True)
|
||||
clients = ["WBX No-Code", "Web Browser", "Mobile"]
|
||||
cx = 0.3
|
||||
for c in clients:
|
||||
add_box(slide, Inches(cx), Inches(1.2), Inches(1.4), Inches(0.5),
|
||||
c, fill_color=RGBColor(200, 220, 255), font_size=8)
|
||||
cx += 1.6
|
||||
|
||||
# Nginx
|
||||
add_box(slide, Inches(5.5), Inches(1.2), Inches(1.8), Inches(0.5),
|
||||
"Nginx (SSL/LB)", fill_color=RGBColor(220, 220, 220), font_color=DARK, font_size=9)
|
||||
|
||||
# Spring Boot WTM
|
||||
add_multiline_box(slide, Inches(0.3), Inches(2.2), Inches(4.5), Inches(2.8),
|
||||
"Spring Boot WTM-API (wbx-spring 프레임워크)", [
|
||||
"JWT 인증 / Azure Entra ID SSO / MFA",
|
||||
"RBAC 권한 (dept_scope)",
|
||||
"통합 결재 엔진 (Handler Registry)",
|
||||
"SSE 실시간 알림",
|
||||
"시수 입력 3종 + 규칙 엔진",
|
||||
"WBS/TEAL 관리 + P6 파서",
|
||||
"리포트 (QueryDSL)",
|
||||
"SAP BTP HR 연동",
|
||||
], fill_color=RGBColor(230, 240, 255))
|
||||
|
||||
# WBX FastAPI
|
||||
add_multiline_box(slide, Inches(5.2), Inches(2.2), Inches(2.3), Inches(2.0),
|
||||
"WBX FastAPI (선택)", [
|
||||
"문서관리",
|
||||
"이메일",
|
||||
"게시판/일정",
|
||||
"/api/gw/*"
|
||||
], fill_color=RGBColor(245, 240, 255))
|
||||
|
||||
# DB
|
||||
add_multiline_box(slide, Inches(7.8), Inches(2.2), Inches(2), Inches(1.5),
|
||||
"Database", [
|
||||
"wtm_db (Azure SQL)",
|
||||
"wbx_gw (MySQL)",
|
||||
"프로필 전환 지원"
|
||||
], fill_color=RGBColor(255, 245, 230))
|
||||
|
||||
# Redis
|
||||
add_box(slide, Inches(7.8), Inches(4.0), Inches(1), Inches(0.5),
|
||||
"Redis", fill_color=GREEN_BG, font_size=9)
|
||||
|
||||
# Blob Storage
|
||||
add_box(slide, Inches(8.9), Inches(4.0), Inches(1), Inches(0.5),
|
||||
"Storage", fill_color=ORANGE_BG, font_size=9)
|
||||
|
||||
# SSO
|
||||
add_box(slide, Inches(5.2), Inches(4.5), Inches(2.3), Inches(0.5),
|
||||
"Azure Entra ID (SSO+MFA)", fill_color=RGBColor(255, 235, 235), font_size=8)
|
||||
|
||||
# SAP BTP
|
||||
add_box(slide, Inches(0.3), Inches(5.3), Inches(2), Inches(0.5),
|
||||
"SAP BTP (HR)", fill_color=RGBColor(255, 240, 220), font_size=9)
|
||||
|
||||
# P6
|
||||
add_box(slide, Inches(2.5), Inches(5.3), Inches(1.5), Inches(0.5),
|
||||
"P6 (WBS)", fill_color=RGBColor(255, 240, 220), font_size=9)
|
||||
|
||||
# Cognite
|
||||
add_box(slide, Inches(4.2), Inches(5.3), Inches(1.5), Inches(0.5),
|
||||
"Cognite", fill_color=RGBColor(255, 240, 220), font_size=9)
|
||||
|
||||
# 기술 스택 슬라이드
|
||||
slide = content_slide(prs, "04-2. 기술 스택")
|
||||
tech_data = [
|
||||
["레이어", "기술", "버전/비고"],
|
||||
["Language", "Java", "21 (LTS)"],
|
||||
["Framework", "Spring Boot + wbx-spring", "3.5.0"],
|
||||
["Security", "Spring Security 6 + OAuth2", "Entra ID SSO + MFA"],
|
||||
["ORM", "Spring Data JPA + QueryDSL", "Hibernate 6"],
|
||||
["DB", "Azure SQL / Oracle / MySQL / PG", "프로필 전환"],
|
||||
["Migration", "Flyway", "DBMS별 DDL 분리"],
|
||||
["Cache", "Redis", "세션 + 캐시"],
|
||||
["WEB/WAS", "Nginx + Embedded Tomcat", "이중화"],
|
||||
["CI/CD", "GitHub Actions + Azure DevOps", "승인 게이트"],
|
||||
["형상관리", "GitHub + Azure Repos", "SemVer"],
|
||||
["모니터링", "Jennifer + Actuator + Prometheus", "한화 표준"],
|
||||
["Cloud", "Microsoft Azure", "Hybrid Security Zone"],
|
||||
]
|
||||
add_table(slide, Inches(0.5), Inches(1.0), Inches(9),
|
||||
tech_data, col_widths=[Inches(2), Inches(3.5), Inches(3.5)])
|
||||
|
||||
# ============================================================
|
||||
# 05. 기능 정의서
|
||||
# ============================================================
|
||||
section_slide(prs, 5, "기능 정의서", "시수 입력 3종, WBS/TEAL, 결재 워크플로우, 리포트")
|
||||
|
||||
# 시수 입력
|
||||
slide = content_slide(prs, "05-1. 시수 입력 (3종 통합 UI)")
|
||||
|
||||
# 탭 UI 도식
|
||||
tabs = ["Non-Project", "Other Project", "EPC Project"]
|
||||
tx = 0.5
|
||||
for i, tab in enumerate(tabs):
|
||||
color = ACCENT if i == 2 else RGBColor(180, 180, 180)
|
||||
add_box(slide, Inches(tx), Inches(1.0), Inches(2), Inches(0.4),
|
||||
tab, fill_color=RGBColor(230, 240, 255) if i == 2 else RGBColor(245, 245, 245),
|
||||
font_color=color, font_size=9)
|
||||
tx += 2.2
|
||||
|
||||
# EPC 입력 폼 도식
|
||||
fields = [
|
||||
"프로젝트: [EPU-2025-001 ▼]",
|
||||
"WBS: [E.01.03 Piping Detail ▼]",
|
||||
"TEAL: [Detail Engineering ▼]",
|
||||
]
|
||||
fy = 1.6
|
||||
for field in fields:
|
||||
add_textbox(slide, Inches(0.8), Inches(fy), Inches(6), Inches(0.3),
|
||||
field, font_size=10, color=DARK)
|
||||
fy += 0.35
|
||||
|
||||
# 주간 그리드
|
||||
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "합계"]
|
||||
vals = ["8.0", "8.0", "8.0", "8.0", "4.0", "-", "36.0h"]
|
||||
grid_data = [days, vals]
|
||||
add_table(slide, Inches(0.5), Inches(2.9), Inches(6.5),
|
||||
grid_data, col_widths=[Inches(0.9)] * 7)
|
||||
|
||||
# 규칙 엔진 박스
|
||||
add_multiline_box(slide, Inches(7.3), Inches(1.0), Inches(2.5), Inches(2.5),
|
||||
"규칙 엔진", [
|
||||
"일 최소 8시간",
|
||||
"주 최대 52시간",
|
||||
"Activity 최소 1개",
|
||||
"Location별 규칙",
|
||||
"미래 날짜 입력 불가",
|
||||
"초과 시 경고/차단",
|
||||
], fill_color=ORANGE_BG)
|
||||
|
||||
# 하단 설명
|
||||
add_textbox(slide, Inches(0.5), Inches(3.6), Inches(6), Inches(0.3),
|
||||
"[임시 저장] [결재 요청] 주간 합계: 36.0 / 52h",
|
||||
font_size=9, color=BODY)
|
||||
|
||||
ts_desc = [
|
||||
("Non-Project (No.55~59)", "Overhead Type 선택 → TEAL Activity → 시간 입력. WBS/프로젝트 입력 불가"),
|
||||
("Other Project (No.60~62)", "프로젝트 선택 → Canonical WBS L2~4 → Activity → 시간. P6 연동 불필요"),
|
||||
("EPC Project (No.64~69)", "프로젝트 → Canonical WBS L2~5 → TEAL → 시간. Revision 관리(PH1-2)"),
|
||||
]
|
||||
fy = 4.2
|
||||
for title, desc in ts_desc:
|
||||
add_textbox(slide, Inches(0.5), Inches(fy), Inches(9), Inches(0.2),
|
||||
title, font_size=9, color=PRIMARY, bold=True)
|
||||
add_textbox(slide, Inches(0.5), Inches(fy + 0.2), Inches(9), Inches(0.25),
|
||||
desc, font_size=8, color=BODY)
|
||||
fy += 0.5
|
||||
|
||||
# WBS/TEAL
|
||||
slide = content_slide(prs, "05-2. WBS · TEAL 관리")
|
||||
|
||||
# Canonical WBS 구조
|
||||
add_textbox(slide, Inches(0.5), Inches(0.9), Inches(4), Inches(0.3),
|
||||
"Canonical WBS 구조 (No.34)", font_size=12, color=PRIMARY, bold=True)
|
||||
wbs_levels = [
|
||||
["Level", "구분", "설명", "예시"],
|
||||
["L1", "Project", "프로젝트", "EPU-2025-001"],
|
||||
["L2", "Phase", "Engineering/Procurement/Construction/...", "Engineering"],
|
||||
["L3", "Asset/Area", "자산 또는 영역", "Hull Structure"],
|
||||
["L4", "Work/Discipline", "작업 또는 Discipline", "Piping"],
|
||||
["L5", "Deliverable", "산출물 (Eng/SCM only)", "Detail Drawing"],
|
||||
]
|
||||
add_table(slide, Inches(0.5), Inches(1.3), Inches(9),
|
||||
wbs_levels, col_widths=[Inches(0.8), Inches(1.5), Inches(4), Inches(2.5)])
|
||||
|
||||
# WBS 흐름
|
||||
add_textbox(slide, Inches(0.5), Inches(3.5), Inches(4), Inches(0.3),
|
||||
"WBS 업로드 흐름", font_size=12, color=PRIMARY, bold=True)
|
||||
flow_steps = ["P6 Export\n(PM 업로드)", "WBS 파싱\n(Level 1~5)", "Canonical\nWBS 매핑", "PCM 승인\n버전 등록", "TEAL\nActivity 연결"]
|
||||
fx = 0.3
|
||||
for i, step in enumerate(flow_steps):
|
||||
add_box(slide, Inches(fx), Inches(3.9), Inches(1.6), Inches(0.8),
|
||||
step, fill_color=RGBColor(230, 240, 255) if i % 2 == 0 else GREEN_BG,
|
||||
font_size=8, font_color=DARK)
|
||||
if i < len(flow_steps) - 1:
|
||||
add_textbox(slide, Inches(fx + 1.6), Inches(4.05), Inches(0.3), Inches(0.3),
|
||||
"→", font_size=16, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
fx += 1.9
|
||||
|
||||
# 결재 워크플로우
|
||||
slide = content_slide(prs, "05-3. 결재 워크플로우")
|
||||
|
||||
# 결재 흐름도
|
||||
steps = [
|
||||
("User\n시수 입력", RGBColor(200, 220, 255)),
|
||||
("제출\n(결재 요청)", RGBColor(255, 240, 200)),
|
||||
("DL\n1차 결재", RGBColor(220, 240, 220)),
|
||||
("PM\n최종 결재", RGBColor(220, 240, 220)),
|
||||
("승인 완료\n(APPROVED)", RGBColor(200, 255, 200)),
|
||||
]
|
||||
sx = 0.3
|
||||
for i, (label, color) in enumerate(steps):
|
||||
add_box(slide, Inches(sx), Inches(1.3), Inches(1.6), Inches(0.9),
|
||||
label, fill_color=color, font_color=DARK, font_size=10)
|
||||
if i < len(steps) - 1:
|
||||
add_textbox(slide, Inches(sx + 1.6), Inches(1.5), Inches(0.3), Inches(0.3),
|
||||
"→", font_size=18, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
sx += 1.9
|
||||
|
||||
# 반려 흐름
|
||||
add_box(slide, Inches(3.5), Inches(2.5), Inches(2.5), Inches(0.5),
|
||||
"반려 → User 수정 후 재제출", fill_color=RGBColor(255, 230, 230),
|
||||
font_color=RED_ACCENT, font_size=9)
|
||||
|
||||
# 결재 기능 표
|
||||
approval_data = [
|
||||
["기능", "설명", "요구사항"],
|
||||
["시수 결재 요청", "User가 주간 시수 작성 후 제출", "No.70"],
|
||||
["승인/반려 + 코멘트", "DL/PM이 승인 또는 반려 (사유 필수)", "No.72"],
|
||||
["일괄 승인", "여러 시수를 한번에 승인", "No.73"],
|
||||
["결재 이력 조회", "과거 결재 내역 전체 조회", "No.74"],
|
||||
["초과 하이라이트", "기준 초과 시 결재자에게 강조 표시", "No.75 (PH1-2)"],
|
||||
["미완료 알림", "미입력/미제출 시수 자동 리마인더", "No.70"],
|
||||
]
|
||||
add_table(slide, Inches(0.3), Inches(3.3), Inches(9.4),
|
||||
approval_data, col_widths=[Inches(2), Inches(5), Inches(1.5)])
|
||||
|
||||
# 리포트
|
||||
slide = content_slide(prs, "05-4. 리포트")
|
||||
report_data = [
|
||||
["리포트", "설명", "Phase", "필터"],
|
||||
["프로젝트별 시수 분석", "프로젝트별 실제 투입 Manhour", "PH1-1", "기간, 프로젝트, Discipline"],
|
||||
["WBS Level별 시수 분석", "Canonical WBS Level별 Manhour", "PH1-1", "기간, 프로젝트, WBS Level"],
|
||||
["Phase별 시수 비율", "Engineering/Procurement 등 비율", "PH1-2", "기간, 프로젝트"],
|
||||
["Non-Project 비율", "Overhead Manhour 투입 비율", "PH1-2", "기간, 부서"],
|
||||
["WBS 버전 이력", "WBS 변경 이력 조회", "PH1-2", "프로젝트, 버전"],
|
||||
["Discipline 생산성", "Discipline별 생산성 분석", "PH2", "기간, Discipline"],
|
||||
]
|
||||
add_table(slide, Inches(0.3), Inches(1.0), Inches(9.4),
|
||||
report_data, col_widths=[Inches(2.2), Inches(3.5), Inches(1), Inches(2)])
|
||||
|
||||
# ============================================================
|
||||
# 06. 데이터베이스 설계
|
||||
# ============================================================
|
||||
section_slide(prs, 6, "데이터베이스 설계", "ERD, 주요 테이블 14개, Flyway 마이그레이션")
|
||||
|
||||
slide = content_slide(prs, "06-1. ERD 개요 (wtm_db)")
|
||||
|
||||
# ERD 도식
|
||||
entities = [
|
||||
("users", Inches(0.3), Inches(1.0), ["id, employee_number", "email, full_name", "department, discipline", "is_active, mfa_enabled"]),
|
||||
("projects", Inches(3.3), Inches(1.0), ["id, project_code", "name, project_type", "status, pm_user_id"]),
|
||||
("canonical_wbs", Inches(6.3), Inches(1.0), ["id, project_id", "wbs_code, level", "name, discipline"]),
|
||||
("timesheets", Inches(0.3), Inches(3.2), ["id, user_id", "week_start_date", "status, total_hours"]),
|
||||
("timesheet_entries", Inches(3.3), Inches(3.2), ["id, timesheet_id", "entry_type, entry_date", "hours, epc_project_id", "canonical_wbs_id"]),
|
||||
("tt_approvals", Inches(6.3), Inches(3.2), ["id, timesheet_id", "requester_id, status", "submitted_at"]),
|
||||
("tt_approval_lines", Inches(6.3), Inches(5.0), ["id, approval_id", "approver_id, order", "role_code, status"]),
|
||||
("wbs_versions", Inches(3.3), Inches(5.0), ["id, project_id", "version_number", "effective_date"]),
|
||||
]
|
||||
|
||||
for name, x, y, fields in entities:
|
||||
h = Inches(0.2 + len(fields) * 0.2)
|
||||
add_multiline_box(slide, x, y, Inches(2.7), h, name, fields, fill_color=LIGHT_BG)
|
||||
|
||||
# DB 테이블 목록
|
||||
slide = content_slide(prs, "06-2. 주요 테이블 목록")
|
||||
db_data = [
|
||||
["테이블", "용도", "주요 컬럼"],
|
||||
["users", "사용자 (HR 필드 포함)", "employee_number, business_unit~part (5레벨)"],
|
||||
["projects", "프로젝트 (EPC/Other/Non)", "project_code, type, status, pm_user_id"],
|
||||
["wbs_versions", "P6 WBS 스냅샷", "version_number, effective_date, source_filename"],
|
||||
["wbs_nodes", "WBS 트리 (L1~L5)", "wbs_code, level, parent_id, planned_hours"],
|
||||
["canonical_wbs", "정규 WBS", "wbs_code, mapped_p6_code, discipline"],
|
||||
["teal_entries", "TEAL Activity", "canonical_wbs_id, activity_code, activity_name"],
|
||||
["timesheets", "주간 시수 헤더", "user_id, week_start_date, status, total_hours"],
|
||||
["timesheet_entries", "일별 시수 상세", "entry_type, entry_date, hours, remark"],
|
||||
["tt_approvals", "결재 요청", "timesheet_id, requester_id, status"],
|
||||
["tt_approval_lines", "결재 라인 (DL/PM)", "approver_id, approval_order, role_code"],
|
||||
["work_rules", "근무 규칙", "location_code, min_daily, max_weekly"],
|
||||
["overhead_types", "Overhead 유형", "code, name (SA 관리)"],
|
||||
["project_assignments", "인력 배정", "project_id, user_id, discipline"],
|
||||
]
|
||||
add_table(slide, Inches(0.2), Inches(1.0), Inches(9.6),
|
||||
db_data, col_widths=[Inches(2), Inches(2.5), Inches(4.5)])
|
||||
|
||||
# ============================================================
|
||||
# 07. API 설계
|
||||
# ============================================================
|
||||
section_slide(prs, 7, "API 설계", "REST API 79개 — /api/wtm/ prefix")
|
||||
|
||||
slide = content_slide(prs, "07. REST API 스펙 (79개, /api/wtm/)")
|
||||
api_data = [
|
||||
["모듈", "PH1-1", "PH1-2", "PH2", "합계", "주요 Endpoint"],
|
||||
["Auth", "8", "0", "0", "8", "/auth/login, /auth/sso, /auth/mfa/*"],
|
||||
["Users", "7", "1", "0", "8", "/users, /users/upload/internal"],
|
||||
["Projects", "7", "0", "0", "7", "/projects, /projects/my, /projects/{id}/members"],
|
||||
["WBS", "6", "1", "0", "7", "/projects/{id}/wbs/upload, /canonical-wbs"],
|
||||
["TEAL", "4", "0", "0", "4", "/projects/{id}/teal/active"],
|
||||
["Timesheets", "8", "0", "0", "8", "/timesheets/week, /entries/batch, /upload"],
|
||||
["Approvals", "7", "1", "0", "8", "/approvals/unified/action/{type}/{id}/*"],
|
||||
["Reports", "4", "2", "3", "9", "/reports/project-hours, /wbs-hours"],
|
||||
["Resource", "8", "0", "0", "8", "/projects/{id}/assignments"],
|
||||
["Config", "4", "0", "0", "4", "/overhead-types, /work-rules"],
|
||||
["Integration", "1", "1", "1", "3", "/integration/hr/sync, /cognite/export"],
|
||||
["Notification", "3", "0", "0", "3", "/notifications/stream (SSE)"],
|
||||
["합계", "67", "6", "6", "79", ""],
|
||||
]
|
||||
add_table(slide, Inches(0.2), Inches(1.0), Inches(9.6),
|
||||
api_data, col_widths=[Inches(1.2), Inches(0.7), Inches(0.7), Inches(0.7), Inches(0.7), Inches(5)])
|
||||
|
||||
# ============================================================
|
||||
# 08. 화면 구성
|
||||
# ============================================================
|
||||
section_slide(prs, 8, "화면 구성", "전체 49개 화면 — PH1-1차 26 / PH1-2차 11 / PH2 12")
|
||||
|
||||
slide = content_slide(prs, "08. 전체 화면 목록 (49개)")
|
||||
screen_data = [
|
||||
["그룹", "PH1-1차 (26개)", "PH1-2차 (11개)", "PH2 (12개)"],
|
||||
["로그인/인증", "로그인, SSO, PW찾기, PW만료 (4)", "", ""],
|
||||
["홈/권한", "역할별 홈 5종 (5)", "권한라우팅, SA로그 (2)", ""],
|
||||
["사용자/인력", "내부/외주 업로드, 관리 (3)", "", "외주 개별입력 (1)"],
|
||||
["프로젝트/WBS", "등록, P6, WBS, TEAL 등 (6)", "WBS비교, 시수조회 (3)", ""],
|
||||
["시수 입력", "Non/Other/EPC 입력, Excel (4)", "EPC Revision (1)", "벤치마킹, Rate (2)"],
|
||||
["결재", "결재요청, 목록, 일괄승인 (3)", "초과알림, MFA (2)", ""],
|
||||
["리포트", "프로젝트별, WBS별 (2)", "Phase비율, NP비율 (2)", "RCP 3종, Discipline 등 (5)"],
|
||||
["기타", "", "HR배치 (1)", "외주포털, Favorite 등 (4)"],
|
||||
]
|
||||
add_table(slide, Inches(0.2), Inches(1.0), Inches(9.6),
|
||||
screen_data, col_widths=[Inches(1.5), Inches(3), Inches(2.5), Inches(2.5)])
|
||||
|
||||
# ============================================================
|
||||
# 09. 인터페이스 설계
|
||||
# ============================================================
|
||||
section_slide(prs, 9, "인터페이스 설계", "SAP BTP (HR), P6 (WBS 파일), Cognite (Export)")
|
||||
|
||||
slide = content_slide(prs, "09. 외부 시스템 인터페이스")
|
||||
|
||||
# SAP BTP
|
||||
add_multiline_box(slide, Inches(0.3), Inches(1.0), Inches(2.5), Inches(2),
|
||||
"SAP SuccessFactors", [
|
||||
"HR Master Data",
|
||||
"Employee Central",
|
||||
"OData API"
|
||||
], fill_color=RGBColor(255, 245, 220))
|
||||
|
||||
add_textbox(slide, Inches(2.8), Inches(1.7), Inches(0.8), Inches(0.3),
|
||||
"→ BTP →", font_size=10, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
add_multiline_box(slide, Inches(3.6), Inches(1.0), Inches(2.5), Inches(2),
|
||||
"SAP BTP (CPI)", [
|
||||
"OData → REST 변환",
|
||||
"필드 매핑",
|
||||
"일 1회 배치 (PH1-2)",
|
||||
"실시간 이벤트 (PH2)"
|
||||
], fill_color=RGBColor(240, 235, 255))
|
||||
|
||||
add_textbox(slide, Inches(6.1), Inches(1.7), Inches(0.5), Inches(0.3),
|
||||
"→", font_size=14, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
add_multiline_box(slide, Inches(6.6), Inches(1.0), Inches(3), Inches(2),
|
||||
"WTM Spring Boot", [
|
||||
"POST /api/wtm/integration/hr/sync",
|
||||
"HrSyncService",
|
||||
"사용자 자동 동기화",
|
||||
"PH1-1: Excel 수동 업로드"
|
||||
], fill_color=RGBColor(230, 240, 255))
|
||||
|
||||
# P6
|
||||
add_box(slide, Inches(0.3), Inches(3.5), Inches(2.5), Inches(0.7),
|
||||
"P6 (WBS Export)\n파일 기반 (No I/F)", fill_color=RGBColor(255, 245, 220),
|
||||
font_color=DARK, font_size=9)
|
||||
|
||||
add_textbox(slide, Inches(2.8), Inches(3.65), Inches(1), Inches(0.3),
|
||||
"→ 파일 →", font_size=9, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
add_box(slide, Inches(3.8), Inches(3.5), Inches(3), Inches(0.7),
|
||||
"WTM: P6 WBS 파서\nPM 업로드 → PCM 승인", fill_color=RGBColor(230, 240, 255),
|
||||
font_color=DARK, font_size=9)
|
||||
|
||||
# Cognite
|
||||
add_box(slide, Inches(0.3), Inches(4.5), Inches(2.5), Inches(0.7),
|
||||
"Cognite (PH2)\nExtractor 서버", fill_color=RGBColor(255, 245, 220),
|
||||
font_color=DARK, font_size=9)
|
||||
|
||||
add_textbox(slide, Inches(2.8), Inches(4.65), Inches(1), Inches(0.3),
|
||||
"← Export", font_size=9, color=ACCENT, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
add_box(slide, Inches(3.8), Inches(4.5), Inches(5.5), Inches(0.7),
|
||||
"Export: Employee + Project + WBS + Time Fact + Mapping Version Metadata",
|
||||
fill_color=RGBColor(230, 240, 255), font_color=DARK, font_size=8)
|
||||
|
||||
# ============================================================
|
||||
# 10. 보안 설계
|
||||
# ============================================================
|
||||
section_slide(prs, 10, "보안 설계", "한화오션 표준 보안 SW 12종, Azure Entra ID SSO + MFA")
|
||||
|
||||
slide = content_slide(prs, "10. 한화오션 표준 보안 SW")
|
||||
sec_data = [
|
||||
["분류", "SW", "용도", "구성"],
|
||||
["서버 보안", "HIWARE", "서버 접근 제어", "Agent 설치"],
|
||||
["서버 보안", "V3 (AhnLab)", "서버 백신", "Agent 설치"],
|
||||
["서버 보안", "Secuver TOS", "파일 무결성", "Agent 설치"],
|
||||
["DB 보안", "Cubeone", "DB 암호화 (컬럼)", "연동"],
|
||||
["DB 보안", "Dbsafer", "DB 접근 제어/감사", "Proxy"],
|
||||
["클라우드", "Azure Defender", "위협 탐지", "Native"],
|
||||
["모니터링", "onTune", "SMS (서버)", "Agent"],
|
||||
["모니터링", "Maxguage", "DB 모니터링", "Proxy"],
|
||||
["모니터링", "Jennifer", "WAS APM", "-javaagent"],
|
||||
["인증", "Azure Entra ID", "SSO + Conditional Access", "OAuth2 OIDC"],
|
||||
["인증", "TOTP (MFA)", "외부사용자 2-Way 인증", "Google/MS Auth"],
|
||||
]
|
||||
add_table(slide, Inches(0.3), Inches(1.0), Inches(9.4),
|
||||
sec_data, col_widths=[Inches(1.3), Inches(2), Inches(3), Inches(2.5)])
|
||||
|
||||
# ============================================================
|
||||
# 11. 단계별 추진 일정
|
||||
# ============================================================
|
||||
section_slide(prs, 11, "단계별 추진 일정", "PH1-1차(9주) + PH1-2차(4주) = 15 M/M")
|
||||
|
||||
slide = content_slide(prs, "11-1. 전체 일정 (Gantt)")
|
||||
|
||||
# Gantt-like 도식
|
||||
months = ["4월", "5월", "6월", "7월~"]
|
||||
mx = 2.5
|
||||
for m in months:
|
||||
add_box(slide, Inches(mx), Inches(1.0), Inches(1.8), Inches(0.35),
|
||||
m, fill_color=PRIMARY, font_color=WHITE, font_size=10)
|
||||
mx += 1.8
|
||||
|
||||
phases = [
|
||||
("분석·설계", 2.5, 2.5, RGBColor(100, 160, 230)),
|
||||
("BE 개발", 3.0, 5.5, RGBColor(70, 140, 200)),
|
||||
("FE 개발", 3.5, 5.0, RGBColor(90, 170, 220)),
|
||||
("QA·UAT", 6.5, 2.0, RGBColor(230, 160, 60)),
|
||||
("PH1-2차", 6.1, 3.6, RGBColor(140, 200, 140)),
|
||||
]
|
||||
gy = 1.6
|
||||
for name, start, width, color in phases:
|
||||
add_box(slide, Inches(start), Inches(gy), Inches(width), Inches(0.35),
|
||||
name, fill_color=color, font_color=WHITE, font_size=9)
|
||||
gy += 0.45
|
||||
|
||||
# 마일스톤
|
||||
milestones = [
|
||||
("4/10", "설계 확정"),
|
||||
("5/7", "BE API 완료"),
|
||||
("5/20", "FE 완료"),
|
||||
("5/31", "★ PH1-1 오픈"),
|
||||
("6/30", "★ PH1-2 오픈"),
|
||||
]
|
||||
add_textbox(slide, Inches(0.5), Inches(4.0), Inches(9), Inches(0.3),
|
||||
"주요 마일스톤", font_size=12, color=PRIMARY, bold=True)
|
||||
my = 4.4
|
||||
for date, desc in milestones:
|
||||
add_textbox(slide, Inches(0.5), Inches(my), Inches(1.5), Inches(0.25),
|
||||
date, font_size=10, color=ACCENT, bold=True)
|
||||
add_textbox(slide, Inches(2.0), Inches(my), Inches(7), Inches(0.25),
|
||||
desc, font_size=10, color=DARK)
|
||||
my += 0.3
|
||||
|
||||
# 인력 투입
|
||||
slide = content_slide(prs, "11-2. 인력 투입 계획")
|
||||
manpower = [
|
||||
["역할", "PH1-1차 (9주)", "PH1-2차 (4주)", "담당"],
|
||||
["BE 시니어 / 기술리드", "2.0 M/M", "1.0 M/M", "아키텍처, SSO, 결재, 코드리뷰"],
|
||||
["풀스택 엔지니어 ①", "2.0 M/M", "1.0 M/M", "WBS, TEAL, 프로젝트"],
|
||||
["풀스택 엔지니어 ②", "2.0 M/M", "1.0 M/M", "시수 입력 3종, 규칙 엔진"],
|
||||
["풀스택 엔지니어 ③", "2.0 M/M", "1.0 M/M", "리포트, 로그인, 사용자"],
|
||||
["DevOps (파트타임)", "1.0 M/M", "0.5 M/M", "Azure 인프라, CI/CD"],
|
||||
["QA 겸임", "1.0 M/M", "0.5 M/M", "통합테스트, UAT 지원"],
|
||||
["합계", "~10.0 M/M", "~5.0 M/M", "총 PH1: ~15 M/M"],
|
||||
]
|
||||
add_table(slide, Inches(0.3), Inches(1.0), Inches(9.4),
|
||||
manpower, col_widths=[Inches(2.2), Inches(1.5), Inches(1.5), Inches(4)])
|
||||
|
||||
# ============================================================
|
||||
# 마지막
|
||||
# ============================================================
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
bg = slide.background.fill
|
||||
bg.solid()
|
||||
bg.fore_color.rgb = PRIMARY
|
||||
add_textbox(slide, Inches(1), Inches(2.5), Inches(8), Inches(1),
|
||||
"감사합니다", font_size=36, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
|
||||
add_textbox(slide, Inches(1), Inches(3.8), Inches(8), Inches(0.5),
|
||||
"한화오션 EPU Time Tracking 시스템을\n함께 성공적으로 완수하겠습니다.",
|
||||
font_size=14, color=RGBColor(180, 200, 230), align=PP_ALIGN.CENTER)
|
||||
add_textbox(slide, Inches(1), Inches(5), Inches(8), Inches(0.5),
|
||||
"아큐라시스템 | accura@accurasoft.co.kr",
|
||||
font_size=12, color=RGBColor(150, 170, 200), align=PP_ALIGN.CENTER)
|
||||
|
||||
prs.save(OUTPUT)
|
||||
size_kb = os.path.getsize(OUTPUT) // 1024
|
||||
try:
|
||||
print(f"Generated: {OUTPUT} ({size_kb} KB)")
|
||||
except UnicodeEncodeError:
|
||||
print(f"Generated: PPT ({size_kb} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
새 Issue에서 참조
사용자 차단