diff --git a/docs/WBX_Spring_Framework_개발자가이드.pdf b/docs/WBX_Spring_Framework_개발자가이드.pdf index 7703a55..6e493af 100644 Binary files a/docs/WBX_Spring_Framework_개발자가이드.pdf and b/docs/WBX_Spring_Framework_개발자가이드.pdf differ diff --git a/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf b/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf index 5e04b08..1985089 100644 Binary files a/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf and b/docs/WBX_Spring_Framework_설치가이드_OnPremise.pdf differ diff --git a/docs/generate_system_design.py b/docs/generate_system_design.py new file mode 100644 index 0000000..9fc3158 --- /dev/null +++ b/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() diff --git a/scripts/install.bat b/scripts/install.bat index 99b7659..cdd73c3 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -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 diff --git a/scripts/install.sh b/scripts/install.sh index 0f1953e..9f4c57b 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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/" # ---------- 결과 ---------- diff --git a/scripts/run-install.bat b/scripts/run-install.bat new file mode 100644 index 0000000..e3dd334 --- /dev/null +++ b/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 diff --git a/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java b/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java index 05e1f14..4a83422 100644 --- a/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java +++ b/src/main/java/kr/co/accura/wbx/spring/audit/WbxAuditLog.java @@ -33,5 +33,6 @@ public class WbxAuditLog { private String ipAddress; @Column(updatable = false) + @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); } diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java b/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java index bfc4312..a029f27 100644 --- a/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java +++ b/src/main/java/kr/co/accura/wbx/spring/rbac/RolePermission.java @@ -25,5 +25,6 @@ public class RolePermission { @Enumerated(EnumType.STRING) @Column(name = "dept_scope", length = 10) + @Builder.Default private DeptScope deptScope = DeptScope.OWN; } diff --git a/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java b/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java index 7a8ef40..eab631b 100644 --- a/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java +++ b/src/main/java/kr/co/accura/wbx/spring/rbac/WbxUserRole.java @@ -29,5 +29,6 @@ public class WbxUserRole { private Long scopeId; @Column(name = "granted_at") + @Builder.Default private LocalDateTime grantedAt = LocalDateTime.now(); }