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은 다음에 포함되어 있습니다:
2026-03-25 17:12:20 +09:00
부모 6b6516d585
커밋 07b9db48ef
9개의 변경된 파일995개의 추가작업 그리고 26개의 파일을 삭제

바이너리 파일은 표시되지 않습니다.

바이너리 파일은 표시되지 않습니다.

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()

파일 보기

@@ -7,6 +7,13 @@ setlocal EnableDelayedExpansion
:: 사용법: scripts\install.bat
:: ============================================================
:: ---------- 프로젝트 루트 설정 ----------
set "PROJECT_ROOT=%~dp0.."
pushd "!PROJECT_ROOT!" || (
echo [FAIL] 프로젝트 루트를 찾을 수 없습니다: %~dp0..
exit /b 1
)
echo.
echo ==========================================
echo WBX Spring Core — 설치 점검
@@ -17,27 +24,55 @@ set ERRORS=0
:: ---------- 1. JDK 21 ----------
echo 1. JDK 확인
set "JDK_OK=0"
where java >nul 2>&1
if %ERRORLEVEL% equ 0 (
if !ERRORLEVEL! equ 0 (
for /f "tokens=3" %%v in ('java -version 2^>^&1 ^| findstr /i "version"') do (
set "JAVA_FULL=%%~v"
)
for /f "tokens=1 delims=." %%m in ("!JAVA_FULL!") do set "JAVA_MAJOR=%%m"
if !JAVA_MAJOR! GEQ 21 (
echo [OK] JDK !JAVA_FULL!
set "JDK_OK=1"
) else (
echo [FAIL] JDK !JAVA_FULL! — 21 이상 필요
set /a ERRORS+=1
echo [INFO] JDK !JAVA_FULL! — 21 이상 필요, 자동 설치 시도...
)
) else (
echo [FAIL] java 명령어 없음 — JDK 21 설치 필요
set /a ERRORS+=1
echo [INFO] java 명령어 없음 — 자동 설치 시도...
)
if !JDK_OK! equ 0 (
where winget >nul 2>&1
if !ERRORLEVEL! equ 0 (
echo [INFO] winget으로 Eclipse Temurin JDK 21 설치 중...
winget install --id EclipseAdoptium.Temurin.21.JDK --accept-source-agreements --accept-package-agreements --silent
if !ERRORLEVEL! equ 0 (
:: 설치된 JDK를 PATH에 추가
for /d %%j in ("C:\Program Files\Eclipse Adoptium\jdk-21*") do (
set "JAVA_HOME=%%j"
)
if defined JAVA_HOME (
set "PATH=!JAVA_HOME!\bin;!PATH!"
echo [OK] JDK 21 설치 완료 — !JAVA_HOME!
echo [INFO] 시스템 PATH 반영을 위해 설치 후 새 터미널을 여세요.
) else (
echo [FAIL] JDK 설치 경로를 찾을 수 없습니다
set /a ERRORS+=1
)
) else (
echo [FAIL] JDK 설치 실패 — 수동으로 JDK 21을 설치하세요
set /a ERRORS+=1
)
) else (
echo [FAIL] winget 없음 — https://adoptium.net 에서 JDK 21을 수동 설치하세요
set /a ERRORS+=1
)
)
:: ---------- 2. Git ----------
echo 2. Git 확인
where git >nul 2>&1
if %ERRORLEVEL% equ 0 (
if !ERRORLEVEL! equ 0 (
for /f "delims=" %%g in ('git --version') do echo [OK] %%g
) else (
echo [FAIL] git 없음
@@ -47,7 +82,7 @@ if %ERRORLEVEL% equ 0 (
:: ---------- 3. Docker (선택) ----------
echo 3. Docker 확인 (선택)
where docker >nul 2>&1
if %ERRORLEVEL% equ 0 (
if !ERRORLEVEL! equ 0 (
for /f "delims=" %%d in ('docker --version') do echo [OK] %%d
) else (
echo [WARN] Docker 미설치 — DB/Redis를 직접 설치해야 합니다
@@ -58,7 +93,7 @@ echo 4. Gradle 빌드
if !ERRORS! GTR 0 (
echo [FAIL] 사전 요구사항 미충족 — 빌드 건너뜀
) else (
call gradlew.bat build -x test --console=plain -q
call "!PROJECT_ROOT!\gradlew.bat" build -x test --console=plain -q
if !ERRORLEVEL! equ 0 (
echo [OK] BUILD SUCCESSFUL
) else (
@@ -69,7 +104,7 @@ if !ERRORS! GTR 0 (
:: ---------- 5. .env 템플릿 ----------
echo 5. 환경변수 파일
if not exist .env (
if not exist "!PROJECT_ROOT!\.env" (
(
echo # ===== WBX Spring Core — 환경변수 =====
echo # 이 파일을 환경에 맞게 수정하세요.
@@ -80,7 +115,7 @@ if not exist .env (
echo # --- 서버 ---
echo SERVER_CONTEXT_PATH=/
echo.
echo # --- JWT ^(필수 변경!^) ---
echo # --- JWT ^(필수 변경^^^!^) ---
echo JWT_SECRET=your-production-secret-key-minimum-256-bits-long
echo.
echo # --- DB ---
@@ -119,7 +154,7 @@ if not exist .env (
echo # AWS_S3_BUCKET=
echo # AWS_ACCESS_KEY=
echo # AWS_SECRET_KEY=
) > .env
) > "!PROJECT_ROOT!\.env"
echo [OK] .env 생성 완료 (값을 수정하세요)
) else (
echo [WARN] .env 이미 존재 — 건너뜀
@@ -127,9 +162,9 @@ if not exist .env (
:: ---------- 6. 디렉토리 ----------
echo 6. 디렉토리 생성
if not exist logs mkdir logs
if not exist uploads mkdir uploads
if not exist backup mkdir backup
if not exist "!PROJECT_ROOT!\logs" mkdir "!PROJECT_ROOT!\logs"
if not exist "!PROJECT_ROOT!\uploads" mkdir "!PROJECT_ROOT!\uploads"
if not exist "!PROJECT_ROOT!\backup" mkdir "!PROJECT_ROOT!\backup"
echo [OK] logs\ uploads\ backup\
:: ---------- 결과 ----------
@@ -140,7 +175,7 @@ if !ERRORS! equ 0 (
echo.
echo 다음 단계:
echo 1. .env 파일을 환경에 맞게 수정
echo 2. DB 생성 (또는 docker compose -f docker-compose-dev.yml up -d)
echo 2. DB 생성 ^(또는 docker compose -f docker-compose-dev.yml up -d^)
echo 3. gradlew.bat bootRun
echo 4. http://localhost:8080/health 확인
) else (
@@ -149,4 +184,5 @@ if !ERRORS! equ 0 (
echo ==========================================
echo.
popd
endlocal

파일 보기

@@ -3,7 +3,6 @@
# WBX Spring Core — Linux/macOS 설치 스크립트
# 사용법: chmod +x scripts/install.sh && ./scripts/install.sh
# ============================================================
set -euo pipefail
# ---------- 색상 ----------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
@@ -14,6 +13,11 @@ info() { echo -e " ${CYAN}[INFO]${NC} $1"; }
ERRORS=0
# ---------- 프로젝트 루트 설정 ----------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT" || { echo "[FAIL] 프로젝트 루트를 찾을 수 없습니다: $SCRIPT_DIR/.."; exit 1; }
echo ""
echo "=========================================="
echo " WBX Spring Core — 설치 점검"
@@ -22,17 +26,100 @@ echo ""
# ---------- 1. JDK 21 ----------
echo "1. JDK 확인"
JDK_OK=0
if command -v java &>/dev/null; then
JAVA_VER=$(java -version 2>&1 | head -1 | awk -F '"' '{print $2}' | cut -d. -f1)
if [ "$JAVA_VER" -ge 21 ] 2>/dev/null; then
ok "JDK $JAVA_VER"
JDK_OK=1
else
fail "JDK $JAVA_VER (21 이상 필요)"
ERRORS=$((ERRORS + 1))
info "JDK $JAVA_VER 21 이상 필요, 자동 설치 시도..."
fi
else
fail "java 명령어 없음 — JDK 21 설치 필요"
ERRORS=$((ERRORS + 1))
info "java 명령어 없음 — 자동 설치 시도..."
fi
if [ $JDK_OK -eq 0 ]; then
if [[ "$(uname)" == "Darwin" ]]; then
# macOS — Homebrew
if command -v brew &>/dev/null; then
info "Homebrew로 Temurin JDK 21 설치 중..."
if brew install --cask temurin@21; then
export JAVA_HOME=$(/usr/libexec/java_home -v 21 2>/dev/null || true)
if [ -n "$JAVA_HOME" ]; then
export PATH="$JAVA_HOME/bin:$PATH"
ok "JDK 21 설치 완료 — $JAVA_HOME"
else
fail "JDK 설치 경로를 찾을 수 없습니다"
ERRORS=$((ERRORS + 1))
fi
else
fail "JDK 설치 실패 — 수동으로 JDK 21을 설치하세요"
ERRORS=$((ERRORS + 1))
fi
else
fail "Homebrew 없음 — https://adoptium.net 에서 JDK 21을 수동 설치하세요"
ERRORS=$((ERRORS + 1))
fi
else
# Linux — apt / yum / dnf
INSTALL_OK=0
if command -v apt-get &>/dev/null; then
info "apt로 Temurin JDK 21 설치 중..."
if sudo apt-get update -qq && sudo apt-get install -y temurin-21-jdk 2>/dev/null; then
INSTALL_OK=1
else
# Adoptium 저장소 추가 후 재시도
info "Adoptium 저장소 추가 중..."
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo tee /etc/apt/keyrings/adoptium.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/adoptium.list >/dev/null
if sudo apt-get update -qq && sudo apt-get install -y temurin-21-jdk; then
INSTALL_OK=1
fi
fi
elif command -v yum &>/dev/null; then
info "yum으로 Temurin JDK 21 설치 중..."
if sudo yum install -y temurin-21-jdk 2>/dev/null; then
INSTALL_OK=1
else
info "Adoptium 저장소 추가 중..."
cat <<-REPO | sudo tee /etc/yum.repos.d/adoptium.repo >/dev/null
[Adoptium]
name=Adoptium
baseurl=https://packages.adoptium.net/artifactory/rpm/rhel/\$releasever/\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.adoptium.net/artifactory/api/gpg/key/public
REPO
if sudo yum install -y temurin-21-jdk; then
INSTALL_OK=1
fi
fi
else
fail "패키지 관리자를 찾을 수 없음 — https://adoptium.net 에서 JDK 21을 수동 설치하세요"
ERRORS=$((ERRORS + 1))
fi
# 설치 결과 확인
if [ $INSTALL_OK -eq 1 ]; then
if command -v java &>/dev/null; then
JAVA_VER=$(java -version 2>&1 | head -1 | awk -F '"' '{print $2}' | cut -d. -f1)
if [ "$JAVA_VER" -ge 21 ] 2>/dev/null; then
ok "JDK 21 설치 완료"
else
fail "JDK 설치 후에도 버전이 21 미만입니다"
ERRORS=$((ERRORS + 1))
fi
else
fail "JDK 설치 후 java 명령어를 찾을 수 없습니다"
ERRORS=$((ERRORS + 1))
fi
elif [ $INSTALL_OK -eq 0 ] && command -v apt-get &>/dev/null || command -v yum &>/dev/null; then
fail "JDK 설치 실패 — 수동으로 JDK 21을 설치하세요"
ERRORS=$((ERRORS + 1))
fi
fi
fi
# ---------- 2. Git ----------
@@ -57,8 +144,8 @@ echo "4. Gradle 빌드"
if [ $ERRORS -gt 0 ]; then
fail "사전 요구사항 미충족 — 빌드 건너뜀"
else
chmod +x gradlew
if ./gradlew build -x test --console=plain -q; then
chmod +x "$PROJECT_ROOT/gradlew"
if "$PROJECT_ROOT/gradlew" build -x test --console=plain -q; then
ok "BUILD SUCCESSFUL"
else
fail "빌드 실패"
@@ -68,8 +155,8 @@ fi
# ---------- 5. .env 템플릿 ----------
echo "5. 환경변수 파일"
if [ ! -f .env ]; then
cat > .env << 'ENVEOF'
if [ ! -f "$PROJECT_ROOT/.env" ]; then
cat > "$PROJECT_ROOT/.env" << 'ENVEOF'
# ===== WBX Spring Core — 환경변수 =====
# 이 파일을 환경에 맞게 수정하세요.
@@ -119,7 +206,7 @@ LOG_PATH=/opt/wbx-app/logs/app.log
# AWS_ACCESS_KEY=
# AWS_SECRET_KEY=
ENVEOF
chmod 600 .env
chmod 600 "$PROJECT_ROOT/.env"
ok ".env 생성 완료 (값을 수정하세요)"
else
warn ".env 이미 존재 — 건너뜀"
@@ -127,7 +214,7 @@ fi
# ---------- 6. 디렉토리 ----------
echo "6. 디렉토리 생성"
mkdir -p logs uploads backup
mkdir -p "$PROJECT_ROOT/logs" "$PROJECT_ROOT/uploads" "$PROJECT_ROOT/backup"
ok "logs/ uploads/ backup/"
# ---------- 결과 ----------

5
scripts/run-install.bat 일반 파일
파일 보기

@@ -0,0 +1,5 @@
@echo off
set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot"
set "PATH=%JAVA_HOME%\bin;%PATH%"
cd /d "%~dp0.."
call scripts\install.bat

파일 보기

@@ -33,5 +33,6 @@ public class WbxAuditLog {
private String ipAddress;
@Column(updatable = false)
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
}

파일 보기

@@ -25,5 +25,6 @@ public class RolePermission {
@Enumerated(EnumType.STRING)
@Column(name = "dept_scope", length = 10)
@Builder.Default
private DeptScope deptScope = DeptScope.OWN;
}

파일 보기

@@ -29,5 +29,6 @@ public class WbxUserRole {
private Long scopeId;
@Column(name = "granted_at")
@Builder.Default
private LocalDateTime grantedAt = LocalDateTime.now();
}