chore: plans 폴더 git 추적 제거 및 코드 업데이트
- plans/ 폴더를 .gitignore에 추가하고 git 추적에서 제거 - WtmAuthController, UserRoleRepository 수정 - ApprovalPendingView, auth.service 수정 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
3
.gitignore
벤더링됨
3
.gitignore
벤더링됨
@@ -29,3 +29,6 @@ Thumbs.db
|
|||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
|
|
||||||
|
# Plans
|
||||||
|
plans/
|
||||||
|
|||||||
@@ -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건) |
|
|
||||||
@@ -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 페이징 파라미터 수용 |
|
|
||||||
@@ -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<UserRole> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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<TimesheetEntry> 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<LocalDate, BigDecimal> 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<TimesheetEntryRequest> 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<TimesheetSummaryDto> 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) 초과 경고 │
|
|
||||||
│ │
|
|
||||||
│ [임시 저장] [제출 (결재 요청)] │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
@@ -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<String, WbsNodeDto> 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<WbsNode> 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<WbsNode> 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<WbsNode> nodesA = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionA);
|
|
||||||
List<WbsNode> nodesB = wbsNodeRepository.findByProjectIdAndVersion(projectId, versionB);
|
|
||||||
|
|
||||||
Map<String, WbsNode> mapA = nodesA.stream()
|
|
||||||
.collect(Collectors.toMap(WbsNode::getWbsCode, Function.identity()));
|
|
||||||
Map<String, WbsNode> 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<TealEntryDto> entries = parseTealFile(file);
|
|
||||||
|
|
||||||
// 2. WBS 코드 검증 (canonical_wbs에 존재하는지)
|
|
||||||
Set<String> 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<TealEntry> 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 목록
|
|
||||||
```
|
|
||||||
@@ -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<TtApprovalLine> 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() {
|
|
||||||
// 주간 미제출 사용자에게 알림
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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<ProjectHoursRow> 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)
|
|
||||||
```
|
|
||||||
@@ -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)
|
|
||||||
@@ -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<HrEmployeeDto> 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 |
|
|
||||||
@@ -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) |
|
|
||||||
@@ -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) | 한화오션 |
|
|
||||||
@@ -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<HrEmployeeDto> 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<User> 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<User> 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** |
|
|
||||||
@@ -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` 테이블에 직접 매핑 (별도 확장 테이블 불필요)
|
|
||||||
@@ -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 + `<script setup>` |
|
|
||||||
| **TypeScript** | 5.5+ | 전체 프로젝트 타입 안전성 |
|
|
||||||
| **PrimeVue** | 4.x | UI 컴포넌트 (80+ 제공) |
|
|
||||||
| **@primeuix/themes** | latest | Aura 테마 프리셋 |
|
|
||||||
| **Pinia** | 2.x | 도메인별 상태 관리 |
|
|
||||||
| **Vue Router** | 4.x | SPA 라우팅, lazy loading |
|
|
||||||
| **Axios** | 1.x | HTTP 클라이언트 (JWT 인터셉터) |
|
|
||||||
| **VueUse** | 12.x | 유틸리티 컴포저블 |
|
|
||||||
| **Chart.js** | 4.x | 차트 (PrimeVue Chart 래퍼) |
|
|
||||||
| **ESLint** | 9.x | 린트 (flat config) |
|
|
||||||
| **Prettier** | 3.x | 코드 포매팅 |
|
|
||||||
| **Vitest** | 2.x | 단위 테스트 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 프로젝트 구조
|
|
||||||
|
|
||||||
### 설계 원칙
|
|
||||||
- **도메인(Feature) 기반 모듈 분리**: 각 개발자가 담당 모듈만 작업, 최소 충돌
|
|
||||||
- **모듈 자급자족**: 각 모듈이 views/components/store/service/types/routes를 독립 보유
|
|
||||||
- **core는 인프라만**: 비즈니스 로직 없이 공유 인프라(API, 인증, 레이아웃)만 제공
|
|
||||||
- **cross-module 의존은 core를 통해서만**: 모듈 간 직접 import 금지
|
|
||||||
|
|
||||||
```
|
|
||||||
wtm-frontend/
|
|
||||||
├── public/
|
|
||||||
│ └── favicon.ico
|
|
||||||
│
|
|
||||||
├── src/
|
|
||||||
│ ├── app/ # 애플리케이션 셸
|
|
||||||
│ │ ├── App.vue
|
|
||||||
│ │ ├── main.ts
|
|
||||||
│ │ ├── router.ts # 루트 라우터 (모듈 라우트 조립)
|
|
||||||
│ │ └── plugins/
|
|
||||||
│ │ ├── primevue.ts # PrimeVue 설정 + 테마
|
|
||||||
│ │ ├── pinia.ts
|
|
||||||
│ │ └── index.ts
|
|
||||||
│ │
|
|
||||||
│ ├── core/ # 공유 인프라 (비즈니스 로직 없음)
|
|
||||||
│ │ ├── api/
|
|
||||||
│ │ │ ├── axios.ts # Axios 인스턴스 + JWT 인터셉터
|
|
||||||
│ │ │ ├── api.types.ts # PageResponse, ErrorResponse 등
|
|
||||||
│ │ │ └── endpoints.ts # BASE_URL 상수
|
|
||||||
│ │ ├── auth/
|
|
||||||
│ │ │ ├── auth.guard.ts # 네비게이션 가드
|
|
||||||
│ │ │ ├── auth.service.ts # 토큰 저장/갱신
|
|
||||||
│ │ │ └── auth.types.ts
|
|
||||||
│ │ ├── composables/ # 공용 컴포저블
|
|
||||||
│ │ │ ├── useAppToast.ts # Toast 래퍼
|
|
||||||
│ │ │ ├── useConfirmAction.ts # ConfirmDialog 래퍼
|
|
||||||
│ │ │ ├── usePagination.ts # DataTable 페이징 헬퍼
|
|
||||||
│ │ │ └── usePermission.ts # 역할/권한 체크
|
|
||||||
│ │ ├── components/ # 공용 레이아웃/베이스 컴포넌트
|
|
||||||
│ │ │ ├── AppLayout.vue # 사이드바 + 탑바 + 콘텐츠
|
|
||||||
│ │ │ ├── AppSidebar.vue
|
|
||||||
│ │ │ ├── AppTopbar.vue
|
|
||||||
│ │ │ ├── BaseCrudTable.vue # 재사용 DataTable + CRUD 셸
|
|
||||||
│ │ │ ├── BaseFormDialog.vue # 재사용 폼 다이얼로그
|
|
||||||
│ │ │ ├── BasePageHeader.vue
|
|
||||||
│ │ │ └── NotFoundView.vue
|
|
||||||
│ │ ├── constants/
|
|
||||||
│ │ │ └── app.constants.ts
|
|
||||||
│ │ ├── types/
|
|
||||||
│ │ │ ├── common.types.ts # 공용 타입 (ID, Timestamp 등)
|
|
||||||
│ │ │ └── menu.types.ts
|
|
||||||
│ │ └── utils/
|
|
||||||
│ │ ├── date.utils.ts
|
|
||||||
│ │ ├── format.utils.ts
|
|
||||||
│ │ └── validation.utils.ts
|
|
||||||
│ │
|
|
||||||
│ ├── modules/ # ===== 도메인 모듈 (개발자별 담당) =====
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── auth/ # 👤 풀스택 ③ 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ ├── LoginView.vue
|
|
||||||
│ │ │ │ ├── ForgotPasswordView.vue
|
|
||||||
│ │ │ │ └── ChangePasswordView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ └── LoginForm.vue
|
|
||||||
│ │ │ ├── auth.routes.ts
|
|
||||||
│ │ │ ├── auth.store.ts
|
|
||||||
│ │ │ ├── auth.service.ts
|
|
||||||
│ │ │ └── auth.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── dashboard/ # 👤 풀스택 ③ 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ └── DashboardView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── StatCard.vue
|
|
||||||
│ │ │ │ ├── TimesheetChart.vue
|
|
||||||
│ │ │ │ ├── PendingApprovalCard.vue
|
|
||||||
│ │ │ │ └── RecentActivityList.vue
|
|
||||||
│ │ │ ├── dashboard.routes.ts
|
|
||||||
│ │ │ ├── dashboard.store.ts
|
|
||||||
│ │ │ ├── dashboard.service.ts
|
|
||||||
│ │ │ └── dashboard.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── user/ # 👤 풀스택 ③ 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ ├── UserListView.vue
|
|
||||||
│ │ │ │ └── UserDetailView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── UserTable.vue
|
|
||||||
│ │ │ │ ├── UserFormDialog.vue
|
|
||||||
│ │ │ │ ├── UserRoleBadge.vue
|
|
||||||
│ │ │ │ └── UserExcelUpload.vue
|
|
||||||
│ │ │ ├── user.routes.ts
|
|
||||||
│ │ │ ├── user.store.ts
|
|
||||||
│ │ │ ├── user.service.ts
|
|
||||||
│ │ │ └── user.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── project/ # 👤 풀스택 ① 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ ├── ProjectListView.vue
|
|
||||||
│ │ │ │ └── ProjectDetailView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── ProjectTable.vue
|
|
||||||
│ │ │ │ ├── ProjectFormDialog.vue
|
|
||||||
│ │ │ │ ├── ProjectStatusTag.vue
|
|
||||||
│ │ │ │ └── ProjectMemberTable.vue
|
|
||||||
│ │ │ ├── project.routes.ts
|
|
||||||
│ │ │ ├── project.store.ts
|
|
||||||
│ │ │ ├── project.service.ts
|
|
||||||
│ │ │ └── project.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── wbs/ # 👤 풀스택 ① 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ ├── WbsTreeView.vue
|
|
||||||
│ │ │ │ └── WbsCompareView.vue # PH1-2
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── WbsTreeTable.vue # PrimeVue TreeTable
|
|
||||||
│ │ │ │ ├── WbsVersionSelect.vue
|
|
||||||
│ │ │ │ ├── WbsUploadDialog.vue
|
|
||||||
│ │ │ │ └── WbsComparePanel.vue # PH1-2
|
|
||||||
│ │ │ ├── wbs.routes.ts
|
|
||||||
│ │ │ ├── wbs.store.ts
|
|
||||||
│ │ │ ├── wbs.service.ts
|
|
||||||
│ │ │ └── wbs.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── teal/ # 👤 풀스택 ① 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ └── TealListView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── TealTable.vue
|
|
||||||
│ │ │ │ ├── TealUploadDialog.vue
|
|
||||||
│ │ │ │ └── TealVersionSelect.vue
|
|
||||||
│ │ │ ├── teal.routes.ts
|
|
||||||
│ │ │ ├── teal.store.ts
|
|
||||||
│ │ │ ├── teal.service.ts
|
|
||||||
│ │ │ └── teal.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── settings/ # 👤 풀스택 ③ 담당 (SA 전용)
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ └── SettingsView.vue # 탭: Overhead Types | Work Rules
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── OverheadTypeTable.vue
|
|
||||||
│ │ │ │ ├── OverheadTypeFormDialog.vue
|
|
||||||
│ │ │ │ └── WorkRuleForm.vue
|
|
||||||
│ │ │ ├── settings.routes.ts
|
|
||||||
│ │ │ ├── settings.store.ts
|
|
||||||
│ │ │ ├── settings.service.ts # overhead-types + work-rules API
|
|
||||||
│ │ │ └── settings.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── timesheet/ # 👤 풀스택 ② 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ ├── TimesheetWeekView.vue # 메인 — 주간 시수 입력
|
|
||||||
│ │ │ │ ├── TimesheetHistoryView.vue
|
|
||||||
│ │ │ │ └── TimesheetUploadView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── TimesheetGrid.vue # DataTable 편집 가능 셀
|
|
||||||
│ │ │ │ ├── TimesheetEntryRow.vue # 한 행 (프로젝트+WBS+TEAL+일별시간)
|
|
||||||
│ │ │ │ ├── TimesheetTabPanel.vue # Non-Project | Other | EPC 탭
|
|
||||||
│ │ │ │ ├── TimesheetSummaryCard.vue # 주간 합계, 규칙 위반 경고
|
|
||||||
│ │ │ │ ├── TimesheetStatusBadge.vue
|
|
||||||
│ │ │ │ ├── TimesheetWeekPicker.vue # 주간 날짜 선택
|
|
||||||
│ │ │ │ └── TimesheetExcelUpload.vue
|
|
||||||
│ │ │ ├── timesheet.routes.ts
|
|
||||||
│ │ │ ├── timesheet.store.ts
|
|
||||||
│ │ │ ├── timesheet.service.ts
|
|
||||||
│ │ │ └── timesheet.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── approval/ # 👤 풀스택 ② 담당
|
|
||||||
│ │ │ ├── views/
|
|
||||||
│ │ │ │ ├── ApprovalPendingView.vue # 결재 대기 목록
|
|
||||||
│ │ │ │ └── ApprovalHistoryView.vue
|
|
||||||
│ │ │ ├── components/
|
|
||||||
│ │ │ │ ├── ApprovalTable.vue
|
|
||||||
│ │ │ │ ├── ApprovalActionBar.vue # 승인/반려/일괄승인 버튼
|
|
||||||
│ │ │ │ ├── ApprovalDetailDialog.vue # 시수 상세 + 코멘트
|
|
||||||
│ │ │ │ ├── ApprovalTimeline.vue # PrimeVue Timeline
|
|
||||||
│ │ │ │ └── ApprovalCommentForm.vue
|
|
||||||
│ │ │ ├── approval.routes.ts
|
|
||||||
│ │ │ ├── approval.store.ts
|
|
||||||
│ │ │ ├── approval.service.ts
|
|
||||||
│ │ │ └── approval.types.ts
|
|
||||||
│ │ │
|
|
||||||
│ │ └── report/ # 👤 풀스택 ① 담당
|
|
||||||
│ │ ├── views/
|
|
||||||
│ │ │ └── ReportView.vue
|
|
||||||
│ │ ├── components/
|
|
||||||
│ │ │ ├── ReportFilterPanel.vue # 기간, 프로젝트, groupBy 필터
|
|
||||||
│ │ │ ├── ProjectHoursChart.vue # Bar/Line 차트
|
|
||||||
│ │ │ ├── WbsHoursChart.vue
|
|
||||||
│ │ │ ├── ReportDataTable.vue # 집계 테이블
|
|
||||||
│ │ │ └── ReportExportButton.vue # Excel 다운로드
|
|
||||||
│ │ ├── report.routes.ts
|
|
||||||
│ │ ├── report.store.ts
|
|
||||||
│ │ ├── report.service.ts
|
|
||||||
│ │ └── report.types.ts
|
|
||||||
│ │
|
|
||||||
│ └── assets/
|
|
||||||
│ ├── styles/
|
|
||||||
│ │ ├── _variables.scss # 색상, 간격 변수
|
|
||||||
│ │ ├── _overrides.scss # PrimeVue 테마 오버라이드
|
|
||||||
│ │ └── main.scss
|
|
||||||
│ └── images/
|
|
||||||
│ └── logo.svg
|
|
||||||
│
|
|
||||||
├── .env # 기본값 (커밋)
|
|
||||||
├── .env.development # 로컬 개발 (커밋)
|
|
||||||
├── .env.production # 운영 (커밋)
|
|
||||||
├── .env.local # 개인 오버라이드 (gitignore)
|
|
||||||
├── .gitignore
|
|
||||||
├── .prettierrc.json
|
|
||||||
├── eslint.config.js
|
|
||||||
├── tsconfig.json
|
|
||||||
├── tsconfig.app.json
|
|
||||||
├── tsconfig.node.json
|
|
||||||
├── vite.config.ts
|
|
||||||
├── index.html
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 개발자 담당 배분
|
|
||||||
|
|
||||||
| 개발자 | 담당 모듈 | 화면 수 | 비고 |
|
|
||||||
|--------|----------|:-------:|------|
|
|
||||||
| **풀스택 ①** | project, wbs, teal, report | 10 | WBS TreeTable, P6 업로드, 차트 |
|
|
||||||
| **풀스택 ②** | timesheet, approval | 7 | 시수 입력 핵심, 결재 워크플로우 |
|
|
||||||
| **풀스택 ③** | auth, dashboard, user | 9 | 로그인/SSO, 대시보드, 인력 관리 |
|
|
||||||
| **공통 (리드)** | core/, app/ | - | 레이아웃, API 인프라, 베이스 컴포넌트 |
|
|
||||||
|
|
||||||
### 충돌 최소화 규칙
|
|
||||||
|
|
||||||
```
|
|
||||||
✅ 허용: modules/timesheet/ 내 파일 자유 수정
|
|
||||||
✅ 허용: core/composables/ 에 새 컴포저블 추가
|
|
||||||
❌ 금지: 다른 모듈 직접 import (modules/approval → modules/timesheet)
|
|
||||||
❌ 금지: core/components/ 기존 파일 단독 수정 (리드 승인 필요)
|
|
||||||
⚠️ 주의: app/router.ts 는 모듈 routes만 import (자동 조립)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 모듈 내부 파일 규칙
|
|
||||||
|
|
||||||
모든 모듈은 동일한 6파일 패턴을 따릅니다:
|
|
||||||
|
|
||||||
```
|
|
||||||
modules/{module}/
|
|
||||||
├── views/ # 라우트에 매핑되는 페이지 컴포넌트
|
|
||||||
│ └── {Module}ListView.vue
|
|
||||||
├── components/ # 모듈 전용 하위 컴포넌트
|
|
||||||
│ └── {Module}Table.vue
|
|
||||||
├── {module}.routes.ts # RouteRecordRaw[] export
|
|
||||||
├── {module}.store.ts # Pinia defineStore (Composition API)
|
|
||||||
├── {module}.service.ts # Axios API 호출 함수
|
|
||||||
└── {module}.types.ts # 모듈 전용 TypeScript 타입/인터페이스
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.1 라우트 파일 (lazy loading)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// modules/timesheet/timesheet.routes.ts
|
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
export const timesheetRoutes: RouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
path: '/timesheets',
|
|
||||||
name: 'timesheet-week',
|
|
||||||
component: () => import('./views/TimesheetWeekView.vue'),
|
|
||||||
meta: { title: '시수 입력', icon: 'pi pi-clock', roles: ['USER', 'DL', 'PM', 'SA'] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/timesheets/history',
|
|
||||||
name: 'timesheet-history',
|
|
||||||
component: () => import('./views/TimesheetHistoryView.vue'),
|
|
||||||
meta: { title: '시수 이력', roles: ['USER', 'DL', 'PM', 'SA'] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/timesheets/upload',
|
|
||||||
name: 'timesheet-upload',
|
|
||||||
component: () => import('./views/TimesheetUploadView.vue'),
|
|
||||||
meta: { title: 'Excel 업로드', roles: ['USER'] },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 서비스 파일 (API 호출)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// modules/timesheet/timesheet.service.ts
|
|
||||||
import api from '@/core/api/axios';
|
|
||||||
import type { Timesheet, TimesheetEntry, TimesheetFilter } from './timesheet.types';
|
|
||||||
import type { PageResponse } from '@/core/api/api.types';
|
|
||||||
|
|
||||||
const BASE = '/api/wtm/timesheets';
|
|
||||||
|
|
||||||
export const timesheetService = {
|
|
||||||
getWeekly: (weekStart: string) =>
|
|
||||||
api.get<Timesheet>(`${BASE}/week`, { params: { weekStart } }),
|
|
||||||
|
|
||||||
saveEntry: (timesheetId: number, entry: Partial<TimesheetEntry>) =>
|
|
||||||
api.post<TimesheetEntry>(`${BASE}/${timesheetId}/entries`, entry),
|
|
||||||
|
|
||||||
saveBatch: (timesheetId: number, entries: Partial<TimesheetEntry>[]) =>
|
|
||||||
api.put<Timesheet>(`${BASE}/${timesheetId}/entries/batch`, entries),
|
|
||||||
|
|
||||||
deleteEntry: (timesheetId: number, entryId: number) =>
|
|
||||||
api.delete<void>(`${BASE}/${timesheetId}/entries/${entryId}`),
|
|
||||||
|
|
||||||
submit: (timesheetId: number) =>
|
|
||||||
api.post<Timesheet>(`${BASE}/${timesheetId}/submit`),
|
|
||||||
|
|
||||||
getHistory: (params: TimesheetFilter) =>
|
|
||||||
api.get<PageResponse<Timesheet>>(`${BASE}/history`, { params }),
|
|
||||||
|
|
||||||
uploadExcel: (file: File, weekStart: string) => {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('file', file);
|
|
||||||
form.append('weekStart', weekStart);
|
|
||||||
return api.post(`${BASE}/upload`, form, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
downloadTemplate: () =>
|
|
||||||
api.get(`${BASE}/upload/template`, { responseType: 'blob' }),
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 스토어 파일 (Pinia Composition API)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// modules/timesheet/timesheet.store.ts
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { timesheetService } from './timesheet.service';
|
|
||||||
import type { Timesheet, TimesheetEntry } from './timesheet.types';
|
|
||||||
|
|
||||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
|
||||||
// State
|
|
||||||
const current = ref<Timesheet | null>(null);
|
|
||||||
const loading = ref(false);
|
|
||||||
const saving = ref(false);
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
const totalHours = computed(() =>
|
|
||||||
current.value?.entries.reduce((sum, e) => sum + e.hours, 0) ?? 0,
|
|
||||||
);
|
|
||||||
const isOverWeeklyLimit = computed(() => totalHours.value > 52);
|
|
||||||
const canSubmit = computed(() =>
|
|
||||||
current.value?.status === 'DRAFT' || current.value?.status === 'REJECTED',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
async function fetchWeekly(weekStart: string) {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const { data } = await timesheetService.getWeekly(weekStart);
|
|
||||||
current.value = data;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveEntry(entry: Partial<TimesheetEntry>) {
|
|
||||||
if (!current.value) return;
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
await timesheetService.saveEntry(current.value.id, entry);
|
|
||||||
await fetchWeekly(current.value.weekStartDate);
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (!current.value) return;
|
|
||||||
await timesheetService.submit(current.value.id);
|
|
||||||
await fetchWeekly(current.value.weekStartDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function $reset() {
|
|
||||||
current.value = null;
|
|
||||||
loading.value = false;
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
current, loading, saving,
|
|
||||||
totalHours, isOverWeeklyLimit, canSubmit,
|
|
||||||
fetchWeekly, saveEntry, submit, $reset,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 타입 파일
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// modules/timesheet/timesheet.types.ts
|
|
||||||
export type TimesheetStatus = 'DRAFT' | 'SUBMITTED' | 'DL_APPROVED' | 'APPROVED' | 'REJECTED';
|
|
||||||
export type EntryType = 'NON_PROJECT' | 'OTHER_PROJECT' | 'EPC';
|
|
||||||
|
|
||||||
export interface Timesheet {
|
|
||||||
id: number;
|
|
||||||
userId: number;
|
|
||||||
weekStartDate: string; // ISO date
|
|
||||||
weekEndDate: string;
|
|
||||||
status: TimesheetStatus;
|
|
||||||
totalHours: number;
|
|
||||||
entries: TimesheetEntry[];
|
|
||||||
submittedAt: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimesheetEntry {
|
|
||||||
id: number;
|
|
||||||
entryType: EntryType;
|
|
||||||
entryDate: string;
|
|
||||||
hours: number;
|
|
||||||
npCategory?: string;
|
|
||||||
otherProjectId?: number;
|
|
||||||
otherCategory?: string;
|
|
||||||
epcProjectId?: number;
|
|
||||||
canonicalWbsId?: number;
|
|
||||||
tealEntryId?: number;
|
|
||||||
revisionNumber?: number;
|
|
||||||
remark?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimesheetFilter {
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
status?: TimesheetStatus;
|
|
||||||
skip?: number;
|
|
||||||
limit?: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 공용 API 인프라 (core/api)
|
|
||||||
|
|
||||||
### 5.1 Axios 인스턴스 + JWT 인터셉터
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// core/api/axios.ts
|
|
||||||
import axios from 'axios';
|
|
||||||
import type { InternalAxiosRequestConfig, AxiosError } from 'axios';
|
|
||||||
import { authService } from '@/core/auth/auth.service';
|
|
||||||
import router from '@/app/router';
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
||||||
timeout: 30000,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request: JWT 토큰 첨부
|
|
||||||
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
|
||||||
const token = authService.getAccessToken();
|
|
||||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Response: 401 시 토큰 갱신 + 재시도
|
|
||||||
let isRefreshing = false;
|
|
||||||
let failedQueue: Array<{ resolve: Function; reject: Function }> = [];
|
|
||||||
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
async (error: AxiosError) => {
|
|
||||||
const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
|
||||||
|
|
||||||
if (error.response?.status === 401 && !original._retry) {
|
|
||||||
if (isRefreshing) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
failedQueue.push({
|
|
||||||
resolve: (token: string) => {
|
|
||||||
original.headers.Authorization = `Bearer ${token}`;
|
|
||||||
resolve(api(original));
|
|
||||||
},
|
|
||||||
reject,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
original._retry = true;
|
|
||||||
isRefreshing = true;
|
|
||||||
try {
|
|
||||||
const newToken = await authService.refreshToken();
|
|
||||||
failedQueue.forEach((q) => q.resolve(newToken));
|
|
||||||
failedQueue = [];
|
|
||||||
original.headers.Authorization = `Bearer ${newToken}`;
|
|
||||||
return api(original);
|
|
||||||
} catch {
|
|
||||||
failedQueue.forEach((q) => q.reject(error));
|
|
||||||
failedQueue = [];
|
|
||||||
authService.clearTokens();
|
|
||||||
router.push({ name: 'login' });
|
|
||||||
return Promise.reject(error);
|
|
||||||
} finally {
|
|
||||||
isRefreshing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export default api;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 공용 응답 타입
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// core/api/api.types.ts
|
|
||||||
|
|
||||||
/** WBX 호환 목록 응답 (items + total) */
|
|
||||||
export interface PageResponse<T> {
|
|
||||||
items: T[];
|
|
||||||
total: number;
|
|
||||||
skip: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** WBX 호환 에러 응답 (detail 키) */
|
|
||||||
export interface ErrorResponse {
|
|
||||||
detail: string;
|
|
||||||
code?: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 라우터 조립 (app/router.ts)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// app/router.ts
|
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
|
||||||
import { authGuard } from '@/core/auth/auth.guard';
|
|
||||||
|
|
||||||
import { authRoutes } from '@/modules/auth/auth.routes';
|
|
||||||
import { dashboardRoutes } from '@/modules/dashboard/dashboard.routes';
|
|
||||||
import { userRoutes } from '@/modules/user/user.routes';
|
|
||||||
import { projectRoutes } from '@/modules/project/project.routes';
|
|
||||||
import { wbsRoutes } from '@/modules/wbs/wbs.routes';
|
|
||||||
import { tealRoutes } from '@/modules/teal/teal.routes';
|
|
||||||
import { timesheetRoutes } from '@/modules/timesheet/timesheet.routes';
|
|
||||||
import { approvalRoutes } from '@/modules/approval/approval.routes';
|
|
||||||
import { reportRoutes } from '@/modules/report/report.routes';
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
|
||||||
routes: [
|
|
||||||
// 인증 불필요 (레이아웃 없음)
|
|
||||||
...authRoutes,
|
|
||||||
|
|
||||||
// 인증 필요 (AppLayout 래핑)
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
component: () => import('@/core/components/AppLayout.vue'),
|
|
||||||
beforeEnter: authGuard,
|
|
||||||
redirect: '/dashboard',
|
|
||||||
children: [
|
|
||||||
...dashboardRoutes,
|
|
||||||
...userRoutes,
|
|
||||||
...projectRoutes,
|
|
||||||
...wbsRoutes,
|
|
||||||
...tealRoutes,
|
|
||||||
...timesheetRoutes,
|
|
||||||
...approvalRoutes,
|
|
||||||
...reportRoutes,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// 404
|
|
||||||
{
|
|
||||||
path: '/:pathMatch(.*)*',
|
|
||||||
name: 'not-found',
|
|
||||||
component: () => import('@/core/components/NotFoundView.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. PrimeVue 컴포넌트 매핑 (WTM 기능별)
|
|
||||||
|
|
||||||
| WTM 기능 | PrimeVue 컴포넌트 | 사용 위치 |
|
|
||||||
|----------|-------------------|----------|
|
|
||||||
| 사용자/프로젝트 CRUD | `DataTable`, `Column`, `Dialog` | UserTable, ProjectTable |
|
|
||||||
| WBS 트리 표시 | `TreeTable`, `Tree` | WbsTreeTable |
|
|
||||||
| 시수 입력 (주간 그리드) | `DataTable` (editable cells), `InputNumber` | TimesheetGrid |
|
|
||||||
| 시수 유형 탭 | `Tabs`, `TabList`, `Tab`, `TabPanels`, `TabPanel` | TimesheetTabPanel |
|
|
||||||
| 결재 이력 | `Timeline` | ApprovalTimeline |
|
|
||||||
| 대시보드 차트 | `Chart` (Chart.js 래퍼) | TimesheetChart, ProjectHoursChart |
|
|
||||||
| 파일 업로드 | `FileUpload` | WbsUploadDialog, TimesheetExcelUpload |
|
|
||||||
| 알림/토스트 | `Toast` | 전역 (useAppToast) |
|
|
||||||
| 삭제 확인 | `ConfirmDialog` | 전역 (useConfirmAction) |
|
|
||||||
| 상태 표시 | `Tag`, `Badge` | TimesheetStatusBadge, ProjectStatusTag |
|
|
||||||
| 날짜 선택 | `DatePicker` | TimesheetWeekPicker, ReportFilterPanel |
|
|
||||||
| 드롭다운 | `Select`, `MultiSelect`, `AutoComplete` | 프로젝트/WBS/TEAL 선택 |
|
|
||||||
| 폼 입력 | `InputText`, `InputNumber`, `Textarea` | 모든 FormDialog |
|
|
||||||
| 사이드바 메뉴 | `PanelMenu` | AppSidebar |
|
|
||||||
| 상단바 | `Menubar`, `Avatar` | AppTopbar |
|
|
||||||
| 로딩 상태 | `Skeleton`, `ProgressBar` | DataTable loading |
|
|
||||||
| 페이지 레이아웃 | `Card`, `Panel`, `Divider` | 전체 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 환경 변수
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env (기본값, 커밋)
|
|
||||||
VITE_APP_TITLE=WTM - Work Time Manager
|
|
||||||
|
|
||||||
# .env.development (로컬 개발, 커밋)
|
|
||||||
VITE_API_BASE_URL=http://localhost:8080
|
|
||||||
VITE_APP_ENV=development
|
|
||||||
|
|
||||||
# .env.production (운영, 커밋)
|
|
||||||
VITE_API_BASE_URL=https://wtmgr.hanwhaocean.com
|
|
||||||
VITE_APP_ENV=production
|
|
||||||
|
|
||||||
# .env.local (개인 오버라이드, gitignore)
|
|
||||||
VITE_API_BASE_URL=http://192.168.x.x:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/env.d.ts — 타입 안전 접근
|
|
||||||
/// <reference types="vite/client" />
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_API_BASE_URL: string;
|
|
||||||
readonly VITE_APP_TITLE: string;
|
|
||||||
readonly VITE_APP_ENV: 'development' | 'staging' | 'production';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. ESLint + Prettier 설정
|
|
||||||
|
|
||||||
### .prettierrc.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 100,
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"vueIndentScriptAndStyle": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### eslint.config.js (핵심 규칙)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Flat config (ESLint 9.x)
|
|
||||||
import js from '@eslint/js';
|
|
||||||
import tseslint from 'typescript-eslint';
|
|
||||||
import pluginVue from 'eslint-plugin-vue';
|
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
{ ignores: ['dist/', 'node_modules/'] },
|
|
||||||
js.configs.recommended,
|
|
||||||
...tseslint.configs.recommended,
|
|
||||||
...pluginVue.configs['flat/recommended'],
|
|
||||||
{
|
|
||||||
files: ['**/*.vue'],
|
|
||||||
languageOptions: { parserOptions: { parser: tseslint.parser } },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
||||||
'@typescript-eslint/consistent-type-imports': 'error',
|
|
||||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eslintConfigPrettier,
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 멀티모듈 연동 (Gradle 루트 + Vite)
|
|
||||||
|
|
||||||
### 최종 프로젝트 트리
|
|
||||||
|
|
||||||
```
|
|
||||||
WBX-Spring/
|
|
||||||
├── settings.gradle # include 'wbx-spring-core', 'wtm-api'
|
|
||||||
├── build.gradle # 공통 Java 설정
|
|
||||||
│
|
|
||||||
├── wbx-spring-core/ # 프레임워크 라이브러리
|
|
||||||
├── wtm-api/ # BE — Spring Boot 앱
|
|
||||||
│
|
|
||||||
└── wtm-frontend/ # FE — Vue 3 + PrimeVue 4
|
|
||||||
├── package.json
|
|
||||||
├── vite.config.ts
|
|
||||||
└── src/
|
|
||||||
├── app/
|
|
||||||
├── core/
|
|
||||||
├── modules/
|
|
||||||
└── assets/
|
|
||||||
```
|
|
||||||
|
|
||||||
### vite.config.ts (API 프록시)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 5173,
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:8080',
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
'vendor-vue': ['vue', 'vue-router', 'pinia'],
|
|
||||||
'vendor-primevue': ['primevue'],
|
|
||||||
'vendor-chart': ['chart.js'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. 개발 흐름 (Day 1 → 안정화)
|
|
||||||
|
|
||||||
```
|
|
||||||
Day 1 — 스캐폴딩 (리드)
|
|
||||||
├── npm create vite@latest wtm-frontend -- --template vue-ts
|
|
||||||
├── PrimeVue 4, Pinia, Vue Router, Axios, VueUse 설치
|
|
||||||
├── core/ 인프라 구성 (axios, auth guard, AppLayout)
|
|
||||||
├── ESLint + Prettier + .editorconfig 설정
|
|
||||||
└── git push → 팀 공유
|
|
||||||
|
|
||||||
Day 2 — 모듈 껍데기 (각 개발자)
|
|
||||||
├── 담당 모듈 폴더 생성 (6파일 패턴)
|
|
||||||
├── 빈 View + routes 등록
|
|
||||||
├── service + types 스텁 작성
|
|
||||||
└── 모듈별 PR → 머지
|
|
||||||
|
|
||||||
Day 3+ — 병렬 개발 (각자 모듈 내 자유 작업)
|
|
||||||
├── 풀스택 ① → project/ → wbs/ → teal/ → report/
|
|
||||||
├── 풀스택 ② → timesheet/ → approval/
|
|
||||||
├── 풀스택 ③ → auth/ → dashboard/ → user/
|
|
||||||
└── 리드 → core/ 개선, 코드 리뷰, BaseCrudTable 추출
|
|
||||||
|
|
||||||
W8 — 통합
|
|
||||||
├── 모듈 간 연동 테스트 (시수 → 결재 → 알림)
|
|
||||||
├── 반응형/접근성 검수
|
|
||||||
└── 번들 사이즈 최적화
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 네이밍 컨벤션
|
|
||||||
|
|
||||||
| 항목 | 규칙 | 예시 |
|
|
||||||
|------|------|------|
|
|
||||||
| Vue 컴포넌트 파일 | PascalCase | `TimesheetGrid.vue` |
|
|
||||||
| View 파일 | PascalCase + `View` 접미사 | `TimesheetWeekView.vue` |
|
|
||||||
| TS 파일 | kebab-case 또는 dot notation | `timesheet.service.ts` |
|
|
||||||
| Pinia 스토어 | `use{Module}Store` | `useTimesheetStore` |
|
|
||||||
| 라우트 name | kebab-case | `timesheet-week` |
|
|
||||||
| CSS 클래스 | kebab-case (BEM 선택) | `timesheet-grid__cell` |
|
|
||||||
| API 서비스 함수 | camelCase, CRUD 동사 | `getEntries`, `createEntry` |
|
|
||||||
| 타입/인터페이스 | PascalCase | `TimesheetEntry` |
|
|
||||||
| Enum/상수 | UPPER_SNAKE_CASE | `TIMESHEET_STATUS` |
|
|
||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Diff 로드
@@ -1,21 +1,48 @@
|
|||||||
package kr.co.accura.wtm.api;
|
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 lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WTM-specific auth endpoints that supplement wbx-spring-core's AuthController.
|
* WTM-specific auth endpoints that supplement wbx-spring-core's AuthController.
|
||||||
* <p>
|
* Overrides /me to include WTM role information from user_roles table.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WtmAuthController {
|
public class WtmAuthController {
|
||||||
|
|
||||||
|
private final UserRoleRepository userRoleRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내 정보 조회 — WTM 역할 포함 (별도 경로)
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/wtm/auth/me/profile")
|
||||||
|
public Map<String, Object> me(@AuthenticationPrincipal WbxUserDetails user) {
|
||||||
|
// wtm user_roles에서 역할 코드 조회 (email 매칭)
|
||||||
|
List<String> roles = userRoleRepository.findRoleCodesByUserEmail(user.getEmail());
|
||||||
|
if (roles.isEmpty() && user.isAdmin()) {
|
||||||
|
roles = List.of("SA");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> 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.
|
* SSO initiation — redirects to OAuth2 authorization endpoint.
|
||||||
* Requires Azure Entra ID configuration.
|
* Requires Azure Entra ID configuration.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package kr.co.accura.wtm.domain.user.repository;
|
|||||||
|
|
||||||
import kr.co.accura.wtm.domain.user.entity.UserRole;
|
import kr.co.accura.wtm.domain.user.entity.UserRole;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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 org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -14,4 +16,7 @@ public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
|
|||||||
List<UserRole> findByUser_IdAndProjectId(Long userId, Long projectId);
|
List<UserRole> findByUser_IdAndProjectId(Long userId, Long projectId);
|
||||||
|
|
||||||
void deleteByUser_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<String> findRoleCodesByUserEmail(@Param("email") String email);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async function loadPending() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const { data } = await approvalService.getPending();
|
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 {
|
} catch {
|
||||||
toast.add({ severity: 'error', summary: '오류', detail: '결재 대기 목록 로드 실패', life: 5000 });
|
toast.add({ severity: 'error', summary: '오류', detail: '결재 대기 목록 로드 실패', life: 5000 });
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const BASE = '/api/wtm/auth';
|
|||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login: (data: LoginRequest) => api.post<LoginResponse>(`${BASE}/login`, data),
|
login: (data: LoginRequest) => api.post<LoginResponse>(`${BASE}/login`, data),
|
||||||
me: () => api.get<AuthUser>(`${BASE}/me`),
|
me: () => api.get<AuthUser>(`${BASE}/me/profile`),
|
||||||
refresh: (refreshToken: string) => api.post(`${BASE}/refresh`, { refreshToken }),
|
refresh: (refreshToken: string) => api.post(`${BASE}/refresh`, { refreshToken }),
|
||||||
logout: () => api.post(`${BASE}/logout`),
|
logout: () => api.post(`${BASE}/logout`),
|
||||||
resetPassword: (email: string) => api.post(`${BASE}/password/reset`, { email }),
|
resetPassword: (email: string) => api.post(`${BASE}/password/reset`, { email }),
|
||||||
|
|||||||
새 Issue에서 참조
사용자 차단