diff --git a/src/app/(common)/components/ToastProvider.jsx b/src/app/_common/components/ToastProvider.jsx similarity index 100% rename from src/app/(common)/components/ToastProvider.jsx rename to src/app/_common/components/ToastProvider.jsx diff --git a/src/app/(common)/components/nodeHook.ts b/src/app/_common/components/nodeHook.ts similarity index 100% rename from src/app/(common)/components/nodeHook.ts rename to src/app/_common/components/nodeHook.ts diff --git a/src/app/_common/components/sidebarConfig.ts b/src/app/_common/components/sidebarConfig.ts new file mode 100644 index 00000000..64e85bfd --- /dev/null +++ b/src/app/_common/components/sidebarConfig.ts @@ -0,0 +1,52 @@ +import React from 'react'; +import { + FiGrid, + FiFolder, + FiCpu, + FiSettings, + FiEye, +} from 'react-icons/fi'; +import { SidebarItem } from '@/app/main/components/types'; + +// 워크플로우 관리 센터의 공통 사이드바 아이템들을 반환하는 함수 +export const getSidebarItems = (): SidebarItem[] => [ + { + id: 'canvas', + title: '워크플로우 캔버스', + description: '새로운 워크플로우 만들기', + icon: React.createElement(FiGrid), + }, + { + id: 'workflows', + title: '완성된 워크플로우', + description: '저장된 워크플로우 관리', + icon: React.createElement(FiFolder), + }, + { + id: 'exec-monitor', + title: '실행 및 모니터링', + description: '워크플로우 실행과 성능 모니터링', + icon: React.createElement(FiCpu), + }, + { + id: 'settings', + title: '고급 환경 설정', + description: 'LLM 및 Tool 환경변수 직접 관리', + icon: React.createElement(FiSettings), + }, + { + id: 'config-viewer', + title: '설정값 확인', + description: '백엔드 환경변수 및 설정 확인', + icon: React.createElement(FiEye), + }, +]; + +// 공통 아이템 클릭 핸들러 (localStorage 사용) +export const createItemClickHandler = (router: any) => { + return (itemId: string) => { + // 클릭한 섹션을 localStorage에 저장하고 /main으로 이동 + localStorage.setItem('activeSection', itemId); + router.push('/main'); + }; +}; diff --git a/src/app/(common)/components/workflowStorage.js b/src/app/_common/components/workflowStorage.js similarity index 100% rename from src/app/(common)/components/workflowStorage.js rename to src/app/_common/components/workflowStorage.js diff --git a/src/app/canvas/components/Header.tsx b/src/app/canvas/components/Header.tsx index 0b4b3df0..ccf624c0 100644 --- a/src/app/canvas/components/Header.tsx +++ b/src/app/canvas/components/Header.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from ' import Link from 'next/link'; import styles from '@/app/canvas/assets/Header.module.scss'; import { LuPanelRightOpen, LuSave, LuCheck, LuX, LuPencil, LuFileText } from "react-icons/lu"; -import { getWorkflowName, saveWorkflowName } from '@/app/(common)/components/workflowStorage'; +import { getWorkflowName, saveWorkflowName } from '@/app/_common/components/workflowStorage'; // Type definitions interface HeaderProps { diff --git a/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx b/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx index 9acb64bf..ba4763cc 100644 --- a/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx @@ -5,7 +5,7 @@ import NodeList from '@/app/canvas/components/Helper/NodeList'; import DraggableNodeItem from '@/app/canvas/components/Helper/DraggableNodeItem'; import { LuSearch, LuArrowLeft, LuBrainCircuit, LuShare2, LuWrench, LuX, LuRefreshCw } from 'react-icons/lu'; import { SiLangchain } from "react-icons/si"; -import { useNodes } from '@/app/(common)/components/nodeHook'; +import { useNodes } from '@/app/_common/components/nodeHook'; import type { Port, Parameter, diff --git a/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx b/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx index 50a11346..6846b5ef 100644 --- a/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx @@ -5,7 +5,7 @@ import styles from '@/app/canvas/assets/WorkflowPanel.module.scss'; import sideMenuStyles from '@/app/canvas/assets/SideMenu.module.scss'; import { LuArrowLeft, LuLayoutTemplate, LuPlay, LuCopy } from "react-icons/lu"; import TemplatePreview from '@/app/canvas/components/SideMenuPanel/TemplatePreview'; -import { getWorkflowState } from '@/app/(common)/components/workflowStorage'; +import { getWorkflowState } from '@/app/_common/components/workflowStorage'; import { devLog } from '@/app/utils/logger'; import Basic_Chatbot from '@/app/canvas/constants/workflow/Basic_Chatbot.json'; diff --git a/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx b/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx index a5263915..0c7214e1 100644 --- a/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx @@ -5,7 +5,7 @@ import styles from '@/app/canvas/assets/WorkflowPanel.module.scss'; import sideMenuStyles from '@/app/canvas/assets/SideMenu.module.scss'; import { LuArrowLeft, LuFolderOpen, LuDownload, LuRefreshCw, LuCalendar, LuTrash2 } from "react-icons/lu"; import { listWorkflows, loadWorkflow, deleteWorkflow } from '@/app/api/workflowAPI'; -import { getWorkflowState } from '@/app/(common)/components/workflowStorage'; +import { getWorkflowState } from '@/app/_common/components/workflowStorage'; import { devLog } from '@/app/utils/logger'; import type { Position, diff --git a/src/app/canvas/page.tsx b/src/app/canvas/page.tsx index 153d7942..eb714306 100644 --- a/src/app/canvas/page.tsx +++ b/src/app/canvas/page.tsx @@ -18,7 +18,7 @@ import { ensureValidWorkflowState, saveWorkflowName, startNewWorkflow, -} from '@/app/(common)/components/workflowStorage'; +} from '@/app/_common/components/workflowStorage'; import { devLog } from '@/app/utils/logger'; import { generateWorkflowHash } from '@/app/utils/generateSha1Hash'; diff --git a/src/app/chat/assets/ChatContent.module.scss b/src/app/chat/assets/ChatContent.module.scss new file mode 100644 index 00000000..de94e921 --- /dev/null +++ b/src/app/chat/assets/ChatContent.module.scss @@ -0,0 +1,166 @@ +// Chat Content Styles +$primary-blue: #2563eb; +$primary-purple: #7c3aed; +$primary-green: #059669; +$gray-50: #f9fafb; +$gray-100: #f3f4f6; +$gray-200: #e5e7eb; +$gray-300: #d1d5db; +$gray-400: #9ca3af; +$gray-500: #6b7280; +$gray-600: #4b5563; +$gray-700: #374151; +$gray-800: #1f2937; +$gray-900: #111827; +$white: #ffffff; + +.chatContainer { + height: 100%; + display: flex; + flex-direction: column; + background: $white; + border-radius: 0.75rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.welcomeSection { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 3rem; + transition: all 0.5s ease; + overflow: hidden; +} + +.workflowSection { + flex: 1; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 2rem; + transition: all 0.5s ease; + overflow: hidden; + + .container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + } +} + +.welcomeContent { + text-align: center; + max-width: 600px; + + h1 { + font-size: 2.5rem; + font-weight: 700; + color: $gray-900; + margin: 0 0 1rem 0; + background: linear-gradient(135deg, $primary-blue 0%, $primary-purple 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + p { + font-size: 1.125rem; + color: $gray-600; + margin: 0 0 3rem 0; + line-height: 1.6; + } +} + +.buttonContainer { + display: flex; + gap: 1.5rem; + margin-top: 2rem; + justify-content: center; + flex-wrap: wrap; +} + +.workflowButton, +.chatButton { + background: $white; + border: 2px solid $gray-200; + border-radius: 1rem; + padding: 2rem 1.5rem; + text-align: center; + transition: all 0.3s ease; + cursor: pointer; + width: 220px; + height: 180px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; + flex-shrink: 0; + + svg { + font-size: 2rem; + color: $primary-blue; + transition: all 0.3s ease; + } + + h3 { + font-size: 1.125rem; + font-weight: 600; + color: $gray-900; + margin: 0; + } + + p { + font-size: 0.875rem; + color: $gray-500; + margin: 0; + line-height: 1.4; + } + + &:hover { + transform: translateY(-3px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + border-color: $primary-blue; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05) 0%, rgba(124, 58, 237, 0.05) 100%); + + svg { + color: $primary-purple; + transform: scale(1.1); + } + } + + &:active { + transform: translateY(-1px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + } +} + +// Responsive Design +@media (max-width: 768px) { + .welcomeContent { + padding: 1.5rem; + + h1 { + font-size: 2rem; + } + + p { + font-size: 1rem; + } + } + + .buttonContainer { + flex-direction: column; + gap: 1rem; + align-items: center; + } + + .workflowButton, + .chatButton { + width: 100%; + max-width: 280px; + height: 160px; + padding: 1.5rem; + } +} diff --git a/src/app/chat/assets/WorkflowSelection.module.scss b/src/app/chat/assets/WorkflowSelection.module.scss new file mode 100644 index 00000000..3e63f442 --- /dev/null +++ b/src/app/chat/assets/WorkflowSelection.module.scss @@ -0,0 +1,441 @@ +@use "sass:color"; + +// Color Variables +$primary-blue: #2563eb; +$primary-green: #059669; +$primary-yellow: #d97706; +$primary-red: #dc2626; +$gray-50: #f9fafb; +$gray-100: #f3f4f6; +$gray-200: #e5e7eb; +$gray-300: #d1d5db; +$gray-400: #9ca3af; +$gray-500: #6b7280; +$gray-600: #4b5563; +$gray-700: #374151; +$gray-900: #111827; +$white: #ffffff; + +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +// Header +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + flex-shrink: 0; +} + +.headerInfo { + display: flex; + align-items: center; + gap: 1rem; + + h2 { + font-size: 1.875rem; + font-weight: 700; + color: $gray-900; + margin: 0 0 0.5rem 0; + } + + p { + color: $gray-600; + margin: 0; + line-height: 1.6; + } +} + +.backButton { + background: $white; + border: 2px solid $gray-200; + border-radius: 0.5rem; + padding: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + + svg { + font-size: 1.25rem; + color: $gray-600; + } + + &:hover { + border-color: $primary-blue; + background: $gray-50; + + svg { + color: $primary-blue; + } + } +} + +.headerActions { + display: flex; + align-items: center; + gap: 1rem; +} + +// Filters +.filters { + display: flex; + gap: 0.5rem; + background: $gray-100; + padding: 0.25rem; + border-radius: 0.5rem; +} + +// 새로고침 버튼 스타일 +.refreshButton { + background: transparent; + border: 1px solid $gray-300; + border-radius: 6px; + padding: 0.5rem; + cursor: pointer; + color: $gray-600; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + + &:hover:not(:disabled) { + background: $gray-50; + border-color: $primary-blue; + color: $primary-blue; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + svg { + width: 16px; + height: 16px; + } +} + +// 스피닝 애니메이션 +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.filterButton { + padding: 0.5rem 1rem; + border: none; + background: transparent; + color: $gray-700; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease-in-out; + font-size: 0.875rem; + + &:hover { + background: $white; + color: $gray-900; + } + + &.active { + background: $white; + color: $primary-blue; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } +} + +// Workflows Grid +.workflowsGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + padding-top: 1rem; + gap: 1rem; + overflow-y: auto; + max-height: 380px; + padding-right: 0.5rem; + + /* 스크롤바 스타일링 */ + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: $gray-100; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: $gray-300; + border-radius: 3px; + + &:hover { + background: $gray-400; + } + } +} + +.workflowCard { + background: $white; + border: 2px solid $gray-200; + border-radius: 0.75rem; + padding: 1rem; + transition: all 0.2s ease-in-out; + cursor: pointer; + position: relative; + min-height: 160px; + max-height: 160px; + display: flex; + flex-direction: column; + + &:hover:not(.disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + border-color: $primary-blue; + } + + &:active:not(.disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + + &:hover { + transform: none; + box-shadow: none; + border-color: $gray-200; + } + } +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.workflowIcon { + width: 2rem; + height: 2rem; + background: rgba($primary-blue, 0.1); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 1rem; + height: 1rem; + color: $primary-blue; + } +} + +.status { + padding: 0.2rem 0.5rem; + border-radius: 0.75rem; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + + &.statusActive { + background: rgba($primary-green, 0.1); + color: $primary-green; + } + + &.statusDraft { + background: rgba($primary-yellow, 0.1); + color: $primary-yellow; + } + + &.statusArchived { + background: rgba($gray-500, 0.1); + color: $gray-500; + } +} + +// Card Content +.cardContent { + margin-bottom: 0.75rem; + flex: 1; + display: flex; + flex-direction: column; +} + +.workflowName { + font-size: 0.95rem; + font-weight: 600; + color: $gray-900; + margin: 0 0 0.5rem 0; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workflowDescription { + color: $gray-600; + line-height: 1.4; + margin: 0 0 0.75rem 0; + font-size: 0.8rem; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.workflowError { + color: $primary-red; + line-height: 1.4; + margin: 0 0 0.75rem 0; + font-size: 0.75rem; + padding: 0.4rem; + background: rgba($primary-red, 0.1); + border-radius: 0.375rem; + border-left: 2px solid $primary-red; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.workflowMeta { + display: flex; + flex-direction: column; + gap: 0.3rem; + margin-top: auto; +} + +.metaItem { + display: flex; + align-items: center; + gap: 0.3rem; + color: $gray-500; + font-size: 0.75rem; + + svg { + width: 0.75rem; + height: 0.75rem; + } +} + +// Loading State +.loadingState { + text-align: center; + padding: 4rem 2rem; + color: $gray-500; + + p { + font-size: 1.125rem; + margin: 0; + } +} + +// Error State +.errorState { + text-align: center; + padding: 4rem 2rem; + color: $primary-red; + + p { + font-size: 1.125rem; + margin: 0 0 1rem 0; + } + + button { + padding: 0.5rem 1rem; + background: $primary-blue; + color: $white; + border: none; + border-radius: 0.375rem; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s ease-in-out; + + &:hover { + background: color.scale($primary-blue, $lightness: -10%); + } + } +} + +// Empty State +.emptyState { + text-align: center; + padding: 4rem 2rem; + color: $gray-500; + + .emptyIcon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + opacity: 0.5; + } + + h3 { + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 0.5rem 0; + color: $gray-700; + } + + p { + margin: 0; + line-height: 1.6; + } +} + +// Responsive Design +@media (max-width: 1024px) { + .workflowsGrid { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: stretch; + } + + .filters { + justify-content: center; + } + + .workflowsGrid { + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + } + + .workflowCard { + min-height: 140px; + max-height: 140px; + padding: 0.75rem; + } +} + +@media (max-width: 480px) { + .workflowsGrid { + grid-template-columns: repeat(2, 1fr); + } +} \ No newline at end of file diff --git a/src/app/chat/components/ChatContent.tsx b/src/app/chat/components/ChatContent.tsx new file mode 100644 index 00000000..1a9f4602 --- /dev/null +++ b/src/app/chat/components/ChatContent.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import styles from '@/app/chat/assets/ChatContent.module.scss'; +import { LuWorkflow } from "react-icons/lu"; +import { IoChatbubblesOutline } from "react-icons/io5"; +import WorkflowSelection from './WorkflowSelection'; + +const ChatContent: React.FC = () => { + const [currentView, setCurrentView] = useState<'welcome' | 'workflow'>('welcome'); + + const handleWorkflowSelect = (workflow: any) => { + // 워크플로우 선택 후 로직 (나중에 구현) + console.log('Selected workflow:', workflow); + }; + + if (currentView === 'workflow') { + return ( +
AI와 대화하며 궁금한 물어보세요.
+채팅에 사용할 워크플로우를 선택하세요.
+워크플로우를 불러오는 중...
+{error}
+ ++ {workflow.description} +
+ )} + {workflow.error && ( ++ 오류: {workflow.error} +
+ )} + +아직 저장된 워크플로우가 없습니다. 새로운 워크플로우를 만들어보세요.
+