diff --git a/.cursorrules b/.cursorrules index 3dfab90..9cd4dc2 100644 --- a/.cursorrules +++ b/.cursorrules @@ -12,7 +12,10 @@ - `.github/workflows/`: CI/CD 워크플로우 ### 기술 스택 -- **Frontend**: React 18, TypeScript, Vite, Vitest +- **Frontend**: React 19, TypeScript, Vite, Vitest +- **UI**: Radix UI + Tailwind CSS +- **상태 관리**: Legend State +- **코드 에디터**: Monaco Editor - **Backend**: Rust, Tauri 2.0 - **JavaScript Engine**: Deno Core 0.323 (V8 기반) - **Package Manager**: pnpm @@ -49,6 +52,33 @@ apps/executeJS/src-tauri/src/ - ✅ 문법 오류 감지 (실제 JavaScript 엔진 수준) - ✅ Chrome DevTools 수준의 출력 +## FSD 아키텍처 규칙 + +### 폴더 구조 +``` +src/ +├── app/ # 앱 초기화 및 프로바이더 +├── pages/ # 페이지 레벨 컴포넌트 +├── widgets/ # 복합 UI 블록 +├── features/ # 비즈니스 로직 기능 +├── shared/ # 공유 유틸리티 +└── main.tsx # Vite 엔트리 +``` + +### 의존성 규칙 +- **app** → pages, widgets, features, shared +- **pages** → widgets, features, shared +- **widgets** → features, shared +- **features** → shared +- **shared** → (다른 레이어 import 금지) + +### 레이어별 책임 +- **app/**: 앱 초기화, 전역 프로바이더 +- **pages/**: 페이지 레벨 컴포넌트 (라우팅 단위) +- **widgets/**: 복합 UI 블록 (여러 features 조합) +- **features/**: 비즈니스 로직 (도메인별) +- **shared/**: 재사용 가능한 유틸리티 + ## 코딩 규칙 ### JavaScript/TypeScript @@ -57,6 +87,8 @@ apps/executeJS/src-tauri/src/ - TypeScript strict 모드 - Vitest + Testing Library로 테스트 - 함수형 컴포넌트와 Hooks 사용 +- Legend State로 상태 관리 +- Radix UI + Tailwind CSS로 스타일링 ### Rust - rustfmt 기본 설정 사용 (4 스페이스) diff --git a/PR_SUMMARY.md b/PR_SUMMARY.md deleted file mode 100644 index 12ae066..0000000 --- a/PR_SUMMARY.md +++ /dev/null @@ -1,200 +0,0 @@ -# 🚀 ExecuteJS: Deno Core 기반 JavaScript 런타임 구현 - -## 📋 PR 개요 - -ExecuteJS 프로젝트에 Deno Core를 기반으로 한 JavaScript 런타임을 구현하여, 데스크톱 애플리케이션에서 JavaScript 코드를 실행할 수 있는 기능을 추가했습니다. - -## 🎯 주요 기능 - -### ✅ JavaScript 런타임 - -- **Deno Core 0.323** 기반 V8 JavaScript 엔진 -- **Chrome DevTools 수준**의 `console.log()` 출력 -- **`alert()`** 함수 지원 -- **실제 JavaScript 엔진** 수준의 문법 오류 감지 -- **변수 할당 및 계산** 지원 - -### ✅ npm 모듈 시뮬레이션 - -- **lodash**: `map`, `filter`, `reduce`, `find`, `chunk` 함수 -- **moment**: `now`, `format` 함수 -- **uuid**: `v4` 함수 -- **require()** 함수를 통한 모듈 로딩 -- **Node.js 스타일** 모듈 시스템 지원 (`module.exports`, `exports`) - -### ✅ Tauri 2.0 호환성 - -- **Send 트레이트** 문제 해결 (`tokio::task::spawn_blocking` 사용) -- **Tauri 2.0** 완전 호환 -- **스레드 안전** 출력 버퍼링 - -## 🏗️ 아키텍처 - -### 핵심 컴포넌트 - -``` -apps/executeJS/src-tauri/src/ -├── deno_runtime.rs # Deno Core 런타임 구현 -├── bootstrap.js # JavaScript API 정의 -├── js_executor.rs # 실행 결과 관리 -└── commands.rs # Tauri 명령어 -``` - -### 실행 흐름 - -1. **초기화**: `DenoExecutor::new()` - 출력 버퍼 설정 -2. **실행**: `execute_script()` - 별도 스레드에서 Deno Core 실행 -3. **API 연결**: `bootstrap.js` - console.log, alert 등 커스텀 API -4. **결과 처리**: 출력 버퍼에서 결과 수집 및 반환 - -## 📁 변경된 파일 - -### 새로 추가된 파일 - -- `apps/executeJS/src-tauri/src/deno_runtime.rs` - Deno Core 런타임 구현 -- `apps/executeJS/src-tauri/src/bootstrap.js` - JavaScript API 정의 - -### 수정된 파일 - -- `apps/executeJS/src-tauri/Cargo.toml` - Deno Core 의존성 추가 -- `apps/executeJS/src-tauri/src/js_executor.rs` - Deno 런타임 통합 -- `apps/executeJS/src-tauri/src/commands.rs` - async 함수로 변경 -- `.gitignore` - Tauri 생성 파일 무시 설정 -- `.cursorrules` - 아키텍처 문서화 - -## 🧪 테스트 결과 - -### 기본 JavaScript 실행 - -```javascript -console.log('Hello World'); // ✅ "Hello World" -alert('Hello Alert'); // ✅ "[ALERT] Hello Alert" -let a = 5; -console.log(a); // ✅ "5" -let x = 1; -let y = 2; -console.log(x + y); // ✅ "3" -``` - -### npm 모듈 사용 - -```javascript -const _ = require('lodash'); -const numbers = [1, 2, 3, 4, 5]; -const doubled = _.map(numbers, (n) => n * 2); -console.log('Lodash test:', doubled); // ✅ "[2, 4, 6, 8, 10]" -``` - -### 문법 오류 감지 - -```javascript -alert('adf'(; // ✅ 문법 오류로 실행 실패 -``` - -## 🔧 기술적 구현 - -### Send 트레이트 문제 해결 - -```rust -// 별도 스레드에서 Deno Core 실행 (Send 트레이트 문제 해결) -let result = tokio::task::spawn_blocking(move || { - let mut js_runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(Rc::new(FsModuleLoader)), - extensions: vec![executejs_runtime::init_ops()], - ..Default::default() - }); - // ... 실행 로직 -}).await?; -``` - -### 커스텀 op 함수 - -```rust -#[op2(fast)] -#[string] -fn op_console_log(#[string] message: String) -> Result<(), AnyError> { - // 출력 버퍼에 메시지 추가 - Ok(()) -} -``` - -### npm 모듈 시뮬레이션 - -```javascript -globalThis.require = (moduleName) => { - const modules = { - 'lodash': { map, filter, reduce, find, chunk }, - 'moment': { now, format }, - 'uuid': { v4 } - }; - return modules[moduleName] || throw new Error(...); -}; -``` - -## 📊 성능 및 안정성 - -- ✅ **스레드 안전**: Mutex를 사용한 출력 버퍼 관리 -- ✅ **메모리 효율**: 각 실행마다 새로운 JsRuntime 인스턴스 -- ✅ **오류 처리**: 실제 JavaScript 엔진 수준의 오류 감지 -- ✅ **테스트 격리**: 테스트 간 전역 상태 충돌 방지 - -## 🚀 사용 예시 - -### 기본 사용법 - -```javascript -// 변수 할당 및 계산 -let a = 10; -let b = 20; -console.log('합계:', a + b); // "합계: 30" - -// 배열 처리 -const numbers = [1, 2, 3, 4, 5]; -const sum = numbers.reduce((acc, n) => acc + n, 0); -console.log('합계:', sum); // "합계: 15" -``` - -### npm 모듈 사용 - -```javascript -// lodash 사용 -const _ = require('lodash'); -const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; -const chunks = _.chunk(data, 3); -console.log('청크:', chunks); // [[1,2,3], [4,5,6], [7,8,9], [10]] - -// moment 사용 -const moment = require('moment'); -console.log('현재 시간:', moment.now()); - -// uuid 사용 -const uuid = require('uuid'); -console.log('UUID:', uuid.v4()); -``` - -## 🔮 향후 계획 - -- [ ] **실제 npm 다운로드**: Tauri 호환성 문제 해결 후 실제 npm 패키지 다운로드 -- [ ] **ES6 import 지원**: `import` 문법으로 모듈 로딩 -- [ ] **더 많은 npm 모듈**: axios, express 등 추가 모듈 지원 -- [ ] **파일 시스템 API**: fs, path 등 Node.js API 지원 - -## 📝 커밋 히스토리 - -- `afa9e5b` - feat: npm 모듈 시뮬레이션 완성 및 버전 업데이트 -- `9928908` - chore: gen 디렉토리를 git에서 제거하고 .gitignore에 추가 -- `2e1ad25` - feat: npm 모듈 시뮬레이션 구현 -- `[이전 커밋들]` - Deno Core 런타임 구현 및 Tauri 통합 - -## 🎉 결론 - -ExecuteJS는 이제 Deno Core 기반의 강력한 JavaScript 런타임을 제공합니다. Chrome DevTools 수준의 출력과 npm 모듈 시뮬레이션을 통해 사용자가 데스크톱에서 JavaScript 코드를 편리하게 실행하고 테스트할 수 있습니다. - -**주요 성과:** - -- ✅ Deno Core 0.323 기반 V8 JavaScript 엔진 통합 -- ✅ Tauri 2.0 완전 호환 -- ✅ npm 모듈 시뮬레이션 (lodash, moment, uuid) -- ✅ Chrome DevTools 수준의 console.log 및 alert 지원 -- ✅ 실제 JavaScript 엔진 수준의 문법 오류 감지 -- ✅ 스레드 안전 및 메모리 효율적인 구현 diff --git a/agent.md b/agent.md index 1f8f1bd..18c51e4 100644 --- a/agent.md +++ b/agent.md @@ -8,13 +8,14 @@ ExecuteJS는 JavaScript 코드를 안전하게 실행할 수 있는 Tauri 기반 ### 기술 스택 -- **Frontend**: React 19, TypeScript, Vite +- **Frontend**: React 19, TypeScript, Vite, Tailwind CSS 4.x - **Backend**: Rust, Tauri 2.0 - **Testing**: Vitest, Testing Library - **Linting**: oxlint, Prettier, rustfmt, clippy - **Documentation**: RSPress - **Package Manager**: pnpm - **CI/CD**: GitHub Actions +- **Architecture**: Feature-Sliced Design (FSD) ### 모노레포 구조 @@ -69,6 +70,9 @@ pnpm type-check # 타입 검사 - **포맷터**: Prettier - **타입**: TypeScript strict 모드 - **테스트**: Vitest + Testing Library +- **아키텍처**: Feature-Sliced Design (FSD) +- **스타일링**: Tailwind CSS 4.x +- **경로 별칭**: `@/*` → `./src/*` ### Rust @@ -109,12 +113,46 @@ chore: 빌드 설정 변경 ## 아키텍처 +### Feature-Sliced Design (FSD) + +프로젝트는 FSD 아키텍처를 따릅니다: + +``` +src/ +├── app/ # 앱 초기화 및 프로바이더 +├── pages/ # 페이지 레벨 컴포넌트 +├── widgets/ # 복합 UI 블록 +├── features/ # 비즈니스 로직 기능 +└── shared/ # 공유 유틸리티 + ├── ui/ # UI 컴포넌트 + └── types/ # 타입 정의 +``` + +#### FSD 레이어 규칙 + +- **app** → pages, widgets, features, shared +- **pages** → widgets, features, shared +- **widgets** → features, shared +- **features** → shared +- **shared** → (다른 레이어 import 금지) + +#### 파일 명명 규칙 + +- React 컴포넌트: kebab-case (예: `editor-page.tsx`) +- 각 레이어에 `index.ts` 파일로 export 정리 +- `export * from` 패턴 사용 + +#### 경로 별칭 + +- `@/*` → `./src/*` (TypeScript, Vite 설정) +- 절대 경로 import 사용 권장 + ### 프론트엔드 (React) - **상태 관리**: React Hooks - **빌드 도구**: Vite - **테스팅**: Vitest + Testing Library -- **스타일링**: CSS Modules +- **스타일링**: Tailwind CSS 4.x ### 백엔드 (Rust + Tauri) @@ -161,6 +199,10 @@ chore: 빌드 설정 변경 3. **테스트**: 새로운 기능은 테스트 포함 필요 4. **문서화**: API 변경 시 문서 업데이트 필요 5. **CI/CD**: 모든 워크플로우가 성공해야 함 +6. **FSD 아키텍처**: 레이어 간 의존성 규칙 준수 필수 +7. **파일 명명**: kebab-case 사용 (예: `editor-page.tsx`) +8. **Import 경로**: `@` 별칭 사용 권장 +9. **Export 정리**: 각 레이어의 `index.ts`에서 `export * from` 패턴 사용 ## 문제 해결 diff --git a/apps/executeJS/package.json b/apps/executeJS/package.json index d7c3974..ecb9702 100644 --- a/apps/executeJS/package.json +++ b/apps/executeJS/package.json @@ -20,13 +20,22 @@ "clean": "rm -rf dist src-tauri/target" }, "dependencies": { - "@tauri-apps/api": "^2.0.0", - "react": "latest", - "react-dom": "latest" + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/themes": "^3.2.1", + "@tauri-apps/api": "^2.9.0", + "react": "^19.2.0", + "react-dom": "latest", + "react-resizable-panels": "^3.0.6", + "zustand": "^5.0.8" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.16", "@tauri-apps/cli": "latest", "@vitejs/plugin-react": "latest", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.16", "vite": "latest" } } diff --git a/apps/executeJS/postcss.config.js b/apps/executeJS/postcss.config.js new file mode 100644 index 0000000..7738160 --- /dev/null +++ b/apps/executeJS/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + autoprefixer: {}, + }, +}; diff --git a/apps/executeJS/src-tauri/icons/icon.ico b/apps/executeJS/src-tauri/icons/icon.ico index 997f4b8..5b1e857 100644 Binary files a/apps/executeJS/src-tauri/icons/icon.ico and b/apps/executeJS/src-tauri/icons/icon.ico differ diff --git a/apps/executeJS/src-tauri/src/lib.rs b/apps/executeJS/src-tauri/src/lib.rs index 918c455..9bfd6af 100644 --- a/apps/executeJS/src-tauri/src/lib.rs +++ b/apps/executeJS/src-tauri/src/lib.rs @@ -30,6 +30,13 @@ pub fn run() { builder .setup(|app_handle| { // JavaScript 실행기 상태 관리 + #[cfg(debug_assertions)] + { + let window = app_handle.get_webview_window("main").unwrap(); + window.open_devtools(); + window.close_devtools(); + } + app_handle.manage(js_executor::JsExecutorState::default()); // 앱 시작 시 초기화 작업 diff --git a/apps/executeJS/src/App.css b/apps/executeJS/src/App.css deleted file mode 100644 index 5a38204..0000000 --- a/apps/executeJS/src/App.css +++ /dev/null @@ -1,286 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; - } - .logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); - } - .logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; - background-color: #1a1a1a; - color: #ffffff; - min-height: 100vh; -} - -.app-header { - text-align: center; - margin-bottom: 30px; - padding-bottom: 20px; - border-bottom: 2px solid #646cff; - color: #ffffff; -} - -.app-header h1 { - color: #ffffff; - margin-bottom: 10px; -} - -.app-info { - color: #a0a0a0; - font-size: 14px; - margin-top: 10px; -} - -.row { - margin: 20px 0; -} - -.code-section { - text-align: left; - margin: 20px 0; -} - -.code-controls { - margin-bottom: 10px; - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.code-controls button { - padding: 8px 16px; - font-size: 14px; -} - -.code-section textarea { - width: 100%; - font-family: 'Courier New', monospace; - font-size: 14px; - padding: 10px; - border: 1px solid #555; - border-radius: 4px; - resize: vertical; - background-color: #2a2a2a; - color: #ffffff; -} - -.result-section { - text-align: left; - margin: 20px 0; - padding: 15px; - border-radius: 4px; - border-left: 4px solid; - background-color: #2a2a2a; - color: #ffffff; -} - -.result-section.success { - background-color: #1a2a1a; - border-left-color: #10b981; - color: #ffffff; -} - -.result-section.error { - background-color: #2a1a1a; - border-left-color: #ef4444; - color: #ffffff; -} - -.result-info { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - font-size: 12px; -} - -.timestamp { - color: #a0a0a0; -} - -.status { - padding: 2px 8px; - border-radius: 12px; - font-weight: bold; - font-size: 11px; -} - -.status.success { - background-color: #d1fae5; - color: #065f46; -} - -.status.error { - background-color: #fee2e2; - color: #991b1b; -} - -.result-section pre { - white-space: pre-wrap; - word-wrap: break-word; - margin: 0; - font-family: 'Courier New', monospace; - color: #ffffff; - background-color: #1a1a1a; - padding: 10px; - border-radius: 4px; -} - -.history-section { - margin: 30px 0; - text-align: left; -} - -.history-list { - max-height: 400px; - overflow-y: auto; - border: 1px solid #555; - border-radius: 4px; - background-color: #2a2a2a; -} - -.history-item { - padding: 15px; - border-bottom: 1px solid #555; - border-left: 4px solid; - background-color: #2a2a2a; - color: #ffffff; -} - -.history-item:last-child { - border-bottom: none; -} - -.history-item.success { - border-left-color: #10b981; -} - -.history-item.error { - border-left-color: #ef4444; -} - -.history-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - font-size: 12px; -} - -.history-code { - font-family: 'Courier New', monospace; - font-size: 13px; - background-color: #1a1a1a; - color: #ffffff; - padding: 8px; - border-radius: 4px; - margin-bottom: 8px; - white-space: pre-wrap; - word-wrap: break-word; - border: 1px solid #555; -} - -.history-result { - font-family: 'Courier New', monospace; - font-size: 12px; - color: #ffffff; - white-space: pre-wrap; - word-wrap: break-word; - background-color: #1a1a1a; - padding: 8px; - border-radius: 4px; - border: 1px solid #555; -} - -.greet-message { - text-align: center; - font-size: 18px; - color: #646cff; - margin: 20px 0; - padding: 10px; - background-color: #1a1a2a; - border-radius: 4px; - border: 1px solid #555; -} - -button { - border-radius: 8px; - border: 1px solid #555; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #2a2a2a; - color: #ffffff; - cursor: pointer; - transition: all 0.25s; - margin: 5px; -} - -button:hover { - border-color: #646cff; - background-color: #3a3a3a; -} - -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -input { - border-radius: 8px; - border: 1px solid #555; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #2a2a2a; - color: #ffffff; - transition: all 0.25s; - margin: 5px; -} - -input:focus, -input:focus-visible { - outline: 4px auto -webkit-focus-ring-color; - border-color: #646cff; - background-color: #3a3a3a; -} diff --git a/apps/executeJS/src/App.test.tsx b/apps/executeJS/src/App.test.tsx deleted file mode 100644 index ff86616..0000000 --- a/apps/executeJS/src/App.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; -import '@testing-library/jest-dom'; -import App from './App'; - -// Mock Tauri API -vi.mock('@tauri-apps/api/core', () => ({ - invoke: vi.fn(), -})); - -describe('App', () => { - it('renders ExecuteJS title', () => { - render(); - expect(screen.getByText('ExecuteJS')).toBeInTheDocument(); - }); - - it('renders JavaScript code execution section', () => { - render(); - expect(screen.getByText('JavaScript 코드 실행')).toBeInTheDocument(); - }); - - it('renders code input textarea', () => { - render(); - expect( - screen.getByPlaceholderText('JavaScript 코드를 입력하세요...') - ).toBeInTheDocument(); - }); - - it('renders execute button', () => { - render(); - expect(screen.getByText('실행')).toBeInTheDocument(); - }); -}); diff --git a/apps/executeJS/src/App.tsx b/apps/executeJS/src/App.tsx deleted file mode 100644 index 7c4fe8b..0000000 --- a/apps/executeJS/src/App.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useState, useEffect } from 'react'; -import { invoke } from '@tauri-apps/api/core'; -import './App.css'; - -interface JsExecutionResult { - code: string; - result: string; - timestamp: string; - success: boolean; - error?: string; -} - -interface AppInfo { - name: string; - version: string; - description: string; - author: string; -} - -function App() { - const [greetMsg, setGreetMsg] = useState(''); - const [name, setName] = useState(''); - const [jsCode, setJsCode] = useState(''); - const [result, setResult] = useState(null); - const [history, setHistory] = useState([]); - const [appInfo, setAppInfo] = useState(null); - - useEffect(() => { - // 앱 정보 로드 - loadAppInfo(); - // 실행 히스토리 로드 - loadHistory(); - }, []); - - async function loadAppInfo() { - try { - const info = await invoke('get_app_info'); - setAppInfo(info); - } catch (error) { - console.error('앱 정보 로드 실패:', error); - } - } - - async function loadHistory() { - try { - const historyData = await invoke( - 'get_js_execution_history' - ); - setHistory(historyData); - } catch (error) { - console.error('히스토리 로드 실패:', error); - } - } - - async function greet() { - setGreetMsg(await invoke('greet', { name })); - } - - async function executeJS() { - try { - const executionResult = await invoke('execute_js', { - code: jsCode, - }); - setResult(executionResult); - // 히스토리 새로고침 - loadHistory(); - } catch (error) { - setResult({ - code: jsCode, - result: '', - timestamp: new Date().toISOString(), - success: false, - error: error as string, - }); - } - } - - async function clearHistory() { - try { - await invoke('clear_js_execution_history'); - setHistory([]); - } catch (error) { - console.error('히스토리 삭제 실패:', error); - } - } - - async function saveCode() { - const filename = prompt('저장할 파일명을 입력하세요:', 'code.js'); - if (filename) { - try { - const message = await invoke('save_js_code', { - code: jsCode, - filename, - }); - alert(message); - } catch (error) { - alert(`저장 실패: ${error}`); - } - } - } - - async function loadCode() { - const filename = prompt('불러올 파일명을 입력하세요:', 'code.js'); - if (filename) { - try { - const code = await invoke('load_js_code', { filename }); - setJsCode(code); - } catch (error) { - alert(`불러오기 실패: ${error}`); - } - } - } - - return ( -
-
-

ExecuteJS

- {appInfo && ( -
-

- v{appInfo.version} - {appInfo.description} -

-
- )} -
- -
-
- setName(e.currentTarget.value)} - placeholder="이름을 입력하세요..." - /> - -
-
- - {greetMsg &&

{greetMsg}

} - -
-
-

JavaScript 코드 실행

-
- - - - -
-