feat: React 18 프론트엔드 추가 및 프로젝트 구조 정리

- wtm-frontend → wtm-frontend-vue 이름 변경
- wtm-frontend-react 추가 (React 18 + PrimeReact + Zustand)
  - 동일한 모듈 구조 및 API 연동 (Vue 버전과 기능 동일)
  - Vue:5173 / React:5174 포트 분리
- 개발자 가이드에 React 프론트엔드 안내 추가
- .gitignore: Claude/OMC, 문서 생성 스크립트, package-lock 제외
- 불필요 파일 git 추적 제거 (.omc, generate_*.py, regenerate_*.py)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
2026-03-30 20:50:23 +09:00
부모 dd263a6e46
커밋 cda5f9591e
212개의 변경된 파일3633개의 추가작업 그리고 5244개의 파일을 삭제

파일 보기

@@ -1,838 +0,0 @@
"""
한화오션 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()

파일 보기

@@ -1,372 +0,0 @@
"""
WBX Spring Framework PDF 가이드 재생성 스크립트.
Docker/Redis → Embedded Redis 변경 사항 반영.
사용법: python regenerate_pdfs.py
"""
import os
import copy
from PyPDF2 import PdfReader, PdfWriter
from fpdf import FPDF
DOCS_DIR = os.path.dirname(os.path.abspath(__file__))
FONT_PATH = r"C:\Windows\Fonts\malgun.ttf"
FONT_BOLD_PATH = r"C:\Windows\Fonts\malgunbd.ttf"
class KoreanPDF(FPDF):
"""Korean-capable PDF with consistent styling."""
def __init__(self):
super().__init__()
self.add_font("malgun", "", FONT_PATH)
self.add_font("malgun", "B", FONT_BOLD_PATH)
self.set_auto_page_break(auto=True, margin=20)
def header_line(self, title):
self.set_font("malgun", "B", 9)
self.set_text_color(100, 100, 100)
self.cell(0, 6, title, ln=True)
self.set_draw_color(30, 60, 120)
self.set_line_width(0.5)
self.line(10, self.get_y(), 200, self.get_y())
self.ln(4)
def section_title(self, text):
self.set_font("malgun", "B", 14)
self.set_text_color(30, 60, 120)
self.cell(0, 10, text, ln=True)
self.ln(2)
def sub_title(self, text):
self.set_font("malgun", "B", 11)
self.set_text_color(40, 40, 40)
self.cell(0, 8, text, ln=True)
self.ln(1)
def body(self, text):
self.set_font("malgun", "", 10)
self.set_text_color(60, 60, 60)
self.multi_cell(0, 6, text)
self.ln(2)
def code_block(self, text):
self.set_fill_color(245, 245, 245)
self.set_font("malgun", "", 9)
self.set_text_color(40, 40, 40)
x = self.get_x()
self.set_x(x + 5)
self.multi_cell(180, 5.5, text, fill=True)
self.ln(3)
def bullet(self, text):
self.set_font("malgun", "", 10)
self.set_text_color(60, 60, 60)
x = self.get_x()
self.set_x(x + 5)
self.cell(5, 6, "\u2022")
self.multi_cell(170, 6, text)
def note_box(self, text):
self.set_fill_color(255, 248, 220)
self.set_draw_color(200, 180, 100)
self.set_font("malgun", "B", 9)
self.set_text_color(120, 90, 0)
y = self.get_y()
self.rect(12, y, 186, 20, style="DF")
self.set_xy(15, y + 3)
self.multi_cell(180, 5, text)
self.set_y(y + 23)
def page_number(self):
self.set_y(-15)
self.set_font("malgun", "", 8)
self.set_text_color(150, 150, 150)
self.cell(0, 10, f"- {self.page_no()} -", align="C")
def replace_pages(src_path, replacements, out_path):
"""Replace specific pages in a PDF with new fpdf2-generated pages.
replacements: dict of {page_index: fpdf_generation_function}
"""
reader = PdfReader(src_path)
writer = PdfWriter()
# Generate replacement pages
replacement_pages = {}
for page_idx, gen_func in replacements.items():
pdf = gen_func()
tmp = os.path.join(DOCS_DIR, f"_tmp_page_{page_idx}.pdf")
pdf.output(tmp)
tmp_reader = PdfReader(tmp)
replacement_pages[page_idx] = tmp_reader.pages[0]
# Build output
for i, page in enumerate(reader.pages):
if i in replacement_pages:
writer.add_page(replacement_pages[i])
else:
writer.add_page(page)
with open(out_path, "wb") as f:
writer.write(f)
# Cleanup temp files
for page_idx in replacements:
tmp = os.path.join(DOCS_DIR, f"_tmp_page_{page_idx}.pdf")
if os.path.exists(tmp):
os.remove(tmp)
print(f" [OK] {os.path.basename(out_path)}")
# ============================================================
# 1. 설치가이드_OnPremise.pdf — Page 3 (requirements), Page 9 (Redis)
# ============================================================
def onprem_page3():
"""Page 3: 사전 요구사항 - Redis를 선택으로 변경."""
pdf = KoreanPDF()
pdf.add_page()
pdf.header_line("WBX Spring Framework | On-Premise 설치 가이드")
pdf.section_title("1. 사전 요구사항 \u00b7 서버 사양")
pdf.sub_title("1-1. 최소 서버 사양")
pdf.body("항목 최소 사양 권장 사양\n"
"OS RHEL 8+ / Ubuntu 22.04+ RHEL 9 / Rocky Linux 9\n"
"CPU 4 vCPU 8 vCPU\n"
"RAM 8 GB 16 GB\n"
"Disk 50 GB SSD 100 GB SSD (RAID)\n"
"Network 1 Gbps 10 Gbps")
pdf.sub_title("1-2. 필수 소프트웨어")
pdf.body("소프트웨어 버전 용도\n"
"JDK 21 (Temurin LTS) Spring Boot 런타임\n"
"DB Oracle 19c+ 등 애플리케이션 데이터\n"
"Nginx 1.24+ 리버스 프록시, SSL")
pdf.sub_title("1-2b. 선택 소프트웨어")
pdf.body("소프트웨어 버전 용도\n"
"Redis 7.x 캐시 (미설치 시 Embedded Redis 자동 구동)")
pdf.note_box("NOTE: Redis를 별도 설치하지 않아도 WBX Spring에 내장된 Embedded Redis가\n"
"자동으로 시작됩니다. 운영 환경에서는 성능을 위해 외부 Redis 설치를 권장합니다.")
pdf.sub_title("1-3. 네트워크 포트")
pdf.body("포트 서비스 접근 범위 비고\n"
"443 HTTPS 외부 Nginx SSL\n"
"80 HTTP 외부->443 리다이렉트\n"
"8080 Spring Boot 내부 Nginx에서만 접근\n"
"8081 wtm-api 내부 WTM 프로젝트\n"
"3306 MySQL 내부 DB 선택에 따라\n"
"5432 PostgreSQL 내부 \n"
"1521 Oracle 내부 \n"
"1433 MSSQL 내부 \n"
"6379 Redis 내부 선택 (Embedded Redis 자동 대체)")
pdf.page_number()
return pdf
def onprem_page9():
"""Page 9: Redis 설치 섹션 재작성."""
pdf = KoreanPDF()
pdf.add_page()
pdf.header_line("WBX Spring Framework | On-Premise 설치 가이드")
pdf.section_title("5. Redis 설치 (선택)")
pdf.note_box("WBX Spring Framework에는 Embedded Redis가 내장되어 있어 별도 설치 없이도\n"
"앱이 정상 구동됩니다. 운영 환경에서 높은 성능이 필요한 경우에만 설치하세요.")
pdf.sub_title("5-1. Embedded Redis (기본 — 설치 불필요)")
pdf.body("앱 시작 시 다음 순서로 자동 동작합니다:")
pdf.bullet("외부 Redis 감지 시 -> 외부 Redis 사용 (운영 환경)")
pdf.bullet("외부 Redis 없음 -> Embedded Redis 자동 시작 (개발/소규모 운영)")
pdf.bullet("Embedded Redis 실패 -> 인메모리 캐시 전환 (최후 안전장치)")
pdf.ln(3)
pdf.sub_title("5-2. 외부 Redis 설치 (선택 — 대규모 운영 환경 권장)")
pdf.code_block(
"sudo dnf install -y redis\n"
"sudo systemctl enable --now redis\n\n"
"# 보안: bind + password\n"
"sudo sed -i 's/^bind .*/bind 127.0.0.1/' /etc/redis/redis.conf\n"
"echo 'requirepass RedisP@ss123' | sudo tee -a /etc/redis/redis.conf\n"
"sudo systemctl restart redis\n\n"
"# 확인\n"
"redis-cli -a RedisP@ss123 ping # PONG"
)
pdf.sub_title("5-3. .env 설정 (외부 Redis 사용 시)")
pdf.code_block(
"SPRING_DATA_REDIS_HOST=127.0.0.1\n"
"# 또는 원격 Redis 서버 주소\n"
"# SPRING_DATA_REDIS_HOST=redis.company.com"
)
pdf.page_number()
return pdf
# ============================================================
# 2. 개발자가이드.pdf — Page 3 (사전 준비), Page 4 (Docker Compose)
# ============================================================
def dev_page3():
"""Page 3: 개발환경 설정 - Docker/Redis 부분 수정."""
pdf = KoreanPDF()
pdf.add_page()
pdf.header_line("WBX Spring Framework | 개발자 가이드")
pdf.section_title("Chapter 0. 개발환경 설정 - IDE \u00b7 프로젝트 생성")
pdf.sub_title("0-1. 사전 준비")
pdf.bullet("JDK 21 설치 (Eclipse Temurin 권장)")
pdf.bullet("Git 설치 (git config user.name / user.email)")
pdf.bullet("Docker Desktop 설치 (선택 - 로컬 DB 컨테이너 사용 시에만)")
pdf.bullet("IDE 설치 (아래 택 1)")
pdf.ln(2)
pdf.note_box("NOTE: Redis는 Embedded Redis가 앱과 함께 자동 시작되므로 별도 설치가 필요\n"
"없습니다. Docker Desktop도 DB를 직접 설치하면 불필요합니다.")
pdf.sub_title("온보딩 플로우")
pdf.body("1) JDK 설치 -> 2) IDE 설정 -> 3) DB 준비 -> 4) 프로젝트 생성 -> 5) bootRun -> 6) Swagger 확인")
pdf.sub_title("0-2. IDE 선택 가이드")
pdf.body("IDE 권장 대상 핵심 장점\n"
"IntelliJ IDEA 메인 개발 Spring Boot 최적, 리팩터링, DB 브라우저\n"
"VS Code 경량/FE 병행 Extension Pack, DevContainer\n"
"Eclipse/STS 무료/레거시 Spring Tool Suite 플러그인")
pdf.sub_title("0-3. IntelliJ IDEA 필수 설정")
pdf.bullet("플러그인: Spring Boot, Lombok, JPA Buddy, GitToolBox, .env, SonarLint")
pdf.bullet("Annotation Processor 활성화 (Settings > Build > Compiler)")
pdf.bullet("Hot Reload: Build project automatically + Allow auto-make")
pdf.ln(1)
pdf.code_block(
"Run Configuration:\n"
" Main class: kr.co.accura.wbx.spring.WbxSpringCoreApplication\n"
" Env: JWT_SECRET=dev-secret-key"
)
pdf.sub_title("0-4. VS Code 필수 설정")
pdf.bullet("Extension: Java Pack, Spring Boot Pack, Lombok, Gradle, REST Client, GitLens")
pdf.bullet(".vscode/settings.json - Lombok, Gradle Wrapper, 포맷터 설정")
pdf.bullet(".vscode/launch.json - Spring Boot 디버깅 프로필 (local, test)")
pdf.page_number()
return pdf
def dev_page4():
"""Page 4: Docker Compose 및 프로젝트 생성 - Redis 부분 수정."""
pdf = KoreanPDF()
pdf.add_page()
pdf.header_line("WBX Spring Framework | 개발자 가이드")
pdf.code_block(
'// .vscode/launch.json\n'
'{\n'
' "type": "java",\n'
' "name": "App (Local)",\n'
' "mainClass": "kr.co.accura.wbx.spring.WbxSpringCoreApplication",\n'
' "env": {"JWT_SECRET": "dev-secret-key"}\n'
'}'
)
pdf.sub_title("0-5. Eclipse / STS 설정")
pdf.bullet("Spring Tools 4 플러그인 설치 (Eclipse Marketplace)")
pdf.bullet("Lombok 설치: java -jar lombok.jar -> Eclipse 경로 선택")
pdf.bullet("Gradle Import: File > Import > Existing Gradle Project")
pdf.bullet("Annotation Processor: Project Properties > Java Compiler 에서 활성화")
pdf.ln(2)
pdf.sub_title("0-6. 프로젝트 생성 (3가지 방법)")
pdf.body("방법 A: Spring Initializr (start.spring.io)")
pdf.code_block("Project: Gradle-Kotlin | Java: 21 | Boot: 3.5.0\n"
"Group: kr.co.accura | Artifact: {앱}-api\n"
"-> Generate -> 압축 해제 -> wbx-spring-starter 의존성 추가")
pdf.body("방법 B: Git Template Repository")
pdf.code_block("git clone https://git.wbx.kr/accura/wbx-spring-template.git my-app\n"
"cd my-app && ./init.sh --name my-app --group kr.co.accura")
pdf.sub_title("0-7. 설치 스크립트 (권장)")
pdf.body("프로젝트에 포함된 설치 스크립트가 JDK 자동 설치, Git 사전 검사, 빌드, .env 템플릿 생성을 자동 처리합니다.")
pdf.code_block("# Windows\nscripts\\install.bat\n\n# Linux/macOS\nchmod +x scripts/install.sh && ./scripts/install.sh")
pdf.body("TIP: JDK 미설치 시 스크립트가 자동으로 설치합니다.")
pdf.sub_title("0-8. 로컬 개발 인프라")
pdf.body("Redis는 Embedded Redis가 앱 시작 시 자동 구동되므로 별도 설치가 필요 없습니다.\n"
"DB만 Docker Compose 또는 직접 설치하면 됩니다.")
pdf.code_block("# Docker로 DB만 시작 (Redis 불필요)\n"
"docker compose -f docker-compose-dev.yml up -d mysql\n\n"
"# 또는 DB를 직접 설치한 경우 바로 앱 실행\n"
"gradlew.bat bootRun")
pdf.note_box("Embedded Redis 동작: 앱 시작 시 외부 Redis 감지 -> 없으면 자동 시작 -> 실패 시 인메모리 캐시")
pdf.page_number()
return pdf
def dev_page5():
"""Page 5: DevContainer + 코드 품질 도구 - Redis 부분 수정."""
pdf = KoreanPDF()
pdf.add_page()
pdf.header_line("WBX Spring Framework | 개발자 가이드")
pdf.sub_title("0-9. DevContainer (VS Code)")
pdf.body("devcontainer.json 으로 JDK 21 + DB가 포함된 완전한 개발 환경을 컨테이너로 제공합니다.\n"
"Redis는 Embedded Redis가 자동 구동되므로 DevContainer에 별도 포함하지 않아도 됩니다.\n"
"팀원 전체 동일 환경 보장.")
pdf.sub_title("0-10. 코드 품질 도구")
pdf.bullet("Spotless - 코드 포맷 자동화 (./gradlew spotlessApply)")
pdf.bullet("CheckStyle - 코드 규칙 검사")
pdf.bullet("JaCoCo - 테스트 커버리지 (최소 70%)")
pdf.bullet("SonarLint - IDE 실시간 코드 분석")
pdf.bullet(".editorconfig - IDE 공통 코드 스타일 (UTF-8, LF, 4 spaces)")
pdf.page_number()
return pdf
# ============================================================
# Main
# ============================================================
def main():
print("WBX Spring Framework PDF 가이드 재생성")
print("=" * 50)
# 1. 설치가이드_OnPremise.pdf
print("\n[1] 설치가이드_OnPremise.pdf")
src = os.path.join(DOCS_DIR, "WBX_Spring_Framework_설치가이드_OnPremise.pdf")
out = os.path.join(DOCS_DIR, "WBX_Spring_Framework_설치가이드_OnPremise.pdf")
replace_pages(src, {
2: onprem_page3, # Page 3: 사전 요구사항 (Redis 선택으로)
8: onprem_page9, # Page 9: Redis 설치 (Embedded Redis 안내)
}, out)
# 2. 설치가이드_Cloud.pdf — 변경 없음 (클라우드 관리형 Redis 사용)
print("\n[2] 설치가이드_Cloud.pdf")
print(" [SKIP] 클라우드 환경은 관리형 Redis(Azure Cache/ElastiCache) 사용 - 변경 불필요")
# 3. 개발자가이드.pdf
print("\n[3] 개발자가이드.pdf")
src = os.path.join(DOCS_DIR, "WBX_Spring_Framework_개발자가이드.pdf")
out = os.path.join(DOCS_DIR, "WBX_Spring_Framework_개발자가이드.pdf")
replace_pages(src, {
2: dev_page3, # Page 3: 사전 준비 (Docker/Redis 선택)
3: dev_page4, # Page 4: Docker Compose (Redis 불필요)
4: dev_page5, # Page 5: DevContainer (Redis 제외)
}, out)
print("\n" + "=" * 50)
print("완료! 변경된 PDF:")
print(" - WBX_Spring_Framework_설치가이드_OnPremise.pdf (Page 3, 9)")
print(" - WBX_Spring_Framework_개발자가이드.pdf (Page 3, 4, 5)")
print(" - WBX_Spring_Framework_설치가이드_Cloud.pdf (변경 없음)")
if __name__ == "__main__":
main()

파일 보기

@@ -120,7 +120,46 @@
GRANT ALL ON mos.* TO 'jsh'@'%';
[2-3] Redis (설치 불필요)
[2-3] Node.js (프론트엔드 개발 시 필요)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
용도: WTM 프론트엔드 개발 서버 실행 (Vue 3 또는 React 18)
* Windows
winget install --id OpenJS.NodeJS.LTS
* macOS
brew install node@22
* Linux
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
sudo apt install -y nodejs
설치 확인:
node -v → v22.x.x
npm -v → 10.x.x
프론트엔드 선택:
+----------------------+------------------+----------+---------------------+
| 프로젝트 | 프레임워크 | 포트 | UI 라이브러리 |
+----------------------+------------------+----------+---------------------+
| wtm-frontend-vue | Vue 3 + Vite | 5173 | PrimeVue 4 |
| wtm-frontend-react | React 18 + Vite | 5174 | PrimeReact 10 |
+----------------------+------------------+----------+---------------------+
두 프론트엔드는 동일한 백엔드 API(wtm-api :8081)에 연결됩니다.
프로젝트 상황에 맞는 프레임워크를 선택하세요.
빠른 시작:
# Vue 3 버전
cd wtm-frontend-vue
npm install && npm run dev → http://localhost:5173
# React 18 버전
cd wtm-frontend-react
npm install && npm run dev → http://localhost:5174
[2-4] Redis (설치 불필요)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
용도: 캐시, 세션 저장소
@@ -178,6 +217,17 @@
http://localhost:8081/swagger-ui → WTM API 문서
http://localhost:8081/admin/login → WTM 관리 콘솔
[WTM 프론트엔드 실행 — Vue 3 또는 React 18 중 택 1]
# Vue 3 버전
cd wtm-frontend-vue
npm install && npm run dev
http://localhost:5173 → WTM 프론트엔드 (Vue 3)
# React 18 버전
cd wtm-frontend-react
npm install && npm run dev
http://localhost:5174 → WTM 프론트엔드 (React 18)
[방법 B] DB 직접 설치 (Docker 없이)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -228,7 +278,7 @@
[환경별 수정]
SPRING_PROFILES_ACTIVE 프로필 조합 (아래 참고)
DB_HOST / DB_PORT DB 서버 주소
CORS_ORIGINS 프론트엔드 URL
CORS_ORIGINS 프론트엔드 URL (Vue: 5173, React: 5174)
[프로필 조합 예시]
로컬 개발 (MySQL) : local,mysql
@@ -250,7 +300,8 @@
| 3306 | MySQL | 기본 DB (mysql 프로필) |
| 5432 | PostgreSQL | 대안 DB (postgresql 프로필) |
| 6379 | Redis | Embedded Redis 자동 구동 (별도 설치 불필요)|
| 5173 | wtm-frontend | Vue 3 개발 서버 |
| 5173 | wtm-frontend-vue | Vue 3 개발 서버 |
| 5174 | wtm-frontend-react| React 18 개발 서버 |
| 8001 | WBX FastAPI | 선택, 그룹웨어 동시 운영 시 |
+--------+------------------+------------------------------------------+
@@ -285,6 +336,19 @@
A: netstat -ano | findstr :8080 으로 프로세스 확인 후 종료
또는 application.yml에서 server.port 변경
Q: 프론트엔드(Vue/React) npm install 실패
A: Node.js 22 LTS가 설치되어 있는지 확인: node -v
node_modules 삭제 후 재설치: rm -rf node_modules && npm install
Q: 프론트엔드에서 API 호출 시 CORS 에러
A: wtm-api의 application.yml에서 CORS 설정 확인:
cors.allowed-origins에 http://localhost:5173, http://localhost:5174 포함 필요
또는 .env에서 CORS_ORIGINS 설정
Q: Vue(5173)와 React(5174) 동시 실행 가능한가?
A: 네, 포트가 다르므로 동시 실행 가능합니다. 둘 다 동일한 wtm-api(:8081)에
연결되며 API 프록시가 각각 설정되어 있습니다.
--------------------------------------------------------------------------------
8. 참고 문서