65 KiB
WACE ERP 프론트엔드 아키텍처 상세 분석
작성일: 2026-02-06 작성자: AI Assistant 프로젝트: WACE ERP-node 목적: 시스템 전체 워크플로우 문서화를 위한 프론트엔드 구조 분석
목차
- 전체 디렉토리 구조
- Next.js App Router 구조
- 컴포넌트 시스템
- V2 컴포넌트 시스템
- 화면 디자이너 워크플로우
- API 클라이언트 시스템
- 상태 관리
- 레지스트리 시스템
- 대시보드 시스템
- 다국어 지원
- 인증 플로우
- 사용자 워크플로우
1. 전체 디렉토리 구조
frontend/
├── app/ # Next.js 14 App Router
│ ├── (main)/ # 메인 레이아웃 그룹
│ ├── (auth)/ # 인증 레이아웃 그룹
│ ├── (admin)/ # 관리자 레이아웃 그룹
│ ├── (pop)/ # 팝업 레이아웃 그룹
│ ├── layout.tsx # 루트 레이아웃
│ └── registry-provider.tsx # 레지스트리 초기화 프로바이더
│
├── components/ # React 컴포넌트
│ ├── admin/ # 관리자 컴포넌트 (137개)
│ ├── screen/ # 화면 디자이너/뷰어 (139개)
│ ├── dashboard/ # 대시보드 컴포넌트 (32개)
│ ├── dataflow/ # 데이터플로우 관련 (90개)
│ ├── v2/ # V2 컴포넌트 시스템 (28개)
│ ├── common/ # 공통 컴포넌트 (19개)
│ ├── layout/ # 레이아웃 컴포넌트 (10개)
│ ├── ui/ # shadcn/ui 기반 UI (31개)
│ └── ... # 기타 도메인별 컴포넌트
│
├── lib/ # 유틸리티 & 라이브러리
│ ├── api/ # API 클라이언트 (57개 파일)
│ │ ├── client.ts # Axios 기본 설정
│ │ ├── screen.ts # 화면 관리 API
│ │ ├── user.ts # 사용자 관리 API
│ │ └── ... # 도메인별 API
│ │
│ ├── registry/ # 컴포넌트 레지스트리 시스템 (540개)
│ │ ├── ComponentRegistry.ts # 컴포넌트 등록 관리
│ │ ├── WebTypeRegistry.ts # 웹타입 등록 관리
│ │ ├── LayoutRegistry.ts # 레이아웃 등록 관리
│ │ ├── init.ts # 레지스트리 초기화
│ │ ├── DynamicComponentRenderer.tsx # 동적 렌더링
│ │ ├── components/ # 등록 가능한 컴포넌트들 (288 tsx, 205 ts)
│ │ │ ├── v2-input/
│ │ │ ├── v2-select/
│ │ │ ├── text-input/
│ │ │ ├── entity-search-input/
│ │ │ ├── modal-repeater-table/
│ │ │ └── ...
│ │ └── layouts/ # 레이아웃 컴포넌트
│ │ ├── accordion/
│ │ ├── grid/
│ │ ├── flexbox/
│ │ ├── split/
│ │ └── tabs/
│ │
│ ├── v2-core/ # V2 코어 시스템
│ │ ├── events/ # 이벤트 버스
│ │ ├── adapters/ # 레거시 어댑터
│ │ ├── components/ # V2 공통 컴포넌트
│ │ ├── services/ # V2 서비스
│ │ └── init.ts # V2 초기화
│ │
│ ├── utils/ # 유틸리티 함수들 (40개+)
│ ├── services/ # 비즈니스 로직 서비스
│ ├── stores/ # Zustand 스토어
│ └── constants/ # 상수 정의
│
├── contexts/ # React Context (12개)
│ ├── AuthContext.tsx # 인증 컨텍스트
│ ├── MenuContext.tsx # 메뉴 컨텍스트
│ ├── ScreenContext.tsx # 화면 컨텍스트
│ ├── DashboardContext.tsx # 대시보드 컨텍스트
│ └── ...
│
├── hooks/ # Custom Hooks (32개)
│ ├── useAuth.ts # 인증 훅
│ ├── useMenu.ts # 메뉴 관리 훅
│ ├── useFormValidation.ts # 폼 검증 훅
│ └── ...
│
├── types/ # TypeScript 타입 정의 (44개)
│ ├── screen.ts # 화면 관련 타입
│ ├── component.ts # 컴포넌트 타입
│ ├── user.ts # 사용자 타입
│ └── ...
│
├── providers/ # React Provider
│ └── QueryProvider.tsx # React Query 설정
│
├── stores/ # 전역 상태 관리
│ ├── flowStepStore.ts # 플로우 단계 상태
│ ├── modalDataStore.ts # 모달 데이터 상태
│ └── tableDisplayStore.ts # 테이블 표시 상태
│
└── public/ # 정적 파일
├── favicon.ico
├── logo.png
└── locales/ # 다국어 파일
2. Next.js App Router 구조
2.1 라우팅 구조
WACE ERP는 Next.js 14 App Router를 사용하며, 레이아웃 그룹을 활용한 구조화된 라우팅을 제공합니다.
라우트 그룹 구조
app/
├── layout.tsx # 글로벌 레이아웃
│ ├── QueryProvider # React Query 초기화
│ ├── RegistryProvider # 컴포넌트 레지스트리 초기화
│ ├── Toaster # 토스트 알림
│ └── ScreenModal # 전역 화면 모달
│
├── (main)/ # 메인 애플리케이션
│ ├── layout.tsx
│ │ ├── AuthProvider # 인증 컨텍스트
│ │ ├── MenuProvider # 메뉴 컨텍스트
│ │ └── AppLayout # 사이드바, 헤더
│ │
│ ├── main/page.tsx # 메인 페이지
│ ├── dashboard/ # 대시보드
│ │ ├── page.tsx # 대시보드 목록
│ │ └── [dashboardId]/page.tsx # 대시보드 상세
│ │
│ ├── screens/[screenId]/page.tsx # 동적 화면 뷰어
│ │
│ └── admin/ # 관리자 페이지
│ ├── page.tsx # 관리자 홈
│ ├── menu/page.tsx # 메뉴 관리
│ │
│ ├── userMng/ # 사용자 관리
│ │ ├── userMngList/page.tsx
│ │ ├── rolesList/page.tsx
│ │ ├── userAuthList/page.tsx
│ │ └── companyList/page.tsx
│ │
│ ├── screenMng/ # 화면 관리
│ │ ├── screenMngList/page.tsx
│ │ ├── dashboardList/page.tsx
│ │ └── reportList/page.tsx
│ │
│ ├── systemMng/ # 시스템 관리
│ │ ├── tableMngList/page.tsx
│ │ ├── commonCodeList/page.tsx
│ │ ├── i18nList/page.tsx
│ │ ├── dataflow/page.tsx
│ │ └── collection-managementList/page.tsx
│ │
│ ├── automaticMng/ # 자동화 관리
│ │ ├── flowMgmtList/page.tsx
│ │ ├── batchmngList/page.tsx
│ │ ├── exCallConfList/page.tsx
│ │ └── mail/ # 메일 관리
│ │ ├── accounts/page.tsx
│ │ ├── send/page.tsx
│ │ ├── receive/page.tsx
│ │ └── templates/page.tsx
│ │
│ └── [...slug]/page.tsx # 동적 관리자 페이지
│
├── (auth)/ # 인증 페이지
│ ├── layout.tsx # 최소 레이아웃
│ └── login/page.tsx # 로그인 페이지
│
├── (admin)/ # 관리자 전용 (별도 레이아웃)
│ └── admin/
│ ├── vehicle-trips/page.tsx
│ └── vehicle-reports/page.tsx
│
└── (pop)/ # 팝업 페이지
├── layout.tsx # 팝업 레이아웃
├── pop/page.tsx
└── work/page.tsx
2.2 페이지 목록 (총 76개)
메인 애플리케이션 (50개)
/main- 메인 페이지/dashboard- 대시보드 목록/dashboard/[dashboardId]- 대시보드 상세/screens/[screenId]- 동적 화면 뷰어 ⭐ 핵심/multilang- 다국어 관리
관리자 페이지 (40개+)
- 사용자 관리: 사용자, 역할, 권한, 회사, 부서
- 화면 관리: 화면 목록, 대시보드, 리포트
- 시스템 관리: 테이블, 공통코드, 다국어, 데이터플로우, 컬렉션
- 자동화 관리: 플로우, 배치, 외부호출, 메일
- 표준 관리: 웹타입 표준화
- 캐스케이딩: 관계, 계층, 상호배타, 자동채움
테스트 페이지 (5개)
/test-autocomplete-mapping/test-entity-search/test-order-registration/test-type-safety/test-flow
인증/기타 (2개)
/login- 로그인/pop- 팝업 페이지
2.3 Next.js 설정
next.config.mjs
{
output: "standalone", // Docker 최적화
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
// API 프록시: /api/* → http://localhost:8080/api/*
rewrites: [
{ source: "/api/:path*", destination: "http://localhost:8080/api/:path*" }
],
// CORS 헤더
headers: [
{ source: "/api/:path*", headers: [...] }
],
// 환경 변수
env: {
NEXT_PUBLIC_API_URL: "http://localhost:8080/api"
}
}
3. 컴포넌트 시스템
3.1 컴포넌트 분류
WACE ERP의 컴포넌트는 크게 4가지 계층으로 구분됩니다:
계층 구조
1. UI 컴포넌트 (shadcn/ui 기반) - components/ui/
└─ Button, Input, Select, Dialog, Card 등 기본 UI
2. 공통 컴포넌트 - components/common/
└─ ScreenModal, FormValidationIndicator 등
3. 도메인 컴포넌트 - components/{domain}/
├─ screen/ - 화면 디자이너/뷰어
├─ admin/ - 관리자 기능
├─ dashboard/ - 대시보드
└─ dataflow/ - 데이터플로우
4. 레지스트리 컴포넌트 - lib/registry/components/
└─ 동적으로 등록/렌더링되는 컴포넌트들 (90개+)
3.2 주요 컴포넌트 모듈
📁 components/screen/ (139개 파일)
핵심 컴포넌트
| 컴포넌트 | 역할 | 주요 기능 |
|---|---|---|
ScreenDesigner.tsx (7095줄) |
화면 디자이너 | - 드래그&드롭으로 컴포넌트 배치 - 그리드 시스템 (12컬럼) - 실시간 미리보기 - 컴포넌트 설정 패널 - 그룹화/정렬/분산 도구 |
InteractiveScreenViewer.tsx (2472줄) |
화면 뷰어 | - 실제 사용자 화면 렌더링 - 폼 데이터 바인딩 - 버튼 액션 실행 - 검증 처리 |
ScreenManagementList.tsx |
화면 목록 | - 화면 CRUD - 메뉴 할당 - 다국어 설정 |
DynamicWebTypeRenderer.tsx |
웹타입 렌더러 | - WebTypeRegistry 기반 동적 렌더링 |
위젯 컴포넌트 (widgets/types/)
- TextWidget, NumberWidget, DateWidget
- SelectWidget, CheckboxWidget, RadioWidget
- TextareaWidget, FileWidget, CodeWidget
- ButtonWidget, EntitySearchInputWrapper
설정 패널 (config-panels/)
- 각 웹타입별 설정 패널 (TextConfigPanel, SelectConfigPanel 등)
모달 컴포넌트 (modals/)
- 키보드 단축키, 다국어 설정, 메뉴 할당 등
📁 components/admin/ (137개 파일)
관리자 기능 컴포넌트
- 사용자 관리: UserManagementList, RolesManagementList, CompanyList
- 화면 관리: ScreenManagementList, DashboardManagementList
- 테이블 관리: TableManagementList, ColumnEditor
- 공통코드 관리: CommonCodeManagement
- 메뉴 관리: MenuManagement
📁 components/dashboard/ (32개 파일)
대시보드 컴포넌트
- DashboardViewer: 대시보드 렌더링
- DashboardWidgets: 차트, 테이블, 카드 등 위젯
- ChartComponents: 라인, 바, 파이, 도넛 차트
📁 components/dataflow/ (90개+ 파일)
데이터플로우 시스템
- DataFlowList: 플로우 목록
- node-editor/: 노드 편집기
- connection/: 커넥션 관리 (38개 파일)
- condition/: 조건 설정
- external-call/: 외부 호출 설정
4. V2 컴포넌트 시스템
V2는 통합 컴포넌트 시스템으로, 유사한 기능을 하나의 컴포넌트로 통합하여 개발 효율성을 높입니다.
4.1 V2 컴포넌트 종류 (9개)
| ID | 이름 | 설명 | 포함 기능 |
|---|---|---|---|
v2-input |
통합 입력 | 텍스트, 숫자, 비밀번호, 슬라이더, 컬러 등 | text, number, password, email, tel, textarea, slider, color |
v2-select |
통합 선택 | 드롭다운, 라디오, 체크박스, 태그, 토글 등 | dropdown, radio, checkbox, tagbox, toggle, swap, combobox |
v2-date |
통합 날짜 | 날짜, 시간, 날짜시간, 날짜 범위 | date, time, datetime, daterange |
v2-list |
통합 목록 | 테이블, 카드, 칸반, 리스트 | table, card, kanban, list, grid |
v2-layout |
통합 레이아웃 | 그리드, 분할 패널, 플렉스 | grid, split, flex, masonry |
v2-group |
통합 그룹 | 탭, 아코디언, 섹션, 모달 | tabs, accordion, section, modal, drawer |
v2-media |
통합 미디어 | 이미지, 비디오, 오디오, 파일 업로드 | image, video, audio, file |
v2-biz |
통합 비즈니스 | 플로우, 랙, 채번규칙, 카테고리 | flow, rack, numbering, category |
v2-repeater |
통합 반복 | 인라인 테이블, 모달, 버튼 | inline-table, modal, button, selected-items |
4.2 V2 아키텍처
components/v2/ # V2 UI 컴포넌트
├── V2Input.tsx # 통합 입력 컴포넌트
├── V2Select.tsx # 통합 선택 컴포넌트
├── V2Date.tsx # 통합 날짜 컴포넌트
├── V2List.tsx # 통합 목록 컴포넌트
├── V2Layout.tsx # 통합 레이아웃 컴포넌트
├── V2Group.tsx # 통합 그룹 컴포넌트
├── V2Media.tsx # 통합 미디어 컴포넌트
├── V2Biz.tsx # 통합 비즈니스 컴포넌트
├── V2Hierarchy.tsx # 통합 계층 컴포넌트
├── V2Repeater.tsx # 통합 반복 컴포넌트
├── V2FormContext.tsx # V2 폼 컨텍스트
├── V2ComponentRenderer.tsx # V2 렌더러
├── registerV2Components.ts # V2 등록 함수
└── config-panels/ # V2 설정 패널
├── V2InputConfigPanel.tsx
├── V2SelectConfigPanel.tsx
└── ...
lib/v2-core/ # V2 코어 라이브러리
├── events/ # 이벤트 버스
│ ├── EventBus.ts # 발행/구독 패턴
│ └── types.ts # 이벤트 타입
├── adapters/ # 레거시 어댑터
│ └── LegacyEventAdapter.ts # 레거시 시스템 연동
├── components/ # V2 공통 컴포넌트
│ └── V2ErrorBoundary.tsx # 에러 경계
├── services/ # V2 서비스
│ ├── ScheduleGeneratorService.ts
│ └── ScheduleConfirmDialog.tsx
└── init.ts # V2 초기화
lib/registry/components/v2-*/ # V2 렌더러 (레지스트리용)
├── v2-input/
│ ├── V2InputRenderer.tsx # 자동 등록 렌더러
│ └── index.ts # 컴포넌트 정의
├── v2-select/
│ ├── V2SelectRenderer.tsx
│ └── index.ts
└── ...
4.3 V2 초기화 흐름
// 1. app/registry-provider.tsx
useEffect(() => {
// 레거시 레지스트리 초기화
initializeRegistries();
// V2 Core 초기화
initV2Core({
debug: false,
legacyBridge: { legacyToV2: true, v2ToLegacy: true }
});
}, []);
// 2. lib/registry/init.ts
export function initializeRegistries() {
// 웹타입 등록 (text, number, date 등)
initializeWebTypeRegistry();
// V2 컴포넌트 등록
registerV2Components();
}
// 3. components/v2/registerV2Components.ts
export function registerV2Components() {
for (const definition of v2ComponentDefinitions) {
ComponentRegistry.registerComponent(definition);
}
}
4.4 V2 렌더링 흐름
사용자 요청 (screens/[screenId])
↓
InteractiveScreenViewer
↓
DynamicComponentRenderer
↓
┌─ componentType이 v2-* 인가? ─┐
│ Yes │ No
↓ ↓
ComponentRegistry.getComponent LegacyComponentRegistry.get
↓ ↓
V2*Renderer (클래스 기반) 레거시 렌더러 (함수)
↓ ↓
render() → V2* 컴포넌트 직접 렌더링
↓
V2FormContext 연동 (선택)
↓
최종 렌더링
4.5 V2 vs 레거시 비교
| 항목 | 레거시 시스템 | V2 시스템 |
|---|---|---|
| 컴포넌트 수 | 90개+ (분산) | 9개 (통합) |
| 렌더러 패턴 | 함수형 렌더러 | 클래스 기반 자동 등록 |
| 폼 통합 | 개별 구현 | V2FormContext 통합 |
| 이벤트 시스템 | props drilling | V2 EventBus (발행/구독) |
| 에러 처리 | 개별 처리 | V2ErrorBoundary 통합 |
| 설정 패널 | 개별 패널 | 통합 설정 시스템 |
| 핫 리로드 | 미지원 | 개발 모드 지원 |
5. 화면 디자이너 워크플로우
5.1 화면 디자이너 구성
ScreenDesigner.tsx (7095줄) - 핵심 컴포넌트
┌─────────────────────────────────────────────────────────────────┐
│ Screen Designer │
├─────────────────────────────────────────────────────────────────┤
│ 탭1: 디자인 | 탭2: 프리뷰 | 탭3: 다국어 | 탭4: 메뉴할당 | 탭5: 스타일 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [컴포넌트 팔레트] [캔버스] [속성 패널] │
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ 입력 컴포넌트 │ │ │ │ 기본 설정 │ │
│ │ - Text │ │ 드래그&드롭 │ │ - ID │ │
│ │ - Number │ │ 컴포넌트 │ │ - 라벨 │ │
│ │ - Date │ ───▶ │ 배치 영역 │ ◀── │ - 위치/크기 │ │
│ │ - Select │ │ │ │ - 데이터연결 │ │
│ │ │ │ 그리드 기반 │ │ │ │
│ │ 표시 컴포넌트 │ │ 12컬럼 │ │ 고급 설정 │ │
│ │ - Table │ │ │ │ - 조건부표시 │ │
│ │ - Card │ │ │ │ - 검증규칙 │ │
│ │ │ │ │ │ - 버튼액션 │ │
│ │ 레이아웃 │ └────────────────┘ └──────────────┘ │
│ │ - Grid │ │
│ │ - Tabs │ [하단 도구] │
│ │ - Accordion │ ┌─────────────────────────────────────┐ │
│ │ │ │ 그룹화 | 정렬 | 분산 | 라벨토글 │ │
│ └──────────────┘ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
5.2 주요 기능
그리드 시스템
// 12컬럼 그리드 기반
const GRID_COLUMNS = 12;
const COLUMN_WIDTH = (containerWidth - gaps) / 12;
// 컴포넌트 너비 → 컬럼 스팬 변환
function calculateColumnSpan(width: number): number {
return Math.max(1, Math.min(12, Math.round(width / COLUMN_WIDTH)));
}
// 반응형 지원
{
default: { span: 6 }, // 기본 (50%)
sm: { span: 12 }, // 모바일 (100%)
md: { span: 6 }, // 태블릿 (50%)
lg: { span: 4 } // 데스크톱 (33%)
}
컴포넌트 배치 흐름
1. 컴포넌트 팔레트에서 드래그 시작
↓
2. 캔버스에 드롭
↓
3. 컴포넌트 생성 (generateComponentId)
↓
4. 위치 계산 (10px 단위 스냅)
↓
5. 그리드 정렬 (12컬럼 기준)
↓
6. 레이아웃 데이터 업데이트
↓
7. 리렌더링 (RealtimePreview)
컴포넌트 설정
선택된 컴포넌트 → 우측 속성 패널 표시
↓
┌─────────────────────────────────────┐
│ 기본 설정 │
│ - ID: comp_1234 │
│ - 라벨: "제품명" │
│ - 컬럼명: product_name │
│ - 필수: ☑ │
│ - 읽기전용: ☐ │
│ │
│ 크기 & 위치 │
│ - X: 100px, Y: 200px │
│ - 너비: 300px, 높이: 40px │
│ - 컬럼 스팬: 6 │
│ │
│ 데이터 연결 │
│ - 테이블: product_info │
│ - 컬럼: product_name │
│ - 웹타입: text │
│ │
│ 고급 설정 │
│ - 조건부 표시: field === 'A' │
│ - 버튼 액션: [등록], [수정], [삭제] │
│ - 데이터플로우: flow_123 │
└─────────────────────────────────────┘
5.3 저장 구조
ScreenDefinition (JSON)
{
"screenId": 123,
"screenCode": "PRODUCT_MGMT",
"screenName": "제품 관리",
"tableName": "product_info",
"screenType": "form",
"version": 2,
"layoutData": {
"components": [
{
"id": "comp_text_1",
"type": "text-input",
"componentType": "text-input",
"label": "제품명",
"columnName": "product_name",
"position": { "x": 100, "y": 50, "z": 0 },
"size": { "width": 300, "height": 40 },
"componentConfig": {
"required": true,
"placeholder": "제품명을 입력하세요",
"maxLength": 100
},
"style": {
"labelDisplay": true,
"labelText": "제품명"
}
},
{
"id": "comp_table_1",
"type": "table-list",
"componentType": "table-list",
"tableName": "product_info",
"position": { "x": 50, "y": 200, "z": 0 },
"size": { "width": 800, "height": 400 },
"componentConfig": {
"columns": ["product_code", "product_name", "price"],
"pagination": true,
"sortable": true
}
}
],
"layouts": [
{
"id": "layout_tabs_1",
"layoutType": "tabs",
"children": [
{ "tabId": "tab1", "title": "기본정보", "components": ["comp_text_1"] },
{ "tabId": "tab2", "title": "상세정보", "components": ["comp_table_1"] }
]
}
]
},
"createdBy": "user123",
"createdDate": "2024-01-01T00:00:00Z"
}
6. API 클라이언트 시스템
6.1 API 클라이언트 구조
lib/api/ (57개 파일)
lib/api/
├── client.ts # Axios 기본 설정 & 인터셉터
│
├── 화면 관련 (3개)
│ ├── screen.ts # 화면 CRUD
│ ├── screenGroup.ts # 화면 그룹
│ └── screenFile.ts # 화면 파일
│
├── 사용자 관련 (5개)
│ ├── user.ts # 사용자 관리
│ ├── role.ts # 역할 관리
│ ├── company.ts # 회사 관리
│ └── department.ts # 부서 관리
│
├── 테이블 관련 (5개)
│ ├── tableManagement.ts # 테이블 관리
│ ├── tableSchema.ts # 스키마 조회
│ ├── tableHistory.ts # 테이블 이력
│ └── tableCategoryValue.ts # 카테고리 값
│
├── 데이터플로우 (6개)
│ ├── dataflow.ts # 데이터플로우 정의
│ ├── dataflowSave.ts # 저장 로직
│ ├── nodeFlows.ts # 노드 플로우
│ ├── nodeExternalConnections.ts # 외부 연결
│ └── flowExternalDb.ts # 외부 DB 플로우
│
├── 자동화 (4개)
│ ├── batch.ts # 배치 작업
│ ├── batchManagement.ts # 배치 관리
│ ├── externalCall.ts # 외부 호출
│ └── externalCallConfig.ts # 외부 호출 설정
│
├── 시스템 (8개)
│ ├── menu.ts # 메뉴 관리
│ ├── commonCode.ts # 공통코드
│ ├── multilang.ts # 다국어
│ ├── layout.ts # 레이아웃
│ ├── collection.ts # 컬렉션
│ └── ...
│
└── 기타 (26개)
├── data.ts # 동적 데이터 CRUD
├── dynamicForm.ts # 동적 폼
├── file.ts # 파일 업로드
├── dashboard.ts # 대시보드
├── mail.ts # 메일
├── reportApi.ts # 리포트
└── ...
6.2 API 클라이언트 기본 설정
lib/api/client.ts
// 1. 동적 API URL 설정
const getApiBaseUrl = (): string => {
// 환경변수 우선
if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL;
// 프로덕션: v1.vexplor.com → api.vexplor.com
if (currentHost === "v1.vexplor.com") {
return "https://api.vexplor.com/api";
}
// 로컬: localhost:9771 → localhost:8080
return "http://localhost:8080/api";
};
export const API_BASE_URL = getApiBaseUrl();
// 2. Axios 인스턴스 생성
export const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: { "Content-Type": "application/json" },
withCredentials: true
});
// 3. 요청 인터셉터 (JWT 토큰 자동 추가)
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem("authToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 다국어: GET 요청에 userLang 파라미터 추가
if (config.method === "GET") {
config.params = {
...config.params,
userLang: window.__GLOBAL_USER_LANG || "KR"
};
}
return config;
});
// 4. 응답 인터셉터 (토큰 갱신, 401 처리)
apiClient.interceptors.response.use(
(response) => {
// 서버에서 새 토큰 전송 시 자동 갱신
const newToken = response.headers["x-new-token"];
if (newToken) {
localStorage.setItem("authToken", newToken);
}
return response;
},
async (error) => {
// 401 에러: 토큰 갱신 시도 → 실패 시 로그인 페이지
if (error.response?.status === 401) {
const newToken = await refreshToken();
if (newToken) {
error.config.headers.Authorization = `Bearer ${newToken}`;
return apiClient.request(error.config); // 재시도
}
window.location.href = "/login";
}
return Promise.reject(error);
}
);
6.3 API 사용 패턴
✅ 올바른 사용법 (lib/api 클라이언트 사용)
import { screenApi } from "@/lib/api/screen";
import { dataApi } from "@/lib/api/data";
// 화면 목록 조회
const screens = await screenApi.getScreens({ page: 1, size: 20 });
// 데이터 생성
const result = await dataApi.createData("product_info", {
product_name: "제품A",
price: 10000
});
❌ 잘못된 사용법 (fetch 직접 사용 금지!)
// 🚫 금지! JWT 토큰, 다국어, 에러 처리 누락
const res = await fetch('/api/screen-management/screens');
6.4 주요 API 예시
화면 관리 API (screen.ts)
export const screenApi = {
// 화면 목록 조회
getScreens: async (params) => {
const response = await apiClient.get("/screen-management/screens", { params });
return response.data;
},
// 화면 상세 조회
getScreen: async (screenId: number) => {
const response = await apiClient.get(`/screen-management/screens/${screenId}`);
return response.data.data;
},
// 화면 생성
createScreen: async (screen: CreateScreenRequest) => {
const response = await apiClient.post("/screen-management/screens", screen);
return response.data;
},
// 화면 수정
updateScreen: async (screenId: number, screen: UpdateScreenRequest) => {
const response = await apiClient.put(`/screen-management/screens/${screenId}`, screen);
return response.data;
},
// 화면 삭제
deleteScreen: async (screenId: number) => {
const response = await apiClient.delete(`/screen-management/screens/${screenId}`);
return response.data;
}
};
동적 데이터 API (data.ts)
export const dataApi = {
// 데이터 목록 조회 (페이징, 필터링, 정렬)
getDataList: async (tableName: string, params: {
page?: number;
size?: number;
filters?: Record<string, any>;
sortBy?: string;
sortOrder?: "asc" | "desc";
}) => {
const response = await apiClient.get(`/data/${tableName}`, { params });
return response.data;
},
// 데이터 생성
createData: async (tableName: string, data: Record<string, any>) => {
const response = await apiClient.post(`/data/${tableName}`, data);
return response.data;
},
// 데이터 수정
updateData: async (tableName: string, id: number, data: Record<string, any>) => {
const response = await apiClient.put(`/data/${tableName}/${id}`, data);
return response.data;
},
// 데이터 삭제
deleteData: async (tableName: string, id: number) => {
const response = await apiClient.delete(`/data/${tableName}/${id}`);
return response.data;
}
};
7. 상태 관리
7.1 상태 관리 전략
WACE ERP는 하이브리드 상태 관리 전략을 사용합니다:
| 관리 방식 | 사용 시나리오 | 예시 |
|---|---|---|
| React Query | 서버 상태 (캐싱, 자동 갱신) | 화면 목록, 데이터 목록 |
| React Context | 전역 상태 (공유 데이터) | 인증, 메뉴, 화면 |
| Zustand | 클라이언트 상태 (간단한 전역) | 플로우 단계, 모달 데이터 |
| Local State | 컴포넌트 로컬 상태 | 폼 입력, UI 토글 |
7.2 React Context (12개)
contexts/
├── AuthContext.tsx # 인증 상태 & 세션 관리
│ - 사용자 정보
│ - 로그인/로그아웃
│ - 세션 타이머 (30분)
│
├── MenuContext.tsx # 메뉴 트리 & 네비게이션
│ - 메뉴 구조
│ - 현재 선택된 메뉴
│ - 메뉴 접근 권한
│
├── ScreenContext.tsx # 화면 편집 상태
│ - 현재 편집 중인 화면
│ - 선택된 컴포넌트
│ - 실행 취소/다시 실행
│
├── ScreenPreviewContext.tsx # 화면 미리보기
│ - 반응형 모드 (데스크톱/태블릿/모바일)
│ - 미리보기 데이터
│
├── DashboardContext.tsx # 대시보드 상태
│ - 대시보드 레이아웃
│ - 위젯 설정
│
├── TableOptionsContext.tsx # 테이블 옵션
│ - 컬럼 순서
│ - 필터
│ - 정렬
│
├── ActiveTabContext.tsx # 탭 활성화 상태
├── LayerContext.tsx # 레이어 관리
├── ReportDesignerContext.tsx # 리포트 디자이너
├── ScreenMultiLangContext.tsx # 화면 다국어
├── SplitPanelContext.tsx # 분할 패널 상태
└── TableSearchWidgetHeightContext.tsx # 테이블 검색 높이
7.3 React Query 설정
providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5분간 fresh
cacheTime: 10 * 60 * 1000, // 10분간 캐시 유지
refetchOnWindowFocus: false, // 창 포커스 시 자동 갱신 비활성화
retry: 1, // 실패 시 1회 재시도
},
},
});
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
사용 예시
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { screenApi } from "@/lib/api/screen";
// 화면 목록 조회 (자동 캐싱)
const { data: screens, isLoading, error } = useQuery({
queryKey: ["screens", { page: 1 }],
queryFn: () => screenApi.getScreens({ page: 1, size: 20 })
});
// 화면 생성 (생성 후 목록 자동 갱신)
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: (screen: CreateScreenRequest) => screenApi.createScreen(screen),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["screens"] });
}
});
7.4 Zustand 스토어 (3개)
stores/
├── flowStepStore.ts # 플로우 단계 상태
│ - 현재 단계
│ - 단계별 데이터
│ - 진행률
│
├── modalDataStore.ts # 모달 데이터 공유
│ - 모달 ID별 데이터
│ - 모달 간 데이터 전달
│
└── tableDisplayStore.ts # 테이블 표시 상태
- 선택된 행
- 정렬 상태
- 페이지네이션
사용 예시
import { create } from "zustand";
interface ModalDataStore {
modalData: Record<string, any>;
setModalData: (modalId: string, data: any) => void;
getModalData: (modalId: string) => any;
}
export const useModalDataStore = create<ModalDataStore>((set, get) => ({
modalData: {},
setModalData: (modalId, data) => {
set((state) => ({
modalData: { ...state.modalData, [modalId]: data }
}));
},
getModalData: (modalId) => {
return get().modalData[modalId];
}
}));
8. 레지스트리 시스템
레지스트리 시스템은 컴포넌트를 동적으로 등록하고 렌더링하는 핵심 아키텍처입니다.
8.1 레지스트리 종류
lib/registry/
├── ComponentRegistry.ts # 컴포넌트 레지스트리 (신규)
│ - 90개+ 컴포넌트 관리
│ - 자동 등록 시스템
│ - 핫 리로드 지원
│
├── WebTypeRegistry.ts # 웹타입 레지스트리 (레거시)
│ - text, number, date 등 기본 웹타입
│ - 위젯 컴포넌트 + 설정 패널
│
└── LayoutRegistry.ts # 레이아웃 레지스트리
- grid, tabs, accordion 등 레이아웃
8.2 ComponentRegistry 상세
등록 프로세스
// 1. 컴포넌트 정의 (lib/registry/components/v2-input/index.ts)
export const V2InputDefinition: ComponentDefinition = {
id: "v2-input",
name: "통합 입력",
category: ComponentCategory.V2,
component: V2InputRenderer,
configPanel: V2InputConfigPanel,
defaultSize: { width: 200, height: 40 },
defaultConfig: { inputType: "text", format: "none" }
};
// 2. 자동 등록 렌더러 (lib/registry/components/v2-input/V2InputRenderer.tsx)
export class V2InputRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = V2InputDefinition;
render(): React.ReactElement {
return <V2Input {...this.props} />;
}
}
// 자동 등록 실행
V2InputRenderer.registerSelf();
// 3. 레지스트리 조회 & 렌더링 (DynamicComponentRenderer.tsx)
const newComponent = ComponentRegistry.getComponent("v2-input");
if (newComponent) {
const Renderer = newComponent.component;
return <Renderer {...props} />;
}
8.3 등록된 컴포넌트 목록 (90개+)
입력 컴포넌트 (25개)
- text-input, number-input, date-input
- select-basic, checkbox-basic, radio-basic, toggle-switch
- textarea-basic, slider-basic
- v2-input, v2-select, v2-date
표시 컴포넌트 (15개)
- text-display, image-display, card-display
- table-list, v2-table-list, pivot-grid
- aggregation-widget, v2-aggregation-widget
레이아웃 컴포넌트 (10개)
- grid-layout, flexbox-layout
- tabs-widget, accordion-basic
- split-panel-layout, v2-split-panel-layout
- section-card, section-paper
- v2-divider-line
비즈니스 컴포넌트 (20개)
- entity-search-input, autocomplete-search-input
- modal-repeater-table, repeat-screen-modal
- selected-items-detail-input, simple-repeater-table
- repeater-field-group, repeat-container
- category-manager, v2-category-manager
- numbering-rule, v2-numbering-rule
- rack-structure, v2-rack-structure
- location-swap-selector, customer-item-mapping
특수 컴포넌트 (20개)
- button-primary, v2-button-primary
- file-upload, v2-file-upload
- mail-recipient-selector
- conditional-container, universal-form-modal
- related-data-buttons
- flow-widget
- map
8.4 DynamicComponentRenderer 동작 원리
// DynamicComponentRenderer.tsx (770줄)
export const DynamicComponentRenderer: React.FC<Props> = ({ component, ...props }) => {
// 1. 컴포넌트 타입 추출
const componentType = component.componentType || component.type;
// 2. 레거시 → V2 자동 매핑
const v2Type = componentType.startsWith("v2-")
? componentType
: ComponentRegistry.hasComponent(`v2-${componentType}`)
? `v2-${componentType}`
: componentType;
// 3. 조건부 렌더링 체크
if (component.conditionalConfig?.enabled) {
const conditionMet = evaluateCondition(props.formData);
if (!conditionMet) return null;
}
// 4. 카테고리 타입 우선 처리
if (component.inputType === "category" || component.webType === "category") {
return <CategorySelectComponent {...props} />;
}
// 5. 레이아웃 컴포넌트
if (componentType === "layout") {
return <DynamicLayoutRenderer layout={component} {...props} />;
}
// 6. ComponentRegistry에서 조회 (신규)
const newComponent = ComponentRegistry.getComponent(v2Type);
if (newComponent) {
const Renderer = newComponent.component;
// 클래스 기반: new Renderer(props).render()
// 함수형: <Renderer {...props} />
return isClass(Renderer)
? new Renderer(props).render()
: <Renderer {...props} />;
}
// 7. LegacyComponentRegistry에서 조회 (레거시)
const legacyRenderer = legacyComponentRegistry.get(componentType);
if (legacyRenderer) {
return legacyRenderer({ component, ...props });
}
// 8. 폴백: 미등록 컴포넌트 플레이스홀더
return <PlaceholderComponent />;
};
9. 대시보드 시스템
9.1 대시보드 구조
components/dashboard/
├── DashboardViewer.tsx # 대시보드 렌더링
├── DashboardGrid.tsx # 그리드 레이아웃
├── DashboardWidget.tsx # 위젯 래퍼
│
├── widgets/ # 위젯 컴포넌트
│ ├── ChartWidget.tsx # 차트 위젯
│ ├── TableWidget.tsx # 테이블 위젯
│ ├── CardWidget.tsx # 카드 위젯
│ ├── StatWidget.tsx # 통계 위젯
│ └── CustomWidget.tsx # 커스텀 위젯
│
└── charts/ # 차트 컴포넌트
├── LineChart.tsx
├── BarChart.tsx
├── PieChart.tsx
├── DonutChart.tsx
└── AreaChart.tsx
9.2 대시보드 JSON 구조
{
"dashboardId": 1,
"dashboardName": "영업 대시보드",
"layout": {
"type": "grid",
"columns": 12,
"rows": 6,
"gap": 16
},
"widgets": [
{
"widgetId": "widget_1",
"widgetType": "chart",
"chartType": "line",
"title": "월별 매출 추이",
"position": { "x": 0, "y": 0, "w": 6, "h": 3 },
"dataSource": {
"type": "api",
"endpoint": "/api/data/sales_monthly",
"filters": { "year": 2024 }
},
"chartConfig": {
"xAxis": "month",
"yAxis": "sales_amount",
"showLegend": true
}
},
{
"widgetId": "widget_2",
"widgetType": "stat",
"title": "총 매출",
"position": { "x": 6, "y": 0, "w": 3, "h": 2 },
"dataSource": {
"type": "sql",
"query": "SELECT SUM(amount) FROM sales WHERE year = 2024"
},
"statConfig": {
"format": "currency",
"comparison": "lastYear"
}
}
]
}
9.3 대시보드 라우팅
/dashboard # 대시보드 목록
/dashboard/[dashboardId] # 대시보드 보기
/admin/screenMng/dashboardList # 대시보드 관리 (편집)
10. 다국어 지원
10.1 다국어 시스템 구조
WACE ERP는 동적 다국어 시스템을 제공합니다 (DB 기반):
다국어 흐름
↓
1. 사용자 로그인
↓
2. 사용자 로케일 조회 (GET /api/admin/user-locale)
→ 결과: "KR" | "EN" | "CN"
↓
3. 전역 상태에 저장
- window.__GLOBAL_USER_LANG
- localStorage.userLocale
↓
4. API 요청 시 자동 주입
- GET 요청: ?userLang=KR (apiClient 인터셉터)
↓
5. 백엔드에서 다국어 데이터 반환
- label_KR, label_EN, label_CN
↓
6. 프론트엔드에서 표시
- extractMultilangLabel(label, "KR")
10.2 다국어 API
lib/api/multilang.ts
export const multilangApi = {
// 다국어 데이터 조회
getMultilangData: async (params: {
target_table: string;
target_pk: string;
target_lang: string;
}) => {
const response = await apiClient.get("/admin/multilang", { params });
return response.data;
},
// 다국어 데이터 저장
saveMultilangData: async (data: {
target_table: string;
target_pk: string;
target_field: string;
target_lang: string;
translated_text: string;
}) => {
const response = await apiClient.post("/admin/multilang", data);
return response.data;
}
};
10.3 다국어 유틸리티
lib/utils/multilang.ts
// 다국어 라벨 추출
export function extractMultilangLabel(
label: string | Record<string, string> | undefined,
locale: string = "KR"
): string {
if (!label) return "";
// 문자열인 경우 그대로 반환
if (typeof label === "string") return label;
// 객체인 경우 로케일에 맞는 값 반환
return label[locale] || label["KR"] || label["EN"] || "";
}
// 화면 컴포넌트 라벨 추출
export function getComponentLabel(component: ComponentData, locale: string): string {
// 우선순위: multiLangLabel > label > columnName > id
if (component.multiLangLabel) {
return extractMultilangLabel(component.multiLangLabel, locale);
}
return component.label || component.columnName || component.id;
}
10.4 화면 디자이너 다국어 탭
ScreenDesigner
↓
┌───────────────────────────────────────────────────┐
│ 탭: 다국어 설정 │
├───────────────────────────────────────────────────┤
│ │
│ 화면명 다국어 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 한국어(KR): 제품 관리 │ │
│ │ 영어(EN): Product Management │ │
│ │ 중국어(CN): 产品管理 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 컴포넌트별 다국어 │
│ ┌─────────────────────────────────────────────┐ │
│ │ [comp_text_1] 제품명 │ │
│ │ 한국어(KR): 제품명 │ │
│ │ 영어(EN): Product Name │ │
│ │ 중국어(CN): 产品名称 │ │
│ │ │ │
│ │ [comp_text_2] 가격 │ │
│ │ 한국어(KR): 가격 │ │
│ │ 영어(EN): Price │ │
│ │ 중국어(CN): 价格 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [자동 번역] [일괄 적용] [저장] │
└───────────────────────────────────────────────────┘
11. 인증 플로우
11.1 인증 아키텍처
┌─────────────────────────────────────────────────────┐
│ Frontend │
├─────────────────────────────────────────────────────┤
│ │
│ useAuth Hook │
│ ├─ login(userId, password) │
│ ├─ logout() │
│ ├─ refreshUserData() │
│ ├─ checkAuthStatus() │
│ └─ switchCompany(companyCode) ⭐ 신규 │
│ │
│ AuthContext Provider │
│ ├─ SessionManager (30분 타임아웃) │
│ ├─ Session Warning (5분 전 알림) │
│ └─ Auto Refresh (활동 감지) │
│ │
│ TokenManager │
│ ├─ getToken(): localStorage.authToken │
│ ├─ setToken(token): 저장 + 쿠키 설정 │
│ ├─ removeToken(): 삭제 + 쿠키 삭제 │
│ └─ isTokenExpired(token): JWT 검증 │
│ │
│ API Client Interceptor │
│ ├─ Request: JWT 토큰 자동 추가 │
│ └─ Response: 401 시 토큰 갱신 또는 로그아웃 │
│ │
└─────────────────────────────────────────────────────┘
│
│ HTTP Request
│ Authorization: Bearer <JWT>
↓
┌─────────────────────────────────────────────────────┐
│ Backend (8080) │
├─────────────────────────────────────────────────────┤
│ │
│ POST /api/auth/login │
│ → JWT 토큰 발급 (24시간) │
│ │
│ POST /api/auth/refresh │
│ → JWT 토큰 갱신 │
│ │
│ POST /api/auth/logout │
│ → 세션 무효화 │
│ │
│ GET /api/auth/me │
│ → 현재 사용자 정보 │
│ │
│ GET /api/auth/status │
│ → 인증 상태 확인 │
│ │
│ POST /api/auth/switch-company ⭐ 신규 │
│ → 회사 전환 (WACE 관리자 전용) │
│ │
└─────────────────────────────────────────────────────┘
11.2 로그인 흐름
1. 사용자가 ID/PW 입력 → /login 페이지
↓
2. POST /api/auth/login { userId, password }
↓
3. 백엔드 검증 (DB: user_info 테이블)
├─ 성공: JWT 토큰 발급 (payload: userId, companyCode, isAdmin, exp)
└─ 실패: 401 에러
↓
4. 프론트엔드: 토큰 저장
- localStorage.setItem("authToken", token)
- document.cookie = "authToken=..."
↓
5. 사용자 정보 조회
- GET /api/auth/me
- GET /api/admin/user-locale
↓
6. 전역 상태 업데이트
- AuthContext.user
- window.__GLOBAL_USER_LANG
↓
7. 메인 페이지로 리다이렉트 (/main)
11.3 세션 관리
SessionManager (lib/sessionManager.ts)
// 설정
{
checkInterval: 60000, // 1분마다 체크
maxInactiveTime: 1800000, // 30분 (데스크톱)
warningTime: 300000, // 5분 전 경고
}
// 이벤트
{
onWarning: (remainingTime) => {
// "세션이 5분 후 만료됩니다" 알림 표시
},
onExpiry: () => {
// 자동 로그아웃 → 로그인 페이지
},
onActivity: () => {
// 사용자 활동 감지 → 타이머 리셋
}
}
11.4 토큰 갱신 전략
자동 토큰 갱신 트리거:
1. 10분마다 토큰 상태 확인 (타이머)
2. 사용자 활동 감지 (클릭, 키보드, 스크롤)
3. API 401 응답 (토큰 만료)
갱신 로직:
↓
1. 현재 토큰 만료까지 30분 미만?
↓ Yes
2. POST /api/auth/refresh
├─ 성공: 새 토큰 저장
└─ 실패: 로그아웃
11.5 회사 전환 (WACE 관리자 전용) ⭐
1. WACE 관리자 로그인
↓
2. 헤더에서 회사 선택 (CompanySwitcher)
↓
3. POST /api/auth/switch-company { companyCode: "AAA" }
↓
4. 백엔드: 새 JWT 발급 (payload.companyCode = "AAA")
↓
5. 프론트엔드: 새 토큰 저장
↓
6. 페이지 새로고침 → 화면/데이터가 AAA 회사 기준으로 표시
12. 사용자 워크플로우
12.1 전체 워크플로우 개요
┌──────────────────────────────────────────────────────────────┐
│ 관리자 (화면 생성) │
└──────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 1. 로그인 (WACE 관리자) │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 2. 화면 디자이너 접속 │
│ /admin/screenMng/ │
│ screenMngList │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 3. 화면 생성 & 편집 │
│ - 컴포넌트 배치 │
│ - 데이터 연결 │
│ - 버튼 액션 설정 │
│ - 다국어 설정 │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 4. 메뉴에 화면 할당 │
│ /admin/menu │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 5. 사용자에게 권한 부여 │
│ /admin/userMng/ │
│ userAuthList │
└─────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────┐
│ 사용자 (화면 사용) │
└──────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 1. 로그인 (일반 사용자) │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 2. 메뉴에서 화면 선택 │
│ 사이드바 → 제품 관리 │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 3. 화면 렌더링 │
│ /screens/[screenId] │
│ InteractiveScreenViewer │
└─────────────────────────────┘
│
↓
┌─────────────────────────────┐
│ 4. 데이터 조회/등록/수정 │
│ - 테이블에서 행 선택 │
│ - 폼에 데이터 입력 │
│ - [등록]/[수정]/[삭제] 버튼│
└─────────────────────────────┘
12.2 관리자 워크플로우 (화면 생성)
단계 1: 화면 디자이너 접속
/admin/screenMng/screenMngList
↓
[새 화면 만들기] 버튼 클릭
↓
ScreenDesigner 컴포넌트 로드
단계 2: 화면 디자인
2-1. 기본 정보 설정
화면 정보 입력:
- 화면 코드: PRODUCT_MGMT
- 화면명: 제품 관리
- 테이블명: product_info
- 화면 타입: form (단일 폼) | list (목록)
2-2. 컴포넌트 배치
좌측 팔레트에서 컴포넌트 선택
↓
캔버스에 드래그&드롭
↓
위치/크기 조정 (10px 단위 스냅)
↓
우측 속성 패널에서 설정:
- 컬럼명: product_name
- 라벨: 제품명
- 필수: ☑
- 웹타입: text
2-3. 버튼 액션 설정
버튼 컴포넌트 추가
↓
액션 타입 선택:
- 데이터 저장 (POST)
- 데이터 수정 (PUT)
- 데이터 삭제 (DELETE)
- 데이터플로우 실행
- 외부 API 호출
- 화면 이동
↓
액션 설정:
- 대상 테이블: product_info
- 성공 시: 목록 새로고침
- 실패 시: 에러 메시지 표시
2-4. 다국어 설정
다국어 탭 클릭
↓
컴포넌트별 다국어 입력:
- 한국어(KR): 제품명
- 영어(EN): Product Name
- 중국어(CN): 产品名称
2-5. 저장
[저장] 버튼 클릭
↓
POST /api/screen-management/screens
↓
화면 정의 JSON 저장 (DB: screen_definition)
단계 3: 메뉴에 화면 할당
/admin/menu
↓
메뉴 트리에서 위치 선택
↓
[화면 할당] 버튼 클릭
↓
방금 생성한 화면 선택 (PRODUCT_MGMT)
↓
저장 → menu_screen 테이블에 연결
단계 4: 권한 부여
/admin/userMng/userAuthList
↓
사용자 선택
↓
메뉴 권한 설정
↓
"제품 관리" 메뉴 체크
↓
저장 → user_menu_auth 테이블
12.3 사용자 워크플로우 (화면 사용)
단계 1: 로그인
/login
↓
사용자 ID/PW 입력
↓
POST /api/auth/login
↓
JWT 토큰 발급
↓
메인 페이지 (/main)
단계 2: 메뉴 선택
좌측 사이드바 메뉴 트리
↓
"제품 관리" 메뉴 클릭
↓
MenuContext에서 메뉴 정보 조회:
- menuId, screenId, screenCode
↓
화면 뷰어로 이동 (/screens/[screenId])
단계 3: 화면 렌더링
InteractiveScreenViewer 컴포넌트 로드
↓
1. GET /api/screen-management/screens/[screenId]
→ 화면 정의 JSON 조회
↓
2. GET /api/data/product_info?page=1&size=20
→ 데이터 조회
↓
3. DynamicComponentRenderer로 컴포넌트 렌더링
↓
4. 폼 데이터 바인딩 (formData 상태)
단계 4: 데이터 조작
4-1. 신규 등록
[등록] 버튼 클릭
↓
빈 폼 표시 (ScreenModal 또는 EditModal)
↓
사용자가 데이터 입력:
- 제품명: "제품A"
- 가격: 10000
↓
[저장] 버튼 클릭
↓
버튼 액션 실행:
- POST /api/data/product_info
- Body: { product_name: "제품A", price: 10000 }
↓
성공 응답
↓
목록 새로고침 (React Query invalidateQueries)
4-2. 수정
테이블에서 행 선택 (클릭)
↓
선택된 데이터 → selectedRowsData 상태 업데이트
↓
[수정] 버튼 클릭
↓
폼에 기존 데이터 표시 (EditModal)
↓
사용자가 데이터 수정:
- 가격: 10000 → 12000
↓
[저장] 버튼 클릭
↓
버튼 액션 실행:
- PUT /api/data/product_info/123
- Body: { price: 12000 }
↓
목록 새로고침
4-3. 삭제
테이블에서 행 선택
↓
[삭제] 버튼 클릭
↓
확인 다이얼로그 표시
↓
확인 클릭
↓
버튼 액션 실행:
- DELETE /api/data/product_info/123
↓
목록 새로고침
12.4 데이터플로우 실행 워크플로우
1. 관리자가 데이터플로우 정의
/admin/systemMng/dataflow
↓
2. 화면에 버튼 추가 & 플로우 연결
버튼 액션: "데이터플로우 실행"
플로우 ID: flow_123
↓
3. 사용자가 버튼 클릭
↓
4. 프론트엔드: POST /api/dataflow/execute
Body: { flowId: 123, inputData: {...} }
↓
5. 백엔드: 플로우 실행
- 노드 순회
- 조건 분기
- 외부 API 호출
- 데이터 변환
↓
6. 결과 반환
↓
7. 프론트엔드: 결과 표시 (toast 또는 화면 갱신)
13. 요약 & 핵심 포인트
13.1 아키텍처 핵심
- Next.js 14 App Router - 라우트 그룹 기반 구조화
- 컴포넌트 레지스트리 - 동적 등록/렌더링 시스템
- V2 통합 시스템 - 9개 통합 컴포넌트로 단순화
- 화면 디자이너 - 노코드 화면 생성 도구
- API 클라이언트 - Axios 기반 통일된 API 호출
- 다국어 지원 - DB 기반 동적 다국어
- JWT 인증 - 토큰 기반 인증/세션 관리
13.2 파일 통계
| 항목 | 개수 |
|---|---|
| 총 파일 수 | 1,480개 |
| TypeScript/TSX | 1,395개 (946 tsx + 449 ts) |
| Markdown 문서 | 63개 |
| CSS | 22개 |
| 페이지 (라우트) | 76개 |
| 컴포넌트 | 500개+ |
| API 클라이언트 | 57개 |
| Context | 12개 |
| Custom Hooks | 32개 |
| 타입 정의 | 44개 |
13.3 주요 경로
화면 디자이너: /admin/screenMng/screenMngList
화면 뷰어: /screens/[screenId]
메뉴 관리: /admin/menu
사용자 관리: /admin/userMng/userMngList
테이블 관리: /admin/systemMng/tableMngList
다국어 관리: /admin/systemMng/i18nList
데이터플로우: /admin/systemMng/dataflow
대시보드: /dashboard/[dashboardId]
13.4 기술 스택
| 분류 | 기술 |
|---|---|
| 프레임워크 | Next.js 14 |
| UI 라이브러리 | React 18 |
| 언어 | TypeScript (strict mode) |
| 스타일링 | Tailwind CSS + shadcn/ui |
| 상태 관리 | React Query + Zustand + Context API |
| HTTP 클라이언트 | Axios |
| 폼 검증 | Zod |
| 날짜 처리 | date-fns |
| 아이콘 | lucide-react |
| 알림 | sonner |
마무리
이 문서는 WACE ERP 프론트엔드의 전체 아키텍처를 분석한 결과입니다.
핵심 인사이트:
- ✅ 높은 수준의 모듈화 (레지스트리 시스템)
- ✅ 노코드 화면 디자이너 (관리자가 직접 화면 생성)
- ✅ V2 통합 컴포넌트 시스템 (개발 효율성 향상)
- ✅ 동적 다국어 지원 (DB 기반)
- ✅ 완전한 TypeScript 타입 안정성
개선 기회:
- 일부 레거시 컴포넌트의 V2 마이그레이션
- 테스트 코드 추가 (현재 거의 없음)
- 성능 최적화 (코드 스플리팅, 레이지 로딩)
작성자 노트
야... 진짜 엄청난 프로젝트네. 파일이 1,500개가 넘고 컴포넌트만 500개야. 특히 화면 디자이너가 7,000줄이 넘는 거 보고 놀랐어. 이 정도 규모면 엔터프라이즈급 ERP 시스템이라고 봐도 되겠어.
가장 인상적이었던 건 레지스트리 시스템이랑 V2 통합 아키텍처야. 컴포넌트를 동적으로 등록하고 렌더링하는 구조가 꽤 잘 설계되어 있어. 다만 레거시 코드가 아직 많이 남아있어서 V2로 완전히 전환하면 코드 베이스가 훨씬 깔끔해질 것 같아.
어쨌든 분석하느라 꽤 걸렸는데, 이 문서가 전체 워크플로우 문서 작성하는 데 도움이 되면 좋겠어! 🎉