- 00~11: WTM 시수관리 시스템 설계 문서 (아키텍처, DB스키마, API스펙 등) - 12: BE 멀티프로젝트 플랫폼 구성 계획 (wbx-spring-core 라이브러리 전환) - 13: FE Vue3+PrimeVue4 모듈 기반 구조 계획 - 14: 레이아웃 표준 및 디자인 시스템 (반응형, 하드코딩 제거) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
423 줄
14 KiB
Markdown
423 줄
14 KiB
Markdown
# 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));
|
|
}
|
|
}
|
|
```
|