Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
commit
0ea5f3d5e4
|
|
@ -688,7 +688,7 @@ router.post(
|
|||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const { tableName, parentKeys, records } = req.body;
|
||||
const { tableName, parentKeys, records, deleteOrphans = true } = req.body;
|
||||
|
||||
// 입력값 검증
|
||||
if (!tableName || !parentKeys || !records || !Array.isArray(records)) {
|
||||
|
|
@ -722,7 +722,8 @@ router.post(
|
|||
parentKeys,
|
||||
records,
|
||||
req.user?.companyCode,
|
||||
req.user?.userId
|
||||
req.user?.userId,
|
||||
deleteOrphans
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
|
|
|||
|
|
@ -1354,7 +1354,8 @@ class DataService {
|
|||
parentKeys: Record<string, any>,
|
||||
records: Array<Record<string, any>>,
|
||||
userCompany?: string,
|
||||
userId?: string
|
||||
userId?: string,
|
||||
deleteOrphans: boolean = true
|
||||
): Promise<
|
||||
ServiceResponse<{ inserted: number; updated: number; deleted: number }>
|
||||
> {
|
||||
|
|
@ -1405,7 +1406,7 @@ class DataService {
|
|||
|
||||
console.log(`✅ 기존 레코드: ${existingRecords.rows.length}개`);
|
||||
|
||||
// 2. 새 레코드와 기존 레코드 비교
|
||||
// 2. id 기반 UPSERT: 레코드에 id(PK)가 있으면 UPDATE, 없으면 INSERT
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
let deleted = 0;
|
||||
|
|
@ -1413,125 +1414,81 @@ class DataService {
|
|||
// 날짜 필드를 YYYY-MM-DD 형식으로 변환하는 헬퍼 함수
|
||||
const normalizeDateValue = (value: any): any => {
|
||||
if (value == null) return value;
|
||||
|
||||
// ISO 날짜 문자열 감지 (YYYY-MM-DDTHH:mm:ss.sssZ)
|
||||
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
||||
return value.split("T")[0]; // YYYY-MM-DD 만 추출
|
||||
return value.split("T")[0];
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
// 새 레코드 처리 (INSERT or UPDATE)
|
||||
for (const newRecord of records) {
|
||||
console.log(`🔍 처리할 새 레코드:`, newRecord);
|
||||
const existingIds = new Set(existingRecords.rows.map((r: any) => r[pkColumn]));
|
||||
const processedIds = new Set<string>(); // UPDATE 처리된 id 추적
|
||||
|
||||
for (const newRecord of records) {
|
||||
// 날짜 필드 정규화
|
||||
const normalizedRecord: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(newRecord)) {
|
||||
normalizedRecord[key] = normalizeDateValue(value);
|
||||
}
|
||||
|
||||
console.log(`🔄 정규화된 레코드:`, normalizedRecord);
|
||||
const recordId = normalizedRecord[pkColumn]; // 프론트에서 보낸 기존 레코드의 id
|
||||
|
||||
// 전체 레코드 데이터 (parentKeys + normalizedRecord)
|
||||
const fullRecord = { ...parentKeys, ...normalizedRecord };
|
||||
|
||||
// 고유 키: parentKeys 제외한 나머지 필드들
|
||||
const uniqueFields = Object.keys(normalizedRecord);
|
||||
|
||||
console.log(`🔑 고유 필드들:`, uniqueFields);
|
||||
|
||||
// 기존 레코드에서 일치하는 것 찾기
|
||||
const existingRecord = existingRecords.rows.find((existing) => {
|
||||
return uniqueFields.every((field) => {
|
||||
const existingValue = existing[field];
|
||||
const newValue = normalizedRecord[field];
|
||||
|
||||
// null/undefined 처리
|
||||
if (existingValue == null && newValue == null) return true;
|
||||
if (existingValue == null || newValue == null) return false;
|
||||
|
||||
// Date 타입 처리
|
||||
if (existingValue instanceof Date && typeof newValue === "string") {
|
||||
return (
|
||||
existingValue.toISOString().split("T")[0] ===
|
||||
newValue.split("T")[0]
|
||||
);
|
||||
}
|
||||
|
||||
// 문자열 비교
|
||||
return String(existingValue) === String(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
if (existingRecord) {
|
||||
// UPDATE: 기존 레코드가 있으면 업데이트
|
||||
if (recordId && existingIds.has(recordId)) {
|
||||
// ===== UPDATE: id(PK)가 DB에 존재 → 해당 레코드 업데이트 =====
|
||||
const fullRecord = { ...parentKeys, ...normalizedRecord };
|
||||
const updateFields: string[] = [];
|
||||
const updateValues: any[] = [];
|
||||
let updateParamIndex = 1;
|
||||
let paramIdx = 1;
|
||||
|
||||
for (const [key, value] of Object.entries(fullRecord)) {
|
||||
if (key !== pkColumn) {
|
||||
// Primary Key는 업데이트하지 않음
|
||||
updateFields.push(`"${key}" = $${updateParamIndex}`);
|
||||
updateFields.push(`"${key}" = $${paramIdx}`);
|
||||
updateValues.push(value);
|
||||
updateParamIndex++;
|
||||
paramIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
updateValues.push(existingRecord[pkColumn]); // WHERE 조건용
|
||||
const updateQuery = `
|
||||
UPDATE "${tableName}"
|
||||
SET ${updateFields.join(", ")}, updated_date = NOW()
|
||||
WHERE "${pkColumn}" = $${updateParamIndex}
|
||||
`;
|
||||
|
||||
await pool.query(updateQuery, updateValues);
|
||||
updated++;
|
||||
|
||||
console.log(`✏️ UPDATE: ${pkColumn} = ${existingRecord[pkColumn]}`);
|
||||
if (updateFields.length > 0) {
|
||||
updateValues.push(recordId);
|
||||
const updateQuery = `
|
||||
UPDATE "${tableName}"
|
||||
SET ${updateFields.join(", ")}, updated_date = NOW()
|
||||
WHERE "${pkColumn}" = $${paramIdx}
|
||||
`;
|
||||
await pool.query(updateQuery, updateValues);
|
||||
updated++;
|
||||
processedIds.add(recordId);
|
||||
console.log(`✏️ UPDATE by id: ${pkColumn} = ${recordId}`);
|
||||
}
|
||||
} else {
|
||||
// INSERT: 기존 레코드가 없으면 삽입
|
||||
|
||||
// 🆕 자동 필드 추가 (company_code, writer, created_date, updated_date, id)
|
||||
// created_date는 프론트엔드에서 전달된 값 무시하고 항상 현재 시간 설정
|
||||
const { created_date: _, ...recordWithoutCreatedDate } = fullRecord;
|
||||
// ===== INSERT: id 없음 또는 DB에 없음 → 새 레코드 삽입 =====
|
||||
const { [pkColumn]: _removedId, created_date: _cd, ...cleanRecord } = normalizedRecord;
|
||||
const fullRecord = { ...parentKeys, ...cleanRecord };
|
||||
const newId = uuidv4();
|
||||
const recordWithMeta: Record<string, any> = {
|
||||
...recordWithoutCreatedDate,
|
||||
id: uuidv4(), // 새 ID 생성
|
||||
...fullRecord,
|
||||
[pkColumn]: newId,
|
||||
created_date: "NOW()",
|
||||
updated_date: "NOW()",
|
||||
};
|
||||
|
||||
// company_code가 없으면 userCompany 사용 (단, userCompany가 "*"가 아닐 때만)
|
||||
if (
|
||||
!recordWithMeta.company_code &&
|
||||
userCompany &&
|
||||
userCompany !== "*"
|
||||
) {
|
||||
if (!recordWithMeta.company_code && userCompany && userCompany !== "*") {
|
||||
recordWithMeta.company_code = userCompany;
|
||||
}
|
||||
|
||||
// writer가 없으면 userId 사용
|
||||
if (!recordWithMeta.writer && userId) {
|
||||
recordWithMeta.writer = userId;
|
||||
}
|
||||
|
||||
const insertFields = Object.keys(recordWithMeta).filter(
|
||||
(key) => recordWithMeta[key] !== "NOW()"
|
||||
);
|
||||
const insertPlaceholders: string[] = [];
|
||||
const insertValues: any[] = [];
|
||||
let insertParamIndex = 1;
|
||||
let paramIdx = 1;
|
||||
|
||||
for (const field of Object.keys(recordWithMeta)) {
|
||||
if (recordWithMeta[field] === "NOW()") {
|
||||
insertPlaceholders.push("NOW()");
|
||||
} else {
|
||||
insertPlaceholders.push(`$${insertParamIndex}`);
|
||||
insertPlaceholders.push(`$${paramIdx}`);
|
||||
insertValues.push(recordWithMeta[field]);
|
||||
insertParamIndex++;
|
||||
paramIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1541,49 +1498,24 @@ class DataService {
|
|||
.join(", ")})
|
||||
VALUES (${insertPlaceholders.join(", ")})
|
||||
`;
|
||||
|
||||
console.log(`➕ INSERT 쿼리:`, {
|
||||
query: insertQuery,
|
||||
values: insertValues,
|
||||
});
|
||||
|
||||
await pool.query(insertQuery, insertValues);
|
||||
inserted++;
|
||||
|
||||
console.log(`➕ INSERT: 새 레코드`);
|
||||
processedIds.add(newId);
|
||||
console.log(`➕ INSERT: 새 레코드 ${pkColumn} = ${newId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 삭제할 레코드 찾기 (기존 레코드 중 새 레코드에 없는 것)
|
||||
for (const existingRecord of existingRecords.rows) {
|
||||
const uniqueFields = Object.keys(records[0] || {});
|
||||
|
||||
const stillExists = records.some((newRecord) => {
|
||||
return uniqueFields.every((field) => {
|
||||
const existingValue = existingRecord[field];
|
||||
const newValue = newRecord[field];
|
||||
|
||||
if (existingValue == null && newValue == null) return true;
|
||||
if (existingValue == null || newValue == null) return false;
|
||||
|
||||
if (existingValue instanceof Date && typeof newValue === "string") {
|
||||
return (
|
||||
existingValue.toISOString().split("T")[0] ===
|
||||
newValue.split("T")[0]
|
||||
);
|
||||
}
|
||||
|
||||
return String(existingValue) === String(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
if (!stillExists) {
|
||||
// DELETE: 새 레코드에 없으면 삭제
|
||||
const deleteQuery = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`;
|
||||
await pool.query(deleteQuery, [existingRecord[pkColumn]]);
|
||||
deleted++;
|
||||
|
||||
console.log(`🗑️ DELETE: ${pkColumn} = ${existingRecord[pkColumn]}`);
|
||||
// 3. 고아 레코드 삭제: deleteOrphans=true일 때만 (EDIT 모드)
|
||||
// CREATE 모드에서는 기존 레코드를 건드리지 않음
|
||||
if (deleteOrphans) {
|
||||
for (const existingRow of existingRecords.rows) {
|
||||
const existId = existingRow[pkColumn];
|
||||
if (!processedIds.has(existId)) {
|
||||
const deleteQuery = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`;
|
||||
await pool.query(deleteQuery, [existId]);
|
||||
deleted++;
|
||||
console.log(`🗑️ DELETE orphan: ${pkColumn} = ${existId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,955 @@
|
|||
# WACE ERP 시스템 전체 워크플로우 문서
|
||||
|
||||
> 작성일: 2026-02-06
|
||||
> 분석 방법: Multi-Agent System (Backend + Frontend + DB 전문가 병렬 분석)
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [시스템 개요](#1-시스템-개요)
|
||||
2. [기술 스택](#2-기술-스택)
|
||||
3. [전체 아키텍처](#3-전체-아키텍처)
|
||||
4. [백엔드 아키텍처](#4-백엔드-아키텍처)
|
||||
5. [프론트엔드 아키텍처](#5-프론트엔드-아키텍처)
|
||||
6. [데이터베이스 구조](#6-데이터베이스-구조)
|
||||
7. [인증/인가 워크플로우](#7-인증인가-워크플로우)
|
||||
8. [화면 디자이너 워크플로우](#8-화면-디자이너-워크플로우)
|
||||
9. [사용자 업무 워크플로우](#9-사용자-업무-워크플로우)
|
||||
10. [플로우 엔진 워크플로우](#10-플로우-엔진-워크플로우)
|
||||
11. [데이터플로우 시스템](#11-데이터플로우-시스템)
|
||||
12. [대시보드 시스템](#12-대시보드-시스템)
|
||||
13. [배치/스케줄 시스템](#13-배치스케줄-시스템)
|
||||
14. [멀티테넌시 아키텍처](#14-멀티테넌시-아키텍처)
|
||||
15. [외부 연동](#15-외부-연동)
|
||||
16. [배포 환경](#16-배포-환경)
|
||||
|
||||
---
|
||||
|
||||
## 1. 시스템 개요
|
||||
|
||||
WACE는 **로우코드(Low-Code) ERP 플랫폼**이다. 관리자가 코드 없이 드래그앤드롭으로 업무 화면을 설계하면, 사용자는 해당 화면으로 바로 업무를 처리할 수 있는 구조다.
|
||||
|
||||
### 핵심 컨셉
|
||||
|
||||
```
|
||||
관리자 → 화면 디자이너로 화면 설계 → 메뉴에 연결
|
||||
↓
|
||||
사용자 → 메뉴 클릭 → 화면 자동 렌더링 → 업무 수행
|
||||
```
|
||||
|
||||
### 주요 특징
|
||||
|
||||
- **드래그앤드롭 화면 디자이너**: 코드 없이 UI 구성
|
||||
- **동적 컴포넌트 시스템**: V2 통합 컴포넌트 10종으로 모든 UI 표현
|
||||
- **플로우 엔진**: 워크플로우(승인, 이동 등) 자동화
|
||||
- **데이터플로우**: 비즈니스 로직을 비주얼 다이어그램으로 설계
|
||||
- **멀티테넌시**: 회사별 완벽한 데이터 격리
|
||||
- **다국어 지원**: KR/EN/CN 다국어 라벨 관리
|
||||
|
||||
---
|
||||
|
||||
## 2. 기술 스택
|
||||
|
||||
| 영역 | 기술 | 비고 |
|
||||
|------|------|------|
|
||||
| **Frontend** | Next.js 15 (App Router) | React 19, TypeScript |
|
||||
| **UI 라이브러리** | shadcn/ui + Radix UI | Tailwind CSS 4 |
|
||||
| **상태 관리** | React Context + Zustand | React Query (서버 상태) |
|
||||
| **Backend** | Node.js + Express | TypeScript |
|
||||
| **Database** | PostgreSQL | Raw Query (ORM 미사용) |
|
||||
| **인증** | JWT | 자동 갱신, 세션 관리 |
|
||||
| **빌드/배포** | Docker | dev/prod 분리 |
|
||||
| **포트** | FE: 9771(dev)/5555(prod) | BE: 8080 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 전체 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 사용자 브라우저 │
|
||||
│ Next.js App (React 19 + shadcn/ui + Tailwind CSS) │
|
||||
│ ├── 인증: JWT + Cookie + localStorage │
|
||||
│ ├── 상태: Context + Zustand + React Query │
|
||||
│ └── API: Axios Client (lib/api/) │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│ HTTP/JSON (JWT Bearer Token)
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Express Backend (Node.js) │
|
||||
│ ├── Middleware: Helmet → CORS → RateLimit → Auth → Permission │
|
||||
│ ├── Routes: 60+ 모듈 │
|
||||
│ ├── Controllers: 69개 │
|
||||
│ ├── Services: 87개 │
|
||||
│ └── Database: pg Pool (Raw Query) │
|
||||
└──────────────────────────┬──────────────────────────────────────┘
|
||||
│ TCP/SQL
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PostgreSQL Database │
|
||||
│ ├── 시스템 테이블: 사용자, 회사, 메뉴, 권한, 화면 │
|
||||
│ ├── 메타데이터: 테이블/컬럼 정의, 코드, 카테고리 │
|
||||
│ ├── 비즈니스: 동적 생성 테이블 (화면별) │
|
||||
│ └── 멀티테넌시: 모든 테이블에 company_code │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 백엔드 아키텍처
|
||||
|
||||
### 4.1 디렉토리 구조
|
||||
|
||||
```
|
||||
backend-node/src/
|
||||
├── app.ts # Express 앱 진입점
|
||||
├── config/ # 환경설정, Multer
|
||||
├── controllers/ # 69개 컨트롤러
|
||||
├── services/ # 87개 서비스
|
||||
├── routes/ # 60+ 라우트 모듈
|
||||
├── middleware/ # 인증, 권한, 에러 처리
|
||||
│ ├── authMiddleware.ts # JWT 인증
|
||||
│ ├── permissionMiddleware.ts # 3단계 권한 체크
|
||||
│ ├── superAdminMiddleware.ts # 슈퍼관리자 전용
|
||||
│ └── errorHandler.ts # 전역 에러 처리
|
||||
├── database/ # DB 연결, 커넥터 팩토리
|
||||
│ ├── db.ts # PostgreSQL Pool
|
||||
│ ├── DatabaseConnectorFactory.ts
|
||||
│ ├── PostgreSQLConnector.ts
|
||||
│ ├── MySQLConnector.ts
|
||||
│ └── MariaDBConnector.ts
|
||||
├── types/ # TypeScript 타입 (26개)
|
||||
└── utils/ # 유틸리티 (16개)
|
||||
```
|
||||
|
||||
### 4.2 미들웨어 스택 (실행 순서)
|
||||
|
||||
```
|
||||
요청 → Helmet (보안 헤더)
|
||||
→ Compression (응답 압축)
|
||||
→ Body Parser (JSON/URLEncoded, 10MB)
|
||||
→ CORS (교차 출처 허용)
|
||||
→ Rate Limiter (10,000 req/min)
|
||||
→ Token Refresh (자동 갱신)
|
||||
→ Route Handlers (비즈니스 로직)
|
||||
→ Error Handler (전역 에러 처리)
|
||||
```
|
||||
|
||||
### 4.3 API 라우트 도메인별 분류
|
||||
|
||||
#### 인증/사용자 관리
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/auth` | 로그인, 로그아웃, 토큰 갱신, 회사 전환 |
|
||||
| `/api/admin/users` | 사용자 CRUD, 비밀번호 초기화, 상태 변경 |
|
||||
| `/api/company-management` | 회사 CRUD |
|
||||
| `/api/departments` | 부서 관리 |
|
||||
| `/api/roles` | 권한 그룹 관리 |
|
||||
|
||||
#### 화면/메뉴 관리
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/screen-management` | 화면 정의 CRUD, 그룹, 파일, 임베딩 |
|
||||
| `/api/admin/menus` | 메뉴 트리 CRUD, 화면 할당 |
|
||||
| `/api/table-management` | 테이블 CRUD, 엔티티 조인, 카테고리 |
|
||||
| `/api/common-codes` | 공통 코드/카테고리 관리 |
|
||||
| `/api/multilang` | 다국어 키/번역 관리 |
|
||||
|
||||
#### 데이터 관리
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/data` | 동적 테이블 CRUD, 조인 쿼리 |
|
||||
| `/api/data/:tableName` | 특정 테이블 데이터 조회 |
|
||||
| `/api/data/join` | 조인 쿼리 실행 |
|
||||
| `/api/dynamic-form` | 동적 폼 데이터 저장 |
|
||||
| `/api/entity-search` | 엔티티 검색 |
|
||||
| `/api/entity-reference` | 엔티티 참조 |
|
||||
| `/api/numbering-rules` | 채번 규칙 관리 |
|
||||
| `/api/cascading-*` | 연쇄 드롭다운 관계 |
|
||||
|
||||
#### 자동화
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/flow` | 플로우 정의/단계/연결/실행 |
|
||||
| `/api/dataflow` | 데이터플로우 다이어그램/실행 |
|
||||
| `/api/batch-configs` | 배치 작업 설정 |
|
||||
| `/api/batch-management` | 배치 작업 관리 |
|
||||
| `/api/batch-execution-logs` | 배치 실행 로그 |
|
||||
|
||||
#### 대시보드/리포트
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/dashboards` | 대시보드 CRUD, 쿼리 실행 |
|
||||
| `/api/reports` | 리포트 생성 |
|
||||
|
||||
#### 외부 연동
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/external-db-connections` | 외부 DB 연결 (PostgreSQL, MySQL, MariaDB, MSSQL, Oracle) |
|
||||
| `/api/external-rest-api-connections` | 외부 REST API 연결 |
|
||||
| `/api/mail` | 메일 발송/수신/템플릿 |
|
||||
| `/api/tax-invoice` | 세금계산서 |
|
||||
|
||||
#### 특수 도메인
|
||||
| 라우트 | 역할 |
|
||||
|--------|------|
|
||||
| `/api/delivery` | 배송/화물 관리 |
|
||||
| `/api/risk-alerts` | 위험 알림 |
|
||||
| `/api/todos` | 할일 관리 |
|
||||
| `/api/bookings` | 예약 관리 |
|
||||
| `/api/digital-twin` | 디지털 트윈 (야드 모니터링) |
|
||||
| `/api/schedule` | 스케줄 자동 생성 |
|
||||
| `/api/vehicle` | 차량 운행 |
|
||||
| `/api/driver` | 운전자 관리 |
|
||||
| `/api/files` | 파일 업로드/다운로드 |
|
||||
| `/api/ddl` | DDL 실행 (슈퍼관리자 전용) |
|
||||
|
||||
### 4.4 서비스 레이어 패턴
|
||||
|
||||
```typescript
|
||||
// 표준 서비스 패턴
|
||||
class ExampleService {
|
||||
// 목록 조회 (멀티테넌시 적용)
|
||||
async findAll(companyCode: string, filters?: any) {
|
||||
if (companyCode === "*") {
|
||||
// 슈퍼관리자: 전체 데이터
|
||||
return await db.query("SELECT * FROM table ORDER BY company_code");
|
||||
} else {
|
||||
// 일반 사용자: 자기 회사 데이터만
|
||||
return await db.query(
|
||||
"SELECT * FROM table WHERE company_code = $1",
|
||||
[companyCode]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 에러 처리 전략
|
||||
|
||||
```typescript
|
||||
// 전역 에러 핸들러 (errorHandler.ts)
|
||||
- PostgreSQL 에러: 중복키(23505), 외래키(23503), 널 제약(23502) 등
|
||||
- JWT 에러: 만료, 유효하지 않은 토큰
|
||||
- 일반 에러: 500 Internal Server Error
|
||||
- 개발 환경: 상세 에러 스택 포함
|
||||
- 운영 환경: 일반적인 에러 메시지만 반환
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 프론트엔드 아키텍처
|
||||
|
||||
### 5.1 디렉토리 구조
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # 인증 (로그인)
|
||||
│ ├── (main)/ # 메인 앱 (인증 필요)
|
||||
│ ├── (pop)/ # 모바일/팝업
|
||||
│ └── (admin)/ # 특수 관리자
|
||||
├── components/ # React 컴포넌트
|
||||
│ ├── screen/ # 화면 디자이너 & 뷰어
|
||||
│ ├── admin/ # 관리 기능
|
||||
│ ├── dashboard/ # 대시보드 위젯
|
||||
│ ├── dataflow/ # 데이터플로우 디자이너
|
||||
│ ├── v2/ # V2 통합 컴포넌트
|
||||
│ ├── ui/ # shadcn/ui 기본 컴포넌트
|
||||
│ └── report/ # 리포트 디자이너
|
||||
├── lib/
|
||||
│ ├── api/ # API 클라이언트 (57개 모듈)
|
||||
│ ├── registry/ # 컴포넌트 레지스트리 (482개)
|
||||
│ ├── utils/ # 유틸리티
|
||||
│ └── v2-core/ # V2 코어 로직
|
||||
├── contexts/ # React Context (인증, 메뉴, 화면 등)
|
||||
├── hooks/ # Custom Hooks
|
||||
├── stores/ # Zustand 상태관리
|
||||
└── middleware.ts # Next.js 인증 미들웨어
|
||||
```
|
||||
|
||||
### 5.2 페이지 라우팅 구조
|
||||
|
||||
```
|
||||
/login → 로그인
|
||||
/main → 메인 대시보드
|
||||
/screens/[screenId] → 동적 화면 뷰어 (사용자)
|
||||
|
||||
/admin/screenMng/screenMngList → 화면 관리
|
||||
/admin/screenMng/dashboardList → 대시보드 관리
|
||||
/admin/screenMng/reportList → 리포트 관리
|
||||
/admin/systemMng/tableMngList → 테이블 관리
|
||||
/admin/systemMng/commonCodeList → 공통코드 관리
|
||||
/admin/systemMng/dataflow → 데이터플로우 관리
|
||||
/admin/systemMng/i18nList → 다국어 관리
|
||||
/admin/userMng/userMngList → 사용자 관리
|
||||
/admin/userMng/companyList → 회사 관리
|
||||
/admin/userMng/rolesList → 권한 관리
|
||||
/admin/automaticMng/flowMgmtList → 플로우 관리
|
||||
/admin/automaticMng/batchmngList → 배치 관리
|
||||
/admin/automaticMng/mail/* → 메일 시스템
|
||||
/admin/menu → 메뉴 관리
|
||||
|
||||
/dashboard/[dashboardId] → 대시보드 뷰어
|
||||
/pop/work → 모바일 작업 화면
|
||||
```
|
||||
|
||||
### 5.3 V2 통합 컴포넌트 시스템
|
||||
|
||||
**"하나의 컴포넌트, 여러 모드"** 철학으로 설계된 10개 통합 컴포넌트:
|
||||
|
||||
| 컴포넌트 | 모드 | 역할 |
|
||||
|----------|------|------|
|
||||
| **V2Input** | text, number, password, slider, color | 텍스트/숫자 입력 |
|
||||
| **V2Select** | dropdown, radio, checkbox, tag, toggle | 선택 입력 |
|
||||
| **V2Date** | date, datetime, time, range | 날짜/시간 입력 |
|
||||
| **V2List** | table, card, kanban, list | 데이터 목록 표시 |
|
||||
| **V2Layout** | grid, split-panel, flex | 레이아웃 구성 |
|
||||
| **V2Group** | tab, accordion, section, modal | 그룹 컨테이너 |
|
||||
| **V2Media** | image, video, audio, file | 미디어 표시 |
|
||||
| **V2Biz** | flow, rack, numbering-rule | 비즈니스 로직 |
|
||||
| **V2Hierarchy** | tree, org-chart, BOM, cascading | 계층 구조 |
|
||||
| **V2Repeater** | inline-table, modal, button | 반복 데이터 |
|
||||
|
||||
### 5.4 API 클라이언트 규칙
|
||||
|
||||
```typescript
|
||||
// 절대 금지: fetch 직접 사용
|
||||
const res = await fetch('/api/flow/definitions'); // ❌
|
||||
|
||||
// 반드시 사용: lib/api/ 클라이언트
|
||||
import { getFlowDefinitions } from '@/lib/api/flow';
|
||||
const res = await getFlowDefinitions(); // ✅
|
||||
```
|
||||
|
||||
환경별 URL 자동 처리:
|
||||
| 환경 | 프론트엔드 | 백엔드 API |
|
||||
|------|-----------|-----------|
|
||||
| 로컬 개발 | localhost:9771 | localhost:8080/api |
|
||||
| 운영 | v1.vexplor.com | api.vexplor.com/api |
|
||||
|
||||
### 5.5 상태 관리 체계
|
||||
|
||||
```
|
||||
전역 상태
|
||||
├── AuthContext → 인증/세션/토큰
|
||||
├── MenuContext → 메뉴 트리/권한
|
||||
├── ScreenPreviewContext → 프리뷰 모드
|
||||
├── ScreenMultiLangContext → 다국어 라벨
|
||||
├── TableOptionsContext → 테이블 옵션
|
||||
└── ActiveTabContext → 활성 탭
|
||||
|
||||
로컬 상태
|
||||
├── Zustand Stores → 화면 디자이너 상태, 사용자 상태
|
||||
└── React Query → 서버 데이터 캐시 (5분 stale, 30분 GC)
|
||||
```
|
||||
|
||||
### 5.6 레지스트리 시스템
|
||||
|
||||
```typescript
|
||||
// 컴포넌트 등록 (482개 등록됨)
|
||||
ComponentRegistry.registerComponent({
|
||||
id: "v2-input",
|
||||
name: "통합 입력",
|
||||
category: ComponentCategory.V2,
|
||||
component: V2Input,
|
||||
configPanel: V2InputConfigPanel,
|
||||
defaultConfig: { inputType: "text" }
|
||||
});
|
||||
|
||||
// 동적 렌더링
|
||||
<DynamicComponentRenderer
|
||||
component={componentData}
|
||||
formData={formData}
|
||||
onFormDataChange={handleChange}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 데이터베이스 구조
|
||||
|
||||
### 6.1 테이블 도메인별 분류
|
||||
|
||||
#### 사용자/인증/회사
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `company_mng` | 회사 마스터 |
|
||||
| `user_info` | 사용자 정보 |
|
||||
| `user_info_history` | 사용자 변경 이력 |
|
||||
| `user_dept` | 사용자-부서 매핑 |
|
||||
| `dept_info` | 부서 정보 |
|
||||
| `authority_master` | 권한 그룹 마스터 |
|
||||
| `authority_sub_user` | 사용자-권한 매핑 |
|
||||
| `login_access_log` | 로그인 로그 |
|
||||
|
||||
#### 메뉴/화면
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `menu_info` | 메뉴 트리 구조 |
|
||||
| `screen_definitions` | 화면 정의 (screenId, 테이블명 등) |
|
||||
| `screen_layouts_v2` | V2 레이아웃 (JSON) |
|
||||
| `screen_layouts` | V1 레이아웃 (레거시) |
|
||||
| `screen_groups` | 화면 그룹 (계층구조) |
|
||||
| `screen_group_screens` | 화면-그룹 매핑 |
|
||||
| `screen_menu_assignments` | 화면-메뉴 할당 |
|
||||
| `screen_field_joins` | 화면 필드 조인 설정 |
|
||||
| `screen_data_flows` | 화면 데이터 플로우 |
|
||||
| `screen_table_relations` | 화면-테이블 관계 |
|
||||
|
||||
#### 메타데이터
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `table_type_columns` | 테이블 타입별 컬럼 정의 (회사별) |
|
||||
| `table_column_category_values` | 컬럼 카테고리 값 |
|
||||
| `code_category` | 공통 코드 카테고리 |
|
||||
| `code_info` | 공통 코드 값 |
|
||||
| `category_column_mapping` | 카테고리-컬럼 매핑 |
|
||||
| `cascading_relation` | 연쇄 드롭다운 관계 |
|
||||
| `numbering_rules` | 채번 규칙 |
|
||||
| `numbering_rule_parts` | 채번 규칙 파트 |
|
||||
|
||||
#### 플로우/자동화
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `flow_definition` | 플로우 정의 |
|
||||
| `flow_step` | 플로우 단계 |
|
||||
| `flow_step_connection` | 플로우 단계 연결 |
|
||||
| `node_flows` | 노드 플로우 (버튼 액션) |
|
||||
| `dataflow_diagrams` | 데이터플로우 다이어그램 |
|
||||
| `batch_definitions` | 배치 작업 정의 |
|
||||
| `batch_schedules` | 배치 스케줄 |
|
||||
| `batch_execution_logs` | 배치 실행 로그 |
|
||||
|
||||
#### 외부 연동
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `external_db_connections` | 외부 DB 연결 정보 |
|
||||
| `external_rest_api_connections` | 외부 REST API 연결 |
|
||||
|
||||
#### 다국어
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `multi_lang_key_master` | 다국어 키 마스터 |
|
||||
|
||||
#### 기타
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `work_history` | 작업 이력 |
|
||||
| `todo_items` | 할일 목록 |
|
||||
| `file_uploads` | 파일 업로드 |
|
||||
| `ddl_audit_log` | DDL 감사 로그 |
|
||||
|
||||
### 6.2 동적 테이블 생성 패턴
|
||||
|
||||
관리자가 화면 생성 시 비즈니스 테이블이 동적으로 생성된다:
|
||||
|
||||
```sql
|
||||
CREATE TABLE "dynamic_table_name" (
|
||||
"id" VARCHAR(500) PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
"created_date" TIMESTAMP DEFAULT now(),
|
||||
"updated_date" TIMESTAMP DEFAULT now(),
|
||||
"writer" VARCHAR(500),
|
||||
"company_code" VARCHAR(500), -- 멀티테넌시 필수!
|
||||
-- 사용자 정의 컬럼들 (모두 VARCHAR(500))
|
||||
"product_name" VARCHAR(500),
|
||||
"price" VARCHAR(500),
|
||||
...
|
||||
);
|
||||
CREATE INDEX idx_dynamic_company ON "dynamic_table_name"(company_code);
|
||||
```
|
||||
|
||||
### 6.3 테이블 관계도
|
||||
|
||||
```
|
||||
company_mng (company_code PK)
|
||||
│
|
||||
├── user_info (company_code FK)
|
||||
│ ├── authority_sub_user (user_id FK)
|
||||
│ └── user_dept (user_id FK)
|
||||
│
|
||||
├── menu_info (company_code)
|
||||
│ └── screen_menu_assignments (menu_objid FK)
|
||||
│
|
||||
├── screen_definitions (company_code)
|
||||
│ ├── screen_layouts_v2 (screen_id FK)
|
||||
│ ├── screen_groups → screen_group_screens (screen_id FK)
|
||||
│ └── screen_field_joins (screen_id FK)
|
||||
│
|
||||
├── authority_master (company_code)
|
||||
│ └── authority_sub_user (master_objid FK)
|
||||
│
|
||||
├── flow_definition (company_code)
|
||||
│ ├── flow_step (flow_id FK)
|
||||
│ └── flow_step_connection (flow_id FK)
|
||||
│
|
||||
└── [동적 비즈니스 테이블들] (company_code)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 인증/인가 워크플로우
|
||||
|
||||
### 7.1 로그인 프로세스
|
||||
|
||||
```
|
||||
┌─── 사용자 ───┐ ┌─── 프론트엔드 ───┐ ┌─── 백엔드 ───┐ ┌─── DB ───┐
|
||||
│ │ │ │ │ │ │ │
|
||||
│ ID/PW 입력 │────→│ POST /auth/login │────→│ 비밀번호 검증 │────→│ user_info│
|
||||
│ │ │ │ │ │ │ 조회 │
|
||||
│ │ │ │ │ JWT 토큰 생성 │ │ │
|
||||
│ │ │ │←────│ 토큰 반환 │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ localStorage 저장│ │ │ │ │
|
||||
│ │ │ Cookie 저장 │ │ │ │ │
|
||||
│ │ │ /main 리다이렉트 │ │ │ │ │
|
||||
└──────────────┘ └──────────────────┘ └──────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 7.2 JWT 토큰 관리
|
||||
|
||||
```
|
||||
토큰 저장: localStorage (주 저장소) + Cookie (SSR 미들웨어용)
|
||||
|
||||
자동 갱신:
|
||||
├── 10분마다 만료 시간 체크
|
||||
├── 만료 30분 전: 백그라운드 자동 갱신
|
||||
├── 401 응답 시: 즉시 갱신 시도
|
||||
└── 갱신 실패 시: /login 리다이렉트
|
||||
|
||||
세션 관리:
|
||||
├── 데스크톱: 30분 비활성 → 세션 만료 (5분 전 경고)
|
||||
└── 모바일: 24시간 비활성 → 세션 만료 (1시간 전 경고)
|
||||
```
|
||||
|
||||
### 7.3 권한 체계 (3단계)
|
||||
|
||||
```
|
||||
SUPER_ADMIN (company_code = "*")
|
||||
├── 모든 회사 데이터 접근 가능
|
||||
├── DDL 실행 가능
|
||||
├── 시스템 설정 변경
|
||||
└── 다른 회사로 전환 (switch-company)
|
||||
|
||||
COMPANY_ADMIN (userType = "COMPANY_ADMIN")
|
||||
├── 자기 회사 데이터만 접근
|
||||
├── 사용자 관리 가능
|
||||
└── 메뉴/화면 관리 가능
|
||||
|
||||
USER (일반 사용자)
|
||||
├── 자기 회사 데이터만 접근
|
||||
├── 권한 그룹에 따른 메뉴 접근
|
||||
└── 할당된 화면만 사용 가능
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 화면 디자이너 워크플로우
|
||||
|
||||
### 8.1 관리자: 화면 설계
|
||||
|
||||
```
|
||||
Step 1: 화면 생성
|
||||
└→ /admin/screenMng/screenMngList
|
||||
└→ "새 화면" 클릭 → 화면명, 설명, 메인 테이블 입력
|
||||
|
||||
Step 2: 화면 디자이너 진입 (ScreenDesigner.tsx)
|
||||
├── 좌측 패널: 컴포넌트 팔레트 (V2 컴포넌트 10종)
|
||||
├── 중앙 캔버스: 드래그앤드롭 영역
|
||||
└── 우측 패널: 선택된 컴포넌트 속성 설정
|
||||
|
||||
Step 3: 컴포넌트 배치
|
||||
└→ V2Input 드래그 → 캔버스 배치 → 속성 설정:
|
||||
├── 위치: x, y 좌표
|
||||
├── 크기: width, height
|
||||
├── 데이터 바인딩: columnName = "product_name"
|
||||
├── 라벨: "제품명"
|
||||
├── 조건부 표시: 특정 조건에서만 보이기
|
||||
└── 플로우 연결: 버튼 클릭 시 실행할 플로우
|
||||
|
||||
Step 4: 레이아웃 저장
|
||||
└→ screen_layouts_v2 테이블에 JSON 형태로 저장
|
||||
└→ Zod 스키마 검증 → V2 형식 우선, V1 호환 저장
|
||||
|
||||
Step 5: 메뉴에 화면 할당
|
||||
└→ /admin/menu → 메뉴 트리에서 "제품 관리" 선택
|
||||
└→ 화면 연결 (screen_menu_assignments)
|
||||
```
|
||||
|
||||
### 8.2 화면 레이아웃 저장 구조 (V2)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "v2",
|
||||
"components": [
|
||||
{
|
||||
"id": "comp-1",
|
||||
"componentType": "v2-input",
|
||||
"position": { "x": 100, "y": 50 },
|
||||
"size": { "width": 200, "height": 40 },
|
||||
"config": {
|
||||
"inputType": "text",
|
||||
"columnName": "product_name",
|
||||
"label": "제품명",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "comp-2",
|
||||
"componentType": "v2-list",
|
||||
"position": { "x": 100, "y": 150 },
|
||||
"size": { "width": 600, "height": 400 },
|
||||
"config": {
|
||||
"listType": "table",
|
||||
"tableName": "products",
|
||||
"columns": ["product_name", "price", "quantity"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 사용자 업무 워크플로우
|
||||
|
||||
### 9.1 전체 흐름
|
||||
|
||||
```
|
||||
사용자 로그인
|
||||
↓
|
||||
메인 대시보드 (/main)
|
||||
↓
|
||||
좌측 메뉴에서 "제품 관리" 클릭
|
||||
↓
|
||||
/screens/[screenId] 라우팅
|
||||
↓
|
||||
InteractiveScreenViewer 렌더링
|
||||
├── screen_definitions에서 화면 정보 로드
|
||||
├── screen_layouts_v2에서 레이아웃 JSON 로드
|
||||
├── V2 → Legacy 변환 (호환성)
|
||||
└── 메인 테이블 데이터 자동 로드
|
||||
↓
|
||||
컴포넌트별 렌더링
|
||||
├── V2Input → formData 바인딩
|
||||
├── V2List → 테이블 데이터 표시
|
||||
├── V2Select → 드롭다운/라디오 선택
|
||||
└── Button → 플로우/액션 연결
|
||||
↓
|
||||
사용자 인터랙션
|
||||
├── 폼 입력 → formData 업데이트
|
||||
├── 테이블 행 선택 → selectedRowsData 업데이트
|
||||
└── 버튼 클릭 → 플로우 실행
|
||||
↓
|
||||
플로우 실행 (nodeFlowButtonExecutor)
|
||||
├── Step 1: 데이터 검증
|
||||
├── Step 2: API 호출 (INSERT/UPDATE/DELETE)
|
||||
├── Step 3: 성공/실패 처리
|
||||
└── Step 4: 테이블 자동 새로고침
|
||||
```
|
||||
|
||||
### 9.2 조건부 표시 워크플로우
|
||||
|
||||
```
|
||||
관리자 설정:
|
||||
"특별 할인 입력" 컴포넌트
|
||||
└→ 조건: product_type === "PREMIUM" 일 때만 표시
|
||||
|
||||
사용자 사용:
|
||||
1. 화면 진입 → evaluateConditional() 실행
|
||||
2. product_type ≠ "PREMIUM" → "특별 할인 입력" 숨김
|
||||
3. 사용자가 product_type을 "PREMIUM"으로 변경
|
||||
4. formData 업데이트 → evaluateConditional() 재평가
|
||||
5. product_type === "PREMIUM" → "특별 할인 입력" 표시!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 플로우 엔진 워크플로우
|
||||
|
||||
### 10.1 플로우 정의 (관리자)
|
||||
|
||||
```
|
||||
/admin/automaticMng/flowMgmtList
|
||||
↓
|
||||
플로우 생성:
|
||||
├── 이름: "제품 승인 플로우"
|
||||
├── 테이블: "products"
|
||||
└── 단계 정의:
|
||||
Step 1: "신청" (requester)
|
||||
Step 2: "부서장 승인" (manager)
|
||||
Step 3: "최종 승인" (director)
|
||||
연결: Step 1 → Step 2 → Step 3
|
||||
```
|
||||
|
||||
### 10.2 플로우 실행 (사용자)
|
||||
|
||||
```
|
||||
1. 사용자: 제품 신청
|
||||
└→ "저장" 버튼 클릭
|
||||
└→ flowApi.startFlow() → 상태: "부서장 승인 대기"
|
||||
|
||||
2. 부서장: 승인 화면
|
||||
└→ V2Biz (flow) 컴포넌트 → 현재 단계 표시
|
||||
└→ [승인] 클릭 → flowApi.approveStep()
|
||||
└→ 상태: "최종 승인 대기"
|
||||
|
||||
3. 이사: 최종 승인
|
||||
└→ [승인] 클릭 → flowApi.approveStep()
|
||||
└→ 상태: "완료"
|
||||
└→ products.approval_status = "APPROVED"
|
||||
```
|
||||
|
||||
### 10.3 데이터 이동 (moveData)
|
||||
|
||||
```
|
||||
플로우의 핵심 동작: 데이터를 한 스텝에서 다음 스텝으로 이동
|
||||
|
||||
Step 1 (접수) → Step 2 (검토) → Step 3 (완료)
|
||||
├── 단건 이동: moveData(flowId, dataId, fromStep, toStep)
|
||||
└── 배치 이동: moveBatchData(flowId, dataIds[], fromStep, toStep)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 데이터플로우 시스템
|
||||
|
||||
### 11.1 개요
|
||||
|
||||
데이터플로우는 비즈니스 로직을 **비주얼 다이어그램**으로 설계하는 시스템이다.
|
||||
|
||||
```
|
||||
/admin/systemMng/dataflow
|
||||
↓
|
||||
React Flow 기반 캔버스
|
||||
├── InputNode: 데이터 입력 (폼 데이터, 테이블 데이터)
|
||||
├── TransformNode: 데이터 변환 (매핑, 필터링, 계산)
|
||||
├── DatabaseNode: DB 조회/저장
|
||||
├── RestApiNode: 외부 API 호출
|
||||
├── ConditionNode: 조건 분기
|
||||
├── LoopNode: 반복 처리
|
||||
├── MergeNode: 데이터 합치기
|
||||
└── OutputNode: 결과 출력
|
||||
```
|
||||
|
||||
### 11.2 데이터플로우 실행
|
||||
|
||||
```
|
||||
버튼 클릭 → 데이터플로우 트리거
|
||||
↓
|
||||
InputNode: formData 수집
|
||||
↓
|
||||
TransformNode: 데이터 가공
|
||||
↓
|
||||
ConditionNode: 조건 분기 (가격 > 10000?)
|
||||
├── Yes → DatabaseNode: INSERT INTO premium_products
|
||||
└── No → DatabaseNode: INSERT INTO standard_products
|
||||
↓
|
||||
OutputNode: 결과 반환 → toast.success("저장 완료")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 대시보드 시스템
|
||||
|
||||
### 12.1 구조
|
||||
|
||||
```
|
||||
관리자: /admin/screenMng/dashboardList
|
||||
└→ 대시보드 생성 → 위젯 추가 → 레이아웃 저장
|
||||
|
||||
사용자: /dashboard/[dashboardId]
|
||||
└→ 위젯 그리드 렌더링 → 실시간 데이터 표시
|
||||
```
|
||||
|
||||
### 12.2 위젯 종류
|
||||
|
||||
| 카테고리 | 위젯 | 역할 |
|
||||
|----------|------|------|
|
||||
| 시각화 | CustomMetricWidget | 커스텀 메트릭 표시 |
|
||||
| | StatusSummaryWidget | 상태 요약 |
|
||||
| 리스트 | CargoListWidget | 화물 목록 |
|
||||
| | VehicleListWidget | 차량 목록 |
|
||||
| 지도 | MapTestWidget | 지도 표시 |
|
||||
| | WeatherMapWidget | 날씨 지도 |
|
||||
| 작업 | TodoWidget | 할일 목록 |
|
||||
| | WorkHistoryWidget | 작업 이력 |
|
||||
| 알림 | BookingAlertWidget | 예약 알림 |
|
||||
| | RiskAlertWidget | 위험 알림 |
|
||||
| 기타 | ClockWidget | 시계 |
|
||||
| | CalendarWidget | 캘린더 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 배치/스케줄 시스템
|
||||
|
||||
### 13.1 구조
|
||||
|
||||
```
|
||||
관리자: /admin/automaticMng/batchmngList
|
||||
↓
|
||||
배치 작업 생성:
|
||||
├── 이름: "일일 재고 집계"
|
||||
├── 실행 쿼리: SQL 또는 데이터플로우 ID
|
||||
├── 스케줄: Cron 표현식 ("0 0 * * *" = 매일 자정)
|
||||
└── 활성화/비활성화
|
||||
↓
|
||||
배치 스케줄러 (batch_schedules)
|
||||
↓
|
||||
자동 실행 → 실행 로그 (batch_execution_logs)
|
||||
```
|
||||
|
||||
### 13.2 배치 실행 흐름
|
||||
|
||||
```
|
||||
Cron 트리거 → 배치 정의 조회 → SQL/데이터플로우 실행
|
||||
↓
|
||||
성공: execution_log에 "SUCCESS" 기록
|
||||
실패: execution_log에 "FAILED" + 에러 메시지 기록
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 멀티테넌시 아키텍처
|
||||
|
||||
### 14.1 핵심 원칙
|
||||
|
||||
```
|
||||
모든 비즈니스 테이블: company_code 컬럼 필수
|
||||
모든 쿼리: WHERE company_code = $1 필수
|
||||
모든 JOIN: ON a.company_code = b.company_code 필수
|
||||
모든 집계: GROUP BY company_code 필수
|
||||
```
|
||||
|
||||
### 14.2 데이터 격리
|
||||
|
||||
```
|
||||
회사 A (company_code = "COMPANY_A"):
|
||||
└→ 자기 데이터만 조회/수정/삭제 가능
|
||||
|
||||
회사 B (company_code = "COMPANY_B"):
|
||||
└→ 자기 데이터만 조회/수정/삭제 가능
|
||||
|
||||
슈퍼관리자 (company_code = "*"):
|
||||
└→ 모든 회사 데이터 조회 가능
|
||||
└→ 일반 회사는 "*" 데이터를 볼 수 없음
|
||||
|
||||
중요: company_code = "*"는 공통 데이터가 아니라 슈퍼관리자 전용 데이터!
|
||||
```
|
||||
|
||||
### 14.3 코드 패턴
|
||||
|
||||
```typescript
|
||||
// 백엔드 표준 패턴
|
||||
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];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 외부 연동
|
||||
|
||||
### 15.1 외부 DB 연결
|
||||
|
||||
```
|
||||
지원 DB: PostgreSQL, MySQL, MariaDB, MSSQL, Oracle
|
||||
|
||||
관리: /api/external-db-connections
|
||||
├── 연결 정보 등록 (host, port, database, credentials)
|
||||
├── 연결 테스트
|
||||
├── 쿼리 실행
|
||||
└── 데이터플로우에서 DatabaseNode로 사용
|
||||
```
|
||||
|
||||
### 15.2 외부 REST API 연결
|
||||
|
||||
```
|
||||
관리: /api/external-rest-api-connections
|
||||
├── API 엔드포인트 등록 (URL, method, headers)
|
||||
├── 인증 설정 (Bearer, Basic, API Key)
|
||||
├── 테스트 호출
|
||||
└── 데이터플로우에서 RestApiNode로 사용
|
||||
```
|
||||
|
||||
### 15.3 메일 시스템
|
||||
|
||||
```
|
||||
관리: /admin/automaticMng/mail/*
|
||||
├── 메일 템플릿 관리
|
||||
├── 메일 발송 (개별/대량)
|
||||
├── 수신 메일 확인
|
||||
└── 발송 이력 조회
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. 배포 환경
|
||||
|
||||
### 16.1 Docker 구성
|
||||
|
||||
```
|
||||
개발 환경 (Mac):
|
||||
├── docker/dev/docker-compose.backend.mac.yml (BE: 8080)
|
||||
└── docker/dev/docker-compose.frontend.mac.yml (FE: 9771)
|
||||
|
||||
운영 환경:
|
||||
├── docker/prod/docker-compose.backend.prod.yml (BE: 8080)
|
||||
└── docker/prod/docker-compose.frontend.prod.yml (FE: 5555)
|
||||
```
|
||||
|
||||
### 16.2 서버 정보
|
||||
|
||||
| 환경 | 서버 | 포트 | DB |
|
||||
|------|------|------|-----|
|
||||
| 개발 | 39.117.244.52 | FE:9771, BE:8080 | 39.117.244.52:11132 |
|
||||
| 운영 | 211.115.91.141 | FE:5555, BE:8080 | 211.115.91.141:11134 |
|
||||
|
||||
### 16.3 백엔드 시작 시 자동 작업
|
||||
|
||||
```
|
||||
서버 시작 (app.ts)
|
||||
├── 마이그레이션 실행 (DB 스키마 업데이트)
|
||||
├── 배치 스케줄러 초기화
|
||||
├── 위험 알림 캐시 로드
|
||||
└── 메일 정리 Cron 시작
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 부록: 업무 진행 요약
|
||||
|
||||
### 새로운 업무 화면을 만드는 전체 프로세스
|
||||
|
||||
```
|
||||
1. [DB] 테이블 관리에서 비즈니스 테이블 생성
|
||||
└→ 컬럼 정의, 타입 설정
|
||||
|
||||
2. [화면] 화면 관리에서 새 화면 생성
|
||||
└→ 메인 테이블 지정
|
||||
|
||||
3. [디자인] 화면 디자이너에서 UI 구성
|
||||
└→ V2 컴포넌트 배치, 데이터 바인딩
|
||||
|
||||
4. [로직] 데이터플로우 설계 (필요시)
|
||||
└→ 저장/수정/삭제 로직 다이어그램
|
||||
|
||||
5. [플로우] 플로우 정의 (승인 프로세스 필요시)
|
||||
└→ 단계 정의, 연결
|
||||
|
||||
6. [메뉴] 메뉴에 화면 할당
|
||||
└→ 사용자가 접근할 수 있게 메뉴 트리 배치
|
||||
|
||||
7. [권한] 권한 그룹에 메뉴 할당
|
||||
└→ 특정 사용자 그룹만 접근 가능하게
|
||||
|
||||
8. [사용] 사용자가 메뉴 클릭 → 업무 시작!
|
||||
```
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
# WACE ERP Backend - 분석 문서 인덱스
|
||||
|
||||
> **분석 완료일**: 2026-02-06
|
||||
> **분석자**: Backend Specialist
|
||||
|
||||
---
|
||||
|
||||
## 📚 문서 목록
|
||||
|
||||
### 1. 📖 상세 분석 문서
|
||||
**파일**: `backend-architecture-detailed-analysis.md`
|
||||
**내용**: 백엔드 전체 아키텍처 상세 분석 (16개 섹션)
|
||||
|
||||
- 전체 개요 및 기술 스택
|
||||
- 디렉토리 구조
|
||||
- 미들웨어 스택 구성
|
||||
- 인증/인가 시스템 (JWT, 3단계 권한)
|
||||
- 멀티테넌시 구현 방식
|
||||
- API 라우트 전체 목록
|
||||
- 비즈니스 도메인별 모듈 (8개 도메인)
|
||||
- 데이터베이스 접근 방식 (Raw Query)
|
||||
- 외부 시스템 연동 (DB/REST API)
|
||||
- 배치/스케줄 처리 (node-cron)
|
||||
- 파일 처리 (multer)
|
||||
- 에러 핸들링
|
||||
- 로깅 시스템 (Winston)
|
||||
- 보안 및 권한 관리
|
||||
- 성능 최적화
|
||||
|
||||
**특징**: 워크플로우 문서에 통합하기 위한 완전한 아키텍처 분석
|
||||
|
||||
---
|
||||
|
||||
### 2. 📄 요약 문서
|
||||
**파일**: `backend-architecture-summary.md`
|
||||
**내용**: 백엔드 아키텍처 핵심 요약 (16개 섹션 압축)
|
||||
|
||||
- 기술 스택 요약
|
||||
- 계층 구조 다이어그램
|
||||
- 디렉토리 구조
|
||||
- 미들웨어 스택 순서
|
||||
- 인증/인가 흐름도
|
||||
- 멀티테넌시 핵심 원칙
|
||||
- API 라우트 카테고리별 정리
|
||||
- 비즈니스 도메인 8개 요약
|
||||
- 데이터베이스 접근 패턴
|
||||
- 외부 연동 아키텍처
|
||||
- 배치 스케줄러 시스템
|
||||
- 파일 처리 흐름
|
||||
- 보안 정책
|
||||
- 에러 핸들링 전략
|
||||
- 로깅 구조
|
||||
- 성능 최적화 전략
|
||||
- **핵심 체크리스트** (개발 시 필수 규칙 8개)
|
||||
|
||||
**특징**: 빠른 참조를 위한 간결한 요약
|
||||
|
||||
---
|
||||
|
||||
### 3. 🔗 API 라우트 완전 매핑
|
||||
**파일**: `backend-api-route-mapping.md`
|
||||
**내용**: 프론트엔드 개발자용 API 엔드포인트 전체 목록 (200+개)
|
||||
|
||||
#### 포함된 API 카테고리
|
||||
1. 인증 API (7개)
|
||||
2. 관리자 API (15개)
|
||||
3. 테이블 관리 API (30개)
|
||||
4. 화면 관리 API (10개)
|
||||
5. 플로우 API (15개)
|
||||
6. 데이터플로우 API (10개)
|
||||
7. 외부 연동 API (15개)
|
||||
8. 배치 API (10개)
|
||||
9. 메일 API (5개)
|
||||
10. 파일 API (5개)
|
||||
11. 대시보드 API (5개)
|
||||
12. 공통코드 API (3개)
|
||||
13. 다국어 API (3개)
|
||||
14. 회사 관리 API (4개)
|
||||
15. 부서 API (2개)
|
||||
16. 권한 그룹 API (2개)
|
||||
17. DDL 실행 API (1개)
|
||||
18. 외부 API 프록시 (2개)
|
||||
19. 디지털 트윈 API (3개)
|
||||
20. 3D 필드 API (2개)
|
||||
21. 스케줄 API (1개)
|
||||
22. 채번 규칙 API (3개)
|
||||
23. 엔티티 검색 API (2개)
|
||||
24. To-Do API (3개)
|
||||
25. 예약 요청 API (2개)
|
||||
26. 리스크/알림 API (2개)
|
||||
27. 헬스 체크 (1개)
|
||||
|
||||
#### 각 API 정보 포함
|
||||
- HTTP 메서드
|
||||
- 엔드포인트 경로
|
||||
- 필요 권한 (공개/인증/관리자/슈퍼관리자)
|
||||
- 기능 설명
|
||||
- Request Body/Query Params
|
||||
- Response 형식
|
||||
|
||||
#### 추가 정보
|
||||
- Base URL (개발/운영)
|
||||
- 공통 헤더 (Authorization)
|
||||
- 응답 형식 (성공/에러)
|
||||
- 에러 코드 목록
|
||||
|
||||
**특징**: 프론트엔드에서 API 호출 시 즉시 참조 가능
|
||||
|
||||
---
|
||||
|
||||
### 4. 📊 JSON 응답 요약
|
||||
**파일**: `backend-analysis-response.json`
|
||||
**내용**: 구조화된 JSON 형식의 분석 결과
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"confidence": "high",
|
||||
"result": {
|
||||
"summary": "...",
|
||||
"details": "...",
|
||||
"files_affected": [...],
|
||||
"key_findings": {
|
||||
"architecture_pattern": "...",
|
||||
"tech_stack": {...},
|
||||
"middleware_stack": [...],
|
||||
"authentication_flow": {...},
|
||||
"permission_levels": {...},
|
||||
"multi_tenancy": {...},
|
||||
"business_domains": {...},
|
||||
"database_access": {...},
|
||||
"security": {...},
|
||||
"performance_optimization": {...}
|
||||
},
|
||||
"critical_rules": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**특징**: 프로그래밍 방식으로 분석 결과 활용 가능
|
||||
|
||||
---
|
||||
|
||||
## 🎯 핵심 요약
|
||||
|
||||
### 아키텍처
|
||||
- **패턴**: Layered Architecture (Controller → Service → Database)
|
||||
- **언어**: TypeScript (Strict Mode)
|
||||
- **프레임워크**: Express.js
|
||||
- **데이터베이스**: PostgreSQL (Raw Query, Connection Pool)
|
||||
- **인증**: JWT (24시간 만료, 자동 갱신)
|
||||
|
||||
### 멀티테넌시
|
||||
```typescript
|
||||
// ✅ 핵심 원칙
|
||||
const companyCode = req.user!.companyCode; // JWT에서 추출
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 슈퍼관리자: 모든 데이터
|
||||
query = "SELECT * FROM table ORDER BY company_code";
|
||||
} else {
|
||||
// 일반 사용자: 자기 회사만 + 슈퍼관리자 숨김
|
||||
query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'";
|
||||
params = [companyCode];
|
||||
}
|
||||
```
|
||||
|
||||
### 권한 체계 (3단계)
|
||||
1. **SUPER_ADMIN** (`company_code = "*"`)
|
||||
- 전체 회사 데이터 접근
|
||||
- DDL 실행, 회사 생성/삭제
|
||||
|
||||
2. **COMPANY_ADMIN** (`company_code = "ILSHIN"`)
|
||||
- 자기 회사 데이터만 접근
|
||||
- 사용자/설정 관리
|
||||
|
||||
3. **USER** (`company_code = "ILSHIN"`)
|
||||
- 자기 회사 데이터만 접근
|
||||
- 읽기/쓰기만
|
||||
|
||||
### 주요 도메인 (8개)
|
||||
1. **관리자** - 사용자/메뉴/권한
|
||||
2. **테이블/화면** - 메타데이터, 동적 화면
|
||||
3. **플로우** - 워크플로우 엔진
|
||||
4. **데이터플로우** - ERD, 관계도
|
||||
5. **외부 연동** - 외부 DB/REST API
|
||||
6. **배치** - Cron 스케줄러
|
||||
7. **메일** - 발송/수신
|
||||
8. **파일** - 업로드/다운로드
|
||||
|
||||
### API 통계
|
||||
- **총 라우트**: 70+개
|
||||
- **총 API**: 200+개
|
||||
- **컨트롤러**: 70+개
|
||||
- **서비스**: 80+개
|
||||
- **미들웨어**: 4개
|
||||
|
||||
---
|
||||
|
||||
## 🚨 개발 시 필수 규칙
|
||||
|
||||
✅ **모든 쿼리에 `company_code` 필터 추가**
|
||||
✅ **JWT 토큰에서 `company_code` 추출 (클라이언트 신뢰 금지)**
|
||||
✅ **Parameterized Query 사용 (SQL Injection 방지)**
|
||||
✅ **슈퍼관리자 데이터 숨김 (`company_code != '*'`)**
|
||||
✅ **비밀번호는 bcrypt, 민감정보는 AES-256**
|
||||
✅ **에러 핸들링 try/catch 필수**
|
||||
✅ **트랜잭션이 필요한 경우 `transaction()` 사용**
|
||||
✅ **파일 업로드는 회사별 디렉토리 분리**
|
||||
|
||||
---
|
||||
|
||||
## 📁 문서 위치
|
||||
|
||||
```
|
||||
ERP-node/docs/
|
||||
├── backend-architecture-detailed-analysis.md (상세 분석, 16개 섹션)
|
||||
├── backend-architecture-summary.md (요약, 간결한 참조)
|
||||
├── backend-api-route-mapping.md (API 200+개 전체 매핑)
|
||||
└── backend-analysis-response.json (JSON 구조화 데이터)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 문서 사용 가이드
|
||||
|
||||
### 처음 백엔드를 이해하려면
|
||||
→ `backend-architecture-summary.md` 읽기 (20분)
|
||||
|
||||
### 특정 기능을 구현하려면
|
||||
→ `backend-architecture-detailed-analysis.md`에서 해당 도메인 섹션 참조
|
||||
|
||||
### API를 호출하려면
|
||||
→ `backend-api-route-mapping.md`에서 엔드포인트 검색
|
||||
|
||||
### 워크플로우 문서에 통합하려면
|
||||
→ `backend-architecture-detailed-analysis.md` 전체 복사
|
||||
|
||||
### 프로그래밍 방식으로 활용하려면
|
||||
→ `backend-analysis-response.json` 파싱
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**마지막 업데이트**: 2026-02-06
|
||||
**다음 업데이트 예정**: 신규 API 추가 시
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
{
|
||||
"status": "success",
|
||||
"confidence": "high",
|
||||
"result": {
|
||||
"summary": "WACE ERP 백엔드 전체 아키텍처 분석 완료",
|
||||
"details": "Node.js + Express + TypeScript + PostgreSQL Raw Query 기반 멀티테넌시 시스템. 70+ 라우트, 70+ 컨트롤러, 80+ 서비스로 구성된 계층형 아키텍처. JWT 인증, 3단계 권한 체계(SUPER_ADMIN/COMPANY_ADMIN/USER), company_code 기반 완전한 데이터 격리 구현.",
|
||||
"files_affected": [
|
||||
"docs/backend-architecture-detailed-analysis.md (상세 분석 문서)",
|
||||
"docs/backend-architecture-summary.md (요약 문서)",
|
||||
"docs/backend-api-route-mapping.md (API 라우트 전체 매핑)"
|
||||
],
|
||||
"key_findings": {
|
||||
"architecture_pattern": "Layered Architecture (Controller → Service → Database)",
|
||||
"tech_stack": {
|
||||
"language": "TypeScript",
|
||||
"runtime": "Node.js 20.10.0+",
|
||||
"framework": "Express.js",
|
||||
"database": "PostgreSQL (pg 라이브러리, Raw Query)",
|
||||
"authentication": "JWT (jsonwebtoken)",
|
||||
"scheduler": "node-cron",
|
||||
"external_db_support": ["PostgreSQL", "MySQL", "MSSQL", "Oracle"]
|
||||
},
|
||||
"directory_structure": {
|
||||
"controllers": "70+ 파일 (API 요청 수신, 응답 생성)",
|
||||
"services": "80+ 파일 (비즈니스 로직, 트랜잭션 관리)",
|
||||
"routes": "70+ 파일 (API 라우팅)",
|
||||
"middleware": "4개 (인증, 권한, 슈퍼관리자, 에러핸들러)",
|
||||
"types": "26개 (TypeScript 타입 정의)",
|
||||
"utils": "유틸리티 함수 (JWT, 암호화, 로거)"
|
||||
},
|
||||
"middleware_stack": [
|
||||
"1. Process Level Exception Handlers",
|
||||
"2. Helmet (보안 헤더)",
|
||||
"3. Compression (Gzip)",
|
||||
"4. Body Parser (10MB limit)",
|
||||
"5. Static Files (/uploads)",
|
||||
"6. CORS (credentials: true)",
|
||||
"7. Rate Limiting (1분 10000회)",
|
||||
"8. Token Auto Refresh (1시간 이내 만료 시 갱신)",
|
||||
"9. API Routes (70+개)",
|
||||
"10. 404 Handler",
|
||||
"11. Error Handler"
|
||||
],
|
||||
"authentication_flow": {
|
||||
"step1": "로그인 요청 → AuthController.login()",
|
||||
"step2": "AuthService.processLogin() → loginPwdCheck() (bcrypt 검증)",
|
||||
"step3": "getPersonBeanFromSession() → 사용자 정보 조회",
|
||||
"step4": "insertLoginAccessLog() → 로그인 이력 저장",
|
||||
"step5": "JwtUtils.generateToken() → JWT 토큰 생성",
|
||||
"step6": "응답: { token, userInfo, firstMenuPath }"
|
||||
},
|
||||
"jwt_payload": {
|
||||
"userId": "사용자 ID",
|
||||
"userName": "사용자명",
|
||||
"companyCode": "회사 코드 (멀티테넌시 키)",
|
||||
"userType": "권한 레벨 (SUPER_ADMIN/COMPANY_ADMIN/USER)",
|
||||
"exp": "만료 시간 (24시간)"
|
||||
},
|
||||
"permission_levels": {
|
||||
"SUPER_ADMIN": {
|
||||
"company_code": "*",
|
||||
"userType": "SUPER_ADMIN",
|
||||
"capabilities": [
|
||||
"전체 회사 데이터 접근",
|
||||
"DDL 실행",
|
||||
"회사 생성/삭제",
|
||||
"시스템 설정 변경"
|
||||
]
|
||||
},
|
||||
"COMPANY_ADMIN": {
|
||||
"company_code": "특정 회사 (예: ILSHIN)",
|
||||
"userType": "COMPANY_ADMIN",
|
||||
"capabilities": [
|
||||
"자기 회사 데이터만 접근",
|
||||
"자기 회사 사용자 관리",
|
||||
"회사 설정 변경"
|
||||
]
|
||||
},
|
||||
"USER": {
|
||||
"company_code": "특정 회사",
|
||||
"userType": "USER",
|
||||
"capabilities": [
|
||||
"자기 회사 데이터만 접근",
|
||||
"읽기/쓰기 권한만"
|
||||
]
|
||||
}
|
||||
},
|
||||
"multi_tenancy": {
|
||||
"principle": "모든 쿼리에 company_code 필터 필수",
|
||||
"pattern": "JWT 토큰에서 company_code 추출 (클라이언트 신뢰 금지)",
|
||||
"super_admin_visibility": "일반 회사 사용자에게 슈퍼관리자(company_code='*') 숨김",
|
||||
"correct_pattern": "WHERE company_code = $1 AND company_code != '*'",
|
||||
"wrong_pattern": "req.body.companyCode 사용 (보안 위험!)"
|
||||
},
|
||||
"api_routes": {
|
||||
"total_count": "200+개",
|
||||
"categories": {
|
||||
"인증/관리자": "15개",
|
||||
"테이블/화면": "40개",
|
||||
"플로우": "15개",
|
||||
"데이터플로우": "5개",
|
||||
"외부 연동": "15개",
|
||||
"배치": "10개",
|
||||
"메일": "5개",
|
||||
"파일": "5개",
|
||||
"기타": "90개"
|
||||
}
|
||||
},
|
||||
"business_domains": {
|
||||
"관리자": {
|
||||
"controller": "adminController.ts",
|
||||
"service": "adminService.ts",
|
||||
"features": ["사용자 관리", "메뉴 관리", "권한 그룹 관리", "시스템 설정"]
|
||||
},
|
||||
"테이블/화면": {
|
||||
"controller": "tableManagementController.ts, screenManagementController.ts",
|
||||
"service": "tableManagementService.ts, screenManagementService.ts",
|
||||
"features": ["테이블 메타데이터", "화면 정의", "화면 그룹", "테이블 로그", "엔티티 관계"]
|
||||
},
|
||||
"플로우": {
|
||||
"controller": "flowController.ts",
|
||||
"service": "flowExecutionService.ts, flowDefinitionService.ts",
|
||||
"features": ["워크플로우 설계", "단계 관리", "데이터 이동", "조건부 이동", "오딧 로그"]
|
||||
},
|
||||
"데이터플로우": {
|
||||
"controller": "dataflowController.ts, dataflowDiagramController.ts",
|
||||
"service": "dataflowService.ts, dataflowDiagramService.ts",
|
||||
"features": ["테이블 관계 정의", "ERD", "다이어그램 시각화", "관계 실행"]
|
||||
},
|
||||
"외부 연동": {
|
||||
"controller": "externalDbConnectionController.ts, externalRestApiConnectionController.ts",
|
||||
"service": "externalDbConnectionService.ts, dbConnectionManager.ts",
|
||||
"features": ["외부 DB 연결", "Connection Pool 관리", "REST API 프록시"]
|
||||
},
|
||||
"배치": {
|
||||
"controller": "batchController.ts, batchManagementController.ts",
|
||||
"service": "batchService.ts, batchSchedulerService.ts",
|
||||
"features": ["Cron 스케줄러", "외부 DB → 내부 DB 동기화", "컬럼 매핑", "실행 이력"]
|
||||
},
|
||||
"메일": {
|
||||
"controller": "mailSendSimpleController.ts, mailReceiveBasicController.ts",
|
||||
"service": "mailSendSimpleService.ts, mailReceiveBasicService.ts",
|
||||
"features": ["메일 발송 (nodemailer)", "메일 수신 (IMAP)", "템플릿 관리", "첨부파일"]
|
||||
},
|
||||
"파일": {
|
||||
"controller": "fileController.ts, screenFileController.ts",
|
||||
"service": "fileSystemManager.ts",
|
||||
"features": ["파일 업로드 (multer)", "파일 다운로드", "화면별 파일 관리"]
|
||||
}
|
||||
},
|
||||
"database_access": {
|
||||
"connection_pool": {
|
||||
"min": "2~5 (환경별)",
|
||||
"max": "10~20 (환경별)",
|
||||
"connectionTimeout": "30000ms",
|
||||
"idleTimeout": "600000ms",
|
||||
"statementTimeout": "60000ms"
|
||||
},
|
||||
"query_patterns": {
|
||||
"multi_row": "query('SELECT ...', [params])",
|
||||
"single_row": "queryOne('SELECT ...', [params])",
|
||||
"transaction": "transaction(async (client) => { ... })"
|
||||
},
|
||||
"sql_injection_prevention": "Parameterized Query 사용 (pg 라이브러리)"
|
||||
},
|
||||
"external_integration": {
|
||||
"supported_databases": ["PostgreSQL", "MySQL", "MSSQL", "Oracle"],
|
||||
"connector_pattern": "Factory Pattern (DatabaseConnectorFactory)",
|
||||
"rest_api": "axios 기반 프록시"
|
||||
},
|
||||
"batch_scheduler": {
|
||||
"library": "node-cron",
|
||||
"timezone": "Asia/Seoul",
|
||||
"cron_examples": {
|
||||
"매일 새벽 2시": "0 2 * * *",
|
||||
"5분마다": "*/5 * * * *",
|
||||
"평일 오전 8시": "0 8 * * 1-5"
|
||||
},
|
||||
"execution_flow": [
|
||||
"1. 소스 DB에서 데이터 조회",
|
||||
"2. 컬럼 매핑 적용",
|
||||
"3. 타겟 DB에 INSERT/UPDATE",
|
||||
"4. 실행 로그 기록"
|
||||
]
|
||||
},
|
||||
"file_handling": {
|
||||
"upload_path": "uploads/{company_code}/{timestamp}-{uuid}-{filename}",
|
||||
"max_file_size": "10MB",
|
||||
"allowed_types": ["이미지", "PDF", "Office 문서"],
|
||||
"library": "multer"
|
||||
},
|
||||
"security": {
|
||||
"password_encryption": "bcrypt (12 rounds)",
|
||||
"sensitive_data_encryption": "AES-256-CBC (외부 DB 비밀번호)",
|
||||
"jwt_secret": "환경변수 관리",
|
||||
"security_headers": ["Helmet (CSP, X-Frame-Options)", "CORS (credentials: true)", "Rate Limiting (1분 10000회)"],
|
||||
"sql_injection_prevention": "Parameterized Query"
|
||||
},
|
||||
"error_handling": {
|
||||
"postgres_error_codes": {
|
||||
"23505": "중복된 데이터",
|
||||
"23503": "참조 무결성 위반",
|
||||
"23502": "필수 입력값 누락"
|
||||
},
|
||||
"process_level": {
|
||||
"unhandledRejection": "로깅 (서버 유지)",
|
||||
"uncaughtException": "로깅 (서버 유지, 주의)",
|
||||
"SIGTERM/SIGINT": "Graceful Shutdown"
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"library": "Winston",
|
||||
"log_files": {
|
||||
"error.log": "에러만 (10MB × 5파일)",
|
||||
"combined.log": "전체 로그 (10MB × 10파일)"
|
||||
},
|
||||
"log_levels": "error (0) → warn (1) → info (2) → debug (5)"
|
||||
},
|
||||
"performance_optimization": {
|
||||
"pool_monitoring": "5분마다 상태 체크, 대기 연결 5개 이상 시 경고",
|
||||
"slow_query_detection": "1초 이상 걸린 쿼리 자동 경고",
|
||||
"caching": "Redis (메뉴: 10분 TTL, 공통코드: 30분 TTL)",
|
||||
"compression": "Gzip (1KB 이상 응답, 레벨 6)"
|
||||
}
|
||||
},
|
||||
"critical_rules": [
|
||||
"✅ 모든 쿼리에 company_code 필터 추가",
|
||||
"✅ JWT 토큰에서 company_code 추출 (클라이언트 신뢰 금지)",
|
||||
"✅ Parameterized Query 사용 (SQL Injection 방지)",
|
||||
"✅ 슈퍼관리자 데이터 숨김 (company_code != '*')",
|
||||
"✅ 비밀번호는 bcrypt, 민감정보는 AES-256",
|
||||
"✅ 에러 핸들링 try/catch 필수",
|
||||
"✅ 트랜잭션이 필요한 경우 transaction() 사용",
|
||||
"✅ 파일 업로드는 회사별 디렉토리 분리"
|
||||
]
|
||||
},
|
||||
"needs_from_others": [],
|
||||
"questions": []
|
||||
}
|
||||
|
|
@ -0,0 +1,542 @@
|
|||
# WACE ERP Backend - API 라우트 완전 매핑
|
||||
|
||||
> **작성일**: 2026-02-06
|
||||
> **목적**: 프론트엔드 개발자용 API 엔드포인트 전체 목록
|
||||
|
||||
---
|
||||
|
||||
## 📌 공통 규칙
|
||||
|
||||
### Base URL
|
||||
```
|
||||
개발: http://localhost:8080
|
||||
운영: http://39.117.244.52:8080
|
||||
```
|
||||
|
||||
### 헤더
|
||||
```http
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {JWT_TOKEN}
|
||||
```
|
||||
|
||||
### 응답 형식
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "성공 메시지",
|
||||
"data": { ... }
|
||||
}
|
||||
|
||||
// 에러 시
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"details": "에러 상세"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 인증 API (`/api/auth`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/auth/login` | 공개 | 로그인 | `{ userId, password }` | `{ token, userInfo, firstMenuPath }` |
|
||||
| POST | `/auth/logout` | 인증 | 로그아웃 | - | `{ success: true }` |
|
||||
| GET | `/auth/me` | 인증 | 현재 사용자 정보 | - | `{ userInfo }` |
|
||||
| GET | `/auth/status` | 공개 | 인증 상태 확인 | - | `{ isLoggedIn, isAdmin }` |
|
||||
| POST | `/auth/refresh` | 인증 | 토큰 갱신 | - | `{ token }` |
|
||||
| POST | `/auth/signup` | 공개 | 회원가입 (공차중계) | `{ userId, password, userName, phoneNumber, licenseNumber, vehicleNumber }` | `{ success: true }` |
|
||||
| POST | `/auth/switch-company` | 슈퍼관리자 | 회사 전환 | `{ companyCode }` | `{ token, companyCode }` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 관리자 API (`/api/admin`)
|
||||
|
||||
### 2.1 사용자 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/admin/users` | 관리자 | 사용자 목록 | `page, limit, search` | `{ users[], total }` |
|
||||
| POST | `/admin/users` | 관리자 | 사용자 생성 | - | `{ user }` |
|
||||
| PUT | `/admin/users/:userId` | 관리자 | 사용자 수정 | - | `{ user }` |
|
||||
| DELETE | `/admin/users/:userId` | 관리자 | 사용자 삭제 | - | `{ success: true }` |
|
||||
| GET | `/admin/users/:userId/history` | 관리자 | 사용자 이력 | - | `{ history[] }` |
|
||||
|
||||
### 2.2 메뉴 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/admin/menus` | 인증 | 메뉴 목록 (트리) | `userId, userLang` | `{ menus[] }` |
|
||||
| POST | `/admin/menus` | 관리자 | 메뉴 생성 | - | `{ menu }` |
|
||||
| PUT | `/admin/menus/:menuId` | 관리자 | 메뉴 수정 | - | `{ menu }` |
|
||||
| DELETE | `/admin/menus/:menuId` | 관리자 | 메뉴 삭제 | - | `{ success: true }` |
|
||||
|
||||
### 2.3 표준 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/admin/web-types` | 인증 | 웹타입 표준 목록 | `{ webTypes[] }` |
|
||||
| GET | `/admin/button-actions` | 인증 | 버튼 액션 표준 | `{ buttonActions[] }` |
|
||||
| GET | `/admin/component-standards` | 인증 | 컴포넌트 표준 | `{ components[] }` |
|
||||
| GET | `/admin/template-standards` | 인증 | 템플릿 표준 | `{ templates[] }` |
|
||||
| GET | `/admin/reports` | 인증 | 리포트 목록 | `{ reports[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 테이블 관리 API (`/api/table-management`)
|
||||
|
||||
### 3.1 테이블 메타데이터
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/table-management/tables` | 인증 | 테이블 목록 | `{ tables[] }` |
|
||||
| GET | `/table-management/tables/:table/columns` | 인증 | 컬럼 목록 | `{ columns[] }` |
|
||||
| GET | `/table-management/tables/:table/schema` | 인증 | 테이블 스키마 | `{ schema }` |
|
||||
| GET | `/table-management/tables/:table/exists` | 인증 | 테이블 존재 여부 | `{ exists: boolean }` |
|
||||
| GET | `/table-management/tables/:table/web-types` | 인증 | 웹타입 정보 | `{ webTypes }` |
|
||||
|
||||
### 3.2 컬럼 설정
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body |
|
||||
|--------|------|------|------|--------------|
|
||||
| POST | `/table-management/tables/:table/columns/:column/settings` | 인증 | 컬럼 설정 업데이트 | `{ web_type, input_type, ... }` |
|
||||
| POST | `/table-management/tables/:table/columns/settings` | 인증 | 전체 컬럼 일괄 업데이트 | `{ columns[] }` |
|
||||
| PUT | `/table-management/tables/:table/label` | 인증 | 테이블 라벨 설정 | `{ label }` |
|
||||
|
||||
### 3.3 데이터 CRUD
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/table-management/tables/:table/data` | 인증 | 데이터 조회 (페이징) | `{ page, limit, filters, sort }` | `{ data[], total }` |
|
||||
| POST | `/table-management/tables/:table/record` | 인증 | 단일 레코드 조회 | `{ conditions }` | `{ record }` |
|
||||
| POST | `/table-management/tables/:table/add` | 인증 | 데이터 추가 | `{ data }` | `{ success: true, id }` |
|
||||
| PUT | `/table-management/tables/:table/edit` | 인증 | 데이터 수정 | `{ conditions, data }` | `{ success: true }` |
|
||||
| DELETE | `/table-management/tables/:table/delete` | 인증 | 데이터 삭제 | `{ conditions }` | `{ success: true }` |
|
||||
|
||||
### 3.4 다중 테이블 저장
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body |
|
||||
|--------|------|------|------|--------------|
|
||||
| POST | `/table-management/multi-table-save` | 인증 | 메인+서브 테이블 저장 | `{ mainTable, mainData, subTables: [{ table, data[] }] }` |
|
||||
|
||||
### 3.5 로그 시스템
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body |
|
||||
|--------|------|------|------|--------------|
|
||||
| POST | `/table-management/tables/:table/log` | 관리자 | 로그 테이블 생성 | - |
|
||||
| GET | `/table-management/tables/:table/log/config` | 인증 | 로그 설정 조회 | - |
|
||||
| GET | `/table-management/tables/:table/log` | 인증 | 로그 데이터 조회 | - |
|
||||
| POST | `/table-management/tables/:table/log/toggle` | 관리자 | 로그 활성화/비활성화 | `{ is_active }` |
|
||||
|
||||
### 3.6 엔티티 관계
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params |
|
||||
|--------|------|------|------|--------------|
|
||||
| GET | `/table-management/tables/entity-relations` | 인증 | 두 테이블 간 관계 조회 | `leftTable, rightTable` |
|
||||
| GET | `/table-management/columns/:table/referenced-by` | 인증 | 현재 테이블 참조 목록 | - |
|
||||
|
||||
### 3.7 카테고리 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/table-management/category-columns` | 인증 | 회사별 카테고리 컬럼 | `{ categoryColumns[] }` |
|
||||
| GET | `/table-management/menu/:menuObjid/category-columns` | 인증 | 메뉴별 카테고리 컬럼 | `{ categoryColumns[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 화면 관리 API (`/api/screen-management`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/screen-management/screens` | 인증 | 화면 목록 | `page, limit` | `{ screens[], total }` |
|
||||
| GET | `/screen-management/screens/:id` | 인증 | 화면 상세 | - | `{ screen }` |
|
||||
| POST | `/screen-management/screens` | 관리자 | 화면 생성 | - | `{ screen }` |
|
||||
| PUT | `/screen-management/screens/:id` | 관리자 | 화면 수정 | - | `{ screen }` |
|
||||
| DELETE | `/screen-management/screens/:id` | 관리자 | 화면 삭제 | - | `{ success: true }` |
|
||||
|
||||
### 화면 그룹
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/screen-groups` | 인증 | 화면 그룹 목록 | `{ screenGroups[] }` |
|
||||
| POST | `/screen-groups` | 관리자 | 그룹 생성 | `{ group }` |
|
||||
|
||||
### 화면 파일
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/screen-files` | 인증 | 화면 파일 목록 | `{ files[] }` |
|
||||
| POST | `/screen-files` | 관리자 | 파일 업로드 | `{ file }` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 플로우 API (`/api/flow`)
|
||||
|
||||
### 5.1 플로우 정의
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/flow/definitions` | 인증 | 플로우 목록 | - | `{ flows[] }` |
|
||||
| GET | `/flow/definitions/:id` | 인증 | 플로우 상세 | - | `{ flow }` |
|
||||
| POST | `/flow/definitions` | 인증 | 플로우 생성 | `{ name, description, targetTable }` | `{ flow }` |
|
||||
| PUT | `/flow/definitions/:id` | 인증 | 플로우 수정 | `{ name, description }` | `{ flow }` |
|
||||
| DELETE | `/flow/definitions/:id` | 인증 | 플로우 삭제 | - | `{ success: true }` |
|
||||
|
||||
### 5.2 단계 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/flow/definitions/:flowId/steps` | 인증 | 단계 목록 | - | `{ steps[] }` |
|
||||
| POST | `/flow/definitions/:flowId/steps` | 인증 | 단계 생성 | `{ name, type, settings }` | `{ step }` |
|
||||
| PUT | `/flow/steps/:stepId` | 인증 | 단계 수정 | `{ name, settings }` | `{ step }` |
|
||||
| DELETE | `/flow/steps/:stepId` | 인증 | 단계 삭제 | - | `{ success: true }` |
|
||||
|
||||
### 5.3 연결 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/flow/connections/:flowId` | 인증 | 연결 목록 | - | `{ connections[] }` |
|
||||
| POST | `/flow/connections` | 인증 | 연결 생성 | `{ fromStepId, toStepId, condition }` | `{ connection }` |
|
||||
| DELETE | `/flow/connections/:connectionId` | 인증 | 연결 삭제 | - | `{ success: true }` |
|
||||
|
||||
### 5.4 데이터 이동
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/flow/move` | 인증 | 데이터 이동 (단건) | `{ flowId, fromStepId, toStepId, recordId }` | `{ success: true }` |
|
||||
| POST | `/flow/move-batch` | 인증 | 데이터 이동 (다건) | `{ flowId, fromStepId, toStepId, recordIds[] }` | `{ success: true, movedCount }` |
|
||||
|
||||
### 5.5 단계 데이터 조회
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/flow/:flowId/step/:stepId/count` | 인증 | 단계 데이터 개수 | - | `{ count }` |
|
||||
| GET | `/flow/:flowId/step/:stepId/list` | 인증 | 단계 데이터 목록 | `page, limit` | `{ data[], total }` |
|
||||
| GET | `/flow/:flowId/step/:stepId/column-labels` | 인증 | 컬럼 라벨 조회 | - | `{ labels }` |
|
||||
| GET | `/flow/:flowId/steps/counts` | 인증 | 모든 단계 카운트 | - | `{ counts[] }` |
|
||||
|
||||
### 5.6 단계 데이터 수정
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| PUT | `/flow/:flowId/step/:stepId/data/:recordId` | 인증 | 인라인 편집 | `{ data }` | `{ success: true }` |
|
||||
|
||||
### 5.7 오딧 로그
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/flow/audit/:flowId/:recordId` | 인증 | 레코드별 오딧 로그 | `{ auditLogs[] }` |
|
||||
| GET | `/flow/audit/:flowId` | 인증 | 플로우 전체 오딧 로그 | `{ auditLogs[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 데이터플로우 API (`/api/dataflow`)
|
||||
|
||||
### 6.1 관계 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/dataflow/relationships` | 인증 | 관계 목록 | - | `{ relationships[] }` |
|
||||
| POST | `/dataflow/relationships` | 인증 | 관계 생성 | `{ fromTable, toTable, fromColumn, toColumn, type }` | `{ relationship }` |
|
||||
| PUT | `/dataflow/relationships/:id` | 인증 | 관계 수정 | `{ name, type }` | `{ relationship }` |
|
||||
| DELETE | `/dataflow/relationships/:id` | 인증 | 관계 삭제 | - | `{ success: true }` |
|
||||
|
||||
### 6.2 다이어그램
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/dataflow-diagrams` | 인증 | 다이어그램 목록 | - | `{ diagrams[] }` |
|
||||
| GET | `/dataflow-diagrams/:id` | 인증 | 다이어그램 상세 | - | `{ diagram }` |
|
||||
| POST | `/dataflow-diagrams` | 인증 | 다이어그램 생성 | `{ name, description }` | `{ diagram }` |
|
||||
|
||||
### 6.3 실행
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/dataflow` | 인증 | 데이터플로우 실행 | `{ relationshipId, params }` | `{ result[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 외부 연동 API
|
||||
|
||||
### 7.1 외부 DB 연결 (`/api/external-db-connections`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/external-db-connections` | 인증 | 연결 목록 | - | `{ connections[] }` |
|
||||
| GET | `/external-db-connections/:id` | 인증 | 연결 상세 | - | `{ connection }` |
|
||||
| POST | `/external-db-connections` | 관리자 | 연결 생성 | `{ connectionName, dbType, host, port, database, username, password }` | `{ connection }` |
|
||||
| PUT | `/external-db-connections/:id` | 관리자 | 연결 수정 | `{ connectionName, ... }` | `{ connection }` |
|
||||
| DELETE | `/external-db-connections/:id` | 관리자 | 연결 삭제 | - | `{ success: true }` |
|
||||
| POST | `/external-db-connections/:id/test` | 인증 | 연결 테스트 | - | `{ success: boolean, message }` |
|
||||
| GET | `/external-db-connections/:id/tables` | 인증 | 테이블 목록 조회 | - | `{ tables[] }` |
|
||||
| GET | `/external-db-connections/:id/tables/:table/columns` | 인증 | 컬럼 목록 조회 | - | `{ columns[] }` |
|
||||
|
||||
### 7.2 외부 REST API (`/api/external-rest-api-connections`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/external-rest-api-connections` | 인증 | API 연결 목록 | - | `{ connections[] }` |
|
||||
| POST | `/external-rest-api-connections` | 관리자 | API 연결 생성 | `{ name, baseUrl, authType, ... }` | `{ connection }` |
|
||||
| POST | `/external-rest-api-connections/:id/test` | 인증 | API 테스트 | `{ endpoint, method }` | `{ response }` |
|
||||
|
||||
### 7.3 멀티 커넥션 (`/api/multi-connection`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/multi-connection/query` | 인증 | 멀티 DB 쿼리 | `{ connections: [{ connectionId, sql }] }` | `{ results[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 배치 API
|
||||
|
||||
### 8.1 배치 설정 (`/api/batch-configs`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/batch-configs` | 인증 | 배치 설정 목록 | - | `{ batchConfigs[] }` |
|
||||
| GET | `/batch-configs/:id` | 인증 | 배치 설정 상세 | - | `{ batchConfig }` |
|
||||
| POST | `/batch-configs` | 관리자 | 배치 설정 생성 | `{ batchName, cronSchedule, sourceConnection, targetTable, mappings }` | `{ batchConfig }` |
|
||||
| PUT | `/batch-configs/:id` | 관리자 | 배치 설정 수정 | `{ batchName, ... }` | `{ batchConfig }` |
|
||||
| DELETE | `/batch-configs/:id` | 관리자 | 배치 설정 삭제 | - | `{ success: true }` |
|
||||
| GET | `/batch-configs/connections` | 관리자 | 사용 가능한 커넥션 목록 | - | `{ connections[] }` |
|
||||
| GET | `/batch-configs/connections/:type/tables` | 관리자 | 테이블 목록 조회 | - | `{ tables[] }` |
|
||||
|
||||
### 8.2 배치 실행 (`/api/batch-management`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/batch-management/:id/execute` | 관리자 | 배치 즉시 실행 | - | `{ success: true, executionLogId }` |
|
||||
|
||||
### 8.3 실행 이력 (`/api/batch-execution-logs`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/batch-execution-logs` | 인증 | 실행 이력 목록 | `batchConfigId, page, limit` | `{ logs[], total }` |
|
||||
| GET | `/batch-execution-logs/:id` | 인증 | 실행 이력 상세 | - | `{ log }` |
|
||||
|
||||
---
|
||||
|
||||
## 9. 메일 API (`/api/mail`)
|
||||
|
||||
### 9.1 계정 관리
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/mail/accounts` | 인증 | 계정 목록 | - | `{ accounts[] }` |
|
||||
| POST | `/mail/accounts` | 관리자 | 계정 추가 | `{ email, smtpHost, smtpPort, password }` | `{ account }` |
|
||||
|
||||
### 9.2 템플릿
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/mail/templates-file` | 인증 | 템플릿 목록 | - | `{ templates[] }` |
|
||||
| POST | `/mail/templates-file` | 관리자 | 템플릿 생성 | `{ name, subject, body }` | `{ template }` |
|
||||
|
||||
### 9.3 발송/수신
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/mail/send` | 인증 | 메일 발송 | `{ accountId, to, subject, body, attachments[] }` | `{ success: true, messageId }` |
|
||||
| GET | `/mail/sent` | 인증 | 발송 이력 | `page, limit` | `{ mails[], total }` |
|
||||
| POST | `/mail/receive` | 인증 | 메일 수신 | `{ accountId }` | `{ mails[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 10. 파일 API (`/api/files`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/files/upload` | 인증 | 파일 업로드 (multipart) | `FormData { file }` | `{ fileId, fileName, filePath, fileSize }` |
|
||||
| GET | `/files` | 인증 | 파일 목록 | `page, limit` | `{ files[], total }` |
|
||||
| GET | `/files/:id` | 인증 | 파일 정보 조회 | - | `{ file }` |
|
||||
| GET | `/files/download/:id` | 인증 | 파일 다운로드 | - | `(파일 스트림)` |
|
||||
| DELETE | `/files/:id` | 인증 | 파일 삭제 | - | `{ success: true }` |
|
||||
| GET | `/uploads/:filename` | 공개 | 정적 파일 서빙 | - | `(파일 스트림)` |
|
||||
|
||||
---
|
||||
|
||||
## 11. 대시보드 API (`/api/dashboards`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/dashboards` | 인증 | 대시보드 목록 | - | `{ dashboards[] }` |
|
||||
| GET | `/dashboards/:id` | 인증 | 대시보드 상세 | - | `{ dashboard }` |
|
||||
| POST | `/dashboards` | 관리자 | 대시보드 생성 | - | `{ dashboard }` |
|
||||
| GET | `/dashboards/:id/widgets` | 인증 | 위젯 데이터 조회 | - | `{ widgets[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 12. 공통코드 API (`/api/common-codes`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/common-codes` | 인증 | 공통코드 목록 | `codeGroup` | `{ codes[] }` |
|
||||
| GET | `/common-codes/:codeGroup/:code` | 인증 | 공통코드 상세 | - | `{ code }` |
|
||||
| POST | `/common-codes` | 관리자 | 공통코드 생성 | `{ codeGroup, code, name }` | `{ code }` |
|
||||
|
||||
---
|
||||
|
||||
## 13. 다국어 API (`/api/multilang`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/multilang` | 인증 | 다국어 키 목록 | `lang` | `{ translations{} }` |
|
||||
| GET | `/multilang/:key` | 인증 | 특정 키 조회 | `lang` | `{ key, value }` |
|
||||
| POST | `/multilang` | 관리자 | 다국어 추가 | `{ key, ko, en, cn }` | `{ translation }` |
|
||||
|
||||
---
|
||||
|
||||
## 14. 회사 관리 API (`/api/company-management`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/company-management` | 슈퍼관리자 | 회사 목록 | - | `{ companies[] }` |
|
||||
| POST | `/company-management` | 슈퍼관리자 | 회사 생성 | `{ companyCode, companyName }` | `{ company }` |
|
||||
| PUT | `/company-management/:code` | 슈퍼관리자 | 회사 수정 | `{ companyName }` | `{ company }` |
|
||||
| DELETE | `/company-management/:code` | 슈퍼관리자 | 회사 삭제 | - | `{ success: true }` |
|
||||
|
||||
---
|
||||
|
||||
## 15. 부서 API (`/api/departments`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/departments` | 인증 | 부서 목록 (트리) | - | `{ departments[] }` |
|
||||
| POST | `/departments` | 관리자 | 부서 생성 | `{ deptCode, deptName, parentDeptCode }` | `{ department }` |
|
||||
|
||||
---
|
||||
|
||||
## 16. 권한 그룹 API (`/api/roles`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/roles` | 인증 | 권한 그룹 목록 | - | `{ roles[] }` |
|
||||
| POST | `/roles` | 관리자 | 권한 그룹 생성 | `{ roleName, permissions[] }` | `{ role }` |
|
||||
|
||||
---
|
||||
|
||||
## 17. DDL 실행 API (`/api/ddl`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/ddl` | 슈퍼관리자 | DDL 실행 | `{ sql }` | `{ success: true, result }` |
|
||||
|
||||
---
|
||||
|
||||
## 18. 외부 API 프록시 (`/api/open-api`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/open-api/weather` | 인증 | 날씨 정보 조회 | `location` | `{ weather }` |
|
||||
| GET | `/open-api/exchange` | 인증 | 환율 정보 조회 | `fromCurrency, toCurrency` | `{ rate }` |
|
||||
|
||||
---
|
||||
|
||||
## 19. 디지털 트윈 API (`/api/digital-twin`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/digital-twin/layouts` | 인증 | 레이아웃 목록 | - | `{ layouts[] }` |
|
||||
| GET | `/digital-twin/templates` | 인증 | 템플릿 목록 | - | `{ templates[] }` |
|
||||
| GET | `/digital-twin/data` | 인증 | 실시간 데이터 | - | `{ data[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 20. 3D 필드 API (`/api/yard-layouts`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/yard-layouts` | 인증 | 필드 레이아웃 목록 | - | `{ yardLayouts[] }` |
|
||||
| POST | `/yard-layouts` | 인증 | 레이아웃 저장 | `{ layout }` | `{ success: true }` |
|
||||
|
||||
---
|
||||
|
||||
## 21. 스케줄 API (`/api/schedule`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/schedule` | 인증 | 스케줄 자동 생성 | `{ params }` | `{ schedule }` |
|
||||
|
||||
---
|
||||
|
||||
## 22. 채번 규칙 API (`/api/numbering-rules`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/numbering-rules` | 인증 | 채번 규칙 목록 | - | `{ rules[] }` |
|
||||
| POST | `/numbering-rules` | 관리자 | 규칙 생성 | `{ ruleName, prefix, format }` | `{ rule }` |
|
||||
| POST | `/numbering-rules/:id/generate` | 인증 | 번호 생성 | - | `{ number }` |
|
||||
|
||||
---
|
||||
|
||||
## 23. 엔티티 검색 API (`/api/entity-search`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| POST | `/entity-search` | 인증 | 엔티티 검색 | `{ table, filters, page, limit }` | `{ results[], total }` |
|
||||
| GET | `/entity/:table/options` | 인증 | V2Select용 옵션 | `search, limit` | `{ options[] }` |
|
||||
|
||||
---
|
||||
|
||||
## 24. To-Do API (`/api/todos`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/todos` | 인증 | To-Do 목록 | `status, assignee` | `{ todos[] }` |
|
||||
| POST | `/todos` | 인증 | To-Do 생성 | `{ title, description, dueDate }` | `{ todo }` |
|
||||
| PUT | `/todos/:id` | 인증 | To-Do 수정 | `{ status }` | `{ todo }` |
|
||||
|
||||
---
|
||||
|
||||
## 25. 예약 요청 API (`/api/bookings`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Request Body | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/bookings` | 인증 | 예약 목록 | - | `{ bookings[] }` |
|
||||
| POST | `/bookings` | 인증 | 예약 생성 | `{ resourceId, startTime, endTime }` | `{ booking }` |
|
||||
|
||||
---
|
||||
|
||||
## 26. 리스크/알림 API (`/api/risk-alerts`)
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Query Params | Response |
|
||||
|--------|------|------|------|--------------|----------|
|
||||
| GET | `/risk-alerts` | 인증 | 리스크/알림 목록 | `priority, status` | `{ alerts[] }` |
|
||||
| POST | `/risk-alerts` | 인증 | 알림 생성 | `{ title, content, priority }` | `{ alert }` |
|
||||
|
||||
---
|
||||
|
||||
## 27. 헬스 체크
|
||||
|
||||
| 메서드 | 경로 | 권한 | 기능 | Response |
|
||||
|--------|------|------|------|----------|
|
||||
| GET | `/health` | 공개 | 서버 상태 확인 | `{ status: "OK", timestamp, uptime, environment }` |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 에러 코드 목록
|
||||
|
||||
| 코드 | HTTP Status | 설명 |
|
||||
|------|-------------|------|
|
||||
| `TOKEN_MISSING` | 401 | 인증 토큰 누락 |
|
||||
| `TOKEN_EXPIRED` | 401 | 토큰 만료 |
|
||||
| `INVALID_TOKEN` | 401 | 유효하지 않은 토큰 |
|
||||
| `AUTHENTICATION_REQUIRED` | 401 | 인증 필요 |
|
||||
| `INSUFFICIENT_PERMISSION` | 403 | 권한 부족 |
|
||||
| `SUPER_ADMIN_REQUIRED` | 403 | 슈퍼관리자 권한 필요 |
|
||||
| `COMPANY_ACCESS_DENIED` | 403 | 회사 데이터 접근 거부 |
|
||||
| `INVALID_INPUT` | 400 | 잘못된 입력 |
|
||||
| `RESOURCE_NOT_FOUND` | 404 | 리소스 없음 |
|
||||
| `DUPLICATE_ENTRY` | 400 | 중복 데이터 |
|
||||
| `FOREIGN_KEY_VIOLATION` | 400 | 참조 무결성 위반 |
|
||||
| `SERVER_ERROR` | 500 | 서버 오류 |
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**마지막 업데이트**: 2026-02-06
|
||||
**총 API 개수**: 200+개
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,342 @@
|
|||
# WACE ERP Backend - 아키텍처 요약
|
||||
|
||||
> **작성일**: 2026-02-06
|
||||
> **목적**: 워크플로우 문서 통합용 백엔드 아키텍처 요약
|
||||
|
||||
---
|
||||
|
||||
## 1. 기술 스택
|
||||
|
||||
```
|
||||
언어: TypeScript (Node.js 20.10.0+)
|
||||
프레임워크: Express.js
|
||||
데이터베이스: PostgreSQL (pg 라이브러리, Raw Query)
|
||||
인증: JWT (jsonwebtoken)
|
||||
스케줄러: node-cron
|
||||
메일: nodemailer + IMAP
|
||||
파일업로드: multer
|
||||
외부DB: MySQL, MSSQL, Oracle 지원
|
||||
```
|
||||
|
||||
## 2. 계층 구조
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Controller │ ← API 요청 수신, 응답 생성
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Service │ ← 비즈니스 로직, 트랜잭션 관리
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Database │ ← PostgreSQL Raw Query
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 3. 디렉토리 구조
|
||||
|
||||
```
|
||||
backend-node/src/
|
||||
├── app.ts # Express 앱 진입점
|
||||
├── config/ # 환경설정
|
||||
├── controllers/ # 70+ 컨트롤러
|
||||
├── services/ # 80+ 서비스
|
||||
├── routes/ # 70+ 라우터
|
||||
├── middleware/ # 인증/권한/에러핸들러
|
||||
├── database/ # DB 연결 (pg Pool)
|
||||
├── types/ # TypeScript 타입 (26개)
|
||||
└── utils/ # 유틸리티 (JWT, 암호화, 로거)
|
||||
```
|
||||
|
||||
## 4. 미들웨어 스택 순서
|
||||
|
||||
```typescript
|
||||
1. Process Level Exception Handlers (unhandledRejection, uncaughtException)
|
||||
2. Helmet (보안 헤더)
|
||||
3. Compression (Gzip)
|
||||
4. Body Parser (JSON, URL-encoded, 10MB limit)
|
||||
5. Static Files (/uploads)
|
||||
6. CORS (credentials: true)
|
||||
7. Rate Limiting (1분 10000회)
|
||||
8. Token Auto Refresh (1시간 이내 만료 시 갱신)
|
||||
9. API Routes (70+개)
|
||||
10. 404 Handler
|
||||
11. Error Handler
|
||||
```
|
||||
|
||||
## 5. 인증/인가 시스템
|
||||
|
||||
### 5.1 인증 흐름
|
||||
|
||||
```
|
||||
로그인 요청
|
||||
↓
|
||||
AuthController.login()
|
||||
↓
|
||||
AuthService.processLogin()
|
||||
├─ loginPwdCheck() → 비밀번호 검증 (bcrypt)
|
||||
├─ getPersonBeanFromSession() → 사용자 정보 조회
|
||||
├─ insertLoginAccessLog() → 로그인 이력 저장
|
||||
└─ JwtUtils.generateToken() → JWT 토큰 생성
|
||||
↓
|
||||
응답: { token, userInfo, firstMenuPath }
|
||||
```
|
||||
|
||||
### 5.2 JWT Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "user123",
|
||||
"userName": "홍길동",
|
||||
"companyCode": "ILSHIN",
|
||||
"userType": "COMPANY_ADMIN",
|
||||
"iat": 1234567890,
|
||||
"exp": 1234654290,
|
||||
"iss": "PMS-System"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 권한 체계 (3단계)
|
||||
|
||||
| 권한 | company_code | userType | 권한 범위 |
|
||||
|------|--------------|----------|-----------|
|
||||
| **SUPER_ADMIN** | `*` | `SUPER_ADMIN` | 전체 회사, DDL 실행, 회사 생성/삭제 |
|
||||
| **COMPANY_ADMIN** | `ILSHIN` | `COMPANY_ADMIN` | 자기 회사만, 사용자/설정 관리 |
|
||||
| **USER** | `ILSHIN` | `USER` | 자기 회사만, 읽기/쓰기 |
|
||||
|
||||
## 6. 멀티테넌시 구현
|
||||
|
||||
### 핵심 원칙
|
||||
```typescript
|
||||
// ✅ 올바른 패턴
|
||||
const companyCode = req.user!.companyCode; // JWT에서 추출
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 슈퍼관리자: 모든 데이터 조회
|
||||
query = "SELECT * FROM table ORDER BY company_code";
|
||||
} else {
|
||||
// 일반 사용자: 자기 회사 + 슈퍼관리자 데이터 제외
|
||||
query = "SELECT * FROM table WHERE company_code = $1 AND company_code != '*'";
|
||||
params = [companyCode];
|
||||
}
|
||||
|
||||
// ❌ 잘못된 패턴 (보안 위험!)
|
||||
const companyCode = req.body.companyCode; // 클라이언트에서 받음
|
||||
```
|
||||
|
||||
### 슈퍼관리자 숨김 규칙
|
||||
```sql
|
||||
-- 일반 회사 사용자에게 슈퍼관리자(company_code='*')는 보이면 안 됨
|
||||
SELECT * FROM user_info
|
||||
WHERE company_code = $1
|
||||
AND company_code != '*' -- 필수!
|
||||
```
|
||||
|
||||
## 7. API 라우트 (70+개)
|
||||
|
||||
### 7.1 인증/관리자
|
||||
- `POST /api/auth/login` - 로그인
|
||||
- `GET /api/auth/me` - 현재 사용자 정보
|
||||
- `POST /api/auth/switch-company` - 회사 전환 (슈퍼관리자)
|
||||
- `GET /api/admin/users` - 사용자 목록
|
||||
- `GET /api/admin/menus` - 메뉴 목록
|
||||
|
||||
### 7.2 테이블/화면
|
||||
- `GET /api/table-management/tables` - 테이블 목록
|
||||
- `POST /api/table-management/tables/:table/data` - 데이터 조회
|
||||
- `POST /api/table-management/multi-table-save` - 다중 테이블 저장
|
||||
- `GET /api/screen-management/screens` - 화면 목록
|
||||
|
||||
### 7.3 플로우
|
||||
- `GET /api/flow/definitions` - 플로우 정의 목록
|
||||
- `POST /api/flow/move` - 데이터 이동 (단건)
|
||||
- `POST /api/flow/move-batch` - 데이터 이동 (다건)
|
||||
|
||||
### 7.4 외부 연동
|
||||
- `GET /api/external-db-connections` - 외부 DB 연결 목록
|
||||
- `POST /api/external-db-connections/:id/test` - 연결 테스트
|
||||
- `POST /api/multi-connection/query` - 멀티 DB 쿼리
|
||||
|
||||
### 7.5 배치
|
||||
- `GET /api/batch-configs` - 배치 설정 목록
|
||||
- `POST /api/batch-management/:id/execute` - 배치 즉시 실행
|
||||
|
||||
### 7.6 메일
|
||||
- `POST /api/mail/send` - 메일 발송
|
||||
- `GET /api/mail/sent` - 발송 이력
|
||||
|
||||
### 7.7 파일
|
||||
- `POST /api/files/upload` - 파일 업로드
|
||||
- `GET /uploads/:filename` - 정적 파일 서빙
|
||||
|
||||
## 8. 비즈니스 도메인 (8개)
|
||||
|
||||
| 도메인 | 컨트롤러 | 주요 기능 |
|
||||
|--------|----------|-----------|
|
||||
| **관리자** | `adminController` | 사용자/메뉴/권한 관리 |
|
||||
| **테이블/화면** | `tableManagementController` | 메타데이터, 동적 화면 생성 |
|
||||
| **플로우** | `flowController` | 워크플로우 엔진, 데이터 이동 |
|
||||
| **데이터플로우** | `dataflowController` | ERD, 관계도 |
|
||||
| **외부 연동** | `externalDbConnectionController` | 외부 DB/REST API |
|
||||
| **배치** | `batchController` | Cron 스케줄러, 데이터 동기화 |
|
||||
| **메일** | `mailSendSimpleController` | 메일 발송/수신 |
|
||||
| **파일** | `fileController` | 파일 업로드/다운로드 |
|
||||
|
||||
## 9. 데이터베이스 접근
|
||||
|
||||
### Connection Pool 설정
|
||||
```typescript
|
||||
{
|
||||
min: 2~5, // 최소 연결 수
|
||||
max: 10~20, // 최대 연결 수
|
||||
connectionTimeout: 30000, // 30초
|
||||
idleTimeout: 600000, // 10분
|
||||
statementTimeout: 60000 // 쿼리 실행 60초
|
||||
}
|
||||
```
|
||||
|
||||
### Raw Query 패턴
|
||||
```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', [userId]);
|
||||
|
||||
// 3. 트랜잭션
|
||||
await transaction(async (client) => {
|
||||
await client.query('INSERT INTO table1 ...', [...]);
|
||||
await client.query('INSERT INTO table2 ...', [...]);
|
||||
});
|
||||
```
|
||||
|
||||
## 10. 외부 시스템 연동
|
||||
|
||||
### 지원 데이터베이스
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- Microsoft SQL Server
|
||||
- Oracle
|
||||
|
||||
### Connector Factory Pattern
|
||||
```typescript
|
||||
DatabaseConnectorFactory
|
||||
├── PostgreSQLConnector
|
||||
├── MySQLConnector
|
||||
├── MSSQLConnector
|
||||
└── OracleConnector
|
||||
```
|
||||
|
||||
## 11. 배치/스케줄 시스템
|
||||
|
||||
### Cron 스케줄러
|
||||
```typescript
|
||||
// node-cron 기반
|
||||
// 매일 새벽 2시: "0 2 * * *"
|
||||
// 5분마다: "*/5 * * * *"
|
||||
// 평일 오전 8시: "0 8 * * 1-5"
|
||||
|
||||
// 서버 시작 시 자동 초기화
|
||||
BatchSchedulerService.initializeScheduler();
|
||||
```
|
||||
|
||||
### 배치 실행 흐름
|
||||
```
|
||||
1. 소스 DB에서 데이터 조회
|
||||
↓
|
||||
2. 컬럼 매핑 적용
|
||||
↓
|
||||
3. 타겟 DB에 INSERT/UPDATE
|
||||
↓
|
||||
4. 실행 로그 기록 (batch_execution_logs)
|
||||
```
|
||||
|
||||
## 12. 파일 처리
|
||||
|
||||
### 업로드 경로
|
||||
```
|
||||
uploads/
|
||||
└── {company_code}/
|
||||
└── {timestamp}-{uuid}-{filename}
|
||||
```
|
||||
|
||||
### Multer 설정
|
||||
- 최대 파일 크기: 10MB
|
||||
- 허용 타입: 이미지, PDF, Office 문서
|
||||
- 파일명 중복 방지: 타임스탬프 + UUID
|
||||
|
||||
## 13. 보안
|
||||
|
||||
### 암호화
|
||||
- **비밀번호**: bcrypt (12 rounds)
|
||||
- **민감정보**: AES-256-CBC (외부 DB 비밀번호 등)
|
||||
- **JWT Secret**: 환경변수 관리
|
||||
|
||||
### 보안 헤더
|
||||
- Helmet (CSP, X-Frame-Options)
|
||||
- CORS (credentials: true)
|
||||
- Rate Limiting (1분 10000회)
|
||||
|
||||
### SQL Injection 방지
|
||||
- Parameterized Query 사용 (pg 라이브러리)
|
||||
- 동적 쿼리 빌더 패턴
|
||||
|
||||
## 14. 에러 핸들링
|
||||
|
||||
### PostgreSQL 에러 코드 매핑
|
||||
- `23505` → "중복된 데이터"
|
||||
- `23503` → "참조 무결성 위반"
|
||||
- `23502` → "필수 입력값 누락"
|
||||
|
||||
### 프로세스 레벨
|
||||
- `unhandledRejection` → 로깅 (서버 유지)
|
||||
- `uncaughtException` → 로깅 (서버 유지, 주의)
|
||||
- `SIGTERM/SIGINT` → Graceful Shutdown
|
||||
|
||||
## 15. 로깅 (Winston)
|
||||
|
||||
### 로그 파일
|
||||
- `logs/error.log` - 에러만 (10MB × 5파일)
|
||||
- `logs/combined.log` - 전체 로그 (10MB × 10파일)
|
||||
|
||||
### 로그 레벨
|
||||
```
|
||||
error (0) → warn (1) → info (2) → debug (5)
|
||||
```
|
||||
|
||||
## 16. 성능 최적화
|
||||
|
||||
### Pool 모니터링
|
||||
- 5분마다 상태 체크
|
||||
- 대기 연결 5개 이상 시 경고
|
||||
|
||||
### 느린 쿼리 감지
|
||||
- 1초 이상 걸린 쿼리 자동 경고
|
||||
|
||||
### 캐싱 (Redis)
|
||||
- 메뉴 목록: 10분 TTL
|
||||
- 공통코드: 30분 TTL
|
||||
|
||||
### Gzip 압축
|
||||
- 1KB 이상 응답만 압축 (레벨 6)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 핵심 체크리스트
|
||||
|
||||
### 개발 시 반드시 지켜야 할 규칙
|
||||
|
||||
✅ **모든 쿼리에 `company_code` 필터 추가**
|
||||
✅ **JWT 토큰에서 `company_code` 추출 (클라이언트 신뢰 금지)**
|
||||
✅ **Parameterized Query 사용 (SQL Injection 방지)**
|
||||
✅ **슈퍼관리자 데이터 숨김 (`company_code != '*'`)**
|
||||
✅ **비밀번호는 bcrypt, 민감정보는 AES-256**
|
||||
✅ **에러 핸들링 try/catch 필수**
|
||||
✅ **트랜잭션이 필요한 경우 `transaction()` 사용**
|
||||
✅ **파일 업로드는 회사별 디렉토리 분리**
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**마지막 업데이트**: 2026-02-06
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -289,6 +289,20 @@ select {
|
|||
}
|
||||
}
|
||||
|
||||
/* ===== Sonner 토스트 애니메이션 완전 제거 ===== */
|
||||
[data-sonner-toaster] [data-sonner-toast] {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
[data-sonner-toaster] [data-sonner-toast][data-mounted="true"] {
|
||||
animation: none !important;
|
||||
}
|
||||
[data-sonner-toaster] [data-sonner-toast][data-removed="true"] {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
/* ===== Print Styles ===== */
|
||||
@media print {
|
||||
* {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogCancel,
|
||||
AlertDialogAction,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||
|
|
@ -67,6 +77,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
// 화면 리셋 키 (컴포넌트 강제 리마운트용)
|
||||
const [resetKey, setResetKey] = useState(0);
|
||||
|
||||
// 모달 닫기 확인 다이얼로그 표시 상태
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false);
|
||||
|
||||
// localStorage에서 연속 모드 상태 복원
|
||||
useEffect(() => {
|
||||
const savedMode = localStorage.getItem("screenModal_continuousMode");
|
||||
|
|
@ -218,10 +231,33 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
const parentDataMapping = splitPanelContext?.parentDataMapping || [];
|
||||
|
||||
// 부모 데이터 소스
|
||||
const rawParentData =
|
||||
splitPanelParentData && Object.keys(splitPanelParentData).length > 0
|
||||
? splitPanelParentData
|
||||
: splitPanelContext?.selectedLeftData || {};
|
||||
// 🔧 수정: 여러 소스를 병합 (우선순위: splitPanelParentData > selectedLeftData > 기존 formData의 링크 필드)
|
||||
// 예: screen 150→226→227 전환 시:
|
||||
// - splitPanelParentData: item_info 데이터 (screen 226에서 전달)
|
||||
// - selectedLeftData: customer_mng 데이터 (SplitPanel 좌측 선택)
|
||||
// - 기존 formData: 이전 모달에서 설정된 link 필드 (customer_code 등)
|
||||
const contextData = splitPanelContext?.selectedLeftData || {};
|
||||
const eventData = splitPanelParentData && Object.keys(splitPanelParentData).length > 0
|
||||
? splitPanelParentData
|
||||
: {};
|
||||
|
||||
// 🆕 기존 formData에서 link 필드(_code, _id)를 가져와 base로 사용
|
||||
// 모달 체인(226→227)에서 이전 모달의 연결 필드가 유지됨
|
||||
const previousLinkFields: Record<string, any> = {};
|
||||
if (formData && typeof formData === "object" && !Array.isArray(formData)) {
|
||||
const linkFieldPatterns = ["_code", "_id"];
|
||||
const excludeFields = ["id", "created_date", "updated_date", "created_at", "updated_at", "writer"];
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
if (excludeFields.includes(key)) continue;
|
||||
if (value === undefined || value === null) continue;
|
||||
const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
|
||||
if (isLinkField) {
|
||||
previousLinkFields[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rawParentData = { ...previousLinkFields, ...contextData, ...eventData };
|
||||
|
||||
// 🔧 신규 등록 모드에서는 연결에 필요한 필드만 전달
|
||||
const parentData: Record<string, any> = {};
|
||||
|
|
@ -495,14 +531,31 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// 🔧 URL 파라미터 제거 (mode, editId, tableName 등)
|
||||
// 사용자가 바깥 클릭/ESC/X 버튼으로 닫으려 할 때 확인 다이얼로그 표시
|
||||
const handleCloseAttempt = useCallback(() => {
|
||||
setShowCloseConfirm(true);
|
||||
}, []);
|
||||
|
||||
// 확인 후 실제로 모달을 닫는 함수
|
||||
const handleConfirmClose = useCallback(() => {
|
||||
setShowCloseConfirm(false);
|
||||
handleCloseInternal();
|
||||
}, []);
|
||||
|
||||
// 닫기 취소 (계속 작업)
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseConfirm(false);
|
||||
}, []);
|
||||
|
||||
const handleCloseInternal = () => {
|
||||
// 🔧 URL 파라미터 제거 (mode, editId, tableName, groupByColumns, dataSourceId 등)
|
||||
if (typeof window !== "undefined") {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.searchParams.delete("mode");
|
||||
currentUrl.searchParams.delete("editId");
|
||||
currentUrl.searchParams.delete("tableName");
|
||||
currentUrl.searchParams.delete("groupByColumns");
|
||||
currentUrl.searchParams.delete("dataSourceId");
|
||||
window.history.pushState({}, "", currentUrl.toString());
|
||||
}
|
||||
|
||||
|
|
@ -514,8 +567,15 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
});
|
||||
setScreenData(null);
|
||||
setFormData({}); // 폼 데이터 초기화
|
||||
setOriginalData(null); // 원본 데이터 초기화
|
||||
setSelectedData([]); // 선택된 데이터 초기화
|
||||
setContinuousMode(false);
|
||||
localStorage.setItem("screenModal_continuousMode", "false");
|
||||
};
|
||||
|
||||
// 기존 handleClose를 유지 (이벤트 핸들러 등에서 사용)
|
||||
const handleClose = handleCloseInternal;
|
||||
|
||||
// 모달 크기 설정 - 화면관리 설정 크기 + 헤더/푸터
|
||||
const getModalStyle = () => {
|
||||
if (!screenDimensions) {
|
||||
|
|
@ -615,10 +675,28 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
]);
|
||||
|
||||
return (
|
||||
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||
<Dialog
|
||||
open={modalState.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
// X 버튼 클릭 시에도 확인 다이얼로그 표시
|
||||
if (!open) {
|
||||
handleCloseAttempt();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className={`${modalStyle.className} ${className || ""} max-w-none flex flex-col`}
|
||||
{...(modalStyle.style && { style: modalStyle.style })}
|
||||
// 바깥 클릭 시 바로 닫히지 않도록 방지
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault();
|
||||
handleCloseAttempt();
|
||||
}}
|
||||
// ESC 키 누를 때도 바로 닫히지 않도록 방지
|
||||
onEscapeKeyDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleCloseAttempt();
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="shrink-0 border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -838,6 +916,36 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
{/* 모달 닫기 확인 다이얼로그 */}
|
||||
<AlertDialog open={showCloseConfirm} onOpenChange={setShowCloseConfirm}>
|
||||
<AlertDialogContent className="!z-[1100] max-w-[95vw] sm:max-w-[400px]">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-base sm:text-lg">
|
||||
화면을 닫으시겠습니까?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||
지금 나가시면 진행 중인 데이터가 저장되지 않습니다.
|
||||
<br />
|
||||
계속 작업하시려면 '계속 작업' 버튼을 눌러주세요.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="gap-2 sm:gap-0">
|
||||
<AlertDialogCancel
|
||||
onClick={handleCancelClose}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
계속 작업
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmClose}
|
||||
className="h-8 flex-1 text-xs bg-destructive text-destructive-foreground hover:bg-destructive/90 sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
나가기
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -449,6 +449,15 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
);
|
||||
};
|
||||
|
||||
// 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (인증 대기 없이 즉시 렌더링)
|
||||
if (isPreviewMode) {
|
||||
return (
|
||||
<div className="h-screen w-full overflow-auto bg-white p-4">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 사용자 정보가 없으면 로딩 표시
|
||||
if (!user) {
|
||||
return (
|
||||
|
|
@ -461,15 +470,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// 프리뷰 모드: 사이드바/헤더 없이 화면만 표시
|
||||
if (isPreviewMode) {
|
||||
return (
|
||||
<div className="h-screen w-full overflow-auto bg-white p-4">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// UI 변환된 메뉴 데이터
|
||||
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -134,7 +134,6 @@ interface ScreenSettingModalProps {
|
|||
fieldMappings?: FieldMappingInfo[];
|
||||
componentCount?: number;
|
||||
onSaveSuccess?: () => void;
|
||||
isPop?: boolean; // POP 화면 여부
|
||||
}
|
||||
|
||||
// 검색 가능한 Select 컴포넌트
|
||||
|
|
@ -240,7 +239,6 @@ export function ScreenSettingModal({
|
|||
fieldMappings = [],
|
||||
componentCount = 0,
|
||||
onSaveSuccess,
|
||||
isPop = false,
|
||||
}: ScreenSettingModalProps) {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -521,7 +519,6 @@ export function ScreenSettingModal({
|
|||
iframeKey={iframeKey}
|
||||
canvasWidth={canvasSize.width}
|
||||
canvasHeight={canvasSize.height}
|
||||
isPop={isPop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -4634,10 +4631,9 @@ interface PreviewTabProps {
|
|||
iframeKey?: number; // iframe 새로고침용 키
|
||||
canvasWidth?: number; // 화면 캔버스 너비
|
||||
canvasHeight?: number; // 화면 캔버스 높이
|
||||
isPop?: boolean; // POP 화면 여부
|
||||
}
|
||||
|
||||
function PreviewTab({ screenId, screenName, companyCode, iframeKey = 0, canvasWidth, canvasHeight, isPop = false }: PreviewTabProps) {
|
||||
function PreviewTab({ screenId, screenName, companyCode, iframeKey = 0, canvasWidth, canvasHeight }: PreviewTabProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -4691,18 +4687,12 @@ function PreviewTab({ screenId, screenName, companyCode, iframeKey = 0, canvasWi
|
|||
if (companyCode) {
|
||||
params.set("company_code", companyCode);
|
||||
}
|
||||
// POP 화면일 경우 디바이스 타입 추가
|
||||
if (isPop) {
|
||||
params.set("device", "tablet");
|
||||
}
|
||||
// POP 화면과 데스크톱 화면 경로 분기
|
||||
const screenPath = isPop ? `/pop/screens/${screenId}` : `/screens/${screenId}`;
|
||||
if (typeof window !== "undefined") {
|
||||
const baseUrl = window.location.origin;
|
||||
return `${baseUrl}${screenPath}?${params.toString()}`;
|
||||
return `${baseUrl}/screens/${screenId}?${params.toString()}`;
|
||||
}
|
||||
return `${screenPath}?${params.toString()}`;
|
||||
}, [screenId, companyCode, isPop]);
|
||||
return `/screens/${screenId}?${params.toString()}`;
|
||||
}, [screenId, companyCode]);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ export function ComponentsPanel({
|
|||
"v2-media", // → 테이블 컬럼의 image/file 입력 타입으로 사용
|
||||
// 플로우 위젯 숨김 처리
|
||||
"flow-widget",
|
||||
// 선택 항목 상세입력 - 기존 컴포넌트 조합으로 대체 가능
|
||||
"selected-items-detail-input",
|
||||
// 선택 항목 상세입력 - 거래처 품목 추가 등에서 사용
|
||||
// "selected-items-detail-input",
|
||||
// 연관 데이터 버튼 - v2-repeater로 대체 가능
|
||||
"related-data-buttons",
|
||||
// ===== V2로 대체된 기존 컴포넌트 (v2 버전만 사용) =====
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[999] bg-black/80",
|
||||
"fixed inset-0 z-[999] bg-black/80",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
|
|||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-[1000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
"bg-background fixed top-[50%] left-[50%] z-[1000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[999] bg-black/60",
|
||||
"fixed inset-0 z-[999] bg-black/60",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[1000] flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
"bg-background fixed top-[50%] left-[50%] z-[1000] flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border p-6 shadow-lg sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
"bg-popover text-popover-foreground z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
"bg-popover text-popover-foreground z-[99999] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
|||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[2000] w-72 rounded-md border p-4 shadow-md outline-none",
|
||||
"bg-popover text-popover-foreground z-[2000] w-72 rounded-md border p-4 shadow-md outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,14 @@ const SheetClose = SheetPrimitive.Close;
|
|||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
top: "inset-x-0 top-0 border-b",
|
||||
bottom: "inset-x-0 bottom-0 border-t",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
@ -60,7 +58,7 @@ const SheetOverlay = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"bg-background/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 backdrop-blur-sm",
|
||||
"bg-background/80 fixed inset-0 z-50 backdrop-blur-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,8 @@ export const dataApi = {
|
|||
upsertGroupedRecords: async (
|
||||
tableName: string,
|
||||
parentKeys: Record<string, any>,
|
||||
records: Array<Record<string, any>>
|
||||
records: Array<Record<string, any>>,
|
||||
options?: { deleteOrphans?: boolean }
|
||||
): Promise<{ success: boolean; inserted?: number; updated?: number; deleted?: number; message?: string; error?: string }> => {
|
||||
try {
|
||||
console.log("📡 [dataApi.upsertGroupedRecords] 요청 데이터:", {
|
||||
|
|
@ -251,6 +252,7 @@ export const dataApi = {
|
|||
tableName,
|
||||
parentKeys,
|
||||
records,
|
||||
deleteOrphans: options?.deleteOrphans ?? true, // 기본값: true (기존 동작 유지)
|
||||
};
|
||||
console.log("📦 [dataApi.upsertGroupedRecords] 요청 본문 (JSON):", JSON.stringify(requestBody, null, 2));
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -54,6 +54,10 @@ export interface FieldGroup {
|
|||
description?: string;
|
||||
/** 그룹 표시 순서 */
|
||||
order?: number;
|
||||
/** 🆕 최대 항목 수 (1이면 1:1 관계 - 자동 생성, + 추가 버튼 숨김) */
|
||||
maxEntries?: number;
|
||||
/** 🆕 이 그룹의 소스 테이블 (카테고리 옵션 로드 시 사용) */
|
||||
sourceTable?: string;
|
||||
/** 🆕 이 그룹의 항목 표시 설정 (그룹별로 다른 표시 형식 가능) */
|
||||
displayItems?: DisplayItem[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,19 +214,53 @@ export function matchComponentSize(
|
|||
* 모든 컴포넌트의 라벨 표시/숨기기를 토글합니다.
|
||||
* 숨겨진 라벨이 하나라도 있으면 모두 표시, 모두 표시되어 있으면 모두 숨기기
|
||||
*/
|
||||
export function toggleAllLabels(components: ComponentData[], forceShow?: boolean): ComponentData[] {
|
||||
// 현재 라벨이 숨겨진(labelDisplay === false) 컴포넌트가 있는지 확인
|
||||
const hasHiddenLabel = components.some(
|
||||
(c) => c.type === "widget" && (c.style as any)?.labelDisplay === false
|
||||
/**
|
||||
* 라벨 토글 대상 타입 판별
|
||||
* label 속성이 있고, style.labelDisplay를 지원하는 컴포넌트인지 확인
|
||||
*/
|
||||
function hasLabelSupport(component: ComponentData): boolean {
|
||||
// 라벨이 없는 컴포넌트는 제외
|
||||
if (!component.label) return false;
|
||||
|
||||
// 그룹, datatable 등은 라벨 토글 대상에서 제외
|
||||
const excludedTypes = ["group", "datatable"];
|
||||
if (excludedTypes.includes(component.type)) return false;
|
||||
|
||||
// 나머지 (widget, component, container, file, flow 등)는 대상
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param components - 전체 컴포넌트 배열
|
||||
* @param selectedIds - 선택된 컴포넌트 ID 목록 (빈 배열이면 전체 대상)
|
||||
* @param forceShow - 강제 표시/숨기기 (지정하지 않으면 자동 토글)
|
||||
*/
|
||||
export function toggleAllLabels(
|
||||
components: ComponentData[],
|
||||
selectedIds: string[] = [],
|
||||
forceShow?: boolean
|
||||
): ComponentData[] {
|
||||
// 대상 컴포넌트 필터: selectedIds가 있으면 선택된 것만, 없으면 전체
|
||||
const targetComponents = components.filter((c) => {
|
||||
if (!hasLabelSupport(c)) return false;
|
||||
if (selectedIds.length > 0) return selectedIds.includes(c.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 대상 중 라벨이 숨겨진 컴포넌트가 있는지 확인
|
||||
const hasHiddenLabel = targetComponents.some(
|
||||
(c) => (c.style as any)?.labelDisplay === false
|
||||
);
|
||||
|
||||
// forceShow가 지정되면 그 값 사용, 아니면 자동 판단
|
||||
// 숨겨진 라벨이 있으면 모두 표시, 아니면 모두 숨기기
|
||||
const shouldShow = forceShow !== undefined ? forceShow : hasHiddenLabel;
|
||||
|
||||
// 대상 ID Set (빠른 조회용)
|
||||
const targetIdSet = new Set(targetComponents.map((c) => c.id));
|
||||
|
||||
return components.map((c) => {
|
||||
// 위젯 타입만 라벨 토글 대상
|
||||
if (c.type !== "widget") return c;
|
||||
// 대상이 아닌 컴포넌트는 건드리지 않음
|
||||
if (!targetIdSet.has(c.id)) return c;
|
||||
|
||||
return {
|
||||
...c,
|
||||
|
|
|
|||
|
|
@ -16,15 +16,12 @@ import {
|
|||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { spawn } from "child_process";
|
||||
import { platform } from "os";
|
||||
import { AGENT_CONFIGS } from "./agents/prompts.js";
|
||||
import { AgentType, ParallelResult } from "./agents/types.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// OS 감지
|
||||
const isWindows = platform() === "win32";
|
||||
logger.info(`Platform detected: ${platform()} (isWindows: ${isWindows})`);
|
||||
|
|
@ -43,12 +40,97 @@ const server = new Server(
|
|||
);
|
||||
|
||||
/**
|
||||
* Cursor Agent CLI를 통해 에이전트 호출
|
||||
* Cursor Team Plan 사용 - API 키 불필요!
|
||||
* 유틸: ms만큼 대기
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor Agent CLI 단일 호출 (내부용)
|
||||
* spawn + stdin 직접 전달
|
||||
*/
|
||||
function spawnAgentOnce(
|
||||
agentType: AgentType,
|
||||
fullPrompt: string,
|
||||
model: string
|
||||
): Promise<string> {
|
||||
const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`;
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let settled = false;
|
||||
|
||||
const child = spawn(agentPath, ['--model', model, '--print'], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`,
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
|
||||
if (stderr) {
|
||||
const significantStderr = stderr
|
||||
.split('\n')
|
||||
.filter((line: string) => line && !line.includes('warning') && !line.includes('info') && !line.includes('debug'))
|
||||
.join('\n');
|
||||
if (significantStderr) {
|
||||
logger.warn(`${agentType} agent stderr`, { stderr: significantStderr.substring(0, 500) });
|
||||
}
|
||||
}
|
||||
|
||||
if (code === 0 || stdout.trim().length > 0) {
|
||||
resolve(stdout.trim());
|
||||
} else {
|
||||
reject(new Error(
|
||||
`Agent exited with code ${code}. stderr: ${stderr.substring(0, 1000)}`
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// 타임아웃 (5분)
|
||||
const timeout = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
child.kill('SIGTERM');
|
||||
reject(new Error(`${agentType} agent timed out after 5 minutes`));
|
||||
}
|
||||
}, 300000);
|
||||
|
||||
child.on('close', () => clearTimeout(timeout));
|
||||
|
||||
// stdin으로 프롬프트 직접 전달
|
||||
child.stdin.write(fullPrompt);
|
||||
child.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor Agent CLI를 통해 에이전트 호출 (재시도 포함)
|
||||
*
|
||||
* 크로스 플랫폼 지원:
|
||||
* - Windows: cmd /c "echo. | agent ..." (stdin 닫기 위해)
|
||||
* - Mac/Linux: ~/.local/bin/agent 사용
|
||||
* - 최대 2회 재시도 (총 3회 시도)
|
||||
* - 재시도 간 2초 대기 (Cursor CLI 동시 실행 제한 대응)
|
||||
*/
|
||||
async function callAgentCLI(
|
||||
agentType: AgentType,
|
||||
|
|
@ -56,60 +138,43 @@ async function callAgentCLI(
|
|||
context?: string
|
||||
): Promise<string> {
|
||||
const config = AGENT_CONFIGS[agentType];
|
||||
|
||||
// 모델 선택: PM은 opus, 나머지는 sonnet
|
||||
const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5';
|
||||
const maxRetries = 2;
|
||||
|
||||
logger.info(`Calling ${agentType} agent via CLI`, { model, task: task.substring(0, 100) });
|
||||
logger.info(`Calling ${agentType} agent via CLI (spawn+retry)`, {
|
||||
model,
|
||||
task: task.substring(0, 100),
|
||||
});
|
||||
|
||||
try {
|
||||
const userMessage = context
|
||||
? `${task}\n\n배경 정보:\n${context}`
|
||||
: task;
|
||||
const userMessage = context
|
||||
? `${task}\n\n배경 정보:\n${context}`
|
||||
: task;
|
||||
const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`;
|
||||
|
||||
// 프롬프트를 임시 파일에 저장하여 쉘 이스케이프 문제 회피
|
||||
const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
// Base64 인코딩으로 특수문자 문제 해결
|
||||
const encodedPrompt = Buffer.from(fullPrompt).toString('base64');
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
const delay = attempt * 2000; // 2초, 4초
|
||||
logger.info(`${agentType} agent retry ${attempt}/${maxRetries} (waiting ${delay}ms)`);
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
let cmd: string;
|
||||
let shell: string;
|
||||
const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`;
|
||||
|
||||
if (isWindows) {
|
||||
// Windows: PowerShell을 통해 Base64 디코딩 후 실행
|
||||
cmd = `powershell -Command "$prompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encodedPrompt}')); echo $prompt | ${agentPath} --model ${model} --print"`;
|
||||
shell = 'powershell.exe';
|
||||
} else {
|
||||
// Mac/Linux: echo로 base64 디코딩 후 파이프
|
||||
cmd = `echo "${encodedPrompt}" | base64 -d | ${agentPath} --model ${model} --print`;
|
||||
shell = '/bin/bash';
|
||||
const result = await spawnAgentOnce(agentType, fullPrompt, model);
|
||||
logger.info(`${agentType} agent completed (attempt ${attempt + 1})`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logger.warn(`${agentType} agent attempt ${attempt + 1} failed`, {
|
||||
error: lastError.message.substring(0, 200),
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(`Executing: ${agentPath} --model ${model} --print`);
|
||||
|
||||
const { stdout, stderr } = await execAsync(cmd, {
|
||||
cwd: process.cwd(),
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
timeout: 300000, // 5분 타임아웃
|
||||
shell,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (stderr && !stderr.includes('warning') && !stderr.includes('info')) {
|
||||
logger.warn(`${agentType} agent stderr`, { stderr: stderr.substring(0, 500) });
|
||||
}
|
||||
|
||||
logger.info(`${agentType} agent completed via CLI`);
|
||||
return stdout.trim();
|
||||
} catch (error) {
|
||||
logger.error(`${agentType} agent CLI error`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 모든 재시도 실패
|
||||
logger.error(`${agentType} agent failed after ${maxRetries + 1} attempts`);
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -277,12 +342,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||
}>;
|
||||
};
|
||||
|
||||
logger.info(`Parallel ask to ${requests.length} agents (TRUE PARALLEL!)`);
|
||||
logger.info(`Parallel ask to ${requests.length} agents (STAGGERED PARALLEL)`);
|
||||
|
||||
// 시차 병렬 실행: 각 에이전트를 500ms 간격으로 시작
|
||||
// Cursor Agent CLI 동시 실행 제한 대응
|
||||
const STAGGER_DELAY = 500; // ms
|
||||
|
||||
// 진짜 병렬 실행! 모든 에이전트가 동시에 작업
|
||||
const results: ParallelResult[] = await Promise.all(
|
||||
requests.map(async (req) => {
|
||||
requests.map(async (req, index) => {
|
||||
try {
|
||||
// 시차 적용 (첫 번째는 즉시, 이후 500ms 간격)
|
||||
if (index > 0) {
|
||||
await sleep(index * STAGGER_DELAY);
|
||||
}
|
||||
const result = await callAgentCLI(req.agent, req.task, req.context);
|
||||
return { agent: req.agent, result };
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue