ERP-node/docs/backend-architecture-detail...

53 KiB

WACE ERP Backend Architecture - 상세 분석 문서

작성일: 2026-02-06
작성자: Backend Specialist
목적: WACE ERP 시스템 백엔드 전체 아키텍처 분석 및 워크플로우 문서화


📑 목차

  1. 전체 개요
  2. 디렉토리 구조
  3. 기술 스택
  4. 미들웨어 스택
  5. 인증/인가 시스템
  6. 멀티테넌시 구현
  7. API 라우트 전체 목록
  8. 비즈니스 도메인별 모듈
  9. 데이터베이스 접근 방식
  10. 외부 시스템 연동
  11. 배치/스케줄 처리
  12. 파일 처리
  13. 에러 핸들링
  14. 로깅 시스템
  15. 보안 및 권한 관리
  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 핵심 라이브러리

{
  "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)

// 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 인증 미들웨어 체인

// 기본 인증
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 토큰 구조

// 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단계)

// 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 토큰 갱신 메커니즘

// refreshTokenIfNeeded 미들웨어
// 1. 토큰 만료까지 1시간 미만 남은 경우
// 2. 자동으로 새 토큰 발급
// 3. 응답 헤더에 "X-New-Token" 추가
// 4. 프론트엔드에서 자동으로 토큰 교체

6. 멀티테넌시 구현

6.1 핵심 원칙

// 🚨 절대 규칙: 모든 쿼리는 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 회사 데이터 격리 패턴

// ✅ 올바른 패턴
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 슈퍼관리자 숨김 규칙

-- 슈퍼관리자 사용자 (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 전용)

// 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)
  • 메뉴 관리 (트리 구조)
  • 권한 그룹 관리
  • 시스템 설정
  • 사용자 이력 조회

핵심 로직:

// 사용자 목록 조회 (멀티테넌시 적용)
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 기반 동적 화면)
  • 화면 그룹 관리
  • 테이블 로그 시스템
  • 엔티티 관계 관리

핵심 로직:

// 테이블 데이터 조회 (페이징, 정렬, 필터)
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) 관리
  • 데이터 이동 (단건/다건)
  • 조건부 이동
  • 오딧 로그

핵심 로직:

// 데이터 이동 (단계 간 전환)
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)
  • 다이어그램 관리 (시각화)
  • 관계 실행 (조인 쿼리 자동 생성)
  • 관계 검증

핵심 로직:

// 테이블 관계 실행 (동적 조인)
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 데이터 동기화
  • 컬럼 매핑
  • 실행 이력 관리

핵심 로직:

// 배치 스케줄러 초기화 (서버 시작 시)
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 프록시

핵심 로직:

// 외부 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)
  • 발송 이력 관리
  • 첨부파일 처리

핵심 로직:

// 메일 발송
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)
  • 파일 다운로드
  • 파일 삭제
  • 화면별 파일 관리

핵심 로직:

// 파일 업로드
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 설정

// 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 쿼리 실행 패턴

// 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 방지)

// ✅ 올바른 방법 (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 동적 쿼리 빌더 패턴

// 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 인터페이스

// 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 연동

// 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 배치 스케줄러 시스템

// 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 배치 실행 이력 관리

// 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 설정

// 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 파일 다운로드

// 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 파일 삭제 (논리 삭제)

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 에러 핸들러 미들웨어

// 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 커스텀 에러 클래스

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 프로세스 레벨 예외 처리

// 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 설정

// 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 로깅 레벨

// 로그 레벨 (우선순위 높음 → 낮음)
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 로깅 패턴

// 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 비밀번호 암호화

// 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)

// 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 방지

// ✅ 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

// 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 설정

// 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 모니터링

// 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 쿼리 실행 시간 로깅

// 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 캐싱 전략

// 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)

// 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