Skip to content

anseonghyeon/code-editor

Repository files navigation

code-editor

웹 기반 코드 에디터입니다. ZIP 파일을 업로드하고, 코드를 수정한 후 수정된 내용을 다운로드할 수 있습니다.

개발기간: 2025.12.17 - 2025.12.21

시연

code-editor 시연

아키텍쳐

graph TB
    subgraph "UI Layer"
        A[AppLayout] --> B[FileToolbar]
        A --> C[FileTree]
        A --> D[Tabs]
        A --> E[FileViewer]
        E --> F[MonacoEditor]
        E --> G[ImageViewer]
    end

    subgraph "State Management"
        H[FileContext]
    end

    subgraph "Utils"
        I[fileUtils]
        J[monacoWorker]
    end

    B -->|업로드/다운로드| H
    C -->|파일 선택| H
    D -->|탭 관리| H
    F -->|파일 편집| H
    F -->|언어 감지| I
    H -->|파일 데이터| C
    H -->|선택된 파일| F
    H -->|열린 탭| D

    style H fill:#e1f5ff
    style F fill:#fff4e1
    style I fill:#f0f0f0
Loading

주요 흐름:

  1. 파일 업로드: FileToolbar → JSZip 파싱 → FileContext 저장
  2. 파일 선택: FileTree 클릭 → FileContext 업데이트 → 탭 추가 → MonacoEditor 렌더링
  3. 파일 편집: MonacoEditor 변경 → editedFiles 상태 업데이트
  4. 파일 다운로드: editedFiles + 원본 파일 병합 → 새 ZIP 생성 → 다운로드

코드 설명

파일 업로드

const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
  const selectedFile = event.target.files?.[0];
  if (!selectedFile) return;

  if (!selectedFile.name.endsWith(".zip")) {
    alert("zip 파일만 업로드 가능합니다");
    return;
  }

  try {
    const zip = new JSZip();
    const parsedZip = await zip.loadAsync(selectedFile);
    setFiles(parsedZip.files);
  } catch (error) {
    console.error("압축 해제 실패:", error);
  } finally {
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  }
};
  1. event.target.files?.[0]로 선택한 파일 중 첫 번째 파일을 가져옵니다.
  2. zip.loadAsync로 업로드된 zip 파일을 파싱하고, 파싱된 파일 객체를 setFiles에 저장합니다.
  3. 마지막으로 fileInputRef.current.value 값을 초기화하여 같은 이름의 zip 파일을 연속으로 올려도 onChange 이벤트가 정상적으로 동작하게 합니다.

파일 트리 렌더링

