diff --git a/backend-node/src/controllers/mailAccountFileController.ts b/backend-node/src/controllers/mailAccountFileController.ts
index 702a5dd4..668dedf3 100644
--- a/backend-node/src/controllers/mailAccountFileController.ts
+++ b/backend-node/src/controllers/mailAccountFileController.ts
@@ -178,14 +178,19 @@ export class MailAccountFileController {
try {
const { id } = req.params;
- // TODO: 실제 SMTP 연결 테스트 구현
- // const account = await mailAccountFileService.getAccountById(id);
- // nodemailer로 연결 테스트
+ const account = await mailAccountFileService.getAccountById(id);
+ if (!account) {
+ return res.status(404).json({
+ success: false,
+ message: '계정을 찾을 수 없습니다.',
+ });
+ }
- return res.json({
- success: true,
- message: '연결 테스트 성공 (미구현)',
- });
+ // mailSendSimpleService의 testConnection 사용
+ const { mailSendSimpleService } = require('../services/mailSendSimpleService');
+ const result = await mailSendSimpleService.testConnection(id);
+
+ return res.json(result);
} catch (error: unknown) {
const err = error as Error;
return res.status(500).json({
diff --git a/backend-node/src/controllers/mailSendSimpleController.ts b/backend-node/src/controllers/mailSendSimpleController.ts
index cbd9b647..15a1bea0 100644
--- a/backend-node/src/controllers/mailSendSimpleController.ts
+++ b/backend-node/src/controllers/mailSendSimpleController.ts
@@ -7,10 +7,12 @@ export class MailSendSimpleController {
*/
async sendMail(req: Request, res: Response) {
try {
+ console.log('📧 메일 발송 요청 수신:', { accountId: req.body.accountId, to: req.body.to, subject: req.body.subject });
const { accountId, templateId, to, subject, variables, customHtml } = req.body;
// 필수 파라미터 검증
if (!accountId || !to || !Array.isArray(to) || to.length === 0) {
+ console.log('❌ 필수 파라미터 누락');
return res.status(400).json({
success: false,
message: '계정 ID와 수신자 이메일이 필요합니다.',
diff --git a/backend-node/src/routes/mailAccountFileRoutes.ts b/backend-node/src/routes/mailAccountFileRoutes.ts
index cc022cb8..35772d39 100644
--- a/backend-node/src/routes/mailAccountFileRoutes.ts
+++ b/backend-node/src/routes/mailAccountFileRoutes.ts
@@ -1,8 +1,12 @@
import { Router } from 'express';
import { mailAccountFileController } from '../controllers/mailAccountFileController';
+import { authenticateToken } from '../middleware/authMiddleware';
const router = Router();
+// 모든 메일 계정 라우트에 인증 미들웨어 적용
+router.use(authenticateToken);
+
router.get('/', (req, res) => mailAccountFileController.getAllAccounts(req, res));
router.get('/:id', (req, res) => mailAccountFileController.getAccountById(req, res));
router.post('/', (req, res) => mailAccountFileController.createAccount(req, res));
diff --git a/backend-node/src/routes/mailReceiveBasicRoutes.ts b/backend-node/src/routes/mailReceiveBasicRoutes.ts
index f8d0d670..d21df689 100644
--- a/backend-node/src/routes/mailReceiveBasicRoutes.ts
+++ b/backend-node/src/routes/mailReceiveBasicRoutes.ts
@@ -4,8 +4,12 @@
import express from 'express';
import { MailReceiveBasicController } from '../controllers/mailReceiveBasicController';
+import { authenticateToken } from '../middleware/authMiddleware';
const router = express.Router();
+
+// 모든 메일 수신 라우트에 인증 미들웨어 적용
+router.use(authenticateToken);
const controller = new MailReceiveBasicController();
// 메일 목록 조회
diff --git a/backend-node/src/routes/mailSendSimpleRoutes.ts b/backend-node/src/routes/mailSendSimpleRoutes.ts
index db56b66d..726a220c 100644
--- a/backend-node/src/routes/mailSendSimpleRoutes.ts
+++ b/backend-node/src/routes/mailSendSimpleRoutes.ts
@@ -1,8 +1,12 @@
import { Router } from 'express';
import { mailSendSimpleController } from '../controllers/mailSendSimpleController';
+import { authenticateToken } from '../middleware/authMiddleware';
const router = Router();
+// 모든 메일 발송 라우트에 인증 미들웨어 적용
+router.use(authenticateToken);
+
// POST /api/mail/send/simple - 메일 발송
router.post('/simple', (req, res) => mailSendSimpleController.sendMail(req, res));
diff --git a/backend-node/src/routes/mailTemplateFileRoutes.ts b/backend-node/src/routes/mailTemplateFileRoutes.ts
index eb79ed34..a4f81b1b 100644
--- a/backend-node/src/routes/mailTemplateFileRoutes.ts
+++ b/backend-node/src/routes/mailTemplateFileRoutes.ts
@@ -1,8 +1,12 @@
import { Router } from 'express';
import { mailTemplateFileController } from '../controllers/mailTemplateFileController';
+import { authenticateToken } from '../middleware/authMiddleware';
const router = Router();
+// 모든 메일 템플릿 라우트에 인증 미들웨어 적용
+router.use(authenticateToken);
+
// 템플릿 CRUD
router.get('/', (req, res) => mailTemplateFileController.getAllTemplates(req, res));
router.get('/:id', (req, res) => mailTemplateFileController.getTemplateById(req, res));
diff --git a/backend-node/src/services/mailSendSimpleService.ts b/backend-node/src/services/mailSendSimpleService.ts
index c5d2fc4f..473f3959 100644
--- a/backend-node/src/services/mailSendSimpleService.ts
+++ b/backend-node/src/services/mailSendSimpleService.ts
@@ -6,6 +6,7 @@
import nodemailer from 'nodemailer';
import { mailAccountFileService } from './mailAccountFileService';
import { mailTemplateFileService } from './mailTemplateFileService';
+import { encryptionService } from './encryptionService';
export interface SendMailRequest {
accountId: string;
@@ -56,18 +57,39 @@ class MailSendSimpleService {
throw new Error('메일 내용이 없습니다.');
}
- // 4. SMTP 연결 생성
+ // 4. 비밀번호 복호화
+ const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
+ console.log('🔐 비밀번호 복호화 완료');
+ console.log('🔐 암호화된 비밀번호 (일부):', account.smtpPassword.substring(0, 30) + '...');
+ console.log('🔐 복호화된 비밀번호 길이:', decryptedPassword.length);
+
+ // 5. SMTP 연결 생성
+ // 포트 465는 SSL/TLS를 사용해야 함
+ const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
+
+ console.log('📧 SMTP 연결 설정:', {
+ host: account.smtpHost,
+ port: account.smtpPort,
+ secure: isSecure,
+ user: account.smtpUsername,
+ });
+
const transporter = nodemailer.createTransport({
host: account.smtpHost,
port: account.smtpPort,
- secure: account.smtpSecure, // SSL/TLS
+ secure: isSecure, // SSL/TLS (포트 465는 자동으로 true)
auth: {
user: account.smtpUsername,
- pass: account.smtpPassword,
+ pass: decryptedPassword, // 복호화된 비밀번호 사용
},
+ // 타임아웃 설정 (30초)
+ connectionTimeout: 30000,
+ greetingTimeout: 30000,
});
- // 5. 메일 발송
+ console.log('📧 메일 발송 시도 중...');
+
+ // 6. 메일 발송
const info = await transporter.sendMail({
from: `"${account.name}" <${account.email}>`,
to: request.to.join(', '),
@@ -75,6 +97,12 @@ class MailSendSimpleService {
html: htmlContent,
});
+ console.log('✅ 메일 발송 성공:', {
+ messageId: info.messageId,
+ accepted: info.accepted,
+ rejected: info.rejected,
+ });
+
return {
success: true,
messageId: info.messageId,
@@ -83,6 +111,8 @@ class MailSendSimpleService {
};
} catch (error) {
const err = error as Error;
+ console.error('❌ 메일 발송 실패:', err.message);
+ console.error('❌ 에러 상세:', err);
return {
success: false,
error: err.message,
@@ -178,22 +208,42 @@ class MailSendSimpleService {
*/
async testConnection(accountId: string): Promise<{ success: boolean; message: string }> {
try {
+ console.log('🔌 SMTP 연결 테스트 시작:', accountId);
+
const account = await mailAccountFileService.getAccountById(accountId);
if (!account) {
throw new Error('계정을 찾을 수 없습니다.');
}
+ // 비밀번호 복호화
+ const decryptedPassword = encryptionService.decrypt(account.smtpPassword);
+ console.log('🔐 비밀번호 복호화 완료');
+
+ // 포트 465는 SSL/TLS를 사용해야 함
+ const isSecure = account.smtpPort === 465 ? true : (account.smtpSecure || false);
+
+ console.log('🔌 SMTP 연결 설정:', {
+ host: account.smtpHost,
+ port: account.smtpPort,
+ secure: isSecure,
+ user: account.smtpUsername,
+ });
+
const transporter = nodemailer.createTransport({
host: account.smtpHost,
port: account.smtpPort,
- secure: account.smtpSecure,
+ secure: isSecure,
auth: {
user: account.smtpUsername,
- pass: account.smtpPassword,
+ pass: decryptedPassword, // 복호화된 비밀번호 사용
},
+ connectionTimeout: 10000, // 10초 타임아웃
+ greetingTimeout: 10000,
});
+ console.log('🔌 SMTP 연결 검증 중...');
await transporter.verify();
+ console.log('✅ SMTP 연결 검증 성공!');
return {
success: true,
@@ -201,6 +251,7 @@ class MailSendSimpleService {
};
} catch (error) {
const err = error as Error;
+ console.error('❌ SMTP 연결 실패:', err.message);
return {
success: false,
message: `연결 실패: ${err.message}`,
diff --git a/docs/리포트_관리_시스템_구현_완료_기능.md b/docs/리포트_관리_시스템_구현_완료_기능.md
new file mode 100644
index 00000000..69ad5670
--- /dev/null
+++ b/docs/리포트_관리_시스템_구현_완료_기능.md
@@ -0,0 +1,1033 @@
+# 리포트 관리 시스템 - 구현 완료 기능
+
+## 📋 목차
+
+1. [리포트 목록 관리](#1-리포트-목록-관리)
+2. [리포트 디자이너](#2-리포트-디자이너)
+3. [쿼리 관리](#3-쿼리-관리)
+4. [컴포넌트 시스템](#4-컴포넌트-시스템)
+5. [레이아웃 도구](#5-레이아웃-도구)
+6. [템플릿 시스템](#6-템플릿-시스템)
+7. [페이지 관리](#7-페이지-관리)
+8. [미리보기 및 출력](#8-미리보기-및-출력)
+
+---
+
+## 1. 리포트 목록 관리
+
+### 1.1 리포트 목록 화면
+
+- **경로**: `/admin/report`
+- **기능**:
+ - 전체 리포트 목록 조회
+ - 검색 필터링 (리포트명)
+ - 정렬 (생성일, 수정일)
+
+### 1.2 리포트 기본 관리
+
+- 리포트 생성
+- 리포트 수정
+- 리포트 복사
+- 리포트 삭제
+- 리포트 상세 정보 표시
+
+### 1.3 UI 개선사항
+
+- 타입 컬럼 제거 (디자인 간소화)
+- 복사/삭제 액션 추가
+
+---
+
+## 2. 리포트 디자이너
+
+### 2.1 기본 구조
+
+- **경로**: `/admin/report/designer/[reportId]`
+- **레이아웃**:
+ - 좌측: 페이지 목록 패널
+ - 중앙 좌측: 컴포넌트 팔레트
+ - 중앙: 캔버스
+ - 우측: 속성/쿼리/페이지 설정 패널 (탭)
+
+### 2.2 캔버스 기능
+
+- 드래그 앤 드롭으로 컴포넌트 배치
+- 컴포넌트 선택/이동/크기 조절
+- 그리드 스냅 (10px 단위)
+- 정렬 가이드라인 (빨간색 라인)
+- 캔버스 중심선 가이드
+- 눈금자 표시 (상단/좌측)
+- 페이지 여백 가이드 (파란색 점선)
+- 컴포넌트가 여백 밖으로 나가지 않도록 제한
+
+### 2.3 컴포넌트 조작
+
+- 다중 선택 (Ctrl/Cmd + 클릭)
+- 복사/붙여넣기 (Ctrl/Cmd + C/V)
+- 실행 취소/재실행 (Ctrl/Cmd + Z/Y)
+- 키보드 화살표 이동 (1px, Shift + 10px)
+- 컴포넌트 잠금/해제
+- 레이어 순서 조정 (맨 앞/뒤, 앞/뒤로)
+
+### 2.4 정렬 도구
+
+- **그룹 정렬**:
+ - 왼쪽/오른쪽/상단/하단 정렬
+ - 수평/수직 중앙 정렬
+- **분산**:
+ - 수평 분산
+ - 수직 분산
+- **크기 조정**:
+ - 너비 맞춤
+ - 높이 맞춤
+ - 크기 맞춤 (너비+높이)
+
+### 2.5 컴포넌트 그룹화
+
+- 여러 컴포넌트를 그룹으로 묶기
+- 그룹 해제
+- 그룹 단위 이동
+
+---
+
+## 3. 쿼리 관리
+
+### 3.1 쿼리 목록
+
+- **UI**: 아코디언 방식 (피그마 스타일)
+- 쿼리 추가/삭제
+- 쿼리 타입: MASTER (1건) / DETAIL (반복)
+- 부드러운 열기/닫기 애니메이션
+
+### 3.2 쿼리 설정
+
+- 쿼리 이름 설정
+- SQL 쿼리 입력 (Textarea)
+- DB 연결 선택:
+ - 내부 DB (PostgreSQL)
+ - 외부 DB 연결 선택
+
+### 3.3 파라미터 관리
+
+- 자동 파라미터 감지 (`$1`, `$2` 등)
+- 작은따옴표 내부 파라미터 무시
+- 파라미터 타입 설정 (텍스트/숫자/날짜)
+- 파라미터 입력 필드 동적 생성
+- 등장 순서대로 정렬
+
+### 3.4 쿼리 실행 및 검증
+
+- SQL 안전성 검증:
+ - SELECT/WITH 쿼리만 허용
+ - 위험한 명령어 차단 (DELETE, DROP, UPDATE 등)
+ - 단일 쿼리만 실행 가능
+- 실행 버튼 (빨간색)
+- 파라미터 미입력 시 실행 버튼 비활성화
+- 실행 결과 표시 (필드명, 데이터 건수)
+- 실행 결과 Context에 저장
+
+### 3.5 외부 DB 연동
+
+- 외부 DB 연결 목록 조회
+- 쿼리별 외부 DB 선택 가능
+- PostgreSQL, MariaDB, MSSQL, Oracle 지원
+
+---
+
+## 4. 컴포넌트 시스템
+
+### 4.1 Text 컴포넌트
+
+- 기본 텍스트 표시
+- 쿼리 필드 바인딩
+- 스타일 설정:
+ - 글꼴 크기
+ - 글꼴 색상
+ - 글꼴 굵기 (보통/굵게)
+ - 텍스트 정렬 (왼쪽/가운데/오른쪽)
+ - 배경색
+ - 테두리 (두께, 색상)
+
+### 4.2 Label 컴포넌트
+
+- 고정 텍스트 레이블
+- Text와 동일한 스타일 옵션
+
+### 4.3 Image 컴포넌트
+
+- 이미지 파일 업로드 (로컬 파일)
+- 이미지 표시 옵션:
+ - Contain (원본 비율 유지)
+ - Cover (영역 채우기)
+ - Fill (늘려서 채우기)
+- 테두리 설정
+
+### 4.4 Divider 컴포넌트
+
+- 구분선 표시
+- 방향: 수평/수직
+- 선 스타일: 실선/점선/이중선
+- 선 두께 설정
+- 선 색상 설정
+
+### 4.5 Signature (서명란) 컴포넌트
+
+- 서명 이미지 업로드
+- 직접 서명 그리기 (마우스 드로잉)
+- 탭 방식 전환 (직접 서명/이미지 업로드)
+- 서명란 밑줄 표시 옵션
+- 레이블 표시 (서명:)
+- 레이블 위치 (좌/우/상/하)
+- 기본 테두리 없음
+
+### 4.6 Stamp (도장란) 컴포넌트
+
+- 도장 이미지 업로드
+- 이름 입력 (좌측 표시)
+- "(인)" 레이블 표시/숨김
+- 레이블이 도장 이미지와 겹침
+- 기본 테두리 없음
+
+### 4.7 Table 컴포넌트
+
+- 쿼리 결과를 테이블로 표시
+- 자동 컬럼 생성 (쿼리 필드 기반)
+- 컬럼 설정:
+ - 필드명
+ - 헤더명
+ - 너비
+ - 정렬 (좌/가운데/우)
+- 테이블 스타일:
+ - 헤더 배경색
+ - 헤더 텍스트 색상
+ - 테두리 표시/숨김
+ - 행 높이
+- 캔버스에서 전체 데이터 표시 (최대 20행 + "외 N건")
+
+### 4.8 컴포넌트 공통 기능
+
+- 위치 (X, Y)
+- 크기 (너비, 높이)
+- 배경색
+- 테두리 (두께, 색상)
+- Z-index (레이어 순서)
+- 잠금 상태
+
+---
+
+## 5. 레이아웃 도구
+
+### 5.1 그리드 스냅
+
+- 10px 단위 그리드 스냅
+- 컴포넌트 배치/크기 조절 시 자동 적용
+
+### 5.2 정렬 가이드라인
+
+- 드래그 중 다른 컴포넌트와 정렬 시 빨간색 라인 표시
+- 정렬 기준:
+ - 좌측/우측/상단/하단 가장자리
+ - 수평/수직 중앙
+- 캔버스 중심선 가이드
+
+### 5.3 눈금자
+
+- 캔버스 상단/좌측에 눈금자 표시
+- mm 단위 표시
+
+### 5.4 레이어 관리
+
+- 컴포넌트 레이어 순서 조정
+- 맨 앞으로 가져오기
+- 맨 뒤로 보내기
+- 앞으로 가져오기
+- 뒤로 보내기
+
+### 5.5 컴포넌트 잠금
+
+- 선택한 컴포넌트 잠금
+- 잠금 시 이동/크기 조절/삭제 불가
+- 잠금 해제
+
+---
+
+## 6. 템플릿 시스템
+
+### 6.1 시스템 템플릿
+
+- 기본 제공 템플릿:
+ - 구매 요청서
+ - 발주서
+ - 기본 템플릿
+- 템플릿 적용 시 레이아웃 및 쿼리 자동 로드
+
+### 6.2 사용자 정의 템플릿
+
+- 현재 레이아웃을 템플릿으로 저장
+- 템플릿 이름, 설명, 카테고리 설정
+- 저장 시 리포트 저장 불필요
+- 저장된 템플릿은 기본 템플릿 목록에 추가
+
+---
+
+## 7. 페이지 관리
+
+### 7.1 페이지 목록 (좌측 패널)
+
+- 페이지 목록 표시
+- 페이지 이름 표시
+- 페이지 크기 및 컴포넌트 개수 표시
+- 현재 선택된 페이지 하이라이트
+
+### 7.2 페이지 조작
+
+- 페이지 추가 (무제한)
+- 페이지 삭제 (최소 1개 유지)
+- 페이지 복제
+- 페이지 이름 변경 (인라인 편집)
+- 드래그로 페이지 순서 변경 (GripVertical 핸들)
+
+### 7.3 페이지 설정 (우측 패널)
+
+- 페이지 정보:
+ - 페이지 이름
+- 페이지 크기:
+ - 너비/높이 (mm)
+ - 방향 (세로/가로)
+ - A4 프리셋 (세로/가로)
+- 페이지 여백:
+ - 상단/하단/좌측/우측 (mm)
+ - 여백 프리셋 (좁게/보통/넓게)
+
+### 7.4 페이지별 컴포넌트 관리
+
+- 각 페이지마다 독립적인 컴포넌트 목록
+- 페이지 전환 시 해당 페이지의 컴포넌트만 표시
+- 컴포넌트는 여백 밖으로 이동 불가
+
+### 7.5 데이터 구조
+
+- JSONB 기반 페이지 저장
+- 기존 단일 페이지 레이아웃 자동 마이그레이션
+- 하위 호환성 유지
+
+---
+
+## 8. 미리보기 및 출력
+
+### 8.1 미리보기 모달
+
+- 모든 페이지 순서대로 렌더링
+- 페이지 번호 및 이름 표시
+- 실제 출력과 동일한 모습
+- 편집 UI 요소 제거 (섹션 라벨, 테두리 등)
+- 쿼리 실행 결과 반영
+
+### 8.2 인쇄 기능
+
+- 브라우저 네이티브 인쇄 대화상자
+- 모든 페이지 인쇄
+- 페이지 나누기 자동 처리
+- 이미지 로드 대기 후 인쇄
+
+### 8.3 PDF 출력
+
+- 브라우저 인쇄 기능 이용
+- "PDF로 저장" 안내 토스트
+
+### 8.4 WORD 출력
+
+- `docx` 라이브러리 사용
+- 순차적 흐름 방식 (절대 위치 미지원)
+- 텍스트 및 테이블 출력
+- 텍스트 편집 가능
+
+### 8.5 출력 옵션
+
+- 테이블 헤더:
+ - 첫 페이지에만 표시
+ - 다음 페이지부터 헤더 없이 데이터만 표시
+
+---
+
+## 9. 기술 스택
+
+### 9.1 Frontend
+
+- **Framework**: Next.js 14
+- **Language**: TypeScript
+- **UI Library**: Shadcn UI
+- **State Management**: React Context API
+- **Drag & Drop**: react-dnd
+- **File Upload**: Multer (Backend)
+- **Document Export**: docx
+
+### 9.2 Backend
+
+- **Runtime**: Node.js
+- **Language**: TypeScript
+- **Database**: PostgreSQL
+- **External DB**: DatabaseConnectorFactory
+ - PostgreSQL
+ - MariaDB
+ - MSSQL
+ - Oracle
+
+### 9.3 주요 라이브러리
+
+- `lucide-react`: 아이콘
+- `uuid`: 고유 ID 생성
+- `pg`: PostgreSQL 연결
+- `docx`: WORD 문서 생성
+
+---
+
+## 10. 주요 파일 구조
+
+### 10.1 Frontend
+
+```
+frontend/
+├── app/(main)/admin/report/
+│ ├── page.tsx # 리포트 목록
+│ └── designer/[reportId]/
+│ └── page.tsx # 리포트 디자이너
+├── components/report/
+│ ├── designer/
+│ │ ├── ReportDesignerCanvas.tsx # 캔버스
+│ │ ├── ReportDesignerLeftPanel.tsx # 좌측 패널
+│ │ ├── ReportDesignerRightPanel.tsx # 우측 패널
+│ │ ├── ReportDesignerToolbar.tsx # 툴바
+│ │ ├── PageListPanel.tsx # 페이지 목록
+│ │ ├── QueryManager.tsx # 쿼리 관리
+│ │ ├── CanvasComponent.tsx # 캔버스 컴포넌트
+│ │ ├── ComponentPropertiesPanel.tsx # 속성 패널
+│ │ ├── ReportPreviewModal.tsx # 미리보기
+│ │ └── Ruler.tsx # 눈금자
+│ └── ReportList.tsx # 리포트 목록
+├── contexts/
+│ └── ReportDesignerContext.tsx # 디자이너 Context
+├── types/
+│ └── report.ts # 타입 정의
+└── lib/api/
+ └── reportApi.ts # API 클라이언트
+```
+
+### 10.2 Backend
+
+```
+backend-node/
+├── src/
+│ ├── controllers/
+│ │ └── reportController.ts # 리포트 컨트롤러
+│ ├── services/
+│ │ └── reportService.ts # 리포트 서비스
+│ ├── routes/
+│ │ └── reportRoutes.ts # 리포트 라우트
+│ └── database/
+│ └── connectors/ # 외부 DB 커넥터
+└── uploads/ # 업로드 파일
+```
+
+---
+
+## 11. 데이터베이스 스키마
+
+### 11.1 주요 테이블
+
+- `REPORT`: 리포트 기본 정보
+- `REPORT_LAYOUT`: 레이아웃 설정 (JSONB)
+- `REPORT_QUERY`: 쿼리 정보
+- `REPORT_TEMPLATE`: 템플릿 정보
+- `EXTERNAL_DB_CONNECTION`: 외부 DB 연결 정보
+
+### 11.2 JSONB 구조
+
+```json
+{
+ "pages": [
+ {
+ "page_id": "uuid",
+ "page_name": "페이지 1",
+ "page_order": 0,
+ "width": 210,
+ "height": 297,
+ "orientation": "portrait",
+ "margins": {
+ "top": 10,
+ "bottom": 10,
+ "left": 10,
+ "right": 10
+ },
+ "background_color": "#ffffff",
+ "components": [
+ {
+ "id": "uuid",
+ "type": "text",
+ "x": 50,
+ "y": 50,
+ "width": 200,
+ "height": 30,
+ "defaultValue": "텍스트",
+ "queryId": "query_uuid",
+ "fieldName": "field_name",
+ "fontSize": 13,
+ "fontColor": "#000000",
+ "fontWeight": "normal",
+ "textAlign": "left",
+ "backgroundColor": "transparent",
+ "borderWidth": 0,
+ "borderColor": "#000000"
+ }
+ ]
+ }
+ ]
+}
+```
+
+---
+
+## 12. 보안 및 검증
+
+### 12.1 SQL 인젝션 방지
+
+- 파라미터 바인딩 사용
+- 위험한 SQL 명령어 차단
+- SELECT/WITH 쿼리만 허용
+- 단일 쿼리 실행만 가능
+
+### 12.2 파일 업로드 보안
+
+- 허용된 파일 확장자만 업로드
+- 파일 크기 제한
+- 안전한 파일명 생성
+
+### 12.3 권한 관리
+
+- 사용자별 리포트 접근 권한
+- 세션 기반 인증
+
+---
+
+## 13. 개선 예정 기능
+
+### 13.1 테이블 페이지 분할
+
+- 테이블이 페이지를 넘어갈 때 자동으로 다음 페이지로 분할
+- 현재는 단일 페이지에만 표시
+
+### 13.2 기업 사용자를 위한 필수 기능 ⭐
+
+> **현재는 기본 기능만 구현되어 있어, 기업 환경에서 사용하기 위해서는 아래 기능들이 추가로 필요합니다.**
+
+#### A. 보안 및 접근 제어 🔒
+
+##### 리포트 권한 관리
+
+- **조회 권한**: 부서별/팀별/역할별 조회 권한 설정
+- **수정 권한**: 작성자/관리자/공동 편집자 권한
+- **출력 권한**: 출력 권한 별도 관리, 워터마크 삽입
+- **삭제 권한**: 승인 프로세스, 논리적 삭제
+
+##### 데이터 보안
+
+- **민감 정보 마스킹**: 주민번호, 계좌번호 등 자동 마스킹
+- **데이터 필터링**: 사용자 권한에 따른 데이터 자동 필터링
+- **쿼리 실행 제한**: 실행 시간, 결과 행 수, 복잡도 제한
+
+##### 감사 로그 (Audit Trail)
+
+- **접근 로그**: 누가, 언제, 어떤 리포트를 조회했는지 기록
+- **수정 로그**: 수정 이력, 버전 관리, 롤백
+- **출력 로그**: 출력 이력, 다운로드 추적
+
+#### B. 문서 관리 📄
+
+##### 문서 번호 체계
+
+- **자동 채번**: 부서별/카테고리별/연도별 고유 번호 자동 부여
+ - 예: `SALES-2025-001`, `HR-2025-0001`
+- **문서 번호 포맷**: 커스터마이징 가능한 번호 형식
+- **문서 번호 컴포넌트**: 리포트에 문서 번호 자동 삽입, 바코드/QR코드 표시
+
+##### 문서 상태 관리
+
+- **작성 중** → **검토 중** → **승인 대기** → **승인 완료** / **반려** → **폐기**
+- 각 상태별 권한 및 동작 제어
+- 상태 변경 이력 추적
+
+##### 문서 생명주기
+
+- **보존 기간 설정**: 1년/3년/5년/영구 보존
+- **보존 기간 만료 알림**: 자동 알림 및 보관 이관
+- **보관 문서 관리**: 별도 보관 문서함, 조회 권한 제한
+
+#### C. 승인 워크플로우 ✅
+
+##### 승인 프로세스
+
+- **승인선 설정**: 부서별/직급별 자동 승인선 구성
+- **단계별 승인**: 1차(팀장) → 2차(부서장) → 최종(임원)
+- **병렬 승인**: 다수 승인자 동시 승인
+- **승인 알림**: 이메일/시스템/모바일 푸시 알림
+
+##### 승인 후 처리
+
+- **리포트 잠금**: 승인 완료 후 수정 불가
+- **배포 관리**: 자동 배포, 배포 대상자 지정
+- **승인 이력**: 승인자, 승인 시점, 승인 의견 기록
+
+#### D. 협업 기능 👥
+
+##### 공유 및 협업
+
+- **리포트 공유**: 링크 공유, 특정 사용자 공유, 공유 기간 설정
+- **공동 편집**: 실시간 편집, Lock 기능, 충돌 방지
+- **댓글 및 리뷰**: 컴포넌트별 댓글, 검토 의견
+
+##### 템플릿 공유
+
+- **템플릿 라이브러리**: 공용 템플릿, 부서별 템플릿
+- **템플릿 승인**: 공용 템플릿 등록 시 승인 필요
+- **템플릿 평점**: 사용자 평가 및 리뷰
+
+#### E. 조직 관리 🏢
+
+##### 부서/팀 관리
+
+- **부서별 리포트**: 부서별 리포트 목록 관리
+- **권한 상속**: 상위 부서 권한 자동 상속
+- **부서 이동 처리**: 부서 이동 시 리포트 이관
+
+##### 역할 관리
+
+- **역할 정의**: 작성자/검토자/승인자/관리자
+- **역할별 권한**: 조회/생성/수정/삭제 권한 세분화
+
+#### F. 규정 준수 (Compliance) ⚖️
+
+##### 전자문서법 준수
+
+- **전자서명**: 리포트에 전자서명 첨부, 서명 검증
+- **타임스탬프**: 생성/수정/승인 시점 타임스탬프, 위변조 방지
+
+##### 개인정보보호법 준수
+
+- **개인정보 표시**: 개인정보 포함 여부 표시, 처리 동의 기록
+- **개인정보 파기**: 보존 기간 만료 후 자동 파기, 복구 불가
+
+#### G. 통합 및 연동 🔗
+
+##### ERP 시스템 연동
+
+- **마스터 데이터 연동**: 거래처, 품목, 고객 정보 실시간 동기화
+- **워크플로우 연동**: 구매/영업 프로세스와 연계
+
+##### 외부 시스템 연동
+
+- **이메일 연동**: 리포트 자동 발송, 승인 알림
+- **클라우드 스토리지**: Google Drive, OneDrive 연동
+- **전자결재 시스템**: SSO 연동, 통합 승인선
+
+#### H. 사용자 편의 기능 💡
+
+##### 즐겨찾기 및 최근 문서
+
+- **즐겨찾기**: 자주 사용하는 리포트 북마크
+- **최근 문서**: 최근 조회/수정 리포트 목록
+
+##### 검색 및 필터링
+
+- **고급 검색**: 전체 텍스트 검색, 필드별 검색
+- **스마트 필터**: 나의 리포트, 공유된 리포트, 승인 대기
+
+##### 대시보드
+
+- **리포트 통계**: 생성 수, 조회수, 출력 횟수
+- **사용자 통계**: 부서별/사용자별 활동 내역
+
+#### I. 성능 및 최적화 ⚡
+
+##### 대용량 데이터 처리
+
+- **스트리밍 출력**: 대용량 데이터 효율적 처리
+- **캐싱**: 쿼리 결과/템플릿 캐싱 (Redis)
+
+##### 동시 접속 처리
+
+- **부하 분산**: 로드 밸런싱, 세션 클러스터링
+- **큐잉 시스템**: 대량 출력 요청 백그라운드 처리
+
+#### J. 모바일 지원 📱
+
+##### 모바일 웹
+
+- **반응형 디자인**: 모바일 화면 최적화
+- **모바일 기능**: 조회, 승인, 간단한 수정
+
+##### 모바일 앱
+
+- **네이티브 앱**: iOS/Android, 오프라인 모드
+- **푸시 알림**: 승인 요청, 문서 공유 알림
+
+#### K. AI/ML 기반 고급 기능 🤖
+
+##### 지능형 리포트 작성 지원
+
+- **AI 기반 템플릿 추천**: 사용자의 과거 리포트 패턴 분석하여 최적 템플릿 자동 추천
+- **스마트 레이아웃 제안**: 데이터 유형에 따라 최적의 컴포넌트 배치 자동 제안
+- **자동 데이터 매핑**: 쿼리 필드와 컴포넌트 자동 연결 제안
+
+##### 자연어 처리 (NLP)
+
+- **음성 명령**: "작년 매출 리포트 작성해줘" 같은 음성 명령으로 리포트 생성
+- **자동 요약 생성**: 긴 리포트 내용을 AI가 자동으로 요약
+- **다국어 자동 번역**: 리포트 내용을 여러 언어로 실시간 번역
+
+##### 이상 탐지 및 예측 분석
+
+- **데이터 이상 감지**: 리포트 데이터에서 비정상 패턴 자동 감지 및 경고
+- **트렌드 예측**: 과거 데이터 기반 미래 추세 예측 및 시각화
+- **자동 인사이트 생성**: 데이터에서 의미있는 패턴 발견 및 설명 제공
+
+#### L. 데이터 거버넌스 🏛️
+
+##### 데이터 품질 관리
+
+- **데이터 검증 규칙**: 입력 데이터 유효성 자동 검증
+- **데이터 표준화**: 일관된 데이터 형식 강제 적용
+- **중복 데이터 감지**: 중복 리포트 및 데이터 자동 감지
+
+##### 데이터 계보 (Data Lineage)
+
+- **데이터 흐름 추적**: 리포트 데이터의 출처부터 최종 사용까지 전체 흐름 시각화
+- **영향도 분석**: 데이터 변경 시 영향받는 리포트 자동 파악
+- **의존성 관리**: 리포트 간 의존 관계 매핑
+
+##### 데이터 카탈로그
+
+- **메타데이터 관리**: 모든 리포트 및 데이터셋의 메타데이터 중앙 관리
+- **데이터 검색**: 키워드로 관련 리포트 및 데이터 빠르게 검색
+- **데이터 사전**: 필드명, 설명, 데이터 타입 등 표준 용어 정의
+
+#### M. 고급 분석 및 BI 통합 📊
+
+##### 내장 분석 도구
+
+- **OLAP 큐브**: 다차원 데이터 분석 지원
+- **피벗 테이블**: 동적 데이터 집계 및 분석
+- **드릴다운/드릴업**: 계층적 데이터 탐색
+
+##### BI 도구 연동
+
+- **Power BI 커넥터**: Power BI와 원활한 데이터 연동
+- **Tableau 통합**: Tableau 대시보드 임베딩
+- **Google Data Studio 연결**: 클라우드 기반 시각화
+
+##### 고급 시각화
+
+- **인터랙티브 차트**: 사용자 상호작용 가능한 동적 차트
+- **지도 시각화**: 지역별 데이터 지도 표시 (Heatmap, Choropleth)
+- **네트워크 다이어그램**: 관계형 데이터 네트워크 시각화
+
+#### N. 자동화 및 스케줄링 ⏰
+
+##### 리포트 자동 생성
+
+- **스케줄 실행**: 일별/주별/월별 자동 리포트 생성
+- **트리거 기반 실행**: 특정 이벤트 발생 시 자동 생성
+- **배치 처리**: 대량 리포트 일괄 생성
+
+##### 자동 배포
+
+- **이메일 자동 발송**: 생성된 리포트 자동 이메일 발송
+- **FTP/SFTP 업로드**: 외부 서버로 자동 업로드
+- **클라우드 스토리지 동기화**: 자동 백업 및 동기화
+
+##### 워크플로우 자동화
+
+- **조건부 워크플로우**: IF-THEN 조건에 따른 자동 실행
+- **연쇄 작업**: 여러 리포트를 순차적으로 자동 생성
+- **실패 처리**: 실행 실패 시 자동 재시도 및 알림
+
+#### O. 버전 관리 및 변경 추적 📝
+
+##### 고급 버전 관리
+
+- **Git 방식 버전 관리**: Branch, Merge, Commit 개념 도입
+- **변경 사항 비교 (Diff)**: 버전 간 변경 내용 시각적 비교
+- **체크포인트 및 태그**: 주요 버전에 태그 부여
+
+##### 변경 승인 프로세스
+
+- **Pull Request 방식**: 변경 제안 및 리뷰 프로세스
+- **코드 리뷰**: 쿼리 및 레이아웃 변경 사항 동료 검토
+- **머지 승인**: 최종 승인 후 메인 버전에 반영
+
+#### P. 테스트 및 품질 관리 🧪
+
+##### 자동화 테스트
+
+- **쿼리 유효성 테스트**: 쿼리 실행 전 자동 검증
+- **레이아웃 테스트**: 다양한 데이터셋으로 레이아웃 검증
+- **출력 품질 테스트**: PDF/WORD 출력 품질 자동 검증
+
+##### A/B 테스트
+
+- **템플릿 A/B 테스트**: 여러 버전의 리포트 동시 배포 및 성과 비교
+- **사용자 피드백 수집**: 사용자 선호도 분석
+
+##### 품질 메트릭
+
+- **리포트 품질 점수**: 가독성, 데이터 정확도 등 자동 평가
+- **성능 모니터링**: 쿼리 실행 시간, 렌더링 속도 추적
+- **에러율 추적**: 실행 실패율 및 원인 분석
+
+#### Q. 커스터마이징 및 확장성 🔧
+
+##### 플러그인 시스템
+
+- **커스텀 컴포넌트**: 사용자 정의 컴포넌트 개발 및 등록
+- **확장 마켓플레이스**: 커뮤니티 개발 컴포넌트/템플릿 공유
+- **API 확장**: Custom Hook 및 이벤트 리스너 제공
+
+##### 스크립트 지원
+
+- **JavaScript 스크립트**: 고급 데이터 처리 로직 작성
+- **Python 통합**: 데이터 과학 라이브러리 활용
+- **SQL 함수 확장**: 사용자 정의 SQL 함수 등록
+
+##### 테마 및 브랜딩
+
+- **커스텀 테마**: 기업 CI/BI에 맞는 색상/폰트 적용
+- **로고 및 워터마크**: 자동 브랜딩 요소 삽입
+- **다국어 커스터마이징**: 기업별 용어 정의
+
+#### R. 비용 관리 및 최적화 💰
+
+##### 리소스 사용량 추적
+
+- **쿼리 비용 추적**: 각 쿼리의 DB 비용 계산
+- **저장소 사용량**: 리포트 저장 공간 모니터링
+- **API 호출 추적**: 외부 API 사용량 및 비용
+
+##### 비용 최적화 제안
+
+- **비효율적 쿼리 감지**: 성능이 낮은 쿼리 자동 식별
+- **캐싱 전략 제안**: 반복 쿼리에 대한 캐싱 권장
+- **아카이빙 제안**: 오래된 리포트 아카이빙 권장
+
+#### S. 고급 보안 기능 🔐
+
+##### 위협 탐지
+
+- **비정상 접근 패턴 감지**: AI 기반 이상 행동 탐지
+- **SQL 인젝션 방어**: 고급 SQL 인젝션 패턴 탐지
+- **브루트포스 공격 방어**: 로그인 시도 제한 및 차단
+
+##### 암호화 및 키 관리
+
+- **필드 레벨 암호화**: 민감 필드 개별 암호화
+- **키 로테이션**: 암호화 키 자동 교체
+- **Hardware Security Module (HSM) 연동**: 하드웨어 기반 보안
+
+##### 보안 인증
+
+- **SOC 2 준수**: 보안 감사 표준 준수
+- **ISO 27001 인증**: 정보 보안 관리 체계
+- **GDPR 준수**: 유럽 개인정보보호 규정 준수
+
+#### T. 사용자 교육 및 온보딩 📚
+
+##### 인터랙티브 튜토리얼
+
+- **가이드 투어**: 신규 사용자 대상 단계별 안내
+- **인터랙티브 도움말**: 각 기능에 대한 실시간 도움말
+- **비디오 튜토리얼**: 주요 기능 사용법 영상
+
+##### 학습 관리
+
+- **학습 경로 제공**: 초급/중급/고급 학습 코스
+- **인증 프로그램**: 사용자 숙련도 인증
+- **커뮤니티 포럼**: 사용자 간 지식 공유
+
+##### 컨텍스트 도움말
+
+- **AI 어시스턴트**: 자연어로 질문하고 답변 받기
+- **실시간 제안**: 작업 중 관련 팁 자동 제공
+- **오류 해결 가이드**: 오류 발생 시 해결 방법 제시
+
+#### U. 통합 협업 플랫폼 🤝
+
+##### 프로젝트 관리 통합
+
+- **Jira 연동**: 이슈와 리포트 연결
+- **Asana 통합**: 작업 관리와 리포트 동기화
+- **Monday.com 커넥터**: 워크플로우 통합
+
+##### 커뮤니케이션 도구
+
+- **Slack 봇**: 리포트 알림 및 명령 실행
+- **MS Teams 통합**: 팀즈 내에서 리포트 조회
+- **Zoom 통합**: 화상회의 중 리포트 공유
+
+##### 문서 관리 시스템
+
+- **SharePoint 연동**: 리포트 자동 업로드
+- **Confluence 통합**: 위키 페이지에 리포트 임베딩
+- **Notion 커넥터**: Notion 데이터베이스 동기화
+
+### 13.3 추가 컴포넌트
+
+- **Barcode**: 1D/2D 바코드 자동 생성
+- **QR Code**: URL/텍스트 QR코드 생성
+- **Chart**: 막대/선/파이/도넛/레이더/산점도 차트
+- **Gauge**: 게이지/미터기 시각화
+- **Watermark**: 반투명 워터마크 자동 삽입
+- **전자서명 필드**: 전자서명 영역 지정
+- **문서 번호 필드**: 자동 채번 번호 표시
+- **승인란**: 승인자 서명 및 날인 영역
+- **변경 이력**: 문서 변경 히스토리 자동 표시
+- **목차**: 다중 페이지 리포트 목차 자동 생성
+- **색인**: 키워드 색인 자동 생성
+- **각주/미주**: 참고 자료 삽입
+- **하이퍼링크**: 외부 링크 또는 페이지 내 링크
+- **북마크**: 특정 위치 마크 및 링크
+- **조건부 컴포넌트**: 데이터 조건에 따라 표시/숨김
+
+### 13.4 고급 기능
+
+#### 데이터 처리
+
+- **조건부 표시** (IF-THEN-ELSE): 데이터 값에 따른 조건부 렌더링
+- **반복 섹션** (DETAIL 쿼리 기반): 동적 행 반복
+- **계산 필드**: 런타임 계산 (price \* quantity)
+- **집계 함수**: SUM, AVG, COUNT, MIN, MAX, STDEV
+- **수식 필드**: 복잡한 수식 지원 (Excel 유사)
+- **룩업 함수**: 다른 데이터셋 참조
+- **문자열 함수**: CONCAT, SUBSTRING, REPLACE, FORMAT
+- **날짜 함수**: DATE_ADD, DATE_DIFF, DATE_FORMAT
+
+#### 레이아웃 고급 기능
+
+- **마스터 페이지**: 공통 헤더/푸터 템플릿
+- **섹션 구분**: Header/Body/Footer 섹션 분리
+- **컬럼 레이아웃**: 2단/3단 컬럼 지원
+- **플로팅 컴포넌트**: 고정 위치 컴포넌트 (페이지 상관없이)
+- **동적 높이**: 내용에 따라 자동 높이 조절
+- **페이지 번호/총 페이지 수**: 자동 계산
+- **현재 날짜/시간**: 출력 시점 시간 자동 삽입
+- **헤더/푸터 변수**: 챕터명, 섹션명 자동 표시
+
+#### 출력 고급 기능
+
+- **조건부 페이지 나누기**: 특정 조건에서만 페이지 분리
+- **그룹 단위 페이지 유지**: 그룹이 페이지 경계에서 분리 방지
+- **반복 헤더/푸터**: 페이지마다 헤더/푸터 반복
+- **챕터 구분**: 챕터별 페이지 번호 재시작
+- **대체 페이지 레이아웃**: 홀수/짝수 페이지 다른 레이아웃
+- **출력 형식별 최적화**: PDF/WORD/EXCEL 각각 최적화
+- **북마크 및 목차 자동 생성**: PDF 북마크 트리
+
+---
+
+## 14. 개발 가이드
+
+### 14.1 로컬 개발 환경
+
+```bash
+# Frontend
+cd frontend
+npm install
+npm run dev
+
+# Backend
+cd backend-node
+npm install
+npm run dev
+```
+
+### 14.2 환경 변수
+
+```env
+# Backend
+DATABASE_URL=postgresql://...
+PORT=3001
+
+# Frontend
+NEXT_PUBLIC_API_URL=http://localhost:3001
+```
+
+### 14.3 빌드 및 배포
+
+```bash
+# Frontend
+npm run build
+npm run start
+
+# Backend
+npm run build
+npm run start
+```
+
+---
+
+## 15. 구현 우선순위 및 로드맵
+
+### Phase 1: 현재 완료 ✅
+
+- 기본 리포트 디자이너
+- 컴포넌트 시스템
+- 쿼리 관리
+- 페이지 관리
+- 미리보기 및 출력
+
+### Phase 2: 기업 필수 기능 (우선순위 높음)
+
+1. **문서 번호 자동 채번 시스템**
+2. **문서 상태 관리** (작성중 → 검토 → 승인 → 완료)
+3. **기본 권한 관리** (조회/수정/삭제 권한)
+4. **감사 로그** (접근/수정/출력 이력)
+5. **승인 워크플로우** (단계별 승인)
+
+### Phase 3: 협업 및 보안 (우선순위 중간)
+
+1. **리포트 공유 기능**
+2. **댓글 및 리뷰 시스템**
+3. **민감 정보 마스킹**
+4. **전자서명 지원**
+5. **문서 보존 기간 관리**
+
+### Phase 4: 고급 기능 (우선순위 낮음)
+
+1. **모바일 지원**
+2. **대시보드 및 통계**
+3. **외부 시스템 연동** (이메일, 클라우드)
+4. **고급 컴포넌트** (차트, 바코드, QR코드)
+5. **성능 최적화** (캐싱, 큐잉)
+
+---
+
+## 16. 마무리
+
+### 현재 상태 📊
+
+현재 구현된 리포트 관리 시스템은 **기본적인 리포트 디자인, 쿼리 연동, 페이지 관리, 출력 기능**을 모두 갖추고 있습니다.
+
+사용자는 직관적인 **드래그 앤 드롭 인터페이스**로 리포트를 디자인하고, **데이터베이스 쿼리**를 연결하여 동적 리포트를 생성할 수 있으며, **다양한 형식(PDF, WORD)**으로 출력할 수 있습니다.
+
+### 기업 환경 적용을 위한 과제 🎯
+
+하지만 **기업 환경에서 실제로 사용**하기 위해서는 다음 기능들이 필수적으로 추가되어야 합니다:
+
+1. **문서 번호 관리**: 체계적인 문서 관리를 위한 자동 채번 시스템
+2. **승인 워크플로우**: 문서의 검토 및 승인 프로세스
+3. **권한 관리**: 부서별/역할별 세분화된 접근 권한
+4. **감사 로그**: 규정 준수를 위한 상세한 이력 관리
+5. **보안 강화**: 민감 정보 보호 및 데이터 보안
+
+### 향후 계획 🚀
+
+**Phase 2**의 기업 필수 기능부터 순차적으로 구현하여, 실제 기업 환경에서 안정적으로 사용할 수 있는 **엔터프라이즈급 리포트 관리 시스템**으로 발전시킬 계획입니다.
+
+특히 **문서 번호 자동 채번**, **승인 워크플로우**, **권한 관리**는 가장 먼저 구현되어야 할 핵심 기능으로 판단됩니다.
diff --git a/frontend/app/(main)/admin/mail/accounts/page.tsx b/frontend/app/(main)/admin/mail/accounts/page.tsx
index 0171f2b6..ca0cf0b9 100644
--- a/frontend/app/(main)/admin/mail/accounts/page.tsx
+++ b/frontend/app/(main)/admin/mail/accounts/page.tsx
@@ -10,6 +10,7 @@ import {
createMailAccount,
updateMailAccount,
deleteMailAccount,
+ testMailAccountConnection,
CreateMailAccountDto,
UpdateMailAccountDto,
} from "@/lib/api/mail";
@@ -104,6 +105,24 @@ export default function MailAccountsPage() {
}
};
+ const handleTestConnection = async (account: MailAccount) => {
+ try {
+ setLoading(true);
+ const result = await testMailAccountConnection(account.id);
+
+ if (result.success) {
+ alert(`✅ SMTP 연결 성공!\n\n${result.message || '정상적으로 연결되었습니다.'}`);
+ } else {
+ alert(`❌ SMTP 연결 실패\n\n${result.message || '연결에 실패했습니다.'}`);
+ }
+ } catch (error: any) {
+ console.error('연결 테스트 실패:', error);
+ alert(`❌ SMTP 연결 테스트 실패\n\n${error.message || '알 수 없는 오류가 발생했습니다.'}`);
+ } finally {
+ setLoading(false);
+ }
+ };
+
return (
@@ -148,6 +167,7 @@ export default function MailAccountsPage() {
onEdit={handleOpenEditModal}
onDelete={handleOpenDeleteModal}
onToggleStatus={handleToggleStatus}
+ onTestConnection={handleTestConnection}
/>
diff --git a/frontend/app/(main)/admin/mail/dashboard/page.tsx b/frontend/app/(main)/admin/mail/dashboard/page.tsx
index 1fa6a728..f2e737bc 100644
--- a/frontend/app/(main)/admin/mail/dashboard/page.tsx
+++ b/frontend/app/(main)/admin/mail/dashboard/page.tsx
@@ -14,6 +14,7 @@ import {
Calendar,
Clock
} from "lucide-react";
+import { getMailAccounts, getMailTemplates } from "@/lib/api/mail";
interface DashboardStats {
totalAccounts: number;
@@ -38,17 +39,15 @@ export default function MailDashboardPage() {
const loadStats = async () => {
setLoading(true);
try {
- // 계정 수
- const accountsRes = await fetch('/api/mail/accounts');
- const accountsData = await accountsRes.json();
+ // 계정 수 (apiClient를 통해 토큰 포함)
+ const accounts = await getMailAccounts();
- // 템플릿 수
- const templatesRes = await fetch('/api/mail/templates-file');
- const templatesData = await templatesRes.json();
+ // 템플릿 수 (apiClient를 통해 토큰 포함)
+ const templates = await getMailTemplates();
setStats({
- totalAccounts: accountsData.success ? accountsData.data.length : 0,
- totalTemplates: templatesData.success ? templatesData.data.length : 0,
+ totalAccounts: accounts.length,
+ totalTemplates: templates.length,
sentToday: 0, // TODO: 실제 발송 통계 API 연동
receivedToday: 0,
sentThisMonth: 0,
diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx
index 88dff27f..54da701b 100644
--- a/frontend/app/(main)/admin/screenMng/page.tsx
+++ b/frontend/app/(main)/admin/screenMng/page.tsx
@@ -83,7 +83,7 @@ export default function ScreenManagementPage() {
{stepConfig.list.title}
-
goToNextStep("design")}>
+ goToNextStep("design")}>
화면 설계하기
@@ -121,7 +121,7 @@ export default function ScreenManagementPage() {
이전 단계
-
goToStep("list")}>
+ goToStep("list")}>
목록으로 돌아가기
diff --git a/frontend/app/(main)/admin/validation-demo/page.tsx b/frontend/app/(main)/admin/validation-demo/page.tsx
index f84a06f5..2372c4ea 100644
--- a/frontend/app/(main)/admin/validation-demo/page.tsx
+++ b/frontend/app/(main)/admin/validation-demo/page.tsx
@@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: true,
style: {
labelFontSize: "14px",
- labelColor: "#3b83f6",
+ labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
@@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: true,
style: {
labelFontSize: "14px",
- labelColor: "#3b83f6",
+ labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
@@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [
},
style: {
labelFontSize: "14px",
- labelColor: "#3b83f6",
+ labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
@@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: false,
style: {
labelFontSize: "14px",
- labelColor: "#3b83f6",
+ labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
@@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [
required: false,
style: {
labelFontSize: "14px",
- labelColor: "#3b83f6",
+ labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
@@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [
},
style: {
labelFontSize: "14px",
- labelColor: "#3b83f6",
+ labelColor: "#212121",
labelFontWeight: "500",
},
} as WidgetComponent,
diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx
index 93111ba8..8b186bfa 100644
--- a/frontend/app/(main)/screens/[screenId]/page.tsx
+++ b/frontend/app/(main)/screens/[screenId]/page.tsx
@@ -148,7 +148,7 @@ export default function ScreenViewPage() {
const screenHeight = layout?.screenResolution?.height || 800;
return (
-
+
{layout && layout.components.length > 0 ? (
// 캔버스 컴포넌트들을 정확한 해상도로 표시
{layout.components
@@ -239,7 +238,7 @@ export default function ScreenViewPage() {
const labelText = component.style?.labelText || component.label || "";
const labelStyle = {
fontSize: component.style?.labelFontSize || "14px",
- color: component.style?.labelColor || "#3b83f6",
+ color: component.style?.labelColor || "#212121",
fontWeight: component.style?.labelFontWeight || "500",
backgroundColor: component.style?.labelBackgroundColor || "transparent",
padding: component.style?.labelPadding || "0",
@@ -379,7 +378,7 @@ export default function ScreenViewPage() {
) : (
// 빈 화면일 때도 깔끔하게 표시
= ({
// 파일 아이콘 가져오기
const getFileIcon = (fileName: string, size: number = 16) => {
const extension = fileName.split('.').pop()?.toLowerCase() || '';
- const iconProps = { size, className: "text-gray-600" };
+ const iconProps = { size, className: "text-muted-foreground" };
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) {
- return
;
+ return
;
}
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'].includes(extension)) {
return
;
@@ -71,7 +71,7 @@ export const GlobalFileViewer: React.FC
= ({
return ;
}
if (['txt', 'md', 'doc', 'docx', 'pdf', 'rtf'].includes(extension)) {
- return ;
+ return ;
}
return ;
};
@@ -272,7 +272,7 @@ export const GlobalFileViewer: React.FC = ({
variant="ghost"
size="sm"
onClick={() => handleRemove(file)}
- className="flex items-center gap-1 text-red-600 hover:text-red-700"
+ className="flex items-center gap-1 text-destructive hover:text-red-700"
>
diff --git a/frontend/components/admin/BatchJobModal.tsx b/frontend/components/admin/BatchJobModal.tsx
index 21eb8df9..e7f75f77 100644
--- a/frontend/components/admin/BatchJobModal.tsx
+++ b/frontend/components/admin/BatchJobModal.tsx
@@ -179,7 +179,7 @@ export default function BatchJobModal({
const getStatusColor = (status: string) => {
switch (status) {
case 'Y': return 'bg-green-100 text-green-800';
- case 'N': return 'bg-red-100 text-red-800';
+ case 'N': return 'bg-destructive/20 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
@@ -314,29 +314,29 @@ export default function BatchJobModal({
-
+
{formData.execution_count || 0}
-
총 실행 횟수
+
총 실행 횟수
{formData.success_count || 0}
-
성공 횟수
+
성공 횟수
-
+
{formData.failure_count || 0}
-
실패 횟수
+
실패 횟수
{formData.last_executed_at && (
-
+
마지막 실행: {new Date(formData.last_executed_at).toLocaleString()}
)}
diff --git a/frontend/components/admin/CategoryItem.tsx b/frontend/components/admin/CategoryItem.tsx
index 92b074b0..b5b34c17 100644
--- a/frontend/components/admin/CategoryItem.tsx
+++ b/frontend/components/admin/CategoryItem.tsx
@@ -55,7 +55,7 @@ export function CategoryItem({ category, isSelected, onSelect, onEdit, onDelete
"cursor-pointer transition-colors",
category.is_active === "Y"
? "bg-green-100 text-green-800 hover:bg-green-200 hover:text-green-900"
- : "bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-700",
+ : "bg-gray-100 text-muted-foreground hover:bg-gray-200 hover:text-gray-700",
updateCategoryMutation.isPending && "cursor-not-allowed opacity-50",
)}
onClick={(e) => {
@@ -71,7 +71,7 @@ export function CategoryItem({ category, isSelected, onSelect, onEdit, onDelete
{category.is_active === "Y" ? "활성" : "비활성"}
-
{category.category_code}
+
{category.category_code}
{category.description &&
{category.description}
}
diff --git a/frontend/components/admin/CodeCategoryFormModal.tsx b/frontend/components/admin/CodeCategoryFormModal.tsx
index 8a6fbd34..67fe8993 100644
--- a/frontend/components/admin/CodeCategoryFormModal.tsx
+++ b/frontend/components/admin/CodeCategoryFormModal.tsx
@@ -180,11 +180,11 @@ export function CodeCategoryFormModal({
{...createForm.register("categoryCode")}
disabled={isLoading}
placeholder="카테고리 코드를 입력하세요"
- className={createForm.formState.errors.categoryCode ? "border-red-500" : ""}
+ className={createForm.formState.errors.categoryCode ? "border-destructive" : ""}
onBlur={() => handleFieldBlur("categoryCode")}
/>
{createForm.formState.errors.categoryCode && (
-
{createForm.formState.errors.categoryCode.message}
+
{createForm.formState.errors.categoryCode.message}
)}
{!createForm.formState.errors.categoryCode && (
카테고리 코드
-
+
카테고리 코드는 수정할 수 없습니다.
)}
@@ -216,20 +216,20 @@ export function CodeCategoryFormModal({
className={
isEditing
? updateForm.formState.errors.categoryName
- ? "border-red-500"
+ ? "border-destructive"
: ""
: createForm.formState.errors.categoryName
- ? "border-red-500"
+ ? "border-destructive"
: ""
}
onBlur={() => handleFieldBlur("categoryName")}
/>
{isEditing
? updateForm.formState.errors.categoryName && (
- {updateForm.formState.errors.categoryName.message}
+ {updateForm.formState.errors.categoryName.message}
)
: createForm.formState.errors.categoryName && (
- {createForm.formState.errors.categoryName.message}
+ {createForm.formState.errors.categoryName.message}
)}
{!(isEditing ? updateForm.formState.errors.categoryName : createForm.formState.errors.categoryName) && (
handleFieldBlur("categoryNameEng")}
/>
{isEditing
? updateForm.formState.errors.categoryNameEng && (
- {updateForm.formState.errors.categoryNameEng.message}
+ {updateForm.formState.errors.categoryNameEng.message}
)
: createForm.formState.errors.categoryNameEng && (
- {createForm.formState.errors.categoryNameEng.message}
+ {createForm.formState.errors.categoryNameEng.message}
)}
{!(isEditing
? updateForm.formState.errors.categoryNameEng
@@ -289,20 +289,20 @@ export function CodeCategoryFormModal({
className={
isEditing
? updateForm.formState.errors.description
- ? "border-red-500"
+ ? "border-destructive"
: ""
: createForm.formState.errors.description
- ? "border-red-500"
+ ? "border-destructive"
: ""
}
onBlur={() => handleFieldBlur("description")}
/>
{isEditing
? updateForm.formState.errors.description && (
- {updateForm.formState.errors.description.message}
+ {updateForm.formState.errors.description.message}
)
: createForm.formState.errors.description && (
- {createForm.formState.errors.description.message}
+ {createForm.formState.errors.description.message}
)}
@@ -320,19 +320,19 @@ export function CodeCategoryFormModal({
className={
isEditing
? updateForm.formState.errors.sortOrder
- ? "border-red-500"
+ ? "border-destructive"
: ""
: createForm.formState.errors.sortOrder
- ? "border-red-500"
+ ? "border-destructive"
: ""
}
/>
{isEditing
? updateForm.formState.errors.sortOrder && (
-
{updateForm.formState.errors.sortOrder.message}
+
{updateForm.formState.errors.sortOrder.message}
)
: createForm.formState.errors.sortOrder && (
-
{createForm.formState.errors.sortOrder.message}
+
{createForm.formState.errors.sortOrder.message}
)}
diff --git a/frontend/components/admin/CodeCategoryPanel.tsx b/frontend/components/admin/CodeCategoryPanel.tsx
index 96f75d04..645193fc 100644
--- a/frontend/components/admin/CodeCategoryPanel.tsx
+++ b/frontend/components/admin/CodeCategoryPanel.tsx
@@ -82,7 +82,7 @@ export function CodeCategoryPanel({ selectedCategoryCode, onSelectCategory }: Co
return (
-
카테고리를 불러오는 중 오류가 발생했습니다.
+
카테고리를 불러오는 중 오류가 발생했습니다.
window.location.reload()} className="mt-2">
다시 시도
@@ -116,7 +116,7 @@ export function CodeCategoryPanel({ selectedCategoryCode, onSelectCategory }: Co
onChange={(e) => setShowActiveOnly(e.target.checked)}
className="rounded border-gray-300"
/>
-
+
활성 카테고리만 표시
diff --git a/frontend/components/admin/CodeDetailPanel.tsx b/frontend/components/admin/CodeDetailPanel.tsx
index 680f59f4..3389ad5b 100644
--- a/frontend/components/admin/CodeDetailPanel.tsx
+++ b/frontend/components/admin/CodeDetailPanel.tsx
@@ -121,7 +121,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
return (
-
코드를 불러오는 중 오류가 발생했습니다.
+
코드를 불러오는 중 오류가 발생했습니다.
window.location.reload()} className="mt-2">
다시 시도
@@ -155,7 +155,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
onChange={(e) => setShowActiveOnly(e.target.checked)}
className="rounded border-gray-300"
/>
-
+
활성 코드만 표시
@@ -221,13 +221,13 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
"transition-colors",
activeCode.isActive === "Y" || activeCode.is_active === "Y"
? "bg-green-100 text-green-800"
- : "bg-gray-100 text-gray-600",
+ : "bg-gray-100 text-muted-foreground",
)}
>
{activeCode.isActive === "Y" || activeCode.is_active === "Y" ? "활성" : "비활성"}
-
+
{activeCode.codeValue || activeCode.code_value}
{activeCode.description && (
diff --git a/frontend/components/admin/CodeFormModal.tsx b/frontend/components/admin/CodeFormModal.tsx
index 6e915904..26b617a4 100644
--- a/frontend/components/admin/CodeFormModal.tsx
+++ b/frontend/components/admin/CodeFormModal.tsx
@@ -168,7 +168,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
{...form.register("codeValue")}
disabled={isLoading || isEditing} // 수정 시에는 비활성화
placeholder="코드값을 입력하세요"
- className={(form.formState.errors as any)?.codeValue ? "border-red-500" : ""}
+ className={(form.formState.errors as any)?.codeValue ? "border-destructive" : ""}
onBlur={(e) => {
const value = e.target.value.trim();
if (value && !isEditing) {
@@ -180,7 +180,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
}}
/>
{(form.formState.errors as any)?.codeValue && (
-
{getErrorMessage((form.formState.errors as any)?.codeValue)}
+
{getErrorMessage((form.formState.errors as any)?.codeValue)}
)}
{!isEditing && !(form.formState.errors as any)?.codeValue && (
{
const value = e.target.value.trim();
if (value) {
@@ -211,7 +211,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
}}
/>
{form.formState.errors.codeName && (
- {getErrorMessage(form.formState.errors.codeName)}
+ {getErrorMessage(form.formState.errors.codeName)}
)}
{!form.formState.errors.codeName && (
{
const value = e.target.value.trim();
if (value) {
@@ -242,7 +242,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
}}
/>
{form.formState.errors.codeNameEng && (
- {getErrorMessage(form.formState.errors.codeNameEng)}
+ {getErrorMessage(form.formState.errors.codeNameEng)}
)}
{!form.formState.errors.codeNameEng && (
{form.formState.errors.description && (
- {getErrorMessage(form.formState.errors.description)}
+ {getErrorMessage(form.formState.errors.description)}
)}
@@ -278,10 +278,10 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
{...form.register("sortOrder", { valueAsNumber: true })}
disabled={isLoading}
min={1}
- className={form.formState.errors.sortOrder ? "border-red-500" : ""}
+ className={form.formState.errors.sortOrder ? "border-destructive" : ""}
/>
{form.formState.errors.sortOrder && (
-
{getErrorMessage(form.formState.errors.sortOrder)}
+
{getErrorMessage(form.formState.errors.sortOrder)}
)}
diff --git a/frontend/components/admin/ColumnDefinitionTable.tsx b/frontend/components/admin/ColumnDefinitionTable.tsx
index c58ed39b..74fd4f33 100644
--- a/frontend/components/admin/ColumnDefinitionTable.tsx
+++ b/frontend/components/admin/ColumnDefinitionTable.tsx
@@ -188,7 +188,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
const hasRowError = rowErrors.length > 0;
return (
-
+
{rowErrors.length > 0 && (
-
+
{rowErrors.map((error, i) => (
{error}
))}
diff --git a/frontend/components/admin/CreateTableModal.tsx b/frontend/components/admin/CreateTableModal.tsx
index 07dae653..7e075ad1 100644
--- a/frontend/components/admin/CreateTableModal.tsx
+++ b/frontend/components/admin/CreateTableModal.tsx
@@ -248,7 +248,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
placeholder="예: customer_info"
className={tableNameError ? "border-red-300" : ""}
/>
- {tableNameError &&
{tableNameError}
}
+ {tableNameError &&
{tableNameError}
}
영문자로 시작, 영문자/숫자/언더스코어만 사용 가능
diff --git a/frontend/components/admin/DDLLogViewer.tsx b/frontend/components/admin/DDLLogViewer.tsx
index c978f162..e0184f38 100644
--- a/frontend/components/admin/DDLLogViewer.tsx
+++ b/frontend/components/admin/DDLLogViewer.tsx
@@ -271,14 +271,14 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
{log.success ? (
) : (
-
+
)}
-
+
{log.success ? "성공" : "실패"}
{log.error_message && (
-
{log.error_message}
+
{log.error_message}
)}
@@ -325,7 +325,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
실패
- {statistics.failedExecutions}
+ {statistics.failedExecutions}
@@ -374,13 +374,13 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
{statistics.recentFailures.length > 0 && (
- 최근 실패 로그
+ 최근 실패 로그
최근 발생한 DDL 실행 실패 내역입니다.
{statistics.recentFailures.map((failure, index) => (
-
+
{failure.ddl_type}
@@ -390,7 +390,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
{format(new Date(failure.executed_at), "MM-dd HH:mm", { locale: ko })}
-
{failure.error_message}
+
{failure.error_message}
))}
diff --git a/frontend/components/admin/DiskUsageSummary.tsx b/frontend/components/admin/DiskUsageSummary.tsx
index 59af8455..096af5f8 100644
--- a/frontend/components/admin/DiskUsageSummary.tsx
+++ b/frontend/components/admin/DiskUsageSummary.tsx
@@ -120,7 +120,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
1000 ? "bg-red-500" : summary.totalSizeMB > 500 ? "bg-yellow-500" : "bg-green-500"
+ summary.totalSizeMB > 1000 ? "bg-destructive/100" : summary.totalSizeMB > 500 ? "bg-yellow-500" : "bg-green-500"
}`}
style={{
width: `${Math.min((summary.totalSizeMB / 2000) * 100, 100)}%`,
diff --git a/frontend/components/admin/ExternalDbConnectionModal.tsx b/frontend/components/admin/ExternalDbConnectionModal.tsx
index cb23768f..b9075bc8 100644
--- a/frontend/components/admin/ExternalDbConnectionModal.tsx
+++ b/frontend/components/admin/ExternalDbConnectionModal.tsx
@@ -450,7 +450,7 @@ export const ExternalDbConnectionModal: React.FC
className={`rounded-md border p-3 text-sm ${
testResult.success
? "border-green-200 bg-green-50 text-green-800"
- : "border-red-200 bg-red-50 text-red-800"
+ : "border-destructive/20 bg-destructive/10 text-red-800"
}`}
>
{testResult.success ? "✅ 연결 성공" : "❌ 연결 실패"}
@@ -469,7 +469,7 @@ export const ExternalDbConnectionModal: React.FC
{!testResult.success && testResult.error && (
오류 코드: {testResult.error.code}
- {testResult.error.details &&
{testResult.error.details}
}
+ {testResult.error.details &&
{testResult.error.details}
}
)}
diff --git a/frontend/components/admin/LayoutFormModal.tsx b/frontend/components/admin/LayoutFormModal.tsx
index 972caa7c..b2fe9804 100644
--- a/frontend/components/admin/LayoutFormModal.tsx
+++ b/frontend/components/admin/LayoutFormModal.tsx
@@ -238,10 +238,10 @@ export const LayoutFormModal: React.FC
= ({ open, onOpenCh
1
@@ -249,19 +249,19 @@ export const LayoutFormModal: React.FC
= ({ open, onOpenCh
-
+
3
@@ -304,13 +304,13 @@ export const LayoutFormModal: React.FC
= ({ open, onOpenCh
setFormData((prev) => ({ ...prev, category: category.id }))}
>
-
+
{category.name}
{category.description}
@@ -346,7 +346,7 @@ export const LayoutFormModal: React.FC
= ({ open, onOpenCh
setFormData((prev) => ({
@@ -362,7 +362,7 @@ export const LayoutFormModal: React.FC = ({ open, onOpenCh
{template.name}
{template.zones}개 영역
-
{template.description}
+
{template.description}
예: {template.example}
{template.icon}
@@ -427,7 +427,7 @@ export const LayoutFormModal: React.FC = ({ open, onOpenCh
{generationResult ? (
@@ -479,7 +479,7 @@ export const LayoutFormModal: React.FC = ({ open, onOpenCh
생성될 파일:
-
+
• {formData.name.toLowerCase()}/index.ts
• {formData.name.toLowerCase()}/{formData.name}Layout.tsx
diff --git a/frontend/components/admin/MenuFormModal.tsx b/frontend/components/admin/MenuFormModal.tsx
index 501a0b8d..f8d80592 100644
--- a/frontend/components/admin/MenuFormModal.tsx
+++ b/frontend/components/admin/MenuFormModal.tsx
@@ -826,10 +826,10 @@ export const MenuFormModal: React.FC = ({
{/* 선택된 화면 정보 표시 */}
{selectedScreen && (
-
+
{selectedScreen.screenName}
-
코드: {selectedScreen.screenCode}
-
생성된 URL: {formData.menuUrl}
+
코드: {selectedScreen.screenCode}
+
생성된 URL: {formData.menuUrl}
)}
diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx
index 81c94ae0..ab7ec016 100644
--- a/frontend/components/admin/MenuManagement.tsx
+++ b/frontend/components/admin/MenuManagement.tsx
@@ -828,7 +828,7 @@ export const MenuManagement: React.FC = () => {
handleMenuTypeChange("admin")}
>
@@ -836,7 +836,7 @@ export const MenuManagement: React.FC = () => {
{getUITextSync("menu.management.admin")}
-
+
{getUITextSync("menu.management.admin.description")}
@@ -849,7 +849,7 @@ export const MenuManagement: React.FC = () => {
handleMenuTypeChange("user")}
>
@@ -857,7 +857,7 @@ export const MenuManagement: React.FC = () => {
{getUITextSync("menu.management.user")}
-
+
{getUITextSync("menu.management.user.description")}
@@ -997,7 +997,7 @@ export const MenuManagement: React.FC = () => {
-
+
{getUITextSync("menu.list.search.result", { count: getCurrentMenus().length })}
@@ -1006,7 +1006,7 @@ export const MenuManagement: React.FC = () => {
-
+
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
diff --git a/frontend/components/admin/MenuTable.tsx b/frontend/components/admin/MenuTable.tsx
index 40b01fa3..5df95d81 100644
--- a/frontend/components/admin/MenuTable.tsx
+++ b/frontend/components/admin/MenuTable.tsx
@@ -67,7 +67,7 @@ export const MenuTable: React.FC = ({
const getLevelBadge = (level: number) => {
switch (level) {
case 0:
- return "bg-blue-100 text-blue-800";
+ return "bg-primary/20 text-blue-800";
case 1:
return "bg-green-100 text-green-800";
case 2:
@@ -239,7 +239,7 @@ export const MenuTable: React.FC = ({
{seq}
-
+
= ({
)}
-
+
{menuUrl ? (
30 ? "truncate" : ""
}`}
onClick={() => {
diff --git a/frontend/components/admin/MonitoringDashboard.tsx b/frontend/components/admin/MonitoringDashboard.tsx
index 43fc1819..500dd4fb 100644
--- a/frontend/components/admin/MonitoringDashboard.tsx
+++ b/frontend/components/admin/MonitoringDashboard.tsx
@@ -74,8 +74,8 @@ export default function MonitoringDashboard() {
const getStatusBadge = (status: string) => {
const variants = {
completed: "bg-green-100 text-green-800",
- failed: "bg-red-100 text-red-800",
- running: "bg-blue-100 text-blue-800",
+ failed: "bg-destructive/20 text-red-800",
+ running: "bg-primary/20 text-blue-800",
pending: "bg-yellow-100 text-yellow-800",
cancelled: "bg-gray-100 text-gray-800",
};
@@ -129,7 +129,7 @@ export default function MonitoringDashboard() {
variant="outline"
size="sm"
onClick={toggleAutoRefresh}
- className={autoRefresh ? "bg-blue-50 text-blue-600" : ""}
+ className={autoRefresh ? "bg-accent text-primary" : ""}
>
{autoRefresh ?
:
}
자동 새로고침
@@ -167,7 +167,7 @@ export default function MonitoringDashboard() {
🔄
- {monitoring.running_jobs}
+ {monitoring.running_jobs}
현재 실행 중인 작업
@@ -193,7 +193,7 @@ export default function MonitoringDashboard() {
❌
- {monitoring.failed_jobs_today}
+ {monitoring.failed_jobs_today}
주의가 필요한 작업
@@ -269,7 +269,7 @@ export default function MonitoringDashboard() {
{execution.error_message ? (
-
+
{execution.error_message}
) : (
diff --git a/frontend/components/admin/MultiLang.tsx b/frontend/components/admin/MultiLang.tsx
index 0d9c2a98..abdadcdb 100644
--- a/frontend/components/admin/MultiLang.tsx
+++ b/frontend/components/admin/MultiLang.tsx
@@ -673,7 +673,7 @@ export default function MultiLangPage() {
setActiveTab("keys")}
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
- activeTab === "keys" ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
+ activeTab === "keys" ? "bg-accent0 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
다국어 키 관리
@@ -681,7 +681,7 @@ export default function MultiLangPage() {
setActiveTab("languages")}
className={`rounded-t-lg px-3 py-1.5 text-sm font-medium transition-colors ${
- activeTab === "languages" ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
+ activeTab === "languages" ? "bg-accent0 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
언어 관리
@@ -698,7 +698,7 @@ export default function MultiLangPage() {
-
총 {languages.length}개의 언어가 등록되어 있습니다.
+
총 {languages.length}개의 언어가 등록되어 있습니다.
{selectedLanguages.size > 0 && (
@@ -759,13 +759,13 @@ export default function MultiLangPage() {
-
검색 결과: {getFilteredLangKeys().length}건
+
검색 결과: {getFilteredLangKeys().length}건
{/* 테이블 영역 */}
-
전체: {getFilteredLangKeys().length}건
+
전체: {getFilteredLangKeys().length}건
= ({ menus
(selectedMenu as any).menu_name_kor ||
"메뉴"}
-
+
URL: {selectedMenu.menu_url || selectedMenu.MENU_URL || (selectedMenu as any).menu_url || "없음"}
-
+
설명:{" "}
{selectedMenu.menu_desc || selectedMenu.MENU_DESC || (selectedMenu as any).menu_desc || "없음"}
@@ -294,7 +294,7 @@ export const ScreenAssignmentTab: React.FC = ({ menus
{screen.isActive === "Y" ? "활성" : "비활성"}
-
+
테이블: {screen.tableName} | 생성일: {screen.createdDate.toLocaleDateString()}
{screen.description && {screen.description}
}
@@ -306,7 +306,7 @@ export const ScreenAssignmentTab: React.FC = ({ menus
setSelectedScreen(screen);
setShowUnassignDialog(true);
}}
- className="text-red-600 hover:text-red-700"
+ className="text-destructive hover:text-red-700"
>
@@ -347,7 +347,7 @@ export const ScreenAssignmentTab: React.FC = ({ menus
setSelectedScreen(screen)}
>
@@ -357,7 +357,7 @@ export const ScreenAssignmentTab: React.FC = ({ menus
{screen.screenCode}
- 테이블: {screen.tableName}
+ 테이블: {screen.tableName}
))
)}
diff --git a/frontend/components/admin/SortableCodeItem.tsx b/frontend/components/admin/SortableCodeItem.tsx
index c3731be0..21d456c7 100644
--- a/frontend/components/admin/SortableCodeItem.tsx
+++ b/frontend/components/admin/SortableCodeItem.tsx
@@ -83,7 +83,7 @@ export function SortableCodeItem({
"cursor-pointer transition-colors",
code.isActive === "Y" || code.is_active === "Y"
? "bg-green-100 text-green-800 hover:bg-green-200 hover:text-green-900"
- : "bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-700",
+ : "bg-gray-100 text-muted-foreground hover:bg-gray-200 hover:text-gray-700",
updateCodeMutation.isPending && "cursor-not-allowed opacity-50",
)}
onClick={(e) => {
@@ -100,7 +100,7 @@ export function SortableCodeItem({
{code.isActive === "Y" || code.is_active === "Y" ? "활성" : "비활성"}
-
{code.codeValue || code.code_value}
+
{code.codeValue || code.code_value}
{code.description &&
{code.description}
}
diff --git a/frontend/components/admin/UserFormModal.tsx b/frontend/components/admin/UserFormModal.tsx
index 0e32a95c..8427fa00 100644
--- a/frontend/components/admin/UserFormModal.tsx
+++ b/frontend/components/admin/UserFormModal.tsx
@@ -24,9 +24,9 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
case "success":
return "text-green-600";
case "error":
- return "text-red-600";
+ return "text-destructive";
default:
- return "text-blue-600";
+ return "text-primary";
}
};
@@ -37,7 +37,7 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
{title}
@@ -398,7 +398,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
{/* 중복확인 결과 메시지 */}
{duplicateCheckMessage && (
{duplicateCheckMessage}
diff --git a/frontend/components/admin/UserPasswordResetModal.tsx b/frontend/components/admin/UserPasswordResetModal.tsx
index 46928bd3..086b1556 100644
--- a/frontend/components/admin/UserPasswordResetModal.tsx
+++ b/frontend/components/admin/UserPasswordResetModal.tsx
@@ -196,7 +196,7 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
{/* 비밀번호 일치 여부 표시 */}
- {showMismatchError && 비밀번호가 일치하지 않습니다.
}
+ {showMismatchError && 비밀번호가 일치하지 않습니다.
}
{isPasswordMatch && 비밀번호가 일치합니다.
}
diff --git a/frontend/components/admin/UserStatusConfirmDialog.tsx b/frontend/components/admin/UserStatusConfirmDialog.tsx
index 72ab1aa3..59a7c1df 100644
--- a/frontend/components/admin/UserStatusConfirmDialog.tsx
+++ b/frontend/components/admin/UserStatusConfirmDialog.tsx
@@ -33,8 +33,8 @@ export function UserStatusConfirmDialog({
const currentStatusText = USER_STATUS_LABELS[user.status as keyof typeof USER_STATUS_LABELS] || user.status;
const newStatusText = USER_STATUS_LABELS[newStatus as keyof typeof USER_STATUS_LABELS] || newStatus;
- const currentStatusColor = user.status === "active" ? "text-blue-600" : "text-gray-600";
- const newStatusColor = newStatus === "active" ? "text-blue-600" : "text-gray-600";
+ const currentStatusColor = user.status === "active" ? "text-primary" : "text-muted-foreground";
+ const newStatusColor = newStatus === "active" ? "text-primary" : "text-muted-foreground";
return (
!open && onCancel()}>
@@ -67,7 +67,7 @@ export function UserStatusConfirmDialog({
취소
-
+
변경
diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx
index d7074aec..f5180379 100644
--- a/frontend/components/admin/dashboard/CanvasElement.tsx
+++ b/frontend/components/admin/dashboard/CanvasElement.tsx
@@ -227,7 +227,7 @@ export function CanvasElement({ element, isSelected, onUpdate, onRemove, onSelec
onConfigure(element)}
@@ -240,7 +240,7 @@ export function CanvasElement({ element, isSelected, onUpdate, onRemove, onSelec
diff --git a/frontend/components/admin/dashboard/ChartConfigPanel.tsx b/frontend/components/admin/dashboard/ChartConfigPanel.tsx
index 5ba617a3..d67cfefb 100644
--- a/frontend/components/admin/dashboard/ChartConfigPanel.tsx
+++ b/frontend/components/admin/dashboard/ChartConfigPanel.tsx
@@ -225,7 +225,7 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC
{/* 설정 미리보기 */}
📋 설정 미리보기
-
+
X축: {currentConfig.xAxis || '미설정'}
Y축: {' '}
@@ -240,7 +240,7 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC
)}
데이터 행 수: {queryResult.rows.length}개
{Array.isArray(currentConfig.yAxis) && currentConfig.yAxis.length > 1 && (
-
+
✨ 다중 시리즈 차트가 생성됩니다!
)}
@@ -249,7 +249,7 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC
{/* 필수 필드 확인 */}
{(!currentConfig.xAxis || !currentConfig.yAxis) && (
-
+
⚠️ X축과 Y축을 모두 설정해야 차트가 표시됩니다.
diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx
index ed0b253f..0b15e59f 100644
--- a/frontend/components/admin/dashboard/DashboardCanvas.tsx
+++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx
@@ -75,7 +75,7 @@ export const DashboardCanvas = forwardRef
(
w-full min-h-full relative
bg-gray-100
bg-grid-pattern
- ${isDragOver ? 'bg-blue-50' : ''}
+ ${isDragOver ? 'bg-accent' : ''}
`}
style={{
backgroundImage: `
diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx
index e13209f1..60d03747 100644
--- a/frontend/components/admin/dashboard/DashboardDesigner.tsx
+++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx
@@ -207,7 +207,7 @@ export default function DashboardDesigner() {
return (
-
+
대시보드 로딩 중...
잠시만 기다려주세요
@@ -221,7 +221,7 @@ export default function DashboardDesigner() {
{/* 편집 중인 대시보드 표시 */}
{dashboardTitle && (
-
+
📝 편집 중: {dashboardTitle}
)}
diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx
index a10cbc7c..619f0dba 100644
--- a/frontend/components/admin/dashboard/DashboardSidebar.tsx
+++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx
@@ -31,7 +31,7 @@ export function DashboardSidebar() {
type="chart"
subtype="bar"
onDragStart={handleDragStart}
- className="border-l-4 border-blue-500"
+ className="border-l-4 border-primary"
/>
{element.title} 설정
-
+
데이터 소스와 차트 설정을 구성하세요
×
@@ -89,7 +89,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
className={`
px-6 py-3 text-sm font-medium border-b-2 transition-colors
${activeTab === 'query'
- ? 'border-blue-500 text-blue-600 bg-blue-50'
+ ? 'border-primary text-primary bg-accent'
: 'border-transparent text-gray-500 hover:text-gray-700'}
`}
>
@@ -100,7 +100,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
className={`
px-6 py-3 text-sm font-medium border-b-2 transition-colors
${activeTab === 'chart'
- ? 'border-blue-500 text-blue-600 bg-blue-50'
+ ? 'border-primary text-primary bg-accent'
: 'border-transparent text-gray-500 hover:text-gray-700'}
`}
>
@@ -147,7 +147,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
취소
@@ -155,7 +155,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
onClick={handleSave}
disabled={!dataSource.query || (!chartConfig.xAxis || !chartConfig.yAxis)}
className="
- px-4 py-2 bg-blue-500 text-white rounded-lg
+ px-4 py-2 bg-accent0 text-white rounded-lg
hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed
"
>
diff --git a/frontend/components/admin/dashboard/QueryEditor.tsx b/frontend/components/admin/dashboard/QueryEditor.tsx
index 671024cd..5aa70a80 100644
--- a/frontend/components/admin/dashboard/QueryEditor.tsx
+++ b/frontend/components/admin/dashboard/QueryEditor.tsx
@@ -153,7 +153,7 @@ ORDER BY Q4 DESC;`
onClick={executeQuery}
disabled={isExecuting || !query.trim()}
className="
- px-3 py-1 bg-blue-500 text-white rounded text-sm
+ px-3 py-1 bg-accent0 text-white rounded text-sm
hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed
flex items-center gap-1
"
@@ -172,10 +172,10 @@ ORDER BY Q4 DESC;`
{/* 샘플 쿼리 버튼들 */}
-
샘플 쿼리:
+
샘플 쿼리:
insertSampleQuery('comparison')}
- className="px-2 py-1 text-xs bg-blue-100 hover:bg-blue-200 rounded font-medium"
+ className="px-2 py-1 text-xs bg-primary/20 hover:bg-blue-200 rounded font-medium"
>
🔥 제품 비교
@@ -224,7 +224,7 @@ ORDER BY Q4 DESC;`
{/* 새로고침 간격 설정 */}
-
자동 새로고침:
+
자동 새로고침:
onDataSourceChange({
@@ -246,7 +246,7 @@ ORDER BY Q4 DESC;`
{/* 오류 메시지 */}
{error && (
-
+
@@ -282,7 +282,7 @@ ORDER BY Q4 DESC;`
{queryResult.rows.slice(0, 10).map((row, idx) => (
{queryResult.columns.map((col, colIdx) => (
-
+
{String(row[col] ?? '')}
))}
diff --git a/frontend/components/auth/AuthGuard.tsx b/frontend/components/auth/AuthGuard.tsx
index 4a47de35..a02dce73 100644
--- a/frontend/components/auth/AuthGuard.tsx
+++ b/frontend/components/auth/AuthGuard.tsx
@@ -115,7 +115,7 @@ export function AuthGuard({
console.log("AuthGuard: 로딩 중 - fallback 표시");
return (
-
+
AuthGuard 로딩 중...
{JSON.stringify(authDebugInfo, null, 2)}
@@ -129,10 +129,10 @@ export function AuthGuard({
console.log("AuthGuard: 인증 실패 - fallback 표시");
return (
-
+
인증 실패
{redirectCountdown !== null && (
-
+
리다이렉트 카운트다운: {redirectCountdown}초 후 {redirectTo}로 이동
)}
@@ -150,7 +150,7 @@ export function AuthGuard({
관리자 권한 없음
{redirectCountdown !== null && (
-
+
리다이렉트 카운트다운: {redirectCountdown}초 후 {redirectTo}로 이동
)}
diff --git a/frontend/components/auth/ErrorMessage.tsx b/frontend/components/auth/ErrorMessage.tsx
index 698e275e..039c2ccd 100644
--- a/frontend/components/auth/ErrorMessage.tsx
+++ b/frontend/components/auth/ErrorMessage.tsx
@@ -9,6 +9,6 @@ export function ErrorMessage({ message }: ErrorMessageProps) {
if (!message) return null;
return (
-
{message}
+
{message}
);
}
diff --git a/frontend/components/auth/LoginHeader.tsx b/frontend/components/auth/LoginHeader.tsx
index 0dfd959c..ea02f782 100644
--- a/frontend/components/auth/LoginHeader.tsx
+++ b/frontend/components/auth/LoginHeader.tsx
@@ -1,4 +1,4 @@
-import { Shield } from "lucide-react";
+import Image from "next/image";
import { UI_CONFIG } from "@/constants/auth";
/**
@@ -7,10 +7,16 @@ import { UI_CONFIG } from "@/constants/auth";
export function LoginHeader() {
return (
-
-
+
+
-
{UI_CONFIG.COMPANY_NAME}
);
}
diff --git a/frontend/components/common/DataTable.tsx b/frontend/components/common/DataTable.tsx
index ebb5086e..a1a3f03e 100644
--- a/frontend/components/common/DataTable.tsx
+++ b/frontend/components/common/DataTable.tsx
@@ -263,7 +263,7 @@ export const createStatusColumn = (accessorKey: string, header: string) => ({
? "bg-gray-50 text-gray-700"
: status === "pending" || status === "대기"
? "bg-yellow-50 text-yellow-700"
- : "bg-red-50 text-red-700",
+ : "bg-destructive/10 text-red-700",
)}
>
{status || "-"}
diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx
index 453ddfe8..bc483325 100644
--- a/frontend/components/common/ScreenModal.tsx
+++ b/frontend/components/common/ScreenModal.tsx
@@ -42,63 +42,36 @@ export const ScreenModal: React.FC
= ({ className }) => {
// 화면의 실제 크기 계산 함수
const calculateScreenDimensions = (components: ComponentData[]) => {
- let maxWidth = 800; // 최소 너비
- let maxHeight = 600; // 최소 높이
+ // 모든 컴포넌트의 경계 찾기
+ let minX = Infinity;
+ let minY = Infinity;
+ let maxX = 0;
+ let maxY = 0;
- console.log("🔍 화면 크기 계산 시작:", { componentsCount: components.length });
-
- components.forEach((component, index) => {
- // position과 size는 BaseComponent에서 별도 속성으로 관리
+ components.forEach((component) => {
const x = parseFloat(component.position?.x?.toString() || "0");
const y = parseFloat(component.position?.y?.toString() || "0");
const width = parseFloat(component.size?.width?.toString() || "100");
const height = parseFloat(component.size?.height?.toString() || "40");
- // 컴포넌트의 오른쪽 끝과 아래쪽 끝 계산
- const rightEdge = x + width;
- const bottomEdge = y + height;
-
- console.log(
- `📏 컴포넌트 ${index + 1} (${component.id}): x=${x}, y=${y}, w=${width}, h=${height}, rightEdge=${rightEdge}, bottomEdge=${bottomEdge}`,
- );
-
- const newMaxWidth = Math.max(maxWidth, rightEdge + 100); // 여백 증가
- const newMaxHeight = Math.max(maxHeight, bottomEdge + 100); // 여백 증가
-
- if (newMaxWidth > maxWidth || newMaxHeight > maxHeight) {
- console.log(`🔄 크기 업데이트: ${maxWidth}×${maxHeight} → ${newMaxWidth}×${newMaxHeight}`);
- maxWidth = newMaxWidth;
- maxHeight = newMaxHeight;
- }
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x + width);
+ maxY = Math.max(maxY, y + height);
});
- console.log("📊 컴포넌트 기반 계산 결과:", { maxWidth, maxHeight });
+ // 컨텐츠 실제 크기 + 넉넉한 여백 (양쪽 각 64px)
+ const contentWidth = maxX - minX;
+ const contentHeight = maxY - minY;
+ const padding = 128; // 좌우 또는 상하 합계 여백
- // 브라우저 크기 제한 확인 (더욱 관대하게 설정)
- const maxAllowedWidth = window.innerWidth * 0.98; // 95% -> 98%
- const maxAllowedHeight = window.innerHeight * 0.95; // 90% -> 95%
+ const finalWidth = Math.max(contentWidth + padding, 400); // 최소 400px
+ const finalHeight = Math.max(contentHeight + padding, 300); // 최소 300px
- console.log("📐 크기 제한 정보:", {
- 계산된크기: { maxWidth, maxHeight },
- 브라우저제한: { maxAllowedWidth, maxAllowedHeight },
- 브라우저크기: { width: window.innerWidth, height: window.innerHeight },
- });
-
- // 컴포넌트 기반 크기를 우선 적용하되, 브라우저 제한을 고려
- const finalDimensions = {
- width: Math.min(maxWidth, maxAllowedWidth),
- height: Math.min(maxHeight, maxAllowedHeight),
+ return {
+ width: Math.min(finalWidth, window.innerWidth * 0.98),
+ height: Math.min(finalHeight, window.innerHeight * 0.95),
};
-
- console.log("✅ 최종 화면 크기:", finalDimensions);
- console.log("🔧 크기 적용 분석:", {
- width적용: maxWidth <= maxAllowedWidth ? "컴포넌트기준" : "브라우저제한",
- height적용: maxHeight <= maxAllowedHeight ? "컴포넌트기준" : "브라우저제한",
- 컴포넌트크기: { maxWidth, maxHeight },
- 최종크기: finalDimensions,
- });
-
- return finalDimensions;
};
// 전역 모달 이벤트 리스너
@@ -113,10 +86,24 @@ export const ScreenModal: React.FC = ({ className }) => {
});
};
+ const handleCloseModal = () => {
+ console.log("🚪 ScreenModal 닫기 이벤트 수신");
+ setModalState({
+ isOpen: false,
+ screenId: null,
+ title: "",
+ size: "md",
+ });
+ setScreenData(null);
+ setFormData({});
+ };
+
window.addEventListener("openScreenModal", handleOpenModal as EventListener);
+ window.addEventListener("closeSaveModal", handleCloseModal);
return () => {
window.removeEventListener("openScreenModal", handleOpenModal as EventListener);
+ window.removeEventListener("closeSaveModal", handleCloseModal);
};
}, []);
@@ -190,17 +177,17 @@ export const ScreenModal: React.FC = ({ className }) => {
};
}
- // 헤더 높이와 패딩을 고려한 전체 높이 계산 (실제 측정값 기반)
- const headerHeight = 80; // DialogHeader + 패딩 (더 정확한 값)
+ // 헤더 높이만 고려 (패딩 제거)
+ const headerHeight = 73; // DialogHeader 실제 높이 (border-b px-6 py-4 포함)
const totalHeight = screenDimensions.height + headerHeight;
return {
className: "overflow-hidden p-0",
style: {
- width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 브라우저 제한 적용
- height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 브라우저 제한 적용
- maxWidth: "98vw", // 안전장치
- maxHeight: "95vh", // 안전장치
+ width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`, // 화면 크기 그대로
+ height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 헤더 + 화면 높이
+ maxWidth: "98vw",
+ maxHeight: "95vh",
},
};
};
@@ -215,12 +202,12 @@ export const ScreenModal: React.FC = ({ className }) => {
{loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."}
-
+
{loading ? (
-
화면을 불러오는 중...
+
화면을 불러오는 중...
) : screenData ? (
@@ -229,6 +216,9 @@ export const ScreenModal: React.FC
= ({ className }) => {
style={{
width: screenDimensions?.width || 800,
height: screenDimensions?.height || 600,
+ transformOrigin: 'center center',
+ maxWidth: '100%',
+ maxHeight: '100%',
}}
>
{screenData.components.map((component) => (
@@ -258,7 +248,7 @@ export const ScreenModal: React.FC = ({ className }) => {
) : (
-
화면 데이터가 없습니다.
+
화면 데이터가 없습니다.
)}
diff --git a/frontend/components/common/ValidationMessage.tsx b/frontend/components/common/ValidationMessage.tsx
index b13cddd3..b46653b7 100644
--- a/frontend/components/common/ValidationMessage.tsx
+++ b/frontend/components/common/ValidationMessage.tsx
@@ -18,6 +18,6 @@ export function ValidationMessage({ message, isValid, isLoading, className }: Va
}
return (
- {message}
+ {message}
);
}
diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx
index b745da3d..da75a511 100644
--- a/frontend/components/dashboard/DashboardViewer.tsx
+++ b/frontend/components/dashboard/DashboardViewer.tsx
@@ -105,10 +105,10 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
return (
{/* 새로고침 상태 표시 */}
-
+
마지막 업데이트: {lastRefresh.toLocaleTimeString()}
{Array.from(loadingElements).length > 0 && (
-
+
({Array.from(loadingElements).length}개 로딩 중...)
)}
@@ -164,7 +164,7 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
{isLoading ? (
@@ -203,8 +203,8 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
{isLoading && (
-
-
업데이트 중...
+
+
업데이트 중...
)}
diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx
index 349b72ec..7f05d91a 100644
--- a/frontend/components/dataflow/DataFlowList.tsx
+++ b/frontend/components/dataflow/DataFlowList.tsx
@@ -244,7 +244,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
-
+
{new Date(diagram.updatedAt).toLocaleDateString()}
@@ -269,7 +269,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
복사
-
handleDelete(diagram)} className="text-red-600">
+ handleDelete(diagram)} className="text-destructive">
삭제
@@ -302,7 +302,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
>
이전
-
+
{currentPage} / {totalPages}
- 관계 삭제
+ 관계 삭제
“{selectedDiagramForAction?.diagramName}” 관계를 완전히 삭제하시겠습니까?
-
+
이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다.
diff --git a/frontend/components/dataflow/DataFlowSidebar.tsx b/frontend/components/dataflow/DataFlowSidebar.tsx
index de757462..f38f4c3a 100644
--- a/frontend/components/dataflow/DataFlowSidebar.tsx
+++ b/frontend/components/dataflow/DataFlowSidebar.tsx
@@ -54,7 +54,7 @@ export const DataFlowSidebar: React.FC = ({
🗑️ 전체 삭제
@@ -72,7 +72,7 @@ export const DataFlowSidebar: React.FC = ({
{/* 통계 정보 */}
통계
-
+
테이블 노드:
{nodes.length}개
diff --git a/frontend/components/dataflow/EdgeInfoPanel.tsx b/frontend/components/dataflow/EdgeInfoPanel.tsx
index ad9be50e..2fcfffd6 100644
--- a/frontend/components/dataflow/EdgeInfoPanel.tsx
+++ b/frontend/components/dataflow/EdgeInfoPanel.tsx
@@ -85,11 +85,11 @@ export const EdgeInfoPanel: React.FC
= ({
{/* 관계 화살표 */}
- →
+ →
{/* To 테이블 */}
-
+
TO
{edgeInfo.toTable}
@@ -97,7 +97,7 @@ export const EdgeInfoPanel: React.FC
= ({
{edgeInfo.toColumns.map((column, index) => (
{column}
diff --git a/frontend/components/dataflow/RelationshipListModal.tsx b/frontend/components/dataflow/RelationshipListModal.tsx
index 6df6c17b..fbbc445b 100644
--- a/frontend/components/dataflow/RelationshipListModal.tsx
+++ b/frontend/components/dataflow/RelationshipListModal.tsx
@@ -134,18 +134,18 @@ export const RelationshipListModal: React.FC = ({
};
return (
-
+
{/* 헤더 */}
-
@@ -159,7 +159,7 @@ export const RelationshipListModal: React.FC = ({
{relationships.map((relationship) => (
@@ -172,7 +172,7 @@ export const RelationshipListModal: React.FC = ({
e.stopPropagation();
handleEdit(relationship);
}}
- className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-blue-100 hover:text-blue-600"
+ className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-primary/20 hover:text-primary"
title="관계 편집"
>
@@ -190,7 +190,7 @@ export const RelationshipListModal: React.FC = ({
e.stopPropagation();
handleDelete(relationship);
}}
- className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-red-100 hover:text-red-600"
+ className="flex h-6 w-6 items-center justify-center rounded text-gray-400 hover:bg-destructive/20 hover:text-destructive"
title="관계 삭제"
>
@@ -204,7 +204,7 @@ export const RelationshipListModal: React.FC = ({
-
+
타입: {relationship.connectionType}
From: {relationship.fromTable}
To: {relationship.toTable}
diff --git a/frontend/components/dataflow/SaveDiagramModal.tsx b/frontend/components/dataflow/SaveDiagramModal.tsx
index 02ae7f99..4f87f69e 100644
--- a/frontend/components/dataflow/SaveDiagramModal.tsx
+++ b/frontend/components/dataflow/SaveDiagramModal.tsx
@@ -149,26 +149,26 @@ const SaveDiagramModal: React.FC
= ({
onKeyPress={handleKeyPress}
placeholder="예: 사용자-부서 관계도"
disabled={isLoading}
- className={nameError ? "border-red-500 focus:border-red-500" : ""}
+ className={nameError ? "border-destructive focus:border-destructive" : ""}
/>
- {nameError && {nameError}
}
+ {nameError && {nameError}
}
{/* 관계 요약 정보 */}
-
{relationships.length}
-
관계 수
+
{relationships.length}
+
관계 수
{connectedTables.length}
-
연결된 테이블
+
연결된 테이블
{relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)}
-
연결된 컬럼
+
연결된 컬럼
@@ -212,7 +212,7 @@ const SaveDiagramModal: React.FC
= ({
{relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`}
-
+
{relationship.fromTable} → {relationship.toTable}
diff --git a/frontend/components/dataflow/SelectedTablesPanel.tsx b/frontend/components/dataflow/SelectedTablesPanel.tsx
index 7788ffda..9f1f1c55 100644
--- a/frontend/components/dataflow/SelectedTablesPanel.tsx
+++ b/frontend/components/dataflow/SelectedTablesPanel.tsx
@@ -24,12 +24,12 @@ export const SelectedTablesPanel: React.FC = ({
canCreateConnection,
}) => {
return (
-
+
{/* 헤더 */}
-
-
📋
+
+ 📋
선택된 테이블
@@ -44,7 +44,7 @@ export const SelectedTablesPanel: React.FC
= ({
✕
@@ -66,8 +66,8 @@ export const SelectedTablesPanel: React.FC
= ({
index === 0
? "border-l-4 border-emerald-400 bg-emerald-50"
: index === 1
- ? "border-l-4 border-blue-400 bg-blue-50"
- : "bg-gray-50"
+ ? "border-l-4 border-blue-400 bg-accent"
+ : "bg-muted"
}`}
>
@@ -88,7 +88,7 @@ export const SelectedTablesPanel: React.FC = ({
)}
-
{tableName}
+
{tableName}
{/* 연결 화살표 (마지막이 아닌 경우) */}
@@ -110,7 +110,7 @@ export const SelectedTablesPanel: React.FC
= ({
disabled={!canCreateConnection}
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
canCreateConnection
- ? "bg-blue-500 text-white hover:bg-blue-600"
+ ? "bg-accent0 text-white hover:bg-blue-600"
: "cursor-not-allowed bg-gray-300 text-gray-500"
}`}
>
@@ -119,7 +119,7 @@ export const SelectedTablesPanel: React.FC = ({
🗑️
초기화
diff --git a/frontend/components/dataflow/TableNode.tsx b/frontend/components/dataflow/TableNode.tsx
index 67fdf8b0..6837efc0 100644
--- a/frontend/components/dataflow/TableNode.tsx
+++ b/frontend/components/dataflow/TableNode.tsx
@@ -56,7 +56,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
onColumnClick(table.tableName, columnKey)}
>
diff --git a/frontend/components/dataflow/TableSelector.tsx b/frontend/components/dataflow/TableSelector.tsx
index 86fc676f..d2aa68ac 100644
--- a/frontend/components/dataflow/TableSelector.tsx
+++ b/frontend/components/dataflow/TableSelector.tsx
@@ -93,7 +93,7 @@ export const TableSelector: React.FC
= ({ companyCode, onTab
{/* 오류 메시지 */}
- {error && {error}
}
+ {error && {error}
}
{/* 테이블 목록 */}
@@ -114,7 +114,7 @@ export const TableSelector: React.FC
= ({ companyCode, onTab
!isSelected && handleAddTable(table)}
>
@@ -126,10 +126,10 @@ export const TableSelector: React.FC = ({ companyCode, onTab
-
+
{table.tableName}
- {isSelected && (추가됨) }
+ {isSelected && (추가됨) }
{table.description &&
{table.description}
}
@@ -142,7 +142,7 @@ export const TableSelector: React.FC
= ({ companyCode, onTab
{/* 통계 정보 */}
-
+
전체 테이블: {tables.length}개
{searchTerm &&
검색 결과: {filteredTables.length}개 }
diff --git a/frontend/components/dataflow/condition/ConditionRenderer.tsx b/frontend/components/dataflow/condition/ConditionRenderer.tsx
index 0b58bc02..4cb5b915 100644
--- a/frontend/components/dataflow/condition/ConditionRenderer.tsx
+++ b/frontend/components/dataflow/condition/ConditionRenderer.tsx
@@ -81,7 +81,7 @@ export const ConditionRenderer: React.FC
= ({
value={condition.logicalOperator || "AND"}
onValueChange={(value: "AND" | "OR") => onUpdateCondition(index, "logicalOperator", value)}
>
-
+
@@ -92,11 +92,11 @@ export const ConditionRenderer: React.FC = ({
)}
{/* 그룹 레벨에 따른 들여쓰기 */}
-
(
-
그룹 시작
+
(
+
그룹 시작
onRemoveCondition(index)} className="h-6 w-6 p-0">
@@ -110,11 +110,11 @@ export const ConditionRenderer: React.FC
= ({
return (
-
)
-
그룹 끝
+
)
+
그룹 끝
onRemoveCondition(index)} className="h-6 w-6 p-0">
@@ -126,7 +126,7 @@ export const ConditionRenderer: React.FC
= ({
value={conditions[index + 1]?.logicalOperator || "AND"}
onValueChange={(value: "AND" | "OR") => onUpdateCondition(index + 1, "logicalOperator", value)}
>
-
+
@@ -150,7 +150,7 @@ export const ConditionRenderer: React.FC = ({
value={condition.logicalOperator || "AND"}
onValueChange={(value: "AND" | "OR") => onUpdateCondition(index, "logicalOperator", value)}
>
-
+
diff --git a/frontend/components/dataflow/condition/WebTypeInput.tsx b/frontend/components/dataflow/condition/WebTypeInput.tsx
index 55c4310f..d92e663f 100644
--- a/frontend/components/dataflow/condition/WebTypeInput.tsx
+++ b/frontend/components/dataflow/condition/WebTypeInput.tsx
@@ -400,7 +400,7 @@ export const WebTypeInput: React.FC = ({
multiple={detailSettings.multiple as boolean}
/>
{value && (
-
+
선택된 파일: {value}
diff --git a/frontend/components/dataflow/connection/ActionConditionsSection.tsx b/frontend/components/dataflow/connection/ActionConditionsSection.tsx
index ff498dc0..b22391ba 100644
--- a/frontend/components/dataflow/connection/ActionConditionsSection.tsx
+++ b/frontend/components/dataflow/connection/ActionConditionsSection.tsx
@@ -84,24 +84,24 @@ export const ActionConditionsSection: React.FC
= (
🔍 이 액션의 실행 조건
{isConditionRequired ? (
- 필수
+ 필수
) : (
(선택사항)
)}
{action.conditions && action.conditions.length > 0 && (
-
+
{action.conditions.length}개
)}
{isConditionRequired && !hasValidConditions && (
- ⚠️ 조건 필요
+ ⚠️ 조건 필요
)}
{action.conditions && action.conditions.length > 0 && (
@@ -151,8 +151,8 @@ export const ActionConditionsSection: React.FC = (
{isConditionRequired ? (
diff --git a/frontend/components/dataflow/connection/ActionFieldMappings.tsx b/frontend/components/dataflow/connection/ActionFieldMappings.tsx
index 86034b05..91e5e1c9 100644
--- a/frontend/components/dataflow/connection/ActionFieldMappings.tsx
+++ b/frontend/components/dataflow/connection/ActionFieldMappings.tsx
@@ -228,7 +228,7 @@ export const ActionFieldMappings: React.FC
= ({
필드 매핑
- (필수)
+ (필수)
@@ -244,7 +244,7 @@ export const ActionFieldMappings: React.FC = ({
{/* 컴팩트한 매핑 표시 */}
{/* 소스 */}
-
+
{
@@ -277,7 +277,7 @@ export const ActionFieldMappings: React.FC = ({
updateFieldMapping(mappingIndex, "sourceTable", "");
updateFieldMapping(mappingIndex, "sourceField", "");
}}
- className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-gray-400 hover:bg-gray-200 hover:text-gray-600"
+ className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-gray-400 hover:bg-gray-200 hover:text-muted-foreground"
title="소스 테이블 지우기"
>
×
@@ -390,7 +390,7 @@ export const ActionFieldMappings: React.FC = ({
{/* 필드 매핑이 없을 때 안내 메시지 */}
{action.fieldMappings.length === 0 && (
-
+
⚠️
diff --git a/frontend/components/dataflow/connection/ColumnTableSection.tsx b/frontend/components/dataflow/connection/ColumnTableSection.tsx
index b34b768c..631403b0 100644
--- a/frontend/components/dataflow/connection/ColumnTableSection.tsx
+++ b/frontend/components/dataflow/connection/ColumnTableSection.tsx
@@ -190,7 +190,7 @@ export const ColumnTableSection: React.FC
= ({
: isMapped
? "bg-gray-100 text-gray-700"
: oppositeSelectedColumn && !isTypeCompatible
- ? "cursor-not-allowed bg-red-50 text-red-400 opacity-60"
+ ? "cursor-not-allowed bg-destructive/10 text-red-400 opacity-60"
: isClickable
? "cursor-pointer hover:bg-gray-50"
: "cursor-not-allowed bg-gray-100 text-gray-400"
@@ -250,7 +250,7 @@ export const ColumnTableSection: React.FC = ({
: hasDefaultValue
? "bg-gray-100"
: oppositeSelectedColumn && !isTypeCompatible
- ? "bg-red-50 opacity-60"
+ ? "bg-destructive/10 opacity-60"
: "bg-white"
}`}
>
@@ -292,7 +292,7 @@ export const ColumnTableSection: React.FC = ({
{isMapped && (
- ← {mapping.fromColumnName}
+ ← {mapping.fromColumnName}
{
e.stopPropagation();
@@ -327,7 +327,7 @@ export const ColumnTableSection: React.FC = ({
{/* 하단 통계 */}
-
+
{isFromTable ? "매핑됨" : "설정됨"}: {mappedCount}/{columns.length}
diff --git a/frontend/components/dataflow/connection/ConnectionTypeSelector.tsx b/frontend/components/dataflow/connection/ConnectionTypeSelector.tsx
index 1959582a..b4218bde 100644
--- a/frontend/components/dataflow/connection/ConnectionTypeSelector.tsx
+++ b/frontend/components/dataflow/connection/ConnectionTypeSelector.tsx
@@ -18,14 +18,14 @@ export const ConnectionTypeSelector: React.FC = ({
onConfigChange({ ...config, connectionType: "simple-key" })}
>
단순 키값 연결
-
중계 테이블 생성
+
중계 테이블 생성
= ({
>
데이터 저장
-
필드 매핑 저장
+
필드 매핑 저장
= ({
>
외부 호출
-
API/이메일 호출
+
API/이메일 호출
diff --git a/frontend/components/dataflow/connection/DeleteConditionPanel.tsx b/frontend/components/dataflow/connection/DeleteConditionPanel.tsx
index 7e60c9f9..d1468d64 100644
--- a/frontend/components/dataflow/connection/DeleteConditionPanel.tsx
+++ b/frontend/components/dataflow/connection/DeleteConditionPanel.tsx
@@ -299,7 +299,7 @@ export const DeleteConditionPanel: React.FC
= ({
=
- !=
+ !=
>
<
@@ -308,11 +308,11 @@ export const DeleteConditionPanel: React.FC = ({
LIKE
IN
- NOT IN
+ NOT IN
EXISTS
- NOT EXISTS
+ NOT EXISTS
diff --git a/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx b/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx
index e55e23a6..b5b33362 100644
--- a/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx
+++ b/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx
@@ -446,9 +446,9 @@ export const InsertFieldMappingPanel: React.FC = (
매핑 진행 상황
-
+
총 {toTableColumns.length}개 컬럼 중{" "}
-
+
{columnMappings.filter((m) => m.fromColumnName || (m.defaultValue && m.defaultValue.trim())).length}
개
{" "}
diff --git a/frontend/components/dataflow/connection/SimpleKeySettings.tsx b/frontend/components/dataflow/connection/SimpleKeySettings.tsx
index 439d9e46..018fbb06 100644
--- a/frontend/components/dataflow/connection/SimpleKeySettings.tsx
+++ b/frontend/components/dataflow/connection/SimpleKeySettings.tsx
@@ -44,7 +44,7 @@ export const SimpleKeySettings: React.FC = ({
{/* 현재 선택된 테이블 표시 */}
-
From 테이블
+
From 테이블
{availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable}
@@ -54,7 +54,7 @@ export const SimpleKeySettings: React.FC = ({
-
To 테이블
+
To 테이블
{availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable}
@@ -67,7 +67,7 @@ export const SimpleKeySettings: React.FC = ({
{/* 컬럼 선택 */}
-
From 컬럼
+
From 컬럼
{fromTableColumns.map((column) => (
@@ -100,7 +100,7 @@ export const SimpleKeySettings: React.FC = ({
-
To 컬럼
+
To 컬럼
{toTableColumns.map((column) => (
@@ -137,7 +137,7 @@ export const SimpleKeySettings: React.FC = ({
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
-
선택된 From 컬럼
+
선택된 From 컬럼
{selectedFromColumns.length > 0 ? (
selectedFromColumns.map((column) => {
@@ -156,7 +156,7 @@ export const SimpleKeySettings: React.FC = ({
-
선택된 To 컬럼
+
선택된 To 컬럼
{selectedToColumns.length > 0 ? (
selectedToColumns.map((column) => {
@@ -178,7 +178,7 @@ export const SimpleKeySettings: React.FC = ({
{/* 단순 키값 연결 설정 */}
-
+
단순 키값 연결 설정
diff --git a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx
index 860724f2..cec7c238 100644
--- a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx
+++ b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx
@@ -37,7 +37,7 @@ export const DataConnectionDesigner: React.FC = () => {
🎨 제어관리 - 데이터 연결 설정
-
+
시각적 필드 매핑으로 데이터 연결을 쉽게 설정하세요
diff --git a/frontend/components/dataflow/connection/redesigned/LeftPanel/AdvancedSettings.tsx b/frontend/components/dataflow/connection/redesigned/LeftPanel/AdvancedSettings.tsx
index 12dc4046..fa2cc2ad 100644
--- a/frontend/components/dataflow/connection/redesigned/LeftPanel/AdvancedSettings.tsx
+++ b/frontend/components/dataflow/connection/redesigned/LeftPanel/AdvancedSettings.tsx
@@ -51,7 +51,7 @@ const AdvancedSettings: React.FC
= ({ connectionType }) =
<>
{/* 트랜잭션 설정 - 컴팩트 */}
-
🔄 트랜잭션 설정
+
🔄 트랜잭션 설정
@@ -98,7 +98,7 @@ const AdvancedSettings: React.FC = ({ connectionType }) =
<>
{/* API 호출 설정 - 컴팩트 */}
-
🌐 API 호출 설정
+
🌐 API 호출 설정
@@ -131,7 +131,7 @@ const AdvancedSettings: React.FC = ({ connectionType }) =
{/* 로깅 설정 - 컴팩트 */}
-
📝 로깅 설정
+
📝 로깅 설정
handleSettingChange("logLevel", value)}>
diff --git a/frontend/components/dataflow/connection/redesigned/LeftPanel/ConnectionTypeSelector.tsx b/frontend/components/dataflow/connection/redesigned/LeftPanel/ConnectionTypeSelector.tsx
index 0d5aa5e4..8c704beb 100644
--- a/frontend/components/dataflow/connection/redesigned/LeftPanel/ConnectionTypeSelector.tsx
+++ b/frontend/components/dataflow/connection/redesigned/LeftPanel/ConnectionTypeSelector.tsx
@@ -49,13 +49,13 @@ export const ConnectionTypeSelector: React.FC = ({
{type.icon}
{type.label}
-
{type.description}
+
{type.description}
diff --git a/frontend/components/dataflow/connection/redesigned/LeftPanel/LeftPanel.tsx b/frontend/components/dataflow/connection/redesigned/LeftPanel/LeftPanel.tsx
index e574fefa..be62206b 100644
--- a/frontend/components/dataflow/connection/redesigned/LeftPanel/LeftPanel.tsx
+++ b/frontend/components/dataflow/connection/redesigned/LeftPanel/LeftPanel.tsx
@@ -88,9 +88,9 @@ const LeftPanel: React.FC = ({ state, actions }) => {
{state.connectionType === "external_call" && (
<>
-
+
외부 호출 모드
-
우측 패널에서 REST API 설정을 구성하세요.
+
우측 패널에서 REST API 설정을 구성하세요.
>
)}
diff --git a/frontend/components/dataflow/connection/redesigned/LeftPanel/MappingInfoPanel.tsx b/frontend/components/dataflow/connection/redesigned/LeftPanel/MappingInfoPanel.tsx
index 92b79908..0618bcc6 100644
--- a/frontend/components/dataflow/connection/redesigned/LeftPanel/MappingInfoPanel.tsx
+++ b/frontend/components/dataflow/connection/redesigned/LeftPanel/MappingInfoPanel.tsx
@@ -35,9 +35,9 @@ export const MappingInfoPanel: React.FC
= ({
-
+
-
+
오류 매핑
@@ -45,9 +45,9 @@ export const MappingInfoPanel: React.FC = ({
-
+
-
+
총 매핑
@@ -88,7 +88,7 @@ export const MappingInfoPanel: React.FC = ({
? "border-orange-500 bg-orange-50"
: mapping.isValid
? "border-green-200 bg-green-50 hover:border-green-300"
- : "border-red-200 bg-red-50 hover:border-red-300"
+ : "border-destructive/20 bg-destructive/10 hover:border-red-300"
}`}
onClick={() => onMappingSelect(mapping.id)}
>
@@ -126,13 +126,13 @@ export const MappingInfoPanel: React.FC = ({
{mapping.isValid ? (
) : (
-
+
)}
{mapping.validationMessage && (
-
+
{mapping.validationMessage}
)}
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfig/ActionConditionBuilder.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfig/ActionConditionBuilder.tsx
index 85c0656c..7b13e38c 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfig/ActionConditionBuilder.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfig/ActionConditionBuilder.tsx
@@ -273,7 +273,7 @@ const ActionConditionBuilder: React.FC
= ({
.map((column) => (
-
📤
+
📤
{column.displayName || column.columnName}
{column.webType || column.dataType}
@@ -359,7 +359,7 @@ const ActionConditionBuilder: React.FC = ({
{/* 선택된 날짜 타입에 대한 설명 */}
{mapping.value?.startsWith("#") && mapping.value !== "#custom" && (
-
+
{mapping.value === "#NOW" && "⏰ 현재 날짜와 시간이 저장됩니다"}
{mapping.value === "#TODAY" && "📅 현재 날짜 (00:00:00)가 저장됩니다"}
{mapping.value === "#YESTERDAY" && "📅 어제 날짜가 저장됩니다"}
@@ -497,7 +497,7 @@ const ActionConditionBuilder: React.FC
= ({
.map((column) => (
- 📤
+ 📤
{column.displayName || column.columnName}
@@ -625,7 +625,7 @@ const ActionConditionBuilder: React.FC = ({
.map((column) => (
- 📤
+ 📤
{column.displayName || column.columnName}
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/ConnectionStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/ConnectionStep.tsx
index a26c9a0f..11b59d15 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/ConnectionStep.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/ConnectionStep.tsx
@@ -80,7 +80,7 @@ export const ConnectionStep: React.FC = ({
연결 선택
-
+
데이터를 가져올 연결과 저장할 연결을 선택하세요
@@ -89,8 +89,8 @@ export const ConnectionStep: React.FC
= ({
{/* FROM 연결 */}
-
-
1
+
+ 1
FROM 연결
(데이터 소스)
@@ -102,20 +102,20 @@ export const ConnectionStep: React.FC
= ({
key={connection.id}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all duration-200 ${
selectedFrom === connection.id
- ? "border-blue-500 bg-blue-50 shadow-md"
+ ? "border-primary bg-accent shadow-md"
: "border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-25"
}`}
onClick={() => handleFromSelect(connection.id)}
>
-
+
{connection.name}
-
{connection.type}
+
{connection.type}
{connection.host}:{connection.port}
{selectedFrom === connection.id && (
-
+
)}
@@ -155,7 +155,7 @@ export const ConnectionStep: React.FC
= ({
{connection.name}
-
{connection.type}
+
{connection.type}
{connection.host}:{connection.port}
{selectedTo === connection.id && (
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/ControlConditionStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/ControlConditionStep.tsx
index d4995b48..6254b807 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/ControlConditionStep.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/ControlConditionStep.tsx
@@ -148,7 +148,7 @@ const ControlConditionStep: React.FC = ({ state, acti
{/* 제어 실행 조건 안내 */}
-
+
제어 실행 조건이란?
@@ -363,7 +363,7 @@ const ControlConditionStep: React.FC = ({ state, acti
variant="ghost"
size="sm"
onClick={() => actions.deleteControlCondition(index)}
- className="text-red-600 hover:text-red-700"
+ className="text-destructive hover:text-red-700"
>
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/FieldMappingStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/FieldMappingStep.tsx
index b5f7bfb0..9513d42d 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/FieldMappingStep.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/FieldMappingStep.tsx
@@ -71,14 +71,14 @@ export const FieldMappingStep: React.FC = ({
필드 매핑
-
+
소스 테이블의 필드를 대상 테이블의 필드에 드래그하여 매핑하세요
{/* 매핑 통계 */}
-
+
{fieldMappings.length}
총 매핑
@@ -88,7 +88,7 @@ export const FieldMappingStep: React.FC
= ({
유효한 매핑
-
+
{fieldMappings.filter(m => !m.isValid).length}
@@ -107,8 +107,8 @@ export const FieldMappingStep: React.FC
= ({
{/* FROM 테이블 필드들 */}
-
-
FROM
+
+ FROM
{fromTable?.name} 필드들
@@ -122,13 +122,13 @@ export const FieldMappingStep: React.FC
= ({
className={`p-3 rounded-lg border-2 cursor-move transition-all duration-200 ${
isFieldMapped(field.name)
? "border-green-300 bg-green-50 opacity-60"
- : "border-blue-200 bg-blue-50 hover:border-blue-400 hover:bg-blue-100"
+ : "border-primary/20 bg-accent hover:border-blue-400 hover:bg-primary/20"
}`}
>
{field.name}
-
{field.type}
+
{field.type}
{field.primaryKey && (
PK
@@ -170,7 +170,7 @@ export const FieldMappingStep: React.FC = ({
{field.name}
-
{field.type}
+
{field.type}
{field.primaryKey && (
PK
@@ -196,7 +196,7 @@ export const FieldMappingStep: React.FC = ({
{fieldMappings.find(m => m.toField.name === field.name)?.isValid ? (
) : (
-
+
)}
)}
@@ -213,7 +213,7 @@ export const FieldMappingStep: React.FC
= ({
이전 단계
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx
index 5adb54cb..471f5ad4 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx
@@ -150,7 +150,7 @@ const MultiActionConfigStep: React.FC = ({
const getLogicalOperatorColor = (operator: string) => {
switch (operator) {
case "AND":
- return "bg-blue-100 text-blue-800";
+ return "bg-primary/20 text-blue-800";
case "OR":
return "bg-orange-100 text-orange-800";
default:
@@ -271,7 +271,7 @@ const MultiActionConfigStep: React.FC = ({
{/* 그룹 간 논리 연산자 선택 */}
{actionGroups.length > 1 && (
-
+
그룹 간 실행 조건
@@ -649,9 +649,9 @@ const MultiActionConfigStep: React.FC
= ({
{/* 그룹 로직 설명 */}
-
+
-
+
{group.logicalOperator} 조건 그룹
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/RightPanel.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/RightPanel.tsx
index 570256fc..f81b48fe 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/RightPanel.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/RightPanel.tsx
@@ -71,7 +71,7 @@ const RightPanel: React.FC
= ({ state, actions }) => {
{/* 헤더 */}
-
+
외부 호출 설정
@@ -89,7 +89,7 @@ const RightPanel: React.FC = ({ state, actions }) => {
value={state.relationshipName || ""}
onChange={(e) => actions.setRelationshipName(e.target.value)}
placeholder="외부호출 관계의 이름을 입력하세요"
- className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
@@ -99,7 +99,7 @@ const RightPanel: React.FC = ({ state, actions }) => {
onChange={(e) => actions.setDescription(e.target.value)}
placeholder="외부호출의 용도나 설명을 입력하세요"
rows={2}
- className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
+ className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
/>
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/StepProgress.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/StepProgress.tsx
index c7af4d75..d4d2eea3 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/StepProgress.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/StepProgress.tsx
@@ -35,7 +35,7 @@ export const StepProgress: React.FC
= ({
? "bg-green-500 text-white"
: step.id === currentStep
? "bg-orange-500 text-white"
- : "bg-gray-200 text-gray-600"
+ : "bg-gray-200 text-muted-foreground"
}`}
>
{step.id < currentStep ? (
@@ -52,7 +52,7 @@ export const StepProgress: React.FC = ({
{step.title}
{step.description}
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/TableStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/TableStep.tsx
index d4423fc6..cb9bc04b 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/TableStep.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/TableStep.tsx
@@ -90,7 +90,7 @@ export const TableStep: React.FC = ({
테이블 선택
-
+
소스 테이블과 대상 테이블을 선택하세요
@@ -99,7 +99,7 @@ export const TableStep: React.FC
= ({
-
+
{fromConnection?.name}
→
@@ -112,8 +112,8 @@ export const TableStep: React.FC
= ({
{/* FROM 테이블 */}
-
-
1
+
+ 1
소스 테이블
(FROM)
@@ -125,20 +125,20 @@ export const TableStep: React.FC
= ({
key={table.name}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all duration-200 ${
selectedFromTable === table.name
- ? "border-blue-500 bg-blue-50 shadow-md"
+ ? "border-primary bg-accent shadow-md"
: "border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-25"
}`}
onClick={() => handleFromTableSelect(table.name)}
>
-
+
{table.name}
-
{table.columns.length}개 컬럼
+
{table.columns.length}개 컬럼
{table.rowCount?.toLocaleString()}개 행
{selectedFromTable === table.name && (
-
+
)}
@@ -171,7 +171,7 @@ export const TableStep: React.FC
= ({
{table.name}
-
{table.columns.length}개 컬럼
+
{table.columns.length}개 컬럼
{table.rowCount?.toLocaleString()}개 행
{selectedToTable === table.name && (
@@ -188,7 +188,7 @@ export const TableStep: React.FC = ({
이전 단계
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldColumn.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldColumn.tsx
index 44853bb5..b3deefd6 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldColumn.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldColumn.tsx
@@ -128,9 +128,9 @@ const FieldColumn: React.FC = ({
: isMapped
? "border-green-500 bg-green-50 shadow-sm"
: isBlockedDropTarget
- ? "border-red-400 bg-red-50 shadow-md"
+ ? "border-red-400 bg-destructive/10 shadow-md"
: isDropTarget
- ? "border-blue-400 bg-blue-50 shadow-md"
+ ? "border-blue-400 bg-accent shadow-md"
: "border-border hover:bg-muted/50 hover:shadow-sm"
} `}
draggable={type === "from" && !isMapped}
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldMappingCanvas.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldMappingCanvas.tsx
index a3fc3b9d..718daf93 100644
--- a/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldMappingCanvas.tsx
+++ b/frontend/components/dataflow/connection/redesigned/RightPanel/VisualMapping/FieldMappingCanvas.tsx
@@ -311,7 +311,7 @@ const FieldMappingCanvas: React.FC = ({
{/* 매핑 규칙 안내 */}
-
+
📋 매핑 규칙
✅ 1:N 매핑 허용 (하나의 소스 필드를 여러 대상 필드에 매핑)
diff --git a/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx b/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx
index 5a2b710e..1af3beed 100644
--- a/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx
+++ b/frontend/components/dataflow/external-call/ExternalCallTestPanel.tsx
@@ -411,7 +411,7 @@ const ExternalCallTestPanel: React.FC
= ({
{testResult.responseTime !== undefined && (
응답 시간
-
+
{testResult.responseTime}ms
diff --git a/frontend/components/dataflow/external-call/RestApiSettings.tsx b/frontend/components/dataflow/external-call/RestApiSettings.tsx
index c3d6b4b0..66a8e651 100644
--- a/frontend/components/dataflow/external-call/RestApiSettings.tsx
+++ b/frontend/components/dataflow/external-call/RestApiSettings.tsx
@@ -272,7 +272,7 @@ const RestApiSettings: React.FC
= ({ settings, onSettingsC
value={settings.apiUrl}
onChange={(e) => handleUrlChange(e.target.value)}
disabled={readonly}
- className={validationErrors.some((e) => e.includes("URL")) ? "border-red-500" : ""}
+ className={validationErrors.some((e) => e.includes("URL")) ? "border-destructive" : ""}
/>
호출할 API의 전체 URL을 입력하세요.
diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx
index 7f365d29..01f17dd4 100644
--- a/frontend/components/layout/AppLayout.tsx
+++ b/frontend/components/layout/AppLayout.tsx
@@ -324,7 +324,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
handleMenuClick(child)}
@@ -376,7 +376,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
return (
@@ -423,7 +423,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
className={`flex w-full items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 hover:cursor-pointer ${
isAdminMode
? "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100"
- : "border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100"
+ : "border border-primary/20 bg-accent text-blue-700 hover:bg-primary/20"
}`}
>
{isAdminMode ? (
@@ -486,7 +486,7 @@ export function AppLayout({ children }: AppLayoutProps) {
fallback={
diff --git a/frontend/components/layout/ProfileModal.tsx b/frontend/components/layout/ProfileModal.tsx
index b50eb657..672d538c 100644
--- a/frontend/components/layout/ProfileModal.tsx
+++ b/frontend/components/layout/ProfileModal.tsx
@@ -22,9 +22,9 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
case "success":
return "text-green-600";
case "error":
- return "text-red-600";
+ return "text-destructive";
default:
- return "text-blue-600";
+ return "text-primary";
}
};
@@ -35,7 +35,7 @@ function AlertModal({ isOpen, onClose, title, message, type = "info" }: AlertMod
{title}
diff --git a/frontend/components/mail/ConfirmDeleteModal.tsx b/frontend/components/mail/ConfirmDeleteModal.tsx
index 9c8a8633..5a4537a9 100644
--- a/frontend/components/mail/ConfirmDeleteModal.tsx
+++ b/frontend/components/mail/ConfirmDeleteModal.tsx
@@ -49,7 +49,7 @@ export default function ConfirmDeleteModal({
{message}
{itemName && (
-
+
삭제 대상: {itemName}
@@ -71,7 +71,8 @@ export default function ConfirmDeleteModal({
삭제
diff --git a/frontend/components/mail/MailAccountModal.tsx b/frontend/components/mail/MailAccountModal.tsx
index 11497ff1..ddfd5df0 100644
--- a/frontend/components/mail/MailAccountModal.tsx
+++ b/frontend/components/mail/MailAccountModal.tsx
@@ -332,7 +332,8 @@ export default function MailAccountModal({
type="button"
onClick={handleTestConnection}
disabled={isTesting}
- className="w-full bg-blue-500 hover:bg-blue-600"
+ variant="default"
+ className="w-full"
>
{isTesting ? (
<>
@@ -351,14 +352,14 @@ export default function MailAccountModal({
{testResult.success ? (
) : (
-
+
)}
{isSubmitting ? (
diff --git a/frontend/components/mail/MailAccountTable.tsx b/frontend/components/mail/MailAccountTable.tsx
index e4ff9680..ec88a466 100644
--- a/frontend/components/mail/MailAccountTable.tsx
+++ b/frontend/components/mail/MailAccountTable.tsx
@@ -21,6 +21,7 @@ interface MailAccountTableProps {
onEdit: (account: MailAccount) => void;
onDelete: (account: MailAccount) => void;
onToggleStatus: (account: MailAccount) => void;
+ onTestConnection: (account: MailAccount) => void;
}
export default function MailAccountTable({
@@ -28,6 +29,7 @@ export default function MailAccountTable({
onEdit,
onDelete,
onToggleStatus,
+ onTestConnection,
}: MailAccountTableProps) {
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState('createdAt');
@@ -82,7 +84,7 @@ export default function MailAccountTable({
return (
-
+
등록된 메일 계정이 없습니다
@@ -174,10 +176,10 @@ export default function MailAccountTable({
{account.name}
- {account.email}
+ {account.email}
-
+
{account.smtpHost}:{account.smtpPort}
@@ -190,7 +192,7 @@ export default function MailAccountTable({
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all hover:scale-105 ${
account.status === 'active'
? 'bg-green-100 text-green-700 hover:bg-green-200'
- : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
+ : 'bg-gray-100 text-muted-foreground hover:bg-gray-200'
}`}
>
{account.status === 'active' ? (
@@ -214,22 +216,29 @@ export default function MailAccountTable({
-
+
{formatDate(account.createdAt)}
onEdit(account)}
+ onClick={() => onTestConnection(account)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
+ title="SMTP 연결 테스트"
+ >
+
+
+ onEdit(account)}
+ className="p-2 text-primary hover:bg-accent rounded-lg transition-colors"
title="수정"
>
onDelete(account)}
- className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
+ className="p-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
title="삭제"
>
@@ -244,7 +253,7 @@ export default function MailAccountTable({
{/* 결과 요약 */}
-
+
전체 {accounts.length}개 중 {sortedAccounts.length}개 표시
{searchTerm && ` (검색: "${searchTerm}")`}
diff --git a/frontend/components/mail/MailDesigner.tsx b/frontend/components/mail/MailDesigner.tsx
index 110156bf..7bcadf1c 100644
--- a/frontend/components/mail/MailDesigner.tsx
+++ b/frontend/components/mail/MailDesigner.tsx
@@ -63,7 +63,7 @@ export default function MailDesigner({
// 컴포넌트 타입 정의
const componentTypes = [
- { type: "text", icon: Type, label: "텍스트", color: "bg-blue-100 hover:bg-blue-200" },
+ { type: "text", icon: Type, label: "텍스트", color: "bg-primary/20 hover:bg-blue-200" },
{ type: "button", icon: MousePointer, label: "버튼", color: "bg-green-100 hover:bg-green-200" },
{ type: "image", icon: ImageIcon, label: "이미지", color: "bg-purple-100 hover:bg-purple-200" },
{ type: "spacer", icon: Square, label: "여백", color: "bg-gray-100 hover:bg-gray-200" },
@@ -201,7 +201,7 @@ export default function MailDesigner({
미리보기
-
+
발송
@@ -253,7 +253,7 @@ export default function MailDesigner({
e.stopPropagation();
removeComponent(comp.id);
}}
- className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-red-500 text-white rounded-full p-1 hover:bg-red-600"
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-destructive text-white rounded-full p-1 hover:bg-destructive/90"
>
diff --git a/frontend/components/mail/MailDetailModal.tsx b/frontend/components/mail/MailDetailModal.tsx
index 671d9ac3..ef657a63 100644
--- a/frontend/components/mail/MailDetailModal.tsx
+++ b/frontend/components/mail/MailDetailModal.tsx
@@ -168,12 +168,12 @@ export default function MailDetailModal({
{loading ? (
- 메일을 불러오는 중...
+ 메일을 불러오는 중...
) : error ? (
-
{error}
+
{error}
다시 시도
@@ -193,17 +193,17 @@ export default function MailDetailModal({
받는사람: {" "}
- {mail.to}
+ {mail.to}
{mail.cc && (
참조: {" "}
- {mail.cc}
+ {mail.cc}
)}
날짜: {" "}
-
+
{formatDate(mail.date)}
@@ -225,7 +225,7 @@ export default function MailDetailModal({
{mail.attachments && mail.attachments.length > 0 && (
-
+
첨부파일 ({mail.attachments.length})
diff --git a/frontend/components/mail/MailTemplateCard.tsx b/frontend/components/mail/MailTemplateCard.tsx
index 4ab30c9f..558c35bf 100644
--- a/frontend/components/mail/MailTemplateCard.tsx
+++ b/frontend/components/mail/MailTemplateCard.tsx
@@ -30,7 +30,7 @@ export default function MailTemplateCard({
const getCategoryColor = (category?: string) => {
const colors: Record
= {
- welcome: 'bg-blue-100 text-blue-700 border-blue-300',
+ welcome: 'bg-primary/20 text-blue-700 border-blue-300',
promotion: 'bg-purple-100 text-purple-700 border-purple-300',
notification: 'bg-green-100 text-green-700 border-green-300',
newsletter: 'bg-orange-100 text-orange-700 border-orange-300',
@@ -52,7 +52,7 @@ export default function MailTemplateCard({
{template.name}
-
+
{template.subject}
@@ -75,7 +75,7 @@ export default function MailTemplateCard({
컴포넌트 {template.components.length}개
{template.components.slice(0, 3).map((component, idx) => (
-
+
{component.type}
{component.type === 'text' && component.content && (
@@ -110,7 +110,7 @@ export default function MailTemplateCard({
onPreview(template)}
>
@@ -138,7 +138,7 @@ export default function MailTemplateCard({
onDelete(template)}
>
diff --git a/frontend/components/mail/MailTemplatePreviewModal.tsx b/frontend/components/mail/MailTemplatePreviewModal.tsx
index 7203762a..ebb1d6e5 100644
--- a/frontend/components/mail/MailTemplatePreviewModal.tsx
+++ b/frontend/components/mail/MailTemplatePreviewModal.tsx
@@ -106,7 +106,7 @@ export default function MailTemplatePreviewModal({
))}
-
+
💡 변수 값을 입력하면 미리보기에 반영됩니다.
@@ -122,15 +122,15 @@ export default function MailTemplatePreviewModal({
- 제목:
+ 제목:
{template.subject}
- 발신:
+ 발신:
your-email@company.com
- 수신:
+ 수신:
recipient@example.com
diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx
index ee4d6fe5..ec109347 100644
--- a/frontend/components/screen/CopyScreenModal.tsx
+++ b/frontend/components/screen/CopyScreenModal.tsx
@@ -117,7 +117,7 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
{/* 원본 화면 정보 */}
원본 화면 정보
-
+
화면명: {sourceScreen?.screenName}
diff --git a/frontend/components/screen/CreateScreenModal.tsx b/frontend/components/screen/CreateScreenModal.tsx
index 45c9e6d5..d1291328 100644
--- a/frontend/components/screen/CreateScreenModal.tsx
+++ b/frontend/components/screen/CreateScreenModal.tsx
@@ -148,7 +148,7 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
onOpenChange(false)} disabled={submitting}>
취소
-
+
생성
diff --git a/frontend/components/screen/DesignerToolbar.tsx b/frontend/components/screen/DesignerToolbar.tsx
index 834a006f..301bff63 100644
--- a/frontend/components/screen/DesignerToolbar.tsx
+++ b/frontend/components/screen/DesignerToolbar.tsx
@@ -55,7 +55,7 @@ export const DesignerToolbar: React.FC = ({
onToggleZoneBorders,
}) => {
return (
-