# 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( 'SELECT * FROM user_info WHERE company_code = $1', [companyCode] ); // 2. 단일 행 쿼리 const user = await queryOne( '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; disconnect(): Promise; query(sql: string, params?: any[]): Promise; testConnection(): Promise; getTables(): Promise; getColumns(tableName: string): Promise; } // 구현 예시: 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 = 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) { 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, 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