diff --git a/HanwhaOCN/wtmgr/00-overview.md b/HanwhaOCN/wtmgr/00-overview.md new file mode 100644 index 0000000..66df35d --- /dev/null +++ b/HanwhaOCN/wtmgr/00-overview.md @@ -0,0 +1,119 @@ +# 한화오션 EPU Work Time Manager (WTM) + +## Spring Boot 3.5 기반 시수관리 시스템 + +> 원본: `d:\sc\wtmgr.pdf` + `d:\sc\requierment.xlsx` +> 작성일: 2026-03-24 / 수정일: 2026-03-25 + +--- + +## 전제조건 + +> **본 계획서는 `plans/wbx-spring/` 프레임워크가 구축되어 있음을 전제로 합니다.** +> +> wbx-spring이 제공하는 기능 (본 계획서에서 다루지 않음): +> - JWT 인증 / Azure Entra ID SSO / 비밀번호 정책 +> - RBAC 권한 (역할-모듈-액션, dept_scope) +> - 통합 결재 엔진 (Handler Registry, Post-Action Event) +> - SSE 실시간 알림 +> - WBX No-Code 플랫폼 연동 (API 응답 호환, 페이징 변환, 에러 포맷) +> - 파일 업로드 (Azure Blob) +> - Nginx URL 라우팅, Multi-DataSource +> +> 상세: `plans/wbx-spring/00-overview.md` + +--- + +## 프로젝트 개요 + +| 항목 | 내용 | +|------|------| +| 프로젝트명 | WTM (Work Time Manager) | +| 고객 | 한화오션 EPU | +| 목적 | EPC 프로젝트 인력 시수 관리, WBS 연동, 결재, 리포트 | +| 기능요구사항 | 86개 (PH1 Y=62건 / PH1→PH2 이관=24건) | +| 비기능요구사항 | 17개 (NF.1~NF.17) | +| 총 화면 수 | 49개 (PH1-1차 26 / PH1-2차 11 / PH2 12) | +| API Prefix | `/api/wtm/` | +| Java 패키지 | `kr.co.accura.wtm` | +| Gradle artifact | `wtm-api` | +| DB | `wtm_db` (Azure SQL) | + +--- + +## 단계별 추진 구조 + +### PH1-1차 (9주: 4/1 ~ 5/31, ~10.0 M/M) +- 핵심 시수 입력 3종 (Non-Project / Other Project / EPC) +- WBS · TEAL 업로드/관리 + P6 파싱 +- 결재 핸들러 구현 (wbx-spring 결재 엔진 위에) +- 기본 리포트 2종 +- HR 파일 업로드, 프로젝트/인력 관리 + +### PH1-2차 (4주: 6/1 ~ 6/30, ~5.0 M/M) +- WBS 버전 비교 UI, EPC Revision 관리 +- 결재 초과 알림, SA 권한 고도화 +- SAP BTP 배치 연동 (HR 자동 동기화) +- Phase · NP 비율 리포트 + +### PH2 (TBD) +- Cognite 연계 (NF.15), 외주 접속 포털, Discipline 생산성 분석 +- Location/Role별 Unit Rate (No.65), 벤치마킹 (No.63) + +### 확인필요사항 (7건) + +| # | 항목 | 관련 요구사항 | 상태 | +|---|------|-------------|------| +| 1 | 외주 사용자 등록 방식 (파일 vs 개별) | No.4, 5, 6 | 미확정 | +| 2 | User 관리 정책 (권한별 홈 라우팅) | No.17, 18 | 확인중 | +| 3 | SA 전체 기능 컨트롤 범위 | No.19 | PH2 이관 | +| 4 | Resource Assignment 규칙 상세 | No.42, 46 | PH2 이관 | +| 5 | EPC L5 Revision 관리 방안 | No.64 | PH1-2차 | +| 6 | Approval 프로세스 상세 | No.70, 71 | 확인중 | +| 7 | Change Order MH 관리 방안 | No.67 | 확인중 | + +--- + +## WTM이 구현하는 것 (비즈니스 로직) + +| 모듈 | 역할 | wbx-spring 활용 | +|------|------|----------------| +| **Timesheet** | 시수 입력 3종, 규칙 엔진 | - | +| **WBS/TEAL** | P6 파싱, Canonical WBS, 버전 관리 | - | +| **Approval Handler** | `TimesheetApprovalHandler` 구현 | `ApprovalHandler` interface 구현 | +| **Report** | 프로젝트별/WBS별 시수 분석 | - | +| **HR 연동** | SAP BTP, Excel 업로드 | `ExcelParserBase` 활용 | +| **프로젝트** | 프로젝트 CRUD, 인력 배정 | - | +| **사용자** | HR 필드 확장, 역할 부여 | `PermissionEvaluator` 활용 | +| **알림** | 미완료 Timesheet 리마인더 | `SseNotificationService` 활용 | + +## WTM이 구현하지 않는 것 (wbx-spring 제공) + +| 기능 | 제공처 | +|------|--------| +| JWT 발급/검증, SSO | wbx-spring-core `auth/` | +| 통합 결재 API (`/approvals/unified/action/...`) | wbx-spring-core `approval/` | +| SSE 실시간 알림 인프라 | wbx-spring-core `notification/` | +| RBAC dept_scope 권한 체크 | wbx-spring-core `rbac/` | +| API 응답 호환 (detail, items) | wbx-spring-core `compat/` | +| skip/limit → Pageable 변환 | wbx-spring-core `compat/` | +| 파일 업로드 (Azure Blob) | wbx-spring-core `file/` | + +--- + +## 문서 구성 + +| 문서 | 내용 | +|------|------| +| `00-overview.md` | 프로젝트 개요 (본 문서) | +| `01-architecture.md` | WTM 모듈 구조 (wbx-spring 위에 구축) | +| `02-database-schema.md` | wtm_db 스키마 (비즈니스 테이블만) | +| `03-timesheet-module.md` | 시수 입력 3종 + 규칙 엔진 | +| `04-wbs-teal-module.md` | WBS · TEAL · P6 파싱 | +| `05-approval-handlers.md` | 결재 핸들러 구현 (wbx-spring 엔진 위에) | +| `06-reporting-module.md` | 리포트 모듈 | +| `07-api-spec.md` | WTM REST API 스펙 (/api/wtm/) | +| `08-sap-btp-integration.md` | SAP SuccessFactors BTP 연동 | +| `09-security-infra.md` | 한화오션 표준 보안 SW, Azure 인프라 | +| `10-schedule-milestones.md` | 일정 · 마일스톤 | +| `11-requirements-traceability.md` | 요구사항 추적표 (86+17+7건) | diff --git a/HanwhaOCN/wtmgr/01-architecture.md b/HanwhaOCN/wtmgr/01-architecture.md new file mode 100644 index 0000000..254ae37 --- /dev/null +++ b/HanwhaOCN/wtmgr/01-architecture.md @@ -0,0 +1,171 @@ +# 01. WTM 모듈 구조 + +> **전제**: wbx-spring 프레임워크(인증/권한/결재/알림/WBX호환)가 구축되어 있음 + +## WTM 프로젝트 구조 + +``` +wtm-api/ +├── build.gradle +├── settings.gradle +├── Dockerfile # wbx-spring 표준 (07-infra-deploy.md) +│ +├── src/main/java/kr/co/accura/wtm/ +│ ├── WtmApplication.java +│ │ +│ ├── domain/ # ★ 비즈니스 도메인만 +│ │ ├── user/ # 사용자 (HR 필드 확장) +│ │ │ ├── entity/User.java +│ │ │ ├── repository/ +│ │ │ ├── service/UserService.java +│ │ │ └── dto/ +│ │ │ +│ │ ├── project/ # 프로젝트 · 인력 배정 +│ │ │ ├── entity/ +│ │ │ ├── repository/ +│ │ │ ├── service/ +│ │ │ └── dto/ +│ │ │ +│ │ ├── wbs/ # WBS · TEAL · P6 파싱 +│ │ │ ├── entity/ +│ │ │ ├── repository/ +│ │ │ ├── service/WbsService.java +│ │ │ ├── parser/P6WbsParser.java +│ │ │ └── dto/ +│ │ │ +│ │ ├── timesheet/ # 시수 입력 3종 +│ │ │ ├── entity/ +│ │ │ ├── repository/ +│ │ │ ├── service/ +│ │ │ ├── rule/TimesheetRuleEngine.java +│ │ │ └── dto/ +│ │ │ +│ │ ├── approval/ # ★ 핸들러만 (엔진은 wbx-spring) +│ │ │ └── handler/ +│ │ │ └── TimesheetApprovalHandler.java +│ │ │ +│ │ └── report/ # 리포트 +│ │ ├── service/ +│ │ └── dto/ +│ │ +│ ├── api/ # REST Controller +│ │ ├── UserController.java +│ │ ├── ProjectController.java +│ │ ├── WbsController.java +│ │ ├── TimesheetController.java +│ │ └── ReportController.java +│ │ +│ ├── integration/ # 외부 연동 +│ │ ├── sap/HrIntegrationController.java +│ │ ├── p6/P6WbsParser.java +│ │ └── cognite/CogniteExportService.java +│ │ +│ └── config/ # WTM 전용 설정만 +│ ├── WtmConfig.java +│ └── WorkRuleConfig.java +│ +├── src/main/resources/ +│ ├── application.yml +│ ├── application-local.yml +│ ├── application-prod.yml +│ └── db/migration/ # wtm_db Flyway +│ ├── V1__init_users.sql +│ ├── V2__init_projects_wbs.sql +│ ├── V3__init_timesheets.sql +│ ├── V4__init_approvals.sql +│ └── V5__init_reports.sql +│ +└── src/test/ +``` + +## 의존성 (build.gradle) + +> 상세 Gradle 스크립트: `12-project-setup-plan.md` 참조 (Single Source of Truth) + +```groovy +plugins { + id 'org.springframework.boot' version '3.5.0' + id 'io.spring.dependency-management' version '1.1.7' +} + +dependencies { + // ★ wbx-spring 프레임워크 (멀티모듈 내 프로젝트 참조) + implementation project(':wbx-spring-core') + + // WTM 전용 의존성만 + implementation 'org.apache.poi:poi-ooxml:5.3.0' // P6 WBS 파싱 + implementation 'org.flywaydb:flyway-sqlserver' // Azure SQL 마이그레이션 + + // QueryDSL (리포트 동적 쿼리) + implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + + // MapStruct (DTO 매핑) + implementation 'org.mapstruct:mapstruct:1.6.3' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'com.h2database:h2' +} +``` + +## application.yml + +```yaml +spring: + application: + name: wtm-api + +wbx: + spring: + api-prefix: /api/wtm # ★ URL prefix + jwt: + secret: ${JWT_SECRET} + expiration: 28800 + approval: + enabled: true + notification: + sse-enabled: true + +spring: + datasource: + app: + url: ${WTM_DB_URL:jdbc:h2:mem:wtm} + username: ${WTM_DB_USER:sa} + password: ${WTM_DB_PASS:} + wbxgw: + url: ${WBX_GW_DB_URL:} + username: ${WBX_GW_DB_USER:} + password: ${WBX_GW_DB_PASS:} + + jpa: + hibernate: + ddl-auto: validate + open-in-view: false + + flyway: + enabled: true + locations: classpath:db/migration + +wtm: + work-rules: + default-min-daily-hours: 8 + default-max-weekly-hours: 52 +``` + +## wbx-spring 활용 포인트 + +| WTM 코드 | wbx-spring 활용 | +|----------|----------------| +| `@PreAuthorize("@wbx.check('TIMESHEET','VIEW')")` | RBAC 권한 체크 | +| `DeptScope scope = evaluator.getScope(...)` | 데이터 필터링 범위 | +| `implements ApprovalHandler` | 결재 핸들러 등록 | +| `sseNotificationService.sendToUser(...)` | 실시간 알림 전송 | +| `Map.of("items", ..., "total", ...)` | WBX DataGrid 호환 응답 | +| `{"detail": "..."}` 에러 | WBX 에러 포맷 자동 적용 | +| `@RequestParam int skip, int limit` | WBX 페이징 파라미터 수용 | diff --git a/HanwhaOCN/wtmgr/02-database-schema.md b/HanwhaOCN/wtmgr/02-database-schema.md new file mode 100644 index 0000000..bc2c17f --- /dev/null +++ b/HanwhaOCN/wtmgr/02-database-schema.md @@ -0,0 +1,422 @@ +# 02. DB 스키마 설계 + +## ERD 개요 + +``` +users ──┬── user_roles ──── roles ──── role_permissions + │ + ├── org_hierarchy (조직 4레벨: BU/Division/Dept/Section) + ├── hr_uploads (HR Master 업로드 이력) + │ +projects ──┬── project_assignments ──── users (인력 배정) + ├── project_type_config (프로젝트 유형별 WBS 규칙) + │ + ├── wbs_versions ──── wbs_nodes (P6 WBS 트리) + ├── canonical_wbs ──── wbs_discipline_assignments + │ + ├── teal_versions ──── teal_entries + │ + ├── timesheets ──┬── timesheet_entries + │ └── timesheet_uploads (Excel 일괄) + │ + └── approvals ──── approval_lines ──── approval_comments + +work_rules (근무 규칙: 일 8h, 주 52h, Location별) +overhead_types (Non-Project Overhead 유형, SA 관리) +sa_access_logs (SA 활동 로그) +reports (View/Materialized View 기반) + +> 상세 보완: 11-requirements-supplement.md 참조 +> (HR 필드, 조직계층, 권한매트릭스, Resource Assignment, 근무규칙 등) +``` + +--- + +## 핵심 테이블 설계 + +### 1. 사용자 / 권한 + +```sql +-- V1__init_users.sql + +CREATE TABLE users ( + id BIGINT IDENTITY PRIMARY KEY, + employee_id VARCHAR(50) NOT NULL UNIQUE, -- 사번 + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(100) NOT NULL, + full_name VARCHAR(255), + hashed_password VARCHAR(255), -- 내부 로그인용 + department VARCHAR(100), + discipline VARCHAR(100), -- Engineering Discipline + position_title VARCHAR(100), + location VARCHAR(50), -- Onshore/Offshore + employment_type VARCHAR(20) DEFAULT 'INTERNAL', -- INTERNAL, SUBCONTRACTOR + is_active BIT DEFAULT 1, + is_locked BIT DEFAULT 0, + failed_attempts INT DEFAULT 0, + last_login_at DATETIME2, + password_changed_at DATETIME2, + azure_oid VARCHAR(255), -- Azure Entra ID Object ID (SSO) + mfa_enabled BIT DEFAULT 0, + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2, + created_by BIGINT, + updated_by BIGINT +); + +CREATE TABLE roles ( + id BIGINT IDENTITY PRIMARY KEY, + code VARCHAR(20) NOT NULL UNIQUE, -- PM, PCM, PTK, DL, SA, USER + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + level INT DEFAULT 0 -- 권한 레벨 (SA=100, PM=80, DL=60 ...) +); + +-- 사용자-역할 (다대다) +CREATE TABLE user_roles ( + id BIGINT IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + role_id BIGINT NOT NULL REFERENCES roles(id), + project_id BIGINT NULL REFERENCES projects(id), -- 프로젝트별 역할 (PM, DL 등) + granted_at DATETIME2 DEFAULT GETDATE(), + granted_by BIGINT REFERENCES users(id), + UNIQUE (user_id, role_id, project_id) +); + +-- 초기 역할 데이터 +-- V2__seed_roles.sql +INSERT INTO roles (code, name, level) VALUES + ('SA', 'System Administrator', 100), + ('PM', 'Project Manager', 80), + ('PCM', 'Project Control Manager', 70), + ('PTK', 'Project Timekeeper', 60), + ('DL', 'Discipline Lead', 50), + ('USER', 'General User', 10); +``` + +### 2. 프로젝트 / WBS / TEAL + +```sql +-- V3__init_projects.sql + +CREATE TABLE projects ( + id BIGINT IDENTITY PRIMARY KEY, + project_code VARCHAR(50) NOT NULL UNIQUE, -- EPU 프로젝트 코드 + name VARCHAR(255) NOT NULL, + description NVARCHAR(1000), + project_type VARCHAR(20) NOT NULL, -- EPC, NON_PROJECT, OTHER + status VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE, CLOSED, HOLD + start_date DATE, + end_date DATE, + pm_user_id BIGINT REFERENCES users(id), + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2 +); + +-- WBS 버전 (P6에서 업로드되는 스냅샷) +CREATE TABLE wbs_versions ( + id BIGINT IDENTITY PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES projects(id), + version_number INT NOT NULL, + effective_date DATE NOT NULL, + source_type VARCHAR(20) DEFAULT 'P6_UPLOAD', -- P6_UPLOAD, MANUAL + source_filename VARCHAR(500), + description NVARCHAR(500), + status VARCHAR(20) DEFAULT 'DRAFT', -- DRAFT, ACTIVE, ARCHIVED + uploaded_by BIGINT REFERENCES users(id), + created_at DATETIME2 DEFAULT GETDATE(), + UNIQUE (project_id, version_number) +); + +-- WBS 노드 (Level 1~5 트리 구조) +CREATE TABLE wbs_nodes ( + id BIGINT IDENTITY PRIMARY KEY, + wbs_version_id BIGINT NOT NULL REFERENCES wbs_versions(id), + parent_id BIGINT NULL REFERENCES wbs_nodes(id), -- 트리 구조 + wbs_code VARCHAR(100) NOT NULL, -- L1.L2.L3.L4.L5 + level INT NOT NULL, -- 1~5 + name NVARCHAR(500) NOT NULL, + discipline VARCHAR(50), + planned_hours DECIMAL(10,2), + sort_order INT DEFAULT 0, + is_leaf BIT DEFAULT 0, -- TEAL 선정 가능 여부 + UNIQUE (wbs_version_id, wbs_code) +); + +-- Canonical WBS (정규화된 WBS 구조) +CREATE TABLE canonical_wbs ( + id BIGINT IDENTITY PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES projects(id), + wbs_code VARCHAR(100) NOT NULL, + level INT NOT NULL, + name NVARCHAR(500) NOT NULL, + parent_code VARCHAR(100), + discipline VARCHAR(50), + is_active BIT DEFAULT 1, + mapped_p6_code VARCHAR(100), -- P6 WBS와 매핑 + created_at DATETIME2 DEFAULT GETDATE(), + UNIQUE (project_id, wbs_code) +); + +-- TEAL (Task Effective Activity List) +CREATE TABLE teal_versions ( + id BIGINT IDENTITY PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES projects(id), + version_number INT NOT NULL, + effective_date DATE NOT NULL, + description NVARCHAR(500), + status VARCHAR(20) DEFAULT 'DRAFT', + uploaded_by BIGINT REFERENCES users(id), + created_at DATETIME2 DEFAULT GETDATE(), + UNIQUE (project_id, version_number) +); + +CREATE TABLE teal_entries ( + id BIGINT IDENTITY PRIMARY KEY, + teal_version_id BIGINT NOT NULL REFERENCES teal_versions(id), + canonical_wbs_id BIGINT REFERENCES canonical_wbs(id), + activity_code VARCHAR(100) NOT NULL, + activity_name NVARCHAR(500), + discipline VARCHAR(50), + is_active BIT DEFAULT 1 +); +``` + +### 3. 시수 입력 (Timesheet) + +```sql +-- V4__init_timesheets.sql + +-- 시수 헤더 (주간 단위) +CREATE TABLE timesheets ( + id BIGINT IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + week_start_date DATE NOT NULL, -- 주 시작일 (월요일) + week_end_date DATE NOT NULL, + status VARCHAR(20) DEFAULT 'DRAFT', -- DRAFT, SUBMITTED, DL_APPROVED, APPROVED, REJECTED + total_hours DECIMAL(10,2) DEFAULT 0, + submitted_at DATETIME2, + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2, + UNIQUE (user_id, week_start_date) +); + +-- 시수 상세 (일별 항목) +CREATE TABLE timesheet_entries ( + id BIGINT IDENTITY PRIMARY KEY, + timesheet_id BIGINT NOT NULL REFERENCES timesheets(id), + entry_type VARCHAR(20) NOT NULL, -- NON_PROJECT, OTHER_PROJECT, EPC + entry_date DATE NOT NULL, + hours DECIMAL(5,2) NOT NULL DEFAULT 0, + + -- NON_PROJECT: 카테고리만 + np_category VARCHAR(100), -- Leave, Training, Admin 등 + + -- OTHER_PROJECT: 프로젝트 + 카테고리 + other_project_id BIGINT NULL REFERENCES projects(id), + other_category VARCHAR(100), + + -- EPC: 프로젝트 + WBS + TEAL + epc_project_id BIGINT NULL REFERENCES projects(id), + canonical_wbs_id BIGINT NULL REFERENCES canonical_wbs(id), + teal_entry_id BIGINT NULL REFERENCES teal_entries(id), + revision_number INT DEFAULT 1, -- EPC Revision (PH1-2) + + remark NVARCHAR(500), + created_at DATETIME2 DEFAULT GETDATE(), + updated_at DATETIME2, + + -- 일별 8시간, 주 52시간 제약은 애플리케이션 레벨에서 검증 + CONSTRAINT chk_hours CHECK (hours >= 0 AND hours <= 24) +); + +CREATE INDEX idx_ts_entries_date ON timesheet_entries(entry_date); +CREATE INDEX idx_ts_entries_type ON timesheet_entries(entry_type); + +-- Excel 일괄 업로드 이력 +CREATE TABLE timesheet_uploads ( + id BIGINT IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + filename VARCHAR(500), + file_path VARCHAR(1000), + total_rows INT, + success_rows INT, + error_rows INT, + error_log NVARCHAR(MAX), -- JSON: [{row, field, error}] + status VARCHAR(20), -- PROCESSING, COMPLETED, FAILED + created_at DATETIME2 DEFAULT GETDATE() +); +``` + +### 4. 결재 (Approval) + +```sql +-- V5__init_approvals.sql + +CREATE TABLE approvals ( + id BIGINT IDENTITY PRIMARY KEY, + timesheet_id BIGINT NOT NULL REFERENCES timesheets(id), + requester_id BIGINT NOT NULL REFERENCES users(id), + project_id BIGINT REFERENCES projects(id), + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED + submitted_at DATETIME2 DEFAULT GETDATE(), + completed_at DATETIME2, + UNIQUE (timesheet_id) +); + +-- 결재 라인 (User → DL → PM 3단계) +CREATE TABLE approval_lines ( + id BIGINT IDENTITY PRIMARY KEY, + approval_id BIGINT NOT NULL REFERENCES approvals(id), + approver_id BIGINT NOT NULL REFERENCES users(id), + approval_order INT NOT NULL, -- 1=DL, 2=PM + role_code VARCHAR(20), -- DL, PM + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED + acted_at DATETIME2, + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE TABLE approval_comments ( + id BIGINT IDENTITY PRIMARY KEY, + approval_id BIGINT NOT NULL REFERENCES approvals(id), + user_id BIGINT NOT NULL REFERENCES users(id), + comment NVARCHAR(2000), + action VARCHAR(20), -- APPROVE, REJECT, COMMENT + created_at DATETIME2 DEFAULT GETDATE() +); +``` + +### 5. 리포트 (View) + +```sql +-- V6__init_report_views.sql + +-- 프로젝트별 시수 집계 View +CREATE VIEW v_project_hours AS +SELECT + te.epc_project_id AS project_id, + p.project_code, + p.name AS project_name, + te.entry_date, + DATEPART(YEAR, te.entry_date) AS year, + DATEPART(MONTH, te.entry_date) AS month, + DATEPART(WEEK, te.entry_date) AS week, + u.discipline, + u.department, + te.entry_type, + SUM(te.hours) AS total_hours, + COUNT(DISTINCT te.timesheet_id) AS timesheet_count +FROM timesheet_entries te +JOIN timesheets ts ON te.timesheet_id = ts.id +JOIN users u ON ts.user_id = u.id +LEFT JOIN projects p ON te.epc_project_id = p.id +WHERE ts.status = 'APPROVED' +GROUP BY te.epc_project_id, p.project_code, p.name, + te.entry_date, DATEPART(YEAR, te.entry_date), + DATEPART(MONTH, te.entry_date), DATEPART(WEEK, te.entry_date), + u.discipline, u.department, te.entry_type; + +-- WBS Level별 시수 집계 View +CREATE VIEW v_wbs_hours AS +SELECT + cw.project_id, + cw.wbs_code, + cw.level AS wbs_level, + cw.name AS wbs_name, + cw.discipline, + SUM(te.hours) AS total_hours, + COUNT(DISTINCT ts.user_id) AS user_count +FROM timesheet_entries te +JOIN timesheets ts ON te.timesheet_id = ts.id +JOIN canonical_wbs cw ON te.canonical_wbs_id = cw.id +WHERE ts.status = 'APPROVED' +GROUP BY cw.project_id, cw.wbs_code, cw.level, cw.name, cw.discipline; +``` + +### 6. 감사 / SA 로그 (PH1-2) + +```sql +-- V7__init_audit.sql + +CREATE TABLE sa_access_logs ( + id BIGINT IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + action VARCHAR(50) NOT NULL, -- LOGIN, VIEW, UPDATE, DELETE, EXPORT + resource VARCHAR(100), -- users, projects, timesheets, ... + resource_id BIGINT, + ip_address VARCHAR(50), + user_agent VARCHAR(500), + detail NVARCHAR(2000), + created_at DATETIME2 DEFAULT GETDATE() +); + +CREATE INDEX idx_sa_log_user ON sa_access_logs(user_id, created_at); +CREATE INDEX idx_sa_log_action ON sa_access_logs(action, created_at); +``` + +--- + +## JPA Entity 설계 패턴 + +```java +// BaseEntity — 모든 엔티티 공통 +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @CreatedBy + @Column(updatable = false) + private Long createdBy; + + @LastModifiedBy + private Long updatedBy; +} +``` + +```java +// User Entity 예시 +@Entity +@Table(name = "users") +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 50) + private String employeeId; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false, length = 100) + private String username; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private EmploymentType employmentType = EmploymentType.INTERNAL; + + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private List userRoles = new ArrayList<>(); + + // 비즈니스 메서드 + public boolean hasRole(String roleCode) { + return userRoles.stream() + .anyMatch(ur -> ur.getRole().getCode().equals(roleCode)); + } + + public boolean hasProjectRole(String roleCode, Long projectId) { + return userRoles.stream() + .anyMatch(ur -> ur.getRole().getCode().equals(roleCode) + && Objects.equals(ur.getProjectId(), projectId)); + } +} +``` diff --git a/HanwhaOCN/wtmgr/03-timesheet-module.md b/HanwhaOCN/wtmgr/03-timesheet-module.md new file mode 100644 index 0000000..7665c05 --- /dev/null +++ b/HanwhaOCN/wtmgr/03-timesheet-module.md @@ -0,0 +1,337 @@ +# 03. 시수 입력 모듈 (3종) + +## 시수 입력 유형 + +``` +┌──────────────────────────────────────────────────────┐ +│ 시수 입력 통합 UI (탭 전환) │ +├──────────────┬──────────────┬────────────────────────┤ +│ Non-Project │ Other Project│ EPC Project │ +│ 시수 입력 │ 시수 입력 │ 시수 입력 │ +├──────────────┼──────────────┼────────────────────────┤ +│ • 카테고리 │ • 프로젝트 │ • 프로젝트 선택 │ +│ 선택 │ 선택 │ • Canonical WBS 선택 │ +│ • Leave │ • 카테고리 │ • TEAL Activity 선택 │ +│ • Training │ 선택 │ • Revision 관리 (PH1-2)│ +│ • Admin │ • 시간 입력 │ • 시간 입력 │ +│ • 시간 입력 │ │ │ +└──────────────┴──────────────┴────────────────────────┘ + │ + ┌──────┴──────┐ + │ 규칙 엔진 │ + │ 일 8h 제한 │ + │ 주 52h 제한 │ + └─────────────┘ +``` + +## 핵심 도메인 모델 + +```java +// 시수 유형 Enum +public enum TimesheetEntryType { + NON_PROJECT, // 비프로젝트 (휴가, 교육, 행정) + OTHER_PROJECT, // 타 프로젝트 + EPC // EPC 프로젝트 (핵심) +} + +// Non-Project 카테고리 +public enum NonProjectCategory { + ANNUAL_LEAVE("연차"), + SICK_LEAVE("병가"), + TRAINING("교육"), + ADMIN("행정"), + PUBLIC_HOLIDAY("공휴일"), + OTHER("기타"); + + private final String displayName; +} + +// Timesheet Entity (주간 단위) +@Entity @Table(name = "timesheets") +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Timesheet extends BaseEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private LocalDate weekStartDate; + + @Column(nullable = false) + private LocalDate weekEndDate; + + @Enumerated(EnumType.STRING) + private TimesheetStatus status = TimesheetStatus.DRAFT; + + @Column(precision = 10, scale = 2) + private BigDecimal totalHours = BigDecimal.ZERO; + + @OneToMany(mappedBy = "timesheet", cascade = CascadeType.ALL, orphanRemoval = true) + private List entries = new ArrayList<>(); + + private LocalDateTime submittedAt; + + // 비즈니스 메서드 + public void addEntry(TimesheetEntry entry) { + entries.add(entry); + entry.setTimesheet(this); + recalculateTotal(); + } + + public void submit() { + if (status != TimesheetStatus.DRAFT && status != TimesheetStatus.REJECTED) { + throw new BusinessException("DRAFT 또는 REJECTED 상태에서만 제출 가능합니다."); + } + this.status = TimesheetStatus.SUBMITTED; + this.submittedAt = LocalDateTime.now(); + } + + private void recalculateTotal() { + this.totalHours = entries.stream() + .map(TimesheetEntry::getHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} + +// TimesheetEntry (일별 상세) +@Entity @Table(name = "timesheet_entries") +@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TimesheetEntry extends BaseEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "timesheet_id", nullable = false) + private Timesheet timesheet; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private TimesheetEntryType entryType; + + @Column(nullable = false) + private LocalDate entryDate; + + @Column(nullable = false, precision = 5, scale = 2) + private BigDecimal hours; + + // Non-Project + @Enumerated(EnumType.STRING) + private NonProjectCategory npCategory; + + // Other Project + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "other_project_id") + private Project otherProject; + private String otherCategory; + + // EPC Project + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "epc_project_id") + private Project epcProject; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "canonical_wbs_id") + private CanonicalWbs canonicalWbs; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "teal_entry_id") + private TealEntry tealEntry; + + private Integer revisionNumber = 1; + private String remark; +} +``` + +## 규칙 엔진 (No.43~45) + +```java +@Component +public class TimesheetRuleEngine { + + private static final BigDecimal MAX_DAILY_HOURS = new BigDecimal("8"); + private static final BigDecimal MAX_WEEKLY_HOURS = new BigDecimal("52"); + private static final BigDecimal WARN_DAILY_HOURS = new BigDecimal("10"); + + /** + * 시수 입력 전 규칙 검증 + * @return 검증 결과 (errors + warnings) + */ + public ValidationResult validate(Timesheet timesheet) { + var result = new ValidationResult(); + + // 1. 일별 시간 제한 (8h 기본, 10h 경고, 24h 하드 리밋) + Map dailyTotals = timesheet.getEntries().stream() + .collect(Collectors.groupingBy( + TimesheetEntry::getEntryDate, + Collectors.reducing(BigDecimal.ZERO, TimesheetEntry::getHours, BigDecimal::add) + )); + + for (var entry : dailyTotals.entrySet()) { + LocalDate date = entry.getKey(); + BigDecimal total = entry.getValue(); + + if (total.compareTo(new BigDecimal("24")) > 0) { + result.addError(date, "일 최대 24시간을 초과할 수 없습니다."); + } else if (total.compareTo(WARN_DAILY_HOURS) > 0) { + result.addWarning(date, + String.format("일 %s시간 입력 — 기준(%sh) 초과", total, MAX_DAILY_HOURS)); + } + } + + // 2. 주간 총 시간 제한 (52h) + if (timesheet.getTotalHours().compareTo(MAX_WEEKLY_HOURS) > 0) { + result.addError(null, + String.format("주간 합계 %s시간 — 최대 %sh 초과", + timesheet.getTotalHours(), MAX_WEEKLY_HOURS)); + } + + // 3. EPC 시수 — WBS/TEAL 필수 + timesheet.getEntries().stream() + .filter(e -> e.getEntryType() == TimesheetEntryType.EPC) + .forEach(e -> { + if (e.getEpcProject() == null) + result.addError(e.getEntryDate(), "EPC 시수 — 프로젝트 필수"); + if (e.getCanonicalWbs() == null) + result.addError(e.getEntryDate(), "EPC 시수 — WBS 선택 필수"); + }); + + // 4. 미래 날짜 입력 불가 + LocalDate today = LocalDate.now(); + timesheet.getEntries().stream() + .filter(e -> e.getEntryDate().isAfter(today)) + .forEach(e -> result.addError(e.getEntryDate(), "미래 날짜에 시수를 입력할 수 없습니다.")); + + return result; + } +} +``` + +## REST API + +```java +@RestController +@RequestMapping("/api/timesheets") +@RequiredArgsConstructor +public class TimesheetController { + + private final TimesheetService timesheetService; + + // 주간 시수 조회 (생성 안 되어 있으면 자동 생성) + @GetMapping("/week") + public TimesheetDto getWeekly( + @RequestParam @DateTimeFormat(iso = DATE) LocalDate weekStart) { + return timesheetService.getOrCreateWeekly( + SecurityUtils.getCurrentUserId(), weekStart); + } + + // 시수 항목 저장 (Auto-save) + @PostMapping("/{timesheetId}/entries") + public TimesheetEntryDto saveEntry( + @PathVariable Long timesheetId, + @Valid @RequestBody TimesheetEntryRequest request) { + return timesheetService.saveEntry(timesheetId, request); + } + + // 시수 항목 일괄 저장 (주간 전체) + @PutMapping("/{timesheetId}/entries/batch") + public TimesheetDto saveBatch( + @PathVariable Long timesheetId, + @Valid @RequestBody List entries) { + return timesheetService.saveBatch(timesheetId, entries); + } + + // 시수 제출 (결재 요청) + @PostMapping("/{timesheetId}/submit") + public TimesheetDto submit(@PathVariable Long timesheetId) { + return timesheetService.submit(timesheetId); + } + + // Excel 일괄 업로드 + @PostMapping("/upload") + public UploadResultDto uploadExcel( + @RequestParam("file") MultipartFile file, + @RequestParam @DateTimeFormat(iso = DATE) LocalDate weekStart) { + return timesheetService.uploadExcel( + SecurityUtils.getCurrentUserId(), file, weekStart); + } + + // 내 시수 이력 (페이징) + @GetMapping("/history") + public Page history( + @RequestParam(required = false) @DateTimeFormat(iso = DATE) LocalDate from, + @RequestParam(required = false) @DateTimeFormat(iso = DATE) LocalDate to, + Pageable pageable) { + return timesheetService.getHistory( + SecurityUtils.getCurrentUserId(), from, to, pageable); + } +} +``` + +## Excel 일괄 업로드 (Apache POI) + +```java +@Service +@RequiredArgsConstructor +public class TimesheetExcelService { + + private final TimesheetService timesheetService; + + /** + * 표준 템플릿 기반 Excel 파싱 + * 컬럼: Date | Type | Project | WBS | TEAL | Hours | Remark + */ + public UploadResult parseAndSave(Long userId, MultipartFile file, LocalDate weekStart) { + var result = new UploadResult(); + + try (Workbook wb = new XSSFWorkbook(file.getInputStream())) { + Sheet sheet = wb.getSheetAt(0); + + for (int i = 1; i <= sheet.getLastRowNum(); i++) { // 헤더 스킵 + Row row = sheet.getRow(i); + if (row == null) continue; + + try { + TimesheetEntryRequest entry = parseRow(row, i); + timesheetService.saveEntry(/* ... */); + result.addSuccess(); + } catch (Exception e) { + result.addError(i, e.getMessage()); + } + } + } + + return result; + } +} +``` + +## 시수 입력 화면 구성 (프론트엔드 가이드) + +``` +┌─────────────────────────────────────────────────────────┐ +│ 시수 입력 2025-04-07 ~ 2025-04-11 │ +│ ┌────────┬────────────┬──────────────┐ │ +│ │Non-Proj│Other Proj │ EPC Project │ ← 탭 전환 │ +│ └────────┴────────────┴──────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐│ +│ │ Project: [EPU-2025-001 ▼] ││ +│ │ WBS: [E.01.03 Piping Detail ▼] ││ +│ │ TEAL: [Detail Engineering ▼] ││ +│ ├──────┬──────┬──────┬──────┬──────┬──────┬──────────┤│ +│ │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Total ││ +│ │ [8.0]│ [8.0]│ [8.0]│ [8.0]│ [4.0]│ [ ]│ 36.0h ││ +│ └──────┴──────┴──────┴──────┴──────┴──────┴──────────┘│ +│ │ +│ [+ 행 추가] 주간 합계: 36.0 / 52h │ +│ │ +│ ⚠ 월요일: 기준(8h) 초과 경고 │ +│ │ +│ [임시 저장] [제출 (결재 요청)] │ +└─────────────────────────────────────────────────────────┘ +``` diff --git a/HanwhaOCN/wtmgr/04-wbs-teal-module.md b/HanwhaOCN/wtmgr/04-wbs-teal-module.md new file mode 100644 index 0000000..3f2b938 --- /dev/null +++ b/HanwhaOCN/wtmgr/04-wbs-teal-module.md @@ -0,0 +1,312 @@ +# 04. WBS · TEAL 관리 모듈 + +## Canonical WBS 레벨 구조 (No.34) + +``` +Level 1: Project +Level 2: Phase (Engineering, Procurement, Construction, Commissioning, etc.) +Level 3: Asset or Area +Level 4: Work or Discipline +Level 5: Deliverable, Package, or Material (Engineering & SCM only) +``` + +> **업로드 주체**: PM이 P6 WBS 파일 업로드 (No.27) → PCM이 승인 (No.35) +> **Canonical WBS는 프로젝트별 수정 불가** (No.33: 한화오션 표준 구조) + +## WBS 데이터 흐름 + +``` +P6 Export File (.xls/.csv) + │ + ▼ +┌───────────────────┐ +│ P6 WBS 파서 │ Level 1~5 파싱 +│ (Apache POI) │ wbs_code 생성 +└───────┬───────────┘ + │ + ▼ +┌───────────────────┐ ┌───────────────────┐ +│ wbs_versions │ ───▶ │ wbs_nodes │ +│ (스냅샷 단위) │ │ (트리 구조) │ +└───────────────────┘ └───────────────────┘ + │ + ▼ 매핑 +┌───────────────────┐ ┌───────────────────┐ +│ canonical_wbs │ ◀─── │ TEAL 관리 │ +│ (정규 WBS 구조) │ │ (Activity List) │ +└───────┬───────────┘ └───────────────────┘ + │ + ▼ 시수 입력 시 선택 +┌───────────────────┐ +│ timesheet_entries│ +│ (EPC 시수) │ +└───────────────────┘ +``` + +## P6 WBS 파일 파서 + +```java +@Service +@RequiredArgsConstructor +public class P6WbsParser { + + /** + * P6 Export 파일을 파싱하여 WBS 트리 구조 생성 + * Level 1~5 계층 구조 지원 + * + * P6 컬럼 예상: + * Activity ID | Activity Name | WBS Code | WBS Name | Level | Planned Hours + */ + public WbsParseResult parse(MultipartFile file) { + var result = new WbsParseResult(); + Map nodeMap = new LinkedHashMap<>(); + + try (Workbook wb = WorkbookFactory.create(file.getInputStream())) { + Sheet sheet = wb.getSheetAt(0); + + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + if (row == null) continue; + + String wbsCode = getCellString(row, 2); // WBS Code (L1.L2.L3.L4.L5) + String wbsName = getCellString(row, 3); + int level = (int) getCellNumeric(row, 4); + double plannedHours = getCellNumeric(row, 5); + + var node = WbsNodeDto.builder() + .wbsCode(wbsCode) + .name(wbsName) + .level(level) + .plannedHours(BigDecimal.valueOf(plannedHours)) + .parentCode(deriveParentCode(wbsCode, level)) + .build(); + + nodeMap.put(wbsCode, node); + result.addNode(node); + } + } catch (Exception e) { + result.setError("파일 파싱 실패: " + e.getMessage()); + } + + // 부모-자식 관계 검증 + validateHierarchy(nodeMap, result); + return result; + } + + /** + * WBS 코드에서 부모 코드 추출 + * 예: "E.01.03.02.01" (Level 5) → "E.01.03.02" (Level 4) + */ + private String deriveParentCode(String wbsCode, int level) { + if (level <= 1) return null; + int lastDot = wbsCode.lastIndexOf('.'); + return lastDot > 0 ? wbsCode.substring(0, lastDot) : null; + } +} +``` + +## WBS 버전 관리 서비스 + +```java +@Service +@RequiredArgsConstructor +@Transactional +public class WbsService { + + private final WbsVersionRepository wbsVersionRepository; + private final WbsNodeRepository wbsNodeRepository; + private final CanonicalWbsRepository canonicalWbsRepository; + private final P6WbsParser p6Parser; + + /** + * P6 WBS 파일 업로드 → 새 버전 생성 + */ + public WbsVersionDto uploadP6Wbs(Long projectId, MultipartFile file, + LocalDate effectiveDate, String description) { + // 1. 파일 파싱 + WbsParseResult parseResult = p6Parser.parse(file); + if (parseResult.hasErrors()) { + throw new BusinessException("WBS 파싱 오류: " + parseResult.getErrors()); + } + + // 2. 버전 번호 자동 증가 + int nextVersion = wbsVersionRepository + .findMaxVersionByProjectId(projectId) + .map(v -> v + 1).orElse(1); + + // 3. WBS 버전 저장 + WbsVersion version = WbsVersion.builder() + .projectId(projectId) + .versionNumber(nextVersion) + .effectiveDate(effectiveDate) + .sourceType(WbsSourceType.P6_UPLOAD) + .sourceFilename(file.getOriginalFilename()) + .description(description) + .status(WbsVersionStatus.DRAFT) + .build(); + wbsVersionRepository.save(version); + + // 4. WBS 노드 벌크 저장 + List nodes = parseResult.getNodes().stream() + .map(dto -> WbsNode.builder() + .wbsVersion(version) + .wbsCode(dto.getWbsCode()) + .level(dto.getLevel()) + .name(dto.getName()) + .discipline(dto.getDiscipline()) + .plannedHours(dto.getPlannedHours()) + .isLeaf(dto.getLevel() == 5) + .build()) + .toList(); + wbsNodeRepository.saveAll(nodes); + + // 5. 부모-자식 관계 설정 + setParentReferences(version.getId(), nodes); + + return WbsVersionDto.from(version, nodes.size()); + } + + /** + * WBS 버전 활성화 (DRAFT → ACTIVE) + * 기존 ACTIVE 버전은 ARCHIVED로 변경 + */ + public void activateVersion(Long versionId) { + WbsVersion version = wbsVersionRepository.findById(versionId) + .orElseThrow(() -> new NotFoundException("WBS 버전을 찾을 수 없습니다.")); + + // 기존 ACTIVE → ARCHIVED + wbsVersionRepository.archiveActiveVersions(version.getProjectId()); + + version.activate(); + wbsVersionRepository.save(version); + + // Canonical WBS 동기화 + syncCanonicalWbs(version); + } + + /** + * Canonical WBS 동기화 + * 활성 WBS 버전의 노드를 canonical_wbs에 반영 + */ + private void syncCanonicalWbs(WbsVersion version) { + List nodes = wbsNodeRepository.findByWbsVersionId(version.getId()); + + for (WbsNode node : nodes) { + canonicalWbsRepository.findByProjectIdAndWbsCode( + version.getProjectId(), node.getWbsCode() + ).ifPresentOrElse( + // 기존 → 업데이트 + existing -> { + existing.updateFrom(node); + existing.setMappedP6Code(node.getWbsCode()); + }, + // 신규 → 생성 + () -> canonicalWbsRepository.save( + CanonicalWbs.fromWbsNode(version.getProjectId(), node)) + ); + } + } + + /** + * WBS 버전 비교 (PH1-2) + */ + public WbsCompareResult compareVersions(Long projectId, int versionA, int versionB) { + List nodesA = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionA); + List nodesB = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionB); + + Map mapA = nodesA.stream() + .collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity())); + Map mapB = nodesB.stream() + .collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity())); + + var result = new WbsCompareResult(); + + // Added in B + mapB.keySet().stream() + .filter(code -> !mapA.containsKey(code)) + .forEach(code -> result.addAdded(mapB.get(code))); + + // Removed from A + mapA.keySet().stream() + .filter(code -> !mapB.containsKey(code)) + .forEach(code -> result.addRemoved(mapA.get(code))); + + // Modified + mapA.keySet().stream() + .filter(mapB::containsKey) + .filter(code -> !mapA.get(code).contentEquals(mapB.get(code))) + .forEach(code -> result.addModified(mapA.get(code), mapB.get(code))); + + return result; + } +} +``` + +## TEAL (Task Effective Activity List) 관리 + +```java +@Service +@RequiredArgsConstructor +@Transactional +public class TealService { + + /** + * TEAL 업로드 — Canonical WBS에 연결된 Activity 목록 + */ + public TealVersionDto uploadTeal(Long projectId, MultipartFile file, + LocalDate effectiveDate) { + // 1. 파일 파싱 (WBS Code | Activity Code | Activity Name | Discipline) + List entries = parseTealFile(file); + + // 2. WBS 코드 검증 (canonical_wbs에 존재하는지) + Set validWbsCodes = canonicalWbsRepository + .findActiveCodesByProjectId(projectId); + + for (TealEntryDto entry : entries) { + if (!validWbsCodes.contains(entry.getWbsCode())) { + throw new BusinessException( + "WBS 코드 '" + entry.getWbsCode() + "'가 Canonical WBS에 없습니다."); + } + } + + // 3. 버전 생성 및 저장 + TealVersion version = TealVersion.create(projectId, effectiveDate); + tealVersionRepository.save(version); + + List tealEntries = entries.stream() + .map(dto -> TealEntry.builder() + .tealVersion(version) + .canonicalWbs(canonicalWbsRepository + .findByProjectIdAndWbsCode(projectId, dto.getWbsCode()).orElseThrow()) + .activityCode(dto.getActivityCode()) + .activityName(dto.getActivityName()) + .discipline(dto.getDiscipline()) + .build()) + .toList(); + tealEntryRepository.saveAll(tealEntries); + + return TealVersionDto.from(version, tealEntries.size()); + } +} +``` + +## REST API + +``` +# WBS +POST /api/projects/{projectId}/wbs/upload P6 WBS 파일 업로드 +GET /api/projects/{projectId}/wbs/versions 버전 목록 +GET /api/projects/{projectId}/wbs/versions/{ver} 버전 상세 (트리) +POST /api/projects/{projectId}/wbs/versions/{ver}/activate 버전 활성화 +GET /api/projects/{projectId}/wbs/compare?a=1&b=2 버전 비교 (PH1-2) + +# Canonical WBS +GET /api/projects/{projectId}/canonical-wbs 정규 WBS 트리 조회 +GET /api/projects/{projectId}/canonical-wbs/flat 플랫 목록 (시수 입력 드롭다운용) + +# TEAL +POST /api/projects/{projectId}/teal/upload TEAL 파일 업로드 +GET /api/projects/{projectId}/teal/versions 버전 목록 +GET /api/projects/{projectId}/teal/active 활성 TEAL 목록 (시수 입력용) +GET /api/projects/{projectId}/teal/by-wbs/{wbsId} WBS별 TEAL Activity 목록 +``` diff --git a/HanwhaOCN/wtmgr/05-approval-handlers.md b/HanwhaOCN/wtmgr/05-approval-handlers.md new file mode 100644 index 0000000..ab3dbd5 --- /dev/null +++ b/HanwhaOCN/wtmgr/05-approval-handlers.md @@ -0,0 +1,97 @@ +# 05. 결재 핸들러 구현 + +> **전제**: wbx-spring 결재 엔진(`ApprovalHandler` interface, `ApprovalHandlerRegistry`, +> `UnifiedApprovalController`, `BaseApprovalLine`, `ApprovalCompletedEvent`)이 제공됨. +> +> WTM은 **핸들러만 구현**하면 결재 API가 자동으로 활성화됩니다. + +## WTM이 구현하는 핸들러 + +```java +@Component +@RequiredArgsConstructor +public class TimesheetApprovalHandler implements ApprovalHandler { + + @Override + public String getTypeKey() { return "timesheet"; } + + @Override + public String getTypeDisplay() { return "시수 결재"; } + + @Override + @Transactional + public ApprovalResult approve(Long lineId, Long approverId, String comment) { + TtApprovalLine line = lineRepository.findById(lineId).orElseThrow(); + line.approve(); + + // 다음 결재자 확인 + Optional next = lineRepository + .findNextPending(line.getApprovalId(), line.getApprovalOrder()); + + Timesheet ts = line.getApproval().getTimesheet(); + + if (next.isPresent()) { + ts.setStatus(TimesheetStatus.DL_APPROVED); + notificationService.sendToUser(next.get().getApproverId(), + NotificationDto.approvalRequest("시수 결재 요청", ts)); + } else { + ts.setStatus(TimesheetStatus.APPROVED); + line.getApproval().complete(); + eventPublisher.publishEvent(new ApprovalCompletedEvent( + "timesheet", ts.getId(), approverId, line.getApproval())); + } + return ApprovalResult.success("승인 완료"); + } + + @Override + @Transactional + public ApprovalResult reject(Long lineId, Long approverId, String comment) { + TtApprovalLine line = lineRepository.findById(lineId).orElseThrow(); + line.reject(); + line.setComment(comment); + line.getApproval().getTimesheet().setStatus(TimesheetStatus.REJECTED); + return ApprovalResult.success("반려 완료"); + } + + // getApprovalHistory(), getPending() 구현 ... +} +``` + +## 결재 흐름 (User → DL → PM) + +``` +User 시수 제출 + ▼ +TimesheetService.submit() + ├── Timesheet.status = SUBMITTED + ├── TtApprovalLine #1 (DL) + #2 (PM) 생성 + └── DL에게 SSE 알림 (wbx-spring notificationService) + ▼ +DL 승인 → POST /api/wtm/approvals/unified/action/timesheet/{lineId}/approve + ├── wbx-spring UnifiedApprovalController → TimesheetApprovalHandler + └── PM에게 SSE 알림 + ▼ +PM 승인 → 최종 + ├── ApprovalCompletedEvent 발행 + └── User에게 승인 완료 알림 +``` + +## 미완료 Timesheet 리마인더 (No.70) + +```java +@Component +public class TimesheetReminderScheduler { + + private final SseNotificationService notificationService; // wbx-spring 제공 + + @Scheduled(cron = "0 0 17 * * MON-FRI") + public void dailyReminder() { + // 당일 미입력 사용자에게 알림 + } + + @Scheduled(cron = "0 0 10 * * FRI") + public void weeklySubmitReminder() { + // 주간 미제출 사용자에게 알림 + } +} +``` diff --git a/HanwhaOCN/wtmgr/06-reporting-module.md b/HanwhaOCN/wtmgr/06-reporting-module.md new file mode 100644 index 0000000..b30e49d --- /dev/null +++ b/HanwhaOCN/wtmgr/06-reporting-module.md @@ -0,0 +1,185 @@ +# 06. 리포트 모듈 + +## PH1-1차 리포트 (2종) + +### 1. 프로젝트별 시수 분석 (No.82) + +``` +GET /api/reports/project-hours + ?projectId=1 + &from=2025-04-01 + &to=2025-05-31 + &groupBy=month // month, week, discipline + &format=json // json, excel +``` + +```java +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReportService { + + private final TimesheetEntryRepository entryRepository; + + public ProjectHoursReport getProjectHoursReport(ProjectHoursFilter filter) { + // QueryDSL 동적 쿼리 + QTimesheetEntry e = QTimesheetEntry.timesheetEntry; + QTimesheet ts = QTimesheet.timesheet; + QUser u = QUser.user; + + var query = queryFactory + .select(Projections.constructor(ProjectHoursRow.class, + e.epcProject.projectCode, + e.epcProject.name, + u.discipline, + e.entryDate, + e.hours.sum() + )) + .from(e) + .join(e.timesheet, ts) + .join(ts.user, u) + .where( + ts.status.eq(TimesheetStatus.APPROVED), + epcProjectIdEq(filter.getProjectId()), + entryDateBetween(filter.getFrom(), filter.getTo()) + ) + .groupBy(e.epcProject.projectCode, e.epcProject.name, + u.discipline, e.entryDate); + + List rows = query.fetch(); + + return ProjectHoursReport.builder() + .filter(filter) + .rows(rows) + .totalHours(rows.stream() + .map(ProjectHoursRow::getHours) + .reduce(BigDecimal.ZERO, BigDecimal::add)) + .generatedAt(LocalDateTime.now()) + .build(); + } +} +``` + +### 2. WBS Level별 시수 분석 (No.83) + +``` +GET /api/reports/wbs-hours + ?projectId=1 + &wbsLevel=3 // 1~5 + &from=2025-04-01 + &to=2025-05-31 + &format=json +``` + +```java +public WbsHoursReport getWbsHoursReport(WbsHoursFilter filter) { + var rows = queryFactory + .select(Projections.constructor(WbsHoursRow.class, + cw.wbsCode, + cw.name, + cw.level, + cw.discipline, + e.hours.sum(), + ts.user.countDistinct() + )) + .from(e) + .join(e.timesheet, ts) + .join(e.canonicalWbs, cw) + .where( + ts.status.eq(TimesheetStatus.APPROVED), + cw.projectId.eq(filter.getProjectId()), + cw.level.eq(filter.getWbsLevel()), + entryDateBetween(filter.getFrom(), filter.getTo()) + ) + .groupBy(cw.wbsCode, cw.name, cw.level, cw.discipline) + .orderBy(cw.wbsCode.asc()) + .fetch(); + + return WbsHoursReport.builder() + .filter(filter) + .rows(rows) + .build(); +} +``` + +## PH1-2차 리포트 (2종) + +### 3. Phase별 시수 비율 (No.85) + +``` +GET /api/reports/phase-ratio?projectId=1&from=...&to=... +``` + +### 4. Non-Project 시수 비율 (No.86) + +``` +GET /api/reports/np-ratio?department=Engineering&from=...&to=... +``` + +## Excel Export + +```java +@Service +public class ReportExcelExporter { + + public byte[] exportProjectHours(ProjectHoursReport report) { + try (Workbook wb = new XSSFWorkbook()) { + Sheet sheet = wb.createSheet("프로젝트별 시수"); + + // 헤더 + CellStyle headerStyle = createHeaderStyle(wb); + Row header = sheet.createRow(0); + String[] headers = {"프로젝트코드", "프로젝트명", "Discipline", "날짜", "시수(h)"}; + for (int i = 0; i < headers.length; i++) { + Cell cell = header.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerStyle); + } + + // 데이터 + int rowIdx = 1; + for (ProjectHoursRow row : report.getRows()) { + Row r = sheet.createRow(rowIdx++); + r.createCell(0).setCellValue(row.getProjectCode()); + r.createCell(1).setCellValue(row.getProjectName()); + r.createCell(2).setCellValue(row.getDiscipline()); + r.createCell(3).setCellValue(row.getDate().toString()); + r.createCell(4).setCellValue(row.getHours().doubleValue()); + } + + // 합계 행 + Row totalRow = sheet.createRow(rowIdx); + totalRow.createCell(3).setCellValue("합계"); + totalRow.createCell(4).setCellValue(report.getTotalHours().doubleValue()); + + // Auto-size + for (int i = 0; i < headers.length; i++) sheet.autoSizeColumn(i); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + wb.write(out); + return out.toByteArray(); + } + } +} +``` + +## REST API (리포트) + +``` +# PH1-1차 +GET /api/reports/project-hours 프로젝트별 시수 분석 +GET /api/reports/project-hours/export Excel 다운로드 +GET /api/reports/wbs-hours WBS Level별 시수 분석 +GET /api/reports/wbs-hours/export Excel 다운로드 + +# PH1-2차 +GET /api/reports/phase-ratio Phase별 시수 비율 +GET /api/reports/np-ratio Non-Project 시수 비율 +GET /api/reports/wbs-version-history WBS 버전 이력 조회 + +# 공통 파라미터 +# projectId - 프로젝트 ID (필수/선택) +# from, to - 기간 (ISO 8601) +# groupBy - 집계 기준 (month, week, discipline) +# format - 응답 형식 (json, excel) +``` diff --git a/HanwhaOCN/wtmgr/07-api-spec.md b/HanwhaOCN/wtmgr/07-api-spec.md new file mode 100644 index 0000000..8d263ca --- /dev/null +++ b/HanwhaOCN/wtmgr/07-api-spec.md @@ -0,0 +1,215 @@ +# 07. REST API 스펙 요약 + +## API 설계 원칙 + +- **Prefix**: 모든 API는 `/api/wtm/` prefix 사용 (WBX GW `/api/gw/`와 분리) +- RESTful 네이밍: 복수형 명사 (`/api/wtm/projects`, `/api/wtm/timesheets`) +- 표준 HTTP 메서드: GET/POST/PUT/DELETE +- 페이징: WBX 호환 — `?skip=0&limit=20` (+ Spring Pageable 변환) +- **응답 포맷 (WBX 호환)**: + - 단일 객체: `{ "id": 1, ... }` (response_path = "") + - 목록: `{ "items": [...], "total": 150 }` (response_path = "items") + - 에러: `{ "detail": "...", "code": "..." }` (FastAPI `detail` 키 호환) +- 인증: `Authorization: Bearer {JWT}` (WBX 공유 JWT) +- API 문서: SpringDoc OpenAPI (`/swagger-ui`) — 개발 모드만 + +--- + +## 전체 API 목록 + +### Auth (인증) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| POST | `/api/wtm/auth/login` | 내부 ID/PW 로그인 → JWT 발급 | PH1-1 | +| GET | `/api/wtm/auth/sso` | Azure Entra ID SSO 시작 | PH1-1 | +| GET | `/api/wtm/auth/sso/callback` | SSO 콜백 → JWT 발급 | PH1-1 | +| POST | `/api/wtm/auth/refresh` | Access Token 갱신 | PH1-1 | +| POST | `/api/wtm/auth/logout` | 로그아웃 | PH1-1 | +| POST | `/api/wtm/auth/password/reset` | 비밀번호 재설정 요청 | PH1-1 | +| PUT | `/api/wtm/auth/password/change` | 비밀번호 변경 | PH1-1 | +| GET | `/api/wtm/auth/me` | 내 정보 (역할 포함) | PH1-1 | + +### Users (사용자) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/users` | 사용자 목록 (검색/필터) | PH1-1 | +| GET | `/api/wtm/users/{id}` | 사용자 상세 | PH1-1 | +| PUT | `/api/wtm/users/{id}` | 사용자 수정 | PH1-1 | +| PUT | `/api/wtm/users/{id}/roles` | 역할 부여/변경 | PH1-1 | +| POST | `/api/wtm/users/upload/internal` | 내부 인력 Excel 업로드 (No.1) | PH1-1 | +| POST | `/api/wtm/users/upload/subcontractor` | 외주 인력 Excel 업로드 (No.4) | PH1-1 | +| GET | `/api/wtm/users/upload/template` | 업로드 템플릿 다운로드 | PH1-1 | +| GET | `/api/wtm/admin/access-logs` | SA 액세스 로그 (No.24) | PH1-2 | + +### Projects (프로젝트) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/projects` | 프로젝트 목록 | PH1-1 | +| POST | `/api/wtm/projects` | 프로젝트 생성 (No.25) | PH1-1 | +| GET | `/api/wtm/projects/{id}` | 프로젝트 상세 | PH1-1 | +| PUT | `/api/wtm/projects/{id}` | 프로젝트 수정 (No.26~28) | PH1-1 | +| GET | `/api/wtm/projects/my` | 내 배정 프로젝트 목록 (No.51~53) | PH1-1 | +| GET | `/api/wtm/projects/{id}/members` | 프로젝트 멤버 목록 | PH1-1 | +| POST | `/api/wtm/projects/{id}/members` | 멤버 배정 (No.47~48) | PH1-1 | + +### WBS + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| POST | `/api/wtm/projects/{id}/wbs/upload` | P6 WBS 파일 업로드 (No.31) | PH1-1 | +| GET | `/api/wtm/projects/{id}/wbs/versions` | WBS 버전 목록 | PH1-1 | +| GET | `/api/wtm/projects/{id}/wbs/versions/{ver}` | WBS 트리 조회 | PH1-1 | +| POST | `/api/wtm/projects/{id}/wbs/versions/{ver}/activate` | 버전 활성화 | PH1-1 | +| GET | `/api/wtm/projects/{id}/canonical-wbs` | Canonical WBS 트리 (No.33~35) | PH1-1 | +| GET | `/api/wtm/projects/{id}/wbs/compare` | 버전 비교 (No.29) | PH1-2 | + +### TEAL + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| POST | `/api/wtm/projects/{id}/teal/upload` | TEAL 업로드 (No.36~41) | PH1-1 | +| GET | `/api/wtm/projects/{id}/teal/versions` | TEAL 버전 목록 | PH1-1 | +| GET | `/api/wtm/projects/{id}/teal/active` | 활성 TEAL (시수 입력용) | PH1-1 | +| GET | `/api/wtm/projects/{id}/teal/by-wbs/{wbsId}` | WBS별 Activity 목록 | PH1-1 | + +### Timesheets (시수) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/timesheets/week` | 주간 시수 조회/생성 | PH1-1 | +| POST | `/api/wtm/timesheets/{id}/entries` | 시수 항목 저장 | PH1-1 | +| PUT | `/api/wtm/timesheets/{id}/entries/batch` | 주간 일괄 저장 | PH1-1 | +| DELETE | `/api/wtm/timesheets/{id}/entries/{entryId}` | 항목 삭제 | PH1-1 | +| POST | `/api/wtm/timesheets/{id}/submit` | 시수 제출 (결재 요청) | PH1-1 | +| POST | `/api/wtm/timesheets/upload` | Excel 일괄 업로드 | PH1-1 | +| GET | `/api/wtm/timesheets/upload/template` | Excel 템플릿 다운로드 | PH1-1 | +| GET | `/api/wtm/timesheets/history` | 내 시수 이력 | PH1-1 | + +### Approvals (결재) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/approvals/pending` | 내 결재 대기 목록 | PH1-1 | +| POST | `/api/wtm/approvals/{id}/approve` | 승인 | PH1-1 | +| POST | `/api/wtm/approvals/{id}/reject` | 반려 | PH1-1 | +| POST | `/api/wtm/approvals/batch-approve` | 일괄 승인 | PH1-1 | +| POST | `/api/wtm/approvals/{id}/comments` | 코멘트 | PH1-1 | +| GET | `/api/wtm/approvals/{id}` | 결재 상세 | PH1-1 | +| GET | `/api/wtm/approvals/history` | 결재 처리 이력 | PH1-1 | +| GET | `/api/wtm/approvals/overdue` | 초과 미처리 목록 (No.75) | PH1-2 | + +### Reports (리포트) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/reports/project-hours` | 프로젝트별 시수 (No.82) | PH1-1 | +| GET | `/api/wtm/reports/project-hours/export` | Excel 다운로드 | PH1-1 | +| GET | `/api/wtm/reports/wbs-hours` | WBS Level별 시수 (No.83) | PH1-1 | +| GET | `/api/wtm/reports/wbs-hours/export` | Excel 다운로드 | PH1-1 | +| GET | `/api/wtm/reports/phase-ratio` | Phase별 비율 (No.85) | PH1-2 | +| GET | `/api/wtm/reports/np-ratio` | NP 비율 (No.86) | PH1-2 | + +### Home (대시보드) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/home/dashboard` | 역할별 대시보드 데이터 | PH1-1 | +| GET | `/api/wtm/home/notifications` | 알림 목록 (미승인 등) | PH1-1 | + +--- + +## 표준 응답 포맷 + +```json +// 성공 — 단일 객체 (apis.response_path = "") +{ "id": 1, "weekStartDate": "2025-04-07", "status": "DRAFT", "entries": [...] } + +// 성공 — 목록 + 페이징 (apis.response_path = "items") +{ + "items": [...], + "total": 150, + "skip": 0, + "limit": 20 +} + +// 에러 — WBX 호환 (detail 키 사용) +{ + "detail": "주간 합계 54시간 — 최대 52h 초과", + "code": "TIMESHEET_RULE_001" +} +``` + +> **WBX 호환 핵심**: 에러는 `detail` 키 (FastAPI 형식), 목록은 `items` 키 + +### Overhead Types (Non-Project 카테고리 관리) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/overhead-types` | Overhead 유형 목록 | PH1-1 | +| POST | `/api/wtm/overhead-types` | Overhead 유형 생성 (SA) | PH1-1 | +| PUT | `/api/wtm/overhead-types/{id}` | Overhead 유형 수정 (SA) | PH1-1 | + +### Work Rules (근무 규칙 설정) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/work-rules` | 근무 규칙 조회 (일 8h, 주 52h, Location별) | PH1-1 | +| PUT | `/api/wtm/work-rules` | 근무 규칙 수정 (SA) | PH1-1 | + +### Resource Assignment (인력 배정) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/projects/{id}/assignments` | 프로젝트 인력 배정 목록 | PH1-1 | +| POST | `/api/wtm/projects/{id}/assignments` | 인력 배정 (PM) | PH1-1 | +| PUT | `/api/wtm/projects/{id}/assignments/{assignId}` | 배정 수정 | PH1-1 | +| DELETE | `/api/wtm/projects/{id}/assignments/{assignId}` | 배정 해제 | PH1-1 | +| GET | `/api/wtm/projects/{id}/assignments/available` | 배정 가능 인력 조회 | PH1-1 | + +### WBS-Discipline (WBS-Discipline 매핑) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/projects/{id}/wbs-disciplines` | WBS별 Discipline 매핑 조회 | PH1-1 | +| PUT | `/api/wtm/projects/{id}/wbs-disciplines` | WBS-Discipline 매핑 저장 | PH1-1 | + +### HR Integration (SAP BTP 연동) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| POST | `/api/wtm/integration/hr/upload` | SA 수동 Excel 업로드 | PH1-2 | +| POST | `/api/wtm/integration/hr/sync` | SAP BTP 자동 동기화 수신 | PH1-2 | + +### Cognite Export (데이터 연계) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/wtm/integration/cognite/export` | Cognite Export (Employee/Project/WBS/Time Fact) | PH2 | + +--- + +## 총 API 수 + +| 모듈 | PH1-1차 | PH1-2차 | PH2 | 합계 | +|------|---------|---------|-----|------| +| Auth | 8 | 0 | 0 | 8 | +| Users | 7 | 1 | 0 | 8 | +| Projects | 7 | 0 | 0 | 7 | +| WBS | 5 | 2 | 0 | 7 | +| TEAL | 4 | 0 | 0 | 4 | +| Timesheets | 8 | 0 | 0 | 8 | +| Approvals | 7 | 1 | 0 | 8 | +| Reports | 4 | 2 | 3 | 9 | +| Home | 2 | 0 | 0 | 2 | +| Overhead Types | 3 | 0 | 0 | 3 | +| Work Rules | 2 | 0 | 0 | 2 | +| Resource Assign | 5 | 0 | 0 | 5 | +| WBS-Discipline | 2 | 0 | 0 | 2 | +| HR Integration | 0 | 2 | 0 | 2 | +| Cognite | 0 | 0 | 1 | 1 | +| **합계** | **67** | **6** | **6** | **79** | + +> 11-requirements-traceability.md에서 추가된 API 반영 (Overhead Types, Work Rules, Resource Assign, WBS-Discipline, HR Integration, Cognite) diff --git a/HanwhaOCN/wtmgr/08-sap-btp-integration.md b/HanwhaOCN/wtmgr/08-sap-btp-integration.md new file mode 100644 index 0000000..f51a618 --- /dev/null +++ b/HanwhaOCN/wtmgr/08-sap-btp-integration.md @@ -0,0 +1,113 @@ +# 08. SAP SuccessFactors BTP 연동 + +## 연동 아키텍처 + +``` +SAP SuccessFactors (HR Master) + │ OData API + ▼ +SAP BTP Integration Suite (CPI) + │ 필드 매핑 + 스케줄링 + │ REST API (JSON) + ▼ +WTM Spring Boot + POST /api/wtm/integration/hr/sync +``` + +## 단계별 구현 + +| Phase | 방식 | 설명 | +|-------|------|------| +| **PH1-1** | Excel 파일 업로드 | SA가 SF Export 파일을 수동 업로드. BTP 불필요 | +| **PH1-2** | BTP 배치 (일 1회) | SAP BTP CPI → WTM REST API 자동 호출 | +| **PH2** | 실시간 이벤트 | SF Employee Events → BTP → WTM Webhook | + +## HR Master Data 필드 매핑 (No.2) + +| SAP SuccessFactors | WTM 컬럼 | 설명 | +|---------------------|----------|------| +| personIdExternal | employee_number | 사번 (= 로그인 ID) | +| firstName + lastName | full_name | 성명 | +| email | email | 이메일 | +| businessUnit | business_unit | 조직 LV1 | +| division | division | 조직 LV2 | +| department | department | 조직 LV3 | +| customString1 | discipline_team | 조직 LV4 (Discipline/Team) | +| customString2 | part | 조직 LV5 (Part) | +| attendanceType | attendance_type | 근태 유형 | +| jobCode | individual_job_code | 직무 코드 | + +## Spring Boot 수신 API + +```java +@RestController +@RequestMapping("/api/wtm/integration/hr") +@PreAuthorize("hasRole('SYSTEM') or @wbx.check('USER', 'ADMIN')") +public class HrIntegrationController { + + /** PH1-1: SA 수동 Excel 업로드 */ + @PostMapping("/upload") + public HrSyncResult uploadExcel(@RequestParam("file") MultipartFile file) { + return hrSyncService.uploadFromExcel(file); + } + + /** PH1-2: SAP BTP 자동 동기화 수신 */ + @PostMapping("/sync") + public HrSyncResult sync(@Valid @RequestBody HrSyncRequest request) { + return hrSyncService.syncAll(request.employees()); + } +} + +public record HrSyncRequest( + List employees, + String syncSource, // "SAP_BTP" | "MANUAL_UPLOAD" + LocalDateTime syncTime +) {} + +public record HrEmployeeDto( + String employeeNumber, String fullName, String email, + String businessUnit, String division, String department, + String disciplineTeam, String part, + String attendanceType, String individualJobCode, + LocalDate startDate, LocalDate endDate, + boolean isActive +) {} +``` + +## P6 연동 (NF.14) + +> 물리적 I/F 없음 — 파일 기반만 + +- PM이 P6 Export Excel을 `/api/wtm/projects/{id}/wbs/upload`에 업로드 +- PCM이 `/api/wtm/projects/{id}/wbs/versions/{ver}/approve`로 승인 +- 월 1회 Snapshot 비교 (No.32) + +## Cognite 연동 (NF.15, PH2) + +``` +Export 대상 (No.80): +- Employee Dimension +- Project Dimension +- Canonical WBS Dimension +- Time Fact Table +- Mapping Version Metadata +``` + +```java +@GetMapping("/api/wtm/integration/cognite/export") +@PreAuthorize("@wbx.check('INTEGRATION', 'EXPORT')") +public CogniteExportData export(@RequestParam LocalDate from, @RequestParam LocalDate to) { + return cogniteExportService.export(from, to); +} +``` + +## 사전 확보 사항 + +| 항목 | 제공 주체 | 마감 | +|------|----------|------| +| BTP 테넌트 접근 권한 | 한화시스템/SAP | W2 | +| SF OData API 엔드포인트 + 인증 정보 | 한화시스템 | W2 | +| 필드 매핑 확정 (SF → WTM) | 한화오션 + 아큐라 | W3 | +| BTP CPI iFlow 개발 권한 | 한화시스템 | PH1-2 | +| P6 WBS Export 샘플 파일 | 한화오션 | W1 | +| Cognite Extractor 서버 접근 | 한화시스템 | PH2 | diff --git a/HanwhaOCN/wtmgr/09-devops-infra.md b/HanwhaOCN/wtmgr/09-devops-infra.md new file mode 100644 index 0000000..366dba2 --- /dev/null +++ b/HanwhaOCN/wtmgr/09-devops-infra.md @@ -0,0 +1,229 @@ +# 09. 한화오션 보안 SW / Azure 인프라 + +> **전제**: Nginx 라우팅, Docker Compose, systemd 등 일반 인프라는 +> `plans/wbx-spring/07-infra-deploy.md`에서 제공. +> 본 문서는 **한화오션 고객 요구 보안 SW + Azure 구성**만 다룹니다. +> +> **비기능 요구사항 매핑**: NF.1~2 (Cloud), NF.3~7 (Security), NF.8~10 (Monitoring), NF.16~17 (Architecture) + +## Azure 인프라 구성 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Azure Resource Group │ +│ │ +│ ┌───────────────┐ ┌──────────────────────────────────┐ │ +│ │ Azure App │ │ Virtual Network (VNet) │ │ +│ │ Gateway │ │ │ │ +│ │ (WAF + SSL) │ │ ┌─────────┐ ┌─────────────┐ │ │ +│ │ │───▶│ │ VM #1 │ │ VM #2 │ │ │ +│ └───────────────┘ │ │ Nginx │ │ Nginx │ │ │ +│ │ │ +Tomcat │ │ +Tomcat │ │ │ +│ │ │ (Active)│ │ (Standby) │ │ │ +│ │ └────┬────┘ └──────┬──────┘ │ │ +│ │ │ │ │ │ +│ │ ┌────┴────────────────┴────┐ │ │ +│ │ │ Azure SQL / PostgreSQL │ │ │ +│ │ │ (PaaS, HA built-in) │ │ │ +│ │ └──────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────┐ ┌──────────────┐ │ │ +│ │ │ Redis │ │ Blob Storage │ │ │ +│ │ │ Cache │ │ (Files) │ │ │ +│ │ └──────────┘ └──────────────┘ │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │ +│ │ Azure Entra │ │ Azure Monitor│ │ Key Vault │ │ +│ │ ID (SSO) │ │ + Log │ │ (Secrets) │ │ +│ └──────────────┘ │ Analytics │ └────────────────┘ │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Nginx 설정 (이중화) + +```nginx +# /etc/nginx/conf.d/wtmgr.conf +upstream wtmgr_backend { + server 127.0.0.1:8080; + keepalive 32; +} + +server { + listen 80; + server_name wtmgr.hanwhaocean.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name wtmgr.hanwhaocean.com; + + ssl_certificate /etc/ssl/certs/wtmgr.pem; + ssl_certificate_key /etc/ssl/private/wtmgr.key; + ssl_protocols TLSv1.2 TLSv1.3; + + # Security Headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=63072000" always; + + # Rate Limiting + limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; + + # API + location /api/ { + proxy_pass http://wtmgr_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 50m; # Excel 업로드 + } + + # Login Rate Limiting + location /api/auth/login { + limit_req zone=login burst=3 nodelay; + proxy_pass http://wtmgr_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # Actuator (내부만) + location /actuator/ { + allow 10.0.0.0/8; + deny all; + proxy_pass http://wtmgr_backend; + } + + # Frontend SPA + location / { + root /var/www/wtmgr/frontend; + index index.html; + try_files $uri $uri/ /index.html; + } +} +``` + +## CI/CD · Docker · 배포 + +> CI/CD 파이프라인(GitHub Actions, Azure DevOps), Dockerfile, docker-compose, +> Nginx 설정 등 일반 인프라는 **wbx-spring 프레임워크**에서 표준 제공합니다. +> +> 상세: `plans/wbx-spring/07-infra-deploy.md` +> +> WTM은 아래 **고객 전용 설정만** 추가합니다: + +### WTM 전용 환경변수 (.env) + +```env +# wbx-spring 공통 (프레임워크 제공) +JWT_SECRET=... +DB_HOST=... +SPRING_PROFILES_ACTIVE=prod,azure,mssql + +# WTM 전용 +WTM_WORK_RULES_DEFAULT_MIN_DAILY=8 +WTM_WORK_RULES_DEFAULT_MAX_WEEKLY=52 +SAP_BTP_ENDPOINT=https://btp.hanwhaocean.com/api +SAP_BTP_CLIENT_ID=... +SAP_BTP_CLIENT_SECRET=... +``` + +### WTM 전용 application-prod.yml (wbx.spring 위에 추가) + +```yaml +wtm: + work-rules: + default-min-daily-hours: 8 + default-max-weekly-hours: 52 + sap: + btp-endpoint: ${SAP_BTP_ENDPOINT} + btp-client-id: ${SAP_BTP_CLIENT_ID} + sync-cron: "0 0 2 * * *" # 매일 02시 +``` + +## 보안 SW — 한화오션 표준 (NF.3~7) + +### 서버 보안 (NF.3) + +| SW | 용도 | 구성 방법 | +|----|------|----------| +| **HIWARE** | 서버 접근 제어 | Azure VM에 HIWARE Agent 설치 | +| **V3** (AhnLab) | 서버 백신 | Azure VM에 V3 Agent 설치 | +| **Secuver TOS** | 서버 보안 (파일 무결성) | Azure VM에 Agent 설치 | + +### DB 보안 (NF.4) + +| SW | 용도 | 구성 방법 | +|----|------|----------| +| **Cubeone** | DB 암호화 (컬럼 레벨) | Azure SQL TDE + Cubeone 연동 | +| **Dbsafer** | DB 접근 제어/감사 | Dbsafer Proxy 서버 구성 | + +### 클라우드 보안 (NF.5) + +| SW | 용도 | +|----|------| +| **Azure Defender** | 위협 탐지 (VM + SQL) | +| **Azure Log Analytics** | 보안 이벤트 분석 | +| Azure WAF | 웹 방화벽 (App Gateway) | +| Azure DDoS Protection | DDoS 방어 | +| Azure Key Vault | 시크릿 관리 (DB PW, JWT 키) | + +### 정보보호 심의 (NF.6~7) + +``` +□ 웹 애플리케이션 취약점 점검 (OWASP Top 10) +□ 서버/네트워크 인프라 보안 점검 +□ 소스코드 보안 점검 (SonarQube / Fortify) +□ 모의해킹 (Penetration Testing) +□ 개인정보 보호 관리 (개인정보보호법 준수) +□ 한화그룹 보안 표준 준수 확인 +``` + +## 모니터링 SW — 한화오션 표준 (NF.8~10) + +| SW | 용도 | 대상 | 구성 | +|----|------|------|------| +| **onTune** | SMS (서버 모니터링) | Azure VM | Agent 설치 | +| **MCCS** | HA (클러스터) | WAS 이중화 | Linux: Keepalived 대안 검토 | +| **Maxguage** | DB 모니터링/성능 분석 | Azure SQL | Proxy 구성 | +| **Jennifer** | WAS(Tomcat) APM | Spring Boot | `-javaagent` 연동 | + +### Jennifer APM 연동 (NF.10) + +> Dockerfile, docker-compose 설정은 wbx-spring 표준을 사용합니다. +> 아래는 **WTM 전용** JVM 옵션 추가 사항입니다. + +```yaml +# systemd 서비스 또는 docker-compose에 JVM 옵션 추가 +JAVA_OPTS: >- + -javaagent:/opt/jennifer/agent.java/jennifer.jar + -Djennifer.config=/opt/jennifer/conf/jennifer.conf + +# application-prod.yml (WTM 전용 메트릭 태그) +management: + metrics: + tags: + application: wtm-api + health: + db: + enabled: true + redis: + enabled: true + +# 알림 규칙: CPU > 80%, Memory > 85%, 5xx > 10/min, Response Time > 3s +``` + +## 백업 전략 (NF.16~17) + +| 항목 | 방식 | 주기 | +|------|------|------| +| Azure SQL | 자동 백업 (Point-in-Time) | 5분 간격 | +| Azure SQL | Long-term Retention | 주간/월간 | +| Blob Storage | GRS (Geo-Redundant) | 실시간 복제 | +| VM 설정 | Azure Backup | 일간 | +| 코드 | GitHub | 실시간 (Git) | diff --git a/HanwhaOCN/wtmgr/10-schedule-milestones.md b/HanwhaOCN/wtmgr/10-schedule-milestones.md new file mode 100644 index 0000000..cba0f69 --- /dev/null +++ b/HanwhaOCN/wtmgr/10-schedule-milestones.md @@ -0,0 +1,168 @@ +# 10. 일정 및 마일스톤 + +## PH1-1차 상세 일정 (9주: 4/1 ~ 5/31) + +### W1~W2 (4/1 ~ 4/12): 분석·설계 + +| 태스크 | 담당 | 산출물 | +|--------|------|--------| +| 요구사항 확정 (86+17개) | 전원 | 요구사항 추적표 | +| DB 스키마 설계 (Flyway V1~V6) | BE 리드 | ERD, DDL | +| REST API 스펙 확정 (79개) | BE 리드 | OpenAPI 스펙 | +| Spring Boot 프로젝트 생성 (3.5.x) | BE 리드 | 프로젝트 템플릿 | +| Azure 인프라 세팅 | DevOps | VM, DB, Redis, Blob | +| Entra ID 계정 확보 (SSO 연동) | DevOps + 고객 | API 계정 정보 | +| P6 WBS 샘플 파일 수령 | PM | 파일 포맷 확인 | +| Canonical WBS 구조 확정 | PM + 고객 | WBS 트리 | +| wbx-spring 프레임워크 연동 확인 (인증/권한/결재/알림) | BE 리드 | 연동 테스트 | +| CI/CD 파이프라인 구성 (wbx-spring 표준 활용) | DevOps | 배포 파이프라인 | + +**마일스톤: 4/10 — WBS 구조 및 설계 확정** + +### W3~W8 (4/13 ~ 5/20): 핵심 개발 + +#### BE 개발 (W3~W8) + +| 주차 | 모듈 | API 수 | 담당 | +|------|------|--------|------| +| W3~W4 | Auth (로그인/SSO/JWT/권한) | 8 | BE 시니어 | +| W3~W4 | User (인력 관리/파일 업로드) | 7 | 풀스택 ③ | +| W4~W5 | Project/WBS/TEAL | 17 | 풀스택 ① | +| W5~W7 | Timesheet 3종 + 규칙 엔진 | 8 | 풀스택 ② | +| W6~W7 | Approval 결재 워크플로우 | 7 | BE 시니어 | +| W7~W8 | Report 2종 + Excel Export | 4 | 풀스택 ③ | +| W7~W8 | Home 대시보드 | 2 | 풀스택 ③ | + +**마일스톤: 4/17 — 분석/설계 1차 완료 (DB, API 스펙)** +**마일스톤: 5/7 — BE 핵심 API 완료 (시수/WBS/결재)** + +#### FE 개발 (W4~W8) + +| 주차 | 화면 | 화면 수 | 담당 | +|------|------|---------|------| +| W4 | 로그인/SSO/비밀번호 | 4 | 풀스택 ③ | +| W4~W5 | 홈 대시보드 (역할별 5종) | 5 | 풀스택 ③ | +| W5 | 사용자/인력 관리 | 3 | 풀스택 ③ | +| W5~W6 | 프로젝트/WBS/TEAL | 6 | 풀스택 ① | +| W6~W7 | 시수 입력 통합 화면 (3종 탭) | 4 | 풀스택 ② | +| W7~W8 | 결재 화면 | 3 | 풀스택 ② | +| W8 | 리포트 2종 | 2 | 풀스택 ① | + +**마일스톤: 5/20 — FE 화면 완료, 통합 테스트 시작** + +### W8~W9 (5/20 ~ 5/31): QA · UAT · 배포 + +| 태스크 | 담당 | +|--------|------| +| 교차 QA (BE ↔ FE) | 전원 | +| 버그 수정 (P1/P2 우선) | 전원 | +| 성능 테스트 (동시 접속 100명) | DevOps | +| UAT (고객 테스트) | PM + 고객 | +| 프로덕션 배포 | DevOps | +| 데이터 마이그레이션 (HR Master 등) | 풀스택 ③ | + +**마일스톤: 5/31 — ★ PH1-1차 오픈** + +--- + +## PH1-2차 상세 일정 (4주: 6/1 ~ 6/30) + +### W1~W2 (6/1 ~ 6/13): 고도화 개발 + +| 기능 | 요구사항 | 담당 | +|------|---------|------| +| WBS 버전 비교 UI | No.29 | 풀스택 ① | +| EPC Revision 관리 | No.64 | 풀스택 ② | +| SA 권한 고도화 + 액세스 로그 | No.19, 24 | BE 시니어 | +| 외부 사용자 MFA 인증 | No.9 | BE 시니어 | + +### W2~W3 (6/9 ~ 6/19): 분석·리포트 + +| 기능 | 요구사항 | 담당 | +|------|---------|------| +| 결재 초과 하이라이트 | No.75 | 풀스택 ② | +| Phase별 시수 비율 리포트 | No.85 | 풀스택 ③ | +| Non-Project 시수 비율 리포트 | No.86 | 풀스택 ③ | +| HR 배치 자동 업데이트 | No.3 | 풀스택 ① | + +### W3~W4 (6/16 ~ 6/30): 통합 QA · 배포 + +| 태스크 | 담당 | +|--------|------| +| 통합 테스트 + 버그 수정 | 전원 | +| DL 결재 정책 확정 반영 | BE 시니어 | +| UAT · 검수 | PM + 고객 | +| 프로덕션 배포 | DevOps | + +**마일스톤: 6/30 — ★ PH1-2차 오픈 (PH1 최종 완료)** + +--- + +## 인력 투입 계획 + +| 역할 | PH1-1차 (9주) | PH1-2차 (4주) | 합계 | +|------|---------------|---------------|------| +| BE 시니어 / 기술 리드 | 2.0 M/M | 1.0 M/M | 3.0 | +| 풀스택 엔지니어 ① (WBS·TEAL·프로젝트) | 2.0 M/M | 1.0 M/M | 3.0 | +| 풀스택 엔지니어 ② (시수·결재·규칙엔진) | 2.0 M/M | 1.0 M/M | 3.0 | +| 풀스택 엔지니어 ③ (리포트·로그인·사용자) | 2.0 M/M | 1.0 M/M | 3.0 | +| DevOps (파트타임) | 1.0 M/M | 0.5 M/M | 1.5 | +| QA 겸임 | 1.0 M/M | 0.5 M/M | 1.5 | +| **합계** | **~10.0 M/M** | **~5.0 M/M** | **~15.0 M/M** | + +--- + +## 리스크 및 의존성 + +| 리스크 | 영향 | 대응 | +|--------|------|------| +| Entra ID 계정 지연 | SSO 개발 차단 | W1 내 확보 필수, ID/PW fallback 우선 구현 | +| P6 WBS 포맷 불확실 | 파서 개발 지연 | W1 내 샘플 파일 수령, 포맷 확정 | +| Canonical WBS 구조 미확정 | 시수 입력 개발 차단 | W2 내 확정 필수 | +| DL 결재 정책 미확정 (No.23) | 결재 로직 불완전 | 기본 구조 선 구현, 정책 확정 후 로직 추가 | +| Azure 인프라 지연 | 배포 차단 | 로컬 Docker 환경으로 개발 병행 | + +--- + +## PH2 이관 항목 및 사유 + +> 출처: `requierment.xlsx` 한글 시트 "협의 및 PH2 이관 제안 사유" 컬럼 + +| No. | 요구사항 | PH2 이관 사유 | +|-----|---------|--------------| +| 3 | HR 정기 배치 자동 업데이트 | 초기에는 수동 파일 업로드로 운영 가능. 배치는 안정화 후 추가 | +| 5 | 외주 인력 개별 입력창 | 등록 정책 미확정. 정책 확정 후 적용 방식 결정 | +| 9 | 외부 사용자 2Way 인증 | 외주 포털 구축 후 적용. 1단계에서 외주 접속 자체 없음 | +| 19 | SA 전체 기능 컨트롤 | 보안 감사용 로그 기능. PH1 운영에 직접 영향 없음 | +| 24 | SA 접속 및 Activity Log | SA 액세스·활동 로그는 보안 감사용. 운영 안정화 후 추가 | +| 29 | WBS 버전 관리 비교 UI | WBS 업로드(No.27·28)는 PH1 필수. 버전 비교는 PH2 | +| 30 | 기존 WBS 시수 조회 | No.29와 연계. 버전 비교 UI와 함께 이관 | +| 32 | P6 WBS 월별 스냅샷 비교 | 운영 안정화 이후 활용 가능한 고도화 기능 | +| 42 | Timesheet 기본 입력 기준 수정 | 기본 일 8h·주 52h 상한은 PH1. 세부 규칙 UI는 PH2 | +| 46 | 초과근무 기준시간, 휴게시간 규칙 | 기본 상한은 PH1. 세부 규칙 설정 UI는 PH2 고도화 | +| 49 | Project별 Location, Job_Role 설정 | Unit Rate 변동 관리는 PH2에서 수행 | +| 50 | WBS-Discipline 자동 Assign | Discipline 및 투입인력 선택방식 미확정 | +| 54 | Favorite / Default 값 설정 | 편의 기능. 사용 패턴 파악 후 2단계 적용 | +| 63 | Project별 Bench Marking | 충분한 시수 데이터 축적 이후 의미. 2단계 제안 | +| 65 | Location/Role/진행률 입력 | 복수 역할·국가 변동 사례 발생 후 적용. 단일 Rate로 운영 | +| 76 | RCP 연계 Plan vs Actual 분석 | RCP 시스템 미개발. RCP 구축 완료 후 연계 예정 | +| 77 | RCP 연계 Capacity Gap 분석 | RCP 미개발. RCP 구축 완료 후 연계 예정 | +| 78 | RCP 연계 Productivity Trend 분석 | RCP 미개발. RCP 구축 완료 후 연계 예정 | +| 79 | WBS 버전 이력 조회 UI | 버전 등록은 PH1. 이력 조회 UI 1단계 필요성 협의 필요 | +| 80 | Cognite 연계 데이터 Export | Extractor 서버 구성 필요. 인프라 준비 후 PH2 구현 | +| 81 | P6 vs Canonical WBS Mis-Align 검토 | 운영 데이터 축적 후 필요성 높아짐. 2단계 적용 | +| 84 | Discipline별 생산성 분석 | Progress Rate 입력(PH2)이 없으면 의미 없음 | +| 85 | Phase별 Manhour 비율 분석 | 기본 리포트(No.82·83)는 PH1. 비율 분석은 고도화로 PH2 협의 | +| 86 | Non-Project Manhour 비율 분석 | 기본 시수 조회로 대체 가능. PH2 이관 협의 | + +> PH1 Y=62건, PH2 이관 N=24건, 합계 86건 + +## 착수 즉시 필요 협조사항 + +| 항목 | 마감 | 제공 주체 | +|------|------|----------| +| Azure Entra ID API 계정 + 연동 정보 | W1 (4/4) | 한화시스템 | +| Canonical WBS 구조 최종 확정 | W2 (4/11) | 한화오션 | +| P6 WBS Export 샘플 파일 | W1 (4/4) | 한화오션 | +| Azure 인프라 접근 권한 | W1 (4/4) | 한화시스템 | +| HR Master Data 샘플 | W1 (4/4) | 한화오션 | diff --git a/HanwhaOCN/wtmgr/11-requirements-traceability.md b/HanwhaOCN/wtmgr/11-requirements-traceability.md new file mode 100644 index 0000000..11d79e0 --- /dev/null +++ b/HanwhaOCN/wtmgr/11-requirements-traceability.md @@ -0,0 +1,934 @@ +# 11. 요구사항 추적표 (requierment.xlsx 기반) + +> 원본: `d:\sc\requierment.xlsx` — 기능 86개 + 비기능 17개 + 확인필요사항 7건 +> 수정일: 2026-03-25 + +--- + +## 0. 기능 요구사항 전체 매핑표 + +> PH1 Y/N은 Excel "Phase 1 (Y/N)" 컬럼 기준 + +### User Registration (No.1~7) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 1 | 기준정보 | SAP SF HR Master 파일 업로드 | Y | 08-sap | PH1 수동 업로드 | +| 2 | 기준정보 | 내부 인력 필드 (사번~Part 5레벨) Read-only | Y | 02-db | users 테이블 | +| 3 | 기준정보 | 정기 배치 자동 업데이트 | N | 08-sap | PH2: 안정화 후 | +| 4 | 기준정보 | 외주 인력 파일 업로드 (표준 템플릿) | Y | 07-api | 확인필요사항#1 | +| 5 | 기준정보 | 외주 인력 개별 입력창 | N | - | PH2: 정책 미확정 | +| 6 | 기준정보 | 파일 업로드는 SA가 관리 | Y | 07-api | SA 권한 | +| 7 | 기준정보 | 외부 사용자 최소 정보 (회사명~내부담당자) | Y | 02-db | users 테이블 확장 | + +### Login (No.8~16) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 8 | 로그인 | 내부 사용자 ID/PW 로그인 | Y | wbx-spring | AuthController | +| 9 | 로그인 | 외부 사용자 2Way 인증 | N | - | PH2: 외주 포털 후 | +| 10 | 로그인 | ID/PW 찾기 기능 | Y | wbx-spring | PasswordPolicy | +| 11 | 로그인 | ID = 사번 (오션 표준) | Y | 02-db | employee_number | +| 12 | 로그인 | PW 정책 (대소문자+특수문자) | Y | wbx-spring | PasswordPolicy | +| 13 | 로그인 | PW 입력 시 "*" 마스킹 | Y | FE 기본 | - | +| 14 | 로그인 | PW 주기적 재설정 | Y | wbx-spring | expiryDays | +| 15 | 로그인 | PW 5회 실패 시 재설정 프로세스 | Y | wbx-spring | maxFailedAttempts | +| 16 | Interface | SSO (Entra ID) 적용 | Y | wbx-spring | SsoSuccessHandler | + +### User Home (No.17~24) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 17 | 권한관리 | 권한별 홈 페이지 라우팅 | Y | 07-api | 확인필요사항#2 | +| 18 | 사용자등급 | 6종 역할 (SA/PM/PCM/PTK/DL/User) | Y | 02-db | roles 테이블 | +| 19 | 사용자등급 | SA 전체 기능 컨트롤 | N | - | PH2: 보안 감사용 | +| 20 | 사용자등급 | PM 권한 (WBS/Member/승인) | Y | 05-approval | | +| 21 | 사용자등급 | PCM/PTK 권한 (WBS Task 배정) | Y | 04-wbs | | +| 22 | 사용자등급 | User 할당 Project/WBS만 조회 | Y | 03-timesheet | | +| 23 | 사용자등급 | DL 결재 권한 (조직원 Timesheet) | Y | 05-approval | DL 정책 TBD | +| 24 | 사용자등급 | SA 접속/Activity 로그 관리 | N | - | PH2: 보안 감사 | + +### Project Registration (No.25~30) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 25 | PJT Creation | 프로젝트 생성 절차 (PM/PCM→SA 승인) | Y | 07-api | | +| 26 | PJT Creation | Project Type 3종 (Non/Other/EPC) | Y | 02-db | project_type | +| 27 | PJT Creation | Canonical WBS 파일 업로드→PM 승인 | Y | 04-wbs | | +| 28 | PJT Edit | 변경 WBS 신규 파일 업로드 | Y | 04-wbs | | +| 29 | PJT Edit | WBS 버전 관리/비교 | N | 04-wbs | PH1-2차 | +| 30 | PJT Edit | 기존 WBS 시수 조회 | N | - | PH2: No.29 연계 | + +### WBS Upload (No.31~41) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 31 | P6 WBS | WBS L5 + Activity 정보 접수 | Y | 04-wbs | P6WbsParser | +| 32 | P6 WBS | 월단위 Snapshot 비교 | N | - | PH2: 고도화 | +| 33 | Canonical WBS | 표준 WBS, Project별 수정 불가 | Y | 04-wbs | | +| 34 | Canonical WBS | WBS 5레벨 구조 (L1~L5) | Y | 04-wbs | | +| 35 | Canonical WBS | WBS Upload 5단계 절차 | Y | 04-wbs | | +| 36 | WBS Versioning | Effective 날짜 기준 버전 등록 | Y | 04-wbs | | +| 37 | WBS Versioning | 종결/폐기 Mapping 사용불가 처리 | Y | 04-wbs | | +| 38 | TEAL | TEAL 선정 기준 (MH투입/관리/측정 가능) | Y | 04-wbs | | +| 39 | TEAL | TEAL 선정 및 Upload 4단계 | Y | 04-wbs | | +| 40 | TEAL Versioning | 버전 정보 (Version/Date/승인/변경Log) | Y | 04-wbs | | +| 41 | TEAL Versioning | 과거 TT는 입력 당시 버전 유지 | Y | 04-wbs | | + +### Resource Assignment (No.42~50) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 42 | Availability | SA가 기본 입력 기준 수정 | N | - | PH2: 확인필요사항#4 | +| 43 | Availability | 1일 최소 근무시간 8시간 | Y | 03-timesheet | RuleEngine | +| 44 | Availability | Activity 최소 1개 이상 | Y | 03-timesheet | | +| 45 | Availability | 주 최대 52시간, Location별 다름 | Y | 03-timesheet | RuleEngine | +| 46 | Availability | 초과근무/휴게시간 규칙 | N | - | PH2: 세부 규칙 UI | +| 47 | Project Assign | Discipline → Project Assign | Y | 07-api | | +| 48 | Project Assign | Non-Project는 별도 Assign 없이 전체 | Y | 03-timesheet | | +| 49 | Availability | Project별 Location/Job_Role 설정 | N | - | PH2: Unit Rate | +| 50 | WBS Assign | WBS-Discipline 자동 Assign | N | - | PH2: 방식 미확정 | + +### My Project Setup (No.51~54) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 51 | Assigned PJT | Assign된 Project 정보 확인 | Y | 07-api | /my-projects | +| 52 | Assigned PJT | 프로젝트 상세 정보 조회 | Y | 07-api | | +| 53 | Assigned PJT | 권한별 정보 제한 | Y | wbx-spring | RBAC | +| 54 | Activity Setup | Favorite/Default 설정 | N | - | PH2: 편의 기능 | + +### Time Sheet (No.55~69) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 55 | Non-Project | Overhead 최소 입력 (Type/Activity/Hour) | Y | 03-timesheet | NON_PROJECT | +| 56 | Non-Project | Overhead Type 4종 (수정 가능) | Y | 02-db | overhead_types | +| 57 | Non-Project | TEAL 기반 Activity 선택 | Y | 03-timesheet | | +| 58 | Non-Project | Resource Assignment 규칙 적용 | Y | 03-timesheet | RuleEngine | +| 59 | Non-Project | WBS/Project 정보 입력 불가 | Y | 03-timesheet | | +| 60 | Other Project | Tender/Pre-FEED/FEED/Internal 포함 | Y | 03-timesheet | OTHER_PROJECT | +| 61 | Other Project | 최소 입력 (PJT/WBS L2~4/Activity/Hour) | Y | 03-timesheet | | +| 62 | Other Project | P6 연동 불필요, Canonical WBS 사용 | Y | 04-wbs | | +| 63 | Other Project | Project별 Bench Marking | N | - | PH2: 데이터 축적 후 | +| 64 | EPC Project | EPC 필수 입력 (PJT/WBS L2~5/Activity/Hour) | Y | 03-timesheet | PH1-2차: Revision | +| 65 | EPC Project | Location/Role/진행률 추가 입력 | N | - | PH2: Unit Rate | +| 66 | EPC Project | P6 WBS 비노출, 할당 WBS만 선택 | Y | 03-timesheet | | +| 67 | 공통 | Remark 입력 (선택) | Y | 03-timesheet | | +| 68 | 공통 | Excel 템플릿 제공 + 임포트 | Y | 03-timesheet | | +| 69 | 공통 | 일자별 Activity별 1행, 합계 표시 | Y | 03-timesheet | | + +### Approval (No.70~75+) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 70 | Process | Daily 입력, Weekly 승인, Alert | Y | 05-approval | | +| 71 | Process | User→DL→PM 3단계 승인 | Y | 05-approval | | +| 72 | Process | 승인/반려 + Comment | Y | 05-approval | | +| 73 | Process | 일괄 승인 | Y | 05-approval | | +| 74 | Process | 승인 이력 조회 | Y | 05-approval | | +| 75 | Process | 초과 기준 하이라이트 (min/max/OT) | Y | 05-approval | PH1-2차 | + +### Reporting (No.76~86) + +| No. | Category | 요구사항 요약 | PH1 | 계획서 | 비고 | +|-----|----------|-------------|:---:|--------|------| +| 76 | RCP연계 | RCP 연계 Plan vs Actual 분석 | N | - | PH2: RCP 시스템 미개발 | +| 77 | RCP연계 | RCP 연계 Capacity Gap 분석 | N | - | PH2: RCP 미개발 | +| 78 | RCP연계 | RCP 연계 Productivity Trend 분석 | N | - | PH2: RCP 미개발 | +| 79 | Version | WBS 버전 관리 + 이력 조회 | N | 04-wbs | PH2: 이력 조회 UI 협의 | +| 80 | Interface | Cognite 연계 데이터 Export | N | 08-sap | PH2: Extractor 서버 필요 | +| 81 | WBS 검토 | P6 WBS vs Canonical WBS Mis-Align 검토 | N | - | PH2: 운영 데이터 축적 후 | +| 82 | 일반 보고서 | Project별 투입 Manhour 분석 | Y | 06-report | PH1 기본 리포트 | +| 83 | 일반 보고서 | Canonical WBS Level별 Manhour 분석 | Y | 06-report | PH1 기본 리포트 | +| 84 | 일반 보고서 | Discipline별 생산성 분석 | N | - | PH2: Progress Rate 필요 | +| 85 | 일반 보고서 | Phase별 Manhour 비율 분석 | N | 06-report | PH1-2차 또는 PH2 협의 | +| 86 | 일반 보고서 | Non-Project Manhour 비율 분석 | N | 06-report | PH1-2차 또는 PH2 협의 | + +### Non-Functional (NF.1~17) + +| NF | Category | 요구사항 요약 | 계획서 | +|----|----------|-------------|--------| +| 1 | Cloud/Server | Azure Hybrid Security Zone | 09-devops | +| 2 | Cloud/Server | Azure IaaS 기반 | 09-devops | +| 3 | Security/Server | HIWARE, V3, Secuver TOS | 09-devops | +| 4 | Security/DB | Cubeone (TDE), Dbsafer (Proxy) | 09-devops | +| 5 | Security/Cloud | Defender + Analytics | 09-devops | +| 6 | Security/정보보호 | 보안 취약점 점검 (웹/모바일/서버/소스) | 09-devops | +| 7 | Security/정보보호 | 개인정보 보호 정책 준수 | 09-devops | +| 8 | Monitoring/Server | onTune (SMS), MCCS (HA) | 09-devops | +| 9 | Monitoring/DB | Maxguage (DB 모니터링) | 09-devops | +| 10 | Monitoring/App | Jennifer (APM) | 09-devops | +| 11 | Auth/Internal | Entra ID SSO | wbx-spring | +| 12 | Auth/External | 2Way 인증 (외부 사용자) | PH2 | +| 13 | Interface/HR | SAP BTP CPI (SuccessFactors) | 08-sap | +| 14 | Interface/P6 | P6 파일 기반 연동 (물리 I/F 없음) | 04-wbs | +| 15 | Interface/Cognite | Extractor 서버 필요 | PH2 | +| 16 | Architecture | HA 구성 (Web, WAS) | 09-devops | +| 17 | Architecture | 백업 전략 (시스템 복구) | 09-devops | + +--- + +## 1. HR Master Data 필드 상세 (No.1~2) + +SAP SuccessFactors에서 업로드되는 내부 인력 정보 필드. **Read-only**로 관리. + +> **No.2 원문 (Eng)**: Employee Number, Employee Name, Business Unit (LV1), Division (LV2), Department (LV3), Discipline/Team (LV4), Part (LV5), Attendance Type, Individual Job Code + +```sql +-- V1 보완: users 테이블 컬럼 (SAP SF 필드 매핑) + +ALTER TABLE users ADD COLUMN employee_number VARCHAR(20); -- 사번 = 로그인 ID (No.11) +ALTER TABLE users ADD COLUMN business_unit VARCHAR(100); -- LV1: Business Unit +ALTER TABLE users ADD COLUMN division VARCHAR(100); -- LV2: Division +ALTER TABLE users ADD COLUMN department VARCHAR(100); -- LV3: Department +ALTER TABLE users ADD COLUMN discipline_team VARCHAR(100); -- LV4: Discipline/Team +ALTER TABLE users ADD COLUMN part VARCHAR(100); -- LV5: Part (★ 5레벨) +ALTER TABLE users ADD COLUMN attendance_type VARCHAR(50); -- 근태유형 (SAP SF) +ALTER TABLE users ADD COLUMN individual_job_code VARCHAR(50); -- 개인 직무코드 (SAP SF) +ALTER TABLE users ADD COLUMN job_role VARCHAR(100); -- Job Role +ALTER TABLE users ADD COLUMN grade VARCHAR(50); -- Grade/직급 +ALTER TABLE users ADD COLUMN start_date DATE; -- 입사일 +ALTER TABLE users ADD COLUMN end_date DATE; -- 종료일 +ALTER TABLE users ADD COLUMN company_name VARCHAR(200); -- 외주: 회사명 (No.7) +ALTER TABLE users ADD COLUMN org_unit VARCHAR(200); -- 외주: 조직 정보 (No.7) +ALTER TABLE users ADD COLUMN internal_contact VARCHAR(200); -- 외주: 내부 담당자 정보 (No.7) + +-- 조직 계층 테이블 (HR 데이터 정규화) — 5레벨 +CREATE TABLE org_hierarchy ( + id BIGINT IDENTITY PRIMARY KEY, + level INT NOT NULL, -- 1=BU, 2=Division, 3=Department, 4=Discipline/Team, 5=Part + code VARCHAR(50) NOT NULL, + name VARCHAR(200) NOT NULL, + parent_id BIGINT REFERENCES org_hierarchy(id), + is_active BIT DEFAULT 1, + UNIQUE (level, code) +); +``` + +### JPA Entity 보완 + +```java +@Entity @Table(name = "users") +public class User extends BaseEntity { + // ... 기존 필드 ... + + @Column(length = 20, unique = true) + private String employeeNumber; // 사번 = 로그인 ID (No.11) + + // 조직 4레벨 (SAP SF 기준) + private String businessUnit; // LV1 + private String division; // LV2 + private String department; // LV3 + private String section; // LV4 + private String discipline; + private String jobRole; + private String grade; + + private LocalDate startDate; + private LocalDate endDate; + + // 외주 전용 (No.7) + private String companyName; + private String internalContact; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private EmploymentType employmentType; // INTERNAL, SUBCONTRACTOR +} +``` + +--- + +## 2. Non-Project Overhead 유형 상세 (No.55~59) + +요구사항 No.56에서 명시된 Overhead Type. **SA가 수정 가능**해야 함. + +```java +// 시스템 초기 Overhead Types (No.56 — 영문 원본 기준, 향후 수정 가능) +// ★ 기존 계획서의 9종 → 원본 4종으로 수정 +public enum OverheadType { + TRAINING("Training", "Employee education and competency development activities"), + SYSTEM_PROCESS_DEV("System and Process Development", "System and process development and improvement activities"), + ORG_OPERATION("Organizational Operation", "Internal organizational administrative tasks"), + CORPORATE_INITIATIVE("Corporate Initiative or Strategy", "Executive-led corporate strategic initiatives"); + // ★ Leave, Sick Leave 등은 Non-Project TEAL Activity로 별도 관리 + // ★ SA가 추가 가능 (DB 테이블 기반) +} +``` + +**보완**: Overhead Type을 Enum 대신 **DB 테이블**로 관리 (SA 수정 가능 요건) + +```sql +CREATE TABLE overhead_types ( + id BIGINT IDENTITY PRIMARY KEY, + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + description NVARCHAR(500), + is_active BIT DEFAULT 1, + sort_order INT DEFAULT 0, + created_at DATETIME2 DEFAULT GETDATE() +); + +-- 초기 데이터 +INSERT INTO overhead_types (code, name, description) VALUES + ('TRAINING', 'Training', 'Employee education and competency development'), + ('SYS_PROC_DEV', 'System and Process Development', 'System and process development and improvement'), + ('ORG_OPERATION', 'Organizational Operation', 'Internal organizational administrative tasks'), + ('CORPORATE_INITIATIVE','Corporate Initiative or Strategy', 'Executive-led corporate strategic initiatives'); +-- ★ 추가 유형은 SA가 화면에서 등록 (No.56: "향후 수정 가능") +``` + +**Non-Project TEAL**: Admin이 Non-Project 전용 TEAL을 설정 → 사용자는 이 TEAL에서만 Activity 선택 (No.57) + +--- + +## 3. Canonical WBS 레벨 구조 상세 (No.33~34) + +요구사항 No.34에서 명시된 정확한 구조: + +``` +Level 1: Project +Level 2: Phase (Engineering, Procurement, Construction, Commissioning, etc.) +Level 3: Asset or Area ★ 기존 계획 "Discipline/Category" → 수정 +Level 4: Work or Discipline ★ 기존 계획 "Sub-Category/Work Package" → 수정 +Level 5: Deliverable, Package, or Material ★ Engineering and SCM only (No.34) +``` + +> **주의**: Level 5는 Engineering/SCM Phase에서만 사용. Construction 등은 Level 4까지만. + +### Project Type별 WBS 적용 (No.26) + +| Project Type | WBS Level | 비고 | +|-------------|-----------|------| +| **EPC Project** | Level 2~5 | P6 WBS 연동, Canonical 매핑 | +| **Other Project** (Tender/Pre-FEED/FEED/IDD) | Level 2~4 | P6 연동 불필요, Canonical만 (No.62) | +| **Non-Project** (Overhead) | 없음 | WBS/Project 입력 불가 (No.59) | + +```sql +-- Project Type Enum 보완 +-- project_type: 'EPC', 'TENDER', 'PRE_FEED', 'FEED', 'INTERNAL_DESIGN', 'NON_PROJECT' + +ALTER TABLE projects ADD COLUMN project_type VARCHAR(30) NOT NULL DEFAULT 'EPC'; + +-- Project Type별 WBS 레벨 제한 +CREATE TABLE project_type_config ( + id BIGINT IDENTITY PRIMARY KEY, + project_type VARCHAR(30) NOT NULL UNIQUE, + min_wbs_level INT DEFAULT 2, + max_wbs_level INT DEFAULT 5, + requires_p6 BIT DEFAULT 0, + requires_teal BIT DEFAULT 1, + description NVARCHAR(200) +); + +INSERT INTO project_type_config VALUES + (1, 'EPC', 2, 5, 1, 1, 'EPC Project - P6 연동, Level 5까지'), + (2, 'TENDER', 2, 4, 0, 1, 'Tender/Bidding'), + (3, 'PRE_FEED', 2, 4, 0, 1, 'Pre-FEED'), + (4, 'FEED', 2, 4, 0, 1, 'FEED'), + (5, 'INTERNAL_DESIGN', 2, 4, 0, 1, 'Internal Design Development'), + (6, 'NON_PROJECT', 0, 0, 0, 0, 'Non-Project/Overhead'); +``` + +--- + +## 4. 권한 매트릭스 (Sheet1 기반) + +요구사항 원본 Sheet1의 기능별 역할 접근 권한: + +| Category | Sub Category | SA(Admin) | PM | PL(LE)/GM/DL | Engineer/Staff | +|----------|-------------|-----------|-----|--------------|----------------| +| **Time Sheet** | Time Sheet Register | O | O | O | O | +| | Excel Import | O | O | O | - | +| **Project Mgmt** | Project Registration | O | O | - | - | +| | Project Information | O | - | - | - | +| **User Mgmt** | User Registration | O | O | O | - | +| | User Information | O | O | O | O | +| | ID/Password Mgmt | O | O | O | O | +| **WBS Mgmt** | WBS Upload | O | - | - | - | +| | WBS Version Mgmt | O | - | - | - | +| | TEAL Management | O | - | - | - | +| **Resource Assign** | Resource Availability | O | O | - | - | +| | Project Assign | O | - | - | - | +| | WBS Assign | O | O | - | - | +| | Task(TEAL) Assign | O | O | O | - | +| **My Project** | Assignment Status | O | O | O | O | +| | Favorite Setting | O | O | O | O | +| **Approval** | Approval Request | O | O | O | O | +| | Approval Management | O | O | O | - | +| | Approval History | O | O | O | O | +| **Report** | Project MH Analysis | O | O | O | - | +| | WBS MH Analysis | O | O | O | - | +| | Discipline MH Analysis | O | O | O | - | +| | Overhead MH Analysis | O | O | O | - | +| | WBS Change Review | O | O | O | - | + +### Spring Security 보완 + +```java +// 역할 코드 보완: PL(LE)/GM = DL (Discipline Lead) +// 원본에서 Admin = SA, PL(LE)/GM = DL 매핑 + +// 권한 테이블 (DB 기반, SA가 수정 가능) +CREATE TABLE role_permissions ( + id BIGINT IDENTITY PRIMARY KEY, + role_id BIGINT NOT NULL REFERENCES roles(id), + module VARCHAR(50) NOT NULL, -- TIMESHEET, PROJECT, USER, WBS, RESOURCE, APPROVAL, REPORT + sub_module VARCHAR(50) NOT NULL, -- REGISTER, IMPORT, UPLOAD, etc. + can_access BIT DEFAULT 0, + UNIQUE (role_id, module, sub_module) +); + +// @PreAuthorize에서 DB 기반 권한 체크 +@Component("perm") +public class PermissionChecker { + public boolean check(String module, String subModule) { + Long userId = SecurityUtils.getCurrentUserId(); + return rolePermissionRepository.hasAccess(userId, module, subModule); + } +} + +// Controller에서 사용 +@GetMapping("/api/reports/project-hours") +@PreAuthorize("@perm.check('REPORT', 'PROJECT_MH_ANALYSIS')") +public ProjectHoursReport getProjectHours(...) { ... } +``` + +--- + +## 5. Resource Assignment 모듈 (누락 보완) + +요구사항 No.42~50: 기존 계획서에 상세 누락된 모듈. + +### 핵심 기능 + +| No. | 기능 | 설명 | +|-----|------|------| +| 42 | 기본 입력 기준 수정 | SA가 Timesheet 기본 설정 변경 가능 | +| 43 | 1일 최소 8시간 | 규칙 엔진 | +| 44 | Activity 최소 1개 | 빈 Timesheet 제출 불가 | +| 45 | 주 최대 52시간 | Location(국가)별 다르게 지정 가능 | +| 46 | 초과근무/휴게시간 규칙 | 규칙 지정 가능 | +| 47 | Project별 Assign | Discipline마다 0~N개 프로젝트 배정 | +| 48 | Non-Project 별도 Assign 불필요 | 모든 사용자 사용 가능 | +| 49 | Location/Job_Role 설정 | 프로젝트별 사용 가능한 Location, Role 설정 (PH2) | +| 50 | WBS Discipline 자동 Assign | PCM/PTK가 표준 템플릿으로 투입인력 일괄 등록 | + +```sql +-- 근무 규칙 설정 (SA 관리, No.42~46) +CREATE TABLE work_rules ( + id BIGINT IDENTITY PRIMARY KEY, + rule_name VARCHAR(100) NOT NULL, + location_code VARCHAR(50), -- 국가/지역별 규칙 (NULL=전체 기본값) + min_daily_hours DECIMAL(4,2) DEFAULT 8.00, + max_daily_hours DECIMAL(4,2) DEFAULT 10.00, + max_weekly_hours DECIMAL(5,2) DEFAULT 52.00, + overtime_threshold DECIMAL(4,2) DEFAULT 8.00, -- 초과근무 기준 + break_time_minutes INT DEFAULT 60, -- 휴게시간 + is_default BIT DEFAULT 0, + is_active BIT DEFAULT 1, + created_at DATETIME2 DEFAULT GETDATE() +); + +INSERT INTO work_rules (rule_name, location_code, min_daily_hours, max_weekly_hours, is_default) VALUES + ('Korea Standard', 'KR', 8.00, 52.00, 1), + ('Offshore Standard', 'OFFSHORE', 8.00, 60.00, 0); + +-- 프로젝트 인력 배정 (No.47~48) +CREATE TABLE project_assignments ( + id BIGINT IDENTITY PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES projects(id), + user_id BIGINT NOT NULL REFERENCES users(id), + discipline VARCHAR(100), + assigned_by BIGINT REFERENCES users(id), -- PM/PCM/PTK + start_date DATE, + end_date DATE, + is_active BIT DEFAULT 1, + created_at DATETIME2 DEFAULT GETDATE(), + UNIQUE (project_id, user_id) +); + +-- WBS별 Discipline 배정 (No.50) +CREATE TABLE wbs_discipline_assignments ( + id BIGINT IDENTITY PRIMARY KEY, + canonical_wbs_id BIGINT NOT NULL REFERENCES canonical_wbs(id), + discipline VARCHAR(100) NOT NULL, + assigned_by BIGINT REFERENCES users(id), + created_at DATETIME2 DEFAULT GETDATE(), + UNIQUE (canonical_wbs_id, discipline) +); +``` + +### REST API 추가 + +``` +# Resource Assignment (02-database-schema, 08-api-spec 보완) +GET /api/work-rules 근무 규칙 목록 (SA) +PUT /api/work-rules/{id} 근무 규칙 수정 (SA) +GET /api/projects/{id}/assignments 프로젝트 인력 배정 목록 +POST /api/projects/{id}/assignments 인력 배정 (PM/PCM/PTK) +POST /api/projects/{id}/assignments/upload 인력 배정 일괄 업로드 (No.50) +DELETE /api/projects/{id}/assignments/{userId} 인력 배정 해제 +GET /api/projects/{id}/wbs-disciplines WBS-Discipline 배정 현황 +POST /api/projects/{id}/wbs-disciplines WBS-Discipline 배정 +``` + +--- + +## 6. 비기능 요구사항 — 한화오션 표준 보안 SW 상세 (NF.3~10) + +### 서버 보안 (NF.3) + +| SW | 용도 | Azure 대응 | +|----|------|-----------| +| **HIWARE** | 서버 접근 제어 | Azure Bastion + HIWARE Agent | +| **V3** (AhnLab) | 서버 백신 | VM에 V3 Agent 설치 | +| **Secuver TOS** | 서버 보안 (파일 무결성 등) | VM에 Agent 설치 | + +### DB 보안 (NF.4) + +| SW | 용도 | Azure 대응 | +|----|------|-----------| +| **Cubeone** | DB 암호화 | Azure SQL TDE + Cubeone (컬럼 레벨) | +| **Dbsafer** | DB 접근 제어 | Dbsafer Proxy 구성 | + +### 클라우드 보안 (NF.5) + +| SW | 용도 | +|----|------| +| **Azure Defender** | 위협 탐지 | +| **Azure Log Analytics** | 보안 이벤트 분석 | + +### 모니터링 (NF.8~10) + +| SW | 용도 | 대상 | +|----|------|------| +| **onTune** | SMS (서버 모니터링) | VM CPU/Memory/Disk | +| **MCCS** | HA (Windows 클러스터) | WAS 이중화 (※ Linux → N/A) | +| **Maxguage** | DB 모니터링 | Azure SQL 성능 분석 | +| **Jennifer** | WAS 모니터링 | Spring Boot Tomcat APM | + +### application-prod.yml 보완 + +```yaml +# Jennifer APM 연동 (NF.10) +# JVM Args: -javaagent:/opt/jennifer/agent.java/jennifer.jar + +# Actuator + Prometheus (Maxguage 보완/대안) +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + health: + db: + enabled: true + redis: + enabled: true +``` + +### 보안성 심의 체크리스트 (NF.6~7) + +``` +□ 웹 애플리케이션 취약점 점검 (OWASP Top 10) +□ 모바일 앱 보안 점검 (해당 시 PH2) +□ 서버/네트워크 인프라 보안 점검 +□ 소스코드 보안 점검 (SonarQube / Fortify) +□ 모의해킹 (Penetration Testing) +□ 개인정보 보호 관리 (개인정보보호법 준수) +□ 한화그룹 보안 표준 준수 확인 +``` + +--- + +## 7. Timesheet 상세 보완 + +### Remark 필드 (No.67) + +``` +필수 입력 아님. 필요 시 아래 내용 입력: +- Change Order Number +- Deliverable Number +- 기타 참고 사항 +``` + +**보완**: Change Order 관련 MH 관리를 위해 별도 Activity 분리 입력 방안 검토 필요 + +### Timesheet 행 구조 (No.69) + +``` +1행 = 1일자 × 1 Activity +------------------------------------------------------ +번호 | 날짜 | WBS L1~5 | Activity | Manhour | Remark +------------------------------------------------------ +1 | 4/7 | E.01.03 | Detail | 4.0 | +2 | 4/7 | E.01.04 | Review | 4.0 | ← 동일 날짜, 다른 Activity +------------------------------------------------------ + 일 합계: 8.0h + 전체 합계: Summary Row 표시 +``` + +### Excel Import 정확한 컬럼 (No.68) + +``` +표준 템플릿 컬럼: +| Date | Project Code | WBS L2 | WBS L3 | WBS L4 | WBS L5 | Activity | Hours | Remark | +``` + +--- + +## 8. 확인필요사항 (7건) 추적 + +원본 Excel 하단의 "확인필요사항" — 프로젝트 착수 전 고객 확정 필요. + +| # | 영역 | 내용 | 현재 상태 | 계획 반영 | +|---|------|------|----------|----------| +| 1 | User Registration | 파트너사 및 외주 인력 관리 정책 — 파트너 마스터 연계 여부 | 별도 파트너 마스터 연계 미고려. 전사 데이터 표준화 후 검토 | PH1-1: 파일 업로드만 | +| 2 | User Home | 사용자 관리규정 및 권한관리 정책 — User Type별 업무범위 | No.18 참조 (기능 기반 역할 정의 확정) | plans/wbx-spring/02-auth-jwt-sso.md 반영 | +| 3 | WBS Upload | WBS별 Resource Plan 방식 — TT에서 투입시수 규모 세팅 여부 | TT에서 WBS별 투입시수 규모 세팅 | 05-wbs-teal.md 반영 | +| 4 | Resource Assignment | PM 인력 배치 시 해당 인력 스케줄 확인 및 참여 확인 절차 | PM/PCM/PTK가 WBS별 Discipline 선정 → 각 Discipline별 투입인력 선정 후 리스트 제출 | Resource Assignment 모듈 | +| 5 | Time Sheet (EPC) | C단계(Construction) 시수 입력 및 관리 방안 | **C단계 시수관리 대상 제외**. EPU 담당자의 C단계 관리업무만 TT에 등록 | 04-timesheet.md 주석 | +| 6 | Approval | 각 업무요건별 승인 절차 — 대상 Action 도출, 프로세스 설계 | User → DL → PM 3단계 확정 | 06-approval.md 반영 | +| 7 | Resource Assignment | Project Assignment 프로세스 상세 | SA가 Project 생성 → PM/PCM이 인력 배정 → 확정 | Resource Assignment 모듈 | + +--- + +## 9. 외부 시스템 인터페이스 상세 (NF.13~15) + +### 인터페이스 전체 맵 + +``` +┌──────────────────┐ ┌─────────────┐ ┌────────────────┐ +│ SAP │ │ │ │ │ +│ SuccessFactors │────▶│ SAP BTP │────▶│ WTMgr │ +│ (HR Master) │ │ (NF.13) │ │ Spring Boot │ +└──────────────────┘ └─────────────┘ │ │ + │ ┌──────────┐│ +┌──────────────────┐ File Upload │ │ REST API ││ +│ Primavera P6 │─────────────────────────▶│ │ ││ +│ (WBS/Schedule) │ (NF.14, 물리적 I/F 없음) │ └──────────┘│ +└──────────────────┘ │ │ + │ ┌──────────┐│ +┌──────────────────┐ Extractor │ │ Export ││ +│ Cognite │◀────────────────────────│ │ API ││ +│ (Data Platform) │ (NF.15) │ └──────────┘│ +└──────────────────┘ └────────────────┘ +``` + +### 9-1. SAP SuccessFactors 연동 (NF.13) — SAP BTP 필수 + +> **NF.13 원문**: "In the case of direct integration with SuccessFactors, SAP BTP must be applied." + +#### 연동 아키텍처 + +``` +SAP SuccessFactors + │ + │ OData API (Employee Central) + ▼ +┌─────────────────────────┐ +│ SAP BTP │ +│ (Business Technology │ +│ Platform) │ +│ │ +│ ┌───────────────────┐ │ +│ │ Integration Suite │ │ +│ │ (CPI/CI) │ │ +│ │ │ │ +│ │ ● OData → REST │ │ +│ │ ● 필드 매핑 │ │ +│ │ ● 스케줄링 (배치) │ │ +│ └───────┬───────────┘ │ +└──────────┼──────────────┘ + │ REST API (JSON) + ▼ +┌─────────────────────────┐ +│ WTMgr │ +│ POST /api/integration │ +│ /hr/sync │ +└─────────────────────────┘ +``` + +#### 단계별 구현 + +| Phase | 방식 | 설명 | +|-------|------|------| +| **PH1-1** | **파일 업로드** (No.1) | SA가 SF에서 Export한 Excel/CSV를 수동 업로드. BTP 불필요 | +| **PH1-2** | **배치 자동화** (No.3) | SAP BTP Integration Suite → 정기 Batch (일 1회) → WTMgr REST API | +| **PH2** | **실시간 이벤트** | SF Employee Events → BTP → WTMgr Webhook (입/퇴사 즉시 반영) | + +#### SAP BTP Integration Suite 설정 + +```yaml +# BTP CPI iFlow 설정 예시 +Source: + System: SAP SuccessFactors + API: /odata/v2/PerPersonal, /odata/v2/EmpJob, /odata/v2/FODepartment + Auth: OAuth2 (SAP Trust) + +Mapping: + SF.personIdExternal → WTMgr.employeeNumber + SF.firstName + lastName → WTMgr.fullName + SF.email → WTMgr.email + SF.businessUnit → WTMgr.businessUnit (LV1) + SF.division → WTMgr.division (LV2) + SF.department → WTMgr.department (LV3) + SF.customString1 → WTMgr.disciplineTeam (LV4) # Discipline/Team + SF.customString2 → WTMgr.part (LV5) # Part + SF.attendanceType → WTMgr.attendanceType + SF.jobCode → WTMgr.individualJobCode + +Target: + System: WTMgr + Endpoint: POST /api/integration/hr/sync + Auth: Service Account JWT (M2M) + Schedule: Daily 02:00 KST +``` + +#### Spring Boot 수신 API + +```java +@RestController +@RequestMapping("/api/integration/hr") +@PreAuthorize("hasRole('SYSTEM') or hasRole('SA')") // M2M 또는 SA만 +public class HrIntegrationController { + + private final HrSyncService hrSyncService; + + /** + * SAP BTP → WTMgr HR 데이터 동기화 + * PH1-1: SA 수동 파일 업로드 대안 + * PH1-2: BTP CPI 자동 호출 + */ + @PostMapping("/sync") + public HrSyncResult syncEmployees(@Valid @RequestBody HrSyncRequest request) { + return hrSyncService.syncAll(request.getEmployees()); + } + + /** + * SA 수동 업로드 (PH1-1: BTP 없이 Excel 업로드) + */ + @PostMapping("/upload") + public HrSyncResult uploadExcel(@RequestParam("file") MultipartFile file) { + return hrSyncService.uploadFromExcel(file); + } +} + +// 동기화 요청 DTO +public record HrSyncRequest( + List employees, + String syncSource, // "SAP_BTP" | "MANUAL_UPLOAD" + LocalDateTime syncTime +) {} + +public record HrEmployeeDto( + String employeeNumber, + String fullName, + String email, + String businessUnit, // LV1 + String division, // LV2 + String department, // LV3 + String disciplineTeam, // LV4 + String part, // LV5 + String attendanceType, + String individualJobCode, + String jobRole, + LocalDate startDate, + LocalDate endDate, + boolean isActive +) {} +``` + +#### SAP BTP 사전 확보 사항 + +| 항목 | 제공 주체 | 마감 | +|------|----------|------| +| BTP 테넌트 접근 권한 | 한화시스템/SAP | W2 | +| SF OData API 엔드포인트 및 인증 정보 | 한화시스템 | W2 | +| 필드 매핑 확정 (SF → WTMgr) | 한화오션 + 아큐라 | W3 | +| BTP CPI iFlow 개발/배포 권한 | 한화시스템 | PH1-2 | + +### 9-2. P6 연동 (NF.14) — 파일 기반만 + +> **NF.14 원문**: "Since there is no physical interface with P6, data integration must be implemented using exported files." + +| 항목 | 내용 | +|------|------| +| 연동 방식 | P6 Export → Excel/CSV → WTMgr Upload | +| 물리적 I/F | **없음** (API 연동 불가) | +| 업로드 주체 | **PM** (No.27), PCM이 승인 (No.35) | +| 필수 필드 | WBS Level 1~5, P6 Activity ID, P6 Activity Name, Version Number, Effective Date, Approval Reference (No.31) | +| 빈도 | WBS 변경 시 수시 + 월 1회 Snapshot 비교 (No.32) | + +### 9-3. Cognite 연동 (NF.15) — PH2 + +> **NF.15 원문**: "An extractor server is required to support interface integration with Cognite." + +``` +Export 대상 Dimension/Fact (No.80): +- Employee Dimension (사번, 이름, 부서, Discipline) +- Project Dimension (프로젝트코드, 이름, Type) +- Canonical WBS Dimension (WBS 코드, Level, 이름) +- Time Fact Table (날짜, 시수, Activity, 승인상태) +- Mapping Version Metadata (★ 기존 계획 누락 → 추가) +``` + +```java +// Cognite Export REST API (PH2) +@GetMapping("/api/integration/cognite/export") +@PreAuthorize("hasRole('SA')") +public CogniteExportData exportForCognite( + @RequestParam @DateTimeFormat(iso = DATE) LocalDate from, + @RequestParam @DateTimeFormat(iso = DATE) LocalDate to) { + + return CogniteExportData.builder() + .employees(employeeDimensionService.export()) + .projects(projectDimensionService.export()) + .canonicalWbs(wbsDimensionService.export()) + .timeFacts(timesheetFactService.export(from, to)) + .mappingVersions(wbsVersionService.exportMetadata()) // ★ 추가 + .exportedAt(LocalDateTime.now()) + .build(); +} +``` + +--- + +## 10. 최종 점검 — 기존 계획서 오류 정정 + +### 정정 사항 요약 + +| # | 기존 계획서 | 수정 내용 | 근거 | +|---|-----------|----------|------| +| 1 | 조직 4레벨 (BU/Division/Dept/Section) | **5레벨**: BU/Division/Dept/Discipline·Team/**Part** + Attendance Type, Individual Job Code | No.2 (Eng) | +| 2 | Canonical WBS L3 = "Discipline/Category" | **L3 = Asset or Area** | No.34 (Eng) | +| 3 | Canonical WBS L4 = "Sub-Category/Work Package" | **L4 = Work or Discipline** | No.34 (Eng) | +| 4 | Canonical WBS L5 = "Deliverable/Activity" | **L5 = Deliverable, Package, or Material (Engineering & SCM only)** | No.34 (Eng) | +| 5 | Overhead 9종 (Leave, Sick Leave 등 포함) | **4종**: Training, Sys&Proc Dev, Org Operation, Corporate Initiative | No.56 (Eng) | +| 6 | WBS 업로드 주체 = PCM/PTK | **PM**이 업로드, **PCM**이 승인 | No.27, No.35 | +| 7 | No.49 Location/Role = PH2 | Phase 미지정 (메인 요구사항), 설계 시 고려 필요 | No.49 | +| 8 | EPC 추가 필드 미반영 | Location, Role/Position, **Earned Value(진행률)** 검토 | No.65 | +| 9 | Cognite Export 4개 Dimension | **5개**: + Mapping Version Metadata | No.80 (Eng) | +| 10 | 미완료 Timesheet 알림 미반영 | **필수**: 담당자에게 미완료 Timesheet alert 전송 | No.70 | +| 11 | Project 생성 = PM 직접 | PM/PCM 정보 입력 → **SA 승인** 후 등록 | No.25 | +| 12 | Non-Project TEAL = 기본 내장 | **Admin이 설정**한 Non-Project TEAL에서만 Activity 선택 | No.57 | +| 13 | SAP BTP 연동 = 간략 설명만 | **상세 아키텍처 추가** (BTP CPI iFlow, OData → REST 매핑) | NF.13 | + +### 04-timesheet-module.md 보완 필요 + +```java +// No.65: EPC Timesheet 추가 필드 (PH2 후보, 설계 시 확장 고려) +@Entity @Table(name = "timesheet_entries") +public class TimesheetEntry extends BaseEntity { + // ... 기존 필드 ... + + // ★ No.65 추가 필드 (nullable, PH2에서 활성화) + private String location; // Onshore/Offshore/국가코드 + private String rolePosition; // Job Role (동일 사용자 복수 Role 가능) + private BigDecimal earnedValue; // 진행률 (%) — Earned Value +} +// ★ 주의: "동일 사용자가 복수 Role → Unit Rate 변경", "Location 변경 → Unit Rate 변경" (No.65 Remark) +``` + +### 06-approval-workflow.md 보완 필요 + +```java +// No.70: 미완료 Timesheet 알림 (★ 기존 계획 누락) +@Component +public class TimesheetReminderScheduler { + + /** + * 매일 오후 5시: 당일 미작성 Timesheet 알림 (No.70) + * "The system must send alerts to responsible users + * for incomplete timesheets to ensure timely submission" + */ + @Scheduled(cron = "0 0 17 * * MON-FRI") + public void sendDailyReminder() { + List usersWithoutEntry = timesheetService + .findUsersWithoutEntryForDate(LocalDate.now()); + + for (User user : usersWithoutEntry) { + notificationService.sendTimesheetReminder(user, LocalDate.now()); + } + } + + /** + * 매주 금요일 오전 10시: 주간 미제출 Timesheet 알림 + */ + @Scheduled(cron = "0 0 10 * * FRI") + public void sendWeeklySubmitReminder() { + LocalDate weekStart = LocalDate.now().with(DayOfWeek.MONDAY); + List unsubmitted = timesheetService + .findUsersWithUnsubmittedWeek(weekStart); + + for (User user : unsubmitted) { + notificationService.sendWeeklySubmitReminder(user, weekStart); + } + } +} +``` + +### 05-wbs-teal-module.md 보완 필요 + +```java +// No.27: WBS 업로드 주체 수정 +// 기존: PCM/PTK 업로드 +// 수정: PM이 업로드 → PCM이 승인 (No.35) + +@PostMapping("/api/projects/{projectId}/wbs/upload") +@PreAuthorize("hasAnyRole('SA', 'PM')") // ★ PM 권한 (기존 PCM → PM) +public WbsVersionDto uploadP6Wbs(...) { ... } + +@PostMapping("/api/projects/{projectId}/wbs/versions/{verId}/approve") +@PreAuthorize("hasAnyRole('SA', 'PCM')") // ★ PCM 승인 +public WbsVersionDto approveWbs(...) { ... } +``` + +--- + +## 11. API 스펙 보완 (07-api-spec.md 추가분) + +### 추가 필요 API (기존 57개 → 최종 78개) + +| Method | Path | 설명 | Phase | +|--------|------|------|-------| +| GET | `/api/overhead-types` | Overhead Type 목록 | PH1-1 | +| POST | `/api/overhead-types` | Overhead Type 추가 (SA) | PH1-1 | +| PUT | `/api/overhead-types/{id}` | Overhead Type 수정 (SA) | PH1-1 | +| GET | `/api/work-rules` | 근무 규칙 목록 | PH1-1 | +| PUT | `/api/work-rules/{id}` | 근무 규칙 수정 (SA) | PH1-1 | +| GET | `/api/projects/{id}/assignments` | 프로젝트 인력 배정 목록 | PH1-1 | +| POST | `/api/projects/{id}/assignments` | 인력 배정 | PH1-1 | +| POST | `/api/projects/{id}/assignments/upload` | 인력 일괄 업로드 | PH1-1 | +| DELETE | `/api/projects/{id}/assignments/{userId}` | 인력 배정 해제 | PH1-1 | +| GET | `/api/projects/{id}/wbs-disciplines` | WBS-Discipline 현황 | PH1-1 | +| POST | `/api/projects/{id}/wbs-disciplines` | WBS-Discipline 배정 | PH1-1 | +| POST | `/api/projects/{id}/wbs/versions/{id}/approve` | WBS 버전 승인 (PCM) ★ 추가 | PH1-1 | +| GET | `/api/role-permissions` | 역할별 권한 매트릭스 (SA) | PH1-1 | +| POST | `/api/integration/hr/sync` | SAP BTP HR 동기화 수신 ★ 추가 | PH1-2 | +| POST | `/api/integration/hr/upload` | SA 수동 HR 파일 업로드 ★ 추가 | PH1-1 | +| GET | `/api/notifications/pending` | 미완료 Timesheet 알림 목록 ★ 추가 | PH1-1 | +| GET | `/api/users/me/favorites` | 내 Favorite 설정 | PH2 | +| PUT | `/api/users/me/favorites` | Favorite 저장 | PH2 | +| GET | `/api/integration/cognite/export` | Cognite Export ★ 추가 | PH2 | +| GET | `/api/reports/wbs-misalign` | WBS Mis-Align 검토 (No.81) ★ 추가 | PH2 | +| GET | `/api/reports/discipline-productivity` | Discipline 생산성 (No.84) ★ 추가 | PH2 | + +### 최종 API 수 + +| 모듈 | PH1-1차 | PH1-2차 | PH2 | 합계 | +|------|---------|---------|-----|------| +| Auth | 8 | 0 | 0 | 8 | +| Users | 7 | 1 | 2 | 10 | +| Projects | 7 | 0 | 0 | 7 | +| WBS | 6 | 1 | 0 | 7 | +| TEAL | 4 | 0 | 0 | 4 | +| Timesheets | 8 | 0 | 0 | 8 | +| Approvals | 7 | 1 | 0 | 8 | +| Reports | 4 | 2 | 3 | 9 | +| Home/Notification | 3 | 0 | 0 | 3 | +| Resource Assign | 8 | 0 | 0 | 8 | +| Config (규칙/Type) | 4 | 0 | 0 | 4 | +| **Integration (SAP/Cognite)** | **1** | **1** | **1** | **3** | +| **합계** | **67** | **6** | **6** | **79** | diff --git a/HanwhaOCN/wtmgr/12-project-setup-plan.md b/HanwhaOCN/wtmgr/12-project-setup-plan.md new file mode 100644 index 0000000..b915363 --- /dev/null +++ b/HanwhaOCN/wtmgr/12-project-setup-plan.md @@ -0,0 +1,659 @@ +# 12. WTM 프로젝트 구성 계획 + +> 작성일: 2026-03-25 +> 목적: wbx-spring-core를 라이브러리로 전환하고, wtm-api 모듈을 생성하기 위한 구체적 작업 계획 + +--- + +## 1. 현황 분석 + +### wbx-spring-core 현재 구조 + +| 항목 | 현재 상태 | 문제점 | +|------|----------|--------| +| Gradle 플러그인 | `org.springframework.boot` (fat JAR) | 라이브러리로 사용 불가 | +| 진입점 | `WbxSpringCoreApplication.java` (`@SpringBootApplication`) | 라이브러리에 main() 불필요 | +| application.yml | 하드코딩된 DB/Redis/서버 설정 | 소비 모듈이 자체 설정 불가 | +| DB 드라이버 | MySQL/PG/Oracle/MSSQL 4개 모두 포함 | 소비 모듈이 필요한 것만 선택해야 함 | +| Admin UI | Thymeleaf 세션 기반 (12개 템플릿) | 모든 소비 모듈에 전이 의존성 | +| Enable 어노테이션 | main 클래스에 `@EnableJpaAuditing` 등 집중 | 라이브러리 전환 시 분리 필요 | + +### wbx-spring-core 제공 기능 (10개 모듈) + +| 패키지 | 기능 | 주요 클래스 | WTM 사용 여부 | +|--------|------|------------|:---:| +| `auth/` | JWT, SSO, MFA, 비밀번호 정책, 리프레시 토큰 | `JwtProvider`, `JwtFilter`, `AuthController` | O | +| `rbac/` | 역할-모듈-액션 권한, dept_scope | `PermissionEvaluator` (bean `"wbx"`) | O | +| `approval/` | 통합 결재 엔진 (Handler Registry) | `ApprovalHandler` interface, `UnifiedApprovalController` | O | +| `notification/` | SSE 실시간 알림 | `SseNotificationService` | O | +| `compat/` | WBX 호환 (detail 에러, skip/limit) | `WbxErrorHandler`, `WbxPaginationConfig` | O | +| `file/` | 파일 스토리지 (Local/Azure/AWS/GCP) | `FileStorageService` interface | O | +| `datasource/` | 멀티 데이터소스 라우팅 | `MultiDataSourceConfig`, `@DataSource` | O | +| `config/` | 시큐리티, CORS, OpenAPI, 중앙 설정 | `SecurityAutoConfig`, `WbxSpringProperties` | O | +| `common/` | 베이스 엔티티, 유틸, 예외 | `BaseEntity`, `SecurityUtils`, `BusinessException` | O | +| `audit/` | 감사 로그 | `AuditLogService` | O | +| `admin/` | 관리 콘솔 UI (Thymeleaf) | `AdminController`, 12개 HTML | 조건부 | + +--- + +## 2. 목표 구조: 멀티프로젝트 플랫폼 + +### 설계 원칙 + +- **wbx-spring-core**: 모든 프로젝트가 공유하는 프레임워크 (1개) +- **{project}-api**: 고객/프로젝트별 백엔드 애플리케이션 (N개) +- **{project}-frontend**: 고객/프로젝트별 프론트엔드 (N개, Gradle 외부) +- **새 프로젝트 추가 = 모듈 2개 추가** (settings.gradle에 include만 추가) + +### 전체 구조 + +``` +WBX-Spring/ +├── settings.gradle # rootProject.name = 'wbx-spring' +│ # include 'wbx-spring-core' +│ # include 'wtm-api' ← 한화오션 WTM +│ # include 'xxx-api' ← 향후 프로젝트 B +│ # include 'yyy-api' ← 향후 프로젝트 C +├── build.gradle # 공통: Java 21, Lombok, Spring BOM +│ +├── wbx-spring-core/ # 🔧 공유 프레임워크 라이브러리 +│ ├── build.gradle # java-library (Boot 플러그인 없음) +│ ├── src/main/java/kr/co/accura/wbx/spring/ +│ │ ├── auth/ # JWT, SSO, MFA, 비밀번호 정책 +│ │ ├── rbac/ # RBAC 권한 (PermissionEvaluator) +│ │ ├── approval/ # 통합 결재 엔진 (Handler Registry) +│ │ ├── notification/ # SSE 실시간 알림 +│ │ ├── compat/ # WBX 호환 (detail 에러, skip/limit) +│ │ ├── file/ # 파일 스토리지 (Local/Azure/AWS/GCP) +│ │ ├── datasource/ # 멀티 데이터소스 라우팅 +│ │ ├── config/ # 시큐리티, CORS, OpenAPI, 중앙 설정 +│ │ ├── common/ # BaseEntity, SecurityUtils, 예외 +│ │ ├── audit/ # 감사 로그 +│ │ └── admin/ # 관리 콘솔 (조건부: wbx.spring.admin-ui.enabled) +│ └── src/main/resources/ +│ ├── META-INF/spring/ +│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports +│ └── templates/admin/ # Thymeleaf (admin-ui.enabled=true 시만) +│ +├── wtm-api/ # 🏗️ 한화오션 WTM 백엔드 +│ ├── build.gradle # spring-boot + project(':wbx-spring-core') +│ ├── src/main/java/kr/co/accura/wtm/ +│ │ ├── WtmApplication.java # @SpringBootApplication +│ │ ├── domain/ +│ │ │ ├── user/ # HR 확장 필드 (discipline, location 등) +│ │ │ ├── project/ # 프로젝트, 인력 배정 +│ │ │ ├── wbs/ # WBS, Canonical WBS, P6 파서 +│ │ │ ├── teal/ # TEAL 버전, Activity +│ │ │ ├── timesheet/ # 시수 3종, 규칙 엔진 +│ │ │ ├── approval/handler/ # TimesheetApprovalHandler +│ │ │ ├── report/ # QueryDSL 동적 리포트 +│ │ │ ├── config/ # OverheadType, WorkRule +│ │ │ └── audit/ # SaAccessLog +│ │ ├── api/ # REST Controller 13개 (79 API) +│ │ ├── integration/ # SAP BTP, P6, Cognite +│ │ └── config/ # WTM 전용 설정 +│ └── src/main/resources/ +│ ├── application.yml # WTM 전용 설정 +│ ├── application-mysql.yml # 개발 (MySQL) +│ ├── application-mssql.yml # 운영 (Azure SQL) +│ └── db/migration/ +│ ├── common/ # ANSI SQL (seed data) +│ ├── mysql/ # 개발용 DDL +│ └── mssql/ # 운영용 DDL +│ +├── wtm-frontend/ # 🎨 한화오션 WTM 프론트엔드 +│ ├── package.json # Vue 3 + PrimeVue 4 + Vite +│ └── src/ +│ ├── app/ # 셸 (router, plugins) +│ ├── core/ # 공유 인프라 +│ ├── modules/ # 도메인 모듈 10개 +│ └── assets/ # 스타일, 이미지 +│ +├── {향후}-api/ # 🆕 프로젝트 B 백엔드 (동일 패턴) +│ ├── build.gradle # project(':wbx-spring-core') +│ └── src/main/java/kr/co/accura/{project}/ +│ ├── {Project}Application.java +│ ├── domain/ # 프로젝트 B 전용 도메인 +│ ├── api/ # 프로젝트 B 전용 API +│ └── config/ +│ +└── {향후}-frontend/ # 🆕 프로젝트 B 프론트엔드 (동일 패턴) + └── src/ + ├── app/ + ├── core/ # 복사 후 커스터마이징 또는 공유 npm 패키지 + └── modules/ +``` + +### 프로젝트 간 관계도 + +``` + ┌─────────────────────┐ + │ wbx-spring-core │ ← 공유 프레임워크 + │ (java-library) │ + │ 인증/권한/결재/알림 │ + └──────┬──────┬───────┘ + │ │ + ┌────────────┘ └────────────┐ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ wtm-api │ │ {향후}-api │ + │ (spring-boot) │ │ (spring-boot) │ + │ 한화오션 WTM │ │ 고객 B 프로젝트 │ + └────────┬────────┘ └────────┬────────┘ + │ │ + ┌────────┴────────┐ ┌────────┴────────┐ + │ wtm-frontend │ │ {향후}-frontend │ + │ (Vue+PrimeVue) │ │ (Vue+PrimeVue) │ + └─────────────────┘ └─────────────────┘ +``` + +### 새 프로젝트 추가 절차 (3단계) + +**Step 1. settings.gradle에 모듈 추가** +```groovy +include '{project}-api' +``` + +**Step 2. {project}-api/build.gradle 생성** +```groovy +plugins { + id 'org.springframework.boot' version '3.5.0' +} +dependencies { + implementation project(':wbx-spring-core') + // 프로젝트 전용 의존성 추가 +} +``` + +**Step 3. Application 클래스 + application.yml 생성** +```java +@SpringBootApplication(scanBasePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.{project}" +}) +@EntityScan(basePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.{project}" +}) +@EnableJpaRepositories(basePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.{project}" +}) +public class {Project}Application { + public static void main(String[] args) { + SpringApplication.run({Project}Application.class, args); + } +} +``` + +> **각 프로젝트는 독립 실행 가능** — 자체 DB, 자체 포트, 자체 JWT 설정. +> wbx-spring-core의 기능(인증/권한/결재/알림)은 `wbx.spring.*` 속성으로 ON/OFF. + +### 프로젝트별 커스터마이징 포인트 + +| 항목 | 설정 위치 | 예시 | +|------|----------|------| +| API prefix | `wbx.spring.api-prefix` | `/api/wtm`, `/api/ems`, `/api/qms` | +| JWT secret | `wbx.spring.jwt.secret` | 프로젝트별 독립 키 | +| DB | `spring.datasource.*` | 프로젝트별 독립 DB | +| 결재 핸들러 | `@Component implements ApprovalHandler` | Bean 등록만으로 자동 연동 | +| SSO (Azure) | `spring.security.oauth2.client.*` | 고객별 Entra ID 테넌트 | +| 파일 스토리지 | `wbx.spring.file.storage-type` | `local`, `azure`, `aws`, `gcp` | +| Admin UI | `wbx.spring.admin-ui.enabled` | `true` / `false` | +| CORS | `wbx.spring.cors.allowed-origins` | 프로젝트별 프론트 URL | +``` + +--- + +## 3. 전환 작업 상세 + +### Phase 0: wbx-spring-core 라이브러리 전환 (선행 작업) + +#### 3.1 루트 프로젝트 생성 + +| 작업 | 파일 | 설명 | +|------|------|------| +| `settings.gradle` 이동 | `WBX-Spring/settings.gradle` | 프로젝트 모듈 등록 | +| 루트 `build.gradle` 생성 | `WBX-Spring/build.gradle` | 공통 설정 (Java 21, repositories, Lombok) | + +```groovy +// WBX-Spring/settings.gradle +rootProject.name = 'wbx-spring' + +// 공유 프레임워크 +include 'wbx-spring-core' + +// === 고객 프로젝트 (추가 시 여기에 include) === +include 'wtm-api' // 한화오션 WTM (시수관리) +// include 'xxx-api' // 향후 프로젝트 B +// include 'yyy-api' // 향후 프로젝트 C +``` + +```groovy +// WBX-Spring/build.gradle (루트) +plugins { + id 'java' + id 'io.spring.dependency-management' version '1.1.7' +} + +subprojects { + group = 'kr.co.accura' + version = '1.0.0-SNAPSHOT' + + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + + repositories { + mavenCentral() + } + + dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:3.5.0" + } + } + + dependencies { + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + + tasks.named('test') { + useJUnitPlatform() + } +} +``` + +#### 3.2 wbx-spring-core build.gradle 변경 + +**핵심 변경: `org.springframework.boot` 플러그인 제거 → `java-library` 적용** + +```groovy +// wbx-spring-core/build.gradle +plugins { + id 'java-library' +} + +dependencies { + // Spring Boot Starters (api로 노출 — 소비 모듈이 사용) + api 'org.springframework.boot:spring-boot-starter-web' + api 'org.springframework.boot:spring-boot-starter-data-jpa' + api 'org.springframework.boot:spring-boot-starter-security' + api 'org.springframework.boot:spring-boot-starter-validation' + api 'org.springframework.boot:spring-boot-starter-data-redis' + api 'org.springframework.boot:spring-boot-starter-cache' + api 'org.springframework.boot:spring-boot-starter-actuator' + api 'org.springframework.boot:spring-boot-starter-oauth2-client' + api 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + + // JWT + api 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + // OpenAPI + api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' + + // Admin Console (조건부) + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + + // Flyway (소비 모듈이 DBMS별 추가) + api 'org.flywaydb:flyway-core' + + // DB 드라이버 — compileOnly (소비 모듈이 runtimeOnly로 선택) + compileOnly 'com.mysql:mysql-connector-j' + compileOnly 'org.postgresql:postgresql' + compileOnly 'com.oracle.database.jdbc:ojdbc11:23.6.0.24.10' + compileOnly 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11' + + // Micrometer + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + // Test + testRuntimeOnly 'com.h2database:h2' +} +``` + +#### 3.3 Auto-Configuration 전환 + +| 작업 | 설명 | +|------|------| +| `WbxSpringCoreApplication.java` 삭제 | main() 제거, 라이브러리에 불필요 | +| `WbxAutoConfiguration.java` 생성 | `@Configuration` + `@EnableJpaAuditing` + `@EnableAsync` + `@EnableScheduling` + `@EnableCaching` | +| `AutoConfiguration.imports` 생성 | Spring Boot 3.x 자동 설정 등록 | +| `application.yml` 정리 | 하드코딩된 설정 제거, 기본값만 `WbxSpringProperties`에서 관리 | +| `HealthController` 이동 | 범용으로 유지 (actuator가 대체 가능) | + +``` +// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +kr.co.accura.wbx.spring.config.WbxAutoConfiguration +``` + +#### 3.4 Admin UI 조건부 활성화 + +```java +// wbx-spring-core +@Configuration +@ConditionalOnProperty(name = "wbx.spring.admin-ui.enabled", havingValue = "true", matchIfMissing = true) +public class AdminAutoConfiguration { + // AdminController, AdminLoginController, AdminUserDetailsService 등록 + // SecurityAutoConfig의 adminFilterChain도 여기로 이동 +} +``` + +--- + +### Phase 1: wtm-api 모듈 생성 + +#### 3.5 wtm-api build.gradle + +```groovy +// wtm-api/build.gradle +plugins { + id 'org.springframework.boot' version '3.5.0' +} + +dependencies { + // wbx-spring 프레임워크 + implementation project(':wbx-spring-core') + + // WTM 전용 + implementation 'org.apache.poi:poi-ooxml:5.3.0' // P6 WBS, Excel 업로드/다운로드 + + // QueryDSL (리포트 동적 쿼리) + implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' + + // MapStruct (DTO 매핑) + implementation 'org.mapstruct:mapstruct:1.6.3' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' + + // Flyway — Azure SQL + implementation 'org.flywaydb:flyway-sqlserver' + + // DB Driver — 개발: MySQL, 운영: MSSQL (Azure SQL) + runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11' + + // Test + testRuntimeOnly 'com.h2database:h2' +} +``` + +#### 3.6 WtmApplication.java + +```java +@SpringBootApplication(scanBasePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.wtm" +}) +@EntityScan(basePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.wtm" +}) +@EnableJpaRepositories(basePackages = { + "kr.co.accura.wbx.spring", + "kr.co.accura.wtm" +}) +public class WtmApplication { + public static void main(String[] args) { + SpringApplication.run(WtmApplication.class, args); + } +} +``` + +> Auto-Configuration 방식 적용 시 `scanBasePackages` 제거 가능 + +#### 3.7 wtm-api application.yml + +```yaml +spring: + application: + name: wtm-api + + datasource: + url: jdbc:mysql://${DB_HOST:ws.ubuilder.co.kr}:${DB_PORT:3306}/${DB_NAME:wtm_db} + username: ${DB_USER:jsh} + password: ${DB_PASS:jsh@} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: validate # Flyway로 스키마 관리 + open-in-view: false + + flyway: + enabled: true + locations: classpath:db/migration + + data: + redis: + host: ${REDIS_HOST:localhost} + port: 6379 + +server: + port: 8080 + +wbx: + spring: + api-prefix: /api/wtm + jwt: + secret: ${JWT_SECRET:dev-secret-key-minimum-256-bits} + expiration: 28800 + admin-ui: + enabled: false # WTM은 자체 Admin 불필요 + cors: + allowed-origins: ${CORS_ORIGINS:http://localhost:5173} + notification: + sse-enabled: true + +wtm: + work-rules: + default-min-daily-hours: 8 + default-max-weekly-hours: 52 +``` + +--- + +## 4. WTM 도메인 모듈별 구현 범위 + +### 신규 JPA Entity (23개) + +> DB 스키마 상세: `02-database-schema.md` (Single Source of Truth) +> Flyway 마이그레이션: `02-database-schema.md`의 V1~V7 참조 (본 문서에서 복제하지 않음) + +| 도메인 | Entity | 테이블 | Phase | +|--------|--------|--------|:-----:| +| user | `User` | `users` | PH1-1 | +| user | `Role` | `roles` | PH1-1 | +| user | `UserRole` | `user_roles` | PH1-1 | +| user | `OrgHierarchy` | `org_hierarchy` | PH1-1 | +| user | `HrUpload` | `hr_uploads` | PH1-1 | +| project | `Project` | `projects` | PH1-1 | +| project | `ProjectAssignment` | `project_assignments` | PH1-1 | +| project | `ProjectTypeConfig` | `project_type_config` | PH1-1 | +| wbs | `WbsVersion` | `wbs_versions` | PH1-1 | +| wbs | `WbsNode` | `wbs_nodes` | PH1-1 | +| wbs | `CanonicalWbs` | `canonical_wbs` | PH1-1 | +| wbs | `WbsDisciplineAssignment` | `wbs_discipline_assignments` | PH1-1 | +| teal | `TealVersion` | `teal_versions` | PH1-1 | +| teal | `TealEntry` | `teal_entries` | PH1-1 | +| timesheet | `Timesheet` | `timesheets` | PH1-1 | +| timesheet | `TimesheetEntry` | `timesheet_entries` | PH1-1 | +| timesheet | `TimesheetUpload` | `timesheet_uploads` | PH1-1 | +| approval | `TtApproval` | `approvals` | PH1-1 | +| approval | `TtApprovalLine` | `approval_lines` | PH1-1 | +| approval | `TtApprovalComment` | `approval_comments` | PH1-1 | +| config | `OverheadType` | `overhead_types` | PH1-1 | +| config | `WorkRule` | `work_rules` | PH1-1 | +| audit | `SaAccessLog` | `sa_access_logs` | PH1-2 | + +> **User Entity 전략 확정**: WTM은 `02-database-schema.md`에 정의된 `users` 테이블을 직접 사용. +> wbx-spring-core의 `WbxUser`와 별도로, WTM 전용 `User` 엔티티가 HR 확장 필드(discipline, location, employment_type 등)를 +> 포함한 `users` 테이블에 매핑됨. 인증은 wbx-spring-core의 `WbxUser`(wbx_users)가 담당하며, +> `users.email = wbx_users.email`로 연동. + +### 신규 REST Controller (13개, 79 API) + +| Controller | API 수 | Phase | +|-----------|:------:|:-----:| +| `AuthController` (WTM 전용 라우팅) | 8 | PH1-1 | +| `UserController` | 8 | PH1-1 ~ PH1-2 | +| `ProjectController` | 7 | PH1-1 | +| `WbsController` | 7 | PH1-1 ~ PH1-2 | +| `TealController` | 4 | PH1-1 | +| `TimesheetController` | 8 | PH1-1 | +| `ApprovalController` | 8 | PH1-1 ~ PH1-2 | +| `ReportController` | 9 | PH1-1 ~ PH2 | +| `HomeController` (대시보드/알림) | 2 | PH1-1 | +| `OverheadTypeController` (SA) | 3 | PH1-1 | +| `WorkRuleController` (SA) | 2 | PH1-1 | +| `ResourceAssignController` | 5 | PH1-1 | +| `HrIntegrationController` | 2 | PH1-2 | + +### wbx-spring-core 활용 포인트 + +```java +// RBAC 권한 체크 +@PreAuthorize("@wbx.check('TIMESHEET', 'VIEW')") + +// 데이터 필터링 범위 +DeptScope scope = evaluator.getScope("TIMESHEET", "VIEW"); + +// 결재 핸들러 등록 (Bean 등록만으로 자동 연동) +@Component +public class TimesheetApprovalHandler implements ApprovalHandler { ... } + +// SSE 실시간 알림 +sseNotificationService.sendToUser(approverId, notification); + +// 에러 응답 (자동 적용) +throw new BusinessException("주간 합계 54시간 — 최대 52h 초과"); +// → {"detail": "주간 합계 54시간 — 최대 52h 초과"} +``` + +--- + +## 5. User Entity 전략 + +### 선택지 + +| 방안 | 장점 | 단점 | +|------|------|------| +| **A: WbxUser 확장** (WTM 필드를 wbx_users에 추가) | 테이블 하나, 간단 | core 엔티티 오염 | +| **B: WtmUser 별도** (wtm.users + wbx.wbx_users 동기화) | 도메인 분리 깔끔 | 두 테이블 동기화 필요 | +| **C: WtmUserProfile** (wbx_users FK → wtm_user_profiles) | core 불변, 확장 자유 | JOIN 필요 | + +### 권장: **방안 C** — WtmUserProfile 확장 테이블 + +```sql +CREATE TABLE wtm_user_profiles ( + id BIGINT IDENTITY PRIMARY KEY, + wbx_user_id BIGINT NOT NULL UNIQUE REFERENCES wbx_users(id), + employee_id VARCHAR(50) UNIQUE, + discipline VARCHAR(100), + location VARCHAR(50), + employment_type VARCHAR(20) DEFAULT 'INTERNAL', + business_unit VARCHAR(100), + division VARCHAR(100), + department VARCHAR(100), + part VARCHAR(100), + created_at DATETIME2 DEFAULT GETDATE() +); +``` + +**이유**: wbx-spring-core의 `WbxUser` 엔티티를 수정하지 않고, WTM 전용 HR 필드를 별도 테이블로 관리. `wbx_user_id` FK로 연결. + +--- + +## 6. 작업 순서 (Implementation Order) + +``` +Phase 0 — wbx-spring-core 라이브러리 전환 (1~2일) +├── 0-1. 루트 settings.gradle, build.gradle 생성 +├── 0-2. wbx-spring-core build.gradle → java-library 전환 +├── 0-3. WbxSpringCoreApplication 삭제, WbxAutoConfiguration 생성 +├── 0-4. application.yml → 기본값만 남기고 정리 +├── 0-5. Admin UI 조건부 활성화 (@ConditionalOnProperty) +├── 0-6. Enable 어노테이션 분리 → 개별 @Configuration +└── 0-7. 빌드 검증 (./gradlew :wbx-spring-core:build) + +Phase 1 — wtm-api 스캐폴딩 (1일) +├── 1-1. wtm-api 디렉토리 + build.gradle 생성 +├── 1-2. WtmApplication.java 생성 +├── 1-3. application.yml (개발 환경) +├── 1-4. Flyway V1~V7 마이그레이션 SQL 생성 +├── 1-5. 빌드 + 부팅 검증 (./gradlew :wtm-api:bootRun) +└── 1-6. wbx-spring-core 연동 검증 (JWT, RBAC, 결재 등) + +Phase 2 — WTM 도메인 구현 (W3~W8, 계획서 10-schedule 참조) +├── 2-1. domain/user + domain/project (W3~W4) +├── 2-2. domain/wbs + domain/teal + P6 파서 (W4~W5) +├── 2-3. domain/timesheet + 규칙 엔진 (W5~W7) +├── 2-4. domain/approval/handler (W6~W7) +├── 2-5. domain/report + Excel Export (W7~W8) +└── 2-6. integration/sap (PH1-2) +``` + +--- + +## 7. 주의사항 + +### 어노테이션 프로세서 순서 +Lombok + MapStruct + QueryDSL을 함께 사용 시: +```groovy +annotationProcessor 'org.projectlombok:lombok' +annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' +annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' +annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' +``` + +### DB 이중 구조 — Flyway 듀얼 마이그레이션 + +- **개발**: MySQL (ws.ubuilder.co.kr) — `wtm_db` 스키마 +- **운영**: Azure SQL (MSSQL) — Flyway 마이그레이션 + +DDL 구문 차이(`IDENTITY` vs `AUTO_INCREMENT`, `DATETIME2` vs `DATETIME`, `GETDATE()` vs `NOW()`)를 +프로필별 마이그레이션 경로로 해결: + +```yaml +# application-mysql.yml (개발) +spring: + flyway: + locations: classpath:db/migration/common,classpath:db/migration/mysql + +# application-mssql.yml (운영) +spring: + flyway: + locations: classpath:db/migration/common,classpath:db/migration/mssql +``` + +``` +src/main/resources/db/migration/ +├── common/ # ANSI SQL 호환 (seed data, VIEW 등) +│ └── V10__seed_roles.sql +├── mysql/ # MySQL 전용 DDL +│ ├── V1__init_users.sql # AUTO_INCREMENT, DATETIME, NOW() +│ ├── V2__init_projects.sql +│ └── ... +└── mssql/ # Azure SQL 전용 DDL + ├── V1__init_users.sql # IDENTITY, DATETIME2, GETDATE() + ├── V2__init_projects.sql + └── ... +``` + +> `02-database-schema.md`의 DDL은 MSSQL 기준. MySQL 버전은 구현 시 변환 생성. + +### wbx-spring-core의 WbxUser와 WTM User +- wbx-spring-core의 `wbx_users` 테이블: 인증/권한 용도 (JWT, SSO, MFA) +- WTM의 `users` 테이블: HR 확장 필드 포함 (discipline, location, employment_type 등) +- 연동: `users.email = wbx_users.email` 기준으로 매칭 +- WTM `User` 엔티티는 `users` 테이블에 직접 매핑 (별도 확장 테이블 불필요) diff --git a/HanwhaOCN/wtmgr/13-frontend-setup-plan.md b/HanwhaOCN/wtmgr/13-frontend-setup-plan.md new file mode 100644 index 0000000..948ba49 --- /dev/null +++ b/HanwhaOCN/wtmgr/13-frontend-setup-plan.md @@ -0,0 +1,820 @@ +# 13. WTM Frontend 프로젝트 구성 계획 + +> 작성일: 2026-03-25 +> 기술 스택: Vue 3.5 + TypeScript + PrimeVue 4 + Pinia + Vite +> 목적: 여러 개발자가 동시 개발 가능한 모듈 기반 프론트엔드 구조 설계 + +--- + +## 1. 기술 스택 + +| 도구 | 버전 | 용도 | +|------|------|------| +| **Vite** | 6.x | 빌드, HMR 개발 서버 | +| **Vue** | 3.5+ | Composition API + ` + + + + +``` + +### 2.3 AppSidebar.vue + +```vue + + + + + + +``` + +### 2.4 AppTopbar.vue + +```vue + + + + + + +``` + +--- + +## 3. 페이지 표준 레이아웃 + +### 3.1 BasePageHeader.vue + +모든 페이지 상단에 사용되는 제목 + 액션 바. + +```vue + + + + + + +``` + +### 3.2 BaseCrudTable.vue + +CRUD 테이블의 공통 셸. 모든 목록 화면이 이것을 사용. + +```vue + + + + + + +``` + +### 3.3 BaseFormDialog.vue + +모든 생성/수정 다이얼로그의 공통 셸. + +```vue + + + + + + +``` + +--- + +## 4. 반응형 폼 그리드 + +### 4.1 FormGrid 유틸리티 클래스 + +```scss +// src/assets/styles/_form-grid.scss +@use 'variables' as *; + +// 폼 필드 컨테이너 — CSS Grid 기반 +.form-grid { + display: grid; + gap: $space-md; + grid-template-columns: repeat(12, 1fr); + + // 기본: 1열 + @media (max-width: $bp-mobile) { + grid-template-columns: 1fr; + } +} + +// 컬럼 span 클래스 +.col-1 { grid-column: span 1; } +.col-2 { grid-column: span 2; } +.col-3 { grid-column: span 3; } +.col-4 { grid-column: span 4; } +.col-6 { grid-column: span 6; } +.col-8 { grid-column: span 8; } +.col-12 { grid-column: span 12; } + +// 모바일에서는 전부 full-width +@media (max-width: $bp-mobile) { + [class^="col-"] { + grid-column: span 1 !important; + } +} + +// 폼 필드 단위 +.form-field { + display: flex; + flex-direction: column; + gap: $space-xs; + + &__label { + font-size: $font-size-sm; + font-weight: 600; + color: $color-text; + + &--required::after { + content: ' *'; + color: $color-danger; + } + } + + &__error { + font-size: $font-size-xs; + color: $color-danger; + } + + &__hint { + font-size: $font-size-xs; + color: $color-text-muted; + } +} +``` + +### 4.2 사용 예시: UserFormDialog + +```vue + +``` + +반응형 동작: +- **Desktop (≥992px)**: 12열 그리드 → 이름(6) + 이메일(6) 한 줄, 사번(4) + 부서(4) + Discipline(4) 한 줄 +- **Mobile (<576px)**: 모두 1열로 스택 + +--- + +## 5. 반응형 DataTable 전략 + +```vue + + + + + + + + + + + + + + +``` + +```scss +// DataTable 반응형 헬퍼 +@media (max-width: $bp-tablet) { + .hidden-mobile { + display: none !important; + } +} +``` + +--- + +## 6. 대시보드 카드 그리드 + +```vue + + + + +``` + +--- + +## 7. main.scss 통합 + +```scss +// src/assets/styles/main.scss + +// 1. 변수 (최상위) +@use 'variables'; + +// 2. 폼 그리드 유틸리티 +@use 'form-grid'; + +// 3. PrimeVue 테마 오버라이드 +@use 'overrides'; + +// 4. 글로벌 리셋/베이스 +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; +} + +body { + margin: 0; + font-family: var(--p-font-family); + color: var(--p-text-color); + background: var(--p-surface-ground); +} + +// 5. 유틸리티 클래스 (최소한) +.text-center { text-align: center; } +.text-right { text-align: right; } +.text-muted { color: var(--p-text-muted-color); } +.text-sm { font-size: variables.$font-size-sm; } +.text-xs { font-size: variables.$font-size-xs; } +.mt-0 { margin-top: 0; } +.mb-0 { margin-bottom: 0; } + +// 6. 카드 표준 (페이지 내 섹션) +.card { + background: var(--p-surface-0); + border: 1px solid var(--p-surface-200); + border-radius: variables.$radius-md; + padding: variables.$space-lg; + + @media (max-width: variables.$bp-mobile) { + padding: variables.$space-md; + border-radius: variables.$radius-sm; + } +} +``` + +--- + +## 8. 표준 페이지 작성 템플릿 + +모든 목록 페이지는 아래 구조를 따릅니다: + +```vue + + + + +``` + +--- + +## 9. 요약: 하드코딩 제거 체크리스트 + +| 하드코딩 대상 | 표준 출처 | 사용 방법 | +|-------------|----------|----------| +| 색상 (#hex) | PrimeVue CSS 변수 `var(--p-*)` | SCSS `$color-*` 변수 경유 | +| px 수치 | `_variables.scss` `$space-*` | 직접 px 금지 | +| 브레이크포인트 | `BREAKPOINTS` 상수 + `$bp-*` | `useBreakpoints()` 또는 SCSS `@media` | +| 사이드바/탑바 크기 | `LAYOUT` 상수 + `$sidebar-*` | AppLayout에서 집중 관리 | +| 페이지 크기 | `PAGINATION` 상수 | BaseCrudTable 기본값 | +| 토스트 시간 | `TOAST` 상수 | useAppToast 래퍼 | +| 날짜 포맷 | `DATE_FORMAT` 상수 | date.utils.ts | +| 상태 레이블/색상 | `*_STATUS` 상수 | Tag severity 자동 매핑 | +| 역할 코드 | `ROLES` 상수 | auth.guard, usePermission | +| 메뉴 구조 | `MENU_ITEMS` 상수 | AppSidebar (역할 필터링 자동) | +| 시수 규칙 수치 | `TIMESHEET_RULES` 상수 | TimesheetGrid 검증 | +| API base URL | `.env` `VITE_API_BASE_URL` | axios.ts |