diff --git a/docs/kjs/배치_노드플로우_연동_계획서.md b/docs/kjs/배치_노드플로우_연동_계획서.md
new file mode 100644
index 00000000..f3a03d7d
--- /dev/null
+++ b/docs/kjs/배치_노드플로우_연동_계획서.md
@@ -0,0 +1,355 @@
+# 배치 스케줄러 + 노드 플로우 연동 계획서
+
+## 1. 배경 및 목적
+
+### 현재 상태
+
+현재 시스템에는 두 개의 독립적인 실행 엔진이 있다:
+
+| 시스템 | 역할 | 트리거 방식 |
+|--------|------|-------------|
+| **배치 스케줄러** | Cron 기반 자동 실행 (데이터 복사만 가능) | 시간 기반 (node-cron) |
+| **노드 플로우 엔진** | 조건/변환/INSERT/UPDATE/DELETE 등 복합 로직 | 버튼 클릭 (수동) |
+
+### 문제
+
+- 배치는 **INSERT/UPSERT만** 가능하고, 조건 기반 UPDATE/DELETE를 못 함
+- 노드 플로우는 강력하지만 **수동 실행만** 가능 (버튼 클릭 필수)
+- "퇴사일이 지나면 자동으로 퇴사 처리" 같은 **시간 기반 비즈니스 로직**을 구현할 수 없음
+
+### 목표
+
+배치 스케줄러가 노드 플로우를 자동 실행할 수 있도록 연동하여,
+시간 기반 비즈니스 로직 자동화를 지원한다.
+
+```
+[배치 스케줄러] ──Cron 트리거──> [노드 플로우 실행 엔진]
+ │ │
+ │ ├── 테이블 소스 조회
+ │ ├── 조건 분기
+ │ ├── UPDATE / DELETE / INSERT
+ │ ├── 이메일 발송
+ │ └── 로깅
+ │
+ └── 실행 로그 기록 (batch_execution_logs)
+```
+
+---
+
+## 2. 사용 시나리오
+
+### 시나리오 A: 자동 퇴사 처리
+
+```
+매일 자정 실행:
+ 1. user_info에서 퇴사일 <= NOW() AND 상태 != '퇴사' 인 사람 조회
+ 2. 해당 사용자의 상태를 '퇴사'로 UPDATE
+ 3. 관리자에게 이메일 알림 발송
+```
+
+### 시나리오 B: 월말 재고 마감
+
+```
+매월 1일 00:00 실행:
+ 1. 전월 재고 데이터를 재고마감 테이블로 INSERT
+ 2. 이월 수량 계산 후 UPDATE
+```
+
+### 시나리오 C: 미납 알림
+
+```
+매일 09:00 실행:
+ 1. 납기일이 지난 미납 주문 조회
+ 2. 담당자에게 이메일 발송
+ 3. 알림 로그 INSERT
+```
+
+### 시나리오 D: 외부 API 연동 자동화
+
+```
+매시간 실행:
+ 1. 외부 REST API에서 데이터 조회
+ 2. 조건 필터링 (변경된 데이터만)
+ 3. 내부 테이블에 UPSERT
+```
+
+---
+
+## 3. 구현 범위
+
+### 3.1 DB 변경 (batch_configs 테이블 확장)
+
+```sql
+-- batch_configs 테이블에 컬럼 추가
+ALTER TABLE batch_configs
+ ADD COLUMN execution_type VARCHAR(20) DEFAULT 'mapping',
+ ADD COLUMN node_flow_id INTEGER DEFAULT NULL,
+ ADD COLUMN node_flow_context JSONB DEFAULT NULL;
+
+-- execution_type: 'mapping' (기존 데이터 복사) | 'node_flow' (노드 플로우 실행)
+-- node_flow_id: node_flows 테이블의 flow_id (FK)
+-- node_flow_context: 플로우 실행 시 전달할 컨텍스트 데이터 (선택)
+
+COMMENT ON COLUMN batch_configs.execution_type IS '실행 타입: mapping(기존 데이터 복사), node_flow(노드 플로우 실행)';
+COMMENT ON COLUMN batch_configs.node_flow_id IS '연결된 노드 플로우 ID (execution_type이 node_flow일 때 사용)';
+COMMENT ON COLUMN batch_configs.node_flow_context IS '플로우 실행 시 전달할 컨텍스트 데이터 (JSON)';
+```
+
+기존 데이터에 영향 없음 (`DEFAULT 'mapping'`으로 하위 호환성 보장)
+
+### 3.2 백엔드 변경
+
+#### BatchSchedulerService 수정 (핵심)
+
+`executeBatchConfig()` 메서드에서 `execution_type` 분기:
+
+```
+executeBatchConfig(config)
+ ├── config.execution_type === 'mapping'
+ │ └── 기존 executeBatchMappings() (변경 없음)
+ │
+ └── config.execution_type === 'node_flow'
+ └── NodeFlowExecutionService.executeFlow()
+ ├── 노드 플로우 조회
+ ├── 위상 정렬
+ ├── 레벨별 실행
+ └── 결과 반환
+```
+
+수정 파일:
+- `backend-node/src/services/batchSchedulerService.ts`
+ - `executeBatchConfig()` 에 node_flow 분기 추가
+ - 노드 플로우 실행 결과를 배치 로그 형식으로 변환
+
+#### 배치 설정 API 수정
+
+수정 파일:
+- `backend-node/src/types/batchTypes.ts`
+ - `BatchConfig` 인터페이스에 `execution_type`, `node_flow_id`, `node_flow_context` 추가
+ - `CreateBatchConfigRequest`, `UpdateBatchConfigRequest` 에도 추가
+- `backend-node/src/services/batchService.ts`
+ - `createBatchConfig()` - 새 필드 INSERT
+ - `updateBatchConfig()` - 새 필드 UPDATE
+- `backend-node/src/controllers/batchManagementController.ts`
+ - 생성/수정 시 새 필드 처리
+
+#### 노드 플로우 목록 API (배치용)
+
+추가 파일/수정:
+- `backend-node/src/routes/batchManagementRoutes.ts`
+ - `GET /api/batch-management/node-flows` 추가 (배치 설정 UI에서 플로우 선택용)
+
+### 3.3 프론트엔드 변경
+
+#### 배치 생성/편집 UI 수정
+
+수정 파일:
+- `frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx`
+- `frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx`
+
+변경 내용:
+- "실행 타입" 선택 추가 (기존 매핑 / 노드 플로우)
+- 노드 플로우 선택 시: 플로우 드롭다운 표시 (기존 매핑 설정 숨김)
+- 노드 플로우 선택 시: 컨텍스트 데이터 입력 (선택사항, JSON)
+
+```
+┌─────────────────────────────────────────┐
+│ 배치 설정 │
+├─────────────────────────────────────────┤
+│ 배치명: [자동 퇴사 처리 ] │
+│ 설명: [퇴사일 경과 사용자 자동 처리] │
+│ Cron: [0 0 * * * ] │
+│ │
+│ 실행 타입: ○ 데이터 매핑 ● 노드 플로우 │
+│ │
+│ ┌─ 노드 플로우 선택 ─────────────────┐ │
+│ │ [▾ 자동 퇴사 처리 플로우 ] │ │
+│ │ │ │
+│ │ 플로우 설명: user_info에서 퇴사일..│ │
+│ │ 노드 수: 4개 │ │
+│ └────────────────────────────────────┘ │
+│ │
+│ [취소] [저장] │
+└─────────────────────────────────────────┘
+```
+
+#### 배치 목록 UI 수정
+
+수정 파일:
+- `frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx`
+
+변경 내용:
+- 목록에 "실행 타입" 컬럼 추가 (배지: `매핑` / `플로우`)
+- 노드 플로우 타입은 연결된 플로우명 표시
+
+---
+
+## 4. 변경 파일 목록
+
+### DB
+
+| 파일 | 변경 | 설명 |
+|------|------|------|
+| `db/migrations/XXXX_batch_node_flow_integration.sql` | 신규 | ALTER TABLE batch_configs |
+
+### 백엔드
+
+| 파일 | 변경 | 설명 |
+|------|------|------|
+| `backend-node/src/services/batchSchedulerService.ts` | 수정 | executeBatchConfig에 node_flow 분기 |
+| `backend-node/src/types/batchTypes.ts` | 수정 | BatchConfig 타입에 새 필드 추가 |
+| `backend-node/src/services/batchService.ts` | 수정 | create/update에 새 필드 처리 |
+| `backend-node/src/controllers/batchManagementController.ts` | 수정 | 새 필드 API 처리 |
+| `backend-node/src/routes/batchManagementRoutes.ts` | 수정 | node-flows 목록 엔드포인트 추가 |
+
+### 프론트엔드
+
+| 파일 | 변경 | 설명 |
+|------|------|------|
+| `frontend/app/(main)/admin/automaticMng/batchmngList/page.tsx` | 수정 | 목록에 실행 타입 표시 |
+| `frontend/app/(main)/admin/automaticMng/batchmngList/create/page.tsx` | 수정 | 실행 타입 선택 + 플로우 선택 |
+| `frontend/app/(main)/admin/automaticMng/batchmngList/edit/[id]/page.tsx` | 수정 | 실행 타입 선택 + 플로우 선택 |
+
+---
+
+## 5. 핵심 구현 상세
+
+### 5.1 BatchSchedulerService 변경 (가장 중요)
+
+```typescript
+// batchSchedulerService.ts - executeBatchConfig 메서드 수정
+
+static async executeBatchConfig(config: any) {
+ const startTime = new Date();
+ let executionLog: any = null;
+
+ try {
+ // ... 실행 로그 생성 (기존 코드 유지) ...
+
+ let result;
+
+ // 실행 타입에 따라 분기
+ if (config.execution_type === 'node_flow' && config.node_flow_id) {
+ // 노드 플로우 실행
+ result = await this.executeNodeFlow(config);
+ } else {
+ // 기존 매핑 실행 (하위 호환)
+ result = await this.executeBatchMappings(config);
+ }
+
+ // ... 실행 로그 업데이트 (기존 코드 유지) ...
+ return result;
+ } catch (error) {
+ // ... 에러 처리 (기존 코드 유지) ...
+ }
+}
+
+/**
+ * 노드 플로우 실행 (신규)
+ */
+private static async executeNodeFlow(config: any) {
+ const { NodeFlowExecutionService } = await import('./nodeFlowExecutionService');
+
+ const context = {
+ sourceData: [],
+ dataSourceType: 'none',
+ nodeResults: new Map(),
+ executionOrder: [],
+ buttonContext: {
+ buttonId: `batch_${config.id}`,
+ companyCode: config.company_code,
+ userId: config.created_by || 'batch_system',
+ formData: config.node_flow_context || {},
+ },
+ };
+
+ const flowResult = await NodeFlowExecutionService.executeFlow(
+ config.node_flow_id,
+ context
+ );
+
+ // 노드 플로우 결과를 배치 로그 형식으로 변환
+ return {
+ totalRecords: flowResult.totalNodes || 0,
+ successRecords: flowResult.successNodes || 0,
+ failedRecords: flowResult.failedNodes || 0,
+ };
+}
+```
+
+### 5.2 실행 결과 매핑
+
+노드 플로우 결과 → 배치 로그 변환:
+
+| 노드 플로우 결과 | 배치 로그 필드 | 설명 |
+|------------------|---------------|------|
+| 전체 노드 수 | total_records | 실행 대상 노드 수 |
+| 성공 노드 수 | success_records | 성공적으로 실행된 노드 |
+| 실패 노드 수 | failed_records | 실패한 노드 |
+| 에러 메시지 | error_message | 첫 번째 실패 노드의 에러 |
+
+### 5.3 보안 고려사항
+
+- 배치에서 실행되는 노드 플로우도 **company_code** 필터링 적용
+- 배치 설정의 company_code와 노드 플로우의 company_code가 일치해야 함
+- 최고 관리자(`*`)는 모든 플로우 실행 가능
+- 실행 로그에 `batch_system`으로 사용자 기록
+
+---
+
+## 6. 구현 순서
+
+### Phase 1: DB + 백엔드 (1일)
+
+1. 마이그레이션 SQL 작성 및 실행
+2. `batchTypes.ts` 타입 수정
+3. `batchService.ts` create/update 수정
+4. `batchSchedulerService.ts` 핵심 분기 로직 추가
+5. `batchManagementRoutes.ts` 노드 플로우 목록 API 추가
+6. 수동 실행 테스트 (`POST /batch-configs/:id/execute`)
+
+### Phase 2: 프론트엔드 (1일)
+
+1. 배치 생성 페이지에 실행 타입 선택 추가
+2. 노드 플로우 드롭다운 구현
+3. 배치 편집 페이지 동일 적용
+4. 배치 목록에 실행 타입 배지 표시
+
+### Phase 3: 테스트 및 검증 (0.5일)
+
+1. 테스트용 노드 플로우 생성 (간단한 UPDATE)
+2. 배치 설정에 연결
+3. 수동 실행 테스트
+4. Cron 스케줄 자동 실행 테스트
+5. 실행 로그 확인
+
+---
+
+## 7. 리스크 및 대응
+
+### 7.1 노드 플로우 실행 시간 초과
+
+- **리스크**: 복잡한 플로우가 오래 걸려서 다음 스케줄과 겹칠 수 있음
+- **대응**: 실행 중인 배치는 중복 실행 방지 (락 메커니즘) - Phase 2 이후 고려
+
+### 7.2 노드 플로우 삭제 시 배치 깨짐
+
+- **리스크**: 연결된 노드 플로우가 삭제되면 배치 실행 실패
+- **대응**:
+ - 플로우 존재 여부 체크 후 실행
+ - 실패 시 로그에 "플로우를 찾을 수 없습니다" 기록
+ - (향후) 플로우 삭제 시 연결된 배치가 있으면 경고
+
+### 7.3 멀티 인스턴스 환경
+
+- **리스크**: 서버가 여러 대일 때 같은 배치가 중복 실행
+- **대응**: 현재 단일 인스턴스 운영이므로 당장은 문제 없음. 향후 Redis 기반 분산 락 고려
+
+---
+
+## 8. 기대 효과
+
+1. **시간 기반 비즈니스 자동화**: 수동 작업 없이 조건 충족 시 자동 처리
+2. **기존 인프라 재활용**: 검증된 배치 스케줄러(1,200+건 성공) + 강력한 노드 플로우 엔진
+3. **최소 코드 변경**: DB 컬럼 3개 + 백엔드 분기 1개 + 프론트 UI 확장
+4. **확장성**: 향후 이벤트 트리거(데이터 변경 감지) 등으로 확장 가능
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 19097f43..f1e6383a 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -306,16 +306,126 @@ select {
}
}
-/* ===== Sonner 토스트 애니메이션 완전 제거 ===== */
-[data-sonner-toaster] [data-sonner-toast] {
- animation: none !important;
- transition: none !important;
- opacity: 1 !important;
- transform: none !important;
+/* ===== Sonner Toast - B안 (하단 중앙 스낵바) ===== */
+
+/* 기본 토스트: 다크 배경 스낵바 */
+[data-sonner-toaster] [data-sonner-toast].sonner-toast-snackbar {
+ --normal-bg: hsl(222 30% 16%);
+ --normal-text: hsl(210 20% 92%);
+ --normal-border: hsl(222 20% 24%);
+
+ background: var(--normal-bg);
+ color: var(--normal-text);
+ border: 1px solid var(--normal-border);
+ border-radius: 10px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
+ padding: 10px 16px;
+ font-size: 13px;
+ font-weight: 500;
+ gap: 10px;
}
-[data-sonner-toaster] [data-sonner-toast][data-mounted="true"] {
- animation: none !important;
+
+/* 다크모드 토스트 */
+.dark [data-sonner-toaster] [data-sonner-toast].sonner-toast-snackbar {
+ --normal-bg: hsl(220 25% 14%);
+ --normal-border: hsl(220 20% 22%);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
+
+/* 성공 토스트 - 좌측 초록 바 */
+[data-sonner-toaster] [data-sonner-toast].sonner-toast-success {
+ --success-bg: hsl(222 30% 16%);
+ --success-text: hsl(210 20% 92%);
+ --success-border: hsl(222 20% 24%);
+
+ background: var(--success-bg) !important;
+ color: var(--success-text) !important;
+ border: 1px solid var(--success-border) !important;
+ border-left: 3px solid hsl(142 76% 42%) !important;
+ border-radius: 10px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
+}
+
+.dark [data-sonner-toaster] [data-sonner-toast].sonner-toast-success {
+ --success-bg: hsl(220 25% 14%) !important;
+ --success-border: hsl(220 20% 22%) !important;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
+/* 에러 토스트 - 좌측 빨간 바 + 약간 붉은 배경 */
+[data-sonner-toaster] [data-sonner-toast].sonner-toast-error {
+ --error-bg: hsl(0 30% 14%);
+ --error-text: hsl(0 20% 92%);
+ --error-border: hsl(0 20% 22%);
+
+ background: var(--error-bg) !important;
+ color: var(--error-text) !important;
+ border: 1px solid var(--error-border) !important;
+ border-left: 3px solid hsl(0 72% 55%) !important;
+ border-radius: 10px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
+}
+
+.dark [data-sonner-toaster] [data-sonner-toast].sonner-toast-error {
+ --error-bg: hsl(0 25% 10%) !important;
+ --error-border: hsl(0 20% 18%) !important;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
+/* 경고 토스트 - 좌측 노란 바 */
+[data-sonner-toaster] [data-sonner-toast].sonner-toast-warning {
+ background: hsl(222 30% 16%) !important;
+ color: hsl(210 20% 92%) !important;
+ border: 1px solid hsl(222 20% 24%) !important;
+ border-left: 3px solid hsl(38 92% 50%) !important;
+ border-radius: 10px;
+}
+
+/* info 토스트 - 좌측 파란 바 */
+[data-sonner-toaster] [data-sonner-toast].sonner-toast-info {
+ background: hsl(222 30% 16%) !important;
+ color: hsl(210 20% 92%) !important;
+ border: 1px solid hsl(222 20% 24%) !important;
+ border-left: 3px solid hsl(217 91% 60%) !important;
+ border-radius: 10px;
+}
+
+/* 토스트 내부 설명 텍스트 */
+[data-sonner-toaster] [data-sonner-toast] [data-description] {
+ color: hsl(210 15% 70%) !important;
+ font-size: 12px !important;
+}
+
+/* 토스트 닫기 버튼: 토스트 안쪽 우측 상단 배치 */
+[data-sonner-toaster] [data-sonner-toast] [data-close-button] {
+ position: absolute !important;
+ top: 50% !important;
+ right: 8px !important;
+ left: auto !important;
+ transform: translateY(-50%) !important;
+ width: 20px !important;
+ height: 20px !important;
+ background: transparent !important;
+ border: none !important;
+ border-radius: 4px !important;
+ color: hsl(210 15% 55%) !important;
+ opacity: 0.6;
+ transition: opacity 0.15s, background 0.15s;
+}
+[data-sonner-toaster] [data-sonner-toast] [data-close-button]:hover {
+ background: hsl(220 20% 24%) !important;
+ color: hsl(210 20% 85%) !important;
+ opacity: 1;
+}
+
+/* 토스트 액션 버튼 */
+[data-sonner-toaster] [data-sonner-toast] [data-button] {
+ color: hsl(217 91% 68%) !important;
+ font-weight: 700;
+ font-size: 12px;
+}
+
+/* 애니메이션 제어: 부드러운 슬라이드 업만 허용, 나머지 제거 */
[data-sonner-toaster] [data-sonner-toast][data-removed="true"] {
animation: none !important;
}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 2c5f1fa3..c57c6504 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -4,7 +4,7 @@ import "./globals.css";
import { ThemeProvider } from "@/components/providers/ThemeProvider";
import { QueryProvider } from "@/providers/QueryProvider";
import { RegistryProvider } from "./registry-provider";
-import { Toaster } from "sonner";
+import { Toaster } from "@/components/ui/sonner";
const inter = Inter({
subsets: ["latin"],
@@ -45,7 +45,7 @@ export default function RootLayout({
{children}
-
+
{/* Portal 컨테이너 */}
diff --git a/frontend/components/ui/sonner.tsx b/frontend/components/ui/sonner.tsx
new file mode 100644
index 00000000..0911ead1
--- /dev/null
+++ b/frontend/components/ui/sonner.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { useTheme } from "next-themes";
+import { Toaster as SonnerToaster } from "sonner";
+
+export function Toaster() {
+ const { theme = "system" } = useTheme();
+
+ return (
+
+ );
+}
diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx
index 31cabb70..616ef121 100644
--- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx
+++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx
@@ -6317,7 +6317,7 @@ export const TableListComponent: React.FC = ({
"hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75",
index % 2 === 0 ? "bg-background" : "bg-muted/20",
isRowSelected && "!bg-primary/10 hover:!bg-primary/15",
- isRowFocused && "ring-primary/50 ring-1 ring-inset",
+ isRowFocused && "bg-accent/50",
isDragEnabled && "cursor-grab active:cursor-grabbing",
isDragging && "bg-muted opacity-50",
isDropTarget && "border-t-primary border-t-2",
@@ -6381,10 +6381,10 @@ export const TableListComponent: React.FC = ({
inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]",
column.columnName === "__checkbox__" ? "px-0 py-[7px] text-center" : "px-3 py-[7px]",
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
- isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset",
+ isCellFocused && !editingCell && "bg-primary/5 shadow-[inset_0_0_0_2px_hsl(var(--primary))]",
editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0",
isModified && !cellValidationError && "bg-amber-100 dark:bg-amber-900/40",
- cellValidationError && "bg-red-50 ring-2 ring-red-500 ring-inset dark:bg-red-950/40",
+ cellValidationError && "bg-red-50 shadow-[inset_0_0_0_2px_hsl(0_84%_60%)] dark:bg-red-950/40",
isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50",
column.editable === false && "bg-muted/10 dark:bg-muted/10",
// 코드 컬럼: mono 폰트 + primary 색상