From c744299e91593104802e4e1111a0aa39b7e9170d Mon Sep 17 00:00:00 2001 From: CocoRoF Date: Wed, 16 Jul 2025 06:08:14 +0000 Subject: [PATCH 1/4] refactor: Enhance WorkflowSelection styles for improved UI and responsiveness --- .../chat/assets/WorkflowSelection.module.scss | 139 ++++++++++++------ 1 file changed, 93 insertions(+), 46 deletions(-) diff --git a/src/app/chat/assets/WorkflowSelection.module.scss b/src/app/chat/assets/WorkflowSelection.module.scss index 3e63f442..953361af 100644 --- a/src/app/chat/assets/WorkflowSelection.module.scss +++ b/src/app/chat/assets/WorkflowSelection.module.scss @@ -64,7 +64,7 @@ $white: #ffffff; display: flex; align-items: center; justify-content: center; - + svg { font-size: 1.25rem; color: $gray-600; @@ -73,7 +73,7 @@ $white: #ffffff; &:hover { border-color: $primary-blue; background: $gray-50; - + svg { color: $primary-blue; } @@ -133,8 +133,13 @@ $white: #ffffff; } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } .filterButton { @@ -167,23 +172,23 @@ $white: #ffffff; padding-top: 1rem; gap: 1rem; overflow-y: auto; - max-height: 380px; + max-height: 670px; 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; } @@ -192,36 +197,63 @@ $white: #ffffff; .workflowCard { background: $white; - border: 2px solid $gray-200; - border-radius: 0.75rem; - padding: 1rem; - transition: all 0.2s ease-in-out; + border: 1px solid $gray-200; + border-radius: 1rem; + padding: 1.25rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; position: relative; - min-height: 160px; - max-height: 160px; + min-height: 205px; display: flex; flex-direction: column; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 1rem; + background: linear-gradient(135deg, rgba($primary-blue, 0.02), rgba($primary-blue, 0.05)); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + } &:hover:not(.disabled) { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); - border-color: $primary-blue; + transform: translateY(-3px); + box-shadow: + 0 10px 30px rgba(0, 0, 0, 0.1), + 0 4px 15px rgba($primary-blue, 0.15); + border-color: rgba($primary-blue, 0.3); + + &::before { + opacity: 1; + } } &:active:not(.disabled) { transform: translateY(-1px); - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + box-shadow: + 0 5px 20px rgba(0, 0, 0, 0.08), + 0 2px 8px rgba($primary-blue, 0.12); } &.disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; - + background: $gray-50; + &:hover { transform: none; - box-shadow: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border-color: $gray-200; + + &::before { + opacity: 0; + } } } } @@ -229,47 +261,52 @@ $white: #ffffff; .cardHeader { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; + align-items: flex-start; + margin-bottom: 1rem; } .workflowIcon { - width: 2rem; - height: 2rem; - background: rgba($primary-blue, 0.1); - border-radius: 0.5rem; + width: 2.25rem; + height: 2.25rem; + background: linear-gradient(135deg, rgba($primary-blue, 0.1), rgba($primary-blue, 0.15)); + border-radius: 0.75rem; display: flex; align-items: center; justify-content: center; + border: 1px solid rgba($primary-blue, 0.1); svg { - width: 1rem; - height: 1rem; + width: 1.125rem; + height: 1.125rem; color: $primary-blue; } } .status { - padding: 0.2rem 0.5rem; - border-radius: 0.75rem; - font-size: 0.65rem; + padding: 0.3rem 0.7rem; + border-radius: 0.5rem; + font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; + border: 1px solid transparent; &.statusActive { - background: rgba($primary-green, 0.1); + background: linear-gradient(135deg, rgba($primary-green, 0.1), rgba($primary-green, 0.15)); color: $primary-green; + border-color: rgba($primary-green, 0.2); } &.statusDraft { - background: rgba($primary-yellow, 0.1); + background: linear-gradient(135deg, rgba($primary-yellow, 0.1), rgba($primary-yellow, 0.15)); color: $primary-yellow; + border-color: rgba($primary-yellow, 0.2); } &.statusArchived { - background: rgba($gray-500, 0.1); + background: linear-gradient(135deg, rgba($gray-500, 0.1), rgba($gray-500, 0.15)); color: $gray-500; + border-color: rgba($gray-500, 0.2); } } @@ -282,14 +319,15 @@ $white: #ffffff; } .workflowName { - font-size: 0.95rem; - font-weight: 600; + font-size: 1rem; + font-weight: 700; color: $gray-900; margin: 0 0 0.5rem 0; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + letter-spacing: -0.01em; } .workflowDescription { @@ -309,34 +347,43 @@ $white: #ffffff; 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; + padding: 0.5rem; + background: linear-gradient(135deg, rgba($primary-red, 0.05), rgba($primary-red, 0.1)); + border-radius: 0.5rem; + border-left: 3px solid $primary-red; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; + border: 1px solid rgba($primary-red, 0.15); } .workflowMeta { display: flex; flex-direction: column; - gap: 0.3rem; + gap: 0.4rem; margin-top: auto; + padding-top: 0.5rem; + border-top: 1px solid $gray-100; } .metaItem { display: flex; align-items: center; - gap: 0.3rem; + gap: 0.4rem; color: $gray-500; font-size: 0.75rem; + font-weight: 500; svg { - width: 0.75rem; - height: 0.75rem; + width: 0.8rem; + height: 0.8rem; + color: $gray-400; + } + + span { + color: $gray-600; } } From 3965d8c2c6c9e2bf80c12be7c4056b07c83d4a00 Mon Sep 17 00:00:00 2001 From: CocoRoF Date: Wed, 16 Jul 2025 07:23:47 +0000 Subject: [PATCH 2/4] Refactor: Remove Canvas Page and Node Hook; Implement Chat Interface with Workflow Execution --- src/app/HomePage.module.scss | 785 ++++++++++++++--- src/app/chat/assets/ChatContent.module.scss | 3 + src/app/chat/assets/ChatInterface.module.scss | 405 +++++++++ src/app/chat/components/ChatContent.tsx | 24 +- src/app/chat/components/ChatInterface.tsx | 270 ++++++ src/app/main/assets/Executor.module.scss | 44 +- src/app/main/components/Executor.tsx | 12 +- src/app/page.tsx | 234 ++++-- .../(canvas_js)/assets/Canvas.module.scss | 46 - temporary/(canvas_js)/assets/Chat.module.scss | 179 ---- temporary/(canvas_js)/assets/Edge.module.scss | 22 - .../assets/ExecutionPanel.module.scss | 154 ---- .../(canvas_js)/assets/Header.module.scss | 190 ----- .../(canvas_js)/assets/MiniCanvas.module.scss | 108 --- temporary/(canvas_js)/assets/Node.module.scss | 314 ------- .../(canvas_js)/assets/NodeList.module.scss | 32 - .../(canvas_js)/assets/PlateeRAG.module.scss | 14 - .../(canvas_js)/assets/SideMenu.module.scss | 285 ------- .../assets/TemplatePreview.module.scss | 213 ----- .../assets/WorkflowPanel.module.scss | 399 --------- temporary/(canvas_js)/components/Canvas.jsx | 790 ------------------ temporary/(canvas_js)/components/Edge.jsx | 33 - .../(canvas_js)/components/ExecutionPanel.jsx | 94 --- temporary/(canvas_js)/components/Header.jsx | 125 --- .../components/Helper/DraggableNodeItem.jsx | 22 - .../components/Helper/NodeList.jsx | 27 - temporary/(canvas_js)/components/Node.jsx | 298 ------- temporary/(canvas_js)/components/SideMenu.jsx | 72 -- .../components/SideMenuPanel/AddNodePanel.jsx | 104 --- .../components/SideMenuPanel/ChatPanel.jsx | 115 --- .../components/SideMenuPanel/MiniCanvas.jsx | 206 ----- .../SideMenuPanel/TemplatePanel.jsx | 257 ------ .../SideMenuPanel/TemplatePreview.jsx | 130 --- .../SideMenuPanel/WorkflowPanel.jsx | 322 ------- temporary/(canvas_js)/constants/nodes.js | 160 ---- .../constants/workflow/Basic_Chatbot.json | 147 ---- .../constants/workflow/Data_Processing.json | 225 ----- temporary/(canvas_js)/nodeHook.js | 59 -- temporary/(canvas_js)/page.tsx | 754 ----------------- 39 files changed, 1576 insertions(+), 6097 deletions(-) create mode 100644 src/app/chat/assets/ChatInterface.module.scss create mode 100644 src/app/chat/components/ChatInterface.tsx delete mode 100644 temporary/(canvas_js)/assets/Canvas.module.scss delete mode 100644 temporary/(canvas_js)/assets/Chat.module.scss delete mode 100644 temporary/(canvas_js)/assets/Edge.module.scss delete mode 100644 temporary/(canvas_js)/assets/ExecutionPanel.module.scss delete mode 100644 temporary/(canvas_js)/assets/Header.module.scss delete mode 100644 temporary/(canvas_js)/assets/MiniCanvas.module.scss delete mode 100644 temporary/(canvas_js)/assets/Node.module.scss delete mode 100644 temporary/(canvas_js)/assets/NodeList.module.scss delete mode 100644 temporary/(canvas_js)/assets/PlateeRAG.module.scss delete mode 100644 temporary/(canvas_js)/assets/SideMenu.module.scss delete mode 100644 temporary/(canvas_js)/assets/TemplatePreview.module.scss delete mode 100644 temporary/(canvas_js)/assets/WorkflowPanel.module.scss delete mode 100644 temporary/(canvas_js)/components/Canvas.jsx delete mode 100644 temporary/(canvas_js)/components/Edge.jsx delete mode 100644 temporary/(canvas_js)/components/ExecutionPanel.jsx delete mode 100644 temporary/(canvas_js)/components/Header.jsx delete mode 100644 temporary/(canvas_js)/components/Helper/DraggableNodeItem.jsx delete mode 100644 temporary/(canvas_js)/components/Helper/NodeList.jsx delete mode 100644 temporary/(canvas_js)/components/Node.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenu.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenuPanel/AddNodePanel.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenuPanel/ChatPanel.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenuPanel/MiniCanvas.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenuPanel/TemplatePanel.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenuPanel/TemplatePreview.jsx delete mode 100644 temporary/(canvas_js)/components/SideMenuPanel/WorkflowPanel.jsx delete mode 100644 temporary/(canvas_js)/constants/nodes.js delete mode 100644 temporary/(canvas_js)/constants/workflow/Basic_Chatbot.json delete mode 100644 temporary/(canvas_js)/constants/workflow/Data_Processing.json delete mode 100644 temporary/(canvas_js)/nodeHook.js delete mode 100644 temporary/(canvas_js)/page.tsx diff --git a/src/app/HomePage.module.scss b/src/app/HomePage.module.scss index cf8cae4f..ac1e3ce3 100644 --- a/src/app/HomePage.module.scss +++ b/src/app/HomePage.module.scss @@ -4,6 +4,8 @@ $primary-blue: #2563eb; $primary-purple: #7c3aed; $primary-green: #059669; $primary-pink: #db2777; +$primary-orange: #ea580c; +$primary-indigo: #4f46e5; $gray-50: #f9fafb; $gray-100: #f3f4f6; $gray-200: #e5e7eb; @@ -47,32 +49,54 @@ $white: #ffffff; // Main Container .container { min-height: 100vh; - background: linear-gradient(135deg, $gray-50 0%, $white 100%); + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%); + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 30%, rgba(148, 163, 184, 0.08) 0%, transparent 50%), + radial-gradient(circle at 80% 70%, rgba(100, 116, 139, 0.06) 0%, transparent 50%); + pointer-events: none; + } } // Header Styles .header { position: relative; - z-index: 10; + z-index: 100; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-bottom: 1px solid rgba(148, 163, 184, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } .nav { - max-width: 1280px; + max-width: 1400px; margin: 0 auto; - padding: 0 1rem; + padding: 0 2rem; @media (min-width: 640px) { - padding: 0 1.5rem; + padding: 0 3rem; } @media (min-width: 1024px) { - padding: 0 2rem; + padding: 0 4rem; + } + + @media (min-width: 1440px) { + padding: 0 5rem; } } .navContent { display: flex; - height: 4rem; + height: 4.5rem; align-items: center; justify-content: space-between; } @@ -83,10 +107,14 @@ $white: #ffffff; flex-shrink: 0; h1 { - font-size: 1.5rem; + font-size: 1.75rem; font-weight: 700; margin: 0; - @include gradient-text($primary-blue, $primary-purple); + background: linear-gradient(135deg, $gray-900, $gray-700); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -0.025em; } } @@ -98,102 +126,383 @@ $white: #ffffff; .getStartedBtn { @include button-primary; - background-color: $primary-blue; + background: linear-gradient(135deg, $primary-blue, $primary-purple); color: $white; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); + font-weight: 600; &:hover { - background-color: color.adjust($primary-blue, $lightness: -10%); + background: linear-gradient(135deg, #1d4ed8, #6d28d9); + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); } svg { margin-left: 0.5rem; width: 1rem; height: 1rem; + transition: transform 0.2s ease; + } + + &:hover svg { + transform: translateX(2px); } } .secondaryBtn { @include button-primary; - background-color: $white; + background-color: rgba(255, 255, 255, 0.8); color: $gray-700; - border: 1px solid $gray-300; + border: 1px solid rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); + font-weight: 500; &:hover { - background-color: $gray-50; + background-color: $white; color: $gray-900; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + svg { + margin-right: 0.5rem; + width: 1rem; + height: 1rem; } } // Main Content .main { position: relative; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 100vw; + overflow-x: hidden; } .heroSection { - max-width: 1280px; - margin: 0 auto; - padding: 5rem 1rem 4rem; - text-align: center; + width: 90vw; + max-width: 1400px; + margin: 2rem auto; + padding: 4rem 2rem; + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-radius: 24px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(255, 255, 255, 0.8); + + @media (min-width: 1024px) { + width: 85vw; + padding: 5rem 4rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; + text-align: left; + margin: 3rem auto; + } @media (min-width: 640px) { - padding-left: 1.5rem; - padding-right: 1.5rem; + width: 88vw; + padding: 4rem 3rem; } - @media (min-width: 1024px) { - padding-left: 2rem; - padding-right: 2rem; + @media (min-width: 1440px) { + width: 80vw; + padding: 6rem 5rem; + } +} + +.heroContent { + @media (max-width: 1023px) { + text-align: center; + } +} + +.heroLabel { + display: inline-block; + margin-bottom: 1.5rem; + + span { + background: linear-gradient(135deg, $gray-100, $gray-50); + border: 1px solid $gray-200; + border-radius: 50px; + padding: 0.5rem 1.5rem; + color: $gray-700; + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.3px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } } .heroTitle { - font-size: 2.25rem; + font-size: 2.5rem; font-weight: 700; - line-height: 1.1; + line-height: 1.2; color: $gray-900; margin-bottom: 1.5rem; @media (min-width: 640px) { - font-size: 3.75rem; + font-size: 3.2rem; + } + + @media (min-width: 1024px) { + font-size: 3.8rem; } .highlight { display: block; - color: $primary-blue; + background: linear-gradient(135deg, $primary-blue, $primary-purple); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-top: 0.5rem; + font-weight: 600; } } .heroDescription { font-size: 1.125rem; - line-height: 1.75; + line-height: 1.7; color: $gray-600; - max-width: 48rem; - margin: 0 auto 2.5rem; + margin-bottom: 2rem; + max-width: 600px; + + @media (min-width: 1024px) { + margin-bottom: 2.5rem; + } +} + +.heroStats { + display: flex; + gap: 2rem; + margin-bottom: 2.5rem; + justify-content: center; + flex-wrap: wrap; + + @media (min-width: 1024px) { + justify-content: flex-start; + } +} + +.statItem { + text-align: center; + padding: 1rem; + background: $gray-50; + border-radius: 12px; + border: 1px solid $gray-100; + + strong { + display: block; + font-size: 1.5rem; + font-weight: 700; + color: $gray-900; + margin-bottom: 0.25rem; + } + + span { + font-size: 0.875rem; + color: $gray-600; + } } .heroActions { display: flex; align-items: center; - justify-content: center; - gap: 1.5rem; + gap: 1rem; flex-wrap: wrap; + justify-content: center; + + @media (min-width: 1024px) { + justify-content: flex-start; + } +} + +.heroVisual { + display: none; + + @media (min-width: 1024px) { + display: block; + position: relative; + } +} + +.heroImage { + position: relative; + perspective: 1000px; + + &::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: + radial-gradient(circle at center, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + animation: float 6s ease-in-out infinite; + pointer-events: none; + } +} + +.mockupScreen { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 16px; + overflow: hidden; + transform: rotateY(-5deg) rotateX(2deg); + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.25), + 0 0 0 1px rgba(255, 255, 255, 0.1); + transition: transform 0.3s ease; + + &:hover { + transform: rotateY(-2deg) rotateX(1deg) scale(1.02); + } +} + +.mockupHeader { + padding: 1rem 1.5rem; + background: rgba(255, 255, 255, 0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + gap: 1rem; + + span { + color: $white; + font-size: 0.875rem; + font-weight: 500; + } +} + +.mockupDots { + display: flex; + gap: 0.5rem; + + span { + width: 12px; + height: 12px; + border-radius: 50%; + + &:nth-child(1) { background: #ff5f57; } + &:nth-child(2) { background: #ffbd2e; } + &:nth-child(3) { background: #28ca42; } + } +} + +.mockupContent { + padding: 2rem; + min-height: 200px; +} + +.mockupNodes { + display: flex; + align-items: center; + gap: 1rem; + justify-content: center; +} + +.mockupNode { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1rem; + border-radius: 12px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; + + svg { + width: 24px; + height: 24px; + } + + span { + font-size: 0.75rem; + color: $white; + font-weight: 500; + } + + &.input { + background: rgba(37, 99, 235, 0.2); + color: #60a5fa; + + &:hover { + background: rgba(37, 99, 235, 0.3); + transform: translateY(-2px); + } + } + + &.ai { + background: rgba(124, 58, 237, 0.2); + color: #a78bfa; + + &:hover { + background: rgba(124, 58, 237, 0.3); + transform: translateY(-2px); + } + } + + &.output { + background: rgba(5, 150, 105, 0.2); + color: #34d399; + + &:hover { + background: rgba(5, 150, 105, 0.3); + transform: translateY(-2px); + } + } +} + +.mockupFlow { + width: 40px; + height: 2px; + background: linear-gradient(90deg, transparent, rgba(37, 99, 235, 0.8), transparent); + position: relative; + + &::after { + content: ''; + position: absolute; + right: -4px; + top: -2px; + width: 0; + height: 0; + border-left: 6px solid rgba(37, 99, 235, 0.8); + border-top: 3px solid transparent; + border-bottom: 3px solid transparent; + } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-20px); } } .primaryBtn { @include button-primary; - background-color: $primary-blue; + background: linear-gradient(135deg, $primary-blue, $primary-purple); color: $white; - padding: 0.75rem 1.5rem; - box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); + padding: 1rem 2rem; + box-shadow: 0 10px 25px rgba(37, 99, 235, 0.3); + font-weight: 600; + border-radius: 12px; &:hover { - background-color: color.adjust($primary-blue, $lightness: -10%); + background: linear-gradient(135deg, #1d4ed8, #6d28d9); transform: translateY(-2px); - box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15); + box-shadow: 0 15px 35px rgba(37, 99, 235, 0.4); } svg { - margin-right: 0.5rem; + margin-right: 0.75rem; width: 1.25rem; height: 1.25rem; } @@ -212,18 +521,32 @@ $white: #ffffff; // Features Section .featuresSection { - max-width: 1280px; - margin: 0 auto; - padding: 6rem 1rem; - + width: 90vw; + max-width: 1400px; + margin: 4rem auto; + padding: 6rem 2rem; + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-radius: 24px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(255, 255, 255, 0.8); + @media (min-width: 640px) { - padding-left: 1.5rem; - padding-right: 1.5rem; + width: 88vw; + padding: 6rem 3rem; } @media (min-width: 1024px) { - padding-left: 2rem; - padding-right: 2rem; + width: 85vw; + margin: 6rem auto; + padding: 8rem 4rem; + } + + @media (min-width: 1440px) { + width: 80vw; + padding: 10rem 5rem; } } @@ -232,20 +555,25 @@ $white: #ffffff; margin-bottom: 4rem; h2 { - font-size: 1.875rem; - font-weight: 700; - color: $gray-900; + font-size: 2.5rem; + font-weight: 800; + background: linear-gradient(135deg, $gray-900, $primary-blue); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; margin-bottom: 1rem; @media (min-width: 640px) { - font-size: 2.25rem; + font-size: 3rem; } } p { - font-size: 1.125rem; + font-size: 1.25rem; color: $gray-600; margin: 0; + max-width: 600px; + margin: 0 auto; } } @@ -256,135 +584,270 @@ $white: #ffffff; @media (min-width: 640px) { grid-template-columns: repeat(2, 1fr); + gap: 2.5rem; } @media (min-width: 1024px) { grid-template-columns: repeat(3, 1fr); + gap: 3rem; + } + + @media (min-width: 1440px) { + grid-template-columns: repeat(3, 1fr); + gap: 4rem; + } + + @media (min-width: 1600px) { + grid-template-columns: repeat(3, 1fr); + gap: 5rem; } } .featureCard { position: relative; + height: 100%; .cardBackground { position: absolute; inset: 0; - border-radius: 0.5rem; - opacity: 0.25; - filter: blur(4px); - transition: opacity 0.2s ease-in-out; + border-radius: 1rem; + opacity: 0.1; + transition: all 0.3s ease; &.blue { - background: linear-gradient(to right, $primary-blue, $primary-purple); + background: linear-gradient(135deg, $primary-blue, $primary-purple); } &.purple { - background: linear-gradient(to right, $primary-purple, $primary-pink); + background: linear-gradient(135deg, $primary-purple, $primary-pink); } &.green { - background: linear-gradient(to right, $primary-green, $primary-blue); + background: linear-gradient(135deg, $primary-green, $primary-blue); + } + + &.orange { + background: linear-gradient(135deg, $primary-orange, #f59e0b); + } + + &.pink { + background: linear-gradient(135deg, $primary-pink, $primary-purple); + } + + &.indigo { + background: linear-gradient(135deg, $primary-indigo, $primary-blue); } } - &:hover .cardBackground { - opacity: 0.4; + &:hover { + .cardBackground { + opacity: 0.15; + transform: scale(1.02); + } + + .cardContent { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + } } .cardContent { position: relative; background-color: $white; - padding: 2rem; - border-radius: 0.5rem; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); - @include card-hover; + padding: 2.5rem 2rem; + border-radius: 1rem; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; + height: 100%; + display: flex; + flex-direction: column; } .cardHeader { display: flex; - align-items: center; - margin-bottom: 1rem; + align-items: flex-start; + margin-bottom: 1.5rem; svg { - width: 2rem; - height: 2rem; + width: 2.5rem; + height: 2.5rem; flex-shrink: 0; - - &.blue { - color: $primary-blue; - } - - &.purple { - color: $primary-purple; - } - - &.green { - color: $primary-green; - } + margin-top: 0.25rem; + + &.blue { color: $primary-blue; } + &.purple { color: $primary-purple; } + &.green { color: $primary-green; } + &.orange { color: $primary-orange; } + &.pink { color: $primary-pink; } + &.indigo { color: $primary-indigo; } } h3 { - font-size: 1.125rem; - font-weight: 500; + font-size: 1.375rem; + font-weight: 600; color: $gray-900; margin: 0 0 0 1rem; + line-height: 1.3; } } .cardDescription { color: $gray-600; - margin: 0; - line-height: 1.6; + margin: 0 0 1.5rem 0; + line-height: 1.7; + font-size: 1rem; + flex-grow: 1; + } + + .cardFeatures { + display: flex; + flex-direction: column; + gap: 0.5rem; + + span { + font-size: 0.875rem; + color: $gray-500; + display: flex; + align-items: center; + + &::before { + content: '✓'; + color: $primary-green; + font-weight: 600; + margin-right: 0.5rem; + } + } } } // CTA Section .ctaSection { - background-color: $primary-blue; - color: $white; + width: 90vw; + max-width: 1400px; + margin: 4rem auto; + position: relative; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-radius: 24px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(255, 255, 255, 0.8); + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + linear-gradient(135deg, $gray-50 0%, $white 100%), + radial-gradient(circle at 30% 20%, rgba(37, 99, 235, 0.03) 0%, transparent 50%), + radial-gradient(circle at 70% 80%, rgba(124, 58, 237, 0.03) 0%, transparent 50%); + pointer-events: none; + } + + @media (min-width: 640px) { + width: 88vw; + } + + @media (min-width: 1024px) { + width: 85vw; + margin: 6rem auto; + } + + @media (min-width: 1440px) { + width: 80vw; + } .ctaContent { - max-width: 1280px; + max-width: 900px; margin: 0 auto; - padding: 4rem 1rem; + padding: 6rem 2rem; text-align: center; + position: relative; + z-index: 1; @media (min-width: 640px) { - padding-left: 1.5rem; - padding-right: 1.5rem; + padding: 7rem 3rem; } @media (min-width: 1024px) { - padding-left: 2rem; - padding-right: 2rem; + padding: 8rem 4rem; } - h2 { - font-size: 1.875rem; - font-weight: 700; - margin-bottom: 1rem; + @media (min-width: 1440px) { + padding: 10rem 5rem; + } + } + + .ctaLabel { + margin-bottom: 1.5rem; + + span { + background: linear-gradient(135deg, $gray-100, $gray-50); + border: 1px solid $gray-200; + border-radius: 50px; + padding: 0.5rem 1.5rem; + color: $gray-700; + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.3px; + display: inline-block; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + } - @media (min-width: 640px) { - font-size: 2.25rem; - } + h2 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: $gray-900; + + @media (min-width: 640px) { + font-size: 3.2rem; } + } - p { - font-size: 1.125rem; - color: rgba(255, 255, 255, 0.8); - margin-bottom: 2rem; + p { + font-size: 1.25rem; + color: $gray-600; + margin-bottom: 3rem; + line-height: 1.6; + } + + .ctaActions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 2rem; + } + + .ctaNote { + span { + font-size: 0.875rem; + color: $gray-500; + font-style: italic; } } } .ctaBtn { @include button-primary; - background-color: $white; - color: $primary-blue; - box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); - + background: linear-gradient(135deg, $primary-blue, $primary-purple); + color: $white; + box-shadow: 0 8px 20px rgba(37, 99, 235, 0.25); + border: none; + font-weight: 600; + padding: 1rem 2rem; + border-radius: 12px; + &:hover { - background-color: $gray-50; + background: linear-gradient(135deg, #1d4ed8, #6d28d9); + transform: translateY(-2px); + box-shadow: 0 12px 30px rgba(37, 99, 235, 0.35); } svg { @@ -394,23 +857,67 @@ $white: #ffffff; } } +.ctaSecondaryBtn { + @include button-primary; + background: $gray-50; + color: $gray-700; + border: 1px solid $gray-200; + font-weight: 600; + padding: 1rem 2rem; + border-radius: 12px; + + &:hover { + background: $white; + color: $gray-900; + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + } + + svg { + margin-right: 0.5rem; + width: 1.25rem; + height: 1.25rem; + } +} + // Footer .footer { - background-color: $gray-50; + background: linear-gradient(135deg, #1f2937, #111827); + color: $white; + position: relative; + width: 100vw; + margin-left: calc(-50vw + 50%); + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, rgba(37, 99, 235, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 50%, rgba(124, 58, 237, 0.1) 0%, transparent 50%); + pointer-events: none; + } .footerContent { - max-width: 1280px; + max-width: 1400px; margin: 0 auto; - padding: 3rem 1rem; + padding: 5rem 2rem 3rem; + position: relative; + z-index: 1; @media (min-width: 640px) { - padding-left: 1.5rem; - padding-right: 1.5rem; + padding: 5rem 3rem 3rem; } @media (min-width: 1024px) { - padding-left: 2rem; - padding-right: 2rem; + padding: 6rem 4rem 3rem; + } + + @media (min-width: 1440px) { + padding: 8rem 5rem 4rem; } } @@ -422,20 +929,19 @@ $white: #ffffff; gap: 1rem; .footerBrand { - display: flex; - align-items: center; - gap: 1rem; - h3 { - font-size: 1.125rem; - font-weight: 600; - color: $gray-900; - margin: 0; + font-size: 1.5rem; + font-weight: 700; + background: linear-gradient(135deg, $white, #e2e8f0); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin: 0 0 0.5rem 0; } p { - font-size: 0.875rem; - color: $gray-600; + font-size: 1rem; + color: rgba(255, 255, 255, 0.8); margin: 0; } } @@ -443,33 +949,44 @@ $white: #ffffff; .footerSocial { display: flex; align-items: center; - gap: 1.5rem; + gap: 1rem; a { - color: $gray-400; - transition: color 0.2s ease-in-out; + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + color: rgba(255, 255, 255, 0.8); + transition: all 0.3s ease; + backdrop-filter: blur(10px); &:hover { - color: $gray-500; + background: rgba(255, 255, 255, 0.2); + color: $white; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); } svg { - width: 1.5rem; - height: 1.5rem; + width: 1.25rem; + height: 1.25rem; } } } } .footerBottom { - margin-top: 2rem; - border-top: 1px solid $gray-200; + border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 2rem; text-align: center; p { font-size: 0.875rem; - color: $gray-500; + color: rgba(255, 255, 255, 0.6); margin: 0; } } diff --git a/src/app/chat/assets/ChatContent.module.scss b/src/app/chat/assets/ChatContent.module.scss index de94e921..c43bdfd9 100644 --- a/src/app/chat/assets/ChatContent.module.scss +++ b/src/app/chat/assets/ChatContent.module.scss @@ -41,11 +41,14 @@ $white: #ffffff; padding: 2rem; transition: all 0.5s ease; overflow: hidden; + height: 100vh; + max-height: 100vh; .container { width: 100%; max-width: 1200px; margin: 0 auto; + height: 100%; } } diff --git a/src/app/chat/assets/ChatInterface.module.scss b/src/app/chat/assets/ChatInterface.module.scss new file mode 100644 index 00000000..33df9d2c --- /dev/null +++ b/src/app/chat/assets/ChatInterface.module.scss @@ -0,0 +1,405 @@ +// Color Variables +$primary-blue: #2563eb; +$primary-green: #059669; +$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-800: #1f2937; +$gray-900: #111827; +$white: #ffffff; + +.container { + width: 100%; + height: 85vh; + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +// Header +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid $gray-200; + flex-shrink: 0; + background: $white; +} + +.headerInfo { + display: flex; + align-items: center; + gap: 1rem; + + h2 { + font-size: 1.5rem; + font-weight: 700; + color: $gray-900; + margin: 0; + } + + p { + color: $gray-600; + margin: 0; + font-size: 0.9rem; + } +} + +.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; + } + } +} + +.chatCount { + display: flex; + align-items: center; + gap: 0.5rem; + color: $gray-600; + font-size: 0.9rem; + font-weight: 500; + + svg { + width: 16px; + height: 16px; + color: $primary-blue; + } +} + +// Chat Container +.chatContainer { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; +} + +.loadingState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: $gray-500; + gap: 1rem; + + p { + font-size: 1rem; + margin: 0; + } +} + +.loadingSpinner { + width: 32px; + height: 32px; + border: 3px solid $gray-200; + border-top: 3px solid $primary-blue; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +// Messages Area +.messagesArea { + flex: 1; + overflow-y: auto; + padding: 1.5rem 0; + display: flex; + flex-direction: column; + gap: 1.5rem; + min-height: 0; + + /* 스크롤바 스타일링 */ + &::-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; + } + } +} + +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + color: $gray-500; + + .emptyIcon { + width: 3rem; + height: 3rem; + margin-bottom: 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; + } +} + +// Message Exchange +.messageExchange { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.userMessage { + display: flex; + flex-direction: column; + align-items: flex-end; + padding-right: 1rem; + + .messageContent { + background: linear-gradient(135deg, $primary-blue, #3b82f6); + color: $white; + padding: 0.75rem 1rem; + border-radius: 1rem 1rem 0.25rem 1rem; + max-width: 70%; + word-wrap: break-word; + box-shadow: 0 2px 8px rgba($primary-blue, 0.25); + } + + .messageTime { + font-size: 0.75rem; + color: $gray-500; + margin-top: 0.25rem; + } +} + +.botMessage { + display: flex; + flex-direction: column; + align-items: flex-start; + + .messageContent { + background: $white; + border: 1px solid $gray-200; + color: $gray-900; + padding: 0.75rem 1rem; + border-radius: 1rem 1rem 1rem 0.25rem; + max-width: 70%; + word-wrap: break-word; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + } +} + +// Typing Indicator +.typingIndicator { + display: flex; + align-items: center; + gap: 0.25rem; + + span { + width: 6px; + height: 6px; + background-color: $gray-400; + border-radius: 50%; + animation: typing 1.4s infinite ease-in-out; + + &:nth-child(1) { + animation-delay: -0.32s; + } + + &:nth-child(2) { + animation-delay: -0.16s; + } + } +} + +@keyframes typing { + + 0%, + 80%, + 100% { + transform: scale(0.8); + opacity: 0.5; + } + + 40% { + transform: scale(1); + opacity: 1; + } +} + +// Input Area +.inputArea { + flex-shrink: 0; + border-top: 1px solid $gray-200; + padding: 1rem 0 0 0; + background: $white; +} + +.inputContainer { + display: flex; + gap: 0.75rem; + align-items: flex-end; +} + +.messageInput { + flex: 1; + padding: 0.75rem 1rem; + border: 2px solid $gray-200; + border-radius: 1rem; + font-size: 0.95rem; + resize: none; + outline: none; + transition: all 0.2s ease; + background: $white; + + &:focus { + border-color: $primary-blue; + box-shadow: 0 0 0 3px rgba($primary-blue, 0.1); + } + + &:disabled { + background: $gray-50; + cursor: not-allowed; + } + + &::placeholder { + color: $gray-400; + } +} + +.sendButton { + background: $primary-blue; + color: $white; + border: none; + border-radius: 1rem; + padding: 0.75rem 1rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 48px; + height: 48px; + + &:hover:not(.disabled) { + background: #1d4ed8; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba($primary-blue, 0.3); + } + + &:active:not(.disabled) { + transform: translateY(0); + } + + &.disabled { + background: $gray-300; + cursor: not-allowed; + } + + svg { + font-size: 1.1rem; + } +} + +.miniSpinner { + width: 16px; + height: 16px; + border: 2px solid transparent; + border-top: 2px solid currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.executingNote { + margin: 0.5rem 0 0 0; + font-size: 0.85rem; + color: $primary-blue; + font-weight: 500; +} + +.errorNote { + margin: 0.5rem 0 0 0; + font-size: 0.85rem; + color: $primary-red; + font-weight: 500; +} + +// Responsive Design +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: stretch; + gap: 1rem; + } + + .headerInfo { + justify-content: space-between; + } + + .userMessage .messageContent, + .botMessage .messageContent { + max-width: 85%; + } + + .inputContainer { + gap: 0.5rem; + } + + .messageInput { + font-size: 16px; // iOS zoom 방지 + } +} \ No newline at end of file diff --git a/src/app/chat/components/ChatContent.tsx b/src/app/chat/components/ChatContent.tsx index 1a9f4602..b504727d 100644 --- a/src/app/chat/components/ChatContent.tsx +++ b/src/app/chat/components/ChatContent.tsx @@ -3,15 +3,32 @@ 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'; +import ChatInterface from './ChatInterface'; const ChatContent: React.FC = () => { - const [currentView, setCurrentView] = useState<'welcome' | 'workflow'>('welcome'); + const [currentView, setCurrentView] = useState<'welcome' | 'workflow' | 'chat'>('welcome'); + const [selectedWorkflow, setSelectedWorkflow] = useState(null); const handleWorkflowSelect = (workflow: any) => { - // 워크플로우 선택 후 로직 (나중에 구현) - console.log('Selected workflow:', workflow); + setSelectedWorkflow(workflow); + setCurrentView('chat'); }; + // 채팅 화면 + if (currentView === 'chat' && selectedWorkflow) { + return ( +
+
+ setCurrentView('workflow')} + /> +
+
+ ); + } + + // 워크플로우 선택 화면 if (currentView === 'workflow') { return (
@@ -25,6 +42,7 @@ const ChatContent: React.FC = () => { ); } + // 웰컴 화면 return (
diff --git a/src/app/chat/components/ChatInterface.tsx b/src/app/chat/components/ChatInterface.tsx new file mode 100644 index 00000000..2b120c7f --- /dev/null +++ b/src/app/chat/components/ChatInterface.tsx @@ -0,0 +1,270 @@ +'use client'; +import React, { useState, useEffect, useRef } from 'react'; +import { + FiSend, + FiArrowLeft, + FiMessageSquare, + FiClock, +} from 'react-icons/fi'; +import styles from '@/app/chat/assets/ChatInterface.module.scss'; +import { getWorkflowIOLogs, executeWorkflowById } from '@/app/api/workflowAPI'; +import toast from 'react-hot-toast'; + +interface Workflow { + id: string; + name: string; + description?: string; + createdAt?: string; + lastModified?: string; + author: string; + nodeCount: number; + status: 'active' | 'draft' | 'archived'; + filename?: string; + error?: string; +} + +interface IOLog { + log_id: number | string; + workflow_name: string; + workflow_id: string; + input_data: string; + output_data: string; + updated_at: string; +} + +interface ChatInterfaceProps { + workflow: Workflow; + onBack: () => void; +} + +const ChatInterface: React.FC = ({ workflow, onBack }) => { + const [ioLogs, setIOLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [executing, setExecuting] = useState(false); + const [error, setError] = useState(null); + const [inputMessage, setInputMessage] = useState(''); + const [pendingLogId, setPendingLogId] = useState(null); + + const messagesRef = useRef(null); + + useEffect(() => { + loadChatLogs(); + }, [workflow]); + + useEffect(() => { + scrollToBottom(); + }, [ioLogs]); + + const scrollToBottom = () => { + if (messagesRef.current) { + messagesRef.current.scrollTop = messagesRef.current.scrollHeight; + } + }; + + const loadChatLogs = async () => { + try { + setLoading(true); + setError(null); + const logs = await getWorkflowIOLogs(workflow.name, workflow.id); + setIOLogs((logs as any).in_out_logs || []); + setPendingLogId(null); + } catch (err) { + setError('채팅 기록을 불러오는데 실패했습니다.'); + setIOLogs([]); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString('ko-KR', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const executeWorkflow = async () => { + if (!inputMessage.trim()) { + return; + } + + setError(null); + setExecuting(true); + const tempId = `pending-${Date.now()}`; + setPendingLogId(tempId); + + // 임시 메시지 추가 + setIOLogs((prev) => [ + ...prev, + { + log_id: tempId, + workflow_name: workflow.name, + workflow_id: workflow.id, + input_data: inputMessage, + output_data: '', + updated_at: new Date().toISOString(), + }, + ]); + + const currentMessage = inputMessage; + setInputMessage(''); + + try { + const result: any = await executeWorkflowById( + workflow.name, + workflow.id, + currentMessage, + ); + + // 결과로 임시 메시지 업데이트 + setIOLogs((prev) => + prev.map((log) => + String(log.log_id) === tempId + ? { + ...log, + output_data: result.outputs + ? JSON.stringify(result.outputs) + : result.message || '처리 완료', + updated_at: new Date().toISOString(), + } + : log, + ), + ); + setPendingLogId(null); + } catch (err) { + // 에러로 임시 메시지 업데이트 + setIOLogs((prev) => + prev.map((log) => + String(log.log_id) === tempId + ? { + ...log, + output_data: err instanceof Error ? err.message : '처리 중 오류가 발생했습니다.', + updated_at: new Date().toISOString(), + } + : log, + ), + ); + setPendingLogId(null); + toast.error('메시지 처리 중 오류가 발생했습니다.'); + } finally { + setExecuting(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey && !executing) { + e.preventDefault(); + executeWorkflow(); + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+

{workflow.name}

+

AI 워크플로우와 대화하세요

+
+
+
+ + {ioLogs.length}개의 대화 +
+
+ + {/* Chat Area */} +
+ {loading ? ( +
+
+

채팅 기록을 불러오는 중...

+
+ ) : ( + <> +
+ {ioLogs.length === 0 ? ( +
+ +

첫 대화를 시작해보세요!

+

"{workflow.name}" 워크플로우가 준비되었습니다.

+
+ ) : ( + ioLogs.map((log) => ( +
+ {/* User Message */} +
+
+ {log.input_data} +
+
+ {formatDate(log.updated_at)} +
+
+ + {/* Bot Message */} +
+
+ {String(log.log_id) === pendingLogId && executing && !log.output_data ? ( +
+ + + +
+ ) : ( + log.output_data + )} +
+
+
+ )) + )} +
+ + {/* Input Area */} +
+
+ setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={executing} + className={styles.messageInput} + /> + +
+ {executing && ( +

+ 워크플로우를 실행 중입니다... +

+ )} + {error && ( +

{error}

+ )} +
+ + )} +
+
+ ); +}; + +export default ChatInterface; diff --git a/src/app/main/assets/Executor.module.scss b/src/app/main/assets/Executor.module.scss index cdbd7592..d657f4fd 100644 --- a/src/app/main/assets/Executor.module.scss +++ b/src/app/main/assets/Executor.module.scss @@ -190,6 +190,41 @@ } } +// Typing Indicator +.typingIndicator { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0; + + span { + width: 6px; + height: 6px; + background-color: #6c757d; + border-radius: 50%; + animation: typing 1.4s infinite ease-in-out; + + &:nth-child(1) { + animation-delay: -0.32s; + } + + &:nth-child(2) { + animation-delay: -0.16s; + } + } +} + +@keyframes typing { + 0%, 80%, 100% { + transform: scale(0.8); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + .executorInputArea { border-top: 1px solid #e9ecef; padding: 1rem 1.5rem; @@ -260,15 +295,6 @@ width: 16px; height: 16px; } - - .miniSpinner { - width: 16px; - height: 16px; - border: 2px solid #e9ecef; - border-top: 2px solid #007bff; - border-radius: 50%; - animation: spin 1s linear infinite; - } } } diff --git a/src/app/main/components/Executor.tsx b/src/app/main/components/Executor.tsx index e7c13781..d8748d70 100644 --- a/src/app/main/components/Executor.tsx +++ b/src/app/main/components/Executor.tsx @@ -232,11 +232,11 @@ const Executor: React.FC = ({ workflow }) => { pendingLogId && executing && !log.output_data ? ( - +
+ + + +
) : ( log.output_data )} @@ -269,7 +269,7 @@ const Executor: React.FC = ({ workflow }) => { > {executing ? (
) : ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 319de423..4c38f515 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,6 +7,12 @@ import { FiZap, FiGrid, FiUsers, + FiMessageCircle, + FiCpu, + FiLayers, + FiTrendingUp, + FiShield, + FiGlobe, } from 'react-icons/fi'; import styles from '@/app/HomePage.module.scss'; @@ -39,84 +45,202 @@ export default function HomePage() { {/* Hero Section */}
-

- Build AI Workflows - - Visually & Intuitively - -

-

- PlateerRAG는 드래그 앤 드롭으로 AI 워크플로우를 구축할 - 수 있는 비주얼 에디터입니다.

- 복잡한 AI 파이프라인을 직관적인 노드 기반 인터페이스로 - 간단하게 만들어보세요. -

-
- - - 지금 시작하기 - - - 관리센터 둘러보기 - +
+
+ 🚀 Next-Gen AI Workflow Platform +
+

+ Build Intelligent AI Workflows + + with Visual Simplicity + +

+

+ PlateerRAG는 차세대 AI 워크플로우 플랫폼입니다. + 드래그 앤 드롭으로 복잡한 AI 파이프라인을 구축하고, + 실시간 채팅으로 AI와 상호작용하며, + 강력한 관리도구로 워크플로우를 효율적으로 운영하세요. +

+
+
+ 5+ + AI 노드 타입 +
+
+ 실시간 + 채팅 인터페이스 +
+
+ 무제한 + 워크플로우 생성 +
+
+
+ + + 워크플로우 만들기 + + + + AI 채팅 체험 + +
+
+
+
+
+
+
+ + + +
+ PlateerRAG Canvas +
+
+
+
+ + Input +
+
+
+ + AI Process +
+
+
+ + Output +
+
+
+
+
{/* Feature Cards */}
-

핵심 기능

+

왜 PlateerRAG인가요?

- PlateerRAG가 제공하는 강력한 기능들을 확인해보세요 + AI 워크플로우 구축의 새로운 표준을 제시하는 혁신적인 기능들

-
+
-

비주얼 워크플로우 에디터

+

비주얼 캔버스 에디터

+
+

+ 직관적인 드래그 앤 드롭으로 복잡한 AI 워크플로우를 + 시각적으로 구성하고 실시간으로 미리보기할 수 있습니다. +

+
+ • 5+ AI 노드 타입 + • 실시간 미리보기 + • 자동 유효성 검사 +
+
+
+ +
+
+
+
+ +

실시간 AI 채팅

+
+

+ 완성된 워크플로우와 자연스럽게 대화하며 + AI의 응답을 실시간으로 확인할 수 있습니다. +

+
+ • 실시간 응답 + • 대화 기록 저장 + • 다중 워크플로우 선택 +
+
+
+ +
+
+
+
+ +

스마트 관리센터

+
+

+ 워크플로우 성능 모니터링, 실행 로그 분석, + 그리고 팀 협업을 위한 통합 관리 환경을 제공합니다. +

+
+ • 성능 대시보드 + • 실행 로그 분석 + • 팀 협업 도구 +
+
+
+ +
+
+
+
+ +

고성능 실행 엔진

- 드래그 앤 드롭으로 노드를 연결하여 복잡한 AI - 워크플로우를 시각적으로 구성할 수 있습니다. + 최적화된 실행 엔진으로 대규모 워크플로우도 + 빠르고 안정적으로 처리합니다.

+
+ • 병렬 처리 지원 + • 자동 스케일링 + • 에러 복구 시스템 +
-
+
- -

실시간 실행

+ +

엔터프라이즈 보안

- 구성한 워크플로우를 실시간으로 실행하고 - 결과를 즉시 확인할 수 있습니다. + 기업급 보안과 데이터 보호 기능으로 + 안전한 AI 워크플로우 환경을 보장합니다.

+
+ • 데이터 암호화 + • 접근 권한 제어 + • 감사 로그 +
-
+
- -

협업 환경

+ +

개방형 생태계

- 팀원들과 함께 워크플로우를 공유하고 협업할 - 수 있는 환경을 제공합니다. + 다양한 AI 모델과 서비스를 쉽게 연동하고 + 확장 가능한 플러그인 시스템을 제공합니다.

+
+ • API 통합 + • 플러그인 지원 + • 커뮤니티 마켓플레이스 +
@@ -125,12 +249,24 @@ export default function HomePage() { {/* CTA Section */}
+
+ 🎯 Ready to Transform Your AI Workflow? +

지금 바로 시작해보세요

-

몇 분 안에 첫 번째 AI 워크플로우를 만들어보세요

- - - 워크플로우 만들기 - +

몇 분 안에 첫 번째 AI 워크플로우를 만들고 실행해보세요

+
+ + + 무료로 시작하기 + + + + 관리센터 둘러보기 + +
+
+ Let's Start +
@@ -141,10 +277,10 @@ export default function HomePage() {

PlateerRAG

-

AI 워크플로우의 새로운 패러다임

+

Next Generation AI Workflow

diff --git a/temporary/(canvas_js)/assets/Canvas.module.scss b/temporary/(canvas_js)/assets/Canvas.module.scss deleted file mode 100644 index 53d51557..00000000 --- a/temporary/(canvas_js)/assets/Canvas.module.scss +++ /dev/null @@ -1,46 +0,0 @@ -.canvasContainer { - width: 100vw; - height: 100vh; - overflow: hidden; - background-color: #f0f2f5; - position: relative; - cursor: grab; -} - -.canvasGrid { - width: 5000vw; - height: 5000vh; - position: relative; - transform-origin: 0 0; - - $background-color: #ffffff; - $grid-color-minor: rgba(0, 0, 0, 0.08); - $grid-color-major: rgba(0, 0, 0, 0.15); - $grid-size-minor: 20px; - $grid-size-major: 100px; - - background-color: $background-color; - background-image: - linear-gradient($grid-color-minor 1px, transparent 1px), - linear-gradient(90deg, $grid-color-minor 1px, transparent 1px), - linear-gradient($grid-color-major 1px, transparent 1px), - linear-gradient(90deg, $grid-color-major 1px, transparent 1px); - - background-size: - $grid-size-minor $grid-size-minor, - $grid-size-minor $grid-size-minor, - $grid-size-major $grid-size-major, - $grid-size-major $grid-size-major; -} - -.svgLayer { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - overflow: visible; - z-index: 10; - -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/Chat.module.scss b/temporary/(canvas_js)/assets/Chat.module.scss deleted file mode 100644 index 54d495d3..00000000 --- a/temporary/(canvas_js)/assets/Chat.module.scss +++ /dev/null @@ -1,179 +0,0 @@ -/* Chat.module.scss */ - -.chatContainer { - display: flex; - flex-direction: column; - height: 100%; /* Make chat container take full height of its parent */ - width: 360px; /* Or a width that suits your layout */ - background-color: #ffffff; - border-radius: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); - border: 1px solid rgba(0, 0, 0, 0.07); - overflow: hidden; /* Important for keeping children within rounded borders */ -} - -.chatHeader { - display: flex; - align-items: center; - gap: 12px; - padding: 12px 16px; - flex-shrink: 0; - border-bottom: 1px solid #f0f0f0; - background-color: #f9f9f9; /* Slightly different background for header */ -} - -.chatHeader h3 { - margin: 0; - font-size: 1rem; /* 16px */ - font-weight: 600; - color: #333; -} - -.backButton { - background: none; - border: none; - font-size: 1.25rem; /* Consistent with SideMenu */ - color: #555; - cursor: pointer; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; -} - -.backButton:hover { - background-color: #f0f0f0; /* Consistent hover effect */ -} - -.messageList { - flex-grow: 1; - padding: 16px; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 12px; - scrollbar-width: thin; - scrollbar-color: #cccccc transparent; -} - -.messageList::-webkit-scrollbar { - width: 6px; -} - -.messageList::-webkit-scrollbar-track { - background: transparent; -} - -.messageList::-webkit-scrollbar-thumb { - background-color: #d1d1d1; - border-radius: 10px; - border: 1px solid transparent; - background-clip: content-box; -} - -.messageList::-webkit-scrollbar-thumb:hover { - background-color: #a8a8a8; -} - -.message { - padding: 10px 14px; - border-radius: 18px; /* More rounded for messages */ - max-width: 80%; - word-wrap: break-word; - font-size: 0.9rem; - line-height: 1.4; -} - -.userMessage { - background-color: #007bff; /* Blue for user messages */ - color: white; - align-self: flex-end; /* Align user messages to the right */ - border-bottom-right-radius: 6px; /* Slightly different rounding for tail effect */ -} - -.botMessage { - background-color: #e9ecef; /* Light grey for bot messages */ - color: #333; - align-self: flex-start; /* Align bot messages to the left */ - border-bottom-left-radius: 6px; /* Slightly different rounding for tail effect */ -} - -.inputArea { - display: flex; - padding: 12px; - border-top: 1px solid #f0f0f0; - background-color: #f9f9f9; /* Match header background */ - gap: 10px; -} - -.textInput { - flex-grow: 1; - padding: 10px 16px; - border-radius: 20px; /* Pill shape input */ - border: 1px solid #e0e0e0; - font-size: 0.95rem; - outline: none; - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -.textInput:focus { - border-color: #007bff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1); -} - -.sendButton { - padding: 10px 18px; - background-color: #007bff; - color: white; - border: none; - border-radius: 20px; /* Pill shape button */ - cursor: pointer; - font-size: 0.95rem; - font-weight: 500; - transition: background-color 0.2s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.sendButton:hover { - background-color: #0056b3; -} - -.sendButton:disabled { - background-color: #a0a0a0; - cursor: not-allowed; -} - -/* Loading dots animation for bot messages */ -.loadingDots span { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: currentColor; /* Use the text color of the bot message */ - margin: 0 2px; - animation: loadingBounce 1.4s infinite ease-in-out both; -} - -.loadingDots span:nth-child(1) { - animation-delay: -0.32s; -} - -.loadingDots span:nth-child(2) { - animation-delay: -0.16s; -} - -@keyframes loadingBounce { - - 0%, - 80%, - 100% { - transform: scale(0); - } - - 40% { - transform: scale(1.0); - } -} diff --git a/temporary/(canvas_js)/assets/Edge.module.scss b/temporary/(canvas_js)/assets/Edge.module.scss deleted file mode 100644 index ac5546e2..00000000 --- a/temporary/(canvas_js)/assets/Edge.module.scss +++ /dev/null @@ -1,22 +0,0 @@ -.edgeGroup { - // cursor: pointer; - pointer-events: auto; - &:hover .edgePath, - &.selected .edgePath { - stroke: #3b82f6; - } -} - -.edgePath { - stroke: #adb5bd; - stroke-width: 2.5; - fill: none; - transition: stroke 0.2s ease; - pointer-events: none; -} - -.edgeHitbox { - fill: none; - stroke: transparent; - stroke-width: 20px; -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/ExecutionPanel.module.scss b/temporary/(canvas_js)/assets/ExecutionPanel.module.scss deleted file mode 100644 index e6142e78..00000000 --- a/temporary/(canvas_js)/assets/ExecutionPanel.module.scss +++ /dev/null @@ -1,154 +0,0 @@ -.executionPanel { - position: absolute; - top: 10px; - width: 450px; - left: 10px; - max-height: 40vh; - background-color: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(10px); - border-radius: 12px; - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); - border: 1px solid rgba(0, 0, 0, 0.1); - z-index: 1000; - display: flex; - flex-direction: column; - overflow: hidden; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); - - &.collapsed { - max-height: auto; - height: auto; - width: 200px; - } -} - -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 16px; - border-bottom: 1px solid #e0e0e0; - flex-shrink: 0; - transition: border-bottom 0.3s ease; - - h4 { - margin: 0; - font-weight: 600; - font-size: 0.95rem; - } - - .executionPanel.collapsed & { - border-bottom: none; - } -} - -.titleSection { - display: flex; - align-items: center; - gap: 8px; -} - -.actions { - display: flex; - align-items: center; - gap: 8px; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} - -.actionButton { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - border: none; - border-radius: 6px; - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s ease; - - &:disabled { - opacity: 0.6; - cursor: not-allowed; - } -} - -.toggleButton { - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - border: none; - border-radius: 6px; - background-color: #f8f9fa; - color: #495057; - font-size: 0.85rem; - cursor: pointer; - transition: all 0.2s ease; - min-width: 24px; - height: 24px; - - &:hover { - background-color: #e9ecef; - color: #343a40; - transform: scale(1.05); - } - - svg { - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - } -} - -.runButton { - background-color: #28a745; - color: white; - - &:hover:not(:disabled) { - background-color: #218838; - } -} - -.outputContainer { - flex-grow: 1; - padding: 16px; - overflow-y: auto; - font-family: 'Courier New', Courier, monospace; - font-size: 0.85rem; - background-color: #f8f9fa; - animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); - - pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; - color: #333; - } -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-10px); - max-height: 0; - } - to { - opacity: 1; - transform: translateY(0); - max-height: 500px; - } -} - -/* --- Loader Animation --- */ -.loader { - border: 2px solid #f3f3f3; - border-top: 2px solid #3498db; - border-radius: 50%; - width: 14px; - height: 14px; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/Header.module.scss b/temporary/(canvas_js)/assets/Header.module.scss deleted file mode 100644 index e270e01a..00000000 --- a/temporary/(canvas_js)/assets/Header.module.scss +++ /dev/null @@ -1,190 +0,0 @@ -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 24px; - height: 60px; // 헤더 높이 - background-color: #ffffff; // 헤더 배경색 - border-bottom: 1px solid #e0e0e0; // 구분선 - flex-shrink: 0; // 헤더가 줄어들지 않도록 설정 - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); -} - -.leftSection { - display: flex; - align-items: center; - gap: 24px; -} - -.logoLink { - text-decoration: none; - transition: all 0.2s ease; - border-radius: 8px; - padding: 8px 12px; - - &:hover { - background-color: #f8f9fa; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } - - &:active { - transform: translateY(0); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); - } -} - -.logo { - font-size: 1.5rem; // 24px - font-weight: 700; - color: #212529; - background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - transition: all 0.2s ease; - - .logoLink:hover & { - background: linear-gradient(135deg, #1d4ed8 0%, #6d28d9 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - } -} - -.workflowNameSection { - min-width: 200px; -} - -.displayMode { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 4px 8px; - border-radius: 6px; - transition: background-color 0.2s ease; - min-width: 200px; - - &:hover { - background-color: #f8f9fa; - } -} - -.workflowName { - font-size: 1rem; - font-weight: 600; - color: #495057; - cursor: pointer; - flex-grow: 1; - text-align: left; -} - -.editMode { - display: flex; - align-items: center; - gap: 4px; -} - -.workflowInput { - font-size: 1rem; - font-weight: 600; - color: #495057; - border: 2px solid #3b82f6; - border-radius: 6px; - padding: 6px 10px; - background-color: #ffffff; - outline: none; - min-width: 150px; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); - - &:focus { - border-color: #2563eb; - } -} - -.editButton { - background: none; - border: none; - padding: 4px; - cursor: pointer; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.9rem; - transition: background-color 0.2s ease; - min-width: 24px; - min-height: 24px; - flex-shrink: 0; - - &:hover { - background-color: #e9ecef; - } -} - -.saveButton { - color: #28a745; - - &:hover { - background-color: #d4edda; - } -} - -.cancelButton { - color: #dc3545; - - &:hover { - background-color: #f8d7da; - } -} - -.nav { - ul { - display: flex; - align-items: center; - list-style: none; - margin: 0; - padding: 0; - gap: 8px; // 메뉴 사이 간격 - } - - li button { - background: none; - border: none; - padding: 8px 16px; - font-size: 0.95rem; // 15px - font-weight: 500; - color: #495057; - cursor: pointer; - border-radius: 6px; - transition: background-color 0.2s ease; - - &:hover { - background-color: #f1f3f5; - } - } -} - -.rightSection { - display: flex; - align-items: center; - gap: 16px; -} - -.menuButton { - background: none; - border: none; - font-size: 1.5rem; // 아이콘 크기 - color: #555; - cursor: pointer; - padding: 8px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - - &:hover { - background-color: #f0f0f0; - } -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/MiniCanvas.module.scss b/temporary/(canvas_js)/assets/MiniCanvas.module.scss deleted file mode 100644 index 5cc15762..00000000 --- a/temporary/(canvas_js)/assets/MiniCanvas.module.scss +++ /dev/null @@ -1,108 +0,0 @@ -// MiniCanvas 스타일 -.miniCanvas { - width: 100%; - height: 100%; - position: relative; - overflow: hidden; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 12px; - border: 2px solid #e2e8f0; - user-select: none; - pointer-events: auto; /* 캔버스 내부 이벤트 활성화 */ -} - -.canvasContent { - width: 100%; - height: 100%; - position: relative; - transition: transform 0.1s ease-out; -} - -.edgesSvg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; -} - -.nodesContainer { - position: relative; - z-index: 2; - pointer-events: none; /* 노드 자체는 클릭 불가 */ -} - -.grid { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: - radial-gradient(circle at 1px 1px, rgba(255,255,255,0.15) 1px, transparent 0); - background-size: 20px 20px; - pointer-events: none; -} - -.zoomControls { - position: absolute; - bottom: 10px; - right: 10px; - display: flex; - align-items: center; - background: rgba(255, 255, 255, 0.9); - border-radius: 20px; - padding: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - backdrop-filter: blur(4px); -} - -.zoomButton { - width: 24px; - height: 24px; - border: none; - background: transparent; - cursor: pointer; - font-size: 14px; - font-weight: bold; - color: #374151; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: background-color 0.2s; - - &:hover { - background-color: rgba(0, 0, 0, 0.1); - } -} - -.zoomLevel { - margin: 0 8px; - font-size: 11px; - color: #6b7280; - min-width: 35px; - text-align: center; -} - -// Node preview 스타일 조정 -.miniCanvas :global(.nodeContainer.preview) { - transform: scale(0.8); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - - :global(.nodeHeader) { - font-size: 11px; - } - - :global(.nodeInputs), :global(.nodeOutputs) { - font-size: 10px; - } -} - -// Edge preview 스타일 조정 -.miniCanvas :global(.edge.preview) { - stroke-width: 2; - opacity: 0.8; -} diff --git a/temporary/(canvas_js)/assets/Node.module.scss b/temporary/(canvas_js)/assets/Node.module.scss deleted file mode 100644 index 8599d6e4..00000000 --- a/temporary/(canvas_js)/assets/Node.module.scss +++ /dev/null @@ -1,314 +0,0 @@ -.node { - position: absolute; - min-width: 350px; - background-color: #ffffff; - border-radius: 12px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); - border: 2px solid #95979c; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - user-select: none; - -webkit-user-select: none; - z-index: 20; - - &.selected { - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4), 0 8px 24px rgba(0, 0, 0, 0.2); - z-index: 30; - } -} - -.header { - padding: 12px 16px; - font-weight: 600; - font-size: 1rem; // 16px - color: #111827; - border-top-left-radius: 12px; - border-top-right-radius: 12px; - border-bottom: 1px solid #f3f4f6; // 매우 옅은 구분선 - cursor: grab; // [추가] 헤더에서만 드래그 가능하도록 커서 변경 - display: flex; - align-items: center; - gap: 8px; -} - -.functionId { - font-size: 0.75rem; - color: #6b7280; - font-weight: normal; -} - -// --- Node Name Editing Styles --- -.nodeName { - cursor: pointer; - user-select: none; - transition: background-color 0.2s ease; - padding: 2px 4px; - border-radius: 4px; - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:hover { - background-color: rgba(59, 130, 246, 0.1); - } -} - -.nameInput { - background: #fff; - border: 2px solid #3b82f6; - border-radius: 4px; - padding: 2px 6px; - font-size: 1rem; - font-weight: 600; - color: #111827; - outline: none; - min-width: 100px; - max-width: 200px; - font-family: inherit; - - &:focus { - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); - } -} - -.body { - padding: 12px 0; -} - -// --- IO 섹션 스타일 --- -.ioContainer { - display: flex; - justify-content: space-between; - padding: 0 16px; -} - -.column { - display: flex; - flex-direction: column; - gap: 14px; - flex-basis: 48%; // [수정] 너비 미세 조정 - - // [추가] Output만 있을 때 컬럼 너비를 100%로 - &.fullWidth { - flex-basis: 100%; - } -} - -.outputColumn { - align-items: flex-end; - - .portRow { - justify-content: flex-end; - } -} - -.sectionHeader { - font-size: 0.8rem; // 11.2px - font-weight: 700; - color: #838a94; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 4px; -} - -.portRow { - display: flex; - align-items: center; - gap: 12px; // [수정] 간격 조정 -} - -.portLabel { - flex: 1; - font-size: 0.875rem; - color: #4b5563; - - &.required::after { - content: " *"; - color: #ef4444; - font-weight: bold; - } -} - - -.port { - position: relative; - z-index: 5; - - padding: 3px 12px; - border-radius: 6px; - min-width: 50px; - - // 텍스트 스타일 - font-size: 0.8rem; - font-weight: 600; - text-align: center; - color: #374151; - - // 테두리 및 기본 색상 - background-color: #fff; - border: 2px solid #adb5bd; - transition: all 0.2s ease; - cursor: crosshair; - - // 타입별 색상 (테두리 색상으로 구분) - &.type-STR { border-color: #5D9CF4; } - &.type-INT { border-color: #7AD37A; } - &.type-FLOAT { border-color: #F7C966; } - &.type-ANY { border-color: #9C5BF4; } // 보라색 계열로 ANY 타입 표시 - - // 다중 연결 포트 스타일 - &.multi { - background-color: #dcf5f7; - } - - // 상호작용 스타일 - &:hover { - transform: scale(1); - filter: brightness(0.95); - } - - &.snapping { - // border-width: 3px; - transform: scale(1.05); - } - - &.invalid-snap { - border-color: #e53e3e !important; - background-color: #fed7d7; - cursor: not-allowed; - } -} - -// .port.multi { -// border-radius: 4px; -// background-color: #d1d5db; -// } - -.divider { - height: 2px; - background-color: #a8abb388; - margin: 16px 0; -} - -.paramSection { - padding: 0 16px; -} - -.param { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.875rem; - margin-top: 10px; - margin-bottom: 6px; - - &:last-child { - margin-bottom: 0; - } -} - -.paramKey { - color: #6b7280; - margin-right: 8px; - - &.required::after { - content: " *"; - color: #ef4444; - font-weight: bold; - } -} - -.paramValue { - color: #111827; - font-weight: 500; - background-color: #f3f4f6; - padding: 4px 8px; - border-radius: 6px; - font-size: 0.8rem; -} - -// --- [새로운 스타일] 파라미터 입력 필드 --- -.paramInput { - flex-grow: 1; - max-width: 60%; - background-color: #f3f4f6; - border: 1px solid transparent; - border-radius: 6px; - padding: 4px 8px; - font-size: 0.8rem; - font-weight: 500; - color: #111827; - outline: none; - transition: border-color 0.2s, box-shadow 0.2s; - text-align: right; - - &:focus { - background-color: #fff; - border-color: #3b82f6; - box-shadow: 0 0 0 1px #3b82f6; - } -} - -// --- [새로운 스타일] 파라미터 선택 필드 (옵션이 있는 경우) --- -.paramSelect { - flex-grow: 1; - max-width: 60%; - background-color: #f3f4f6; - border: 1px solid transparent; - border-radius: 6px; - padding: 4px 8px; - font-size: 0.8rem; - font-weight: 500; - color: #111827; - outline: none; - transition: border-color 0.2s, box-shadow 0.2s; - text-align: right; - cursor: pointer; - position: relative; // z-index 문제 방지 - z-index: 1; // 기본 z-index 설정 - - &:focus { - background-color: #fff; - border-color: #3b82f6; - box-shadow: 0 0 0 1px #3b82f6; - z-index: 1000; // focus 시 최상위로 - } - - &:hover { - background-color: #e5e7eb; - } -} - -// --- Advanced Parameters Styles --- -.advancedParams { - margin-top: 8px; - border-top: 1px solid #e5e7eb; - padding-top: 8px; -} - -.advancedHeader { - display: flex; - align-items: center; - justify-content: center; - padding: 6px 12px; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 6px; - cursor: pointer; - font-size: 0.75rem; - font-weight: 500; - color: #64748b; - transition: all 0.2s ease; - margin-bottom: 8px; - - &:hover { - background-color: #f1f5f9; - border-color: #cbd5e1; - color: #475569; - } - - span { - user-select: none; - } -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/NodeList.module.scss b/temporary/(canvas_js)/assets/NodeList.module.scss deleted file mode 100644 index 606e069f..00000000 --- a/temporary/(canvas_js)/assets/NodeList.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -.nodeList { - width: 100%; - border-bottom: 1px solid #e0e0e0; -} - -.header { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - background-color: #fff; - border: none; - cursor: pointer; - font-size: 1rem; - font-weight: 500; - text-align: left; - - &:hover { - background-color: #f8f9fa; - } -} - -.icon { - transition: transform 0.2s ease-in-out; -} - -.content { - padding: 8px 16px 16px; - background-color: #f8f9fa; - // 나중에 노드 아이템들이 들어갈 공간 -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/PlateeRAG.module.scss b/temporary/(canvas_js)/assets/PlateeRAG.module.scss deleted file mode 100644 index 2562cf4c..00000000 --- a/temporary/(canvas_js)/assets/PlateeRAG.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -.pageContainer { - display: flex; - flex-direction: column; - width: 100vw; - height: 100vh; - overflow: hidden; - background-color: #f8f9fa; -} - -.mainContent { - flex-grow: 1; - position: relative; - display: flex; -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/SideMenu.module.scss b/temporary/(canvas_js)/assets/SideMenu.module.scss deleted file mode 100644 index fd998e94..00000000 --- a/temporary/(canvas_js)/assets/SideMenu.module.scss +++ /dev/null @@ -1,285 +0,0 @@ -@keyframes pop-in { - from { - opacity: 0; - transform: scale(0.95) translateY(-5px); - } - - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -.sideMenuContainer { - position: absolute; - top: 10px; - right: 10px; - min-width: 320px; // 최소 너비 설정 - width: auto; // 동적 너비 - max-width: 600px; // 최대 너비 제한 - height: auto; // 기본 메뉴 높이 - background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%); - border-radius: 14px; // 둥근 테두리 증가 - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06); // 더 깊은 그림자 - border: 1px solid rgba(0, 0, 0, 0.05); - z-index: 1000; // 다른 요소들 위에 보이도록 z-index 설정 - overflow: hidden; // 내부 콘텐츠가 테두리를 넘지 않도록 함 - - animation: pop-in 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; - transform-origin: top right; // 오른쪽 위를 기준으로 애니메이션이 발생 - transition: width 0.3s ease-in-out, height 0.3s ease-in-out; - - display: flex; - flex-direction: column; - - // AddNodePanel 뷰일 때 크기 확장 - &[data-view="addNodes"] { - min-width: 400px; // 확장 시 최소 너비 - width: auto; // 동적 너비 - max-width: 600px; // 최대 너비 제한 - height: 85vh; - max-height: 700px; - } - - // WorkflowPanel 뷰일 때 크기 확장 - &[data-view="workflow"] { - min-width: 400px; // 확장 시 최소 너비 - width: auto; // 동적 너비 - max-width: 600px; // 최대 너비 제한 - height: 85vh; - max-height: 700px; - } - - // TemplatePanel 뷰일 때 크기 확장 - &[data-view="template"] { - min-width: 400px; // WorkflowPanel과 동일한 최소 너비 - width: auto; // 동적 너비 - max-width: 600px; // 최대 너비 제한 - height: 85vh; - max-height: 700px; - } -} - -// --- 패널 공통 헤더 --- -.header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 20px; - flex-shrink: 0; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); - - h3 { - margin: 0; - font-size: 1.05rem; // 16px → 17px로 약간 증가 - font-weight: 600; - color: #374151; - letter-spacing: 0.2px; - } -} - -.backButton { - background: none; - border: none; - font-size: 1.25rem; - color: #6b7280; - cursor: pointer; - padding: 6px; - display: flex; - border-radius: 8px; - transition: all 0.2s ease; - - &:hover { - background-color: rgba(59, 130, 246, 0.08); - color: #3b82f6; - transform: translateX(-1px); - } -} - -.refreshButton { - background: none; - border: none; - font-size: 1.1rem; - color: #6b7280; - cursor: pointer; - padding: 6px; - display: flex; - border-radius: 8px; - margin-left: auto; - transition: all 0.2s ease; - - &:hover { - background-color: rgba(34, 197, 94, 0.08); - color: #22c55e; - transform: rotate(90deg); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &.loading { - animation: spin 1s linear infinite; - } -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -// --- 메인 메뉴 스타일 --- -.mainMenu { - padding: 12px 8px; // 좌우 패딩을 줄임 (16px → 8px) - display: flex; - flex-direction: column; - gap: 4px; -} - -.menuItem { - display: flex; - align-items: center; - gap: 14px; - width: 100%; - padding: 12px 16px; - font-size: 0.95rem; - font-weight: 500; - text-align: left; - background-color: transparent; - border: none; - border-radius: 10px; - cursor: pointer; - color: #374151; - transition: all 0.2s ease; - position: relative; - margin: 0; // 마진 제거 - box-sizing: border-box; // 박스 사이징 명시 - - svg { - font-size: 1.2rem; - color: #6b7280; - transition: all 0.2s ease; - } - - &:hover { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(147, 51, 234, 0.06) 100%); - color: #1e40af; - transform: translateX(1px); // 이동 거리를 줄임 (2px → 1px) - - svg { - color: #3b82f6; - transform: scale(1.05); - } - } - - &:active { - transform: translateX(0px) scale(0.98); // 액티브 시 이동 제거 - } -} - -// --- AddNodePanel 스타일 --- -.searchBar { - position: relative; - padding: 16px; - flex-shrink: 0; - - input { - width: 100%; - padding: 10px 16px 10px 40px; - border-radius: 8px; - border: 1px solid #e0e0e0; - font-size: 0.95rem; - outline: none; - transition: all 0.2s ease; - - &:focus { - border-color: #007bff; - box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15); - } - } -} - -.searchIcon, -.clearIcon { - position: absolute; - top: 50%; - transform: translateY(-50%); - color: #aaa; -} - -.searchIcon { - left: 30px; -} - -.clearIcon { - right: 30px; - cursor: pointer; -} - -.tabs { - display: flex; - padding: 0 16px; - border-bottom: 1px solid #e0e0e0; - flex-shrink: 0; - - overflow-x: auto; - - &::-webkit-scrollbar { - display: none; - } - - -ms-overflow-style: none; - scrollbar-width: none; -} - -.tab { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - font-weight: 500; - color: #555; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - flex-shrink: 0; - border: none; - background-color: transparent; - cursor: pointer; - - &.active { - color: #007bff; - border-bottom-color: #007bff; - } -} - -.nodeList { - flex-grow: 1; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: #cccccc transparent; - - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background-color: #d1d1d1; - border-radius: 10px; - border: 2px solid transparent; - background-clip: content-box; - } - - &::-webkit-scrollbar-thumb:hover { - background-color: #a8a8a8; - } -} \ No newline at end of file diff --git a/temporary/(canvas_js)/assets/TemplatePreview.module.scss b/temporary/(canvas_js)/assets/TemplatePreview.module.scss deleted file mode 100644 index 2a07a6f6..00000000 --- a/temporary/(canvas_js)/assets/TemplatePreview.module.scss +++ /dev/null @@ -1,213 +0,0 @@ -// TemplatePreview 스타일 -.overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; - animation: fadeIn 0.2s ease-out; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.previewContainer { - width: 90vw; - max-width: 1000px; - height: 80vh; - max-height: 700px; - background: #ffffff; - border-radius: 16px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - display: flex; - flex-direction: column; - overflow: hidden; - animation: slideUp 0.3s ease-out; -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 20px 24px; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); -} - -.titleSection { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; - - h3 { - margin: 0; - font-size: 1.25rem; - font-weight: 700; - color: #374151; - } -} - -.tagsContainer { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.category { - font-size: 0.75rem; - font-weight: 600; - color: #3b82f6; - background-color: rgba(59, 130, 246, 0.1); - padding: 4px 8px; - border-radius: 6px; - border: 1px solid rgba(59, 130, 246, 0.2); -} - -.actions { - display: flex; - align-items: center; - gap: 12px; -} - -.useButton { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); - color: white; - border: none; - border-radius: 8px; - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); - } - - &:active { - transform: translateY(0); - } -} - -.closeButton { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: none; - border: 1px solid #e2e8f0; - border-radius: 8px; - color: #6b7280; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background-color: #fee2e2; - border-color: #fca5a5; - color: #dc2626; - } -} - -.previewContent { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.canvasContainer { - flex: 1; - background: #f8fafc; - border-bottom: 1px solid #e2e8f0; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - position: relative; /* MiniCanvas 이벤트를 위해 추가 */ -} - -.previewCanvas { - width: 100%; - height: 100%; - border: 2px solid #e2e8f0; - border-radius: 12px; - background: #ffffff; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); -} - -.edges { - pointer-events: none; -} - -.nodes { - pointer-events: none; -} - -.templateInfo { - padding: 20px 24px; - background: #ffffff; -} - -.description { - margin: 0 0 16px 0; - font-size: 0.95rem; - color: #6b7280; - line-height: 1.5; -} - -.stats { - display: flex; - gap: 24px; -} - -.stat { - display: flex; - align-items: center; - gap: 8px; -} - -.statLabel { - font-size: 0.85rem; - font-weight: 600; - color: #374151; -} - -.statValue { - font-size: 0.85rem; - font-weight: 700; - color: #3b82f6; - background-color: rgba(59, 130, 246, 0.1); - padding: 2px 6px; - border-radius: 4px; -} diff --git a/temporary/(canvas_js)/assets/WorkflowPanel.module.scss b/temporary/(canvas_js)/assets/WorkflowPanel.module.scss deleted file mode 100644 index ebfd5890..00000000 --- a/temporary/(canvas_js)/assets/WorkflowPanel.module.scss +++ /dev/null @@ -1,399 +0,0 @@ -.workflowPanel { - display: flex; - flex-direction: column; - height: 100%; - background-color: #ffffff; -} - -.actionButtons { - display: flex; - flex-direction: column; - gap: 4px; - padding: 8px; - flex-shrink: 0; -} - -.actionButton { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - padding: 10px; - font-size: 0.9rem; - font-weight: 500; - text-align: left; - background-color: transparent; - border: none; - border-radius: 6px; - cursor: pointer; - color: #333; - transition: background-color 0.2s ease; - - svg { - font-size: 1.1rem; - color: #555; - } - - &:hover { - background-color: #f5f5f5; - } - - span { - flex-grow: 1; - text-align: left; - } -} - -.workflowList { - flex: 1; - overflow-y: auto; - padding: 16px; - scrollbar-width: thin; - scrollbar-color: #cccccc transparent; - - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background-color: #d1d1d1; - border-radius: 10px; - border: 2px solid transparent; - background-clip: content-box; - } - - &::-webkit-scrollbar-thumb:hover { - background-color: #a8a8a8; - } -} - -.listHeader { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; - padding: 10px 14px; - background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); - border: 1px solid #cbd5e1; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - - h3 { - font-size: 0.95rem; - font-weight: 600; - color: #374151; - margin: 0; - letter-spacing: 0.1px; - } - - .count { - font-size: 0.8rem; - font-weight: 500; - color: #6b7280; - background-color: rgba(59, 130, 246, 0.08); - padding: 3px 8px; - border-radius: 12px; - border: 1px solid rgba(59, 130, 246, 0.15); - } -} - -.loadingState, .errorState, .emptyState { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 30px 16px; - text-align: center; - color: #666; - - .spinIcon { - font-size: 1.5rem; - margin-bottom: 8px; - animation: spin 1s linear infinite; - } - - span { - font-size: 0.9rem; - font-weight: 500; - margin-bottom: 4px; - } - - p { - font-size: 0.8rem; - color: #888; - margin: 0; - } -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -.errorState { - .retryButton { - margin-top: 8px; - padding: 6px 12px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.8rem; - font-weight: 500; - transition: background-color 0.2s ease; - - &:hover { - background-color: #0056b3; - } - } -} - -.workflowItems { - display: flex; - flex-direction: column; - gap: 6px; -} - -.workflowItem { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px; - background-color: #ffffff; - border: 1px solid #e0e0e0; - border-radius: 6px; - transition: all 0.2s ease; - - &:hover { - border-color: #ccc; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - } -} - -.workflowInfo { - flex: 1; - min-width: 0; -} - -.workflowName { - font-size: 0.9rem; - font-weight: 600; - color: #333; - word-break: break-word; -} - -.workflowMeta { - display: flex; - align-items: center; - gap: 8px; -} - -.filename { - font-size: 0.75rem; - color: #666; - font-family: monospace; - background-color: #f0f0f0; - padding: 1px 4px; - border-radius: 3px; -} - -.workflowActions { - display: flex; - gap: 6px; - align-items: center; -} - -.loadButton { - padding: 6px 12px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.8rem; - font-weight: 500; - transition: background-color 0.2s ease; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - background-color: #0056b3; - } - - &:active { - background-color: #004085; - } -} - -.deleteButton { - padding: 6px; - background-color: #dc3545; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.8rem; - transition: background-color 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - min-width: 28px; - height: 28px; - - &:hover { - background-color: #c82333; - } - - &:active { - background-color: #bd2130; - } -} - -// --- TemplatePanel 스타일 --- -.templateList { - display: flex; - flex-direction: column; - gap: 12px; -} - -.templateItem { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; // templateHeader와 templateActions 사이에 간격 추가 - padding: 16px; - background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); - border: 1px solid #e2e8f0; - border-radius: 12px; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - - &:hover { - border-color: #cbd5e1; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - transform: translateY(-1px); - } -} - -.templateHeader { - display: flex; - align-items: flex-start; - gap: 12px; - flex: 1; -} - -.templateIcon { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); - border-radius: 10px; - color: white; - font-size: 1.2rem; - flex-shrink: 0; -} - -.templateInfo { - flex: 1; - min-width: 0; -} - -.templateName { - font-size: 0.95rem; - font-weight: 600; - color: #374151; - margin: 0 0 4px 0; - line-height: 1.2; -} - -.templateDescription { - font-size: 0.8rem; - color: #6b7280; - margin: 0 0 8px 0; - line-height: 1.3; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.templateMeta { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; -} - -.templateTags { - display: flex; - gap: 4px; - align-items: center; - flex-wrap: nowrap; // 한 줄에 유지 - overflow: hidden; // 넘치는 부분 숨김 -} - -.templateCategory { - font-size: 0.7rem; - font-weight: 500; - color: #3b82f6; - background-color: rgba(59, 130, 246, 0.08); - padding: 2px 6px; - border-radius: 6px; - border: 1px solid rgba(59, 130, 246, 0.15); - white-space: nowrap; // 텍스트 줄바꿈 방지 - flex-shrink: 0; // 태그가 축소되지 않도록 -} - -.templateNodes { - font-size: 0.7rem; - color: #6b7280; - background-color: #f3f4f6; - padding: 2px 6px; - border-radius: 6px; - white-space: nowrap; - flex-shrink: 0; -} - -.templateActions { - display: flex; - gap: 6px; - flex-shrink: 0; -} - -.templateActionButton { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - background: none; - border: 1px solid #e2e8f0; - border-radius: 8px; - cursor: pointer; - color: #6b7280; - font-size: 0.9rem; - transition: all 0.2s ease; - - &:hover { - background-color: #f3f4f6; - border-color: #cbd5e1; - color: #374151; - transform: scale(1.05); - } - - &:active { - transform: scale(0.95); - } -} diff --git a/temporary/(canvas_js)/components/Canvas.jsx b/temporary/(canvas_js)/components/Canvas.jsx deleted file mode 100644 index 85f9ade1..00000000 --- a/temporary/(canvas_js)/components/Canvas.jsx +++ /dev/null @@ -1,790 +0,0 @@ -"use client"; - -import React, { useRef, useEffect, useState, forwardRef, useImperativeHandle, useCallback, memo, useLayoutEffect } from 'react'; -import styles from '@/app/canvas/assets/Canvas.module.scss'; -import Node from '@/app/canvas/components/Node'; -import Edge from '@/app/canvas/components/Edge'; -import { devLog } from '@/app/utils/logger'; - -const MIN_SCALE = 0.6; -const MAX_SCALE = 20; -const ZOOM_SENSITIVITY = 0.05; -const SNAP_DISTANCE = 40; - -const areTypesCompatible = (sourceType, targetType) => { - if (!sourceType || !targetType) return true; - if (sourceType === targetType) return true; - if (targetType === 'ANY') return true; - if (sourceType === 'INT' && targetType === 'FLOAT') return true; - return false; -}; - -const validateRequiredInputs = (nodes, edges) => { - for (const node of nodes) { - if (!node.data.inputs || node.data.inputs.length === 0) continue; - for (const input of node.data.inputs) { - if (input.required) { - const hasConnection = edges.some(edge => - edge.target.nodeId === node.id && - edge.target.portId === input.id - ); - - if (!hasConnection) { - return { - isValid: false, - nodeId: node.id, - nodeName: node.data.nodeName, - inputName: input.name - }; - } - } - } - } - return { isValid: true }; -}; - -const Canvas = forwardRef(({ onStateChange, ...otherProps }, ref) => { - const contentRef = useRef(null); - const containerRef = useRef(null); - - const [view, setView] = useState({ x: 0, y: 0, scale: 1 }); - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); - const [selectedNodeId, setSelectedNodeId] = useState(null); - const [selectedEdgeId, setSelectedEdgeId] = useState(null); - const [dragState, setDragState] = useState({ type: 'none', startX: 0, startY: 0 }); - const [edgePreview, setEdgePreview] = useState(null); - const [portPositions, setPortPositions] = useState({}); - const [snappedPortKey, setSnappedPortKey] = useState(null); - const [isSnapTargetValid, setIsSnapTargetValid] = useState(true); - const [copiedNode, setCopiedNode] = useState(null); - const [lastDeleted, setLastDeleted] = useState(null); // 삭제 복원을 위한 상태 - - const nodesRef = useRef(nodes); - const edgePreviewRef = useRef(edgePreview); - const portRefs = useRef(new Map()); - const snappedPortKeyRef = useRef(snappedPortKey); - const isSnapTargetValidRef = useRef(isSnapTargetValid); - - useLayoutEffect(() => { - const newPortPositions = {}; - const contentEl = contentRef.current; - if (!contentEl) return; - - const contentRect = contentEl.getBoundingClientRect(); - - portRefs.current.forEach((portEl, key) => { - if (portEl) { - const portRect = portEl.getBoundingClientRect(); - const x = (portRect.left + portRect.width / 2 - contentRect.left) / view.scale; - const y = (portRect.top + portRect.height / 2 - contentRect.top) / view.scale; - newPortPositions[key] = { x, y }; - } - }); - setPortPositions(newPortPositions); - }, [nodes, view.scale]); - - - - useEffect(() => { - if (onStateChange) { - const currentState = { view, nodes, edges }; - if (nodes.length > 0 || edges.length > 0) { - devLog.log('Canvas state changed, calling onStateChange:', { - nodesCount: nodes.length, - edgesCount: edges.length, - view: view - }); - onStateChange(currentState); - } else { - devLog.log('Canvas state is empty, skipping onStateChange to preserve localStorage'); - } - } else { - devLog.warn('onStateChange callback is not provided to Canvas'); - } - }, [nodes, edges, view, onStateChange]); - - const registerPortRef = useCallback((nodeId, portId, portType, el) => { - const key = `${nodeId}__PORTKEYDELIM__${portId}__PORTKEYDELIM__${portType}`; - if (el) { - portRefs.current.set(key, el); - } else { - portRefs.current.delete(key); - } - }, []); - - const getCenteredView = useCallback(() => { - const container = containerRef.current; - const content = contentRef.current; - - if (container && content) { - const containerWidth = container.clientWidth; - const containerHeight = container.clientHeight; - const contentWidth = content.offsetWidth; - const contentHeight = content.offsetHeight; - - // 컨테이너나 콘텐츠 크기가 0이면 기본값 반환 - if (containerWidth <= 0 || containerHeight <= 0) { - devLog.log('Container not ready for centered view calculation, using default'); - return { x: 0, y: 0, scale: 1 }; - } - - const centeredView = { - x: (containerWidth - contentWidth) / 2, - y: (containerHeight - contentHeight) / 2, - scale: 1 - }; - - devLog.log('Calculated centered view:', centeredView, 'container:', { containerWidth, containerHeight }, 'content:', { contentWidth, contentHeight }); - return centeredView; - } - - devLog.log('Container or content not ready for centered view calculation'); - return { x: 0, y: 0, scale: 1 }; - }, []); - - - useImperativeHandle(ref, () => ({ - getCanvasState: () => ({ view, nodes, edges }), - addNode: (nodeData, clientX, clientY) => { - const container = containerRef.current; - if (!container) return; - const rect = container.getBoundingClientRect(); - const worldX = (clientX - rect.left - view.x) / view.scale; - const worldY = (clientY - rect.top - view.y) / view.scale; - - const newNode = { - id: `${nodeData.id}-${Date.now()}`, - data: nodeData, - position: { x: worldX, y: worldY }, - }; - setNodes(prev => [...prev, newNode]); - }, - loadCanvasState: (state) => { - if (state.nodes) setNodes(state.nodes); - if (state.edges) setEdges(state.edges); - if (state.view) setView(state.view); - }, - loadWorkflowState: (state) => { - devLog.log('Canvas loadWorkflowState called with:', { - hasNodes: !!state.nodes, - nodesCount: state.nodes?.length || 0, - hasEdges: !!state.edges, - edgesCount: state.edges?.length || 0, - hasView: !!state.view, - view: state.view - }); - - if (state.nodes) { - devLog.log('Setting nodes:', state.nodes.length); - setNodes(state.nodes); - } - if (state.edges) { - devLog.log('Setting edges:', state.edges.length); - setEdges(state.edges); - } - if (state.view) { - devLog.log('Setting view:', state.view); - setView(state.view); - } - - devLog.log('Canvas loadWorkflowState completed'); - }, - getCenteredView, - clearSelectedNode: () => { - setSelectedNodeId(null); - setSelectedEdgeId(null); - }, - validateAndPrepareExecution: () => { - const validationResult = validateRequiredInputs(nodes, edges); - if (!validationResult.isValid) { - setSelectedNodeId(validationResult.nodeId); - setSelectedEdgeId(null); - return { - error: `Required input "${validationResult.inputName}" is missing in node "${validationResult.nodeName}"`, - nodeId: validationResult.nodeId - }; - } - setSelectedNodeId(null); - setSelectedEdgeId(null); - return { success: true }; - } - })); - - const calculateDistance = (pos1, pos2) => { - if (!pos1 || !pos2) return Infinity; - return Math.sqrt(Math.pow(pos1.x - pos2.x, 2) + Math.pow(pos1.y - pos2.y, 2)); - }; - - const copySelectedNode = () => { - if (selectedNodeId) { - const nodeToCopy = nodes.find(node => node.id === selectedNodeId); - if (nodeToCopy) { - setCopiedNode(nodeToCopy); - devLog.log('Node copied:', nodeToCopy.data.nodeName); - } - } - }; - - const pasteNode = () => { - if (copiedNode) { - const newNode = { - ...copiedNode, - id: `${copiedNode.data.id}-${Date.now()}`, - position: { - x: copiedNode.position.x + 50, - y: copiedNode.position.y + 50 - } - }; - - setNodes(prev => [...prev, newNode]); - setSelectedNodeId(newNode.id); - devLog.log('Node pasted:', newNode.data.nodeName); - } - }; - - - const handleParameterChange = useCallback((nodeId, paramId, value) => { - devLog.log('=== Canvas Parameter Change ==='); - devLog.log('Received:', { nodeId, paramId, value }); - - setNodes(prevNodes => { - devLog.log('Previous nodes count:', prevNodes.length); - - const targetNodeIndex = prevNodes.findIndex(node => node.id === nodeId); - if (targetNodeIndex === -1) { - devLog.warn('Target node not found:', nodeId); - return prevNodes; - } - - const targetNode = prevNodes[targetNodeIndex]; - devLog.log('Found target node:', targetNode.data.nodeName); - - if (!targetNode.data.parameters || !Array.isArray(targetNode.data.parameters)) { - devLog.warn('No parameters found in target node'); - return prevNodes; - } - - const targetParamIndex = targetNode.data.parameters.findIndex(param => param.id === paramId); - if (targetParamIndex === -1) { - devLog.warn('Target parameter not found:', paramId); - return prevNodes; - } - - const targetParam = targetNode.data.parameters[targetParamIndex]; - const newValue = typeof targetParam.value === 'number' ? Number(value) : value; - - if (targetParam.value === newValue) { - devLog.log('Parameter value unchanged, skipping update'); - return prevNodes; - } - - devLog.log('Updating parameter:', { - paramName: targetParam.name, - paramId, - oldValue: targetParam.value, - newValue - }); - - const newNodes = [...prevNodes]; - newNodes[targetNodeIndex] = { - ...targetNode, - data: { - ...targetNode.data, - parameters: [ - ...targetNode.data.parameters.slice(0, targetParamIndex), - { ...targetParam, value: newValue }, - ...targetNode.data.parameters.slice(targetParamIndex + 1) - ] - } - }; - - devLog.log('Parameter update completed successfully'); - devLog.log('=== End Canvas Parameter Change ==='); - return newNodes; - }); - }, []); - - const handleNodeNameChange = useCallback((nodeId, newName) => { - devLog.log('=== Canvas Node Name Change ==='); - devLog.log('Received:', { nodeId, newName }); - - setNodes(prevNodes => { - const targetNodeIndex = prevNodes.findIndex(node => node.id === nodeId); - if (targetNodeIndex === -1) { - devLog.warn('Target node not found:', nodeId); - return prevNodes; - } - - const targetNode = prevNodes[targetNodeIndex]; - if (targetNode.data.nodeName === newName) { - devLog.log('Node name unchanged, skipping update'); - return prevNodes; - } - - devLog.log('Updating node name:', { - nodeId, - oldName: targetNode.data.nodeName, - newName - }); - - const newNodes = [ - ...prevNodes.slice(0, targetNodeIndex), - { - ...targetNode, - data: { - ...targetNode.data, - nodeName: newName - } - }, - ...prevNodes.slice(targetNodeIndex + 1) - ]; - - devLog.log('Node name update completed successfully'); - devLog.log('=== End Canvas Node Name Change ==='); - return newNodes; - }); - }, []); - - const findPortData = (nodeId, portId, portType) => { - const node = nodes.find(n => n.id === nodeId); - if (!node) return null; - const portList = portType === 'input' ? node.data.inputs : node.data.outputs; - return portList?.find(p => p.id === portId) || null; - }; - - const handleCanvasMouseDown = (e) => { - const target = e.target; - const isParameterInput = target.matches('input, select, option') || - target.classList.contains('paramInput') || - target.classList.contains('paramSelect') || - target.closest('.param') || - target.closest('[class*="param"]'); - - if (isParameterInput) { - devLog.log('Canvas mousedown blocked for parameter input:', target); - return; - } - - if (e.button !== 0) return; - setSelectedNodeId(null); - setSelectedEdgeId(null); - setDragState({ type: 'canvas', startX: e.clientX - view.x, startY: e.clientY - view.y }); - }; - - const handleMouseMove = (e) => { - if (dragState.type === 'none') return; - - if (dragState.type === 'canvas') { - setView(prev => ({ ...prev, x: e.clientX - dragState.startX, y: e.clientY - dragState.startY })); - } else if (dragState.type === 'node') { - const newX = (e.clientX / view.scale) - dragState.offsetX; - const newY = (e.clientY / view.scale) - dragState.offsetY; - setNodes(prevNodes => - prevNodes.map(node => - node.id === dragState.nodeId ? { ...node, position: { x: newX, y: newY } } : node - ) - ); - } else if (dragState.type === 'edge') { - const container = containerRef.current; - if (!container || !edgePreviewRef.current) return; - - const rect = container.getBoundingClientRect(); - const mousePos = { - x: (e.clientX - rect.left - view.x) / view.scale, - y: (e.clientY - rect.top - view.y) / view.scale, - }; - - setEdgePreview(prev => ({ ...prev, targetPos: mousePos })); - - let closestPortKey = null; - let minSnapDistance = SNAP_DISTANCE; - const edgeSource = edgePreviewRef.current.source; - - if (edgeSource) { - portRefs.current.forEach((portEl, key) => { - const parts = key.split('__PORTKEYDELIM__'); - if (parts.length !== 3) return; - const [targetNodeId, targetPortId, targetPortType] = parts; - if (targetPortType === 'input' && edgeSource.nodeId !== targetNodeId) { - const targetPortWorldPos = portPositions[key]; - if (targetPortWorldPos) { - const distance = calculateDistance(mousePos, targetPortWorldPos); - if (distance < minSnapDistance) { - minSnapDistance = distance; - closestPortKey = key; - } - } - } - }); - - if (closestPortKey) { - const parts = closestPortKey.split('__PORTKEYDELIM__'); - const targetPort = findPortData(parts[0], parts[1], parts[2]); - const isValid = areTypesCompatible(edgeSource.type, targetPort.type); - setIsSnapTargetValid(isValid); - } else { - setIsSnapTargetValid(true); - } - } - setSnappedPortKey(closestPortKey); - } - }; - - const handleKeyDown = useCallback((e) => { - // 입력 필드에서의 키 이벤트는 무시 - if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') { - return; - } - - const isCtrlOrCmd = e.ctrlKey || e.metaKey; - - if (isCtrlOrCmd && e.key === 'c') { - e.preventDefault(); - copySelectedNode(); - } - else if (isCtrlOrCmd && e.key === 'v') { - e.preventDefault(); - pasteNode(); - } - else if (isCtrlOrCmd && e.key === 'z') { - e.preventDefault(); - if (lastDeleted) { - setNodes(prev => [...prev, lastDeleted.node]); - setEdges(prev => [...prev, ...lastDeleted.edges]); - setLastDeleted(null); - devLog.log('Node restored:', lastDeleted.node.data.nodeName); - } - } - else if (e.key === 'Delete' && selectedNodeId) { - e.preventDefault(); - const nodeToDelete = nodes.find(node => node.id === selectedNodeId); - if (nodeToDelete) { - const connectedEdges = edges.filter(edge => edge.source.nodeId === selectedNodeId || edge.target.nodeId === selectedNodeId); - setLastDeleted({ node: nodeToDelete, edges: connectedEdges }); - - setNodes(prev => prev.filter(node => node.id !== selectedNodeId)); - setEdges(prev => prev.filter(edge => edge.source.nodeId !== selectedNodeId && edge.target.nodeId !== selectedNodeId)); - setSelectedNodeId(null); - devLog.log('Node deleted and saved for undo:', nodeToDelete.data.nodeName); - } - } - }, [selectedNodeId, copiedNode, nodes, edges, lastDeleted]); - - const handleNodeMouseDown = useCallback((e, nodeId) => { - if (e.button !== 0) return; - setSelectedNodeId(nodeId); - setSelectedEdgeId(null); - const node = nodes.find(n => n.id === nodeId); - if (node) { - setDragState({ - type: 'node', - nodeId, - offsetX: (e.clientX / view.scale) - node.position.x, - offsetY: (e.clientY / view.scale) - node.position.y, - }); - } - }, [nodes, view.scale]); - - const handleEdgeClick = useCallback((edgeId) => { - setSelectedEdgeId(edgeId); - setSelectedNodeId(null); - }, []); - - const handlePortMouseUp = useCallback(({ nodeId, portId, portType, type }) => { - const currentEdgePreview = edgePreviewRef.current; - if (!currentEdgePreview) return; - - if (currentEdgePreview && !areTypesCompatible(currentEdgePreview.source.type, type)) { - setSnappedPortKey(null); - setIsSnapTargetValid(true); - setEdgePreview(null); - return; - } - - if (!currentEdgePreview || currentEdgePreview.source.portType === portType) { - setEdgePreview(null); - return; - }; - - const sourceNodeId = currentEdgePreview.source.nodeId; - if (sourceNodeId === nodeId) { - setEdgePreview(null); - return; - } - - const newEdgeSignature = `${currentEdgePreview.source.nodeId}:${currentEdgePreview.source.portId}-${nodeId}:${portId}`; - const isDuplicate = edges.some(edge => - `${edge.source.nodeId}:${edge.source.portId}-${edge.target.nodeId}:${edge.target.portId}` === newEdgeSignature - ); - - if (isDuplicate) { - setEdgePreview(null); - return; - } - - let newEdges = [...edges]; - if (portType === 'input') { - const targetPort = findPortData(nodeId, portId, 'input'); - if (targetPort && !targetPort.multi) { - newEdges = newEdges.filter(edge => !(edge.target.nodeId === nodeId && edge.target.portId === portId)); - } - } - - const newEdge = { - id: `edge-${newEdgeSignature}-${Date.now()}`, - source: currentEdgePreview.source, - target: { nodeId, portId, portType } - }; - setEdges([...newEdges, newEdge]); - setEdgePreview(null); - setSnappedPortKey(null); - setIsSnapTargetValid(true); - }, [edges, nodes]); - - const handleMouseUp = useCallback(() => { - setDragState({ type: 'none' }); - - if (dragState.type === 'edge') { - const snappedKey = snappedPortKeyRef.current; - if (snappedKey) { - const source = edgePreviewRef.current.source; - const parts = snappedKey.split('__PORTKEYDELIM__'); - const [targetNodeId, targetPortId, targetPortType] = parts; - - const targetPortData = findPortData(targetNodeId, targetPortId, targetPortType); - - if (targetPortData && areTypesCompatible(source.type, targetPortData.type)) { - handlePortMouseUp({ - nodeId: targetNodeId, - portId: targetPortId, - portType: targetPortType, - type: targetPortData.type - }); - } - } - } - - setEdgePreview(null); - setSnappedPortKey(null); - setIsSnapTargetValid(true); - - }, [dragState.type, handlePortMouseUp]); - - const handlePortMouseDown = useCallback(({ nodeId, portId, portType, isMulti, type }) => { - if (portType === 'input') { - let existingEdge - if (!isMulti) { - existingEdge = edges.find(e => e.target.nodeId === nodeId && e.target.portId === portId); - } else{ - existingEdge = edges.findLast(e => e.target.nodeId === nodeId && e.target.portId === portId); - } - if (existingEdge) { - setDragState({ type: 'edge' }); - devLog.log(existingEdge) - const sourcePosKey = `${existingEdge.source.nodeId}__PORTKEYDELIM__${existingEdge.source.portId}__PORTKEYDELIM__${existingEdge.source.portType}`; - const sourcePos = portPositions[sourcePosKey]; - const targetPosKey = `${existingEdge.target.nodeId}__PORTKEYDELIM__${existingEdge.target.portId}__PORTKEYDELIM__${existingEdge.target.portType}`; - const targetPos = portPositions[targetPosKey]; - - const sourcePortData = findPortData(existingEdge.source.nodeId, existingEdge.source.portId, existingEdge.source.portType); - - if (sourcePos && sourcePortData) { - setEdgePreview({ - source: { ...existingEdge.source, type: sourcePortData.type }, - startPos: sourcePos, - targetPos: targetPos - }); - } - - setEdges(prevEdges => prevEdges.filter(e => e.id !== existingEdge.id)); - return; - } - - } - - if (portType === 'output') { - setDragState({ type: 'edge' }); - const startPosKey = `${nodeId}__PORTKEYDELIM__${portId}__PORTKEYDELIM__${portType}`; - const startPos = portPositions[startPosKey]; - if (startPos) { - setEdgePreview({ source: { nodeId, portId, portType, type }, startPos, targetPos: startPos }); - } - return; - } - }, [edges, portPositions, nodes]); - - - useEffect(() => { - nodesRef.current = nodes; - edgePreviewRef.current = edgePreview; - snappedPortKeyRef.current = snappedPortKey; - isSnapTargetValidRef.current = isSnapTargetValid; - }, [nodes, edgePreview, snappedPortKey, isSnapTargetValid]); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - const handleWheel = (e) => { - e.preventDefault(); - setView(prevView => { - const delta = e.deltaY > 0 ? -1 : 1; - const newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, prevView.scale + delta * ZOOM_SENSITIVITY * prevView.scale)); - if (newScale === prevView.scale) return prevView; - const rect = container.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - const worldX = (mouseX - prevView.x) / prevView.scale; - const worldY = (mouseY - prevView.y) / prevView.scale; - const newX = mouseX - worldX * newScale; - const newY = mouseY - worldY * newScale; - return { x: newX, y: newY, scale: newScale }; - }); - }; - container.addEventListener('wheel', handleWheel, { passive: false }); - return () => container.removeEventListener('wheel', handleWheel); - }, []); - - useEffect(() => { - const container = containerRef.current; - if (container) { - container.addEventListener('keydown', handleKeyDown); - container.setAttribute('tabindex', '0'); - - return () => { - container.removeEventListener('keydown', handleKeyDown); - }; - } - }, [handleKeyDown]); - - useEffect(() => { - const container = containerRef.current; - const content = contentRef.current; - if (container && content) { - const centeredView = getCenteredView(); - setView(centeredView); - } - }, []); - - useEffect(() => { - devLog.log('Canvas mounted, checking initial state'); - if (onStateChange && (nodes.length > 0 || edges.length > 0)) { - devLog.log('Canvas has content, sending initial state'); - const initialState = { view, nodes, edges }; - onStateChange(initialState); - } else { - devLog.log('Canvas is empty, not sending initial state to avoid overwriting localStorage'); - } - }, []); - - useEffect(() => { - const handleKeyDown = (e) => { - // 입력 필드에서의 키 이벤트는 무시 - if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') { - return; - } - - if (e.key === 'Delete' || e.key === 'Backspace') { - e.preventDefault(); // 페이지 뒤로가기 방지 - if (selectedNodeId) { - setNodes(prev => prev.filter(node => node.id !== selectedNodeId)); - setEdges(prev => prev.filter(edge => edge.source.nodeId !== selectedNodeId && edge.target.nodeId !== selectedNodeId)); - setSelectedNodeId(null); - } else if (selectedEdgeId) { - setEdges(prev => prev.filter(edge => edge.id !== selectedEdgeId)); - setSelectedEdgeId(null); - } - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedNodeId, selectedEdgeId]); - - return ( -
containerRef.current?.focus()} - tabIndex={0} - style={{ outline: 'none' }} - > -
- {nodes.map(node => ( - setSelectedNodeId(null)} - /> - ))} - - - {edges - .filter(edge => edge.id !== selectedEdgeId) - .map(edge => { - const sourceKey = `${edge.source.nodeId}__PORTKEYDELIM__${edge.source.portId}__PORTKEYDELIM__${edge.source.portType}`; - const targetKey = `${edge.target.nodeId}__PORTKEYDELIM__${edge.target.portId}__PORTKEYDELIM__${edge.target.portType}`; - const sourcePos = portPositions[sourceKey]; - const targetPos = portPositions[targetKey]; - return ; - })} - - {edges - .filter(edge => edge.id === selectedEdgeId) - .map(edge => { - const sourceKey = `${edge.source.nodeId}__PORTKEYDELIM__${edge.source.portId}__PORTKEYDELIM__${edge.source.portType}`; - const targetKey = `${edge.target.nodeId}__PORTKEYDELIM__${edge.target.portId}__PORTKEYDELIM__${edge.target.portType}`; - const sourcePos = portPositions[sourceKey]; - const targetPos = portPositions[targetKey]; - return ; - })} - {edgePreview?.targetPos && ( - - )} - - -
-
- ); -}); - -Canvas.displayName = 'Canvas'; -export default memo(Canvas); \ No newline at end of file diff --git a/temporary/(canvas_js)/components/Edge.jsx b/temporary/(canvas_js)/components/Edge.jsx deleted file mode 100644 index 3368f883..00000000 --- a/temporary/(canvas_js)/components/Edge.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { memo } from 'react'; -import styles from '@/app/canvas/assets/Edge.module.scss'; - -const getBezierPath = (x1, y1, x2, y2) => { - const controlPointX1 = x1 + Math.abs(x2 - x1) * 0.5; - const controlPointX2 = x2 - Math.abs(x2 - x1) * 0.5; - return `M ${x1},${y1} C ${controlPointX1},${y1} ${controlPointX2},${y2} ${x2},${y2}`; -}; - -const Edge = ({ id, sourcePos, targetPos, onEdgeClick, isSelected, isPreview = false }) => { - if (!sourcePos || !targetPos) return null; - - const d = getBezierPath(sourcePos.x, sourcePos.y, targetPos.x, targetPos.y); - - const handleEdgeClick = (e) => { - if (isPreview) return; // 프리뷰 모드에서는 클릭 비활성화 - e.stopPropagation(); - onEdgeClick(id); - }; - - return ( - - - - - - ); -}; - -export default memo(Edge); \ No newline at end of file diff --git a/temporary/(canvas_js)/components/ExecutionPanel.jsx b/temporary/(canvas_js)/components/ExecutionPanel.jsx deleted file mode 100644 index cca85978..00000000 --- a/temporary/(canvas_js)/components/ExecutionPanel.jsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client"; -import React, { useState } from 'react'; -import styles from '@/app/canvas/assets/ExecutionPanel.module.scss'; -import { LuPlay, LuTrash2, LuCircleX, LuChevronUp, LuChevronDown } from 'react-icons/lu'; - -const OutputRenderer = ({ output }) => { - if (!output) { - return
Click 'Run' to execute the workflow.
; - } - - if (output.error) { - return ( -
-
- - Execution Failed -
-
{output.error}
-
- ); - } - - const { outputs } = output; - return ( -
-
-
-                    {JSON.stringify(outputs, null, 2)}
-                
-
-
- ); -}; - - -const ExecutionPanel = ({ onExecute, onClear, output, isLoading }) => { - const [isExpanded, setIsExpanded] = useState(true); - - const toggleExpanded = () => { - setIsExpanded(!isExpanded); - }; - - return ( -
-
-
- -

Execution

-
-
- - -
-
- {isExpanded && ( -
-
-                        
-                    
-
- )} -
- ); -}; - -export default ExecutionPanel; \ No newline at end of file diff --git a/temporary/(canvas_js)/components/Header.jsx b/temporary/(canvas_js)/components/Header.jsx deleted file mode 100644 index 411389ce..00000000 --- a/temporary/(canvas_js)/components/Header.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -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'; - -const Header = ({ onMenuClick, onSave, onLoad, onExport, onNewWorkflow, workflowName: externalWorkflowName, onWorkflowNameChange }) => { - const [workflowName, setWorkflowName] = useState('Workflow'); - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(''); - const inputRef = useRef(null); - - useEffect(() => { - if (externalWorkflowName) { - setWorkflowName(externalWorkflowName); - } else { - const savedName = getWorkflowName(); - setWorkflowName(savedName); - } - }, [externalWorkflowName]); - - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus(); - inputRef.current.select(); - } - }, [isEditing]); - - const handleEditClick = () => { - setEditValue(workflowName); - setIsEditing(true); - }; - - const handleSaveClick = () => { - const trimmedValue = editValue.trim(); - const finalValue = trimmedValue || 'Workflow'; - setWorkflowName(finalValue); - saveWorkflowName(finalValue); - - // 부모 컴포넌트에 변경사항 알림 - if (onWorkflowNameChange) { - onWorkflowNameChange(finalValue); - } - - setIsEditing(false); - }; - - const handleCancelClick = () => { - setEditValue(workflowName); - setIsEditing(false); - }; - - const handleKeyDown = (e) => { - if (e.key === 'Enter') { - handleSaveClick(); - } else if (e.key === 'Escape') { - handleCancelClick(); - } - }; - - return ( -
-
- -
- PlateeRAG -
- -
- {isEditing ? ( -
- setEditValue(e.target.value)} - onKeyDown={handleKeyDown} - className={styles.workflowInput} - placeholder="Workflow name..." - /> - - -
- ) : ( -
- {workflowName} - -
- )} -
-
-
- - - -
-
- ); -}; - -export default Header; \ No newline at end of file diff --git a/temporary/(canvas_js)/components/Helper/DraggableNodeItem.jsx b/temporary/(canvas_js)/components/Helper/DraggableNodeItem.jsx deleted file mode 100644 index bab6b389..00000000 --- a/temporary/(canvas_js)/components/Helper/DraggableNodeItem.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import styles from '@/app/canvas/assets/SideMenu.module.scss'; - -const DraggableNodeItem = ({ nodeData }) => { - const onDragStart = (event) => { - event.dataTransfer.setData('application/json', JSON.stringify(nodeData)); - event.dataTransfer.effectAllowed = 'move'; - }; - - return ( -
- {nodeData.nodeName} -
- ); -}; - -export default DraggableNodeItem; \ No newline at end of file diff --git a/temporary/(canvas_js)/components/Helper/NodeList.jsx b/temporary/(canvas_js)/components/Helper/NodeList.jsx deleted file mode 100644 index 0a555fee..00000000 --- a/temporary/(canvas_js)/components/Helper/NodeList.jsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; -import React, { useState } from 'react'; -import styles from '@/app/canvas/assets/NodeList.module.scss'; -import { LuChevronDown } from 'react-icons/lu'; - -const NodeList = ({ title, children }) => { - const [isOpen, setIsOpen] = useState(false); - - return ( -
- - {isOpen && ( -
- {children} -
- )} -
- ); -}; - -export default NodeList; \ No newline at end of file diff --git a/temporary/(canvas_js)/components/Node.jsx b/temporary/(canvas_js)/components/Node.jsx deleted file mode 100644 index 00b8b7b9..00000000 --- a/temporary/(canvas_js)/components/Node.jsx +++ /dev/null @@ -1,298 +0,0 @@ -// src/app/components/Node.jsx -import React, { memo, useState, useEffect } from 'react'; -import styles from '@/app/canvas/assets/Node.module.scss'; -import { devLog } from '@/app/utils/logger'; - -const Node = ({ id, data, position, onNodeMouseDown, isSelected, onPortMouseDown, onPortMouseUp, registerPortRef, snappedPortKey, onParameterChange, isSnapTargetInvalid, isPreview = false, onNodeNameChange, onClearSelection }) => { - const { nodeName, inputs, parameters, outputs, functionId } = data; - const [showAdvanced, setShowAdvanced] = useState(false); - const [isEditingName, setIsEditingName] = useState(false); - const [editingName, setEditingName] = useState(nodeName); - - // nodeName이 변경될 때 editingName 동기화 - useEffect(() => { - setEditingName(nodeName); - }, [nodeName]); - - // 노드 이름 편집 관련 함수들 - const handleNameDoubleClick = (e) => { - if (isPreview) return; - e.stopPropagation(); - setIsEditingName(true); - setEditingName(nodeName); - }; - - const handleNameChange = (e) => { - setEditingName(e.target.value); - }; - - const handleNameKeyDown = (e) => { - if (e.key === 'Enter') { - handleNameSubmit(); - } else if (e.key === 'Escape') { - handleNameCancel(); - } - e.stopPropagation(); - }; - - const handleNameSubmit = () => { - const trimmedName = editingName.trim(); - if (trimmedName && trimmedName !== nodeName && onNodeNameChange) { - onNodeNameChange(id, trimmedName); - } else { - // 변경사항이 없거나 빈 문자열인 경우 원래 값으로 복원 - setEditingName(nodeName); - } - setIsEditingName(false); - }; - - const handleNameCancel = () => { - setEditingName(nodeName); - setIsEditingName(false); - }; - - const handleNameBlur = () => { - handleNameSubmit(); - }; - - // 파라미터를 기본/고급으로 분리 - const basicParameters = parameters?.filter(param => !param.optional) || []; - const advancedParameters = parameters?.filter(param => param.optional) || []; - const hasAdvancedParams = advancedParameters.length > 0; - - const toggleAdvanced = (e) => { - e.stopPropagation(); - setShowAdvanced(prev => !prev); - }; - - // 파라미터 렌더링 함수 - const renderParameter = (param) => ( -
- - {param.name} - - {param.options && param.options.length > 0 ? ( - - ) : ( - handleParamValueChange(e, param.id)} - onMouseDown={(e) => { - devLog.log('input onMouseDown'); - e.stopPropagation(); - }} - onClick={(e) => { - devLog.log('input onClick'); - e.stopPropagation(); - }} - onFocus={(e) => { - devLog.log('input onFocus'); - e.stopPropagation(); - // 파라미터 편집 시 노드 선택 해제 - if (onClearSelection) { - onClearSelection(); - } - }} - onKeyDown={(e) => { - // 키보드 이벤트 전파 방지 (백스페이스, 삭제 등) - e.stopPropagation(); - }} - className={`${styles.paramInput} paramInput`} - step={param.step} - min={param.min} - max={param.max} - /> - )} -
- ); - - const handleMouseDown = (e) => { - if (isPreview) return; // 프리뷰 모드에서는 드래그 비활성화 - e.stopPropagation(); - onNodeMouseDown(e, id); - }; - - const handleParamValueChange = (e, paramId) => { - devLog.log('=== Parameter Change Event ==='); - devLog.log('nodeId:', id, 'paramId:', paramId, 'value:', e.target.value); - - // 이벤트 전파 중단 - e.preventDefault(); - e.stopPropagation(); - - try { - // 값 검증 - const value = e.target.value; - if (value === undefined || value === null) { - devLog.warn('Invalid parameter value:', value); - return; - } - - devLog.log('Calling onParameterChange...'); - // 안전한 콜백 호출 - if (typeof onParameterChange === 'function') { - onParameterChange(id, paramId, value); - devLog.log('onParameterChange completed successfully'); - } else { - devLog.error('onParameterChange is not a function'); - } - } catch (error) { - devLog.error('Error in handleParamValueChange:', error); - } - devLog.log('=== End Parameter Change ==='); - }; - - const hasInputs = inputs?.length > 0; - const hasOutputs = outputs?.length > 0; - const hasIO = hasInputs || hasOutputs; - const hasParams = parameters?.length > 0; - const hasOnlyOutputs = hasOutputs && !hasInputs; - - // 노드 이름 표시용 (10자 넘으면 ... 처리) - const displayName = nodeName.length > 20 ? nodeName.substring(0, 20) + '...' : nodeName; - - return ( -
-
- {isEditingName ? ( - { - e.stopPropagation(); - }} - onClick={(e) => { - e.stopPropagation(); - }} - onFocus={(e) => { - e.stopPropagation(); - }} - className={styles.nameInput} - autoFocus - /> - ) : ( - - {displayName} - - )} - {functionId && ({functionId})} -
-
- {hasIO && ( -
- {hasInputs && ( -
-
INPUT
- {inputs.map(portData => { - const portKey = `${id}__PORTKEYDELIM__${portData.id}__PORTKEYDELIM__input`; - const isSnapping = snappedPortKey === portKey; - - const portClasses = [ styles.port, styles.inputPort, portData.multi ? styles.multi : '', styles[`type-${portData.type}`], isSnapping ? styles.snapping : '', isSnapping && isSnapTargetInvalid ? styles['invalid-snap'] : '' ].filter(Boolean).join(' '); - - return ( -
-
registerPortRef && registerPortRef(id, portData.id, 'input', el)} - className={portClasses} - onMouseDown={isPreview ? undefined : (e) => { e.stopPropagation(); onPortMouseDown({ nodeId: id, portId: portData.id, portType: 'input', isMulti: portData.multi, type: portData.type }) }} - onMouseUp={isPreview ? undefined : (e) => { e.stopPropagation(); onPortMouseUp({ nodeId: id, portId: portData.id, portType: 'input', type: portData.type }) }} - > - {portData.type} -
- - {portData.name} - -
- ) - })} -
- )} - {hasOutputs && ( -
-
OUTPUT
- {outputs.map(portData => { - const portClasses = [ styles.port, styles.outputPort, portData.multi ? styles.multi : '', styles[`type-${portData.type}`] ].filter(Boolean).join(' '); - - return ( -
- {portData.name} -
registerPortRef && registerPortRef(id, portData.id, 'output', el)} - className={portClasses} - onMouseDown={isPreview ? undefined : (e) => { e.stopPropagation(); onPortMouseDown({ nodeId: id, portId: portData.id, portType: 'output', isMulti: portData.multi, type: portData.type }) }} - onMouseUp={isPreview ? undefined : (e) => { e.stopPropagation(); onPortMouseUp({ nodeId: id, portId: portData.id, portType: 'output', type: portData.type }) }} - > - {portData.type} -
-
- ) - })} -
- )} -
- )} - {hasParams && ( - <> - {hasIO &&
} -
-
PARAMETER
- {basicParameters.map(param => renderParameter(param))} - {hasAdvancedParams && ( -
-
- Advanced {showAdvanced ? '▲' : '▼'} -
- {showAdvanced && advancedParameters.map(param => renderParameter(param))} -
- )} -
- - )} -
-
- ); -}; - -export default memo(Node); \ No newline at end of file diff --git a/temporary/(canvas_js)/components/SideMenu.jsx b/temporary/(canvas_js)/components/SideMenu.jsx deleted file mode 100644 index cb923ab2..00000000 --- a/temporary/(canvas_js)/components/SideMenu.jsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; -import React, { useState } from 'react'; -import styles from '@/app/canvas/assets/SideMenu.module.scss'; -import AddNodePanel from '@/app/canvas/components/SideMenuPanel/AddNodePanel'; -import ChatPanel from '@/app/canvas/components/SideMenuPanel/ChatPanel'; -import WorkflowPanel from '@/app/canvas/components/SideMenuPanel/WorkflowPanel'; -import TemplatePanel from '@/app/canvas/components/SideMenuPanel/TemplatePanel'; -import { LuCirclePlus, LuCircleHelp, LuSettings, LuLayoutGrid, LuMessageSquare, LuLayoutTemplate } from "react-icons/lu"; - -// 메인 메뉴 UI -const MainMenu = ({ onNavigate }) => { - return ( - <> -
- - - - - - -
- - ); -}; - -// SideMenu의 전체 컨테이너 및 뷰 전환 로직 -const SideMenu = ({ menuRef, onLoad, onExport, onLoadWorkflow }) => { - const [view, setView] = useState('main'); - - return ( - // menuRef를 받아 외부 클릭 감지에 사용 - - ); -}; - -export default SideMenu; \ No newline at end of file diff --git a/temporary/(canvas_js)/components/SideMenuPanel/AddNodePanel.jsx b/temporary/(canvas_js)/components/SideMenuPanel/AddNodePanel.jsx deleted file mode 100644 index 082c4a74..00000000 --- a/temporary/(canvas_js)/components/SideMenuPanel/AddNodePanel.jsx +++ /dev/null @@ -1,104 +0,0 @@ -"use client"; -import React, { useState, useEffect } from 'react'; -import styles from '@/app/canvas/assets/SideMenu.module.scss'; -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'; - -const iconMap = { - LuBrainCircuit: , - LuShare2: , - LuWrench: , - SiLangchain: , -}; - -const AddNodePanel = ({ onBack }) => { - const { nodes: nodeSpecs, isLoading, error, exportAndRefreshNodes } = useNodes(); - const [activeTab, setActiveTab] = useState(null); - - useEffect(() => { - if (nodeSpecs && nodeSpecs.length > 0) { - setActiveTab(nodeSpecs[0].categoryId); - } - }, [nodeSpecs]); - - const activeTabData = nodeSpecs.find(tab => tab.categoryId === activeTab); - if (isLoading) { - return ( - <> -
- -

