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/docker/deploy/backend.Dockerfile b/docker/deploy/backend.Dockerfile new file mode 100644 index 00000000..e5195e48 --- /dev/null +++ b/docker/deploy/backend.Dockerfile @@ -0,0 +1,43 @@ +# syntax=docker/dockerfile:1 + +# Base image (Debian-based for glibc + OpenSSL compatibility) +FROM node:20-bookworm-slim AS base +WORKDIR /app +ENV NODE_ENV=production +# Install OpenSSL, curl (for healthcheck), and required certs +RUN apt-get update \ + && apt-get install -y --no-install-recommends openssl ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# Dependencies stage (install production dependencies) +FROM base AS deps +COPY package*.json ./ +RUN npm ci --omit=dev --prefer-offline --no-audit && npm cache clean --force + +# Build stage (compile TypeScript) +FROM node:20-bookworm-slim AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci --prefer-offline --no-audit && npm cache clean --force +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# Runtime image +FROM base AS runner +ENV NODE_ENV=production + +# Copy production node_modules +COPY --from=deps /app/node_modules ./node_modules +# Copy built files +COPY --from=build /app/dist ./dist +# Copy package files +COPY package*.json ./ + +# Create logs and uploads directories and set permissions (use existing node user with UID 1000) +RUN mkdir -p logs uploads && chown -R node:node logs uploads && chmod -R 755 logs uploads + +EXPOSE 3001 +USER node +CMD ["node", "dist/app.js"] + diff --git a/docker/deploy/docker-compose.yml b/docker/deploy/docker-compose.yml new file mode 100644 index 00000000..9388b57e --- /dev/null +++ b/docker/deploy/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3.8" + +services: + # Node.js 백엔드 + backend: + build: + context: ../../backend-node + dockerfile: ../docker/deploy/backend.Dockerfile + container_name: pms-backend-prod + restart: always + environment: + NODE_ENV: production + PORT: "3001" + HOST: 0.0.0.0 + DATABASE_URL: postgresql://postgres:ph0909!!@39.117.244.52:11132/plm + JWT_SECRET: ilshin-plm-super-secret-jwt-key-2024 + JWT_EXPIRES_IN: 24h + CORS_ORIGIN: https://v1.vexplor.com + CORS_CREDENTIALS: "true" + LOG_LEVEL: info + ENCRYPTION_KEY: ilshin-plm-mail-encryption-key-32characters-2024-secure + volumes: + - /home/vexplor/backend_data:/app/uploads + labels: + - traefik.enable=true + - traefik.http.routers.backend.rule=Host(`api.vexplor.com`) + - traefik.http.routers.backend.entrypoints=websecure,web + - traefik.http.routers.backend.tls=true + - traefik.http.routers.backend.tls.certresolver=le + - traefik.http.services.backend.loadbalancer.server.port=3001 + + # Next.js 프론트엔드 + frontend: + build: + context: ../../frontend + dockerfile: ../docker/deploy/frontend.Dockerfile + args: + - NEXT_PUBLIC_API_URL=https://api.vexplor.com/api + container_name: pms-frontend-prod + restart: always + environment: + NODE_ENV: production + NEXT_PUBLIC_API_URL: https://api.vexplor.com/api + NEXT_TELEMETRY_DISABLED: "1" + PORT: "3000" + HOSTNAME: 0.0.0.0 + volumes: + - /home/vexplor/frontend_data:/app/data + labels: + - traefik.enable=true + - traefik.http.routers.frontend.rule=Host(`v1.vexplor.com`) + - traefik.http.routers.frontend.entrypoints=websecure,web + - traefik.http.routers.frontend.tls=true + - traefik.http.routers.frontend.tls.certresolver=le + - traefik.http.services.frontend.loadbalancer.server.port=3000 + +networks: + default: + name: toktork_server_default + external: true diff --git a/docker/deploy/frontend.Dockerfile b/docker/deploy/frontend.Dockerfile new file mode 100644 index 00000000..01315ce1 --- /dev/null +++ b/docker/deploy/frontend.Dockerfile @@ -0,0 +1,59 @@ +# Multi-stage build for Next.js +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm install + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Disable telemetry during the build +ENV NEXT_TELEMETRY_DISABLED 1 + +# 빌드 시 환경변수 설정 (ARG로 받아서 ENV로 설정) +ARG NEXT_PUBLIC_API_URL=https://api.vexplor.com/api +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL + +# Build the application +ENV DISABLE_ESLINT_PLUGIN=true +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy the Next.js build output +COPY --from=builder /app/public ./public + +# Production 모드에서는 .next 폴더 전체를 복사 +COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next +COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json + +# node_modules 복사 (production dependencies) +COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +# Next.js start 명령어 사용 +CMD ["npm", "start"] + 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}

-
@@ -121,7 +121,7 @@ export default function ScreenManagementPage() { 이전 단계 -
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)/main/page.tsx b/frontend/app/(main)/main/page.tsx index 6ce00bd6..0c6eb73e 100644 --- a/frontend/app/(main)/main/page.tsx +++ b/frontend/app/(main)/main/page.tsx @@ -9,13 +9,13 @@ import { Badge } from "@/components/ui/badge"; */ export default function MainPage() { return ( -
+
{/* 메인 컨텐츠 */} {/* Welcome Message */}
-

PLM 솔루션에 오신 것을 환영합니다!

+

Vexolor에 오신 것을 환영합니다!

제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.

Spring Boot 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
@@ -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 (
-

카테고리를 불러오는 중 오류가 발생했습니다.

+

카테고리를 불러오는 중 오류가 발생했습니다.

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

코드를 불러오는 중 오류가 발생했습니다.

+

코드를 불러오는 중 오류가 발생했습니다.

@@ -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
2
템플릿 선택
-
+
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() {
      -
      검색 결과: {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}
      -

      {message}

      +

      {message}

      {/* 비밀번호 일치 여부 표시 */} - {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 @@ -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;` {/* 샘플 쿼리 버튼들 */}
      - 샘플 쿼리: + 샘플 쿼리: @@ -224,7 +224,7 @@ ORDER BY Q4 DESC;` {/* 새로고침 간격 설정 */}
      - + { @@ -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}
      {/* 하단 통계 */} -
      +
      {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 = ({ {/* 현재 선택된 테이블 표시 */}
      - +
      {availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable} @@ -54,7 +54,7 @@ export const SimpleKeySettings: React.FC = ({
      - +
      {availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable} @@ -67,7 +67,7 @@ export const SimpleKeySettings: React.FC = ({ {/* 컬럼 선택 */}
      - +
      {fromTableColumns.map((column) => (
      - +
      {toTableColumns.map((column) => (