Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/report

This commit is contained in:
dohyeons 2025-10-02 18:01:14 +09:00
commit 57c4e8317d
183 changed files with 2927 additions and 1109 deletions

View File

@ -178,14 +178,19 @@ export class MailAccountFileController {
try {
const { id } = req.params;
// TODO: 실제 SMTP 연결 테스트 구현
// const account = await mailAccountFileService.getAccountById(id);
// nodemailer로 연결 테스트
const account = await mailAccountFileService.getAccountById(id);
if (!account) {
return res.status(404).json({
success: false,
message: '계정을 찾을 수 없습니다.',
});
}
return res.json({
success: true,
message: '연결 테스트 성공 (미구현)',
});
// mailSendSimpleService의 testConnection 사용
const { mailSendSimpleService } = require('../services/mailSendSimpleService');
const result = await mailSendSimpleService.testConnection(id);
return res.json(result);
} catch (error: unknown) {
const err = error as Error;
return res.status(500).json({

View File

@ -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와 수신자 이메일이 필요합니다.',

View File

@ -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));

View File

@ -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();
// 메일 목록 조회

View File

@ -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));

View File

@ -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));

View File

@ -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}`,

View File

@ -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"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`,

View File

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

View File

@ -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>
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
))}

View File

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

View File

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

View File

@ -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)}%`,

View File

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

View File

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

View File

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

View File

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

View File

@ -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={() => {

View File

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

View File

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

View File

@ -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>
))
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: `

View File

@ -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>
)}

View File

@ -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="📚"

View File

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

View File

@ -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>
))}

View File

@ -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>
)}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 || "-"}

View File

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

View File

@ -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>
);
}

View File

@ -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>
)}

View File

@ -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>
&ldquo;{selectedDiagramForAction?.diagramName}&rdquo; ?
<br />
<span className="font-medium text-red-600">
<span className="font-medium text-destructive">
, .
</span>
</DialogDescription>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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=">">&gt;</SelectItem>
<SelectItem value="<">&lt;</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>

View File

@ -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>{" "}

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
</>
)}

View File

@ -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>
)}

View File

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

View File

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

View File

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

View File

@ -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" />

View File

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

View File

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

View File

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

View File

@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />

View File

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

View File

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

View File

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

View File

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