Add Nodes

-
-
Loading nodes...
- - ) - } - - if (error) { - return ( - <> -
- -

Add Nodes

-
-
Error: {error}
- - ) - } - - return ( - <> -
- -

Add Nodes

- -
- -
- - - -
- -
- {nodeSpecs.map(tab => ( - - ))} -
- -
- {/* [수정] categories -> functions, 내부 키 이름들도 변경 */} - {activeTabData?.functions?.map(func => ( - - {func.nodes?.map(node => ( - - ))} - - ))} -
- - ); -}; - -export default AddNodePanel; \ No newline at end of file diff --git a/temporary/(canvas_js)/components/SideMenuPanel/ChatPanel.jsx b/temporary/(canvas_js)/components/SideMenuPanel/ChatPanel.jsx deleted file mode 100644 index a94d22fe..00000000 --- a/temporary/(canvas_js)/components/SideMenuPanel/ChatPanel.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { sendMessage } from '@/app/api/chatAPI'; -import styles from '@/app/canvas/assets/Chat.module.scss'; -import sideMenuStyles from '@/app/canvas/assets/SideMenu.module.scss'; -import { LuArrowLeft, LuSend } from "react-icons/lu"; -import { devLog } from '@/app/utils/logger'; - -const ChatPanel = ({ onBack }) => { - const [messages, setMessages] = useState([]); - const [inputValue, setInputValue] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const messageListRef = useRef(null); - - useEffect(() => { - if (messageListRef.current) { - messageListRef.current.scrollTop = messageListRef.current.scrollHeight; - } - }, [messages]); - - const handleInputChange = (e) => { - setInputValue(e.target.value); - }; - - const handleSendMessage = async () => { - if (inputValue.trim() === '' || isLoading) return; - - const userMessage = { - id: Date.now(), - text: inputValue, - sender: 'user', - timestamp: new Date(), - }; - setMessages(prevMessages => [...prevMessages, userMessage]); - setIsLoading(true); - setInputValue(''); - - try { - const response = await sendMessage(userMessage.text); - const botMessage = { - id: Date.now() + 1, - text: response.text, - sender: 'bot', - timestamp: new Date(), - }; - setMessages(prevMessages => [...prevMessages, botMessage]); - } catch (error) { - devLog.error("Error sending message:", error); - const errorMessage = { - id: Date.now() + 1, - text: "Sorry, I couldn't get a response. Please try again.", - sender: 'bot', - timestamp: new Date(), - }; - setMessages(prevMessages => [...prevMessages, errorMessage]); - } finally { - setIsLoading(false); - } - }; - - const handleKeyPress = (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; - - return ( -
-
- -

Chat

-
- -
- {messages.map(msg => ( -
- {msg.text} -
- ))} - {isLoading && messages.length > 0 && messages[messages.length -1].sender === 'user' && ( -
- -
- )} -
- -
-