diff --git a/.gitignore b/.gitignore index 8968de5..6fa5ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ Thumbs.db # Logs *.log logs/ + +# Plans +plans/ diff --git a/plans/wtmgr/00-overview.md b/plans/wtmgr/00-overview.md deleted file mode 100644 index 66df35d..0000000 --- a/plans/wtmgr/00-overview.md +++ /dev/null @@ -1,119 +0,0 @@ -# 한화오션 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/plans/wtmgr/01-architecture.md b/plans/wtmgr/01-architecture.md deleted file mode 100644 index 254ae37..0000000 --- a/plans/wtmgr/01-architecture.md +++ /dev/null @@ -1,171 +0,0 @@ -# 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/plans/wtmgr/02-database-schema.md b/plans/wtmgr/02-database-schema.md deleted file mode 100644 index bc2c17f..0000000 --- a/plans/wtmgr/02-database-schema.md +++ /dev/null @@ -1,422 +0,0 @@ -# 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/plans/wtmgr/03-timesheet-module.md b/plans/wtmgr/03-timesheet-module.md deleted file mode 100644 index 7665c05..0000000 --- a/plans/wtmgr/03-timesheet-module.md +++ /dev/null @@ -1,337 +0,0 @@ -# 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/plans/wtmgr/04-wbs-teal-module.md b/plans/wtmgr/04-wbs-teal-module.md deleted file mode 100644 index 3f2b938..0000000 --- a/plans/wtmgr/04-wbs-teal-module.md +++ /dev/null @@ -1,312 +0,0 @@ -# 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/plans/wtmgr/05-approval-handlers.md b/plans/wtmgr/05-approval-handlers.md deleted file mode 100644 index ab3dbd5..0000000 --- a/plans/wtmgr/05-approval-handlers.md +++ /dev/null @@ -1,97 +0,0 @@ -# 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/plans/wtmgr/06-reporting-module.md b/plans/wtmgr/06-reporting-module.md deleted file mode 100644 index b30e49d..0000000 --- a/plans/wtmgr/06-reporting-module.md +++ /dev/null @@ -1,185 +0,0 @@ -# 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/plans/wtmgr/07-api-spec.md b/plans/wtmgr/07-api-spec.md deleted file mode 100644 index 8d263ca..0000000 --- a/plans/wtmgr/07-api-spec.md +++ /dev/null @@ -1,215 +0,0 @@ -# 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/plans/wtmgr/08-sap-btp-integration.md b/plans/wtmgr/08-sap-btp-integration.md deleted file mode 100644 index f51a618..0000000 --- a/plans/wtmgr/08-sap-btp-integration.md +++ /dev/null @@ -1,113 +0,0 @@ -# 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/plans/wtmgr/09-devops-infra.md b/plans/wtmgr/09-devops-infra.md deleted file mode 100644 index 366dba2..0000000 --- a/plans/wtmgr/09-devops-infra.md +++ /dev/null @@ -1,229 +0,0 @@ -# 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/plans/wtmgr/10-schedule-milestones.md b/plans/wtmgr/10-schedule-milestones.md deleted file mode 100644 index cba0f69..0000000 --- a/plans/wtmgr/10-schedule-milestones.md +++ /dev/null @@ -1,168 +0,0 @@ -# 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/plans/wtmgr/11-requirements-traceability.md b/plans/wtmgr/11-requirements-traceability.md deleted file mode 100644 index 11d79e0..0000000 --- a/plans/wtmgr/11-requirements-traceability.md +++ /dev/null @@ -1,934 +0,0 @@ -# 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/plans/wtmgr/12-project-setup-plan.md b/plans/wtmgr/12-project-setup-plan.md deleted file mode 100644 index b915363..0000000 --- a/plans/wtmgr/12-project-setup-plan.md +++ /dev/null @@ -1,659 +0,0 @@ -# 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/plans/wtmgr/13-frontend-setup-plan.md b/plans/wtmgr/13-frontend-setup-plan.md deleted file mode 100644 index 948ba49..0000000 --- a/plans/wtmgr/13-frontend-setup-plan.md +++ /dev/null @@ -1,820 +0,0 @@ -# 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 | diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java b/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java index efd1371..49449e7 100644 --- a/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java +++ b/wtm-api/src/main/java/kr/co/accura/wtm/api/WtmAuthController.java @@ -1,21 +1,48 @@ package kr.co.accura.wtm.api; +import kr.co.accura.wbx.spring.auth.WbxUserDetails; +import kr.co.accura.wtm.domain.user.repository.UserRoleRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.List; import java.util.Map; /** * WTM-specific auth endpoints that supplement wbx-spring-core's AuthController. - *

