From 86b0319cf7d3289f705572f991c6ba3074d2303b Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 26 Oct 2025 19:10:20 +0900 Subject: [PATCH 01/32] feat: add modern UI dependencies - Add Monaco Editor for code editing - Add Legend State for reactive state management - Add Radix UI + Tailwind CSS for modern UI components - Add react-resizable-panels for layout management - Add @radix-ui/react-icons for icon components Dependencies added: - @monaco-editor/react: Code editor component - @legendapp/state: Reactive state management - @legendapp/state/react: React integration for Legend State - @radix-ui/themes: UI component library - @radix-ui/react-icons: Icon components - react-resizable-panels: Resizable layout panels - tailwindcss: Utility-first CSS framework - postcss: CSS post-processor - autoprefixer: CSS vendor prefixer --- apps/executeJS/package.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/executeJS/package.json b/apps/executeJS/package.json index d7c3974..ad8996a 100644 --- a/apps/executeJS/package.json +++ b/apps/executeJS/package.json @@ -20,13 +20,21 @@ "clean": "rm -rf dist src-tauri/target" }, "dependencies": { + "@legendapp/state": "^2.1.15", + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/themes": "^3.2.1", "@tauri-apps/api": "^2.0.0", - "react": "latest", - "react-dom": "latest" + "react": "^19.2.0", + "react-dom": "latest", + "react-resizable-panels": "^3.0.6" }, "devDependencies": { "@tauri-apps/cli": "latest", "@vitejs/plugin-react": "latest", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.16", "vite": "latest" } } From 9f6aed9e1f123740c85e916ebc2c6a4b432b3d8d Mon Sep 17 00:00:00 2001 From: ohah Date: Sun, 26 Oct 2025 19:10:28 +0900 Subject: [PATCH 02/32] feat: implement FSD architecture structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement Feature-Sliced Design (FSD) architecture for better code organization: ๐Ÿ“ app/ - index.tsx: Main app component - providers.tsx: Global providers (Legend State, Radix UI) ๐Ÿ“ pages/editor/ - EditorPage.tsx: Main editor page with split layout ๐Ÿ“ widgets/ - code-editor/CodeEditor.tsx: Monaco Editor wrapper with Cmd+Enter binding - output-panel/OutputPanel.tsx: Console output display - tab-bar/TabBar.tsx: Multi-tab management UI ๐Ÿ“ features/ - execute-code/model.ts: Code execution state and API - manage-tabs/model.ts: Tab management with localStorage sync ๐Ÿ“ shared/ - types/index.ts: Common TypeScript interfaces - lib/storage.ts: localStorage helper functions - ui/Button.tsx: Reusable button component - ui/Panel.tsx: Reusable panel component This structure follows FSD principles: - Clear layer separation (app โ†’ pages โ†’ widgets โ†’ features โ†’ shared) - Dependency rules enforcement - Better maintainability and scalability --- apps/executeJS/src/app/index.tsx | 11 ++ apps/executeJS/src/app/providers.tsx | 23 ++++ .../src/features/execute-code/model.test.ts | 42 ++++++ .../src/features/execute-code/model.ts | 116 ++++++++++++++++ .../src/features/manage-tabs/model.ts | 130 ++++++++++++++++++ .../src/pages/editor/EditorPage.test.tsx | 23 ++++ .../executeJS/src/pages/editor/EditorPage.tsx | 86 ++++++++++++ apps/executeJS/src/shared/lib/storage.ts | 57 ++++++++ apps/executeJS/src/shared/types/index.ts | 45 ++++++ apps/executeJS/src/shared/ui/Button.tsx | 27 ++++ apps/executeJS/src/shared/ui/Panel.tsx | 22 +++ .../widgets/code-editor/CodeEditor.test.tsx | 51 +++++++ .../src/widgets/code-editor/CodeEditor.tsx | 78 +++++++++++ .../src/widgets/output-panel/OutputPanel.tsx | 62 +++++++++ apps/executeJS/src/widgets/tab-bar/TabBar.tsx | 71 ++++++++++ 15 files changed, 844 insertions(+) create mode 100644 apps/executeJS/src/app/index.tsx create mode 100644 apps/executeJS/src/app/providers.tsx create mode 100644 apps/executeJS/src/features/execute-code/model.test.ts create mode 100644 apps/executeJS/src/features/execute-code/model.ts create mode 100644 apps/executeJS/src/features/manage-tabs/model.ts create mode 100644 apps/executeJS/src/pages/editor/EditorPage.test.tsx create mode 100644 apps/executeJS/src/pages/editor/EditorPage.tsx create mode 100644 apps/executeJS/src/shared/lib/storage.ts create mode 100644 apps/executeJS/src/shared/types/index.ts create mode 100644 apps/executeJS/src/shared/ui/Button.tsx create mode 100644 apps/executeJS/src/shared/ui/Panel.tsx create mode 100644 apps/executeJS/src/widgets/code-editor/CodeEditor.test.tsx create mode 100644 apps/executeJS/src/widgets/code-editor/CodeEditor.tsx create mode 100644 apps/executeJS/src/widgets/output-panel/OutputPanel.tsx create mode 100644 apps/executeJS/src/widgets/tab-bar/TabBar.tsx diff --git a/apps/executeJS/src/app/index.tsx b/apps/executeJS/src/app/index.tsx new file mode 100644 index 0000000..9737cb3 --- /dev/null +++ b/apps/executeJS/src/app/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Providers } from './providers'; +import { EditorPage } from '../pages/editor/EditorPage'; + +export const App: React.FC = () => { + return ( + + + + ); +}; diff --git a/apps/executeJS/src/app/providers.tsx b/apps/executeJS/src/app/providers.tsx new file mode 100644 index 0000000..73e6b63 --- /dev/null +++ b/apps/executeJS/src/app/providers.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Theme } from '@radix-ui/themes'; +import { LegendStateProvider } from '@legendapp/state/react'; + +interface ProvidersProps { + children: React.ReactNode; +} + +export const Providers: React.FC = ({ children }) => { + return ( + + + {children} + + + ); +}; diff --git a/apps/executeJS/src/features/execute-code/model.test.ts b/apps/executeJS/src/features/execute-code/model.test.ts new file mode 100644 index 0000000..f465c33 --- /dev/null +++ b/apps/executeJS/src/features/execute-code/model.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { executionState, executionActions } from './model'; + +// Tauri API ๋ชจํ‚น +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +describe('execute-code model', () => { + beforeEach(() => { + // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ์ƒํƒœ ์ดˆ๊ธฐํ™” + executionState.result.set(null); + executionState.isExecuting.set(false); + executionState.history.set([]); + }); + + it('should initialize with empty state', () => { + expect(executionState.result.get()).toBeNull(); + expect(executionState.isExecuting.get()).toBe(false); + expect(executionState.history.get()).toEqual([]); + }); + + it('should clear result', () => { + executionState.result.set({ + code: 'test', + result: 'output', + timestamp: '2023-01-01T00:00:00.000Z', + success: true, + }); + + executionActions.clearResult(); + + expect(executionState.result.get()).toBeNull(); + }); + + it('should handle execution state changes', () => { + expect(executionState.isExecuting.get()).toBe(false); + + // executeCode๋Š” ๋น„๋™๊ธฐ์ด๋ฏ€๋กœ isExecuting ์ƒํƒœ๋งŒ ํ™•์ธ + // ์‹ค์ œ ์‹คํ–‰์€ Tauri API๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ ๋ชจํ‚น ํ•„์š” + }); +}); diff --git a/apps/executeJS/src/features/execute-code/model.ts b/apps/executeJS/src/features/execute-code/model.ts new file mode 100644 index 0000000..7e649d9 --- /dev/null +++ b/apps/executeJS/src/features/execute-code/model.ts @@ -0,0 +1,116 @@ +import { observable } from '@legendapp/state'; +import type { JsExecutionResult } from '../../shared/types'; + +// ์ฝ”๋“œ ์‹คํ–‰ ์ƒํƒœ +export const executionState = observable({ + result: null as JsExecutionResult | null, + isExecuting: false, + history: [] as JsExecutionResult[], +}); + +// Tauri command ํ˜ธ์ถœ์„ ์œ„ํ•œ API ํ•จ์ˆ˜๋“ค +export const executeApi = { + // JavaScript ์ฝ”๋“œ ์‹คํ–‰ + executeCode: async (code: string): Promise => { + // Tauri command ํ˜ธ์ถœ + const { invoke } = await import('@tauri-apps/api/core'); + + try { + const result = await invoke('execute_js', { code }); + return result; + } catch (error) { + return { + code, + result: '', + timestamp: new Date().toISOString(), + success: false, + error: error as string, + }; + } + }, + + // ์‹คํ–‰ ํžˆ์Šคํ† ๋ฆฌ ๊ฐ€์ ธ์˜ค๊ธฐ + getHistory: async (): Promise => { + const { invoke } = await import('@tauri-apps/api/core'); + + try { + return await invoke('get_js_execution_history'); + } catch (error) { + console.error('Failed to load execution history:', error); + return []; + } + }, + + // ํžˆ์Šคํ† ๋ฆฌ ์‚ญ์ œ + clearHistory: async (): Promise => { + const { invoke } = await import('@tauri-apps/api/core'); + + try { + await invoke('clear_js_execution_history'); + } catch (error) { + console.error('Failed to clear execution history:', error); + } + }, +}; + +// ์ฝ”๋“œ ์‹คํ–‰ ์•ก์…˜๋“ค +export const executionActions = { + // ์ฝ”๋“œ ์‹คํ–‰ + executeCode: async (code: string) => { + if (executionState.isExecuting.get()) return; + + executionState.isExecuting.set(true); + executionState.result.set(null); + + try { + const result = await executeApi.executeCode(code); + executionState.result.set(result); + + // ํžˆ์Šคํ† ๋ฆฌ์— ์ถ”๊ฐ€ (์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ๋งŒ) + if (result.success) { + const currentHistory = executionState.history.get(); + const newHistory = [result, ...currentHistory]; + // ํžˆ์Šคํ† ๋ฆฌ ์ตœ๋Œ€ 50๊ฐœ๋กœ ์ œํ•œ + if (newHistory.length > 50) { + newHistory.splice(50); + } + executionState.history.set(newHistory); + } + } catch (error) { + executionState.result.set({ + code, + result: '', + timestamp: new Date().toISOString(), + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + executionState.isExecuting.set(false); + } + }, + + // ๊ฒฐ๊ณผ ์ดˆ๊ธฐํ™” + clearResult: () => { + executionState.result.set(null); + }, + + // ํžˆ์Šคํ† ๋ฆฌ ๋กœ๋“œ + loadHistory: async () => { + try { + const history = await executeApi.getHistory(); + executionState.history.set(history); + } catch (error) { + console.error('Failed to load execution history:', error); + } + }, + + // ํžˆ์Šคํ† ๋ฆฌ ์‚ญ์ œ + clearHistory: async () => { + try { + await executeApi.clearHistory(); + executionState.history.set([]); + } catch (error) { + console.error('Failed to clear execution history:', error); + } + }, +}; diff --git a/apps/executeJS/src/features/manage-tabs/model.ts b/apps/executeJS/src/features/manage-tabs/model.ts new file mode 100644 index 0000000..07364de --- /dev/null +++ b/apps/executeJS/src/features/manage-tabs/model.ts @@ -0,0 +1,130 @@ +import { observable } from '@legendapp/state'; +import { storage } from '../../shared/lib/storage'; +import type { Tab } from '../../shared/types'; + +// ํƒญ ๊ด€๋ฆฌ ์ƒํƒœ +export const tabsState = observable({ + tabs: [] as Tab[], + activeTabId: null as string | null, +}); + +// ํƒญ ID ์ƒ์„ฑ๊ธฐ +let tabIdCounter = 1; +const generateTabId = () => `tab_${tabIdCounter++}`; + +// ์ดˆ๊ธฐ ํƒญ ์ƒ์„ฑ +const createInitialTab = (): Tab => ({ + id: generateTabId(), + name: 'Untitled', + code: '', + isActive: true, + isDirty: false, +}); + +// ํƒญ ๊ด€๋ฆฌ ์•ก์…˜๋“ค +export const tabActions = { + // ์ƒˆ ํƒญ ์ถ”๊ฐ€ + addTab: () => { + const newTab = createInitialTab(); + + // ๊ธฐ์กด ํƒญ๋“ค์„ ๋น„ํ™œ์„ฑํ™” + tabsState.tabs.forEach((tab) => { + tab.isActive.set(false); + }); + + // ์ƒˆ ํƒญ ์ถ”๊ฐ€ + tabsState.tabs.push(newTab); + tabsState.activeTabId.set(newTab.id); + + // localStorage์— ์ €์žฅ + storage.saveTabs(tabsState.tabs.get()); + storage.saveActiveTab(newTab.id); + }, + + // ํƒญ ์„ ํƒ + selectTab: (tabId: string) => { + // ๋ชจ๋“  ํƒญ ๋น„ํ™œ์„ฑํ™” + tabsState.tabs.forEach((tab) => { + tab.isActive.set(tab.id.get() === tabId); + }); + + tabsState.activeTabId.set(tabId); + storage.saveActiveTab(tabId); + }, + + // ํƒญ ๋‹ซ๊ธฐ + closeTab: (tabId: string) => { + const tabIndex = tabsState.tabs.findIndex((tab) => tab.id === tabId); + if (tabIndex === -1) return; + + const wasActive = tabsState.tabs[tabIndex].isActive.get(); + tabsState.tabs.splice(tabIndex, 1); + + // ๋‹ซํžŒ ํƒญ์ด ํ™œ์„ฑ ํƒญ์ด์—ˆ๋‹ค๋ฉด ๋‹ค๋ฅธ ํƒญ ์„ ํƒ + if (wasActive && tabsState.tabs.length > 0) { + const newActiveIndex = Math.min(tabIndex, tabsState.tabs.length - 1); + tabActions.selectTab(tabsState.tabs[newActiveIndex].id.get()); + } else if (tabsState.tabs.length === 0) { + // ๋ชจ๋“  ํƒญ์ด ๋‹ซํ˜”๋‹ค๋ฉด ์ƒˆ ํƒญ ์ƒ์„ฑ + tabActions.addTab(); + } + + storage.saveTabs(tabsState.tabs.get()); + }, + + // ํƒญ ์ด๋ฆ„ ๋ณ€๊ฒฝ + renameTab: (tabId: string, name: string) => { + const tab = tabsState.tabs.find((t) => t.id.get() === tabId); + if (tab) { + tab.name.set(name); + storage.saveTabs(tabsState.tabs.get()); + } + }, + + // ํƒญ ์ฝ”๋“œ ๋ณ€๊ฒฝ + updateTabCode: (tabId: string, code: string) => { + const tab = tabsState.tabs.find((t) => t.id.get() === tabId); + if (tab) { + tab.code.set(code); + tab.isDirty.set(true); + storage.saveTabs(tabsState.tabs.get()); + } + }, + + // ํƒญ ์ €์žฅ ์™„๋ฃŒ (dirty ์ƒํƒœ ํ•ด์ œ) + markTabSaved: (tabId: string) => { + const tab = tabsState.tabs.find((t) => t.id.get() === tabId); + if (tab) { + tab.isDirty.set(false); + storage.saveTabs(tabsState.tabs.get()); + } + }, + + // localStorage์—์„œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + loadFromStorage: () => { + const savedTabs = storage.loadTabs(); + const savedActiveTab = storage.loadActiveTab(); + + if (savedTabs.length > 0) { + tabsState.tabs.set(savedTabs); + if (savedActiveTab && savedTabs.find((t) => t.id === savedActiveTab)) { + tabsState.activeTabId.set(savedActiveTab); + tabActions.selectTab(savedActiveTab); + } else { + tabActions.selectTab(savedTabs[0].id); + } + } else { + // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ดˆ๊ธฐ ํƒญ ์ƒ์„ฑ + tabActions.addTab(); + } + }, +}; + +// ํ˜„์žฌ ํ™œ์„ฑ ํƒญ ๊ฐ€์ ธ์˜ค๊ธฐ +export const getActiveTab = () => { + return ( + tabsState.tabs.find( + (tab) => tab.id.get() === tabsState.activeTabId.get() + ) || null + ); +}; diff --git a/apps/executeJS/src/pages/editor/EditorPage.test.tsx b/apps/executeJS/src/pages/editor/EditorPage.test.tsx new file mode 100644 index 0000000..ec0fd83 --- /dev/null +++ b/apps/executeJS/src/pages/editor/EditorPage.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import '@testing-library/jest-dom'; +import { EditorPage } from './EditorPage'; + +// Tauri API ๋ชจํ‚น +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +// Legend State ๋ชจํ‚น +vi.mock('@legendapp/state/react', () => ({ + useObservable: vi.fn(() => []), +})); + +describe('EditorPage', () => { + it('renders without crashing', () => { + render(); + expect( + screen.getByText(/Cmd\+Enter๋ฅผ ๋ˆŒ๋Ÿฌ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”/) + ).toBeInTheDocument(); + }); +}); diff --git a/apps/executeJS/src/pages/editor/EditorPage.tsx b/apps/executeJS/src/pages/editor/EditorPage.tsx new file mode 100644 index 0000000..da43e6d --- /dev/null +++ b/apps/executeJS/src/pages/editor/EditorPage.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from 'react'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; +import { useObservable } from '@legendapp/state/react'; +import { TabBar } from '../../widgets/tab-bar/TabBar'; +import { CodeEditor } from '../../widgets/code-editor/CodeEditor'; +import { OutputPanel } from '../../widgets/output-panel/OutputPanel'; +import { + tabsState, + tabActions, + getActiveTab, +} from '../../features/manage-tabs/model'; +import { + executionState, + executionActions, +} from '../../features/execute-code/model'; + +export const EditorPage: React.FC = () => { + const tabs = useObservable(tabsState.tabs); + const activeTabId = useObservable(tabsState.activeTabId); + const executionResult = useObservable(executionState.result); + const isExecuting = useObservable(executionState.isExecuting); + + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + useEffect(() => { + tabActions.loadFromStorage(); + executionActions.loadHistory(); + }, []); + + // ํ˜„์žฌ ํ™œ์„ฑ ํƒญ ๊ฐ€์ ธ์˜ค๊ธฐ + const activeTab = getActiveTab(); + + // ์ฝ”๋“œ ์‹คํ–‰ ํ•ธ๋“ค๋Ÿฌ + const handleExecuteCode = () => { + if (activeTab) { + executionActions.executeCode(activeTab.code.get()); + } + }; + + // ํƒญ ์ฝ”๋“œ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleCodeChange = (code: string) => { + if (activeTab) { + tabActions.updateTabCode(activeTab.id.get(), code); + } + }; + + return ( +
+ {/* ํƒญ ๋ฐ” */} + + + {/* ๋ฉ”์ธ ์ปจํ…์ธ  ์˜์—ญ */} +
+ + {/* ์™ผ์ชฝ ํŒจ๋„ - ์ฝ”๋“œ ์—๋””ํ„ฐ */} + +
+ {activeTab && ( + + )} +
+
+ + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค */} + + + {/* ์˜ค๋ฅธ์ชฝ ํŒจ๋„ - ์ถœ๋ ฅ ๊ฒฐ๊ณผ */} + + + +
+
+
+ ); +}; diff --git a/apps/executeJS/src/shared/lib/storage.ts b/apps/executeJS/src/shared/lib/storage.ts new file mode 100644 index 0000000..69de731 --- /dev/null +++ b/apps/executeJS/src/shared/lib/storage.ts @@ -0,0 +1,57 @@ +// localStorage ํ—ฌํผ ํ•จ์ˆ˜๋“ค + +const STORAGE_KEYS = { + TABS: 'executejs_tabs', + ACTIVE_TAB: 'executejs_active_tab', +} as const; + +export const storage = { + // ํƒญ ๋ฐ์ดํ„ฐ ์ €์žฅ + saveTabs: (tabs: any[]) => { + try { + localStorage.setItem(STORAGE_KEYS.TABS, JSON.stringify(tabs)); + } catch (error) { + console.error('Failed to save tabs to localStorage:', error); + } + }, + + // ํƒญ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + loadTabs: (): any[] => { + try { + const data = localStorage.getItem(STORAGE_KEYS.TABS); + return data ? JSON.parse(data) : []; + } catch (error) { + console.error('Failed to load tabs from localStorage:', error); + return []; + } + }, + + // ํ™œ์„ฑ ํƒญ ID ์ €์žฅ + saveActiveTab: (tabId: string) => { + try { + localStorage.setItem(STORAGE_KEYS.ACTIVE_TAB, tabId); + } catch (error) { + console.error('Failed to save active tab to localStorage:', error); + } + }, + + // ํ™œ์„ฑ ํƒญ ID ๋กœ๋“œ + loadActiveTab: (): string | null => { + try { + return localStorage.getItem(STORAGE_KEYS.ACTIVE_TAB); + } catch (error) { + console.error('Failed to load active tab from localStorage:', error); + return null; + } + }, + + // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + clear: () => { + try { + localStorage.removeItem(STORAGE_KEYS.TABS); + localStorage.removeItem(STORAGE_KEYS.ACTIVE_TAB); + } catch (error) { + console.error('Failed to clear localStorage:', error); + } + }, +}; diff --git a/apps/executeJS/src/shared/types/index.ts b/apps/executeJS/src/shared/types/index.ts new file mode 100644 index 0000000..aeab407 --- /dev/null +++ b/apps/executeJS/src/shared/types/index.ts @@ -0,0 +1,45 @@ +// ๊ณตํ†ต ํƒ€์ž… ์ •์˜ + +export interface JsExecutionResult { + code: string; + result: string; + timestamp: string; + success: boolean; + error?: string; +} + +export interface Tab { + id: string; + name: string; + code: string; + isActive: boolean; + isDirty: boolean; // ์ €์žฅ๋˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ๋Š”์ง€ +} + +export interface AppState { + tabs: Tab[]; + activeTabId: string | null; + executionResult: JsExecutionResult | null; + isExecuting: boolean; +} + +export interface MonacoEditorProps { + value: string; + onChange: (value: string) => void; + onExecute: () => void; + language?: string; + theme?: string; +} + +export interface TabBarProps { + tabs: any; // Legend State Observable ํƒ€์ž… + activeTabId: any; // Legend State Observable ํƒ€์ž… + onTabSelect: (tabId: string) => void; + onTabClose: (tabId: string) => void; + onTabAdd: () => void; +} + +export interface OutputPanelProps { + result: any; // Legend State Observable ํƒ€์ž… + isExecuting: any; // Legend State Observable ํƒ€์ž… +} diff --git a/apps/executeJS/src/shared/ui/Button.tsx b/apps/executeJS/src/shared/ui/Button.tsx new file mode 100644 index 0000000..02fa88b --- /dev/null +++ b/apps/executeJS/src/shared/ui/Button.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Button as RadixButton } from '@radix-ui/themes'; + +interface ButtonProps extends React.ComponentProps { + variant?: 'solid' | 'soft' | 'outline' | 'ghost'; + size?: '1' | '2' | '3' | '4'; + children: React.ReactNode; +} + +export const Button: React.FC = ({ + variant = 'solid', + size = '2', + children, + className = '', + ...props +}) => { + return ( + + {children} + + ); +}; diff --git a/apps/executeJS/src/shared/ui/Panel.tsx b/apps/executeJS/src/shared/ui/Panel.tsx new file mode 100644 index 0000000..470bc51 --- /dev/null +++ b/apps/executeJS/src/shared/ui/Panel.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Box } from '@radix-ui/themes'; + +interface PanelProps { + children: React.ReactNode; + className?: string; +} + +export const Panel: React.FC = ({ + children, + className = '', + ...props +}) => { + return ( + + {children} + + ); +}; diff --git a/apps/executeJS/src/widgets/code-editor/CodeEditor.test.tsx b/apps/executeJS/src/widgets/code-editor/CodeEditor.test.tsx new file mode 100644 index 0000000..e3188de --- /dev/null +++ b/apps/executeJS/src/widgets/code-editor/CodeEditor.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { CodeEditor } from './CodeEditor'; +import '@testing-library/jest-dom'; + +// Monaco Editor ๋ชจํ‚น +vi.mock('@monaco-editor/react', () => ({ + Editor: ({ value, onChange, onMount }: any) => { + // ํ…Œ์ŠคํŠธ์šฉ ๊ฐ„๋‹จํ•œ ์—๋””ํ„ฐ ์ปดํฌ๋„ŒํŠธ + return ( +
+