diff --git a/apps/executeJS/src/widgets/code-editor/code-editor.tsx b/apps/executeJS/src/widgets/code-editor/code-editor.tsx index 7845edc..b0388ab 100644 --- a/apps/executeJS/src/widgets/code-editor/code-editor.tsx +++ b/apps/executeJS/src/widgets/code-editor/code-editor.tsx @@ -1,7 +1,23 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; + import { Editor, EditorProps } from '@monaco-editor/react'; +import type { Options as PrettierOptions } from 'prettier'; +import prettier from 'prettier/standalone'; +import babel from 'prettier/plugins/babel'; +import estree from 'prettier/plugins/estree'; +import typescript from 'prettier/plugins/typescript'; + import type { CodeEditorProps } from '../../shared/types'; +const prettierOptions: PrettierOptions = { + semi: true, + trailingComma: 'es5', + singleQuote: true, + printWidth: 80, + tabWidth: 2, + useTabs: false, +}; + export const CodeEditor: React.FC = ({ value, onChange, @@ -10,18 +26,91 @@ export const CodeEditor: React.FC = ({ theme = 'vs-dark', }) => { const editorRef = useRef(null); + const disposablesRef = useRef>([]); // Monaco Editor 설정 const handleEditorDidMount: EditorProps['onMount'] = (editor, monaco) => { try { editorRef.current = editor; - // Cmd+Enter 키바인딩 추가 + // 이전 등록된 포맷터가 남아있는 경우, 먼저 해제 + if (disposablesRef.current.length > 0) { + disposablesRef.current.forEach((disposable) => { + disposable.dispose(); + }); + disposablesRef.current = []; + } + + // JavaScript 포맷터 등록 + const jsDisposable = + monaco.languages.registerDocumentFormattingEditProvider('javascript', { + async provideDocumentFormattingEdits(model) { + const text = model.getValue(); + + try { + const formatted = await prettier.format(text, { + ...prettierOptions, + parser: 'babel', + plugins: [babel, estree], + }); + + return [ + { + range: model.getFullModelRange(), + text: formatted, + }, + ]; + } catch (error) { + console.error('Prettier formatting error:', error); + return []; + } + }, + }); + + // TypeScript 포맷터 등록 + const tsDisposable = + monaco.languages.registerDocumentFormattingEditProvider('typescript', { + async provideDocumentFormattingEdits(model) { + const text = model.getValue(); + + try { + const formatted = await prettier.format(text, { + ...prettierOptions, + parser: 'typescript', + plugins: [typescript, estree], + }); + + return [ + { + range: model.getFullModelRange(), + text: formatted, + }, + ]; + } catch (error) { + console.error('Prettier formatting error:', error); + return []; + } + }, + }); + + // Disposable들을 ref에 저장 + disposablesRef.current = [jsDisposable, tsDisposable]; + + // 단축키 바인딩 if (monaco && monaco.KeyMod && monaco.KeyCode) { + // Cmd+Enter 코드 실행 editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { const currentValue = editor.getValue(); onExecute?.(currentValue); }); + + // Cmd+Shift+F prettier 포맷 실행 + editor.addCommand( + monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, + () => { + editor.getAction('editor.action.formatDocument')?.run(); + } + ); } // 에디터 포커스 @@ -73,6 +162,17 @@ export const CodeEditor: React.FC = ({ acceptSuggestionOnEnter: 'off' as const, }; + // Cleanup: unmount 시 포맷터 등록 해제 + useEffect(() => { + return () => { + // 모든 disposable 해제 + disposablesRef.current.forEach((disposable) => { + disposable.dispose(); + }); + disposablesRef.current = []; + }; + }, []); + return (