[
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": ".nvmrc",
    "content": "16\n"
  },
  {
    "path": "README.md",
    "content": "![cartoon](public/assets/readme/cartoon.png)\n\n# 😉 [EASYME.md](https://www.easy-me.com/)\n\n![Generic badge](https://img.shields.io/badge/version-0.1.0-green.svg)\n[![Netlify Status](https://api.netlify.com/api/v1/badges/e4b6f69b-eaaf-465e-8a50-218a405dca2f/deploy-status)](https://app.netlify.com/sites/easymemd/deploys)   \n<a href=\"https://www.buymeacoffee.com/oneahan\">\n  <img src=\"public/assets/readme/bmac.webp\" alt=\"Buy Me a Coffee\" width=\"220\" />\n</a>\n\n\n`#README` `#Markdown` `#리드미` `#빠르고쉽게` `#에디터`   \n\nREADME.md를 쉽게 작성하는 방법! **EASYME.md**   \n\n**사이트 바로 가기 👉 [클릭!](https://www.easy-me.com/)**\n\n---\n\n![cover](public/assets/readme/cover.png)\n\nMarkdown 문법, 알고는 있는데.. README.md 작성할 때만 되면 버벅거리는 당신.   \n지금 'Markdown 사용법'이라고 검색하고 계신 거 아니죠? 🤭   \n이젠 더 이상 그럴 필요가 없어요. **EASYME.md를 통해 쉽게 Markdown을 작성할 수 있거든요.**   \n왼쪽 화면에 글을 작성하면 오른쪽 화면에 실시간으로 Markdown이 적용된 글을 확인할 수 있어요. Markdown 문법이 잘 기억나지 않는다고요? 괜찮아요! 🙂 툴바창에 다양한 기능을 적용하면 자동으로 Markdown 문법이 적용되니까요.   \n어때요? 이제 쉽게 README.md를 작성할 수 있겠죠?   \n\n*(🤙 지금 이 글도 EASYME.md를 통해 작성하였답니다)*\n\n<br>\n\n# 📖 Contents\n\n- [😉 EASYME.md](#-easymemd)\n- [📖 Contents](#-contents)\n- [🌈 Background](#-background)\n- [🔗 Link](#-link)\n  - [Github Repositories](#github-repositories)\n- [🔍 Preview](#-preview)\n- [🛠 Features](#-features)\n- [📈 Release Note](#-release-note)\n- [⚠️ Requirement](#️-requirement)\n- [⚙️ Installation](#️-installation)\n  - [Setup](#setup)\n  - [Client](#client)\n  - [Server](#server)\n- [🪃 Skills](#-skills)\n  - [Client](#client-1)\n  - [Server](#server-1)\n  - [Test](#test)\n- [🪛 Project Control](#-project-control)\n- [🚀 Deployment](#-deployment)\n- [🧗 Challenges](#-challenges)\n  - [1. React Quill을 선택, 그리고 그 안에서 도전](#1-react-quill을-선택-그리고-그-안에서-도전)\n    - [1) 에디터에 HTML문법이 자동으로 적용되는 현상](#1-에디터에-html문법이-자동으로-적용되는-현상)\n    - [2) GET 요청을 통해 데이터를 받아올 때 적용이 되지 않는 현상](#2-get-요청을-통해-데이터를-받아올-때-적용이-되지-않는-현상)\n  - [2. React Quill을 걷어내다](#2-react-quill을-걷어내다)\n  - [3. Redo, Undo가 작동하지 않다](#3-redo-undo가-작동하지-않다)\n- [🙏 마무리하며..](#-마무리하며)\n\n<br>\n\n# 🌈 Background\n팀 프로젝트 때, README를 작성하면서 겪었던 불편함을 해소하기 위해 시작한 프로젝트입니다. Markdown 문법은 알고 있지만 자주 쓰지 않기에 그때 그때 찾아야 하는 번거로움이 있습니다. 이와 비슷한 불편함이 다른 개발자들도 분명 있을거라 생각이 되었습니다.\n\nEASYME.md를 통해 조금이라도 개발자들이 README를 작성하는데 겪는 불편함이 해소되길 바라는 마음으로 만들게 되었습니다.\n\n<br>\n\n# 🔗 Link\n\n- [https://www.easy-me.com](https://www.easy-me.com/)\n\n## Github Repositories\n\n- Client: [https://github.com/EASYME-md/client](https://github.com/EASYME-md/client)\n- Server: [https://github.com/EASYME-md/server](https://github.com/EASYME-md/server)\n\n<br>\n\n# 🔍 Preview\n\n![title](public/assets/readme/preview.gif)\n\n<br>\n\n# 🛠 Features\n- Screen\n    - 왼쪽 화면은 직접 텍스트를 작성할 수 있는 Editor입니다.\n    - 오른쪽 화면은 왼쪽 텍스트 작성에 따라 Markdown 문법이 적용된 Preview를 확인할 수 있습니다.\n    - 상단에 Custom Toolbar를 통해 텍스트 Style을 Markdown 문법으로 적용할 수 있습니다.\n\n- Custom Toolbar\n    - 커서 위치, 텍스트 드래그를 기준으로 Markdown 기능이 적용됩니다.\n    - 드래그한 영역을 대소문자로 변형을 해줍니다.\n    - 드래그한 영역을 리스트로 만들어줍니다.\n    - 접기, 목차, 테이블 등의 템플릿을 제공합니다.\n    - Editor 화면만 보기, Markdown 화면만 보기, Full Screen 모드를 제공합니다.\n\n- 공유하기 / 저장하기\n    - 공유하기 아이콘 버튼을 클릭하면 작성한 글이 저장되며 링크가 생성됩니다.\n    - 작성한 글을 저장하고 다른 사람에 공유할 수 있습니다.\n    - 작성 도중 단축키 `Ctrl+S(Command+S)`로 글을 저장할 수도 있습니다.\n\n<br>\n\n# 📈 Release Note\n| version | log |\n| --- | --- |\n| 0.1.0 | 툴바에 텍스트 전체 삭제 기능 추가, Tab Key 기능 추가 |\n| ~ 0.0.1 | 기능 적용시 스크롤 최상단으로 가는 현상 개선, 저장 및 공유 기능 개선 |\n\n<br>\n\n# ⚠️ Requirement\n\n최신 Chrome Browser 사용을 권장합니다.\n\n<br>\n\n# ⚙️ Installation\n\n## Setup\n\n- Local 환경에서 실행하기 위해 아래 사전 준비가 필요합니다.\n    - [MongoDB Address](https://www.mongodb.com/ko-kr/cloud/atlas/efficiency)\n    - [Google Analytics Tracking ID](https://analytics.google.com/analytics/web)\n\n## Client\n\n```\ngit clone https://github.com/EASYME-md/client\ncd client\nnpm install\nnpm start\n```\n\n- root 디렉토리에 `.env` 파일을 생성하고 `<>`에 환경변수를 입력 후 저장해주세요.\n\n```\nREACT_APP_SERVER_URI=https://api.easy-me.com\nREACT_APP_CLIENT_URI=https://easy-me.com\nREACT_APP_TRACKING_ID=<GA Tracking ID>\n```\n\n## Server\n\n```\ngit clone https://github.com/EASYME-md/server\ncd server\nnpm install\nnpm start\n```\n\n- root 디렉토리에 `.env` 파일을 생성하고 `<>`에 환경변수를 입력 후 저장해주세요.\n\n```\nMONGODB_ADDRESS=<mongoDB address>\nCLIENT_URI=https://easy-me.com\n```\n\n<br>\n\n# 🪃 Skills\n\n## Client\n\n- ES2015+\n- React\n- React Router\n- React Helmet\n- Redux Toolkit\n- Redux Saga\n- Google Analytics\n- Emotion\n\n## Server\n\n- ES2015+\n- Node.js\n- Express\n- MongoDB Atlas\n- Mongoose\n\n## Test\n\n- Client: Jest, Testing Library\n- Server: Mocha, Chai, Supertest\n\n<br>\n\n# 🪛 Project Control\n\n- Version Control: Git, Github\n- Task Control: Notion, Figma\n\n<br>\n\n# 🚀 Deployment\n\n- Client: Netlify\n- Server: AWS Elastic Beanstalk\n\n<br>\n\n# 🧗 Challenges\n\n2주 동안 기능 개발을 하면서 겪은 어려움 또는 도전은 아래와 같습니다.\n\n<br>\n\n## 1. React Quill을 선택, 그리고 그 안에서 도전\n\n초기 에디터 구현은 현재 작업이 완료된 `<textarea>`가 아닌 `React Quill`이라는 에디터 라이브러리를 사용했습니다. 그 당시 에디터 라이브러리를 선택할 때 스스로 몇 가지 기준을 두었습니다.\n\nMarkdown을 미리 볼 수 있어야하기 때문에 **Markdown Preview와 연동이 원활해야 할 것**, Tool의 기능들은 직접 구현할 것이기 때문에 **Markdown 문법을 직접 제공하는 에디터는 피할 것(또는 있어도 사용하지 말 것)**, 즉 **Toolbar를 Custom하게 작업할 수 있는 라이브러리를 선택**할 것. 그 외에도 npm trends를 통해 라이브러리의 크기, 이용자 수, 이슈, 업데이트 등을 확인하였고 가장 적합하다고 판단한 `React Quill`을 선택하게 되었습니다.\n\n### 1) 에디터에 HTML문법이 자동으로 적용되는 현상\n\nQuill 에디터에서 텍스트를 작성하면 자동으로 HTML문법이 적용됩니다. 예를 들어 `안녕하세요`라는 글자를 작성하면 에디터에는 보이지 않지만 내부적으로 `<p>안녕하세요</p>` 라고 적용이 되는 것이죠. 이렇게 되면 문제는 Markdown Preview에서 Markdown 문법이 제대로 보이지 않고 HTML Tag가 그대로 보여집니다. 이를 해결하기 위해 텍스트 value를 Quill의 메서드 `getText()`안에 넣어주면 HTML Tag를 제외하고 오직 텍스트만 적용됩니다. 그러나 이렇게 할 경우 또 다른 문제가 발생하였는데요.\n\n`getText()`를 사용할 경우 기존에 잘 작성되던 텍스트가 1글자만 쳐진다는 것이었습니다. 이 문제는 Quill 컴포넌트의 속성 `value`를 `defaultValue`로 변경해주니 해결할 수 있었습니다. 하지만 이때까지만 해도 또 다른 문제가 발생할 것이라고는 상상하지 못했습니다.\n\n### 2) GET 요청을 통해 데이터를 받아올 때 적용이 되지 않는 현상\n\n그렇습니다. 바로 이것이 위와 연결되는 문제입니다. `defaultValue`를 적용할 경우 서버에 저장된 데이터를 받아올 때 해당 데이터가 적용되지 않았습니다. `defaultValue`라는 이름과 걸맞게 기존에 세팅해둔 텍스트 value만 나올 뿐 새로 받아온 텍스트 value는 적용되지 않았던 것입니다. 되돌아가서 `defaultValue`라는 속성을 `value`로 바꾸니 잘 적용이 되었지만 역시 위에서 설명한 것처럼 1글자만 쳐지는 현상이 생겼고 이 대립의 상황에 전전긍긍했습니다.\n\n**아, 상태!** 떠올랐습니다. 텍스트 value는 모두 상태에서 관리하고 공유됩니다. GET 요청으로 받아온 데이터 역시 상태에 저장되고 공유받습니다. 그렇다면 `defaultValue`에 값을 넣기 전에 상태를 확인하고 유무에 따라 값을 제어하면 되지 않을까요? 맞습니다. 처음에 화면이 렌더링 될 때 로딩 상태로 두고 로딩 상태일 때 화면을 보여주지 않기로 합니다. 로딩 상태일 때 GET 요청을 통해 데이터를 받고 요청이 성공 여부에 따라 화면을 보여주기로 했습니다. 결과는 성공적이었습니다.\n\n이로써 문제는 해결할 수 있었고, 유한상태기계라는 개념을 숙지할 수 있었던 계기가 되었습니다.\n\n<br>\n\n## 2. React Quill을 걷어내다\n\nQuill에서 제공하는 기본 메서드와 Custom Toolbar 등으로 작업을 시작한 1주 차에 기능 구현이 거의 마무리가 되었습니다. 생각보다 일찍 기능 구현이 끝났기 때문에 스스로에게도 고민이 생겼습니다. 기능 외의 것들을 더 신경 쓸 수 있는 시간이 확보되었다는 것은 긍정적인 사실이나, **'기술적인 도전과 성장'에 초점을 두었을 때 스스로 이로운 방향은 아닐 수도 있겠다는 생각이 들었습니다.** 그래서 과감하게 Quill을 걷어내고 기본으로 제공하는 `<textarea>`를 사용하기로 했습니다.\n\n결론적으로, `<textarea>`를 사용했을 때 오히려 이로운 점이 많았습니다. 기존에 Quill을 사용했을 때는 Quill에 의존하여 제한적인 부분이 많았습니다. Quill에서는 module과 format을 세팅해야 하는 번거로움이 있었고, module안에 Custom Toolbar의 Event Handling을 구현할 때 각각 Component로 구현하는 것이 아닌 Event Handling 함수를 직접 넣어줘야 했습니다. 그래서 각각의 Tool을 보여주는 Component를 만들고 그 안에 해당 기능을 동작하는 Event Handling 함수를 넣는 것이 아니라 따로 바깥에 만들어주고 export 시켜준 함수를 import 받아서 module에 적용해야만 했습니다. 폴더 구조나 코드가 복잡해진다는 단점도 존재했습니다.\n\n`<textarea>`로 변경한 후에 훨씬 유연해졌습니다. 각 기능과 동작을 Component 안에서 모두 해결할 수 있었습니다. Component도 훨씬 깔끔해졌습니다. Quill 내부에 값들을 `console.log`를 통해 확인해가며 기능을 적용했던 경험이 `<textarea>`에서도 고스란히 도움이 되었습니다. 커서의 위치, 드래그 영역 계산, 드래그가 여러 행일 경우 각 행의 첫 번째 index를 찾는 것 등 `<textarea>`가 Quill에 비해 잘 정제되어 있지는 않았지만 보다 내부 메서드나 value가 방대했고 잘 적용하기만 한다면 제한적인 부분이 덜했기 때문에 `<textarea>`로 이전한 것은 좋은 결정이었다고 생각합니다.\n\n<br>\n\n## 3. Redo, Undo가 작동하지 않다\n\n`<textarea>`를 사용할 경우 큰 문제점이 하나 있었습니다. 바로 redo, undo(`Ctrl/Command+Z` 포함)가 작동되지 않는다는 것입니다. textarea의 current.value 값을 찾아서 직접 변경해주기 때문에 이전 값을 기억하지 못하는 것으로 생각되었습니다.\n\n이에 대해 `document.execCommand()`를 사용하여 해결할 수 있었습니다. 하지만 MDN에 따르면 해당 메서드는 더 이상 사용하지 않으며 권장하지 않는 방법이라고 명시되어 있었습니다. 오랜 시도와 도전 끝에 '변경된 값을 그대로 `return`하는 게 아니라 `return`하기 전에 replace를 해주면 되지 않을까?'라는 아이디어가 떠 올랐고 `replace()`를 사용했지만 끝내 실패했습니다.\n\n해결책을 마련하기 위해 여러 차례 방법을 모색하던중 `text-field-edit`이라는 라이브러리를 알게 되었습니다. 해당 라이브러리는 자체적으로 텍스트를 다른 텍스트로 감싸주거나 insertText 기능 등 Markdown 문법을 적용하기에 훨씬 수월하게 설계되어 있었던 라이브러리였습니다. **하지만 단순히 라이브러리로 쉽게 적용하는 것은 스스로 성장에도 도움이 되지 않을 거라 판단했고,** textarea 안에 요소들을 토대로 직접 구현하는 게 더 의미가 있다고 생각했습니다. 대신에 해당 라이브러리의 `replace` 기능만 활용하기로 했습니다. 초기에 생각했던 아이디어의 가능여부를 확인하고 싶었기 때문입니다. 결국 `replace`를 사용하여 redo, undo가 작동되는 것을 확인할 수 있었습니다.\n\n내 아이디어가 어느 정도 증명되었다는 것에서 뿌듯함을 느낀 것은 사실이지만 라이브러리의 도움 없이 직접 구현해보고 싶었던 마음이었기에 내심 아쉬움도 있습니다. 이 부분은 여기서 끝내지 않고 스스로가 더 성장할 수 있도록 직접 구현에 도전할 예정입니다.\n\n<br>\n\n# 🙏 마무리하며..\n\n\"가능할까? 가능하다!\"   \n\n개발자로 전향을 마음먹기 전, 스타트업 회사에서 마케터로 3년 가까이 있었습니다. 고객들의 불편함을 해소해주는 일들을 옆에서 지켜보고 동참하면서 언젠간 '내가 직접 만든 제품이 고객들의 불편함을 조금이나마 해소해줄 수 있다면 좋겠다'는 생각을 했습니다.   \n\n그 첫 시작이 이 프로젝트라고 생각합니다. 스스로 고객(여기서는 수많은 개발자 중 한 명이겠죠)이 되어 README를 작성할 때 겪었던 불편함을 개선해보고 싶었습니다. 단순히 머릿속에 둥둥 떠다니는 아이디어가 '가능할까?'라는 호기심 어린 물음에서 '가능하다!'라는 마침표를 찍었다는 점에서 의미가 깊습니다.   \n\n이 프로젝트를 시작으로 새로운 마침표들을 하나둘씩 찍어가는 개발자가 되고 싶습니다 🙂   \n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {\n  'presets': [\n    '@babel/preset-env',\n    [\n      '@babel/preset-react',\n      {\n        'runtime': 'automatic',\n      }\n    ]\n  ],\n};\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  moduleFileExtensions: ['js', 'json', 'jsx'],\n  transform: {\n    '^.+\\\\.(js|jsx)?$': 'babel-jest',\n  },\n  moduleNameMapper: {\n    '^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',\n  },\n  testMatch: [\n    '<rootDir>/**/*.test.(js|jsx)'\n  ],\n  transformIgnorePatterns: [\n    '<rootDir>/node_modules/(?!text-field-edit)'\n  ],\n  'setupFilesAfterEnv': ['<rootDir>/src/setupTests.js'],\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"easyme\",\n  \"homepage\": \"https://www.easy-me.com\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@emotion/react\": \"^11.4.1\",\n    \"@emotion/styled\": \"^11.3.0\",\n    \"@reduxjs/toolkit\": \"^1.6.1\",\n    \"@uiw/react-markdown-preview\": \"^3.3.3\",\n    \"lz-string\": \"^1.5.0\",\n    \"nanoid\": \"^3.1.28\",\n    \"prop-types\": \"^15.7.2\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-ga\": \"^3.3.0\",\n    \"react-helmet-async\": \"^1.1.2\",\n    \"react-icons\": \"^4.2.0\",\n    \"react-redux\": \"^7.2.5\",\n    \"react-router-dom\": \"^5.3.0\",\n    \"react-scripts\": \"4.0.3\",\n    \"redux-saga\": \"^1.1.3\",\n    \"text-field-edit\": \"^3.1.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/polyfill\": \"^7.12.1\",\n    \"@babel/preset-env\": \"^7.15.8\",\n    \"@babel/preset-react\": \"^7.14.5\",\n    \"@testing-library/jest-dom\": \"^5.11.4\",\n    \"@testing-library/react\": \"^11.1.0\",\n    \"@testing-library/user-event\": \"^12.1.10\",\n    \"jest-environment-jsdom\": \"^27.2.5\",\n    \"jest-transform-stub\": \"^2.0.0\",\n    \"redux-logger\": \"^3.0.6\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"jest --watchAll\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "public/_redirects",
    "content": "/.well-known/*  /.well-known/:splat  200!\n/*              /index.html          200\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"ko\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"theme-color\" content=\"#2E3341\" />\n\n    <title>EASYME.md | 누구나 쉬운 README·리드미 온라인 마크다운 실시간 미리보기 에디터</title>\n    <meta name=\"description\" content=\"EASYME.md(이지미)는 README와 Markdown 문법에 익숙하지 않아도 편하게 글을 작성할 수 있는 웹 에디터입니다. 툴바로 서식 적용, 실시간 미리보기, 링크 한 번으로 공유까지 지원합니다.\" />\n    <meta name=\"keywords\" content=\"easyme, readme, 리드미, 이지미, markdown, markdownsite, 마크다운, 마크다운작성사이트, 마크다운에디터, 실시간 미리보기\" />\n    <meta name=\"robots\" content=\"index, follow\" />\n    <meta name=\"google-site-verification\" content=\"sioQswZ6K0vzDLaveDWBachAp5y9pCFuv_2widqDXc4\" />\n    <meta name=\"naver-site-verification\" content=\"59a8659d9d6fce0d75c4e70921609fad75cd54fa\" />\n\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <link rel=\"canonical\" href=\"https://www.easy-me.com/\" />\n\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:locale\" content=\"ko_KR\" />\n    <meta property=\"og:site_name\" content=\"EASYME.md\" />\n    <meta property=\"og:title\" content=\"EASYME.md | 누구나 쉬운 README·리드미 온라인 마크다운 실시간 미리보기 에디터\" />\n    <meta property=\"og:description\" content=\"EASYME.md(이지미)는 README와 Markdown 문법에 익숙하지 않아도 편하게 글을 작성할 수 있는 웹 에디터입니다. 툴바로 서식 적용, 실시간 미리보기, 링크 한 번으로 공유까지 지원합니다.\" />\n    <meta property=\"og:url\" content=\"https://www.easy-me.com/\" />\n    <meta property=\"og:image\" content=\"https://www.easy-me.com/preview.png\" />\n    <meta property=\"og:image:width\" content=\"1200\" />\n    <meta property=\"og:image:height\" content=\"630\" />\n\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content=\"EASYME.md | 누구나 쉬운 README·리드미 온라인 마크다운 실시간 미리보기 에디터\" />\n    <meta name=\"twitter:description\" content=\"EASYME.md(이지미)는 README와 Markdown 문법에 익숙하지 않아도 편하게 글을 작성할 수 있는 웹 에디터입니다. 툴바로 서식 적용, 실시간 미리보기, 링크 한 번으로 공유까지 지원합니다.\" />\n    <meta name=\"twitter:image\" content=\"https://www.easy-me.com/preview.png\" />\n  </head>\n  <body>\n    <noscript>이 사이트를 이용하려면 JavaScript를 활성화해주세요.</noscript>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "public/robots.txt",
    "content": "Sitemap: https://www.easy-me.com/sitemap.xml\nUser-agent: *\nAllow: /\n"
  },
  {
    "path": "public/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset\n      xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n      xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n      xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9\n            http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">\n\n<url>\n  <loc>https://www.easy-me.com</loc>\n  <lastmod>2026-04-15</lastmod>\n  <changefreq>weekly</changefreq>\n  <priority>1.0</priority>\n</url>\n<url>\n  <loc>https://www.easy-me.com/d</loc>\n  <lastmod>2026-04-15</lastmod>\n  <changefreq>weekly</changefreq>\n  <priority>0.8</priority>\n</url>\n\n</urlset>\n"
  },
  {
    "path": "src/App.js",
    "content": "import React from 'react';\nimport ReactGA from 'react-ga';\nimport { createBrowserHistory } from 'history';\nimport { Router, Route, Redirect, Switch } from 'react-router-dom';\n\nimport Home from './components/Home';\nimport ErrorPage from './components/shared/ErrorPage';\nimport ReactHelmet from './components/ReactHelmet';\n\nconst App = () => {\n  const TRACKING_ID = process.env.REACT_APP_TRACKING_ID;\n  const history = createBrowserHistory();\n  const pathName = window.location.pathname;\n\n  ReactGA.initialize(TRACKING_ID);\n\n  history.listen(() => {\n    ReactGA.set({ page: pathName });\n    ReactGA.pageview(pathName);\n  });\n\n  return (\n    <Router history={history}>\n      <ReactHelmet />\n      <Switch>\n        <Route exact path='/d' component={Home} />\n        <Route path='/d/:linkId' component={Home} />\n        <Route>\n          <ErrorPage message='404 Not Found' />\n        </Route>\n      </Switch>\n      <Route exact path='/'>\n        <Redirect to={{ pathname: '/d', hash: window.location.hash }} />\n      </Route>\n    </Router>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "src/api/index.js",
    "content": "const SERVER_URI = process.env.REACT_APP_SERVER_URI;\n\nexport const fetchContents = async (linkId) => {\n  try {\n    if (!linkId) {\n      return;\n    }\n\n    const response = await fetch(`${SERVER_URI}/d/${linkId}`, {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n        Accept: 'application/json',\n      },\n    });\n\n    const { message, code, text } = await response.json();\n\n    if (message === 'NOT_FOUND') {\n      throw new Error(`${code} Not Found`);\n    }\n\n    if (message === 'OK') {\n      return text;\n    }\n  } catch (err) {\n    throw err.message;\n  }\n};\n\nexport const saveContents = async (linkId, text) => {\n  try {\n    const response = await fetch(`${SERVER_URI}/d/${linkId}`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        Accept: 'application/json',\n      },\n      body: JSON.stringify({ linkId, text }),\n    });\n\n    const { code, message } = await response.json();\n\n    if (message !== 'OK') {\n      throw Error(`${code} Internal Server Error`);\n    }\n\n  } catch (err) {\n    throw err.message;\n  }\n};\n"
  },
  {
    "path": "src/assets/fonts/font.css",
    "content": "@font-face {\n  font-family: 'Rubik';\n  font-display: fallback;\n\n  src: local('Rubik'),\n      url('./Rubik-ExtraBold.eot?iefix') format('embedded-opentype'),\n      url('./Rubik-ExtraBold.woff2') format('woff2'),\n      url('./Rubik-ExtraBold.woff') format('woff'),\n      url('./Rubik-ExtraBold.ttf') format('truetype');\n\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Noto Sans KR';\n  font-display: fallback;\n\n  src: local('Noto Sans KR'),\n      url('./NotoSansKR-Regular.eot?iefix') format('embedded-opentype'),\n      url('./NotoSansKR-Regular.woff2') format('woff2'),\n      url('./NotoSansKR-Regular.woff') format('woff'),\n      url('./NotoSansKR-Regular.ttf') format('truetype');\n\n  font-style: normal;\n}\n"
  },
  {
    "path": "src/components/CustomToolbar.js",
    "content": "import React from 'react';\nimport styled from '@emotion/styled';\n\nimport CustomUndo from './CustomTools/CustomUndo';\nimport CustomRedo from './CustomTools/CustomRedo';\nimport CustomHeader from './CustomTools/CustomHeader';\nimport CustomBold from './CustomTools/CustomBold';\nimport CustomItalic from './CustomTools/CustomItalic';\nimport CustomStrikethrough from './CustomTools/CustomStrikethrough';\nimport CustomUnderline from './CustomTools/CustomUnderline';\nimport CustomHorizontalRule from './CustomTools/CustomHorizontalRule';\nimport CustomBlockQuote from './CustomTools/CustomBlockQuote';\nimport CustomFold from './CustomTools/CustomFold';\nimport CustomFirstLetterUppercase from './CustomTools/CustomFirstLetterUppercase';\nimport CustomUppercase from './CustomTools/CustomUppercase';\nimport CustomLowercase from './CustomTools/CustomLowercase';\nimport CustomContents from './CustomTools/CustomContents';\nimport CustomUnOrderedList from './CustomTools/CustomUnOrderedList';\nimport CustomOrderedList from './CustomTools/CustomOrderedList';\nimport CustomLink from './CustomTools/CustomLink';\nimport CustomImageLink from './CustomTools/CustomImageLink';\nimport CustomCodeInline from './CustomTools/CustomCodeInline';\nimport CustomCodeBlock from './CustomTools/CustomCodeBlock';\nimport CustomTable from './CustomTools/CustomTable';\nimport CustomEditorView from './CustomTools/CustomEditorView';\nimport CustomMarkdownView from './CustomTools/CustomMarkdownView';\nimport CustomFullScreen from './CustomTools/CustomFullScreen';\nimport CustomAllTextRemoval from './CustomTools/CustomAllTextRemoval';\nimport CustomShare from './CustomTools/CustomShare';\nimport SectionLine from './shared/SectionLine';\n\nconst CustomToolbar = () => {\n  return (\n    <ToolbarWrapper>\n      <CustomRedo />\n      <CustomUndo />\n      <CustomAllTextRemoval />\n      <SectionLine />\n      <CustomHeader />\n      <SectionLine />\n      <CustomBold />\n      <CustomItalic />\n      <CustomStrikethrough />\n      <CustomUnderline />\n      <CustomHorizontalRule />\n      <CustomBlockQuote />\n      <CustomFold />\n      <SectionLine />\n      <CustomFirstLetterUppercase />\n      <CustomUppercase />\n      <CustomLowercase />\n      <SectionLine />\n      <CustomContents />\n      <CustomUnOrderedList />\n      <CustomOrderedList />\n      <CustomLink />\n      <CustomImageLink />\n      <CustomCodeInline />\n      <CustomCodeBlock />\n      <CustomTable />\n      <SectionLine />\n      <CustomEditorView />\n      <CustomMarkdownView />\n      <CustomFullScreen />\n      <SectionLine />\n      <CustomShare />\n    </ToolbarWrapper>\n  );\n};\n\nconst ToolbarWrapper = styled.div`\n  button {\n    cursor: pointer;\n    vertical-align: middle;\n    border: none;\n    background: none;\n  }\n\n  svg {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    font-size: 14px;\n  }\n\n  select {\n    font-weight: 600;\n    font-family: inherit;\n    padding: 3px;\n    width: 120px;\n    border: 1px solid #dddddd;\n\n    :focus {\n      outline: none;\n    }\n  }\n`;\n\nexport default CustomToolbar;\n"
  },
  {
    "path": "src/components/CustomTools/CustomAllTextRemoval.js",
    "content": "import React from 'react';\nimport { FaEraser } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport { replace } from 'text-field-edit';\n\nconst CustomAllTextRemoval = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const emptyText = '';\n\n    replace(textArea, textArea.value, emptyText);\n    textArea.focus();\n    dispatch(addText(emptyText));\n  };\n\n  return (\n    <button title='Remove all text' onClick={handleButton}>\n      <FaEraser />\n    </button>\n  );\n};\n\nexport default CustomAllTextRemoval;\n"
  },
  {
    "path": "src/components/CustomTools/CustomBlockQuote.js",
    "content": "import React from 'react';\nimport { FaQuoteLeft } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentRow from '../../utils/addTypeCurrentRow';\n\nconst CustomBlockQuote = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeCurrentRow(textArea, '>');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Block quote' onClick={handleButton}>\n      <FaQuoteLeft />\n    </button>\n  );\n};\n\nexport default CustomBlockQuote;\n"
  },
  {
    "path": "src/components/CustomTools/CustomBold.js",
    "content": "import React from 'react';\nimport { FaBold } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomBold = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '**');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Bold' onClick={handleButton}>\n      <FaBold />\n    </button>\n  );\n};\n\nexport default CustomBold;\n"
  },
  {
    "path": "src/components/CustomTools/CustomCodeBlock.js",
    "content": "import React from 'react';\nimport { GoFileCode } from 'react-icons/go';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomCodeBlock = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '\\n```\\n', '\\n```\\n');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Code block' onClick={handleButton}>\n      <GoFileCode />\n    </button>\n  );\n};\n\nexport default CustomCodeBlock;\n"
  },
  {
    "path": "src/components/CustomTools/CustomCodeInline.js",
    "content": "import React from 'react';\nimport { FaCode } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomCodeInline = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '`', '`');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Code inline' onClick={handleButton}>\n      <FaCode />\n    </button>\n  );\n};\n\nexport default CustomCodeInline;\n"
  },
  {
    "path": "src/components/CustomTools/CustomContents.js",
    "content": "import React from 'react';\nimport { CgListTree } from 'react-icons/cg';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentPosition from '../../utils/addTypeCurrentPosition';\n\nconst CustomContents = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeCurrentPosition(textArea, '1. [title1](#write-title-here!)   \\n2. [title2](#only-lowercase)   \\n3. [title3](#use\"-\"instead-of-spacing-words)   \\n4. [title4](#example)   \\n    - [❓ EASYME.md가 뭐예요?](#-easymemd가-뭐예요)   \\n    - [🛠 기능 엿보기](#-기능-엿보기)');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Table of contents' onClick={handleButton}>\n      <CgListTree />\n    </button>\n  );\n};\n\nexport default CustomContents;\n"
  },
  {
    "path": "src/components/CustomTools/CustomEditorView.js",
    "content": "import React from 'react';\nimport { MdSpeakerNotes, MdSpeakerNotesOff } from 'react-icons/md';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { toggleEditor } from '../../features/slice';\n\nconst CustomEditorView = () => {\n  const dispatch = useDispatch();\n  const { fullEditor } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    dispatch(toggleEditor());\n  };\n\n  return (\n    <button title='Only editor preview' onClick={handleButton}>\n      {fullEditor\n        ? <MdSpeakerNotesOff />\n        : <MdSpeakerNotes />}\n    </button>\n  );\n};\n\nexport default CustomEditorView;\n"
  },
  {
    "path": "src/components/CustomTools/CustomFirstLetterUppercase.js",
    "content": "import React from 'react';\nimport { IoText } from 'react-icons/io5';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\nimport { insert } from 'text-field-edit';\n\nimport { addText } from '../../features/slice';\n\nconst CustomFirstLetterUppercase = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const startPosition = textArea.selectionStart;\n    const endPosition = textArea.selectionEnd;\n    if (startPosition === endPosition) return;\n\n    const scroll = textArea.scrollTop;\n    const draggedText = textArea.value.substring(startPosition, endPosition);\n    const firstLetter = draggedText.substring(0, 1).toUpperCase();\n    const restLetters = draggedText.substring(1).toLowerCase();\n    const transformed = firstLetter + restLetters;\n\n    textArea.focus();\n    insert(textArea, transformed);\n    textArea.setSelectionRange(startPosition, endPosition);\n    textArea.scrollTop = scroll;\n\n    dispatch(addText(textArea.value));\n  };\n\n  return (\n    <button\n      title='Convert first letter to uppercase'\n      onClick={handleButton}\n    >\n      <IoText />\n    </button>\n  );\n};\n\nexport default CustomFirstLetterUppercase;\n"
  },
  {
    "path": "src/components/CustomTools/CustomFold.js",
    "content": "import React from 'react';\nimport { AiOutlineCaretRight } from 'react-icons/ai';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomFold = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '\\n<details><summary>', '\\n</summary>\\n\\n*Write here!*\\n</details>\\n');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Fold' onClick={handleButton}>\n      <AiOutlineCaretRight />\n    </button>\n  );\n};\n\nexport default CustomFold;\n"
  },
  {
    "path": "src/components/CustomTools/CustomFullScreen.js",
    "content": "import React from 'react';\nimport { BsArrowsFullscreen, BsFullscreenExit } from 'react-icons/bs';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { toggleFullScreen } from '../../features/slice';\n\nconst CustomFullScreen = () => {\n  const dispatch = useDispatch();\n  const { fullScreen } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    dispatch(toggleFullScreen());\n  };\n\n  return (\n    <button title='Full screen' onClick={handleButton}>\n      {fullScreen\n        ? <BsFullscreenExit />\n        : <BsArrowsFullscreen />}\n    </button>\n  );\n};\n\nexport default CustomFullScreen;\n"
  },
  {
    "path": "src/components/CustomTools/CustomHeader.js",
    "content": "import React from 'react';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentRow from '../../utils/addTypeCurrentRow';\n\nconst CustomHeader = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = (e) => {\n    const targetValue = e.target.value;\n    let header = '';\n\n    for (let i = 0; i < targetValue; i++) {\n      header += '#';\n    }\n\n    header += ' ';\n\n    const resultValue = addTypeCurrentRow(textArea, header);\n\n    dispatch(addText(resultValue));\n    e.target.value = 0;\n  };\n\n  return (\n    <select onChange={handleButton}>\n      <option value={0}>Select Header</option>\n      <option value={1}># Heading</option>\n      <option value={2}>## Heading</option>\n      <option value={3}>### Heading</option>\n      <option value={4}>#### Heading</option>\n      <option value={5}>##### Heading</option>\n      <option value={6}>###### Heading</option>\n    </select>\n  );\n};\n\nexport default CustomHeader;\n"
  },
  {
    "path": "src/components/CustomTools/CustomHorizontalRule.js",
    "content": "import React from 'react';\nimport { AiOutlineLine } from 'react-icons/ai';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentPosition from '../../utils/addTypeCurrentPosition';\n\nconst CustomHorizontalRule = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeCurrentPosition(textArea, '\\n---\\n');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Horizontal rule' onClick={handleButton}>\n      <AiOutlineLine />\n    </button>\n  );\n};\n\nexport default CustomHorizontalRule;\n"
  },
  {
    "path": "src/components/CustomTools/CustomImageLink.js",
    "content": "import React from 'react';\nimport { FaRegImage } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentPosition from '../../utils/addTypeCurrentPosition';\n\nconst CustomImageLink = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const link = prompt('Enter the Image URL', 'http://');\n\n    if (link) {\n      const resultValue = addTypeCurrentPosition(textArea, `![title](${link})   \\n`);\n\n      dispatch(addText(resultValue));\n    }\n  };\n\n  return (\n    <button title='image' onClick={handleButton}>\n      <FaRegImage />\n    </button>\n  );\n};\n\nexport default CustomImageLink;\n"
  },
  {
    "path": "src/components/CustomTools/CustomItalic.js",
    "content": "import React from 'react';\nimport { FaItalic } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomItalic = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '*');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Italic' onClick={handleButton}>\n      <FaItalic />\n    </button>\n  );\n};\n\nexport default CustomItalic;\n"
  },
  {
    "path": "src/components/CustomTools/CustomLink.js",
    "content": "import React from 'react';\nimport { FaLink } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentPosition from '../../utils/addTypeCurrentPosition';\n\nconst CustomLink = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const link = prompt('Enter the URL', 'http://');\n\n    if (link) {\n      const resultValue = addTypeCurrentPosition(textArea, `[title](${link})   \\n`);\n\n      dispatch(addText(resultValue));\n    }\n  };\n\n  return (\n    <button title='Link' onClick={handleButton}>\n      <FaLink />\n    </button>\n  );\n};\n\nexport default CustomLink;\n"
  },
  {
    "path": "src/components/CustomTools/CustomLowercase.js",
    "content": "import React from 'react';\nimport { FaAmilia } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\nimport { insert } from 'text-field-edit';\n\nimport { addText } from '../../features/slice';\n\nconst CustomLowercase = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const startPosition = textArea.selectionStart;\n    const endPosition = textArea.selectionEnd;\n    if (startPosition === endPosition) return;\n\n    const scroll = textArea.scrollTop;\n    const draggedText = textArea.value.substring(startPosition, endPosition).toLowerCase();\n\n    textArea.focus();\n    insert(textArea, draggedText);\n    textArea.setSelectionRange(startPosition, endPosition);\n    textArea.scrollTop = scroll;\n\n    dispatch(addText(textArea.value));\n  };\n\n  return (\n    <button title='Lowercase' onClick={handleButton}>\n      <FaAmilia />\n    </button>\n  );\n};\n\nexport default CustomLowercase;\n"
  },
  {
    "path": "src/components/CustomTools/CustomMarkdownView.js",
    "content": "import React from 'react';\nimport { BsEyeFill, BsFillEyeSlashFill } from 'react-icons/bs';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { toggleMarkdown } from '../../features/slice';\n\nconst CustomMarkdownView = () => {\n  const dispatch = useDispatch();\n  const { fullMarkdown } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    dispatch(toggleMarkdown());\n  };\n\n  return (\n    <button title='Only markdown preview' onClick={handleButton}>\n      {fullMarkdown\n        ? <BsFillEyeSlashFill />\n        : <BsEyeFill />}\n    </button>\n  );\n};\n\nexport default CustomMarkdownView;\n"
  },
  {
    "path": "src/components/CustomTools/CustomOrderedList.js",
    "content": "import React from 'react';\nimport { FaListOl } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeDraggedRows from '../../utils/addTypeDraggedRows';\n\nconst CustomOrderedList = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeDraggedRows(textArea, false);\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Ordered list' onClick={handleButton}>\n      <FaListOl />\n    </button>\n  );\n};\n\nexport default CustomOrderedList;\n"
  },
  {
    "path": "src/components/CustomTools/CustomRedo.js",
    "content": "import React from 'react';\nimport { FaRedoAlt } from 'react-icons/fa';\n\nconst CustomRedo = () => {\n  const handleButton = () => {\n    document.execCommand('redo');\n  };\n\n  return (\n    <button title='Redo' onClick={handleButton}>\n      <FaRedoAlt />\n    </button>\n  );\n};\n\nexport default CustomRedo;\n"
  },
  {
    "path": "src/components/CustomTools/CustomShare.js",
    "content": "import React from 'react';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\nimport { FaShareSquare } from 'react-icons/fa';\n\nimport shareDocument from '../../utils/shareDocument';\n\nconst CustomShare = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => shareDocument(textArea, dispatch);\n\n  return (\n    <button title='Share link (Cmd/Ctrl+S)' onClick={handleButton}>\n      <FaShareSquare />\n    </button>\n  );\n};\n\nexport default CustomShare;\n"
  },
  {
    "path": "src/components/CustomTools/CustomStrikethrough.js",
    "content": "import React from 'react';\nimport { FaStrikethrough } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomStrikethrough = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '<s>', '</s>');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Strikethrough' onClick={handleButton}>\n      <FaStrikethrough />\n    </button>\n  );\n};\n\nexport default CustomStrikethrough;\n"
  },
  {
    "path": "src/components/CustomTools/CustomTable.js",
    "content": "import React from 'react';\nimport { FaTable } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeCurrentPosition from '../../utils/addTypeCurrentPosition';\n\nconst CustomTable = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeCurrentPosition(textArea, '\\n\\n| title1 | title2 | title3 |\\n| --- | --- | --- |\\n| 1 | 2 | 3 |\\n| 4 | 5 | 6 |\\n| 7 | 8 | 9 |\\n');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Table' onClick={handleButton}>\n      <FaTable />\n    </button>\n  );\n};\n\nexport default CustomTable;\n"
  },
  {
    "path": "src/components/CustomTools/CustomUnOrderedList.js",
    "content": "import React from 'react';\nimport { FaListUl } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeDraggedRows from '../../utils/addTypeDraggedRows';\n\nconst CustomUnOrderedList = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeDraggedRows(textArea, '- ');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Unordered list' onClick={handleButton}>\n      <FaListUl />\n    </button>\n  );\n};\n\nexport function handleUnOrderedList() {\n  if (this.quill.getSelection()) {\n    const cursorPosition = this.quill.getSelection().index;\n    const lastCursorPosition = this.quill.selection.getRange()[0].length;\n    const offset = this.quill.selection.getRange()[1].start.offset;\n    const draggedRow = this.quill.getLines(cursorPosition, lastCursorPosition + 1);\n    const draggedRowLength = draggedRow.length;\n    let startingPosition = cursorPosition - offset;\n\n    for (let i = 0; i < draggedRowLength; i++) {\n      if (i === 0) {\n        this.quill.insertText(startingPosition, '- ');\n        continue;\n      }\n\n      const nextRow = draggedRow[i - 1].cache.length;\n      startingPosition += nextRow;\n\n      this.quill.insertText(startingPosition, '- ');\n    }\n  }\n};\n\nexport default CustomUnOrderedList;\n"
  },
  {
    "path": "src/components/CustomTools/CustomUnderline.js",
    "content": "import React from 'react';\nimport { FaUnderline } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText } from '../../features/slice';\nimport addTypeBeforeAndAfter from '../../utils/addTypeBeforeAndAfter';\n\nconst CustomUnderline = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const resultValue = addTypeBeforeAndAfter(textArea, '<u>', '</u>');\n\n    dispatch(addText(resultValue));\n  };\n\n  return (\n    <button title='Underline' onClick={handleButton}>\n      <FaUnderline />\n    </button>\n  );\n};\n\nexport default CustomUnderline;\n"
  },
  {
    "path": "src/components/CustomTools/CustomUndo.js",
    "content": "import React from 'react';\nimport { FaUndoAlt } from 'react-icons/fa';\n\nconst CustomUndo = () => {\n  const handleButton = () => {\n    document.execCommand('undo');\n  };\n\n  return (\n    <button title='Undo' onClick={handleButton}>\n      <FaUndoAlt />\n    </button>\n  );\n};\n\nexport default CustomUndo;\n"
  },
  {
    "path": "src/components/CustomTools/CustomUppercase.js",
    "content": "import React from 'react';\nimport { FaFont } from 'react-icons/fa';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\nimport { insert } from 'text-field-edit';\n\nimport { addText } from '../../features/slice';\n\nconst CustomUppercase = () => {\n  const dispatch = useDispatch();\n  const { textArea } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleButton = () => {\n    const startPosition = textArea.selectionStart;\n    const endPosition = textArea.selectionEnd;\n    if (startPosition === endPosition) return;\n\n    const scroll = textArea.scrollTop;\n    const draggedText = textArea.value.substring(startPosition, endPosition).toUpperCase();\n\n    textArea.focus();\n    insert(textArea, draggedText);\n    textArea.setSelectionRange(startPosition, endPosition);\n    textArea.scrollTop = scroll;\n\n    dispatch(addText(textArea.value));\n  };\n\n  return (\n    <button title='Uppercase' onClick={handleButton}>\n      <FaFont />\n    </button>\n  );\n};\n\nexport default CustomUppercase;\n"
  },
  {
    "path": "src/components/Editor.js",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport styled from '@emotion/styled';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport { addText, addTextArea } from '../features/slice';\nimport addTypeBeforeAndAfter from '../utils/addTypeBeforeAndAfter';\nimport addTypeCurrentPosition from '../utils/addTypeCurrentPosition';\nimport shareDocument from '../utils/shareDocument';\nimport { saveDraft } from '../utils/draftStorage';\nimport { keyType } from '../constants';\n\nconst DISPATCH_DEBOUNCE_MS = 150;\n\nconst Editor = () => {\n  const inputText = useRef();\n  const dispatch = useDispatch();\n  const { text, fullEditor, fullMarkdown } = useSelector((state) => state.contents, shallowEqual);\n\n  const [localText, setLocalText] = useState(text);\n  const debounceTimer = useRef();\n\n  useEffect(() => {\n    dispatch(addTextArea(inputText.current));\n  }, [dispatch]);\n\n  useEffect(() => {\n    setLocalText((prev) => (prev === text ? prev : text));\n  }, [text]);\n\n  useEffect(() => () => clearTimeout(debounceTimer.current), []);\n\n  const flushDispatch = (value) => {\n    clearTimeout(debounceTimer.current);\n    dispatch(addText(value));\n    saveDraft(value);\n  };\n\n  const onChangeText = (e) => {\n    const value = e.target.value;\n    setLocalText(value);\n    clearTimeout(debounceTimer.current);\n    debounceTimer.current = setTimeout(() => {\n      dispatch(addText(value));\n      saveDraft(value);\n    }, DISPATCH_DEBOUNCE_MS);\n  };\n\n  const handleKeyDown = async (e) => {\n    const isMetaKey = e.metaKey || e.ctrlKey;\n\n    if (e.keyCode === keyType.TAB) {\n      e.preventDefault();\n      const resultValue = addTypeCurrentPosition(inputText.current, '  ');\n\n      setLocalText(resultValue);\n      flushDispatch(resultValue);\n    }\n\n    if (e.keyCode === keyType.S && isMetaKey) {\n      e.preventDefault();\n      shareDocument(inputText.current, dispatch);\n    }\n\n    if (e.keyCode === keyType.B && isMetaKey) {\n      const resultValue = addTypeBeforeAndAfter(inputText.current, '**');\n\n      setLocalText(resultValue);\n      flushDispatch(resultValue);\n    }\n\n    if (e.keyCode === keyType.I && isMetaKey) {\n      const resultValue = addTypeBeforeAndAfter(inputText.current, '*');\n\n      setLocalText(resultValue);\n      flushDispatch(resultValue);\n    }\n\n    if (e.keyCode === keyType.D && isMetaKey) {\n      e.preventDefault();\n      const resultValue = addTypeBeforeAndAfter(inputText.current, '<s>', '</s>');\n\n      setLocalText(resultValue);\n      flushDispatch(resultValue);\n    }\n\n    if (e.keyCode === keyType.U && isMetaKey) {\n      e.preventDefault();\n      const resultValue = addTypeBeforeAndAfter(inputText.current, '<u>', '</u>');\n\n      setLocalText(resultValue);\n      flushDispatch(resultValue);\n    }\n  };\n\n  const handleClassName = () => {\n    if (fullEditor) {\n      return 'full-editor';\n    }\n\n    if (fullMarkdown) {\n      return 'none-editor';\n    }\n\n    return '';\n  };\n\n  return (\n    <TextArea\n      id='textarea'\n      className={handleClassName()}\n      value={localText}\n      onChange={onChangeText}\n      onKeyDown={handleKeyDown}\n      ref={inputText}\n      spellCheck='false'\n    />\n  );\n};\n\nconst TextArea = styled.textarea`\n  resize: none;\n  box-sizing: border-box;\n  font-family: 'Noto Sans KR';\n  font-size: 16px;\n  padding: 20px;\n  border: none;\n  border-right: 1px solid #dddddd;\n  width: 50%;\n\n  :focus {\n    outline: none;\n  }\n\n  ${({ className }) => {\n    if (className === 'full-editor') {\n      return `\n        width: 100% !important;\n      `;\n    }\n\n    if (className === 'none-editor') {\n      return `\n        display: none;\n      `;\n    }\n  }};\n\n  @media (max-width: 768px) {\n    width: 100%;\n    height: 50%;\n    border-right: none;\n    border-bottom: 1px solid #dddddd;\n    padding: 12px;\n    font-size: 15px;\n  }\n`;\n\nexport default Editor;\n"
  },
  {
    "path": "src/components/Home.js",
    "content": "import React, { useEffect } from 'react';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\nimport { useLocation } from 'react-router-dom';\n\nimport Title from './Title';\nimport TextScreen from './TextScreen';\nimport ErrorPage from './shared/ErrorPage';\nimport Loading from './Loading';\nimport { load, loadSuccess, resetError } from '../features/slice';\nimport { parseShareHash } from '../utils/urlShare';\nimport { loadDraft } from '../utils/draftStorage';\n\nconst Home = () => {\n  const dispatch = useDispatch();\n  const { pathname } = useLocation();\n  const { isLoading, error } = useSelector((state) => state.contents, shallowEqual);\n\n  useEffect(() => {\n    dispatch(resetError());\n\n    const hashText = parseShareHash(window.location.hash);\n    if (hashText) {\n      dispatch(loadSuccess(hashText));\n      return;\n    }\n\n    let link = pathname.replace('/d/', '');\n\n    if (link === '/d') {\n      link = '';\n    }\n\n    if (!link) {\n      const draft = loadDraft();\n      if (draft) {\n        dispatch(loadSuccess(draft));\n        return;\n      }\n    }\n\n    dispatch(load(link));\n  }, [dispatch, pathname]);\n\n  if (error) {\n    return <ErrorPage message={error} />;\n  }\n\n  return (\n    <>\n      <Title />\n      {isLoading\n        ? <Loading />\n        : <TextScreen />\n      }\n    </>\n  );\n};\n\nexport default Home;\n"
  },
  {
    "path": "src/components/Loading.js",
    "content": "import React from 'react';\n/** @jsxImportSource @emotion/react */\nimport { keyframes } from '@emotion/react';\nimport styled from '@emotion/styled';\n\nimport TextScreenWrapper from './shared/TextScreenWrapper';\n\nconst Loading = () => {\n  return (\n    <TextScreenWrapper>\n      <LoadingWrapper>\n        <Spinner />\n      </LoadingWrapper>\n    </TextScreenWrapper>\n  );\n};\n\nconst LoadingWrapper = styled.div`\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top:0;\n  left:0;\n`;\n\nconst loadingSpinner = keyframes`\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n`;\n\nconst Spinner = styled.div`\n  box-sizing: border-box;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 65px;\n  height: 65px;\n  margin-top: -32px;\n  margin-left: -32px;\n  border-radius: 50%;\n  border: 8px solid transparent;\n  border-top-color: #7f8eaa;\n  border-bottom-color: #7f8eaa;\n  animation: ${loadingSpinner} .8s ease infinite;\n`;\n\nexport default Loading;\n"
  },
  {
    "path": "src/components/MarkdownView.js",
    "content": "import React from 'react';\nimport styled from '@emotion/styled';\nimport { useSelector, shallowEqual } from 'react-redux';\nimport MarkdownPreview from '@uiw/react-markdown-preview';\n\nconst MarkdownView = () => {\n  const { text, fullEditor, fullMarkdown } = useSelector((state) => state.contents, shallowEqual);\n\n  const handleClassName = () => {\n    if (fullEditor) {\n      return 'none-preview';\n    }\n\n    if (fullMarkdown) {\n      return 'full-preview';\n    }\n\n    return '';\n  };\n\n  return (\n    <Markdown\n      className={handleClassName()}\n      source={text}\n    />\n  );\n};\n\nconst Markdown = styled(MarkdownPreview)`\n  overflow: scroll;\n  box-sizing: border-box;\n  background-color: white;\n  padding: 20px;\n  padding-bottom: 70px;\n  border: none;\n  width: 50%;\n\n  ${({ className }) => {\n    if (className === 'full-preview') {\n      return `\n        width: 100% !important;\n      `;\n    }\n\n    if (className === 'none-preview') {\n      return `\n        display: none;\n      `;\n    }\n  }};\n\n  @media (max-width: 768px) {\n    width: 100%;\n    height: 50%;\n    padding: 12px;\n    padding-bottom: 40px;\n  }\n`;\n\nexport default React.memo(MarkdownView);\n"
  },
  {
    "path": "src/components/ReactHelmet.js",
    "content": "import React from 'react';\nimport { Helmet } from 'react-helmet-async';\n\nconst SITE_URL = 'https://www.easy-me.com';\nconst TITLE = 'EASYME.md | 누구나 쉬운 README·리드미 온라인 마크다운 실시간 미리보기 에디터';\nconst DESCRIPTION = 'EASYME.md(이지미)는 README와 Markdown 문법에 익숙하지 않아도 편하게 글을 작성할 수 있는 웹 에디터입니다. 툴바로 서식 적용, 실시간 미리보기, 링크 한 번으로 공유까지 지원합니다.';\nconst IMAGE_URL = `${SITE_URL}/preview.png`;\n\nconst ReactHelmet = () => {\n  return (\n    <Helmet>\n      <html lang='ko' />\n      <title>{TITLE}</title>\n\n      <meta charSet='utf-8' />\n      <meta name='viewport' content='width=device-width, initial-scale=1.0' />\n      <meta name='description' content={DESCRIPTION} />\n      <meta name='keywords' content='easyme, readme, 리드미, 이지미, markdown, markdownsite, 마크다운, 마크다운작성사이트, 마크다운에디터, 실시간 미리보기' />\n      <meta name='robots' content='index, follow' />\n      <meta name='google-site-verification' content='sioQswZ6K0vzDLaveDWBachAp5y9pCFuv_2widqDXc4' />\n      <meta name='naver-site-verification' content='59a8659d9d6fce0d75c4e70921609fad75cd54fa' />\n\n      <link rel='icon' href='/favicon.ico' />\n      <link rel='canonical' href={SITE_URL} />\n\n      <meta property='og:type' content='website' />\n      <meta property='og:locale' content='ko_KR' />\n      <meta property='og:site_name' content='EASYME.md' />\n      <meta property='og:title' content={TITLE} />\n      <meta property='og:description' content={DESCRIPTION} />\n      <meta property='og:url' content={SITE_URL} />\n      <meta property='og:image' content={IMAGE_URL} />\n      <meta property='og:image:width' content='1200' />\n      <meta property='og:image:height' content='630' />\n\n      <meta name='twitter:card' content='summary_large_image' />\n      <meta name='twitter:title' content={TITLE} />\n      <meta name='twitter:description' content={DESCRIPTION} />\n      <meta name='twitter:image' content={IMAGE_URL} />\n    </Helmet>\n  );\n};\n\nexport default ReactHelmet;\n"
  },
  {
    "path": "src/components/SharingModal.js",
    "content": "import React, { useRef, useState } from 'react';\n/** @jsxImportSource @emotion/react */\nimport { css } from '@emotion/react';\nimport styled from '@emotion/styled';\nimport { AiOutlineLink } from 'react-icons/ai';\nimport PropTypes from 'prop-types';\n\nconst SharingModal = ({ url, updateModal }) => {\n  const linkValue = useRef();\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = async () => {\n    try {\n      await navigator.clipboard.writeText(url);\n    } catch (err) {\n      linkValue.current.select();\n      document.execCommand('copy');\n    }\n    setCopied(true);\n    setTimeout(() => setCopied(false), 1500);\n  };\n\n  return (\n    <div css={link}>\n      <Background onClick={() => updateModal(false)} />\n      <ModalWrapper>\n        <ModalWindow>\n          <IconWrapper onClick={handleCopy}>\n            <AiOutlineLink css={icon} />\n          </IconWrapper>\n          <InputWrapper>\n            <input\n              type='text'\n              value={url}\n              ref={linkValue}\n              readOnly\n              onMouseDown={(e) => e.preventDefault()}\n              onDragStart={(e) => e.preventDefault()}\n              onContextMenu={(e) => e.preventDefault()}\n            />\n            <button onClick={handleCopy}>{copied ? '복사됨' : '복사'}</button>\n          </InputWrapper>\n        </ModalWindow>\n      </ModalWrapper>\n    </div>\n  );\n};\n\nSharingModal.propTypes = {\n  url: PropTypes.string.isRequired,\n  updateModal: PropTypes.func.isRequired,\n};\n\nconst link = css`\n  cursor: default;\n`;\n\nconst icon = css`\n  font-size: 30px !important;\n  color: #ffffff;\n`;\n\nconst Background = styled.div`\n  position: fixed;\n  z-index: 3000;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background: rgba(0, 0, 0, 0.25);\n`;\n\nconst ModalWrapper = styled.div`\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 3100;\n`;\n\nconst ModalWindow = styled.div`\n  width: 400px;\n  height: 160px;\n  padding: 40px;\n  text-align: center;\n  background: rgba(255, 255, 255, 1);\n  border: 1px solid rgba(255, 255, 255, 0.18);\n  box-shadow: 4px 4px 12px 0px rgba(0, 0, 0, 0.2);\n  border-radius: 10px;\n`;\n\nconst IconWrapper = styled.div`\n  cursor: pointer;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin: 0 auto;\n  padding: 10px;\n  width: 50px;\n  height: 50px;\n  border-radius: 50%;\n  background-color: #2E3341;\n`;\n\nconst InputWrapper = styled.div`\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin-top: 40px;\n  border: 1px solid #e0e0e0;\n  border-radius: 3px;\n  width: 100%;\n  height: 40px;\n  background-color: #f9f9f9;\n\n  input {\n    width: 65%;\n    border: none;\n    background-color: #f9f9f9;\n    user-select: none;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    pointer-events: none;\n  }\n\n  button {\n    cursor: pointer;\n    font-weight: 700;\n    text-align: right;\n    color: #2E3341;\n    width: 25%;\n    height: 100%;\n    border: none;\n    background-color: #f9f9f9;\n  }\n`;\n\nexport default SharingModal;\n"
  },
  {
    "path": "src/components/TextScreen.js",
    "content": "import React from 'react';\nimport styled from '@emotion/styled';\nimport { useDispatch, useSelector, shallowEqual } from 'react-redux';\n\nimport CustomToolbar from './CustomToolbar';\nimport MarkdownView from './MarkdownView';\nimport Editor from './Editor';\nimport SaveBox from './shared/SaveBox';\nimport SharingModal from './SharingModal';\nimport { setShareUrl } from '../features/slice';\n\nimport TextScreenWrapper from './shared/TextScreenWrapper';\n\nconst TextScreen = () => {\n  const dispatch = useDispatch();\n  const { isSaved, fullScreen, shareUrl } = useSelector(\n    (state) => state.contents,\n    shallowEqual,\n  );\n\n  return (\n    <>\n      {isSaved && <SaveBox />}\n      {shareUrl && (\n        <SharingModal url={shareUrl} updateModal={() => dispatch(setShareUrl(''))} />\n      )}\n      <Header className={fullScreen ? 'full-screen' : ''}>\n        <CustomToolbar />\n      </Header>\n      <TextScreenWrapper>\n        <Main>\n          <Editor />\n          <MarkdownView />\n        </Main>\n      </TextScreenWrapper>\n    </>\n  );\n};\n\nconst Header = styled.div`\n  position: relative;\n  box-sizing: border-box;\n  padding: 5px;\n  width: 90%;\n  margin: 0 auto;\n  z-index: 2000;\n  background-color: white;\n  border-radius: 5px 5px 0px 0px;\n  border-bottom: 1px solid #dddddd;\n\n  ${({ className }) => {\n    if (className === 'full-screen') {\n      return `\n        position: fixed;\n        width: 100%;\n        top: 0;\n      `;\n    }\n  }};\n\n  @media (max-width: 768px) {\n    width: 96%;\n    padding: 4px;\n  }\n`;\n\nconst Main = styled.div`\n  display: flex;\n  width: 100%;\n  height: 100%;\n\n  @media (max-width: 768px) {\n    flex-direction: column;\n  }\n`;\n\nexport default TextScreen;\n"
  },
  {
    "path": "src/components/Title.js",
    "content": "import React from 'react';\n/** @jsxImportSource @emotion/react */\nimport { css } from '@emotion/react';\nimport styled from '@emotion/styled';\nimport { useRouteMatch } from 'react-router';\n\nimport character from '../assets/images/easyme.png';\n\nconst Title = () => {\n  const { url } = useRouteMatch();\n\n  const handleTitle = () => {\n    window.location.href = url;\n  };\n\n  return (\n    <Wrapper>\n      <ClickWrapper onClick={handleTitle}>\n        <img src={character} css={image} alt='easyme' />\n        <div css={title}>EASYME.md</div>\n      </ClickWrapper>\n    </Wrapper>\n  );\n};\n\nconst Wrapper = styled.div`\n  font-family: 'Rubik';\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin-top: 30px;\n  margin-bottom: 30px;\n\n  @media (max-width: 768px) {\n    margin-top: 16px;\n    margin-bottom: 16px;\n  }\n`;\n\nconst ClickWrapper = styled.div`\n  cursor: pointer;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  user-select: none;\n  -ms-user-select: none;\n  -moz-user-select: none;\n  -webkit-user-select: none;\n`;\n\nconst image = css`\n  width: 100px;\n  height: 100px;\n  border-radius: 50%;\n  box-shadow: 4px 6px 10px rgba(33, 40, 56, 0.1);\n  -ms-user-drag: none;\n  -moz-user-drag: none;\n  -webkit-user-drag: none;\n\n  @media (max-width: 768px) {\n    width: 60px;\n    height: 60px;\n  }\n`;\n\nconst title = css`\n  position: relative;\n  padding-left: 20px;\n  margin-top: 20px;\n  margin-bottom: 20px;\n  text-align: center;\n  color: white;\n  font-size: 4rem;\n  text-shadow: 4px 6px 10px rgba(33, 40, 56, 0.2);\n\n  @media (max-width: 768px) {\n    font-size: 2.2rem;\n    padding-left: 12px;\n    margin-top: 10px;\n    margin-bottom: 10px;\n  }\n`;\n\nexport default Title;\n"
  },
  {
    "path": "src/components/__test__/Title.test.js",
    "content": "/** * @jest-environment jsdom */\nimport React from 'react';\nimport { BrowserRouter as Router } from 'react-router-dom';\nimport { render } from '@testing-library/react';\n\nimport Title from '../Title';\n\ndescribe('<Title />', () => {\n  it('should match the title', () => {\n    const { getByText } = render(\n      <Router>\n        <Title />\n      </Router>\n    );\n\n    const title = getByText('EASYME.md');\n\n    expect(title).toBeInTheDocument();\n    expect(title).toHaveTextContent('EASYME.md');\n  });\n\n  it('should match image', () => {\n    const { getByAltText } = render(\n      <Router>\n        <Title />\n      </Router>\n    );\n\n    const image = getByAltText('easyme');\n\n    expect(image).toHaveAttribute('src');\n  });\n});\n"
  },
  {
    "path": "src/components/shared/ErrorPage.js",
    "content": "import React from 'react';\nimport styled from '@emotion/styled';\nimport { Link } from 'react-router-dom';\nimport PropTypes from 'prop-types';\n\nconst ErrorPage = ({ message }) => {\n  const NOT_FOUND = '404 Not Found';\n\n  return (\n    <Wrapper>\n      <State>\n        {message === NOT_FOUND\n          ? message\n          : '일시적인 오류가 발생했습니다.'\n        }</State>\n      <Message>\n        {message === NOT_FOUND\n          ? <>\n            <div>요청한 페이지를 찾을 수 없습니다. 공유되지 않은 주소입니다.</div>\n            <div>공유를 원할 경우, 툴바 가장 오른쪽에 공유 버튼을 눌러주세요.</div>\n          </>\n          : <>\n            <div>잠시 후에 다시 시도해주세요.</div>\n          </>\n        }\n      </Message>\n      <Button to='/'>홈으로 돌아가기</Button>\n    </Wrapper>\n  );\n};\n\nErrorPage.propTypes = {\n  message: PropTypes.string.isRequired,\n};\n\nconst Wrapper = styled.div`\n  position: absolute;\n  padding-top: 10%;\n  width: 100%;\n  height: 100%;\n  text-align: center;\n  background-color: white;\n`;\n\nconst State = styled.div`\n  margin-bottom: 10px;\n  font-size: 30px;\n`;\n\nconst Message = styled.div`\n  div {\n    margin-bottom: 10px;\n  }\n`;\n\nconst Button = styled(Link)`\n  display: flex;\n  margin: 0 auto;\n  cursor: pointer;\n  width: 160px;\n  margin-top: 20px;\n  border: none;\n  border-radius: 10px;\n  font-size: 18px;\n  font-weight: 600;\n  background-color: #7f8eaa;\n  justify-content: center;\n  text-decoration-line: none;\n  padding: 20px;\n  color: white;\n`;\n\nexport default ErrorPage;\n"
  },
  {
    "path": "src/components/shared/SaveBox.js",
    "content": "import React, { useEffect } from 'react';\n/** @jsxImportSource @emotion/react */\nimport { keyframes } from '@emotion/react';\nimport styled from '@emotion/styled';\nimport { useDispatch } from 'react-redux';\n\nimport { saveText } from '../../features/slice';\n\nconst SaveBox = () => {\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    setTimeout(() => {\n      dispatch(saveText());\n    }, 700);\n  }, [dispatch]);\n\n  return (\n    <BoxWrapper>\n      <Box>저장되었습니다.</Box>\n    </BoxWrapper>\n  );\n};\n\nconst transition = keyframes`\n  20%, 70% {\n    opacity: 100;\n    transform: translateY(-5px);\n  }\n  80% {\n    opacity: 0;\n    transform: translateY(-5px);\n  }\n`;\n\nconst BoxWrapper = styled.div`\n  position: absolute;\n  left: 50%;\n  top: 90%;\n  transform: translate(-50%, -50%);\n  z-index: 5000;\n`;\n\nconst Box = styled.div`\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  top: 90%;\n  left: 50%;\n  padding: 10px 20px;\n  width: 120px;\n  height: 30px;\n  background-color: white;\n  border-radius: 10px;\n  opacity: 0;\n  font-weight: 600;\n  box-shadow: 4px 4px 12px 0px rgba(0, 0, 0, 0.1);\n  animation: ${transition} 1s 0.1s linear;\n  animation-iteration-count: 1;\n`;\n\nexport default SaveBox;\n"
  },
  {
    "path": "src/components/shared/SectionLine.js",
    "content": "import React from 'react';\n/** @jsxImportSource @emotion/react */\nimport { css } from '@emotion/react';\n\nconst SectionLine = () => {\n  return (\n    <span css={line}></span>\n  );\n};\n\nconst line = css`\n  display: inline-block;\n  vertical-align: middle;\n  margin-left: 5px;\n  margin-right: 5px;\n  border-left: 1px solid #dddddd;\n  height: 25px;\n`;\n\nexport default SectionLine;\n"
  },
  {
    "path": "src/components/shared/TextScreenWrapper.js",
    "content": "import React from 'react';\nimport styled from '@emotion/styled';\nimport { useSelector, shallowEqual } from 'react-redux';\n\nconst TextScreenWrapper = ({ children }) => {\n  const { fullScreen } = useSelector((state) => state.contents, shallowEqual);\n\n  return (\n    <Wrapper>\n      <ScreenWrapper className={fullScreen ? 'full-screen' : ''}>\n        {children}\n      </ScreenWrapper>\n    </Wrapper>\n  );\n};\n\nconst Wrapper = styled.div`\n  width: 90%;\n  margin: 0 auto;\n\n  @media (max-width: 768px) {\n    width: 96%;\n  }\n`;\n\nconst ScreenWrapper = styled.div`\n  position: relative;\n  overflow: hidden;\n  height: 70vh;\n  background: white;\n  border: 1px solid rgba(255, 255, 255, 0.18);\n  box-shadow: 4px 4px 12px 0px rgba(0, 0, 0, 0.2);\n  border-radius: 0px 0px 5px 5px;\n  z-index: 1000;\n\n  ${({ className }) => {\n    if (className === 'full-screen') {\n      return `\n        position: fixed !important;\n        width: 100% !important;\n        height: 100% !important;\n        z-index: 300;\n        top: 2rem;\n        left: 0;\n        background: white;\n      `;\n    }\n  }};\n\n  @media (max-width: 819px) {\n    top: 7%;\n  }\n\n  @media (max-width: 768px) {\n    height: 78vh;\n  }\n\n  @media (max-width: 333px) {\n    top: 9%;\n  }\n`;\n\nexport default TextScreenWrapper;\n"
  },
  {
    "path": "src/components/shared/__test__/Editor.test.js",
    "content": "/** * @jest-environment jsdom */\nimport React from 'react';\nimport { Provider } from 'react-redux';\nimport { BrowserRouter as Router } from 'react-router-dom';\nimport { render, fireEvent } from '@testing-library/react';\n\nimport Editor from '../../Editor';\nimport createStore from '../../../store';\n\nconst store = createStore();\n\ndescribe('<Editor />', () => {\n  const text = 'EASYME.md를 방문해주셔서 감사합니다!';\n\n  it('should display when adding text', () => {\n    render(\n      <Provider store={store}>\n        <Router>\n          <Editor />\n        </Router>\n      </Provider>\n    );\n\n    const textArea = document.querySelector('#textarea');\n\n    fireEvent.change(textArea, { target: { value: text } });\n\n    expect(textArea).toBeEnabled();\n    expect(textArea).toHaveDisplayValue(text);\n  });\n});\n"
  },
  {
    "path": "src/components/shared/__test__/ErrorPage.test.js",
    "content": "/** * @jest-environment jsdom */\nimport React from 'react';\nimport { BrowserRouter as Router } from 'react-router-dom';\nimport { render } from '@testing-library/react';\n\nimport ErrorPage from '../ErrorPage';\n\ndescribe('<ErrorPage />', () => {\n  const notFoundMessage = '요청한 페이지를 찾을 수 없습니다. 공유되지 않은 주소입니다.';\n  const errorMessage = '잠시 후에 다시 시도해주세요.';\n\n  it('should render 404 Not Found', () => {\n    const { getByText } = render(\n      <Router>\n        <ErrorPage message='404 Not Found' />\n      </Router>\n    );\n\n    const state = getByText('404 Not Found');\n    const message = getByText(notFoundMessage);\n    const homeButton = getByText('홈으로 돌아가기');\n\n    expect(state).toBeInTheDocument();\n    expect(message).toBeInTheDocument();\n    expect(homeButton).toBeInTheDocument();\n\n    expect(state).toHaveTextContent('404 Not Found');\n    expect(message).toHaveTextContent(notFoundMessage);\n    expect(message).not.toHaveTextContent(errorMessage);\n  });\n\n  it('should render error message other than 404 Not Found', () => {\n    const { getByText } = render(\n      <Router>\n        <ErrorPage message='500 Internal server error' />\n      </Router>\n    );\n\n    const state = getByText('일시적인 오류가 발생했습니다.');\n    const message = getByText(errorMessage);\n\n    expect(state).toBeInTheDocument();\n    expect(message).toBeInTheDocument();\n\n    expect(state).toHaveTextContent('일시적인 오류가 발생했습니다.');\n    expect(message).toHaveTextContent(errorMessage);\n    expect(message).not.toHaveTextContent(notFoundMessage);\n  });\n});\n"
  },
  {
    "path": "src/constants/index.js",
    "content": "export const keyType = {\n  TAB: 9,\n  B: 66,\n  D: 68,\n  I: 73,\n  S: 83,\n  U: 85\n};\n"
  },
  {
    "path": "src/constants/welcomeMessage.js",
    "content": "const WELCOME_MESSAGE = `## 🙌 안녕하세요. EASYME.md를 만든 원아입니다!\n![easyme](/assets/readme/cartoon.png)   \n\n## ❓ EASYME.md가 뭐예요?   \n- **EASYME.md**는 **<u>개발자가 README.md를 좀 더 쉽게 작성할 수 있도록</u>** 하기 위해 만들었어요.   \n- 블로그에서 글을 쓰는 것처럼 쉽게 글을 작성하고 스타일을 적용하면 오른쪽(👉)에 미리보기로 확인하실 수 있어요.   \n- 스타일을 적용하면 마크다운 문법 및 md 파일에서 인식할 수 있는 소스코드가 자동으로 적용돼요.   \n- 커서 위치, 드래그한 영역 등에 따라 스타일을 적용할 수 있으니 자유롭게 사용해보세요!\n- 복사하기를 통해 본문 내용을 복사하고 여러분의 README에 적용해보세요.   \n\n## 🙋‍♀️ 좀 더 구체적으로 가르쳐주세요!   \n1. 왼쪽 공간에서 블로그에 글을 쓰는 것처럼 README를 작성해주세요!   \n2. 👆 위에 툴바창에 보이는 다양한 스타일을 적용해보세요!   \n3. 다 작성하셨나요? 예쁘게 잘 나왔는지 오른쪽 미리보기 화면에서 확인해보세요.   \n4. 오른쪽에 작성한 글 전체를 복사하세요!   \n(저장을 원할 경우 \\`Ctrl + S\\` / \\`Command + S\\` 또는 툴바창 제일 오른쪽에 \\`공유하기 아이콘\\`을 클릭해주세요.)   \n5. 이제 여러분의 **README.md** 에 붙여넣으세요!   \n(저장 또는 공유를 할 경우 링크를 다른 사람에게 전달할 수 있어요! 😀)  \n\n## 🛠 기능 엿보기   \n\n1. [❓ EASYME.md가 뭐예요?  ](#-easymemd가-뭐예요)\n2. [🙋‍♀️ 좀 더 구체적으로 가르쳐주세요!](#-좀-더-구체적으로-가르쳐주세요)\n3. [🛠 기능 엿보기](#-기능-엿보기)\n    - [Header](#header)   \n    - [Text Style1](#text-style1)   \n    - [Text Stlye2](#text-style2)   \n    - [List](#list)      \n    - [Link](#link)   \n    - [Code Block](#code-block)   \n    - [Table](#table)   \n   \n## Header\n- # H1 Header   \n- ## H2 Header   \n- ### H3 Header   \n- #### H4 Header   \n- ##### H5 Header   \n- ###### H6 Header   \n\n<br>   \n\n## Text Style1\n- **진하게** (\\`Ctrl(Command) + B\\`)   \n- *기울이기* (\\`Ctrl(Command) + I\\`)   \n- <s>취소선</s> (\\`Ctrl(Command) + D\\`)   \n- <u>밑줄</u> (\\`Ctrl(Command) + U\\`)   \n\n<br>   \n   \n## Text Style2\n\n>인용문   \n   \n<details><summary>접고 펴는 기능\n</summary>\n\n*Write here!*\n</details>\n\n- EASYME.md를 드래그하고 상단에 \\`Aa\\` 아이콘을 누르면? 👉 Easyme.md   \n- EASYME.md를 드래그하고 상단에 \\`A\\` 아이콘을 누르면? 👉 EASYME.MD   \n- EASYME.md를 드래그하고 상단에 \\`a\\` 아이콘을 누르면? 👉 easyme.md   \n   \n<br>   \n   \n## List   \n### Table of contents\n1. [title1](#write-title-here!)   \n2. [title2](#only-lowercase)   \n3. [title3](#use\"-\"instead-of-spacing-words)   \n4. [title4](#example)   \n    - [❓ EASYME.md가 뭐예요?](#-easymemd가-뭐예요)   \n    - [🛠 기능 엿보기](#-기능-엿보기)\n   \n### Unordered list   \n- unordered list1   \n- unordered list2   \n- unordered list3   \n- unordered list4   \n   \n### Ordered list   \n1. ordered list1   \n2. ordered list2   \n3. ordered list3   \n4. ordered list4   \n   \n<br>   \n   \n## Link   \n### General link\n- [🚗 Visit EASYME.md's Repo](https://github.com/EASYME-md/client)   \n- [🙋‍♂️ Visit ONE:A's Github](https://github.com/onealog)\n\n### Image link\n![onealog](/assets/readme/easyme.png)   \n   \n<br>   \n   \n## Code Block   \n### Code inline\n- \\`console.log('Hello EASYME.md!');\\`   \n   \n### Code block\n\\`\\`\\`js\nfunction makeDeveloper(name, language) {\n  if (name === 'ONE:A' && language === 'JavaScript') {\n    return 'perfect!';\n  }\n\n  return false;\n}\n\nmakeDeveloper('ONE:A', 'JavaScript');\n\\`\\`\\`\n\n<br>   \n   \n## Table   \n\n\n| title1 | title2 | title3 |\n| --- | --- | --- |\n| 1 | 2 | 3 |\n| 4 | 5 | 6 |\n| 7 | 8 | 9 |\n\n\n<br>   \n\n`;\n\nexport default WELCOME_MESSAGE;\n"
  },
  {
    "path": "src/features/saga.js",
    "content": "import { call, put, takeLatest } from 'redux-saga/effects';\n\nimport { load, loadSuccess, loadFail } from './slice';\nimport { fetchContents } from '../api';\n\nfunction* handleContentsLoad(action) {\n  const linkId = action.payload;\n\n  try {\n    const text = yield call(() => fetchContents(linkId));\n\n    yield put(loadSuccess(text));\n  } catch (err) {\n    yield put(loadFail(err));\n  }\n}\n\nexport function* watchContents() {\n  yield takeLatest(load, handleContentsLoad);\n}\n"
  },
  {
    "path": "src/features/slice.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\n\nimport WELCOME_MESSAGE from '../constants/welcomeMessage';\n\nconst initialState = {\n  isLoading: false,\n  isSaved: false,\n  linkId: '',\n  text: '',\n  textArea: null,\n  fullEditor: false,\n  fullMarkdown: false,\n  fullScreen: false,\n  error: null,\n  shareUrl: '',\n};\n\nconst reducers = {\n  addLinkId: (state, action) => {\n    state.linkId = action.payload;\n  },\n  addText: (state, action) => {\n    state.text = action.payload;\n  },\n  addTextArea: (state, action) => {\n    state.textArea = action.payload;\n  },\n  resetError: (state) => {\n    state.error = null;\n  },\n  addError: (state, action) => {\n    state.error = action.payload;\n  },\n  toggleEditor: (state) => {\n    state.fullEditor = !state.fullEditor;\n  },\n  toggleMarkdown: (state) => {\n    state.fullMarkdown = !state.fullMarkdown;\n  },\n  toggleFullScreen: (state) => {\n    state.fullScreen = !state.fullScreen;\n  },\n  saveText: (state) => {\n    state.isSaved = !state.isSaved;\n  },\n  setShareUrl: (state, action) => {\n    state.shareUrl = action.payload;\n  },\n  load: (state) => {\n    state.isLoading = true;\n  },\n  loadSuccess: (state, action) => {\n    state.isLoading = false;\n    state.text = action.payload || WELCOME_MESSAGE;\n  },\n  loadFail: (state, action) => {\n    state.isLoading = false;\n    state.error = action.payload;\n  },\n};\n\nconst name = 'contents';\nconst slice = createSlice({\n  name, initialState, reducers,\n});\n\nexport const contents = slice.name;\nexport const {\n  addLinkId, addText, addTextArea, resetError, addError,\n  toggleEditor, toggleMarkdown, toggleFullScreen,\n  saveText, setShareUrl, load, loadSuccess, loadFail } = slice.actions;\n\nexport default slice.reducer;\n"
  },
  {
    "path": "src/index.css",
    "content": "body {\n  margin: 0;\n  background-color: #7f8eaa;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { HelmetProvider } from 'react-helmet-async';\n\nimport './index.css';\nimport './assets/fonts/font.css';\nimport createStore from './store';\nimport App from './App';\n\nconst store = createStore();\n\nReactDOM.render(\n  <React.StrictMode>\n    <Provider store={store}>\n      <HelmetProvider>\n        <App />\n      </HelmetProvider>\n    </Provider>\n  </React.StrictMode>,\n  document.getElementById('root')\n);\n"
  },
  {
    "path": "src/setupTests.js",
    "content": "import '@testing-library/jest-dom';\nimport '@babel/polyfill';\n"
  },
  {
    "path": "src/store/index.js",
    "content": "import { combineReducers, configureStore } from '@reduxjs/toolkit';\nimport createSagaMiddleware from 'redux-saga';\nimport { all } from 'redux-saga/effects';\n\nimport contentsReducer, { contents } from '../features/slice';\nimport { watchContents } from '../features/saga';\n\nexport const rootReducer = combineReducers({\n  [contents]: contentsReducer,\n});\n\nconst sagaMiddleware = createSagaMiddleware();\nfunction* rootSaga() {\n  yield all([\n    watchContents()\n  ]);\n}\n\nconst createStore = () => {\n  const store = configureStore({\n    reducer: rootReducer,\n    devTools: true,\n    middleware: [sagaMiddleware],\n  });\n\n  sagaMiddleware.run(rootSaga);\n\n  return store;\n};\n\nexport default createStore;\n"
  },
  {
    "path": "src/utils/__test__/utils.test.js",
    "content": "/** * @jest-environment jsdom */\nimport { unmountComponentAtNode } from 'react-dom';\n\nimport addTypeCurrentPosition from '../addTypeCurrentPosition';\nimport addTypeBeforeAndAfter from '../addTypeBeforeAndAfter';\nimport addTypeCrrentRow from '../addTypeCurrentRow';\nimport addTypeDraggedRows from '../addTypeDraggedRows';\n\nlet textArea = null;\n\nbeforeEach(() => {\n  document.execCommand = jest.fn();\n  textArea = document.createElement('textarea');\n  document.body.appendChild(textArea);\n  textArea.textContent = 'EASYME.md입니다.';\n  textArea.selectionStart = 0;\n  textArea.selectionEnd = 9;\n});\n\nafterEach(() => {\n  unmountComponentAtNode(textArea);\n  textArea.remove();\n  textArea = null;\n});\n\ndescribe('utils function', () => {\n  it('01. addTypeCurrentPosition test', () => {\n    expect(addTypeCurrentPosition(textArea, '현재 텍스트 뒤에 추가되는 것은 ')).toEqual('현재 텍스트 뒤에 추가되는 것은 EASYME.md입니다.');\n  });\n\n  it('02. addTypeBeforeAndAfter test', () => {\n    expect(addTypeBeforeAndAfter(textArea, '**')).toEqual('**EASYME.md**입니다.');\n  });\n\n  it('03. addTypeCrrentRow test', () => {\n    expect(addTypeCrrentRow(textArea, '- ')).toEqual('- EASYME.md입니다.');\n  });\n\n  it('04. addTypeDraggedRows test', () => {\n    textArea.textContent = 'EASYME.md입니다.\\n쉽고 빠르게 리드미를 작성해보세요!';\n    textArea.selectionEnd = 34;\n\n    expect(addTypeDraggedRows(textArea, '- ')).toEqual('- EASYME.md입니다.\\n- 쉽고 빠르게 리드미를 작성해보세요!');\n  });\n});\n"
  },
  {
    "path": "src/utils/addTypeBeforeAndAfter.js",
    "content": "import { wrapSelection } from 'text-field-edit';\n\nconst addTypeBeforeAndAfter = (textArea, typeA, typeB = typeA) => {\n  const scroll = textArea.scrollTop;\n\n  textArea.focus();\n  wrapSelection(textArea, typeA, typeB);\n  textArea.scrollTop = scroll;\n\n  return textArea.value;\n};\n\nexport default addTypeBeforeAndAfter;\n"
  },
  {
    "path": "src/utils/addTypeCurrentPosition.js",
    "content": "import { insert } from 'text-field-edit';\n\nconst addTypeCurrentPosition = (textArea, type) => {\n  const scroll = textArea.scrollTop;\n\n  textArea.focus();\n  insert(textArea, type);\n  textArea.scrollTop = scroll;\n\n  return textArea.value;\n};\n\nexport default addTypeCurrentPosition;\n"
  },
  {
    "path": "src/utils/addTypeCurrentRow.js",
    "content": "import { insert } from 'text-field-edit';\n\nconst addTypeCurrentRow = (textArea, type) => {\n  const scroll = textArea.scrollTop;\n  const startPosition = textArea.selectionStart;\n  const beforeCursor = textArea.value.substring(0, startPosition);\n  const rowStart = beforeCursor.lastIndexOf('\\n') + 1;\n\n  textArea.focus();\n  textArea.setSelectionRange(rowStart, rowStart);\n  insert(textArea, type);\n\n  const newPosition = startPosition + type.length;\n  textArea.setSelectionRange(newPosition, newPosition);\n  textArea.scrollTop = scroll;\n\n  return textArea.value;\n};\n\nexport default addTypeCurrentRow;\n"
  },
  {
    "path": "src/utils/addTypeDraggedRows.js",
    "content": "import { insert } from 'text-field-edit';\n\nconst addTypeDraggedRows = (textArea, type) => {\n  const scroll = textArea.scrollTop;\n  const selStart = textArea.selectionStart;\n  const selEnd = textArea.selectionEnd;\n  const value = textArea.value;\n\n  const beforeSelection = value.substring(0, selStart);\n  const firstRowStart = beforeSelection.lastIndexOf('\\n') + 1;\n\n  const affectedText = value.substring(firstRowStart, selEnd);\n  const rows = affectedText.split('\\n');\n\n  const prefixedRows = type\n    ? rows.map((row) => type + row)\n    : rows.map((row, i) => `${i + 1}. ${row}`);\n  const newText = prefixedRows.join('\\n');\n\n  textArea.focus();\n  textArea.setSelectionRange(firstRowStart, selEnd);\n  insert(textArea, newText);\n\n  const lengthDiff = newText.length - affectedText.length;\n  textArea.setSelectionRange(selStart + (type ? type.length : '1. '.length), selEnd + lengthDiff);\n  textArea.scrollTop = scroll;\n\n  return textArea.value;\n};\n\nexport default addTypeDraggedRows;\n"
  },
  {
    "path": "src/utils/draftStorage.js",
    "content": "const DRAFT_KEY = 'easyme:draft';\n\nexport const loadDraft = () => {\n  try {\n    return localStorage.getItem(DRAFT_KEY);\n  } catch (err) {\n    return null;\n  }\n};\n\nexport const saveDraft = (text) => {\n  try {\n    if (text) {\n      localStorage.setItem(DRAFT_KEY, text);\n    } else {\n      localStorage.removeItem(DRAFT_KEY);\n    }\n  } catch (err) {\n    // localStorage may be unavailable (private mode, quota) — silently ignore\n  }\n};\n\nexport const clearDraft = () => {\n  try {\n    localStorage.removeItem(DRAFT_KEY);\n  } catch (err) {\n    // ignore\n  }\n};\n"
  },
  {
    "path": "src/utils/shareDocument.js",
    "content": "import { setShareUrl } from '../features/slice';\nimport { buildShareUrl } from './urlShare';\n\nconst shareDocument = async (textArea, dispatch) => {\n  const text = textArea ? textArea.value : '';\n  const url = buildShareUrl(text);\n\n  try {\n    await navigator.clipboard.writeText(url);\n  } catch (err) {\n    // clipboard unavailable (insecure origin, permission denied) — modal still shows the URL\n  }\n\n  dispatch(setShareUrl(url));\n};\n\nexport default shareDocument;\n"
  },
  {
    "path": "src/utils/urlShare.js",
    "content": "import LZString from 'lz-string';\n\nexport const HASH_PREFIX = '#s=';\n\nexport const encodeForShare = (text) => {\n  if (!text) return '';\n  return LZString.compressToEncodedURIComponent(text);\n};\n\nexport const decodeFromShare = (compressed) => {\n  if (!compressed) return '';\n  try {\n    return LZString.decompressFromEncodedURIComponent(compressed) || '';\n  } catch (err) {\n    return '';\n  }\n};\n\nexport const parseShareHash = (hash) => {\n  if (!hash || !hash.startsWith(HASH_PREFIX)) return '';\n  return decodeFromShare(hash.slice(HASH_PREFIX.length));\n};\n\nexport const buildShareUrl = (text) => {\n  const compressed = encodeForShare(text);\n  return `${window.location.origin}/d${HASH_PREFIX}${compressed}`;\n};\n"
  }
]