1856 lines
53 KiB
Markdown
1856 lines
53 KiB
Markdown
# WACE ERP Backend Architecture - 상세 분석 문서
|
|
|
|
> **작성일**: 2026-02-06
|
|
> **작성자**: Backend Specialist
|
|
> **목적**: WACE ERP 시스템 백엔드 전체 아키텍처 분석 및 워크플로우 문서화
|
|
|
|
---
|
|
|
|
## 📑 목차
|
|
|
|
1. [전체 개요](#1-전체-개요)
|
|
2. [디렉토리 구조](#2-디렉토리-구조)
|
|
3. [기술 스택](#3-기술-스택)
|
|
4. [미들웨어 스택](#4-미들웨어-스택)
|
|
5. [인증/인가 시스템](#5-인증인가-시스템)
|
|
6. [멀티테넌시 구현](#6-멀티테넌시-구현)
|
|
7. [API 라우트 전체 목록](#7-api-라우트-전체-목록)
|
|
8. [비즈니스 도메인별 모듈](#8-비즈니스-도메인별-모듈)
|
|
9. [데이터베이스 접근 방식](#9-데이터베이스-접근-방식)
|
|
10. [외부 시스템 연동](#10-외부-시스템-연동)
|
|
11. [배치/스케줄 처리](#11-배치스케줄-처리)
|
|
12. [파일 처리](#12-파일-처리)
|
|
13. [에러 핸들링](#13-에러-핸들링)
|
|
14. [로깅 시스템](#14-로깅-시스템)
|
|
15. [보안 및 권한 관리](#15-보안-및-권한-관리)
|
|
16. [성능 최적화](#16-성능-최적화)
|
|
|
|
---
|
|
|
|
## 1. 전체 개요
|
|
|
|
### 1.1 프로젝트 정보
|
|
- **프로젝트명**: WACE ERP Backend (Node.js)
|
|
- **언어**: TypeScript (Strict Mode)
|
|
- **런타임**: Node.js 20.10.0+
|
|
- **프레임워크**: Express.js
|
|
- **데이터베이스**: PostgreSQL (Raw Query 기반)
|
|
- **포트**: 8080 (기본값)
|
|
|
|
### 1.2 아키텍처 특징
|
|
1. **Layered Architecture**: Controller → Service → Database 3계층 구조
|
|
2. **Multi-tenancy**: company_code 기반 완전한 데이터 격리
|
|
3. **JWT 인증**: Stateless 토큰 기반 인증 시스템
|
|
4. **Raw Query**: ORM 없이 PostgreSQL 직접 쿼리 (성능 최적화)
|
|
5. **Connection Pool**: pg 라이브러리 기반 안정적인 연결 관리
|
|
6. **Type-Safe**: TypeScript 타입 시스템 적극 활용
|
|
|
|
### 1.3 주요 기능
|
|
- 관리자 기능 (사용자/권한/메뉴 관리)
|
|
- 테이블/화면 메타데이터 관리 (동적 화면 생성)
|
|
- 플로우 관리 (워크플로우 엔진)
|
|
- 데이터플로우 다이어그램 (ERD/관계도)
|
|
- 외부 DB 연동 (PostgreSQL, MySQL, MSSQL, Oracle)
|
|
- 외부 REST API 연동
|
|
- 배치 자동 실행 (Cron 스케줄러)
|
|
- 메일 발송/수신
|
|
- 파일 업로드/다운로드
|
|
- 다국어 지원
|
|
- 대시보드/리포트
|
|
|
|
---
|
|
|
|
## 2. 디렉토리 구조
|
|
|
|
```
|
|
backend-node/
|
|
├── src/
|
|
│ ├── app.ts # Express 앱 진입점
|
|
│ ├── config/ # 환경 설정
|
|
│ │ ├── environment.ts # 환경변수 관리
|
|
│ │ └── multerConfig.ts # 파일 업로드 설정
|
|
│ ├── controllers/ # 컨트롤러 (70+ 파일)
|
|
│ │ ├── authController.ts
|
|
│ │ ├── adminController.ts
|
|
│ │ ├── tableManagementController.ts
|
|
│ │ ├── flowController.ts
|
|
│ │ ├── dataflowController.ts
|
|
│ │ ├── batchController.ts
|
|
│ │ └── ...
|
|
│ ├── services/ # 비즈니스 로직 (80+ 파일)
|
|
│ │ ├── authService.ts
|
|
│ │ ├── adminService.ts
|
|
│ │ ├── tableManagementService.ts
|
|
│ │ ├── flowExecutionService.ts
|
|
│ │ ├── batchSchedulerService.ts
|
|
│ │ └── ...
|
|
│ ├── routes/ # API 라우터 (70+ 파일)
|
|
│ │ ├── authRoutes.ts
|
|
│ │ ├── adminRoutes.ts
|
|
│ │ ├── tableManagementRoutes.ts
|
|
│ │ ├── flowRoutes.ts
|
|
│ │ └── ...
|
|
│ ├── middleware/ # 미들웨어 (4개)
|
|
│ │ ├── authMiddleware.ts # JWT 인증
|
|
│ │ ├── permissionMiddleware.ts # 권한 체크
|
|
│ │ ├── superAdminMiddleware.ts # 슈퍼관리자 전용
|
|
│ │ └── errorHandler.ts # 에러 핸들러
|
|
│ ├── database/ # DB 연결
|
|
│ │ ├── db.ts # PostgreSQL Pool 관리
|
|
│ │ ├── DatabaseConnectorFactory.ts # 외부 DB 연결
|
|
│ │ └── runMigration.ts # 마이그레이션
|
|
│ ├── types/ # TypeScript 타입 정의 (26개)
|
|
│ │ ├── auth.ts
|
|
│ │ ├── batchTypes.ts
|
|
│ │ ├── flow.ts
|
|
│ │ └── ...
|
|
│ ├── utils/ # 유틸리티 함수
|
|
│ │ ├── jwtUtils.ts # JWT 토큰 관리
|
|
│ │ ├── permissionUtils.ts # 권한 체크
|
|
│ │ ├── logger.ts # Winston 로거
|
|
│ │ ├── encryptUtil.ts # 암호화/복호화
|
|
│ │ ├── passwordEncryption.ts # 비밀번호 암호화
|
|
│ │ └── ...
|
|
│ └── interfaces/ # 인터페이스
|
|
│ └── DatabaseConnector.ts # DB 커넥터 인터페이스
|
|
├── scripts/ # 스크립트
|
|
│ ├── dev/ # 개발 환경 스크립트
|
|
│ └── prod/ # 운영 환경 스크립트
|
|
├── data/ # 정적 데이터 (JSON)
|
|
├── uploads/ # 업로드된 파일
|
|
├── logs/ # 로그 파일
|
|
├── package.json # NPM 의존성
|
|
├── tsconfig.json # TypeScript 설정
|
|
└── .env # 환경변수
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 기술 스택
|
|
|
|
### 3.1 핵심 라이브러리
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"express": "^4.18.2", // 웹 프레임워크
|
|
"pg": "^8.16.3", // PostgreSQL 클라이언트
|
|
"jsonwebtoken": "^9.0.2", // JWT 토큰
|
|
"bcryptjs": "^2.4.3", // 비밀번호 암호화
|
|
"dotenv": "^16.3.1", // 환경변수
|
|
"cors": "^2.8.5", // CORS 처리
|
|
"helmet": "^7.1.0", // 보안 헤더
|
|
"compression": "^1.7.4", // Gzip 압축
|
|
"express-rate-limit": "^7.1.5", // Rate Limiting
|
|
"winston": "^3.11.0", // 로깅
|
|
"multer": "^1.4.5-lts.1", // 파일 업로드
|
|
"node-cron": "^4.2.1", // Cron 스케줄러
|
|
"axios": "^1.11.0", // HTTP 클라이언트
|
|
"nodemailer": "^6.10.1", // 메일 발송
|
|
"imap": "^0.8.19", // 메일 수신
|
|
"mysql2": "^3.15.0", // MySQL 연결
|
|
"mssql": "^11.0.1", // MSSQL 연결
|
|
"oracledb": "^6.9.0", // Oracle 연결
|
|
"uuid": "^13.0.0", // UUID 생성
|
|
"joi": "^17.11.0" // 데이터 검증
|
|
},
|
|
"devDependencies": {
|
|
"@types/express": "^4.17.21",
|
|
"@types/pg": "^8.15.5",
|
|
"typescript": "^5.3.3",
|
|
"nodemon": "^3.1.10",
|
|
"ts-node": "^10.9.2",
|
|
"jest": "^29.7.0",
|
|
"prettier": "^3.1.0",
|
|
"eslint": "^8.55.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.2 데이터베이스 연결
|
|
- **메인 DB**: PostgreSQL (pg 라이브러리)
|
|
- **외부 DB 지원**: MySQL, MSSQL, Oracle, PostgreSQL
|
|
- **Connection Pool**: Min 2~5 / Max 10~20
|
|
- **Timeout**: Connection 30s / Query 60s
|
|
|
|
---
|
|
|
|
## 4. 미들웨어 스택
|
|
|
|
### 4.1 미들웨어 실행 순서 (app.ts)
|
|
|
|
```typescript
|
|
// 1. 프로세스 레벨 예외 처리
|
|
process.on('unhandledRejection', ...)
|
|
process.on('uncaughtException', ...)
|
|
process.on('SIGTERM', ...)
|
|
process.on('SIGINT', ...)
|
|
|
|
// 2. 보안 미들웨어
|
|
app.use(helmet({
|
|
contentSecurityPolicy: { ... }, // CSP 설정
|
|
frameguard: { ... } // Iframe 보호
|
|
}))
|
|
|
|
// 3. 압축 미들웨어
|
|
app.use(compression())
|
|
|
|
// 4. Body Parser
|
|
app.use(express.json({ limit: '10mb' }))
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
|
|
|
|
// 5. 정적 파일 서빙 (/uploads)
|
|
app.use('/uploads', express.static(...))
|
|
|
|
// 6. CORS 설정
|
|
app.use(cors({
|
|
origin: [...], // 허용 도메인
|
|
credentials: true, // 쿠키 포함
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
|
|
}))
|
|
|
|
// 7. Rate Limiting (1분에 10000회)
|
|
app.use('/api/', limiter)
|
|
|
|
// 8. 토큰 자동 갱신 (1시간 이내 만료 시 갱신)
|
|
app.use('/api/', refreshTokenIfNeeded)
|
|
|
|
// 9. API 라우터 (70+개)
|
|
app.use('/api/auth', authRoutes)
|
|
app.use('/api/admin', adminRoutes)
|
|
// ...
|
|
|
|
// 10. 404 핸들러
|
|
app.use('*', notFoundHandler)
|
|
|
|
// 11. 에러 핸들러
|
|
app.use(errorHandler)
|
|
```
|
|
|
|
### 4.2 인증 미들웨어 체인
|
|
|
|
```typescript
|
|
// 기본 인증
|
|
authenticateToken → Controller
|
|
|
|
// 관리자 권한
|
|
authenticateToken → requireAdmin → Controller
|
|
|
|
// 슈퍼관리자 권한
|
|
authenticateToken → requireSuperAdmin → Controller
|
|
|
|
// 회사 데이터 접근
|
|
authenticateToken → requireCompanyAccess → Controller
|
|
|
|
// DDL 실행 권한
|
|
authenticateToken → requireDDLPermission → Controller
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 인증/인가 시스템
|
|
|
|
### 5.1 인증 플로우
|
|
|
|
```
|
|
┌──────────┐
|
|
│ 로그인 │
|
|
│ 요청 │
|
|
└────┬─────┘
|
|
│
|
|
▼
|
|
┌────────────────────┐
|
|
│ AuthController │
|
|
│ .login() │
|
|
└────┬───────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────┐
|
|
│ AuthService │
|
|
│ .processLogin() │
|
|
└────┬───────────────┘
|
|
│
|
|
├─► 1. loginPwdCheck() → DB에서 비밀번호 검증
|
|
│ (마스터 패스워드: qlalfqjsgh11)
|
|
│
|
|
├─► 2. getPersonBeanFromSession() → 사용자 정보 조회
|
|
│ (user_info, dept_info, company_mng JOIN)
|
|
│
|
|
├─► 3. insertLoginAccessLog() → 로그인 이력 저장
|
|
│
|
|
└─► 4. JwtUtils.generateToken() → JWT 토큰 생성
|
|
(payload: userId, userName, companyCode, userType)
|
|
```
|
|
|
|
### 5.2 JWT 토큰 구조
|
|
|
|
```typescript
|
|
// Payload
|
|
{
|
|
userId: "user123", // 사용자 ID
|
|
userName: "홍길동", // 사용자명
|
|
deptName: "개발팀", // 부서명
|
|
companyCode: "ILSHIN", // 회사 코드 (멀티테넌시 키)
|
|
companyName: "일신정공", // 회사명
|
|
userType: "COMPANY_ADMIN", // 권한 레벨
|
|
userTypeName: "회사관리자", // 권한명
|
|
iat: 1234567890, // 발급 시간
|
|
exp: 1234654290, // 만료 시간 (24시간)
|
|
iss: "PMS-System", // 발급자
|
|
aud: "PMS-Users" // 대상
|
|
}
|
|
```
|
|
|
|
### 5.3 권한 체계 (3단계)
|
|
|
|
```typescript
|
|
// 1. SUPER_ADMIN (최고 관리자)
|
|
- company_code = "*"
|
|
- userType = "SUPER_ADMIN"
|
|
- 전체 회사 데이터 접근 가능
|
|
- DDL 실행 가능
|
|
- 회사 생성/삭제 가능
|
|
- 시스템 설정 변경 가능
|
|
|
|
// 2. COMPANY_ADMIN (회사 관리자)
|
|
- company_code = "ILSHIN" (특정 회사)
|
|
- userType = "COMPANY_ADMIN"
|
|
- 자기 회사 데이터만 접근
|
|
- 자기 회사 사용자 관리 가능
|
|
- 회사 설정 변경 가능
|
|
|
|
// 3. USER (일반 사용자)
|
|
- company_code = "ILSHIN"
|
|
- userType = "USER" | "GUEST" | "PARTNER"
|
|
- 자기 회사 데이터만 접근
|
|
- 읽기/쓰기 권한만
|
|
```
|
|
|
|
### 5.4 토큰 갱신 메커니즘
|
|
|
|
```typescript
|
|
// refreshTokenIfNeeded 미들웨어
|
|
// 1. 토큰 만료까지 1시간 미만 남은 경우
|
|
// 2. 자동으로 새 토큰 발급
|
|
// 3. 응답 헤더에 "X-New-Token" 추가
|
|
// 4. 프론트엔드에서 자동으로 토큰 교체
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 멀티테넌시 구현
|
|
|
|
### 6.1 핵심 원칙
|
|
|
|
```typescript
|
|
// 🚨 절대 규칙: 모든 쿼리는 company_code 필터 필수
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
if (companyCode === "*") {
|
|
// 슈퍼관리자: 모든 데이터 조회 가능
|
|
query = "SELECT * FROM table ORDER BY company_code";
|
|
} else {
|
|
// 일반 사용자: 자기 회사 데이터만
|
|
query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'";
|
|
params = [companyCode];
|
|
}
|
|
```
|
|
|
|
### 6.2 회사 데이터 격리 패턴
|
|
|
|
```typescript
|
|
// ✅ 올바른 패턴
|
|
async function getDataList(req: AuthenticatedRequest) {
|
|
const companyCode = req.user!.companyCode; // JWT에서 추출
|
|
|
|
// 슈퍼관리자 체크
|
|
if (companyCode === "*") {
|
|
// 모든 회사 데이터 조회
|
|
return await query("SELECT * FROM data WHERE 1=1");
|
|
}
|
|
|
|
// 일반 사용자: 자기 회사 + 슈퍼관리자 데이터 제외
|
|
return await query(
|
|
"SELECT * FROM data WHERE company_code = $1 AND company_code != '*'",
|
|
[companyCode]
|
|
);
|
|
}
|
|
|
|
// ❌ 잘못된 패턴 (절대 금지!)
|
|
async function getDataList(req: AuthenticatedRequest) {
|
|
const companyCode = req.body.companyCode; // 클라이언트에서 받음 (위험!)
|
|
return await query("SELECT * FROM data WHERE company_code = $1", [companyCode]);
|
|
}
|
|
```
|
|
|
|
### 6.3 슈퍼관리자 숨김 규칙
|
|
|
|
```sql
|
|
-- 슈퍼관리자 사용자 (company_code = '*')는
|
|
-- 일반 회사 사용자에게 보이면 안 됨
|
|
|
|
-- ✅ 올바른 쿼리
|
|
SELECT * FROM user_info
|
|
WHERE company_code = $1
|
|
AND company_code != '*' -- 슈퍼관리자 숨김
|
|
|
|
-- ❌ 잘못된 쿼리
|
|
SELECT * FROM user_info
|
|
WHERE company_code = $1 -- 슈퍼관리자 노출 위험
|
|
```
|
|
|
|
### 6.4 회사 전환 기능 (SUPER_ADMIN 전용)
|
|
|
|
```typescript
|
|
// POST /api/auth/switch-company
|
|
// WACE 관리자가 특정 회사로 컨텍스트 전환
|
|
{
|
|
companyCode: "ILSHIN" // 전환할 회사 코드
|
|
}
|
|
|
|
// 새로운 JWT 토큰 발급 (company_code만 변경)
|
|
// userType은 "SUPER_ADMIN" 유지
|
|
```
|
|
|
|
---
|
|
|
|
## 7. API 라우트 전체 목록
|
|
|
|
### 7.1 인증/관리자 기능
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/auth/login` | POST | 로그인 | 공개 |
|
|
| `/api/auth/logout` | POST | 로그아웃 | 인증 |
|
|
| `/api/auth/me` | GET | 현재 사용자 정보 | 인증 |
|
|
| `/api/auth/status` | GET | 인증 상태 확인 | 공개 |
|
|
| `/api/auth/refresh` | POST | 토큰 갱신 | 인증 |
|
|
| `/api/auth/signup` | POST | 회원가입 (공차중계) | 공개 |
|
|
| `/api/auth/switch-company` | POST | 회사 전환 | 슈퍼관리자 |
|
|
| `/api/admin/users` | GET | 사용자 목록 | 관리자 |
|
|
| `/api/admin/users` | POST | 사용자 생성 | 관리자 |
|
|
| `/api/admin/menus` | GET | 메뉴 목록 | 인증 |
|
|
| `/api/admin/web-types` | GET | 웹타입 표준 관리 | 인증 |
|
|
| `/api/admin/button-actions` | GET | 버튼 액션 표준 | 인증 |
|
|
| `/api/admin/component-standards` | GET | 컴포넌트 표준 | 인증 |
|
|
| `/api/admin/template-standards` | GET | 템플릿 표준 | 인증 |
|
|
| `/api/admin/reports` | GET | 리포트 관리 | 인증 |
|
|
|
|
### 7.2 테이블/화면 관리
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/table-management/tables` | GET | 테이블 목록 | 인증 |
|
|
| `/api/table-management/tables/:table/columns` | GET | 컬럼 목록 | 인증 |
|
|
| `/api/table-management/tables/:table/data` | POST | 데이터 조회 | 인증 |
|
|
| `/api/table-management/tables/:table/add` | POST | 데이터 추가 | 인증 |
|
|
| `/api/table-management/tables/:table/edit` | PUT | 데이터 수정 | 인증 |
|
|
| `/api/table-management/tables/:table/delete` | DELETE | 데이터 삭제 | 인증 |
|
|
| `/api/table-management/tables/:table/log` | POST | 로그 테이블 생성 | 관리자 |
|
|
| `/api/table-management/multi-table-save` | POST | 다중 테이블 저장 | 인증 |
|
|
| `/api/screen-management/screens` | GET | 화면 목록 | 인증 |
|
|
| `/api/screen-management/screens/:id` | GET | 화면 상세 | 인증 |
|
|
| `/api/screen-management/screens` | POST | 화면 생성 | 관리자 |
|
|
| `/api/screen-groups` | GET | 화면 그룹 관리 | 인증 |
|
|
| `/api/screen-files` | GET | 화면 파일 관리 | 인증 |
|
|
|
|
### 7.3 플로우 관리
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/flow/definitions` | GET | 플로우 정의 목록 | 인증 |
|
|
| `/api/flow/definitions` | POST | 플로우 생성 | 인증 |
|
|
| `/api/flow/definitions/:id/steps` | GET | 단계 목록 | 인증 |
|
|
| `/api/flow/definitions/:id/steps` | POST | 단계 생성 | 인증 |
|
|
| `/api/flow/connections/:flowId` | GET | 연결 목록 | 인증 |
|
|
| `/api/flow/connections` | POST | 연결 생성 | 인증 |
|
|
| `/api/flow/move` | POST | 데이터 이동 (단건) | 인증 |
|
|
| `/api/flow/move-batch` | POST | 데이터 이동 (다건) | 인증 |
|
|
| `/api/flow/:flowId/step/:stepId/count` | GET | 단계 데이터 개수 | 인증 |
|
|
| `/api/flow/:flowId/step/:stepId/list` | GET | 단계 데이터 목록 | 인증 |
|
|
| `/api/flow/audit/:flowId/:recordId` | GET | 오딧 로그 조회 | 인증 |
|
|
|
|
### 7.4 데이터플로우/다이어그램
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/dataflow/relationships` | GET | 관계 목록 | 인증 |
|
|
| `/api/dataflow/relationships` | POST | 관계 생성 | 인증 |
|
|
| `/api/dataflow-diagrams` | GET | 다이어그램 목록 | 인증 |
|
|
| `/api/dataflow-diagrams/:id` | GET | 다이어그램 상세 | 인증 |
|
|
| `/api/dataflow` | POST | 데이터플로우 실행 | 인증 |
|
|
|
|
### 7.5 외부 연동
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/external-db-connections` | GET | 외부 DB 연결 목록 | 인증 |
|
|
| `/api/external-db-connections` | POST | 외부 DB 연결 생성 | 관리자 |
|
|
| `/api/external-db-connections/:id/test` | POST | 연결 테스트 | 인증 |
|
|
| `/api/external-db-connections/:id/tables` | GET | 테이블 목록 | 인증 |
|
|
| `/api/external-rest-api-connections` | GET | REST API 연결 목록 | 인증 |
|
|
| `/api/external-rest-api-connections` | POST | REST API 연결 생성 | 관리자 |
|
|
| `/api/external-rest-api-connections/:id/test` | POST | API 테스트 | 인증 |
|
|
| `/api/multi-connection/query` | POST | 멀티 DB 쿼리 | 인증 |
|
|
|
|
### 7.6 배치 관리
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/batch-configs` | GET | 배치 설정 목록 | 인증 |
|
|
| `/api/batch-configs` | POST | 배치 설정 생성 | 관리자 |
|
|
| `/api/batch-configs/:id` | PUT | 배치 설정 수정 | 관리자 |
|
|
| `/api/batch-configs/:id` | DELETE | 배치 설정 삭제 | 관리자 |
|
|
| `/api/batch-management/:id/execute` | POST | 배치 즉시 실행 | 관리자 |
|
|
| `/api/batch-execution-logs` | GET | 실행 이력 | 인증 |
|
|
|
|
### 7.7 메일 시스템
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/mail/accounts` | GET | 계정 목록 | 인증 |
|
|
| `/api/mail/templates-file` | GET | 템플릿 목록 | 인증 |
|
|
| `/api/mail/send` | POST | 메일 발송 | 인증 |
|
|
| `/api/mail/sent` | GET | 발송 이력 | 인증 |
|
|
| `/api/mail/receive` | POST | 메일 수신 | 인증 |
|
|
|
|
### 7.8 파일 관리
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/files/upload` | POST | 파일 업로드 | 인증 |
|
|
| `/api/files/download/:id` | GET | 파일 다운로드 | 인증 |
|
|
| `/api/files` | GET | 파일 목록 | 인증 |
|
|
| `/api/files/:id` | DELETE | 파일 삭제 | 인증 |
|
|
| `/uploads/:filename` | GET | 정적 파일 서빙 | 공개 |
|
|
|
|
### 7.9 기타 기능
|
|
|
|
| 경로 | 메서드 | 기능 | 권한 |
|
|
|------|--------|------|------|
|
|
| `/api/dashboards` | GET | 대시보드 데이터 | 인증 |
|
|
| `/api/common-codes` | GET | 공통코드 조회 | 인증 |
|
|
| `/api/multilang` | GET | 다국어 조회 | 인증 |
|
|
| `/api/company-management` | GET | 회사 목록 | 슈퍼관리자 |
|
|
| `/api/departments` | GET | 부서 목록 | 인증 |
|
|
| `/api/roles` | GET | 권한 그룹 관리 | 인증 |
|
|
| `/api/ddl` | POST | DDL 실행 | 슈퍼관리자 |
|
|
| `/api/open-api/weather` | GET | 날씨 정보 | 인증 |
|
|
| `/api/open-api/exchange` | GET | 환율 정보 | 인증 |
|
|
| `/api/digital-twin` | GET | 디지털 트윈 | 인증 |
|
|
| `/api/yard-layouts` | GET | 3D 필드 레이아웃 | 인증 |
|
|
| `/api/schedule` | POST | 스케줄 자동 생성 | 인증 |
|
|
| `/api/numbering-rules` | GET | 채번 규칙 관리 | 인증 |
|
|
| `/api/entity-search` | POST | 엔티티 검색 | 인증 |
|
|
| `/api/todos` | GET | To-Do 관리 | 인증 |
|
|
| `/api/bookings` | GET | 예약 요청 관리 | 인증 |
|
|
| `/api/risk-alerts` | GET | 리스크/알림 관리 | 인증 |
|
|
| `/health` | GET | 헬스 체크 | 공개 |
|
|
|
|
---
|
|
|
|
## 8. 비즈니스 도메인별 모듈
|
|
|
|
### 8.1 관리자 도메인 (Admin)
|
|
|
|
**컨트롤러**: `adminController.ts`
|
|
**서비스**: `adminService.ts`
|
|
**주요 기능**:
|
|
- 사용자 관리 (CRUD)
|
|
- 메뉴 관리 (트리 구조)
|
|
- 권한 그룹 관리
|
|
- 시스템 설정
|
|
- 사용자 이력 조회
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 사용자 목록 조회 (멀티테넌시 적용)
|
|
async getUserList(params) {
|
|
const companyCode = params.userCompanyCode;
|
|
|
|
// 슈퍼관리자: 모든 회사 사용자 조회
|
|
if (companyCode === "*") {
|
|
return await query("SELECT * FROM user_info WHERE 1=1");
|
|
}
|
|
|
|
// 일반 관리자: 자기 회사 사용자만 + 슈퍼관리자 숨김
|
|
return await query(
|
|
"SELECT * FROM user_info WHERE company_code = $1 AND company_code != '*'",
|
|
[companyCode]
|
|
);
|
|
}
|
|
```
|
|
|
|
### 8.2 테이블/화면 관리 도메인
|
|
|
|
**컨트롤러**: `tableManagementController.ts`, `screenManagementController.ts`
|
|
**서비스**: `tableManagementService.ts`, `screenManagementService.ts`
|
|
**주요 기능**:
|
|
- 테이블 메타데이터 관리 (컬럼 설정)
|
|
- 화면 정의 (JSON 기반 동적 화면)
|
|
- 화면 그룹 관리
|
|
- 테이블 로그 시스템
|
|
- 엔티티 관계 관리
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 테이블 데이터 조회 (페이징, 정렬, 필터)
|
|
async getTableData(tableName, filters, pagination) {
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
// 동적 WHERE 절 생성
|
|
const whereClauses = [`company_code = '${companyCode}'`];
|
|
|
|
// 필터 조건 추가
|
|
filters.forEach(filter => {
|
|
whereClauses.push(`${filter.column} ${filter.operator} '${filter.value}'`);
|
|
});
|
|
|
|
// 페이징 + 정렬
|
|
const sql = `
|
|
SELECT * FROM ${tableName}
|
|
WHERE ${whereClauses.join(' AND ')}
|
|
ORDER BY ${pagination.sortBy} ${pagination.sortOrder}
|
|
LIMIT ${pagination.limit} OFFSET ${pagination.offset}
|
|
`;
|
|
|
|
return await query(sql);
|
|
}
|
|
```
|
|
|
|
### 8.3 플로우 도메인 (Flow)
|
|
|
|
**컨트롤러**: `flowController.ts`
|
|
**서비스**: `flowExecutionService.ts`, `flowDefinitionService.ts`
|
|
**주요 기능**:
|
|
- 플로우 정의 (워크플로우 설계)
|
|
- 단계(Step) 관리
|
|
- 단계 간 연결(Connection) 관리
|
|
- 데이터 이동 (단건/다건)
|
|
- 조건부 이동
|
|
- 오딧 로그
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 데이터 이동 (단계 간 전환)
|
|
async moveData(flowId, fromStepId, toStepId, recordIds) {
|
|
return await transaction(async (client) => {
|
|
// 1. 연결 조건 확인
|
|
const connection = await getConnection(fromStepId, toStepId);
|
|
|
|
// 2. 조건 평가 (있으면)
|
|
if (connection.condition) {
|
|
const isValid = await evaluateCondition(connection.condition, recordIds);
|
|
if (!isValid) throw new Error("조건 불충족");
|
|
}
|
|
|
|
// 3. 데이터 이동
|
|
await client.query(
|
|
`UPDATE ${connection.targetTable}
|
|
SET flow_step_id = $1, updated_at = now()
|
|
WHERE id = ANY($2) AND company_code = $3`,
|
|
[toStepId, recordIds, companyCode]
|
|
);
|
|
|
|
// 4. 오딧 로그 기록
|
|
await insertAuditLog(flowId, recordIds, fromStepId, toStepId);
|
|
});
|
|
}
|
|
```
|
|
|
|
### 8.4 데이터플로우 도메인 (Dataflow)
|
|
|
|
**컨트롤러**: `dataflowController.ts`, `dataflowDiagramController.ts`
|
|
**서비스**: `dataflowService.ts`, `dataflowDiagramService.ts`
|
|
**주요 기능**:
|
|
- 테이블 관계 정의 (ERD)
|
|
- 다이어그램 관리 (시각화)
|
|
- 관계 실행 (조인 쿼리 자동 생성)
|
|
- 관계 검증
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 테이블 관계 실행 (동적 조인)
|
|
async executeRelationship(relationshipId) {
|
|
const rel = await getRelationship(relationshipId);
|
|
|
|
// 동적 조인 쿼리 생성
|
|
const sql = `
|
|
SELECT
|
|
a.*,
|
|
b.*
|
|
FROM ${rel.fromTableName} a
|
|
${rel.relationshipType === '1:N' ? 'LEFT JOIN' : 'INNER JOIN'}
|
|
${rel.toTableName} b
|
|
ON a.${rel.fromColumnName} = b.${rel.toColumnName}
|
|
WHERE a.company_code = $1
|
|
`;
|
|
|
|
return await query(sql, [companyCode]);
|
|
}
|
|
```
|
|
|
|
### 8.5 배치 도메인 (Batch)
|
|
|
|
**컨트롤러**: `batchController.ts`, `batchManagementController.ts`
|
|
**서비스**: `batchService.ts`, `batchSchedulerService.ts`
|
|
**주요 기능**:
|
|
- 배치 설정 관리
|
|
- Cron 스케줄링 (node-cron)
|
|
- 외부 DB → 내부 DB 데이터 동기화
|
|
- 컬럼 매핑
|
|
- 실행 이력 관리
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 배치 스케줄러 초기화 (서버 시작 시)
|
|
async initializeScheduler() {
|
|
const activeBatches = await getBatchConfigs({ is_active: 'Y' });
|
|
|
|
for (const batch of activeBatches) {
|
|
// Cron 스케줄 등록
|
|
const task = cron.schedule(batch.cron_schedule, async () => {
|
|
await executeBatchConfig(batch);
|
|
}, {
|
|
timezone: "Asia/Seoul"
|
|
});
|
|
|
|
scheduledTasks.set(batch.id, task);
|
|
}
|
|
}
|
|
|
|
// 배치 실행
|
|
async executeBatchConfig(config) {
|
|
// 1. 소스 DB에서 데이터 가져오기
|
|
const sourceData = await getSourceData(config.source_connection);
|
|
|
|
// 2. 컬럼 매핑 적용
|
|
const mappedData = applyColumnMapping(sourceData, config.batch_mappings);
|
|
|
|
// 3. 타겟 DB에 INSERT/UPDATE
|
|
await upsertToTarget(mappedData, config.target_table);
|
|
|
|
// 4. 실행 로그 기록
|
|
await logExecution(config.id, sourceData.length);
|
|
}
|
|
```
|
|
|
|
### 8.6 외부 연동 도메인 (External)
|
|
|
|
**컨트롤러**: `externalDbConnectionController.ts`, `externalRestApiConnectionController.ts`
|
|
**서비스**: `externalDbConnectionService.ts`, `dbConnectionManager.ts`
|
|
**주요 기능**:
|
|
- 외부 DB 연결 설정 (PostgreSQL, MySQL, MSSQL, Oracle)
|
|
- Connection Pool 관리
|
|
- 연결 테스트
|
|
- 외부 REST API 연결 설정
|
|
- API 프록시
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 외부 DB 연결 (Factory 패턴)
|
|
class DatabaseConnectorFactory {
|
|
static getConnector(dbType: string, config: any) {
|
|
switch (dbType) {
|
|
case 'POSTGRESQL':
|
|
return new PostgreSQLConnector(config);
|
|
case 'MYSQL':
|
|
return new MySQLConnector(config);
|
|
case 'MSSQL':
|
|
return new MSSQLConnector(config);
|
|
case 'ORACLE':
|
|
return new OracleConnector(config);
|
|
default:
|
|
throw new Error(`지원하지 않는 DB 타입: ${dbType}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 외부 DB 쿼리 실행
|
|
async executeExternalQuery(connectionId, sql) {
|
|
// 1. 연결 정보 조회 (암호화된 비밀번호 복호화)
|
|
const connection = await getConnection(connectionId);
|
|
connection.password = decrypt(connection.password);
|
|
|
|
// 2. 커넥터 생성
|
|
const connector = DatabaseConnectorFactory.getConnector(
|
|
connection.db_type,
|
|
connection
|
|
);
|
|
|
|
// 3. 연결 및 쿼리 실행
|
|
await connector.connect();
|
|
const result = await connector.query(sql);
|
|
await connector.disconnect();
|
|
|
|
return result;
|
|
}
|
|
```
|
|
|
|
### 8.7 메일 도메인 (Mail)
|
|
|
|
**컨트롤러**: `mailSendSimpleController.ts`, `mailReceiveBasicController.ts`
|
|
**서비스**: `mailSendSimpleService.ts`, `mailReceiveBasicService.ts`
|
|
**주요 기능**:
|
|
- 메일 계정 관리 (파일 기반)
|
|
- 메일 템플릿 관리
|
|
- 메일 발송 (nodemailer)
|
|
- 메일 수신 (IMAP)
|
|
- 발송 이력 관리
|
|
- 첨부파일 처리
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 메일 발송
|
|
async sendEmail(params) {
|
|
// 1. 계정 정보 조회
|
|
const account = await getMailAccount(params.accountId);
|
|
|
|
// 2. 템플릿 적용 (있으면)
|
|
let emailBody = params.body;
|
|
if (params.templateId) {
|
|
const template = await getTemplate(params.templateId);
|
|
emailBody = renderTemplate(template.content, params.variables);
|
|
}
|
|
|
|
// 3. nodemailer 전송
|
|
const transporter = nodemailer.createTransport({
|
|
host: account.smtp_host,
|
|
port: account.smtp_port,
|
|
secure: true,
|
|
auth: {
|
|
user: account.email,
|
|
pass: decrypt(account.password)
|
|
}
|
|
});
|
|
|
|
const result = await transporter.sendMail({
|
|
from: account.email,
|
|
to: params.to,
|
|
subject: params.subject,
|
|
html: emailBody,
|
|
attachments: params.attachments
|
|
});
|
|
|
|
// 4. 발송 이력 저장
|
|
await saveSentHistory(params, result);
|
|
}
|
|
```
|
|
|
|
### 8.8 파일 도메인 (File)
|
|
|
|
**컨트롤러**: `fileController.ts`, `screenFileController.ts`
|
|
**서비스**: `fileSystemManager.ts`
|
|
**주요 기능**:
|
|
- 파일 업로드 (multer)
|
|
- 파일 다운로드
|
|
- 파일 삭제
|
|
- 화면별 파일 관리
|
|
|
|
**핵심 로직**:
|
|
```typescript
|
|
// 파일 업로드
|
|
const upload = multer({
|
|
storage: multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const dir = `uploads/${companyCode}`;
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
cb(null, dir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const uniqueName = `${Date.now()}-${uuidv4()}-${file.originalname}`;
|
|
cb(null, uniqueName);
|
|
}
|
|
}),
|
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
|
fileFilter: (req, file, cb) => {
|
|
// 파일 타입 검증
|
|
const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx|xls|xlsx/;
|
|
const isValid = allowedTypes.test(file.mimetype);
|
|
cb(null, isValid);
|
|
}
|
|
});
|
|
|
|
// 파일 메타데이터 저장
|
|
async saveFileMetadata(file, userId, companyCode) {
|
|
return await query(
|
|
`INSERT INTO file_info (
|
|
file_name, file_path, file_size, mime_type,
|
|
company_code, created_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
|
[file.originalname, file.path, file.size, file.mimetype, companyCode, userId]
|
|
);
|
|
}
|
|
```
|
|
|
|
### 8.9 대시보드 도메인 (Dashboard)
|
|
|
|
**컨트롤러**: `DashboardController.ts`
|
|
**서비스**: `DashboardService.ts`
|
|
**주요 기능**:
|
|
- 대시보드 설정 관리
|
|
- 위젯 관리
|
|
- 통계 데이터 조회
|
|
- 차트 데이터 생성
|
|
|
|
---
|
|
|
|
## 9. 데이터베이스 접근 방식
|
|
|
|
### 9.1 Connection Pool 설정
|
|
|
|
```typescript
|
|
// database/db.ts
|
|
const pool = new Pool({
|
|
host: "localhost",
|
|
port: 5432,
|
|
database: "ilshin",
|
|
user: "postgres",
|
|
password: "postgres",
|
|
|
|
// Pool 설정
|
|
min: config.nodeEnv === "production" ? 5 : 2, // 최소 연결 수
|
|
max: config.nodeEnv === "production" ? 20 : 10, // 최대 연결 수
|
|
|
|
// Timeout 설정
|
|
connectionTimeoutMillis: 30000, // 연결 대기 30초
|
|
idleTimeoutMillis: 600000, // 유휴 연결 유지 10분
|
|
statement_timeout: 60000, // 쿼리 실행 60초
|
|
query_timeout: 60000,
|
|
|
|
// 연결 유지
|
|
keepAlive: true,
|
|
keepAliveInitialDelayMillis: 10000,
|
|
|
|
// Application Name
|
|
application_name: "WACE-PLM-Backend"
|
|
});
|
|
```
|
|
|
|
### 9.2 쿼리 실행 패턴
|
|
|
|
```typescript
|
|
// 1. 기본 쿼리 (다중 행)
|
|
const users = await query<User>(
|
|
'SELECT * FROM user_info WHERE company_code = $1',
|
|
[companyCode]
|
|
);
|
|
|
|
// 2. 단일 행 쿼리
|
|
const user = await queryOne<User>(
|
|
'SELECT * FROM user_info WHERE user_id = $1',
|
|
['user123']
|
|
);
|
|
|
|
// 3. 트랜잭션
|
|
const result = await transaction(async (client) => {
|
|
await client.query('INSERT INTO table1 (...) VALUES (...)', [...]);
|
|
await client.query('INSERT INTO table2 (...) VALUES (...)', [...]);
|
|
return { success: true };
|
|
});
|
|
```
|
|
|
|
### 9.3 Parameterized Query (SQL Injection 방지)
|
|
|
|
```typescript
|
|
// ✅ 올바른 방법 (Parameterized Query)
|
|
const users = await query(
|
|
'SELECT * FROM user_info WHERE user_id = $1 AND dept_code = $2',
|
|
[userId, deptCode]
|
|
);
|
|
|
|
// ❌ 잘못된 방법 (SQL Injection 위험!)
|
|
const users = await query(
|
|
`SELECT * FROM user_info WHERE user_id = '${userId}'`
|
|
);
|
|
```
|
|
|
|
### 9.4 동적 쿼리 빌더 패턴
|
|
|
|
```typescript
|
|
// utils/queryBuilder.ts
|
|
class QueryBuilder {
|
|
private table: string;
|
|
private whereClauses: string[] = [];
|
|
private params: any[] = [];
|
|
private paramIndex: number = 1;
|
|
|
|
constructor(table: string) {
|
|
this.table = table;
|
|
}
|
|
|
|
where(column: string, value: any) {
|
|
this.whereClauses.push(`${column} = $${this.paramIndex++}`);
|
|
this.params.push(value);
|
|
return this;
|
|
}
|
|
|
|
whereIn(column: string, values: any[]) {
|
|
this.whereClauses.push(`${column} = ANY($${this.paramIndex++})`);
|
|
this.params.push(values);
|
|
return this;
|
|
}
|
|
|
|
build() {
|
|
const where = this.whereClauses.length > 0
|
|
? `WHERE ${this.whereClauses.join(' AND ')}`
|
|
: '';
|
|
return {
|
|
sql: `SELECT * FROM ${this.table} ${where}`,
|
|
params: this.params
|
|
};
|
|
}
|
|
}
|
|
|
|
// 사용 예시
|
|
const { sql, params } = new QueryBuilder('user_info')
|
|
.where('company_code', companyCode)
|
|
.where('user_type', 'USER')
|
|
.whereIn('dept_code', ['D001', 'D002'])
|
|
.build();
|
|
|
|
const users = await query(sql, params);
|
|
```
|
|
|
|
---
|
|
|
|
## 10. 외부 시스템 연동
|
|
|
|
### 10.1 외부 DB 연결 아키텍처
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ Backend Service │
|
|
└──────────┬──────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────┐
|
|
│ DatabaseConnector │
|
|
│ Factory │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌─────┴─────┬─────────┬─────────┐
|
|
▼ ▼ ▼ ▼
|
|
┌─────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
|
|
│PostgreSQL│ │ MySQL │ │ MSSQL │ │ Oracle │
|
|
│Connector│ │Connector│ │Connect│ │Connect │
|
|
└─────────┘ └─────────┘ └───────┘ └────────┘
|
|
│ │ │ │
|
|
▼ ▼ ▼ ▼
|
|
┌─────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
|
|
│External │ │External │ │External│ │External│
|
|
│ PG DB │ │MySQL DB │ │MSSQL DB│ │Oracle │
|
|
└─────────┘ └─────────┘ └───────┘ └────────┘
|
|
```
|
|
|
|
### 10.2 외부 DB Connector 인터페이스
|
|
|
|
```typescript
|
|
// interfaces/DatabaseConnector.ts
|
|
interface DatabaseConnector {
|
|
connect(): Promise<void>;
|
|
disconnect(): Promise<void>;
|
|
query(sql: string, params?: any[]): Promise<any[]>;
|
|
testConnection(): Promise<boolean>;
|
|
getTables(): Promise<string[]>;
|
|
getColumns(tableName: string): Promise<ColumnInfo[]>;
|
|
}
|
|
|
|
// 구현 예시: PostgreSQL
|
|
class PostgreSQLConnector implements DatabaseConnector {
|
|
private pool: Pool;
|
|
|
|
constructor(config: ExternalDbConnection) {
|
|
this.pool = new Pool({
|
|
host: config.host,
|
|
port: config.port,
|
|
database: config.database,
|
|
user: config.username,
|
|
password: decrypt(config.password),
|
|
ssl: config.use_ssl ? { rejectUnauthorized: false } : false
|
|
});
|
|
}
|
|
|
|
async connect() {
|
|
await this.pool.connect();
|
|
}
|
|
|
|
async query(sql: string, params?: any[]) {
|
|
const result = await this.pool.query(sql, params);
|
|
return result.rows;
|
|
}
|
|
|
|
async getTables() {
|
|
const result = await this.query(
|
|
`SELECT tablename FROM pg_tables
|
|
WHERE schemaname = 'public'
|
|
ORDER BY tablename`
|
|
);
|
|
return result.map(row => row.tablename);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 10.3 외부 REST API 연동
|
|
|
|
```typescript
|
|
// services/externalRestApiConnectionService.ts
|
|
async callExternalApi(connectionId: number, endpoint: string, options: any) {
|
|
// 1. 연결 정보 조회
|
|
const connection = await getApiConnection(connectionId);
|
|
|
|
// 2. 인증 헤더 생성
|
|
const headers: any = {
|
|
'Content-Type': 'application/json',
|
|
...connection.headers
|
|
};
|
|
|
|
if (connection.auth_type === 'BEARER') {
|
|
headers['Authorization'] = `Bearer ${decrypt(connection.auth_token)}`;
|
|
} else if (connection.auth_type === 'API_KEY') {
|
|
headers[connection.api_key_header] = decrypt(connection.api_key);
|
|
}
|
|
|
|
// 3. Axios 요청
|
|
const response = await axios({
|
|
method: options.method || 'GET',
|
|
url: `${connection.base_url}${endpoint}`,
|
|
headers,
|
|
data: options.body,
|
|
timeout: connection.timeout || 30000
|
|
});
|
|
|
|
return response.data;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. 배치/스케줄 처리
|
|
|
|
### 11.1 배치 스케줄러 시스템
|
|
|
|
```typescript
|
|
// services/batchSchedulerService.ts
|
|
class BatchSchedulerService {
|
|
private static scheduledTasks: Map<number, ScheduledTask> = new Map();
|
|
|
|
// 서버 시작 시 모든 활성 배치 스케줄링
|
|
static async initializeScheduler() {
|
|
const activeBatches = await getBatchConfigs({ is_active: 'Y' });
|
|
|
|
for (const batch of activeBatches) {
|
|
this.scheduleBatch(batch);
|
|
}
|
|
}
|
|
|
|
// 개별 배치 스케줄 등록
|
|
static scheduleBatch(config: any) {
|
|
if (!cron.validate(config.cron_schedule)) {
|
|
throw new Error(`Invalid cron: ${config.cron_schedule}`);
|
|
}
|
|
|
|
const task = cron.schedule(
|
|
config.cron_schedule,
|
|
async () => {
|
|
await this.executeBatchConfig(config);
|
|
},
|
|
{ timezone: "Asia/Seoul" }
|
|
);
|
|
|
|
this.scheduledTasks.set(config.id, task);
|
|
}
|
|
|
|
// 배치 실행
|
|
static async executeBatchConfig(config: any) {
|
|
const startTime = new Date();
|
|
|
|
try {
|
|
// 1. 소스 DB에서 데이터 조회
|
|
const sourceData = await this.getSourceData(config);
|
|
|
|
// 2. 컬럼 매핑 적용
|
|
const mappedData = this.applyMapping(sourceData, config.batch_mappings);
|
|
|
|
// 3. 타겟 DB에 저장
|
|
await this.upsertToTarget(mappedData, config);
|
|
|
|
// 4. 성공 로그
|
|
await BatchExecutionLogService.updateExecutionLog(executionLogId, {
|
|
execution_status: 'SUCCESS',
|
|
end_time: new Date(),
|
|
success_records: mappedData.length
|
|
});
|
|
} catch (error) {
|
|
// 5. 실패 로그
|
|
await BatchExecutionLogService.updateExecutionLog(executionLogId, {
|
|
execution_status: 'FAILURE',
|
|
end_time: new Date(),
|
|
error_message: error.message
|
|
});
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 11.2 Cron 표현식 예시
|
|
|
|
```
|
|
# 형식: 초 분 시 일 월 요일
|
|
|
|
# 매 시간 정각
|
|
0 * * * *
|
|
|
|
# 매일 새벽 2시
|
|
0 2 * * *
|
|
|
|
# 매주 월요일 오전 9시
|
|
0 9 * * 1
|
|
|
|
# 매월 1일 자정
|
|
0 0 1 * *
|
|
|
|
# 5분마다
|
|
*/5 * * * *
|
|
|
|
# 평일 오전 8시~오후 6시, 매 시간
|
|
0 8-18 * * 1-5
|
|
```
|
|
|
|
### 11.3 배치 실행 이력 관리
|
|
|
|
```typescript
|
|
// services/batchExecutionLogService.ts
|
|
interface ExecutionLog {
|
|
id: number;
|
|
batch_config_id: number;
|
|
execution_status: 'RUNNING' | 'SUCCESS' | 'FAILURE';
|
|
start_time: Date;
|
|
end_time?: Date;
|
|
total_records: number;
|
|
success_records: number;
|
|
failure_records: number;
|
|
error_message?: string;
|
|
company_code: string;
|
|
}
|
|
|
|
// 실행 로그 저장
|
|
async createExecutionLog(data: Partial<ExecutionLog>) {
|
|
return await query(
|
|
`INSERT INTO batch_execution_logs (
|
|
batch_config_id, company_code, execution_status,
|
|
start_time, total_records
|
|
) VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
|
[data.batch_config_id, data.company_code, data.execution_status,
|
|
data.start_time, data.total_records]
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. 파일 처리
|
|
|
|
### 12.1 파일 업로드 흐름
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ Frontend │
|
|
└──────┬──────┘
|
|
│ (FormData)
|
|
▼
|
|
┌─────────────────────┐
|
|
│ Multer Middleware │ → 파일 저장 (uploads/COMPANY_CODE/)
|
|
└──────┬──────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────┐
|
|
│ FileController │ → 메타데이터 저장 (file_info 테이블)
|
|
└──────┬──────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────┐
|
|
│ Response │ → { fileId, fileName, filePath, fileSize }
|
|
└─────────────────────┘
|
|
```
|
|
|
|
### 12.2 Multer 설정
|
|
|
|
```typescript
|
|
// config/multerConfig.ts
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const uploadDir = path.join(process.cwd(), 'uploads', companyCode);
|
|
|
|
// 디렉토리 없으면 생성
|
|
if (!fs.existsSync(uploadDir)) {
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
}
|
|
|
|
cb(null, uploadDir);
|
|
},
|
|
|
|
filename: (req, file, cb) => {
|
|
// 파일명 중복 방지: 타임스탬프 + UUID + 원본파일명
|
|
const timestamp = Date.now();
|
|
const uniqueId = uuidv4();
|
|
const extension = path.extname(file.originalname);
|
|
const basename = path.basename(file.originalname, extension);
|
|
const uniqueName = `${timestamp}-${uniqueId}-${basename}${extension}`;
|
|
|
|
cb(null, uniqueName);
|
|
}
|
|
});
|
|
|
|
export const upload = multer({
|
|
storage,
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024 // 10MB
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
// 허용 확장자 검증
|
|
const allowedMimeTypes = [
|
|
'image/jpeg', 'image/png', 'image/gif',
|
|
'application/pdf',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
];
|
|
|
|
if (allowedMimeTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('허용되지 않는 파일 형식입니다.'));
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
### 12.3 파일 다운로드
|
|
|
|
```typescript
|
|
// controllers/fileController.ts
|
|
async downloadFile(req: AuthenticatedRequest, res: Response) {
|
|
const fileId = parseInt(req.params.id);
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
// 1. 파일 정보 조회 (권한 체크)
|
|
const file = await queryOne(
|
|
`SELECT * FROM file_info
|
|
WHERE id = $1 AND company_code = $2`,
|
|
[fileId, companyCode]
|
|
);
|
|
|
|
if (!file) {
|
|
return res.status(404).json({ error: '파일을 찾을 수 없습니다.' });
|
|
}
|
|
|
|
// 2. 파일 존재 확인
|
|
if (!fs.existsSync(file.file_path)) {
|
|
return res.status(404).json({ error: '파일이 존재하지 않습니다.' });
|
|
}
|
|
|
|
// 3. 파일 다운로드
|
|
res.download(file.file_path, file.file_name);
|
|
}
|
|
```
|
|
|
|
### 12.4 파일 삭제 (논리 삭제)
|
|
|
|
```typescript
|
|
async deleteFile(req: AuthenticatedRequest, res: Response) {
|
|
const fileId = parseInt(req.params.id);
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
// 1. 논리 삭제 (is_active = 'N')
|
|
await query(
|
|
`UPDATE file_info
|
|
SET is_active = 'N', deleted_at = now(), deleted_by = $3
|
|
WHERE id = $1 AND company_code = $2`,
|
|
[fileId, companyCode, req.user!.userId]
|
|
);
|
|
|
|
// 2. 물리 파일은 삭제하지 않음 (복구 가능)
|
|
// 주기적으로 삭제된 지 30일 지난 파일만 물리 삭제
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 13. 에러 핸들링
|
|
|
|
### 13.1 에러 핸들러 미들웨어
|
|
|
|
```typescript
|
|
// middleware/errorHandler.ts
|
|
export const errorHandler = (err, req, res, next) => {
|
|
let error = { ...err };
|
|
error.message = err.message;
|
|
|
|
// 1. PostgreSQL 에러 처리
|
|
if (err.code) {
|
|
switch (err.code) {
|
|
case '23505': // unique_violation
|
|
error = new AppError('중복된 데이터가 존재합니다.', 400);
|
|
break;
|
|
case '23503': // foreign_key_violation
|
|
error = new AppError('참조 무결성 제약 조건 위반입니다.', 400);
|
|
break;
|
|
case '23502': // not_null_violation
|
|
error = new AppError('필수 입력값이 누락되었습니다.', 400);
|
|
break;
|
|
default:
|
|
error = new AppError(`데이터베이스 오류: ${err.message}`, 500);
|
|
}
|
|
}
|
|
|
|
// 2. JWT 에러 처리
|
|
if (err.name === 'JsonWebTokenError') {
|
|
error = new AppError('유효하지 않은 토큰입니다.', 401);
|
|
}
|
|
if (err.name === 'TokenExpiredError') {
|
|
error = new AppError('토큰이 만료되었습니다.', 401);
|
|
}
|
|
|
|
// 3. 에러 로깅
|
|
logger.error({
|
|
message: error.message,
|
|
stack: error.stack,
|
|
url: req.url,
|
|
method: req.method,
|
|
ip: req.ip
|
|
});
|
|
|
|
// 4. 응답
|
|
const statusCode = error.statusCode || 500;
|
|
res.status(statusCode).json({
|
|
success: false,
|
|
error: {
|
|
message: error.message,
|
|
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
|
|
}
|
|
});
|
|
};
|
|
```
|
|
|
|
### 13.2 커스텀 에러 클래스
|
|
|
|
```typescript
|
|
export class AppError extends Error {
|
|
public statusCode: number;
|
|
public isOperational: boolean;
|
|
|
|
constructor(message: string, statusCode: number = 500) {
|
|
super(message);
|
|
this.statusCode = statusCode;
|
|
this.isOperational = true;
|
|
Error.captureStackTrace(this, this.constructor);
|
|
}
|
|
}
|
|
|
|
// 사용 예시
|
|
if (!user) {
|
|
throw new AppError('사용자를 찾을 수 없습니다.', 404);
|
|
}
|
|
```
|
|
|
|
### 13.3 프로세스 레벨 예외 처리
|
|
|
|
```typescript
|
|
// app.ts
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
logger.error('⚠️ Unhandled Promise Rejection:', {
|
|
reason: reason?.message || reason,
|
|
stack: reason?.stack
|
|
});
|
|
// 프로세스 종료하지 않고 로깅만 수행
|
|
});
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
logger.error('🔥 Uncaught Exception:', {
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
// 예외 발생 후에도 서버 유지 (주의: 불안정할 수 있음)
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
logger.info('📴 SIGTERM 시그널 수신, graceful shutdown 시작...');
|
|
// 연결 풀 정리, 진행 중인 요청 완료 대기
|
|
process.exit(0);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 14. 로깅 시스템
|
|
|
|
### 14.1 Winston Logger 설정
|
|
|
|
```typescript
|
|
// utils/logger.ts
|
|
import winston from 'winston';
|
|
|
|
const logger = winston.createLogger({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
format: winston.format.combine(
|
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.splat(),
|
|
winston.format.json()
|
|
),
|
|
transports: [
|
|
// 파일 로그
|
|
new winston.transports.File({
|
|
filename: 'logs/error.log',
|
|
level: 'error',
|
|
maxsize: 10485760, // 10MB
|
|
maxFiles: 5
|
|
}),
|
|
new winston.transports.File({
|
|
filename: 'logs/combined.log',
|
|
maxsize: 10485760,
|
|
maxFiles: 10
|
|
}),
|
|
|
|
// 콘솔 로그 (개발 환경)
|
|
...(process.env.NODE_ENV === 'development' ? [
|
|
new winston.transports.Console({
|
|
format: winston.format.combine(
|
|
winston.format.colorize(),
|
|
winston.format.simple()
|
|
)
|
|
})
|
|
] : [])
|
|
]
|
|
});
|
|
|
|
export { logger };
|
|
```
|
|
|
|
### 14.2 로깅 레벨
|
|
|
|
```typescript
|
|
// 로그 레벨 (우선순위 높음 → 낮음)
|
|
logger.error('에러 발생', { error }); // 0
|
|
logger.warn('경고 메시지'); // 1
|
|
logger.info('정보 메시지'); // 2
|
|
logger.http('HTTP 요청'); // 3
|
|
logger.verbose('상세 정보'); // 4
|
|
logger.debug('디버그 정보', { data }); // 5
|
|
logger.silly('매우 상세한 정보'); // 6
|
|
```
|
|
|
|
### 14.3 로깅 패턴
|
|
|
|
```typescript
|
|
// 1. 인증 로그
|
|
logger.info(`인증 성공: ${userInfo.userId} (${req.ip})`);
|
|
logger.warn(`인증 실패: ${errorMessage} (${req.ip})`);
|
|
|
|
// 2. 쿼리 로그 (디버그 모드)
|
|
if (config.debug) {
|
|
logger.debug('쿼리 실행:', {
|
|
query: sql,
|
|
params,
|
|
rowCount: result.rowCount,
|
|
duration: `${duration}ms`
|
|
});
|
|
}
|
|
|
|
// 3. 에러 로그
|
|
logger.error('배치 실행 실패:', {
|
|
batchId: config.id,
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
|
|
// 4. 비즈니스 로그
|
|
logger.info(`플로우 데이터 이동: ${fromStepId} → ${toStepId}`, {
|
|
flowId,
|
|
recordCount: recordIds.length
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 15. 보안 및 권한 관리
|
|
|
|
### 15.1 비밀번호 암호화
|
|
|
|
```typescript
|
|
// utils/encryptUtil.ts
|
|
export class EncryptUtil {
|
|
// 비밀번호 해싱 (bcrypt)
|
|
static hash(password: string): string {
|
|
return bcrypt.hashSync(password, 12);
|
|
}
|
|
|
|
// 비밀번호 검증
|
|
static matches(plainPassword: string, hashedPassword: string): boolean {
|
|
return bcrypt.compareSync(plainPassword, hashedPassword);
|
|
}
|
|
}
|
|
|
|
// 사용 예시
|
|
const hashedPassword = EncryptUtil.hash('mypassword');
|
|
const isValid = EncryptUtil.matches('mypassword', hashedPassword);
|
|
```
|
|
|
|
### 15.2 민감 정보 암호화 (AES-256)
|
|
|
|
```typescript
|
|
// utils/credentialEncryption.ts
|
|
import crypto from 'crypto';
|
|
|
|
const ALGORITHM = 'aes-256-cbc';
|
|
const SECRET_KEY = process.env.ENCRYPTION_KEY || 'default-32-char-secret-key!!!!';
|
|
const IV_LENGTH = 16;
|
|
|
|
// 암호화
|
|
export function encrypt(text: string): string {
|
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(SECRET_KEY), iv);
|
|
|
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
|
|
return iv.toString('hex') + ':' + encrypted;
|
|
}
|
|
|
|
// 복호화
|
|
export function decrypt(encryptedText: string): string {
|
|
const parts = encryptedText.split(':');
|
|
const iv = Buffer.from(parts[0], 'hex');
|
|
const encryptedData = parts[1];
|
|
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(SECRET_KEY), iv);
|
|
|
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
|
|
return decrypted;
|
|
}
|
|
|
|
// 사용 예시: 외부 DB 비밀번호 저장
|
|
const encryptedPassword = encrypt('db_password');
|
|
await query(
|
|
'INSERT INTO external_db_connections (password) VALUES ($1)',
|
|
[encryptedPassword]
|
|
);
|
|
|
|
// 사용 시 복호화
|
|
const connection = await queryOne('SELECT * FROM external_db_connections WHERE id = $1', [id]);
|
|
const plainPassword = decrypt(connection.password);
|
|
```
|
|
|
|
### 15.3 SQL Injection 방지
|
|
|
|
```typescript
|
|
// ✅ Parameterized Query (항상 사용)
|
|
const users = await query(
|
|
'SELECT * FROM user_info WHERE user_id = $1 AND company_code = $2',
|
|
[userId, companyCode]
|
|
);
|
|
|
|
// ❌ 문자열 연결 (절대 사용 금지!)
|
|
const users = await query(
|
|
`SELECT * FROM user_info WHERE user_id = '${userId}'`
|
|
);
|
|
```
|
|
|
|
### 15.4 Rate Limiting
|
|
|
|
```typescript
|
|
// app.ts
|
|
const limiter = rateLimit({
|
|
windowMs: 1 * 60 * 1000, // 1분
|
|
max: 10000, // 최대 10000회 (개발: 완화, 운영: 100)
|
|
message: {
|
|
error: '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'
|
|
},
|
|
skip: (req) => {
|
|
// 헬스 체크는 Rate Limiting 제외
|
|
return req.path === '/health';
|
|
}
|
|
});
|
|
|
|
app.use('/api/', limiter);
|
|
```
|
|
|
|
### 15.5 CORS 설정
|
|
|
|
```typescript
|
|
// config/environment.ts
|
|
const getCorsOrigin = () => {
|
|
// 개발 환경: 모든 origin 허용
|
|
if (process.env.NODE_ENV === 'development') {
|
|
return true;
|
|
}
|
|
|
|
// 운영 환경: 허용 도메인만
|
|
return [
|
|
'http://localhost:9771',
|
|
'http://39.117.244.52:5555',
|
|
'https://v1.vexplor.com',
|
|
'https://api.vexplor.com'
|
|
];
|
|
};
|
|
|
|
// app.ts
|
|
app.use(cors({
|
|
origin: getCorsOrigin(),
|
|
credentials: true, // 쿠키 포함
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
|
|
}));
|
|
```
|
|
|
|
---
|
|
|
|
## 16. 성능 최적화
|
|
|
|
### 16.1 Connection Pool 모니터링
|
|
|
|
```typescript
|
|
// database/db.ts
|
|
// 5분마다 Pool 상태 체크
|
|
setInterval(() => {
|
|
if (pool) {
|
|
const status = {
|
|
totalCount: pool.totalCount, // 전체 연결 수
|
|
idleCount: pool.idleCount, // 유휴 연결 수
|
|
waitingCount: pool.waitingCount // 대기 중인 연결 수
|
|
};
|
|
|
|
// 대기 연결이 5개 이상이면 경고
|
|
if (status.waitingCount > 5) {
|
|
console.warn('⚠️ PostgreSQL 연결 풀 대기열 증가:', status);
|
|
}
|
|
}
|
|
}, 5 * 60 * 1000);
|
|
```
|
|
|
|
### 16.2 쿼리 실행 시간 로깅
|
|
|
|
```typescript
|
|
// database/db.ts
|
|
export async function query(text: string, params?: any[]) {
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
const startTime = Date.now();
|
|
const result = await client.query(text, params);
|
|
const duration = Date.now() - startTime;
|
|
|
|
// 1초 이상 걸린 쿼리는 경고
|
|
if (duration > 1000) {
|
|
logger.warn('⚠️ 느린 쿼리 감지:', {
|
|
query: text,
|
|
params,
|
|
duration: `${duration}ms`
|
|
});
|
|
}
|
|
|
|
return result.rows;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
```
|
|
|
|
### 16.3 캐싱 전략
|
|
|
|
```typescript
|
|
// utils/cache.ts (Redis 기반)
|
|
import Redis from 'redis';
|
|
|
|
const redis = Redis.createClient({
|
|
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
|
});
|
|
|
|
// 캐시 조회 (있으면 반환, 없으면 쿼리 후 캐싱)
|
|
export async function getCachedData(key: string, fetcher: () => Promise<any>, ttl: number = 300) {
|
|
// 1. 캐시 확인
|
|
const cached = await redis.get(key);
|
|
if (cached) {
|
|
return JSON.parse(cached);
|
|
}
|
|
|
|
// 2. 캐시 미스 → DB 조회
|
|
const data = await fetcher();
|
|
|
|
// 3. 캐시 저장 (TTL: 5분)
|
|
await redis.setEx(key, ttl, JSON.stringify(data));
|
|
|
|
return data;
|
|
}
|
|
|
|
// 사용 예시: 메뉴 목록 캐싱
|
|
const menuList = await getCachedData(
|
|
`menu:${companyCode}:${userId}`,
|
|
() => AdminService.getUserMenuList(params),
|
|
600 // 10분 캐싱
|
|
);
|
|
```
|
|
|
|
### 16.4 압축 (Gzip)
|
|
|
|
```typescript
|
|
// app.ts
|
|
import compression from 'compression';
|
|
|
|
app.use(compression({
|
|
level: 6, // 압축 레벨 (0~9)
|
|
threshold: 1024, // 1KB 이상만 압축
|
|
filter: (req, res) => {
|
|
// JSON 응답만 압축
|
|
return req.headers['x-no-compression'] ? false : compression.filter(req, res);
|
|
}
|
|
}));
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 핵심 요약
|
|
|
|
### 아키텍처 패턴
|
|
- **Layered Architecture**: Controller → Service → Database
|
|
- **Multi-tenancy**: `company_code` 기반 완전한 데이터 격리
|
|
- **JWT 인증**: Stateless 토큰 기반 (24시간 만료)
|
|
- **Raw Query**: ORM 없이 PostgreSQL 직접 쿼리
|
|
|
|
### 보안 원칙
|
|
1. **모든 쿼리에 company_code 필터 필수**
|
|
2. **JWT 토큰에서 company_code 추출 (클라이언트 신뢰 금지)**
|
|
3. **Parameterized Query로 SQL Injection 방지**
|
|
4. **비밀번호 bcrypt, 민감정보 AES-256 암호화**
|
|
5. **Rate Limiting으로 DDoS 방지**
|
|
|
|
### 주요 도메인
|
|
- 관리자 (사용자/메뉴/권한)
|
|
- 테이블/화면 메타데이터
|
|
- 플로우 (워크플로우 엔진)
|
|
- 데이터플로우 (ERD/관계도)
|
|
- 외부 연동 (DB/REST API)
|
|
- 배치 (Cron 스케줄러)
|
|
- 메일 (발송/수신)
|
|
- 파일 (업로드/다운로드)
|
|
|
|
### API 개수
|
|
- **총 70+ 라우트**
|
|
- **인증/관리자**: 15개
|
|
- **테이블/화면**: 20개
|
|
- **플로우**: 10개
|
|
- **외부 연동**: 10개
|
|
- **배치**: 6개
|
|
- **메일**: 5개
|
|
- **파일**: 4개
|
|
|
|
---
|
|
|
|
**문서 버전**: 1.0
|
|
**마지막 업데이트**: 2026-02-06
|