const TreeNodeItem = ({ node, depth }: TreeNodeItemProps) => {
  const [isOpen, setIsOpen] = useState(false);
  const { openFile } = useFileContext();

  const handleClick = () => {
    if (node.isDirectory) {
      setIsOpen(!isOpen);
    } else {
      openFile(node.path);
    }
  };

  return (
    <>
      <TreeItem depth={depth} onClick={handleClick}>
        {node.isDirectory && (
          <>
            {isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
            <Folder size={16} />
          </>
        )}
        {!node.isDirectory && <File size={16} />}
        {node.name}
      </TreeItem>

      {/* 폴더가 열려있으면 자식들 재귀적으로 렌더링 */}
      {node.isDirectory &&
        isOpen &&
        node.children.map((child) => (
          <TreeNodeItem key={child.path} node={child} depth={depth + 1} />
        ))}
    </>
  );
};
  1. TreeNodeItem함수로 전달된 node의 isDirectory 필드에 따라 알맞은 아이콘을 렌더링한다
  2. 만약 그 노드가 디렉토리이고 열려있다면 그 노드의 자식 노드들을 재귀적으로 depth를 1씩 증가시켜서 렌더링한다
  3. 클릭된 파일의 path는 openFile(node.path)에 저장되어 tabs와 monaco editor에서 사용된다

파일 편집

editor.onDidChangeModelContent(() => {
  const newValue = editor.getValue();
  const currentFile = selectedFileRef.current; 
  if (currentFile && !currentFile.isImage) {
    updateFileContent(currentFile.name, newValue);
  }
});
  1. monaco editor의 content변경이되면 이벤트 리스너 onDidChangeModelContent가 감지하여 editor.getValue()로 content를 읽어오고 수정된 파일명과 새로운 content를 따로 저장해둔다

파일 다운로드

const downloadFiles = async () => {
    if (Object.keys(files).length === 0) {
      alert('업로드된 파일이 없습니다.');
      return;
    }

    const zip = new JSZip();

    for (const [path, file] of Object.entries(files)) {
      if (file.dir) continue;

      if (editedFiles[path]) {
        zip.file(path, editedFiles[path]); 
      } else {
        const content = await file.async('arraybuffer');
        zip.file(path, content); 
      }
    }
  1. 모든 파일을 순회하여 파일 수정단계에 수정된 파일인지 editedFiles[path]로 체크하고 편집된 파일이라면 수정본을, 편집된적이 없는 파일이라면 원본을 zip에 저장한다
  2. 이후 zip.generateAsync로 zip파일을 생성하여 수정본을 다운로드 할수있게된다

기술 스택

분류 기술 스택
빌드 도구 Vite
언어 & 프레임워크 TypeScript React
스타일링 & UI Emotion Lucide
코드 에디터 Monaco Editor
파일 처리 JSZip
코드 품질 ESLint
배포 Vercel

디렉토리 구조

code-editor/
├── public/            # 정적 파일
├── src/
│   ├── components/        # React 컴포넌트
│   │   ├── editor/       # 에디터 관련 컴포넌트
│   │   │   ├── FileViewer.tsx      # 파일 뷰어 (이미지/에디터 분기)
│   │   │   ├── ImageViewer.tsx     # 이미지 뷰어
│   │   │   ├── MonacoEditor.tsx    # Monaco 에디터 래퍼
│   │   │   └── Tabs.tsx            # 탭 UI
│   │   ├── files/        # 파일 관리 컴포넌트
│   │   │   ├── FileToolbar.tsx     # 업로드/다운로드 툴바
│   │   │   └── FileTree.tsx        # 파일 트리 네비게이션
│   │   └── AppLayout.tsx # 전체 레이아웃
│   ├── contexts/         # Context API
│   │   └── FileContext.tsx         # 파일 상태 관리
│   ├── styles/          # 스타일 관련
│   │   ├── GlobalStyles.tsx       # 전역 스타일
│   │   ├── theme.ts              # 테마 설정
│   │   └── emotion.d.ts          # Emotion 타입 정의
│   ├── utils/           # 유틸리티 함수
│   │   ├── fileUtils.ts          # 파일 관련 헬퍼
│   │   └── monacoWorker.ts       # Monaco 워커 설정
│   ├── App.tsx          # 루트 컴포넌트
│   └── main.tsx         # 앱 진입점
├── eslint.config.js     # ESLint 설정
├── tsconfig.json        # TypeScript 설정
├── vite.config.ts       # Vite 설정
└── package.json         # 프로젝트 의존성

git규칙

커밋 메시지

형식: <type>: <subject>

Type 종류:

  • feat: 새로운 기능 추가
  • fix: 버그 수정
  • docs: 문서 수정
  • style: 코드 포맷팅, 세미콜론 누락 등 (코드 변경 없음)
  • refactor: 코드 리팩토링
  • chore: 빌드 작업, 패키지 매니저 설정 등
예시
feat: 업로드 버튼 구현
fix: 파일 업로드 없이 다운로드되는 버그 수정
docs: README 아키텍처 다이어그램 추가
style: 코드 포맷팅 적용
refactor: getLanguage 함수를 fileUtils로 분리
chore: vite 설정 업데이트

브랜치 전략

배포 브랜치: main (Vercel 자동 배포)

형식: <type>/<description>

Type 종류:

  • feat/: 새로운 기능 개발
  • fix/: 버그 수정
  • docs/: 문서 작업
  • style/: 스타일 관련 작업
  • refactor/: 리팩토링
  • chore/: 기타 작업
예시
feat/file-upload
fix/download-button-bug
docs/readme
style/button-component
refactor/utils
chore/setup-eslint

About

코딧 - 웹 코드 에디터

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors