Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/report
This commit is contained in:
commit
57c4e8317d
|
|
@ -178,14 +178,19 @@ export class MailAccountFileController {
|
|||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// TODO: 실제 SMTP 연결 테스트 구현
|
||||
// const account = await mailAccountFileService.getAccountById(id);
|
||||
// nodemailer로 연결 테스트
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '연결 테스트 성공 (미구현)',
|
||||
const account = await mailAccountFileService.getAccountById(id);
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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와 수신자 이메일이 필요합니다.',
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
// 메일 목록 조회
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||
|
|
@ -148,6 +167,7 @@ export default function MailAccountsPage() {
|
|||
onEdit={handleOpenEditModal}
|
||||
onDelete={handleOpenDeleteModal}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onTestConnection={handleTestConnection}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function ScreenManagementPage() {
|
|||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToNextStep("design")}>
|
||||
<Button variant="default" className="shadow-sm" onClick={() => goToNextStep("design")}>
|
||||
화면 설계하기 <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -121,7 +121,7 @@ export default function ScreenManagementPage() {
|
|||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
이전 단계
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToStep("list")}>
|
||||
<Button variant="default" className="shadow-sm" onClick={() => goToStep("list")}>
|
||||
목록으로 돌아가기
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import { Badge } from "@/components/ui/badge";
|
|||
*/
|
||||
export default function MainPage() {
|
||||
return (
|
||||
<div className="pt-10 space-y-6">
|
||||
<div className="space-y-6 pt-10">
|
||||
{/* 메인 컨텐츠 */}
|
||||
{/* Welcome Message */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-6 text-center">
|
||||
<h3 className="text-lg font-semibold">PLM 솔루션에 오신 것을 환영합니다!</h3>
|
||||
<h3 className="text-lg font-semibold">Vexolor에 오신 것을 환영합니다!</h3>
|
||||
<p className="text-muted-foreground">제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.</p>
|
||||
<div className="flex justify-center space-x-2">
|
||||
<Badge variant="secondary">Spring Boot</Badge>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export default function ScreenViewPage() {
|
|||
const screenHeight = layout?.screenResolution?.height || 800;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 pt-10">
|
||||
<div className="h-full w-full overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 p-10">
|
||||
{layout && layout.components.length > 0 ? (
|
||||
// 캔버스 컴포넌트들을 정확한 해상도로 표시
|
||||
<div
|
||||
|
|
@ -158,7 +158,6 @@ export default function ScreenViewPage() {
|
|||
height: `${screenHeight}px`,
|
||||
minWidth: `${screenWidth}px`,
|
||||
minHeight: `${screenHeight}px`,
|
||||
margin: "0 auto 40px auto", // 하단 여백 추가
|
||||
}}
|
||||
>
|
||||
{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() {
|
|||
) : (
|
||||
// 빈 화면일 때도 깔끔하게 표시
|
||||
<div
|
||||
className="mx-auto flex items-center justify-center bg-white"
|
||||
className="mx-auto flex items-center justify-center bg-white rounded-xl border border-gray-200/60 shadow-lg shadow-gray-900/5"
|
||||
style={{
|
||||
width: `${screenWidth}px`,
|
||||
height: `${screenHeight}px`,
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
// 파일 아이콘 가져오기
|
||||
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 <Image {...iconProps} className="text-blue-600" />;
|
||||
return <Image {...iconProps} className="text-primary" />;
|
||||
}
|
||||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'].includes(extension)) {
|
||||
return <Video {...iconProps} className="text-purple-600" />;
|
||||
|
|
@ -71,7 +71,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
return <Archive {...iconProps} className="text-yellow-600" />;
|
||||
}
|
||||
if (['txt', 'md', 'doc', 'docx', 'pdf', 'rtf'].includes(extension)) {
|
||||
return <FileText {...iconProps} className="text-red-600" />;
|
||||
return <FileText {...iconProps} className="text-destructive" />;
|
||||
}
|
||||
return <File {...iconProps} />;
|
||||
};
|
||||
|
|
@ -272,7 +272,7 @@ export const GlobalFileViewer: React.FC<GlobalFileViewerProps> = ({
|
|||
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"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{formData.execution_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">총 실행 횟수</div>
|
||||
<div className="text-sm text-muted-foreground">총 실행 횟수</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{formData.success_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">성공 횟수</div>
|
||||
<div className="text-sm text-muted-foreground">성공 횟수</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
<div className="text-2xl font-bold text-destructive">
|
||||
{formData.failure_count || 0}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">실패 횟수</div>
|
||||
<div className="text-sm text-muted-foreground">실패 횟수</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.last_executed_at && (
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
마지막 실행: {new Date(formData.last_executed_at).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">{category.category_code}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{category.category_code}</p>
|
||||
{category.description && <p className="mt-1 text-sm text-gray-500">{category.description}</p>}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<p className="text-sm text-red-600">{createForm.formState.errors.categoryCode.message}</p>
|
||||
<p className="text-sm text-destructive">{createForm.formState.errors.categoryCode.message}</p>
|
||||
)}
|
||||
{!createForm.formState.errors.categoryCode && (
|
||||
<ValidationMessage
|
||||
|
|
@ -200,7 +200,7 @@ export function CodeCategoryFormModal({
|
|||
{isEditing && editingCategory && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="categoryCodeDisplay">카테고리 코드</Label>
|
||||
<Input id="categoryCodeDisplay" value={editingCategory.category_code} disabled className="bg-gray-50" />
|
||||
<Input id="categoryCodeDisplay" value={editingCategory.category_code} disabled className="bg-muted" />
|
||||
<p className="text-sm text-gray-500">카테고리 코드는 수정할 수 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -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 && (
|
||||
<p className="text-sm text-red-600">{updateForm.formState.errors.categoryName.message}</p>
|
||||
<p className="text-sm text-destructive">{updateForm.formState.errors.categoryName.message}</p>
|
||||
)
|
||||
: createForm.formState.errors.categoryName && (
|
||||
<p className="text-sm text-red-600">{createForm.formState.errors.categoryName.message}</p>
|
||||
<p className="text-sm text-destructive">{createForm.formState.errors.categoryName.message}</p>
|
||||
)}
|
||||
{!(isEditing ? updateForm.formState.errors.categoryName : createForm.formState.errors.categoryName) && (
|
||||
<ValidationMessage
|
||||
|
|
@ -251,20 +251,20 @@ export function CodeCategoryFormModal({
|
|||
className={
|
||||
isEditing
|
||||
? updateForm.formState.errors.categoryNameEng
|
||||
? "border-red-500"
|
||||
? "border-destructive"
|
||||
: ""
|
||||
: createForm.formState.errors.categoryNameEng
|
||||
? "border-red-500"
|
||||
? "border-destructive"
|
||||
: ""
|
||||
}
|
||||
onBlur={() => handleFieldBlur("categoryNameEng")}
|
||||
/>
|
||||
{isEditing
|
||||
? updateForm.formState.errors.categoryNameEng && (
|
||||
<p className="text-sm text-red-600">{updateForm.formState.errors.categoryNameEng.message}</p>
|
||||
<p className="text-sm text-destructive">{updateForm.formState.errors.categoryNameEng.message}</p>
|
||||
)
|
||||
: createForm.formState.errors.categoryNameEng && (
|
||||
<p className="text-sm text-red-600">{createForm.formState.errors.categoryNameEng.message}</p>
|
||||
<p className="text-sm text-destructive">{createForm.formState.errors.categoryNameEng.message}</p>
|
||||
)}
|
||||
{!(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 && (
|
||||
<p className="text-sm text-red-600">{updateForm.formState.errors.description.message}</p>
|
||||
<p className="text-sm text-destructive">{updateForm.formState.errors.description.message}</p>
|
||||
)
|
||||
: createForm.formState.errors.description && (
|
||||
<p className="text-sm text-red-600">{createForm.formState.errors.description.message}</p>
|
||||
<p className="text-sm text-destructive">{createForm.formState.errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -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 && (
|
||||
<p className="text-sm text-red-600">{updateForm.formState.errors.sortOrder.message}</p>
|
||||
<p className="text-sm text-destructive">{updateForm.formState.errors.sortOrder.message}</p>
|
||||
)
|
||||
: createForm.formState.errors.sortOrder && (
|
||||
<p className="text-sm text-red-600">{createForm.formState.errors.sortOrder.message}</p>
|
||||
<p className="text-sm text-destructive">{createForm.formState.errors.sortOrder.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export function CodeCategoryPanel({ selectedCategoryCode, onSelectCategory }: Co
|
|||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600">카테고리를 불러오는 중 오류가 발생했습니다.</p>
|
||||
<p className="text-destructive">카테고리를 불러오는 중 오류가 발생했습니다.</p>
|
||||
<Button variant="outline" onClick={() => window.location.reload()} className="mt-2">
|
||||
다시 시도
|
||||
</Button>
|
||||
|
|
@ -116,7 +116,7 @@ export function CodeCategoryPanel({ selectedCategoryCode, onSelectCategory }: Co
|
|||
onChange={(e) => setShowActiveOnly(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="activeOnly" className="text-sm text-gray-600">
|
||||
<label htmlFor="activeOnly" className="text-sm text-muted-foreground">
|
||||
활성 카테고리만 표시
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
|
|||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600">코드를 불러오는 중 오류가 발생했습니다.</p>
|
||||
<p className="text-destructive">코드를 불러오는 중 오류가 발생했습니다.</p>
|
||||
<Button variant="outline" onClick={() => window.location.reload()} className="mt-2">
|
||||
다시 시도
|
||||
</Button>
|
||||
|
|
@ -155,7 +155,7 @@ export function CodeDetailPanel({ categoryCode }: CodeDetailPanelProps) {
|
|||
onChange={(e) => setShowActiveOnly(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="activeOnlyCodes" className="text-sm text-gray-600">
|
||||
<label htmlFor="activeOnlyCodes" className="text-sm text-muted-foreground">
|
||||
활성 코드만 표시
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -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" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{activeCode.codeValue || activeCode.code_value}
|
||||
</p>
|
||||
{activeCode.description && (
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<p className="text-sm text-red-600">{getErrorMessage((form.formState.errors as any)?.codeValue)}</p>
|
||||
<p className="text-sm text-destructive">{getErrorMessage((form.formState.errors as any)?.codeValue)}</p>
|
||||
)}
|
||||
{!isEditing && !(form.formState.errors as any)?.codeValue && (
|
||||
<ValidationMessage
|
||||
|
|
@ -199,7 +199,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
|||
{...form.register("codeName")}
|
||||
disabled={isLoading}
|
||||
placeholder="코드명을 입력하세요"
|
||||
className={form.formState.errors.codeName ? "border-red-500" : ""}
|
||||
className={form.formState.errors.codeName ? "border-destructive" : ""}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
if (value) {
|
||||
|
|
@ -211,7 +211,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
|||
}}
|
||||
/>
|
||||
{form.formState.errors.codeName && (
|
||||
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.codeName)}</p>
|
||||
<p className="text-sm text-destructive">{getErrorMessage(form.formState.errors.codeName)}</p>
|
||||
)}
|
||||
{!form.formState.errors.codeName && (
|
||||
<ValidationMessage
|
||||
|
|
@ -230,7 +230,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
|||
{...form.register("codeNameEng")}
|
||||
disabled={isLoading}
|
||||
placeholder="코드 영문명을 입력하세요"
|
||||
className={form.formState.errors.codeNameEng ? "border-red-500" : ""}
|
||||
className={form.formState.errors.codeNameEng ? "border-destructive" : ""}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
if (value) {
|
||||
|
|
@ -242,7 +242,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
|||
}}
|
||||
/>
|
||||
{form.formState.errors.codeNameEng && (
|
||||
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.codeNameEng)}</p>
|
||||
<p className="text-sm text-destructive">{getErrorMessage(form.formState.errors.codeNameEng)}</p>
|
||||
)}
|
||||
{!form.formState.errors.codeNameEng && (
|
||||
<ValidationMessage
|
||||
|
|
@ -262,10 +262,10 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
|
|||
disabled={isLoading}
|
||||
placeholder="설명을 입력하세요"
|
||||
rows={3}
|
||||
className={form.formState.errors.description ? "border-red-500" : ""}
|
||||
className={form.formState.errors.description ? "border-destructive" : ""}
|
||||
/>
|
||||
{form.formState.errors.description && (
|
||||
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.description)}</p>
|
||||
<p className="text-sm text-destructive">{getErrorMessage(form.formState.errors.description)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -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 && (
|
||||
<p className="text-sm text-red-600">{getErrorMessage(form.formState.errors.sortOrder)}</p>
|
||||
<p className="text-sm text-destructive">{getErrorMessage(form.formState.errors.sortOrder)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
|||
const hasRowError = rowErrors.length > 0;
|
||||
|
||||
return (
|
||||
<TableRow key={index} className={hasRowError ? "bg-red-50" : ""}>
|
||||
<TableRow key={index} className={hasRowError ? "bg-destructive/10" : ""}>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
|
|
@ -199,7 +199,7 @@ export function ColumnDefinitionTable({ columns, onChange, disabled = false }: C
|
|||
className={hasRowError ? "border-red-300" : ""}
|
||||
/>
|
||||
{rowErrors.length > 0 && (
|
||||
<div className="space-y-1 text-xs text-red-600">
|
||||
<div className="space-y-1 text-xs text-destructive">
|
||||
{rowErrors.map((error, i) => (
|
||||
<div key={i}>{error}</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ export function CreateTableModal({ isOpen, onClose, onSuccess }: CreateTableModa
|
|||
placeholder="예: customer_info"
|
||||
className={tableNameError ? "border-red-300" : ""}
|
||||
/>
|
||||
{tableNameError && <p className="text-sm text-red-600">{tableNameError}</p>}
|
||||
{tableNameError && <p className="text-sm text-destructive">{tableNameError}</p>}
|
||||
<p className="text-muted-foreground text-xs">영문자로 시작, 영문자/숫자/언더스코어만 사용 가능</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -271,14 +271,14 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
|||
{log.success ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-600" />
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
)}
|
||||
<span className={log.success ? "text-green-600" : "text-red-600"}>
|
||||
<span className={log.success ? "text-green-600" : "text-destructive"}>
|
||||
{log.success ? "성공" : "실패"}
|
||||
</span>
|
||||
</div>
|
||||
{log.error_message && (
|
||||
<div className="mt-1 max-w-xs truncate text-xs text-red-600">{log.error_message}</div>
|
||||
<div className="mt-1 max-w-xs truncate text-xs text-destructive">{log.error_message}</div>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
|
|
@ -325,7 +325,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
|||
<CardTitle className="text-sm font-medium">실패</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{statistics.failedExecutions}</div>
|
||||
<div className="text-2xl font-bold text-destructive">{statistics.failedExecutions}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -374,13 +374,13 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
|||
{statistics.recentFailures.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base text-red-600">최근 실패 로그</CardTitle>
|
||||
<CardTitle className="text-base text-destructive">최근 실패 로그</CardTitle>
|
||||
<CardDescription>최근 발생한 DDL 실행 실패 내역입니다.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{statistics.recentFailures.map((failure, index) => (
|
||||
<div key={index} className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<div key={index} className="rounded-lg border border-destructive/20 bg-destructive/10 p-3">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getDDLTypeBadgeVariant(failure.ddl_type)}>{failure.ddl_type}</Badge>
|
||||
|
|
@ -390,7 +390,7 @@ export function DDLLogViewer({ isOpen, onClose }: DDLLogViewerProps) {
|
|||
{format(new Date(failure.executed_at), "MM-dd HH:mm", { locale: ko })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-red-600">{failure.error_message}</div>
|
||||
<div className="text-sm text-destructive">{failure.error_message}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
|
|||
<div className="mt-2 h-2 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
summary.totalSizeMB > 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)}%`,
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
|
|||
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"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{testResult.success ? "✅ 연결 성공" : "❌ 연결 실패"}</div>
|
||||
|
|
@ -469,7 +469,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
|
|||
{!testResult.success && testResult.error && (
|
||||
<div className="mt-2 text-xs">
|
||||
<div>오류 코드: {testResult.error.code}</div>
|
||||
{testResult.error.details && <div className="mt-1 text-red-600">{testResult.error.details}</div>}
|
||||
{testResult.error.details && <div className="mt-1 text-destructive">{testResult.error.details}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -238,10 +238,10 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div className="mb-6 flex items-center justify-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className={`flex items-center gap-2 ${step === "basic" ? "text-blue-600" : step === "template" || step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
||||
className={`flex items-center gap-2 ${step === "basic" ? "text-primary" : step === "template" || step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "basic" ? "bg-blue-100 text-blue-600" : step === "template" || step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "basic" ? "bg-primary/20 text-primary" : step === "template" || step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
|
|
@ -249,19 +249,19 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
</div>
|
||||
<div className="h-px w-8 bg-gray-300" />
|
||||
<div
|
||||
className={`flex items-center gap-2 ${step === "template" ? "text-blue-600" : step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
||||
className={`flex items-center gap-2 ${step === "template" ? "text-primary" : step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "template" ? "bg-blue-100 text-blue-600" : step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "template" ? "bg-primary/20 text-primary" : step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<span className="text-sm font-medium">템플릿 선택</span>
|
||||
</div>
|
||||
<div className="h-px w-8 bg-gray-300" />
|
||||
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-blue-600" : "text-gray-400"}`}>
|
||||
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-primary" : "text-gray-400"}`}>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "advanced" ? "bg-blue-100 text-blue-600" : "bg-gray-100"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "advanced" ? "bg-primary/20 text-primary" : "bg-gray-100"}`}
|
||||
>
|
||||
3
|
||||
</div>
|
||||
|
|
@ -304,13 +304,13 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<Card
|
||||
key={category.id}
|
||||
className={`cursor-pointer transition-all ${
|
||||
formData.category === category.id ? "bg-blue-50 ring-2 ring-blue-500" : "hover:bg-gray-50"
|
||||
formData.category === category.id ? "bg-accent ring-2 ring-blue-500" : "hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => setFormData((prev) => ({ ...prev, category: category.id }))}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<IconComponent className="h-5 w-5 text-gray-600" />
|
||||
<IconComponent className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium">{category.name}</div>
|
||||
<div className="text-xs text-gray-500">{category.description}</div>
|
||||
|
|
@ -346,7 +346,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<Card
|
||||
key={template.id}
|
||||
className={`cursor-pointer transition-all ${
|
||||
formData.template === template.id ? "bg-blue-50 ring-2 ring-blue-500" : "hover:bg-gray-50"
|
||||
formData.template === template.id ? "bg-accent ring-2 ring-blue-500" : "hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() =>
|
||||
setFormData((prev) => ({
|
||||
|
|
@ -362,7 +362,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div className="font-medium">{template.name}</div>
|
||||
<Badge variant="secondary">{template.zones}개 영역</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">{template.description}</div>
|
||||
<div className="text-sm text-muted-foreground">{template.description}</div>
|
||||
<div className="text-xs text-gray-500">예: {template.example}</div>
|
||||
<div className="rounded bg-gray-100 p-2 text-center font-mono text-xs">{template.icon}</div>
|
||||
</div>
|
||||
|
|
@ -427,7 +427,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div className="space-y-4">
|
||||
{generationResult ? (
|
||||
<Alert
|
||||
className={generationResult.success ? "border-green-200 bg-green-50" : "border-red-200 bg-red-50"}
|
||||
className={generationResult.success ? "border-green-200 bg-green-50" : "border-destructive/20 bg-destructive/10"}
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription className={generationResult.success ? "text-green-800" : "text-red-800"}>
|
||||
|
|
@ -479,7 +479,7 @@ export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenCh
|
|||
<div>
|
||||
<strong>생성될 파일:</strong>
|
||||
</div>
|
||||
<ul className="ml-4 space-y-1 text-xs text-gray-600">
|
||||
<ul className="ml-4 space-y-1 text-xs text-muted-foreground">
|
||||
<li>• {formData.name.toLowerCase()}/index.ts</li>
|
||||
<li>
|
||||
• {formData.name.toLowerCase()}/{formData.name}Layout.tsx
|
||||
|
|
|
|||
|
|
@ -826,10 +826,10 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
|||
|
||||
{/* 선택된 화면 정보 표시 */}
|
||||
{selectedScreen && (
|
||||
<div className="rounded-md border bg-blue-50 p-3">
|
||||
<div className="rounded-md border bg-accent p-3">
|
||||
<div className="text-sm font-medium text-blue-900">{selectedScreen.screenName}</div>
|
||||
<div className="text-xs text-blue-600">코드: {selectedScreen.screenCode}</div>
|
||||
<div className="text-xs text-blue-600">생성된 URL: {formData.menuUrl}</div>
|
||||
<div className="text-xs text-primary">코드: {selectedScreen.screenCode}</div>
|
||||
<div className="text-xs text-primary">생성된 URL: {formData.menuUrl}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -828,7 +828,7 @@ export const MenuManagement: React.FC = () => {
|
|||
<CardContent className="space-y-3 pt-4">
|
||||
<Card
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedMenuType === "admin" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
||||
selectedMenuType === "admin" ? "border-primary bg-accent" : "hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleMenuTypeChange("admin")}
|
||||
>
|
||||
|
|
@ -836,7 +836,7 @@ export const MenuManagement: React.FC = () => {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium">{getUITextSync("menu.management.admin")}</h3>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{getUITextSync("menu.management.admin.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -849,7 +849,7 @@ export const MenuManagement: React.FC = () => {
|
|||
|
||||
<Card
|
||||
className={`cursor-pointer transition-all ${
|
||||
selectedMenuType === "user" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
|
||||
selectedMenuType === "user" ? "border-primary bg-accent" : "hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleMenuTypeChange("user")}
|
||||
>
|
||||
|
|
@ -857,7 +857,7 @@ export const MenuManagement: React.FC = () => {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium">{getUITextSync("menu.management.user")}</h3>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{getUITextSync("menu.management.user.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -997,7 +997,7 @@ export const MenuManagement: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{getUITextSync("menu.list.search.result", { count: getCurrentMenus().length })}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1006,7 +1006,7 @@ export const MenuManagement: React.FC = () => {
|
|||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const MenuTable: React.FC<MenuTableProps> = ({
|
|||
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<MenuTableProps> = ({
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{seq}</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`font-medium ${companyName && companyName !== getText(MENU_MANAGEMENT_KEYS.STATUS_UNSPECIFIED) ? "text-green-600" : "text-gray-500"}`}
|
||||
|
|
@ -253,12 +253,12 @@ export const MenuTable: React.FC<MenuTableProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-left text-sm text-gray-600">
|
||||
<TableCell className="text-left text-sm text-muted-foreground">
|
||||
<div className="max-w-[200px]">
|
||||
{menuUrl ? (
|
||||
<div className="group relative">
|
||||
<div
|
||||
className={`cursor-pointer transition-colors hover:text-blue-600 ${
|
||||
className={`cursor-pointer transition-colors hover:text-primary ${
|
||||
menuUrl.length > 30 ? "truncate" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -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 ? <Pause className="h-4 w-4 mr-1" /> : <Play className="h-4 w-4 mr-1" />}
|
||||
자동 새로고침
|
||||
|
|
@ -167,7 +167,7 @@ export default function MonitoringDashboard() {
|
|||
<div className="text-2xl">🔄</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-blue-600">{monitoring.running_jobs}</div>
|
||||
<div className="text-2xl font-bold text-primary">{monitoring.running_jobs}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
현재 실행 중인 작업
|
||||
</p>
|
||||
|
|
@ -193,7 +193,7 @@ export default function MonitoringDashboard() {
|
|||
<div className="text-2xl">❌</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{monitoring.failed_jobs_today}</div>
|
||||
<div className="text-2xl font-bold text-destructive">{monitoring.failed_jobs_today}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
주의가 필요한 작업
|
||||
</p>
|
||||
|
|
@ -269,7 +269,7 @@ export default function MonitoringDashboard() {
|
|||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
{execution.error_message ? (
|
||||
<span className="text-red-600 text-sm truncate block">
|
||||
<span className="text-destructive text-sm truncate block">
|
||||
{execution.error_message}
|
||||
</span>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -673,7 +673,7 @@ export default function MultiLangPage() {
|
|||
<button
|
||||
onClick={() => 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() {
|
|||
<button
|
||||
onClick={() => 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() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600">총 {languages.length}개의 언어가 등록되어 있습니다.</div>
|
||||
<div className="text-sm text-muted-foreground">총 {languages.length}개의 언어가 등록되어 있습니다.</div>
|
||||
<div className="flex space-x-2">
|
||||
{selectedLanguages.size > 0 && (
|
||||
<Button variant="destructive" onClick={handleDeleteLanguages}>
|
||||
|
|
@ -759,13 +759,13 @@ export default function MultiLangPage() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<div className="text-sm text-gray-600">검색 결과: {getFilteredLangKeys().length}건</div>
|
||||
<div className="text-sm text-muted-foreground">검색 결과: {getFilteredLangKeys().length}건</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div>
|
||||
<div className="mb-2 text-sm text-gray-600">전체: {getFilteredLangKeys().length}건</div>
|
||||
<div className="mb-2 text-sm text-muted-foreground">전체: {getFilteredLangKeys().length}건</div>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={getFilteredLangKeys()}
|
||||
|
|
|
|||
|
|
@ -231,10 +231,10 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
(selectedMenu as any).menu_name_kor ||
|
||||
"메뉴"}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
URL: {selectedMenu.menu_url || selectedMenu.MENU_URL || (selectedMenu as any).menu_url || "없음"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
설명:{" "}
|
||||
{selectedMenu.menu_desc || selectedMenu.MENU_DESC || (selectedMenu as any).menu_desc || "없음"}
|
||||
</p>
|
||||
|
|
@ -294,7 +294,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
{screen.isActive === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
테이블: {screen.tableName} | 생성일: {screen.createdDate.toLocaleDateString()}
|
||||
</p>
|
||||
{screen.description && <p className="mt-1 text-sm text-gray-500">{screen.description}</p>}
|
||||
|
|
@ -306,7 +306,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
setSelectedScreen(screen);
|
||||
setShowUnassignDialog(true);
|
||||
}}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
className="text-destructive hover:text-red-700"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -347,7 +347,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
<div
|
||||
key={screen.screenId}
|
||||
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
|
||||
selectedScreen?.screenId === screen.screenId ? "border-blue-500 bg-blue-50" : "hover:bg-gray-50"
|
||||
selectedScreen?.screenId === screen.screenId ? "border-primary bg-accent" : "hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => setSelectedScreen(screen)}
|
||||
>
|
||||
|
|
@ -357,7 +357,7 @@ export const ScreenAssignmentTab: React.FC<ScreenAssignmentTabProps> = ({ menus
|
|||
{screen.screenCode}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">테이블: {screen.tableName}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">테이블: {screen.tableName}</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">{code.codeValue || code.code_value}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{code.codeValue || code.code_value}</p>
|
||||
{code.description && <p className="mt-1 text-sm text-gray-500">{code.description}</p>}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<DialogTitle className={getTypeColor()}>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-gray-600">{message}</p>
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onClose} className="w-20">
|
||||
|
|
@ -398,7 +398,7 @@ export function UserFormModal({ isOpen, onClose, onSuccess }: UserFormModalProps
|
|||
{/* 중복확인 결과 메시지 */}
|
||||
{duplicateCheckMessage && (
|
||||
<div
|
||||
className={`mt-1 text-sm ${duplicateCheckType === "success" ? "text-green-600" : "text-red-600"}`}
|
||||
className={`mt-1 text-sm ${duplicateCheckType === "success" ? "text-green-600" : "text-destructive"}`}
|
||||
>
|
||||
{duplicateCheckMessage}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export function UserPasswordResetModal({ isOpen, onClose, userId, userName, onSu
|
|||
</div>
|
||||
|
||||
{/* 비밀번호 일치 여부 표시 */}
|
||||
{showMismatchError && <p className="text-sm text-red-600">비밀번호가 일치하지 않습니다.</p>}
|
||||
{showMismatchError && <p className="text-sm text-destructive">비밀번호가 일치하지 않습니다.</p>}
|
||||
{isPasswordMatch && <p className="text-sm text-green-600">비밀번호가 일치합니다.</p>}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
|
||||
|
|
@ -67,7 +67,7 @@ export function UserStatusConfirmDialog({
|
|||
<Button variant="outline" onClick={onCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={onConfirm} className={newStatus === "active" ? "bg-blue-500 hover:bg-blue-600" : ""}>
|
||||
<Button onClick={onConfirm} variant="default">
|
||||
변경
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export function CanvasElement({ element, isSelected, onUpdate, onRemove, onSelec
|
|||
<button
|
||||
className="
|
||||
w-6 h-6 flex items-center justify-center
|
||||
text-gray-400 hover:bg-blue-500 hover:text-white
|
||||
text-gray-400 hover:bg-accent0 hover:text-white
|
||||
rounded transition-colors duration-200
|
||||
"
|
||||
onClick={() => onConfigure(element)}
|
||||
|
|
@ -240,7 +240,7 @@ export function CanvasElement({ element, isSelected, onUpdate, onRemove, onSelec
|
|||
<button
|
||||
className="
|
||||
element-close w-6 h-6 flex items-center justify-center
|
||||
text-gray-400 hover:bg-red-500 hover:text-white
|
||||
text-gray-400 hover:bg-destructive/100 hover:text-white
|
||||
rounded transition-colors duration-200
|
||||
"
|
||||
onClick={handleRemove}
|
||||
|
|
@ -259,7 +259,7 @@ export function CanvasElement({ element, isSelected, onUpdate, onRemove, onSelec
|
|||
{isLoadingData ? (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-500">
|
||||
<div className="text-center">
|
||||
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<div className="text-sm">데이터 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC
|
|||
{/* 설정 미리보기 */}
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-2">📋 설정 미리보기</div>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div><strong>X축:</strong> {currentConfig.xAxis || '미설정'}</div>
|
||||
<div>
|
||||
<strong>Y축:</strong>{' '}
|
||||
|
|
@ -240,7 +240,7 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC
|
|||
)}
|
||||
<div><strong>데이터 행 수:</strong> {queryResult.rows.length}개</div>
|
||||
{Array.isArray(currentConfig.yAxis) && currentConfig.yAxis.length > 1 && (
|
||||
<div className="text-blue-600 mt-2">
|
||||
<div className="text-primary mt-2">
|
||||
✨ 다중 시리즈 차트가 생성됩니다!
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -249,7 +249,7 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC
|
|||
|
||||
{/* 필수 필드 확인 */}
|
||||
{(!currentConfig.xAxis || !currentConfig.yAxis) && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||
<div className="text-red-800 text-sm">
|
||||
⚠️ X축과 Y축을 모두 설정해야 차트가 표시됩니다.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
w-full min-h-full relative
|
||||
bg-gray-100
|
||||
bg-grid-pattern
|
||||
${isDragOver ? 'bg-blue-50' : ''}
|
||||
${isDragOver ? 'bg-accent' : ''}
|
||||
`}
|
||||
style={{
|
||||
backgroundImage: `
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export default function DashboardDesigner() {
|
|||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<div className="text-lg font-medium text-gray-700">대시보드 로딩 중...</div>
|
||||
<div className="text-sm text-gray-500 mt-1">잠시만 기다려주세요</div>
|
||||
</div>
|
||||
|
|
@ -221,7 +221,7 @@ export default function DashboardDesigner() {
|
|||
<div className="flex-1 relative overflow-auto border-r-2 border-gray-300">
|
||||
{/* 편집 중인 대시보드 표시 */}
|
||||
{dashboardTitle && (
|
||||
<div className="absolute top-2 left-2 z-10 bg-blue-500 text-white px-3 py-1 rounded-lg text-sm font-medium shadow-lg">
|
||||
<div className="absolute top-2 left-2 z-10 bg-accent0 text-white px-3 py-1 rounded-lg text-sm font-medium shadow-lg">
|
||||
📝 편집 중: {dashboardTitle}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
<DraggableItem
|
||||
icon="📚"
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
|||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
{element.title} 설정
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
데이터 소스와 차트 설정을 구성하세요
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 text-2xl"
|
||||
className="text-gray-400 hover:text-muted-foreground text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
|
@ -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
|
|||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
className="px-4 py-2 text-muted-foreground border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
@ -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
|
||||
"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;`
|
|||
|
||||
{/* 샘플 쿼리 버튼들 */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<span className="text-sm text-gray-600">샘플 쿼리:</span>
|
||||
<span className="text-sm text-muted-foreground">샘플 쿼리:</span>
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
🔥 제품 비교
|
||||
</button>
|
||||
|
|
@ -224,7 +224,7 @@ ORDER BY Q4 DESC;`
|
|||
|
||||
{/* 새로고침 간격 설정 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-sm text-gray-600">자동 새로고침:</label>
|
||||
<label className="text-sm text-muted-foreground">자동 새로고침:</label>
|
||||
<select
|
||||
value={dataSource?.refreshInterval || 30000}
|
||||
onChange={(e) => onDataSourceChange({
|
||||
|
|
@ -246,7 +246,7 @@ ORDER BY Q4 DESC;`
|
|||
|
||||
{/* 오류 메시지 */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||
<div className="text-red-800 text-sm font-medium">❌ 오류</div>
|
||||
<div className="text-red-700 text-sm mt-1">{error}</div>
|
||||
</div>
|
||||
|
|
@ -282,7 +282,7 @@ ORDER BY Q4 DESC;`
|
|||
{queryResult.rows.slice(0, 10).map((row, idx) => (
|
||||
<tr key={idx} className="border-b border-gray-100">
|
||||
{queryResult.columns.map((col, colIdx) => (
|
||||
<td key={colIdx} className="py-1 px-2 text-gray-600">
|
||||
<td key={colIdx} className="py-1 px-2 text-muted-foreground">
|
||||
{String(row[col] ?? '')}
|
||||
</td>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function AuthGuard({
|
|||
console.log("AuthGuard: 로딩 중 - fallback 표시");
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 rounded bg-blue-100 p-4">
|
||||
<div className="mb-4 rounded bg-primary/20 p-4">
|
||||
<h3 className="font-bold">AuthGuard 로딩 중...</h3>
|
||||
<pre className="text-xs">{JSON.stringify(authDebugInfo, null, 2)}</pre>
|
||||
</div>
|
||||
|
|
@ -129,10 +129,10 @@ export function AuthGuard({
|
|||
console.log("AuthGuard: 인증 실패 - fallback 표시");
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 rounded bg-red-100 p-4">
|
||||
<div className="mb-4 rounded bg-destructive/20 p-4">
|
||||
<h3 className="font-bold">인증 실패</h3>
|
||||
{redirectCountdown !== null && (
|
||||
<div className="mb-2 text-red-600">
|
||||
<div className="mb-2 text-destructive">
|
||||
<strong>리다이렉트 카운트다운:</strong> {redirectCountdown}초 후 {redirectTo}로 이동
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -150,7 +150,7 @@ export function AuthGuard({
|
|||
<div className="mb-4 rounded bg-orange-100 p-4">
|
||||
<h3 className="font-bold">관리자 권한 없음</h3>
|
||||
{redirectCountdown !== null && (
|
||||
<div className="mb-2 text-red-600">
|
||||
<div className="mb-2 text-destructive">
|
||||
<strong>리다이렉트 카운트다운:</strong> {redirectCountdown}초 후 {redirectTo}로 이동
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ export function ErrorMessage({ message }: ErrorMessageProps) {
|
|||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<div className="my-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{message}</div>
|
||||
<div className="my-4 rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-red-700">{message}</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-xl bg-slate-900 shadow-lg">
|
||||
<Shield className="h-10 w-10 text-white" />
|
||||
<div className="mx-auto mb-2 flex items-center justify-center">
|
||||
<Image
|
||||
src="/images/vexplor.png"
|
||||
alt={UI_CONFIG.COMPANY_NAME}
|
||||
width={180}
|
||||
height={60}
|
||||
className="object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<h1 className="mb-2 text-3xl font-bold text-slate-900">{UI_CONFIG.COMPANY_NAME}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 || "-"}
|
||||
|
|
|
|||
|
|
@ -42,63 +42,36 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ 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<ScreenModalProps> = ({ 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<ScreenModalProps> = ({ 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<ScreenModalProps> = ({ className }) => {
|
|||
<DialogDescription>{loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 p-4">
|
||||
<div className="flex-1 flex items-center justify-center overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||
<p className="text-gray-600">화면을 불러오는 중...</p>
|
||||
<p className="text-muted-foreground">화면을 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : screenData ? (
|
||||
|
|
@ -229,6 +216,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ 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<ScreenModalProps> = ({ className }) => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-gray-600">화면 데이터가 없습니다.</p>
|
||||
<p className="text-muted-foreground">화면 데이터가 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ export function ValidationMessage({ message, isValid, isLoading, className }: Va
|
|||
}
|
||||
|
||||
return (
|
||||
<p className={cn("text-sm transition-colors", isValid ? "text-green-600" : "text-red-600", className)}>{message}</p>
|
||||
<p className={cn("text-sm transition-colors", isValid ? "text-green-600" : "text-destructive", className)}>{message}</p>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,10 +105,10 @@ export function DashboardViewer({ elements, dashboardId, refreshInterval }: Dash
|
|||
return (
|
||||
<div className="relative w-full h-full bg-gray-100 overflow-auto">
|
||||
{/* 새로고침 상태 표시 */}
|
||||
<div className="absolute top-4 right-4 z-10 bg-white rounded-lg shadow-sm px-3 py-2 text-xs text-gray-600">
|
||||
<div className="absolute top-4 right-4 z-10 bg-white rounded-lg shadow-sm px-3 py-2 text-xs text-muted-foreground">
|
||||
마지막 업데이트: {lastRefresh.toLocaleTimeString()}
|
||||
{Array.from(loadingElements).length > 0 && (
|
||||
<span className="ml-2 text-blue-600">
|
||||
<span className="ml-2 text-primary">
|
||||
({Array.from(loadingElements).length}개 로딩 중...)
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -164,7 +164,7 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
|||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
className="text-gray-400 hover:text-muted-foreground disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
|
@ -203,8 +203,8 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
|||
{isLoading && (
|
||||
<div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<div className="text-sm text-gray-600">업데이트 중...</div>
|
||||
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
<div className="text-sm text-muted-foreground">업데이트 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<Calendar className="mr-1 h-3 w-3 text-gray-400" />
|
||||
{new Date(diagram.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
|
|
@ -269,7 +269,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Copy className="mr-2 h-4 w-4" />
|
||||
복사
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(diagram)} className="text-red-600">
|
||||
<DropdownMenuItem onClick={() => handleDelete(diagram)} className="text-destructive">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
|
|
@ -302,7 +302,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
>
|
||||
이전
|
||||
</Button>
|
||||
<span className="text-sm text-gray-600">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
|
|
@ -342,11 +342,11 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-600">관계 삭제</DialogTitle>
|
||||
<DialogTitle className="text-destructive">관계 삭제</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계를 완전히 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
<span className="font-medium text-destructive">
|
||||
이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다.
|
||||
</span>
|
||||
</DialogDescription>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
|
||||
<button
|
||||
onClick={onClearAll}
|
||||
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
||||
className="w-full rounded-lg bg-destructive/100 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
||||
>
|
||||
🗑️ 전체 삭제
|
||||
</button>
|
||||
|
|
@ -72,7 +72,7 @@ export const DataFlowSidebar: React.FC<DataFlowSidebarProps> = ({
|
|||
{/* 통계 정보 */}
|
||||
<div className="mt-6 rounded-lg bg-gray-50 p-4">
|
||||
<div className="mb-2 text-sm font-semibold text-gray-700">통계</div>
|
||||
<div className="space-y-1 text-sm text-gray-600">
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<div className="flex justify-between">
|
||||
<span>테이블 노드:</span>
|
||||
<span className="font-medium">{nodes.length}개</span>
|
||||
|
|
|
|||
|
|
@ -85,11 +85,11 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
|
||||
{/* 관계 화살표 */}
|
||||
<div className="flex justify-center">
|
||||
<span className="text-l text-gray-600">→</span>
|
||||
<span className="text-l text-muted-foreground">→</span>
|
||||
</div>
|
||||
|
||||
{/* To 테이블 */}
|
||||
<div className="rounded-lg border-l-4 border-blue-400 bg-blue-50 p-3">
|
||||
<div className="rounded-lg border-l-4 border-blue-400 bg-accent p-3">
|
||||
<div className="mb-2 text-xs font-bold tracking-wide text-blue-700 uppercase">TO</div>
|
||||
<div className="mb-2 text-base font-bold text-gray-800">{edgeInfo.toTable}</div>
|
||||
<div className="space-y-1">
|
||||
|
|
@ -97,7 +97,7 @@ export const EdgeInfoPanel: React.FC<EdgeInfoPanelProps> = ({
|
|||
{edgeInfo.toColumns.map((column, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center rounded-md bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 ring-1 ring-blue-200"
|
||||
className="inline-flex items-center rounded-md bg-primary/20 px-2.5 py-0.5 text-xs font-medium text-blue-800 ring-1 ring-blue-200"
|
||||
>
|
||||
{column}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -134,18 +134,18 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto absolute top-4 right-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
|
||||
<div className="pointer-events-auto absolute top-4 right-4 z-40 w-80 rounded-xl border border-primary/20 bg-white shadow-lg">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded-full bg-blue-100 p-1">
|
||||
<span className="text-sm text-blue-600">🔗</span>
|
||||
<div className="rounded-full bg-primary/20 p-1">
|
||||
<span className="text-sm text-primary">🔗</span>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-gray-800">테이블 간 관계 목록</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-gray-400 transition-colors hover:bg-gray-100 hover:text-muted-foreground"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
|
|
@ -159,7 +159,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
{relationships.map((relationship) => (
|
||||
<div
|
||||
key={relationship.id}
|
||||
className="rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-blue-50"
|
||||
className="rounded-lg border border-gray-200 p-3 transition-all hover:border-blue-300 hover:bg-accent"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-gray-900">
|
||||
|
|
@ -172,7 +172,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
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="관계 편집"
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -190,7 +190,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
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="관계 삭제"
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -204,7 +204,7 @@ export const RelationshipListModal: React.FC<RelationshipListModalProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<p>타입: {relationship.connectionType}</p>
|
||||
<p>From: {relationship.fromTable}</p>
|
||||
<p>To: {relationship.toTable}</p>
|
||||
|
|
|
|||
|
|
@ -149,26 +149,26 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
onKeyPress={handleKeyPress}
|
||||
placeholder="예: 사용자-부서 관계도"
|
||||
disabled={isLoading}
|
||||
className={nameError ? "border-red-500 focus:border-red-500" : ""}
|
||||
className={nameError ? "border-destructive focus:border-destructive" : ""}
|
||||
/>
|
||||
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
|
||||
{nameError && <p className="text-sm text-destructive">{nameError}</p>}
|
||||
</div>
|
||||
|
||||
{/* 관계 요약 정보 */}
|
||||
<div className="grid grid-cols-3 gap-4 rounded-lg bg-gray-50 p-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">{relationships.length}</div>
|
||||
<div className="text-sm text-gray-600">관계 수</div>
|
||||
<div className="text-2xl font-bold text-primary">{relationships.length}</div>
|
||||
<div className="text-sm text-muted-foreground">관계 수</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{connectedTables.length}</div>
|
||||
<div className="text-sm text-gray-600">연결된 테이블</div>
|
||||
<div className="text-sm text-muted-foreground">연결된 테이블</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">연결된 컬럼</div>
|
||||
<div className="text-sm text-muted-foreground">연결된 컬럼</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
{relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-600">
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{relationship.fromTable} → {relationship.toTable}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
canCreateConnection,
|
||||
}) => {
|
||||
return (
|
||||
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
|
||||
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-primary/20 bg-white shadow-lg">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-sm text-blue-600">📋</span>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/20">
|
||||
<span className="text-sm text-primary">📋</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800">선택된 테이블</div>
|
||||
|
|
@ -44,7 +44,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-muted-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
|
@ -66,8 +66,8 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
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"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
|
|
@ -88,7 +88,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">{tableName}</div>
|
||||
<div className="text-xs text-muted-foreground">{tableName}</div>
|
||||
</div>
|
||||
|
||||
{/* 연결 화살표 (마지막이 아닌 경우) */}
|
||||
|
|
@ -110,7 +110,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
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<SelectedTablesPanelProps> = ({
|
|||
</button>
|
||||
<button
|
||||
onClick={onClear}
|
||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
|
||||
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-muted-foreground hover:bg-gray-300"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
<span>초기화</span>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => {
|
|||
<div
|
||||
key={columnKey}
|
||||
className={`relative cursor-pointer rounded px-2 py-1 text-xs transition-colors ${
|
||||
isSelected ? "bg-blue-100 text-blue-800 ring-2 ring-blue-500" : "text-gray-700 hover:bg-gray-100"
|
||||
isSelected ? "bg-primary/20 text-blue-800 ring-2 ring-blue-500" : "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => onColumnClick(table.tableName, columnKey)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
</div>
|
||||
|
||||
{/* 오류 메시지 */}
|
||||
{error && <div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
|
||||
{error && <div className="rounded-lg bg-destructive/10 p-4 text-sm text-destructive">{error}</div>}
|
||||
|
||||
{/* 테이블 목록 */}
|
||||
<div className="max-h-96 space-y-2 overflow-y-auto">
|
||||
|
|
@ -114,7 +114,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
<Card
|
||||
key={table.tableName}
|
||||
className={`cursor-pointer transition-all hover:shadow-md ${
|
||||
isSelected ? "cursor-not-allowed border-blue-500 bg-blue-50 opacity-60" : "hover:border-gray-300"
|
||||
isSelected ? "cursor-not-allowed border-primary bg-accent opacity-60" : "hover:border-gray-300"
|
||||
}`}
|
||||
onDoubleClick={() => !isSelected && handleAddTable(table)}
|
||||
>
|
||||
|
|
@ -126,10 +126,10 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Database className="h-3 w-3" />
|
||||
<span className="font-mono">{table.tableName}</span>
|
||||
{isSelected && <span className="font-medium text-blue-600">(추가됨)</span>}
|
||||
{isSelected && <span className="font-medium text-primary">(추가됨)</span>}
|
||||
</div>
|
||||
|
||||
{table.description && <p className="line-clamp-2 text-xs text-gray-500">{table.description}</p>}
|
||||
|
|
@ -142,7 +142,7 @@ export const TableSelector: React.FC<TableSelectorProps> = ({ companyCode, onTab
|
|||
</div>
|
||||
|
||||
{/* 통계 정보 */}
|
||||
<div className="rounded-lg bg-gray-50 p-3 text-xs text-gray-600">
|
||||
<div className="rounded-lg bg-gray-50 p-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>전체 테이블: {tables.length}개</span>
|
||||
{searchTerm && <span>검색 결과: {filteredTables.length}개</span>}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
value={condition.logicalOperator || "AND"}
|
||||
onValueChange={(value: "AND" | "OR") => onUpdateCondition(index, "logicalOperator", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
||||
<SelectTrigger className="h-8 w-24 border-primary/20 bg-accent text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -92,11 +92,11 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
)}
|
||||
{/* 그룹 레벨에 따른 들여쓰기 */}
|
||||
<div
|
||||
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
|
||||
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-accent/50 p-2"
|
||||
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
||||
>
|
||||
<span className="font-mono text-sm text-blue-600">(</span>
|
||||
<span className="text-xs text-blue-600">그룹 시작</span>
|
||||
<span className="font-mono text-sm text-primary">(</span>
|
||||
<span className="text-xs text-primary">그룹 시작</span>
|
||||
<Button size="sm" variant="ghost" onClick={() => onRemoveCondition(index)} className="h-6 w-6 p-0">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
|
@ -110,11 +110,11 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
return (
|
||||
<div key={condition.id} className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
|
||||
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-accent/50 p-2"
|
||||
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
||||
>
|
||||
<span className="font-mono text-sm text-blue-600">)</span>
|
||||
<span className="text-xs text-blue-600">그룹 끝</span>
|
||||
<span className="font-mono text-sm text-primary">)</span>
|
||||
<span className="text-xs text-primary">그룹 끝</span>
|
||||
<Button size="sm" variant="ghost" onClick={() => onRemoveCondition(index)} className="h-6 w-6 p-0">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
|
@ -126,7 +126,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
value={conditions[index + 1]?.logicalOperator || "AND"}
|
||||
onValueChange={(value: "AND" | "OR") => onUpdateCondition(index + 1, "logicalOperator", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
||||
<SelectTrigger className="h-8 w-24 border-primary/20 bg-accent text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -150,7 +150,7 @@ export const ConditionRenderer: React.FC<ConditionRendererProps> = ({
|
|||
value={condition.logicalOperator || "AND"}
|
||||
onValueChange={(value: "AND" | "OR") => onUpdateCondition(index, "logicalOperator", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
||||
<SelectTrigger className="h-8 w-24 border-primary/20 bg-accent text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ export const WebTypeInput: React.FC<WebTypeInputProps> = ({
|
|||
multiple={detailSettings.multiple as boolean}
|
||||
/>
|
||||
{value && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>선택된 파일: {value}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -84,24 +84,24 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
|||
<summary
|
||||
className={`flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium hover:bg-gray-50 hover:text-gray-900 ${
|
||||
isConditionRequired && !hasValidConditions
|
||||
? "border-red-300 bg-red-50 text-red-700"
|
||||
? "border-red-300 bg-destructive/10 text-red-700"
|
||||
: "border-gray-200 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
🔍 이 액션의 실행 조건
|
||||
{isConditionRequired ? (
|
||||
<span className="rounded bg-red-100 px-1 py-0.5 text-xs font-semibold text-red-700">필수</span>
|
||||
<span className="rounded bg-destructive/20 px-1 py-0.5 text-xs font-semibold text-red-700">필수</span>
|
||||
) : (
|
||||
<span className="text-gray-500">(선택사항)</span>
|
||||
)}
|
||||
{action.conditions && action.conditions.length > 0 && (
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">
|
||||
<span className="rounded-full bg-primary/20 px-2 py-0.5 text-xs text-blue-700">
|
||||
{action.conditions.length}개
|
||||
</span>
|
||||
)}
|
||||
{isConditionRequired && !hasValidConditions && (
|
||||
<span className="rounded bg-red-100 px-1 py-0.5 text-xs text-red-600">⚠️ 조건 필요</span>
|
||||
<span className="rounded bg-destructive/20 px-1 py-0.5 text-xs text-destructive">⚠️ 조건 필요</span>
|
||||
)}
|
||||
</div>
|
||||
{action.conditions && action.conditions.length > 0 && (
|
||||
|
|
@ -151,8 +151,8 @@ export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = (
|
|||
<div
|
||||
className={`rounded border p-3 text-xs ${
|
||||
isConditionRequired
|
||||
? "border-red-200 bg-red-50 text-red-700"
|
||||
: "border-gray-200 bg-gray-50 text-gray-600"
|
||||
? "border-destructive/20 bg-destructive/10 text-red-700"
|
||||
: "border-gray-200 bg-gray-50 text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{isConditionRequired ? (
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
|||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium">필드 매핑</Label>
|
||||
<span className="text-xs text-red-600">(필수)</span>
|
||||
<span className="text-xs text-destructive">(필수)</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={addFieldMapping} className="h-6 text-xs">
|
||||
<Plus className="mr-1 h-2 w-2" />
|
||||
|
|
@ -244,7 +244,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
|||
{/* 컴팩트한 매핑 표시 */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{/* 소스 */}
|
||||
<div className="flex items-center gap-1 rounded bg-blue-50 px-2 py-1">
|
||||
<div className="flex items-center gap-1 rounded bg-accent px-2 py-1">
|
||||
<Select
|
||||
value={mapping.sourceTable || "__EMPTY__"}
|
||||
onValueChange={(value) => {
|
||||
|
|
@ -277,7 +277,7 @@ export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
|||
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<ActionFieldMappingsProps> = ({
|
|||
|
||||
{/* 필드 매핑이 없을 때 안내 메시지 */}
|
||||
{action.fieldMappings.length === 0 && (
|
||||
<div className="rounded border border-red-200 bg-red-50 p-3 text-xs text-red-700">
|
||||
<div className="rounded border border-destructive/20 bg-destructive/10 p-3 text-xs text-red-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-red-500">⚠️</span>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const ColumnTableSection: React.FC<ColumnTableSectionProps> = ({
|
|||
: 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<ColumnTableSectionProps> = ({
|
|||
: 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<ColumnTableSectionProps> = ({
|
|||
|
||||
{isMapped && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="truncate text-xs text-blue-600">← {mapping.fromColumnName}</span>
|
||||
<span className="truncate text-xs text-primary">← {mapping.fromColumnName}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -327,7 +327,7 @@ export const ColumnTableSection: React.FC<ColumnTableSectionProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 하단 통계 */}
|
||||
<div className="rounded-b-lg border border-gray-200 bg-gray-50 p-3 text-xs text-gray-600">
|
||||
<div className="rounded-b-lg border border-gray-200 bg-gray-50 p-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
{isFromTable ? "매핑됨" : "설정됨"}: {mappedCount}/{columns.length}
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
|
|||
<div
|
||||
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
||||
config.connectionType === "simple-key"
|
||||
? "border-blue-500 bg-blue-50"
|
||||
? "border-primary bg-accent"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
onClick={() => onConfigChange({ ...config, connectionType: "simple-key" })}
|
||||
>
|
||||
<Key className="mx-auto h-6 w-6 text-blue-500" />
|
||||
<div className="mt-1 text-xs font-medium">단순 키값 연결</div>
|
||||
<div className="text-xs text-gray-600">중계 테이블 생성</div>
|
||||
<div className="text-xs text-muted-foreground">중계 테이블 생성</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -38,7 +38,7 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
|
|||
>
|
||||
<Save className="mx-auto h-6 w-6 text-green-500" />
|
||||
<div className="mt-1 text-xs font-medium">데이터 저장</div>
|
||||
<div className="text-xs text-gray-600">필드 매핑 저장</div>
|
||||
<div className="text-xs text-muted-foreground">필드 매핑 저장</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -54,7 +54,7 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
|
|||
>
|
||||
<Globe className="mx-auto h-6 w-6 text-orange-500" />
|
||||
<div className="mt-1 text-xs font-medium">외부 호출</div>
|
||||
<div className="text-xs text-gray-600">API/이메일 호출</div>
|
||||
<div className="text-xs text-muted-foreground">API/이메일 호출</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ export const DeleteConditionPanel: React.FC<DeleteConditionPanelProps> = ({
|
|||
<SelectContent>
|
||||
<SelectItem value="=">=</SelectItem>
|
||||
<SelectItem value="!=">
|
||||
<span className="text-red-600">!=</span>
|
||||
<span className="text-destructive">!=</span>
|
||||
</SelectItem>
|
||||
<SelectItem value=">">></SelectItem>
|
||||
<SelectItem value="<"><</SelectItem>
|
||||
|
|
@ -308,11 +308,11 @@ export const DeleteConditionPanel: React.FC<DeleteConditionPanelProps> = ({
|
|||
<SelectItem value="LIKE">LIKE</SelectItem>
|
||||
<SelectItem value="IN">IN</SelectItem>
|
||||
<SelectItem value="NOT IN">
|
||||
<span className="text-red-600">NOT IN</span>
|
||||
<span className="text-destructive">NOT IN</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="EXISTS">EXISTS</SelectItem>
|
||||
<SelectItem value="NOT EXISTS">
|
||||
<span className="text-red-600">NOT EXISTS</span>
|
||||
<span className="text-destructive">NOT EXISTS</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
|
|||
|
|
@ -446,9 +446,9 @@ export const InsertFieldMappingPanel: React.FC<InsertFieldMappingPanelProps> = (
|
|||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-800">매핑 진행 상황</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
총 {toTableColumns.length}개 컬럼 중{" "}
|
||||
<span className="font-bold text-blue-600">
|
||||
<span className="font-bold text-primary">
|
||||
{columnMappings.filter((m) => m.fromColumnName || (m.defaultValue && m.defaultValue.trim())).length}
|
||||
개
|
||||
</span>{" "}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
{/* 현재 선택된 테이블 표시 */}
|
||||
<div className="mb-4 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-gray-600">From 테이블</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">From 테이블</Label>
|
||||
<div className="mt-1">
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable}
|
||||
|
|
@ -54,7 +54,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-gray-600">To 테이블</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">To 테이블</Label>
|
||||
<div className="mt-1">
|
||||
<span className="text-sm font-medium text-gray-800">
|
||||
{availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable}
|
||||
|
|
@ -67,7 +67,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
{/* 컬럼 선택 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-gray-600">From 컬럼</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">From 컬럼</Label>
|
||||
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
|
||||
{fromTableColumns.map((column) => (
|
||||
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
|
||||
|
|
@ -100,7 +100,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-gray-600">To 컬럼</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">To 컬럼</Label>
|
||||
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
|
||||
{toTableColumns.map((column) => (
|
||||
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
|
||||
|
|
@ -137,7 +137,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
|
||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-gray-600">선택된 From 컬럼</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">선택된 From 컬럼</Label>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{selectedFromColumns.length > 0 ? (
|
||||
selectedFromColumns.map((column) => {
|
||||
|
|
@ -156,7 +156,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs font-medium text-gray-600">선택된 To 컬럼</Label>
|
||||
<Label className="text-xs font-medium text-muted-foreground">선택된 To 컬럼</Label>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{selectedToColumns.length > 0 ? (
|
||||
selectedToColumns.map((column) => {
|
||||
|
|
@ -178,7 +178,7 @@ export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 단순 키값 연결 설정 */}
|
||||
<div className="rounded-lg border border-l-4 border-l-blue-500 bg-blue-50/30 p-4">
|
||||
<div className="rounded-lg border border-l-4 border-l-blue-500 bg-accent/30 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Key className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">단순 키값 연결 설정</span>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const DataConnectionDesigner: React.FC = () => {
|
|||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
🎨 제어관리 - 데이터 연결 설정
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
<p className="text-muted-foreground mt-1">
|
||||
시각적 필드 매핑으로 데이터 연결을 쉽게 설정하세요
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ connectionType }) =
|
|||
<>
|
||||
{/* 트랜잭션 설정 - 컴팩트 */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-gray-600">🔄 트랜잭션 설정</h4>
|
||||
<h4 className="text-xs font-medium text-muted-foreground">🔄 트랜잭션 설정</h4>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="batchSize" className="text-xs text-gray-500">
|
||||
|
|
@ -98,7 +98,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ connectionType }) =
|
|||
<>
|
||||
{/* API 호출 설정 - 컴팩트 */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-gray-600">🌐 API 호출 설정</h4>
|
||||
<h4 className="text-xs font-medium text-muted-foreground">🌐 API 호출 설정</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="timeout" className="text-xs text-gray-500">
|
||||
|
|
@ -131,7 +131,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ connectionType }) =
|
|||
|
||||
{/* 로깅 설정 - 컴팩트 */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-gray-600">📝 로깅 설정</h4>
|
||||
<h4 className="text-xs font-medium text-muted-foreground">📝 로깅 설정</h4>
|
||||
<div>
|
||||
<Select value={settings.logLevel} onValueChange={(value) => handleSettingChange("logLevel", value)}>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
|
|
|
|||
|
|
@ -49,13 +49,13 @@ export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({
|
|||
<div className={`p-2 rounded-lg ${
|
||||
connectionType === type.id
|
||||
? "bg-orange-100 text-orange-600"
|
||||
: "bg-gray-100 text-gray-600"
|
||||
: "bg-gray-100 text-muted-foreground"
|
||||
}`}>
|
||||
{type.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{type.label}</h3>
|
||||
<p className="text-sm text-gray-600">{type.description}</p>
|
||||
<p className="text-sm text-muted-foreground">{type.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,9 +88,9 @@ const LeftPanel: React.FC<LeftPanelProps> = ({ state, actions }) => {
|
|||
{state.connectionType === "external_call" && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="rounded-md bg-blue-50 p-3">
|
||||
<div className="rounded-md bg-accent p-3">
|
||||
<h3 className="mb-1 text-sm font-medium text-blue-800">외부 호출 모드</h3>
|
||||
<p className="text-xs text-blue-600">우측 패널에서 REST API 설정을 구성하세요.</p>
|
||||
<p className="text-xs text-primary">우측 패널에서 REST API 설정을 구성하세요.</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export const MappingInfoPanel: React.FC<MappingInfoPanelProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 p-3 rounded-lg border border-red-200">
|
||||
<div className="bg-destructive/10 p-3 rounded-lg border border-destructive/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="w-4 h-4 text-red-600" />
|
||||
<XCircle className="w-4 h-4 text-destructive" />
|
||||
<span className="text-sm font-medium text-red-800">오류 매핑</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-red-900 mt-1">
|
||||
|
|
@ -45,9 +45,9 @@ export const MappingInfoPanel: React.FC<MappingInfoPanelProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
||||
<div className="bg-accent p-3 rounded-lg border border-primary/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="w-4 h-4 text-blue-600" />
|
||||
<Database className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm font-medium text-blue-800">총 매핑</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-900 mt-1">
|
||||
|
|
@ -88,7 +88,7 @@ export const MappingInfoPanel: React.FC<MappingInfoPanelProps> = ({
|
|||
? "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<MappingInfoPanelProps> = ({
|
|||
{mapping.isValid ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-red-600" />
|
||||
<XCircle className="w-4 h-4 text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mapping.validationMessage && (
|
||||
<p className="text-xs text-red-600 mt-1">
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{mapping.validationMessage}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
|||
.map((column) => (
|
||||
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600">📤</span>
|
||||
<span className="text-primary">📤</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.webType || column.dataType}
|
||||
|
|
@ -359,7 +359,7 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
|||
|
||||
{/* 선택된 날짜 타입에 대한 설명 */}
|
||||
{mapping.value?.startsWith("#") && mapping.value !== "#custom" && (
|
||||
<div className="text-muted-foreground rounded bg-blue-50 p-2 text-xs">
|
||||
<div className="text-muted-foreground rounded bg-accent p-2 text-xs">
|
||||
{mapping.value === "#NOW" && "⏰ 현재 날짜와 시간이 저장됩니다"}
|
||||
{mapping.value === "#TODAY" && "📅 현재 날짜 (00:00:00)가 저장됩니다"}
|
||||
{mapping.value === "#YESTERDAY" && "📅 어제 날짜가 저장됩니다"}
|
||||
|
|
@ -497,7 +497,7 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
|||
.map((column) => (
|
||||
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600">📤</span>
|
||||
<span className="text-primary">📤</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
|
@ -625,7 +625,7 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
|
|||
.map((column) => (
|
||||
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600">📤</span>
|
||||
<span className="text-primary">📤</span>
|
||||
<span>{column.displayName || column.columnName}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const ConnectionStep: React.FC<ConnectionStepProps> = ({
|
|||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
연결 선택
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
데이터를 가져올 연결과 저장할 연결을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -89,8 +89,8 @@ export const ConnectionStep: React.FC<ConnectionStepProps> = ({
|
|||
{/* FROM 연결 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-blue-600 font-bold">1</span>
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<span className="text-primary font-bold">1</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">FROM 연결</h3>
|
||||
<span className="text-sm text-gray-500">(데이터 소스)</span>
|
||||
|
|
@ -102,20 +102,20 @@ export const ConnectionStep: React.FC<ConnectionStepProps> = ({
|
|||
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)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="w-6 h-6 text-blue-600" />
|
||||
<Database className="w-6 h-6 text-primary" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{connection.name}</h4>
|
||||
<p className="text-sm text-gray-600">{connection.type}</p>
|
||||
<p className="text-sm text-muted-foreground">{connection.type}</p>
|
||||
<p className="text-xs text-gray-500">{connection.host}:{connection.port}</p>
|
||||
</div>
|
||||
{selectedFrom === connection.id && (
|
||||
<CheckCircle className="w-5 h-5 text-blue-600" />
|
||||
<CheckCircle className="w-5 h-5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,7 +155,7 @@ export const ConnectionStep: React.FC<ConnectionStepProps> = ({
|
|||
<Database className="w-6 h-6 text-green-600" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{connection.name}</h4>
|
||||
<p className="text-sm text-gray-600">{connection.type}</p>
|
||||
<p className="text-sm text-muted-foreground">{connection.type}</p>
|
||||
<p className="text-xs text-gray-500">{connection.host}:{connection.port}</p>
|
||||
</div>
|
||||
{selectedTo === connection.id && (
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ const ControlConditionStep: React.FC<ControlConditionStepProps> = ({ state, acti
|
|||
<CardContent className="flex h-full flex-col overflow-hidden p-0">
|
||||
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto p-4">
|
||||
{/* 제어 실행 조건 안내 */}
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<div className="rounded-lg border border-primary/20 bg-accent p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-blue-800">제어 실행 조건이란?</h4>
|
||||
<div className="space-y-1 text-sm text-blue-700">
|
||||
<p>
|
||||
|
|
@ -363,7 +363,7 @@ const ControlConditionStep: React.FC<ControlConditionStepProps> = ({ 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"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -71,14 +71,14 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
필드 매핑
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
소스 테이블의 필드를 대상 테이블의 필드에 드래그하여 매핑하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 매핑 통계 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<div className="bg-accent p-4 rounded-lg border border-primary/20">
|
||||
<div className="text-2xl font-bold text-blue-900">{fieldMappings.length}</div>
|
||||
<div className="text-sm text-blue-700">총 매핑</div>
|
||||
</div>
|
||||
|
|
@ -88,7 +88,7 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
</div>
|
||||
<div className="text-sm text-green-700">유효한 매핑</div>
|
||||
</div>
|
||||
<div className="bg-red-50 p-4 rounded-lg border border-red-200">
|
||||
<div className="bg-destructive/10 p-4 rounded-lg border border-destructive/20">
|
||||
<div className="text-2xl font-bold text-red-900">
|
||||
{fieldMappings.filter(m => !m.isValid).length}
|
||||
</div>
|
||||
|
|
@ -107,8 +107,8 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
{/* FROM 테이블 필드들 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
<div className="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-blue-600 font-bold text-sm">FROM</span>
|
||||
<div className="w-6 h-6 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<span className="text-primary font-bold text-sm">FROM</span>
|
||||
</div>
|
||||
{fromTable?.name} 필드들
|
||||
</h3>
|
||||
|
|
@ -122,13 +122,13 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
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"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{field.name}</div>
|
||||
<div className="text-sm text-gray-600">{field.type}</div>
|
||||
<div className="text-sm text-muted-foreground">{field.type}</div>
|
||||
{field.primaryKey && (
|
||||
<span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
|
||||
PK
|
||||
|
|
@ -170,7 +170,7 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{field.name}</div>
|
||||
<div className="text-sm text-gray-600">{field.type}</div>
|
||||
<div className="text-sm text-muted-foreground">{field.type}</div>
|
||||
{field.primaryKey && (
|
||||
<span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">
|
||||
PK
|
||||
|
|
@ -196,7 +196,7 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
{fieldMappings.find(m => m.toField.name === field.name)?.isValid ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
<AlertCircle className="w-5 h-5 text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -213,7 +213,7 @@ export const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
|||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 px-6 py-3 rounded-lg font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 transition-all duration-200"
|
||||
className="flex items-center gap-2 px-6 py-3 rounded-lg font-medium text-muted-foreground bg-gray-100 hover:bg-gray-200 transition-all duration-200"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
이전 단계
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
|||
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<MultiActionConfigStepProps> = ({
|
|||
|
||||
{/* 그룹 간 논리 연산자 선택 */}
|
||||
{actionGroups.length > 1 && (
|
||||
<div className="rounded-md border bg-blue-50 p-3">
|
||||
<div className="rounded-md border bg-accent p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium text-blue-900">그룹 간 실행 조건</h4>
|
||||
</div>
|
||||
|
|
@ -649,9 +649,9 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 그룹 로직 설명 */}
|
||||
<div className="mt-4 rounded-md bg-blue-50 p-3">
|
||||
<div className="mt-4 rounded-md bg-accent p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 text-blue-600" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 text-primary" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-blue-900">{group.logicalOperator} 조건 그룹</div>
|
||||
<div className="text-blue-700">
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const RightPanel: React.FC<RightPanelProps> = ({ state, actions }) => {
|
|||
{/* 헤더 */}
|
||||
<div className="flex-shrink-0 px-4 py-2">
|
||||
<div className="flex items-center gap-3 border-b pb-2">
|
||||
<Globe className="h-5 w-5 text-blue-600" />
|
||||
<Globe className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">외부 호출 설정</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
|
|
@ -89,7 +89,7 @@ const RightPanel: React.FC<RightPanelProps> = ({ 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -99,7 +99,7 @@ const RightPanel: React.FC<RightPanelProps> = ({ 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const StepProgress: React.FC<StepProgressProps> = ({
|
|||
? "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<StepProgressProps> = ({
|
|||
{step.title}
|
||||
</h3>
|
||||
<p className={`text-xs ${
|
||||
step.id <= currentStep ? "text-gray-600" : "text-gray-400"
|
||||
step.id <= currentStep ? "text-muted-foreground" : "text-gray-400"
|
||||
}`}>
|
||||
{step.description}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const TableStep: React.FC<TableStepProps> = ({
|
|||
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
테이블 선택
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
소스 테이블과 대상 테이블을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -99,7 +99,7 @@ export const TableStep: React.FC<TableStepProps> = ({
|
|||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="w-5 h-5 text-blue-600" />
|
||||
<Database className="w-5 h-5 text-primary" />
|
||||
<span className="font-medium text-gray-900">{fromConnection?.name}</span>
|
||||
<span className="text-sm text-gray-500">→</span>
|
||||
<Database className="w-5 h-5 text-green-600" />
|
||||
|
|
@ -112,8 +112,8 @@ export const TableStep: React.FC<TableStepProps> = ({
|
|||
{/* FROM 테이블 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-blue-600 font-bold">1</span>
|
||||
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<span className="text-primary font-bold">1</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">소스 테이블</h3>
|
||||
<span className="text-sm text-gray-500">(FROM)</span>
|
||||
|
|
@ -125,20 +125,20 @@ export const TableStep: React.FC<TableStepProps> = ({
|
|||
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)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Table className="w-6 h-6 text-blue-600" />
|
||||
<Table className="w-6 h-6 text-primary" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{table.name}</h4>
|
||||
<p className="text-sm text-gray-600">{table.columns.length}개 컬럼</p>
|
||||
<p className="text-sm text-muted-foreground">{table.columns.length}개 컬럼</p>
|
||||
<p className="text-xs text-gray-500">{table.rowCount?.toLocaleString()}개 행</p>
|
||||
</div>
|
||||
{selectedFromTable === table.name && (
|
||||
<CheckCircle className="w-5 h-5 text-blue-600" />
|
||||
<CheckCircle className="w-5 h-5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,7 +171,7 @@ export const TableStep: React.FC<TableStepProps> = ({
|
|||
<Table className="w-6 h-6 text-green-600" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{table.name}</h4>
|
||||
<p className="text-sm text-gray-600">{table.columns.length}개 컬럼</p>
|
||||
<p className="text-sm text-muted-foreground">{table.columns.length}개 컬럼</p>
|
||||
<p className="text-xs text-gray-500">{table.rowCount?.toLocaleString()}개 행</p>
|
||||
</div>
|
||||
{selectedToTable === table.name && (
|
||||
|
|
@ -188,7 +188,7 @@ export const TableStep: React.FC<TableStepProps> = ({
|
|||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 px-6 py-3 rounded-lg font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 transition-all duration-200"
|
||||
className="flex items-center gap-2 px-6 py-3 rounded-lg font-medium text-muted-foreground bg-gray-100 hover:bg-gray-200 transition-all duration-200"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
이전 단계
|
||||
|
|
|
|||
|
|
@ -128,9 +128,9 @@ const FieldColumn: React.FC<FieldColumnProps> = ({
|
|||
: 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}
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ const FieldMappingCanvas: React.FC<FieldMappingCanvasProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 매핑 규칙 안내 */}
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div className="mt-4 rounded-lg border border-primary/20 bg-accent p-3">
|
||||
<h4 className="mb-2 text-sm font-medium">📋 매핑 규칙</h4>
|
||||
<div className="text-muted-foreground space-y-1 text-xs">
|
||||
<p>✅ 1:N 매핑 허용 (하나의 소스 필드를 여러 대상 필드에 매핑)</p>
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ const ExternalCallTestPanel: React.FC<ExternalCallTestPanelProps> = ({
|
|||
{testResult.responseTime !== undefined && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">응답 시간</Label>
|
||||
<div className="mt-1 rounded border bg-blue-50 p-2">
|
||||
<div className="mt-1 rounded border bg-accent p-2">
|
||||
<span className="font-mono text-sm text-blue-700">{testResult.responseTime}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ const RestApiSettings: React.FC<RestApiSettingsProps> = ({ 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" : ""}
|
||||
/>
|
||||
<div className="text-muted-foreground text-xs">호출할 API의 전체 URL을 입력하세요.</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
<div
|
||||
className={`group flex h-10 cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${
|
||||
pathname === menu.url
|
||||
? "border-l-4 border-blue-500 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
? "border-l-4 border-primary bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
: isExpanded
|
||||
? "bg-slate-100 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||
|
|
@ -352,7 +352,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
key={child.id}
|
||||
className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${
|
||||
pathname === child.url
|
||||
? "border-l-4 border-blue-500 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
? "border-l-4 border-primary bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||
}`}
|
||||
onClick={() => handleMenuClick(child)}
|
||||
|
|
@ -376,7 +376,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
<p>로딩중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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={
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
|
||||
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
<p>로딩중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<DialogTitle className={getTypeColor()}>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-gray-600">{message}</p>
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={onClose} className="w-20">
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export default function ConfirmDeleteModal({
|
|||
<div className="p-6 space-y-4">
|
||||
<p className="text-gray-700">{message}</p>
|
||||
{itemName && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
|
||||
<p className="text-sm font-medium text-red-800">
|
||||
삭제 대상: <span className="font-bold">{itemName}</span>
|
||||
</p>
|
||||
|
|
@ -71,7 +71,8 @@ export default function ConfirmDeleteModal({
|
|||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
className="flex-1 bg-red-500 hover:bg-red-600"
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<div
|
||||
className={`mt-3 p-3 rounded-lg flex items-start gap-2 ${
|
||||
testResult.success
|
||||
? 'bg-green-50 border border-green-200'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
? 'bg-green-50 border border-green-500/20'
|
||||
: 'bg-destructive/10 border border-destructive/20'
|
||||
}`}
|
||||
>
|
||||
{testResult.success ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<p
|
||||
className={`text-sm ${
|
||||
|
|
@ -385,7 +386,8 @@ export default function MailAccountModal({
|
|||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1 bg-orange-500 hover:bg-orange-600"
|
||||
variant="default"
|
||||
className="flex-1"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
|
|
|
|||
|
|
@ -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<keyof MailAccount>('createdAt');
|
||||
|
|
@ -82,7 +84,7 @@ export default function MailAccountTable({
|
|||
return (
|
||||
<div className="bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-12 text-center border-2 border-dashed border-gray-300">
|
||||
<Mail className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-lg font-medium text-gray-600 mb-2">
|
||||
<p className="text-lg font-medium text-muted-foreground mb-2">
|
||||
등록된 메일 계정이 없습니다
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
|
|
@ -174,10 +176,10 @@ export default function MailAccountTable({
|
|||
<div className="font-medium text-gray-900">{account.name}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-600">{account.email}</div>
|
||||
<div className="text-sm text-muted-foreground">{account.email}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{account.smtpHost}:{account.smtpPort}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
|
|
@ -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({
|
|||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatDate(account.createdAt)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() => onEdit(account)}
|
||||
onClick={() => onTestConnection(account)}
|
||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="SMTP 연결 테스트"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onEdit(account)}
|
||||
className="p-2 text-primary hover:bg-accent rounded-lg transition-colors"
|
||||
title="수정"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => 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="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
|
@ -244,7 +253,7 @@ export default function MailAccountTable({
|
|||
</div>
|
||||
|
||||
{/* 결과 요약 */}
|
||||
<div className="text-sm text-gray-600 text-center">
|
||||
<div className="text-sm text-muted-foreground text-center">
|
||||
전체 {accounts.length}개 중 {sortedAccounts.length}개 표시
|
||||
{searchTerm && ` (검색: "${searchTerm}")`}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<Eye className="w-4 h-4 mr-2" />
|
||||
미리보기
|
||||
</Button>
|
||||
<Button onClick={handleSend} className="w-full bg-orange-500 hover:bg-orange-600 text-white">
|
||||
<Button onClick={handleSend} variant="default" className="w-full">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
발송
|
||||
</Button>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -168,12 +168,12 @@ export default function MailDetailModal({
|
|||
{loading ? (
|
||||
<div className="flex justify-center items-center py-16">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
|
||||
<span className="ml-3 text-gray-600">메일을 불러오는 중...</span>
|
||||
<span className="ml-3 text-muted-foreground">메일을 불러오는 중...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<p className="text-red-600">{error}</p>
|
||||
<p className="text-destructive">{error}</p>
|
||||
<Button onClick={loadMailDetail} variant="outline" className="mt-4">
|
||||
다시 시도
|
||||
</Button>
|
||||
|
|
@ -193,17 +193,17 @@ export default function MailDetailModal({
|
|||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">받는사람:</span>{" "}
|
||||
<span className="text-gray-600">{mail.to}</span>
|
||||
<span className="text-muted-foreground">{mail.to}</span>
|
||||
</div>
|
||||
{mail.cc && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">참조:</span>{" "}
|
||||
<span className="text-gray-600">{mail.cc}</span>
|
||||
<span className="text-muted-foreground">{mail.cc}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">날짜:</span>{" "}
|
||||
<span className="text-gray-600">
|
||||
<span className="text-muted-foreground">
|
||||
{formatDate(mail.date)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -225,7 +225,7 @@ export default function MailDetailModal({
|
|||
{mail.attachments && mail.attachments.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Paperclip className="w-4 h-4 text-gray-600" />
|
||||
<Paperclip className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="font-medium text-gray-700">
|
||||
첨부파일 ({mail.attachments.length})
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function MailTemplateCard({
|
|||
|
||||
const getCategoryColor = (category?: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
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({
|
|||
<h3 className="font-semibold text-gray-900 truncate">
|
||||
{template.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 truncate mt-1">
|
||||
<p className="text-sm text-muted-foreground truncate mt-1">
|
||||
{template.subject}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -75,7 +75,7 @@ export default function MailTemplateCard({
|
|||
<p className="text-xs text-gray-500 mb-2">컴포넌트 {template.components.length}개</p>
|
||||
<div className="space-y-1">
|
||||
{template.components.slice(0, 3).map((component, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 text-xs text-gray-600">
|
||||
<div key={idx} className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-orange-400" />
|
||||
<span className="capitalize">{component.type}</span>
|
||||
{component.type === 'text' && component.content && (
|
||||
|
|
@ -110,7 +110,7 @@ export default function MailTemplateCard({
|
|||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300"
|
||||
className="flex-1 hover:bg-accent hover:text-primary hover:border-blue-300"
|
||||
onClick={() => onPreview(template)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
|
|
@ -138,7 +138,7 @@ export default function MailTemplateCard({
|
|||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hover:bg-red-50 hover:text-red-600 hover:border-red-300"
|
||||
className="hover:bg-destructive/10 hover:text-destructive hover:border-red-300"
|
||||
onClick={() => onDelete(template)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export default function MailTemplatePreviewModal({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="mt-6 p-4 bg-accent border border-primary/20 rounded-lg">
|
||||
<p className="text-xs text-blue-800">
|
||||
💡 변수 값을 입력하면 미리보기에 반영됩니다.
|
||||
</p>
|
||||
|
|
@ -122,15 +122,15 @@ export default function MailTemplatePreviewModal({
|
|||
<div className="bg-gray-100 px-6 py-4 border-b border-gray-200">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex">
|
||||
<span className="font-semibold text-gray-600 w-20">제목:</span>
|
||||
<span className="font-semibold text-muted-foreground w-20">제목:</span>
|
||||
<span className="text-gray-900">{template.subject}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-semibold text-gray-600 w-20">발신:</span>
|
||||
<span className="font-semibold text-muted-foreground w-20">발신:</span>
|
||||
<span className="text-gray-700">your-email@company.com</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-semibold text-gray-600 w-20">수신:</span>
|
||||
<span className="font-semibold text-muted-foreground w-20">수신:</span>
|
||||
<span className="text-gray-700">recipient@example.com</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
|
|||
{/* 원본 화면 정보 */}
|
||||
<div className="rounded-md bg-gray-50 p-3">
|
||||
<h4 className="mb-2 text-sm font-medium text-gray-700">원본 화면 정보</h4>
|
||||
<div className="space-y-1 text-sm text-gray-600">
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<span className="font-medium">화면명:</span> {sourceScreen?.screenName}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
|||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid || submitting} className="bg-blue-600 hover:bg-blue-700">
|
||||
<Button onClick={handleSubmit} disabled={!isValid || submitting} variant="default">
|
||||
생성
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
onToggleZoneBorders,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-white px-4 py-3 shadow-sm">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gradient-to-r from-gray-50 to-white px-4 py-3 shadow-sm">
|
||||
{/* 좌측: 네비게이션 및 화면 정보 */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="ghost" size="sm" onClick={onBack} className="flex items-center space-x-2">
|
||||
|
|
@ -66,7 +66,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
<div className="h-6 w-px bg-gray-300" />
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Menu className="h-5 w-5 text-gray-600" />
|
||||
<Menu className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-gray-900">{screenName || "화면 설계"}</h1>
|
||||
{tableName && (
|
||||
|
|
@ -85,7 +85,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
variant={panelStates.tables?.isOpen ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onTogglePanel("tables")}
|
||||
className={cn("flex items-center space-x-2", panelStates.tables?.isOpen && "bg-blue-600 text-white")}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Database className="h-4 w-4" />
|
||||
<span>테이블</span>
|
||||
|
|
@ -98,7 +98,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
variant={panelStates.templates?.isOpen ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onTogglePanel("templates")}
|
||||
className={cn("flex items-center space-x-2", panelStates.templates?.isOpen && "bg-blue-600 text-white")}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Layout className="h-4 w-4" />
|
||||
<span>템플릿</span>
|
||||
|
|
@ -111,7 +111,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
variant={panelStates.components?.isOpen ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onTogglePanel("components")}
|
||||
className={cn("flex items-center space-x-2", panelStates.components?.isOpen && "bg-blue-600 text-white")}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Cog className="h-4 w-4" />
|
||||
<span>컴포넌트</span>
|
||||
|
|
@ -124,7 +124,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
variant={panelStates.properties?.isOpen ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onTogglePanel("properties")}
|
||||
className={cn("flex items-center space-x-2", panelStates.properties?.isOpen && "bg-blue-600 text-white")}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>속성</span>
|
||||
|
|
@ -137,7 +137,7 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
|||
variant={panelStates.styles?.isOpen ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => onTogglePanel("styles")}
|
||||
className={cn("flex items-center space-x-2", panelStates.styles?.isOpen && "bg-blue-600 text-white")}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Palette className="h-4 w-4" />
|
||||
<span>스타일</span>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue