feat: FE 화면 구현 완료 + 샘플 데이터 + 결재라인 연동

- WBS/TEAL 화면 실제 구현 (TreeTable, FileUpload, 버전관리)
- 시수이력/결재이력 화면 구현 (DataTable, Filter, Timeline)
- 비밀번호변경 화면 추가
- 로그인 snake_case 응답 매핑 수정
- Vite 프록시 8081 포트 수정
- auth guard에서 fetchMe 자동 호출
- V108 샘플 데이터 (10명 사용자, 4주 시수 215건, 결재 9건)
- 배너 추가 (WBX Spring)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
2026-03-25 22:17:32 +09:00
부모 df723f1d59
커밋 9707a6eeb1
33개의 변경된 파일2323개의 추가작업 그리고 20개의 파일을 삭제

파일 보기

@@ -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 로드