- * wbx-spring-core already provides: /api/wtm/auth/login, /me, /refresh, /logout, /password/change. - * This controller adds only the MISSING endpoints: SSO and password-reset. + * Overrides /me to include WTM role information from user_roles table. */ @RestController @RequiredArgsConstructor public class WtmAuthController { + private final UserRoleRepository userRoleRepository; + + /** + * 내 정보 조회 — WTM 역할 포함 (별도 경로) + */ + @GetMapping("/api/wtm/auth/me/profile") + public Map me(@AuthenticationPrincipal WbxUserDetails user) { + // wtm user_roles에서 역할 코드 조회 (email 매칭) + List roles = userRoleRepository.findRoleCodesByUserEmail(user.getEmail()); + if (roles.isEmpty() && user.isAdmin()) { + roles = List.of("SA"); + } + + Map result = new HashMap<>(); + result.put("id", user.getId()); + result.put("email", user.getEmail()); + result.put("username", user.getUsername()); + result.put("full_name", user.getFullName()); + result.put("is_admin", user.isAdmin()); + result.put("department_id", user.getDepartmentId() != null ? user.getDepartmentId() : 0); + result.put("roles", roles); + return result; + } + /** * SSO initiation — redirects to OAuth2 authorization endpoint. * Requires Azure Entra ID configuration. diff --git a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java index ec43d8b..a328337 100644 --- a/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java +++ b/wtm-api/src/main/java/kr/co/accura/wtm/domain/user/repository/UserRoleRepository.java @@ -2,6 +2,8 @@ package kr.co.accura.wtm.domain.user.repository; import kr.co.accura.wtm.domain.user.entity.UserRole; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -14,4 +16,7 @@ public interface UserRoleRepository extends JpaRepository { List findByUser_IdAndProjectId(Long userId, Long projectId); void deleteByUser_IdAndProjectId(Long userId, Long projectId); + + @Query("SELECT r.code FROM UserRole ur JOIN ur.role r JOIN ur.user u WHERE u.email = :email") + List findRoleCodesByUserEmail(@Param("email") String email); } diff --git a/wtm-frontend/src/modules/approval/views/ApprovalPendingView.vue b/wtm-frontend/src/modules/approval/views/ApprovalPendingView.vue index 76303e0..57331b5 100644 --- a/wtm-frontend/src/modules/approval/views/ApprovalPendingView.vue +++ b/wtm-frontend/src/modules/approval/views/ApprovalPendingView.vue @@ -25,7 +25,7 @@ async function loadPending() { loading.value = true; try { const { data } = await approvalService.getPending(); - items.value = Array.isArray(data) ? data : (data as any).items ?? []; + items.value = Array.isArray(data) ? data : (data as any).content ?? (data as any).items ?? []; } catch { toast.add({ severity: 'error', summary: '오류', detail: '결재 대기 목록 로드 실패', life: 5000 }); } finally { diff --git a/wtm-frontend/src/modules/auth/auth.service.ts b/wtm-frontend/src/modules/auth/auth.service.ts index 129364b..4432421 100644 --- a/wtm-frontend/src/modules/auth/auth.service.ts +++ b/wtm-frontend/src/modules/auth/auth.service.ts @@ -5,7 +5,7 @@ const BASE = '/api/wtm/auth'; export const authApi = { login: (data: LoginRequest) => api.post(`${BASE}/login`, data), - me: () => api.get(`${BASE}/me`), + me: () => api.get(`${BASE}/me/profile`), refresh: (refreshToken: string) => api.post(`${BASE}/refresh`, { refreshToken }), logout: () => api.post(`${BASE}/logout`), resetPassword: (email: string) => api.post(`${BASE}/password/reset`, { email }),