refactor: Update ComponentsPanel and SelectedItemsDetailInputComponent for improved functionality

- Updated ComponentsPanel to clarify the usage of the "selected-items-detail-input" component, indicating its application in the context of adding items for clients.
- Enhanced SelectedItemsDetailInputComponent by introducing independent editing states for group entries, allowing for better management of item edits within groups.
- Adjusted input field heights and styles for consistency and improved user experience.
- Added a new property `maxEntries` to the FieldGroup interface to support 1:1 relationships and automatic entry generation.
- Implemented overflow support for the component to handle cases with many items, ensuring a smoother user interface.
This commit is contained in:
DDD1542 2026-02-07 17:45:44 +09:00
parent 08dde416b1
commit 79d8f0b160
10 changed files with 6210 additions and 148 deletions

1728
docs/DB_WORKFLOW_ANALYSIS.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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. [사용] 사용자가 메뉴 클릭 → 업무 시작!
```

View File

@ -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 추가 시

View File

@ -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": []
}

View File

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

View File

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

View File

@ -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 버전만 사용) =====

View File

@ -94,8 +94,11 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
// 🆕 입력 모드 상태 (modal 모드일 때 사용)
const [isEditing, setIsEditing] = useState(false);
const [editingItemId, setEditingItemId] = useState<string | null>(null); // 현재 편집 중인 품목 ID
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); // 현재 편집 중인 그룹 ID
const [editingDetailId, setEditingDetailId] = useState<string | null>(null); // 현재 편집 중인 항목 ID
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); // 현재 편집 중인 그룹 ID (레거시 호환)
const [editingDetailId, setEditingDetailId] = useState<string | null>(null); // 현재 편집 중인 항목 ID (레거시 호환)
// 🆕 그룹별 독립 편집 상태: { [groupId]: entryId }
const [editingEntries, setEditingEntries] = useState<Record<string, string | null>>({});
// 🆕 코드 카테고리별 옵션 캐싱
const [codeOptions, setCodeOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
@ -404,9 +407,14 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const newItems: ItemData[] = modalData.map((item) => {
const fieldGroups: Record<string, GroupEntry[]> = {};
// 각 그룹에 대해 빈 배열 초기화
// 각 그룹에 대해 초기화 (maxEntries === 1이면 자동 1개 생성)
groups.forEach((group) => {
fieldGroups[group.id] = [];
if (group.maxEntries === 1) {
// 1:1 관계: 빈 entry 1개 자동 생성
fieldGroups[group.id] = [{ id: `${group.id}_auto_1` }];
} else {
fieldGroups[group.id] = [];
}
});
// 그룹이 없으면 기본 그룹 생성
@ -757,6 +765,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const componentStyle: React.CSSProperties = {
width: "100%",
height: "100%",
overflowY: "auto", // 항목이 많을 때 스크롤 지원
...component.style,
...style,
};
@ -976,6 +985,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
setEditingItemId(itemId);
setEditingDetailId(newEntryId);
setEditingGroupId(groupId);
// 그룹별 독립 편집: 해당 그룹만 열기 (다른 그룹은 유지)
setEditingEntries((prev) => ({ ...prev, [groupId]: newEntryId }));
};
// 🆕 그룹 항목 제거 핸들러
@ -992,14 +1003,34 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
};
}),
);
// 제거된 항목이 편집 중이었으면 해당 그룹 편집 닫기
setEditingEntries((prev) => {
if (prev[groupId] === entryId) {
const next = { ...prev };
delete next[groupId];
return next;
}
return prev;
});
};
// 🆕 그룹 항목 편집 핸들러 (클릭하면 수정 가능)
// 🆕 그룹 항목 편집 핸들러 (클릭하면 수정 가능) - 독립 편집
const handleEditGroupEntry = (itemId: string, groupId: string, entryId: string) => {
setIsEditing(true);
setEditingItemId(itemId);
setEditingGroupId(groupId);
setEditingDetailId(entryId);
// 그룹별 독립 편집: 해당 그룹만 토글 (다른 그룹은 유지)
setEditingEntries((prev) => ({ ...prev, [groupId]: entryId }));
};
// 🆕 특정 그룹의 편집 닫기 (다른 그룹은 유지)
const closeGroupEditing = (groupId: string) => {
setEditingEntries((prev) => {
const next = { ...prev };
delete next[groupId];
return next;
});
};
// 🆕 다음 품목으로 이동
@ -1059,7 +1090,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
type="text"
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
maxLength={field.validation?.maxLength}
className="h-10 text-sm"
className="h-7 text-xs"
/>
);
@ -1081,12 +1112,12 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
readOnly
disabled
className={cn(
"h-10 text-sm",
"h-7 text-xs",
"bg-primary/10 border-primary/30 text-primary font-semibold",
"cursor-not-allowed",
)}
/>
<div className="text-primary/70 absolute top-1/2 right-2 -translate-y-1/2 text-[10px]"> </div>
<div className="text-primary/70 absolute top-1/2 right-2 -translate-y-1/2 text-[9px]"> </div>
</div>
);
}
@ -1098,7 +1129,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
min={field.validation?.min}
max={field.validation?.max}
className="h-10 text-sm"
className="h-7 text-xs"
/>
);
@ -1117,7 +1148,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
target.showPicker();
}
}}
className="h-10 cursor-pointer text-sm"
className="h-7 cursor-pointer text-xs"
/>
);
@ -1137,8 +1168,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
<Textarea
{...commonProps}
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
rows={2}
className="resize-none text-xs sm:text-sm"
rows={1}
className="min-h-[28px] resize-none text-xs"
/>
);
@ -1163,7 +1194,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
onValueChange={(val) => handleFieldChange(itemId, groupId, entryId, field.name, val)}
disabled={componentConfig.disabled || componentConfig.readonly}
>
<SelectTrigger size="default" className="w-full">
<SelectTrigger size="default" className="h-7 w-full text-xs">
<SelectValue placeholder={field.placeholder || "선택하세요"} />
</SelectTrigger>
<SelectContent>
@ -1264,28 +1295,42 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const displayItems = group?.displayItems || [];
if (displayItems.length === 0) {
// displayItems가 없으면 기본 방식 (해당 그룹의 필드만 나열)
const fields = (componentConfig.additionalFields || []).filter((f) =>
componentConfig.fieldGroups && componentConfig.fieldGroups.length > 0 ? f.groupId === groupId : true,
);
return fields
// displayItems가 없으면 기본 방식 (해당 그룹의 visible 필드만 나열)
const fields = (componentConfig.additionalFields || []).filter((f) => {
// 그룹 필터
const matchGroup = componentConfig.fieldGroups && componentConfig.fieldGroups.length > 0
? f.groupId === groupId
: true;
// hidden 필드 제외 (width: "0px"인 필드)
const isVisible = f.width !== "0px";
return matchGroup && isVisible;
});
// 값이 있는 필드만 "라벨: 값" 형식으로 표시
const displayParts = fields
.map((f) => {
const value = entry[f.name];
if (!value) return "-";
if (!value && value !== 0) return null;
const strValue = String(value);
// 🔧 ISO 날짜 형식 자동 감지 및 포맷팅 (필드 타입 무관)
// ISO 8601 형식: YYYY-MM-DDTHH:mm:ss.sssZ 또는 YYYY-MM-DD
// ISO 날짜 형식 자동 포맷팅
const isoDateMatch = strValue.match(/^(\d{4})-(\d{2})-(\d{2})(T|\s|$)/);
if (isoDateMatch) {
const [, year, month, day] = isoDateMatch;
return `${year}.${month}.${day}`;
return `${f.label}: ${year}.${month}.${day}`;
}
return strValue;
return `${f.label}: ${strValue}`;
})
.join(" / ");
.filter(Boolean);
// 빈 항목 표시: 그룹 필드명으로 안내 메시지 생성
if (displayParts.length === 0) {
const fieldLabels = fields.slice(0, 2).map(f => f.label).join("/");
return `신규 ${fieldLabels} 입력`;
}
return displayParts.join(" ");
}
// displayItems 설정대로 렌더링
@ -1459,15 +1504,117 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
// 빈 상태 렌더링
if (items.length === 0) {
// 디자인 모드: 샘플 데이터로 미리보기 표시
if (isDesignMode) {
const sampleDisplayCols = componentConfig.displayColumns || [];
const sampleFields = (componentConfig.additionalFields || []).filter(f => f.name !== "item_id" && f.width !== "0px");
const sampleGroups = componentConfig.fieldGroups || [{ id: "default", title: "입력 정보", order: 0 }];
const gridCols = sampleGroups.length === 1 ? "grid-cols-1" : "grid-cols-2";
return (
<div style={componentStyle} className={className} onClick={handleClick}>
<div className="bg-card space-y-3 p-3">
{/* 미리보기 안내 배너 */}
<div className="bg-muted/50 flex items-center gap-2 rounded-md px-3 py-2 text-xs">
<span className="text-primary font-medium">[]</span>
<span className="text-muted-foreground"> </span>
</div>
{/* 샘플 품목 카드 2개 */}
{[1, 2].map((idx) => (
<Card key={idx} className="border shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between text-sm font-semibold">
<span>
{idx}. {sampleDisplayCols.length > 0 ? `샘플 ${sampleDisplayCols[0]?.label || "품목"} ${idx}` : `샘플 항목 ${idx}`}
</span>
<Button type="button" variant="ghost" size="sm" className="h-6 w-6 p-0 text-red-400" disabled>
<X className="h-3 w-3" />
</Button>
</CardTitle>
{sampleDisplayCols.length > 0 && (
<div className="text-muted-foreground text-xs">
{sampleDisplayCols.map((col, i) => (
<span key={col.name}>
{i > 0 && " | "}
<span className="text-muted-foreground/60">{col.label}: </span>
</span>
))}
</div>
)}
</CardHeader>
<CardContent className="pt-0">
<div className={`grid ${gridCols} gap-2`}>
{sampleGroups.map((group) => {
const groupFields = sampleFields.filter(f =>
sampleGroups.length <= 1 || f.groupId === group.id
);
if (groupFields.length === 0) return null;
const isSingle = group.maxEntries === 1;
return (
<Card key={group.id} className="border shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between text-xs font-semibold">
<span>{group.title}</span>
{!isSingle && (
<Button type="button" size="sm" variant="outline" className="h-6 text-[10px]" disabled>
+
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-1 pb-3">
{isSingle ? (
/* 1:1 그룹: 인라인 폼 미리보기 */
<div className="grid grid-cols-2 gap-1">
{groupFields.slice(0, 4).map(f => (
<div key={f.name} className="space-y-0.5">
<span className="text-muted-foreground text-[9px]">{f.label}</span>
<div className="bg-muted/40 h-5 rounded border text-[10px] leading-5 px-1"></div>
</div>
))}
{groupFields.length > 4 && (
<div className="text-muted-foreground col-span-2 text-[9px]"> {groupFields.length - 4} </div>
)}
</div>
) : (
/* 1:N 그룹: 다중 항목 미리보기 */
<>
<div className="bg-muted/30 flex items-center justify-between rounded border p-1.5 text-[10px]">
<span className="truncate">
1. {groupFields.slice(0, 2).map(f => `${f.label}: 샘플`).join(" / ")}
</span>
<X className="h-2.5 w-2.5 shrink-0 text-gray-400" />
</div>
{idx === 1 && (
<div className="bg-muted/30 flex items-center justify-between rounded border p-1.5 text-[10px]">
<span className="truncate">
2. {groupFields.slice(0, 2).map(f => `${f.label}: 샘플`).join(" / ")}
</span>
<X className="h-2.5 w-2.5 shrink-0 text-gray-400" />
</div>
)}
</>
)}
</CardContent>
</Card>
);
})}
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
// 런타임 빈 상태
return (
<div style={componentStyle} className={className} onClick={handleClick}>
<div className="border-border bg-muted/30 flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 text-center">
<p className="text-muted-foreground text-sm">{componentConfig.emptyMessage}</p>
{isDesignMode && (
<p className="text-muted-foreground mt-2 text-xs">
💡 "다음" .
</p>
)}
</div>
</div>
);
@ -1483,141 +1630,147 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
const sortedGroups = [...effectiveGroups].sort((a, b) => (a.order || 0) - (b.order || 0));
// 그룹 수에 따라 grid 열 수 결정
const gridCols = sortedGroups.length === 1 ? "grid-cols-1" : "grid-cols-2";
return (
<div className="grid grid-cols-2 gap-3">
<div className={`grid ${gridCols} gap-3`}>
{sortedGroups.map((group) => {
const groupFields = fields.filter((f) => (groups.length === 0 ? true : f.groupId === group.id));
if (groupFields.length === 0) return null;
const groupEntries = item.fieldGroups[group.id] || [];
const isEditingThisGroup = isEditing && editingItemId === item.id && editingGroupId === group.id;
// 그룹별 독립 편집 상태 사용
const editingEntryIdForGroup = editingEntries[group.id] || null;
// 1:1 관계 그룹 (maxEntries === 1): 인라인 폼으로 바로 표시
const isSingleEntry = group.maxEntries === 1;
const singleEntry = isSingleEntry ? (groupEntries[0] || { id: `${group.id}_auto_1` }) : null;
// hidden 필드 제외 (width: "0px"인 필드)
const visibleFields = groupFields.filter((f) => f.width !== "0px");
return (
<Card key={group.id} className="border-2 shadow-sm">
<CardHeader className="pb-3">
<CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between text-sm font-semibold">
<span>{group.title}</span>
<Button
type="button"
onClick={() => handleAddGroupEntry(item.id, group.id)}
size="sm"
variant="outline"
className="h-7 text-xs"
>
+
</Button>
{/* 1:N 그룹만 + 추가 버튼 표시 */}
{!isSingleEntry && (
<Button
type="button"
onClick={() => handleAddGroupEntry(item.id, group.id)}
size="sm"
variant="outline"
className="h-6 text-[11px]"
>
+
</Button>
)}
</CardTitle>
{group.description && <p className="text-muted-foreground text-xs">{group.description}</p>}
{group.description && <p className="text-muted-foreground text-[11px]">{group.description}</p>}
</CardHeader>
<CardContent className="space-y-3">
{/* 이미 입력된 항목들 */}
{groupEntries.length > 0 ? (
<div className="space-y-2">
{groupEntries.map((entry, idx) => {
const isEditingThisEntry = isEditingThisGroup && editingDetailId === entry.id;
if (isEditingThisEntry) {
// 편집 모드: 입력 필드 표시 (가로 배치)
return (
<Card key={entry.id} className="border-primary border-dashed">
<CardContent className="p-3">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs font-medium"> </span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setIsEditing(false);
setEditingItemId(null);
setEditingGroupId(null);
setEditingDetailId(null);
}}
className="h-6 text-xs"
>
</Button>
</div>
{/* 🆕 가로 Grid 배치 (2~3열) */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{groupFields.map((field) => (
<div key={field.name} className="space-y-1">
<label className="text-xs font-medium">
{field.label}
{field.required && <span className="text-destructive ml-1">*</span>}
</label>
{renderField(field, item.id, group.id, entry.id, entry)}
</div>
))}
</div>
</CardContent>
</Card>
);
} else {
// 읽기 모드: 텍스트 표시 (클릭하면 수정)
return (
<div
key={entry.id}
className="bg-muted/30 hover:bg-muted/50 flex cursor-pointer items-center justify-between rounded border p-2 text-xs"
onClick={() => handleEditGroupEntry(item.id, group.id, entry.id)}
>
<span className="flex items-center gap-1">
{idx + 1}. {renderDisplayItems(entry, item, group.id)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleRemoveGroupEntry(item.id, group.id, entry.id);
}}
className="h-6 w-6 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
);
}
})}
<CardContent className="space-y-1.5 pt-0">
{/* === 1:1 그룹: 인라인 폼 (항상 편집 모드) - 컴팩트 === */}
{isSingleEntry && singleEntry && (
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5">
{visibleFields.map((field) => (
<div key={field.name} className={cn(
"space-y-0.5",
field.type === "textarea" && "col-span-2"
)}>
<label className="text-[11px] font-medium leading-none">
{field.label}
{field.required && <span className="text-destructive ml-0.5">*</span>}
</label>
{renderField(field, item.id, group.id, singleEntry.id, singleEntry)}
</div>
))}
</div>
) : (
<p className="text-muted-foreground text-xs italic"> .</p>
)}
{/* 새 항목 입력 중 */}
{isEditingThisGroup && editingDetailId && !groupEntries.find((e) => e.id === editingDetailId) && (
<Card className="border-primary border-dashed">
<CardContent className="space-y-2 p-3">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs font-medium"> </span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setIsEditing(false);
setEditingItemId(null);
setEditingGroupId(null);
setEditingDetailId(null);
}}
className="h-6 text-xs"
>
</Button>
{/* === 1:N 그룹: 다중 입력 (독립 편집) === */}
{!isSingleEntry && (
<>
{groupEntries.length > 0 ? (
<div className="space-y-1.5">
{groupEntries.map((entry, idx) => {
// 그룹별 독립 편집 상태 확인
const isEditingThisEntry = editingEntryIdForGroup === entry.id;
return (
<div key={entry.id} className="bg-muted/30 rounded border">
{/* 헤더 (항상 표시) */}
<div
className="hover:bg-muted/50 flex cursor-pointer items-center justify-between px-2 py-1.5 text-xs"
onClick={() => {
if (isEditingThisEntry) {
// 이 그룹만 닫기 (다른 그룹은 유지)
closeGroupEditing(group.id);
} else {
handleEditGroupEntry(item.id, group.id, entry.id);
}
}}
>
<span className="flex items-center gap-1">
{idx + 1}. {renderDisplayItems(entry, item, group.id)}
</span>
<div className="flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleRemoveGroupEntry(item.id, group.id, entry.id);
}}
className="h-5 w-5 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
{/* 폼 영역 (편집 시에만 아래로 펼침) - 컴팩트 */}
{isEditingThisEntry && (
<div className="border-t px-2 pb-2 pt-1.5">
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5">
{visibleFields.map((field) => (
<div key={field.name} className={cn(
"space-y-0.5",
field.type === "textarea" && "col-span-2"
)}>
<label className="text-[11px] font-medium leading-none">
{field.label}
{field.required && <span className="text-destructive ml-0.5">*</span>}
</label>
{renderField(field, item.id, group.id, entry.id, entry)}
</div>
))}
</div>
<div className="mt-1.5 flex justify-end">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => closeGroupEditing(group.id)}
className="h-6 px-3 text-[11px]"
>
</Button>
</div>
</div>
)}
</div>
);
})}
</div>
{groupFields.map((field) => (
<div key={field.name} className="space-y-1">
<label className="text-xs font-medium">
{field.label}
{field.required && <span className="text-destructive ml-1">*</span>}
</label>
{renderField(field, item.id, group.id, editingDetailId, {})}
</div>
))}
</CardContent>
</Card>
) : (
<p className="text-muted-foreground text-xs italic"> .</p>
)}
{/* 새 항목은 handleAddGroupEntry에서 아코디언 항목으로 직접 추가됨 */}
</>
)}
</CardContent>
</Card>

View File

@ -54,6 +54,8 @@ export interface FieldGroup {
description?: string;
/** 그룹 표시 순서 */
order?: number;
/** 🆕 최대 항목 수 (1이면 1:1 관계 - 자동 생성, + 추가 버튼 숨김) */
maxEntries?: number;
/** 🆕 이 그룹의 항목 표시 설정 (그룹별로 다른 표시 형식 가능) */
displayItems?: DisplayItem[];
}