diff --git a/README.md b/README.md index aca59f0..ad69b02 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # SRS -이용하기 위해서 [mdbook](https://github.com/rust-lang/mdBook)을 설치해야 합니다. +이 문서 빌더를 이용하기 위해서 [mdbook](https://github.com/rust-lang/mdBook)을 설치해야 합니다. 이 [링크](https://rust-lang.github.io/mdBook/guide/installation.html)에서 설치하면 됩니다. +그리고 [deno](https://deno.land/)도 설치를 해야합니다. 이 [링크](https://deno.land/#installation) +를 따라서 설치해주세요. + +python이 설치되어있어야 cli.py를 사용할 수 있습니다. + +문서를 pdf 포맷으로 만드려면 java가 설치되어있어야 합니다. 운영체제가 Windows라면 설치 후 환경변수를 +설정해주세요. + ```bash mdbook serve ``` @@ -10,11 +18,39 @@ mdbook serve ## cli.py -deno가 설치되어 있어야 합니다. - -처음 실행시 issue를 가지고 오는 작업이 필요합니다. 먼저, `GITHUB_TOKEN` 환경변수에 깃헙 token을 설정해주세요. `.env`를 지원합니다. - 토큰을 발급받기 위해선 [다음 링크](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)를 참조하면 됩니다. +처음 실행시 issue를 가지고 오는 작업이 필요합니다. ```bash ./cli.py build --update_issues ``` -를 실행해 주세요. \ No newline at end of file +를 실행해 주세요. + +cli.py 는 여러 subcommand를 가집니다. +아래애서 v옵션과 h옵션은 항상 verbose 모드와 help를 의미합니다. + +### build +``` +usage: cli.py build [-h] [-v] [--update_issues] +``` +작성된 문서를 html로 변환합니다. `update_issues` 옵션을 주어서 먼저 +issue를 업데이트할 수 있습니다. issue들의 정보는 `cache/issues.json`에 생성 +됩니다. + +### issueUpdate +``` +usage: cli.py issueUpdate [-h] [-v] [--outDir OUTDIR] +``` +Issue를 업데이트 합니다. 정확히는 지정된 `OUTDIR`에 `issues.json`이란 +파일을 만듭니다. OUTDIR의 기본값은 `cache/` 입니다. + +### serve +``` +usage: cli.py serve [-h] [-v] [-p PORT] +``` +문서를 html로 변환하고 웹서버를 엽니다. 변화가 생기면 다시 빌드합니다. + +### buildPdf +``` +usage: cli.py buildPdf [-h] [-v] [--outDir OUTDIR] [--browser-path BROWSER_PATH] +``` +변환된 html을 pdf로 바꾸고 OUTDIR에 저장합니다. 그러므로 문서를 먼저 `build`하고 시도해야 합니다. +browser-path 옵션으로 html을 pdf로 바꾸는 것에 사용될 browser를 지정할 수 있습니다. diff --git a/cli.py b/cli.py index 062afed..c564ffd 100644 --- a/cli.py +++ b/cli.py @@ -22,8 +22,6 @@ def build(args): parser = argparse.ArgumentParser(description='Compiling the documentation', prog="cli.py build") parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode') parser.add_argument('--update_issues', action='store_true', help='update issues') - parser.add_argument('--outDir', default="build", help='output directory') - parser.add_argument('-w', '--watch', action='store_true', help='watch for changes') args = parser.parse_args(args) if args.verbose: @@ -41,9 +39,7 @@ def serve(args): parser = argparse.ArgumentParser(description='Serve the documentation and reload changes', prog="cli.py serve") parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode') parser.add_argument('-p', '--port', default=3000, help='port') - parser.add_argument('--update_issues', action='store_true', help='update issues') - outDir = "build" - issuePath = os.path.join(outDir,"issues.json") + args = parser.parse_args(args) if args.verbose: print("serve start") @@ -59,7 +55,7 @@ def help(_args): def issueUpdate(args): parser = argparse.ArgumentParser(description='Update issues', prog="cli.py issueUpdate") parser.add_argument('-v', '--verbose', action='store_true', help='verbose mode') - parser.add_argument('--outDir', default="build", help='output directory') + parser.add_argument('--outDir', default="cache", help='output directory') args = parser.parse_args(args) issuePath = os.path.join(args.outDir,"issues.json") updateIssue(issuePath, args.verbose) diff --git a/log.json b/log.json deleted file mode 100644 index 67169ab..0000000 --- a/log.json +++ /dev/null @@ -1 +0,0 @@ -[{"root":"C:\\Users\\Monoid\\Desktop\\SRS\\SRS","config":{"book":{"authors":["monoid"],"language":"ko","multilingual":false,"src":"src","title":"Software Requirement Specification"},"output":{"html":{"additional-js":["mermaid.min.js","mermaid-init.js"],"live-reload-endpoint":"__livereload","site-url":"/"}},"preprocessor":{"etap":{"before":["mermaid"],"command":"deno run -A --no-check tools/preprop.ts"},"mermaid":{"command":"mdbook-mermaid"}}},"renderer":"html","mdbook_version":"0.4.18"},{"sections":[{"Chapter":{"name":"Index","content":"# 목차(Index)\r\n\r\n1. [Introduction](./intro.md)\r\n 1. [목적(Purpose)](./intro.md#11-목적purpose)\r\n 2. [범위(scope)](./intro.md#12-범위scope)\r\n 3. [용어 및 약어 정의(Definitions, acronyms and abbreviations)](./intro.md#13-용어-및-약어-정의definitions-acronyms-and-abbreviations)\r\n 4. [참고자료(References)](./intro.md#14-참고자료references)\r\n 5. [개요(Overview)](./intro.md#15-개요overview)\r\n2. [Overall Description](./overall.md)\r\n 1. [제품 관점(Product perspective)](./overall.md#21-제품-관점product-perspective)\r\n 1. [시스템 인터페이스(System interfaces)](./overall.md#211-시스템-인터페이스system-interfaces)\r\n 2. [사용자 인터페이스(User interfaces)](./overall.md#212-사용자-인터페이스user-interfaces)\r\n 3. [하드웨어 인터페이스(Hardware interfaces)](./overall.md#213-하드웨어-인터페이스hardware-interfaces)\r\n 4. [소프트웨어 인터페이스(Software interfaces)](./overall.md#214-소프트웨어-인터페이스software-interfaces)\r\n 5. [통신 인터페이스(Communications interfaces)](./overall.md#215-통신-인터페이스communications-interfaces)\r\n 6. [메모리 제약사항(Memory constraints)](./overall.md#216-메모리-제약사항memory-constraints)\r\n 7. [운영(Operations)](./overall.md#217-운영operations)\r\n 8. [사이트 적용 요건(Site adaption requirements)](./overall.md#218-사이트-적용-요건site-adaption-requirements)\r\n 2. [제품 기능(Product functions)](./overall.md#22-제품-기능product-functions)\r\n <%\r\n const table = it.table;\r\n let index = 1;\r\n for (const [c,issues] of table) {\r\n const name = `${c} Operation`;\r\n const href = `2.2.${index} ${c} Operation`\r\n %><%= `${index++}. [${name}](./overall.md#${it.toHeadId(href)})\\n ` %><%}%><%=\"\\n\"%>\r\n3. [Specific Requirement](./specific.md)\r\n 1. [외부 인터페이스 요구사항(External interface requirements)](./specific.md#31-외부-인터페이스-요구사항external-interface-requirements)\r\n 2. [기능 요구사항(Functional requirements)](./specific.md#32-기능-요구사항functional-requirements)\r\n <%= it.issues.map((i)=>`(#${i.number}) ${i.title}`).map(\r\n x=>`* [${x}](./specific.md#${it.toHeadId(x)})`\r\n ).join(\"\\n \") %><%= \"\\n\"%>\r\n 3. [성능 요구사항(Performance requirements)](./specific.md#33-성능-요구사항performance-requirements)\r\n 4. [논리적 데이터베이스 요구사항(Logical database requirements)](./specific.md#34-논리적-데이터베이스-요구사항logical-database-requirements)\r\n 5. [설계 제약사항(Design constraints)](./specific.md#35-설계-제약사항design-constraints)\r\n 1. [표준 준수(Standards compliance)](./specific.md#351-표준-준수standards-compliance)\r\n 6. [소프트웨어 시스템 속성(Software system attributes)](./specific.md#36-소프트웨어-시스템-속성software-system-attributes)\r\n 7. [상세 요구사항의 구성(Organizing the specific requirements)](./specific.md#37-상세-요구사항의-구성organizing-the-specific-requirements)\r\n 1. [객체(Objects)](./specific.md#371-객체objects)\r\n 2. [사용자 인터페이스 상세](./specific.md#372-사용자-인터페이스-상세)\r\n4. [Supporting information](./support.md)\r\n5. [Architecture](./architecture.md)\r\n6. [Testing](./testing.md)","number":null,"sub_items":[],"path":"index.md","source_path":"index.md","parent_names":[]}},{"Chapter":{"name":"Introduction","content":"# 1. 소개(Introduction)\n\n> Version : 1.0.1\n\n 본 문서는 전북대학교 컴퓨터공학과의 Floor 팀에서 Scrap Yard라는 어플리케이션을 설계 및 구현하기 위한 소프트웨어 요구사항 명세서(SRS)이다.\n\n## 1.1. 목적(Purpose)\n\n본 문서의 목적은 프로젝트의 관련된 모든 아이디어들을 정리하고 분석해서 나열하는 것이다. 또한 프로젝트를 더 잘 이해하기 위해 이 제품이 어떻게 사용될지 예측하고 분류하고, 나중에 개발될 요소를 설명하고, 고려 중이지만 폐기될 수 있는 요구사항들을 문서화한다.\n\n## 1.2. 범위(Scope)\n\n본 문서의 범위는 ScrapYard의 기능들과 그 환경이다.\n\nScrapYard는 문서 작성 밎 문서를 아카이빙 할 수 있는 웹 어플리케이션이다. 같이 제공되는 확장기능을 통해 북마크(즐겨찾기)를 구조적으로 보관할 수 있고 미리보기를 보여줄 수 있다.\n\n또한 문서를 다른 사람과 링크로 공유할 수 있다.\n\n개인정보의 관리를 자기 자신이 제어할 수 있도록 파일과 문서에 대한 메타데이터가 어떠한 외부 DB가 있는 것이 아닌 파일에서 사람이 읽을 수 있는 형태로 관리된다. \n\n## 1.3. 용어 및 약어 정의(Definitions, acronyms and abbreviations)\n\n|용어 및 약어|정의|\n|---|----|\n|ScrapYard|현재 개발하는 앱의 명칭|\n|DnD|드래그 앤 드롭의 약자|\n\n## 1.4. 참고자료(References)\n\n- [repo](https://github.com/vi117/scrap-yard)\n- [react](https://reactjs.org/)\n- [recoil](https://recoiljs.org/)\n- [MUI](https://mui.com/)\n- [dndkit](https://docs.dndkit.com/)\n- [markdown](https://commonmark.org/)\n\n## 1.5. 개요(Overview)\n\n 2장에서는 종합적인 요구사항을 서술하고, 3장에서는 기능 및 UI에 대해서 상세한 요구사항을 설명한다.\n","number":[1],"sub_items":[],"path":"intro.md","source_path":"intro.md","parent_names":[]}},{"Chapter":{"name":"Overall Description","content":"# 2. 전체 시스템 개요(Overall description)\n\n### 2.1. 제품 관점(Product perspective)\n\n### 2.1.1. 시스템 인터페이스(System interfaces)\n\n본 시스템은 Cross-platform 소프트웨어이다. 다음과 같은 브라우저가 원활히 실행될 수 있는 시스템에서 동작 할 수 있다.\n- Chrome 버전 61 이상\n- Firefox 버전 60 이상\n- Edge 버전 79 이상\n- Safari 버전 11 이상\n- Chrome for Android 버전 100 이상\n- Samsung internet 버전 8.2 이상\n\n### 2.1.2. 사용자 인터페이스(User interfaces)\n\n웹으로 동작하는 GUI이다. 키보드와 마우스, 터치 인터페이스로 동작할 수 있다. GUI의 디자인은 Material Design이나 Metro Design 같이 플랫한 디자인을 추구한다.\n\n### 2.1.3. 하드웨어 인터페이스(Hardware interfaces)\n\n해당되지 않음.\n\n### 2.1.4. 소프트웨어 인터페이스(Software interfaces)\n\n이 프로젝트의 결과물은 클립보드를 통해서 여러 타입의 데이터를 import/export한다.\n\n### 2.1.5. 통신 인터페이스(Communications interfaces)\n\n해당되지 않음.\n\n### 2.1.6. 메모리 제약사항(Memory constraints)\n\n서버는 2GB 메모리 환경에서 정상 작동해야 한다.\n\n### 2.1.7. 운영(Operations)\n\n해당되지 않음.\n\n### 2.1.8. 사이트 적용 요건(Site adaption requirements)\n\n해당되지 않음.\n\n## 2.2. 제품 기능(Product functions)\n\n본 프로젝트의 결과물은 다음과 같은 기능을 수행한다.\n\n<%\n const table = it.table;\n let index = 1;\n for (const [c,issues] of table) {\n%><%= `### 2.2.${index++} ${c} Operation\\n\\n` %><%\n let subIndex = 1;\n for (const i of issues) {\n%><%=`${subIndex++}. #${i.number} ${i.title}\\n` %><%\n }\n%>\n\n<%\n }\n%>\n\n## 2.3. 사용자 특성(User characteristics)\n\n사용자는 기본적인 GUI 조작을 할 줄 알며 인터넷 사용을 원활히 할 수 있고 기본적인 영어를 읽고 쓸 줄 알며, markdown을 작성할 수 있는 사용자로 한정한다. 일반적으로 13세 이상 65세 이하의 사람을 사용자로 가정한다.\n\n## 2.4. 제약사항(Constraints)\n\n- 이 프로젝트는 MIT License로 개발되고 있으므로 라이브러리의 라이센스도 신경을 쓴다.\n- XSS 공격에 안전해야 한다.\n\n## 2.5. 가정 및 의존성(Assumptions and dependencies)\n\n해당되지 않음.\n\n## 2.6. 단계별 요구사항(Apportioning of requirements)\n\n해당되지 않음.","number":[2],"sub_items":[],"path":"overall.md","source_path":"overall.md","parent_names":[]}},{"Chapter":{"name":"Specific Requirement","content":"# 3. 상세요구사항(Specific Requirements)\n\n## 3.1. 외부 인터페이스 요구사항(External interface requirements)\n\n해당되지 않음.\n## 3.2. 기능 요구사항(Functional requirements)\n\n<%~ it.issues.map(i => `### (#${i.number}) ${i.title}\\n${i.body.replaceAll(\"\\r\\n\",\"\\n\")}`).join(\"\\n\\n\") %>\n\n## 3.3. 성능 요구사항(Performance requirements)\n\n1. 최소 1000 RPS를 보장해야한다.\n2. 첫 로드후 로딩하면 0.5s 이내에 동작해야합니다\n3. 동시 편집 이용자를 5명까지는 허용해야 합니다.\n4. 적어도 400개의 파일을 관리할 수 있어야 합니다.\n\n## 3.4. 논리적 데이터베이스 요구사항(Logical database requirements)\n\n```mermaid\nerDiagram\n User {\n int id\n string accessToken\n date expiredAt\n }\n User ||--o{ Permission : has\n Permission {\n string name\n string path\n }\n```\n\n 단순하게 공유를 위한 User 구조만 있다.\n\n\n## 3.5. 설계 제약사항(Design constraints)\n\n2.1.1. 에서 언급했듯이 다음 브라우저에서 동작해야 한다.\n\n- Chrome 버전 61 이상\n- Firefox 버전 60 이상\n- Edge 버전 79 이상\n- Safari 버전 11 이상\n- Chrome for Android 버전 100 이상\n- Samsung internet 버전 8.2 이상\n\n그리고 모바일에서도 큰 불편함 없이 원활히 동작해야 한다.\n\n### 3.5.1. 표준 준수(Standards compliance)\n\n해당되지 않음.\n\n## 3.6. 소프트웨어 시스템 속성(Software system attributes)\n\n해당되지 않음.\n\n## 3.7. 상세 요구사항의 구성(Organizing the specific requirements)\n\n### 3.7.1. 객체(Objects)\n\n다음과 같은 UML을 그릴 수 있다.\n\n```mermaid\nclassDiagram\n class Document{\n - URL path\n - string[] tags\n \n + renderChunk()\n + remove()\n + addTag(name: string)\n + deleteTag(name: string)\n + async share(option: ShareOption): URL\n + renderNavigator()\n }\n class Chunk{\n - id_t id\n - Content data\n - bool focused\n - pos_t cursorPos\n\n + focus(index: number)\n + unfocus(index: number)\n + remove(index: number)\n + insertBefore()\n + draw()\n + drawPreview()\n + autoComplete(ctx: AutoCompleteContext)\n + swapWith(p : Chunk)\n + edit()\n }\n Document \"1\" <-- \"n\" Chunk : List\n class Fileview{\n + listDirectory(path: URL)\n + open(path: URL)\n + remove(path: URL)\n + create(path: URL)\n + upload(file: Uint8Array| FileStream)\n + download(path: URL)\n + export(path: URL, option: ExportOption)\n }\n Fileview <.. Document : create\n class StashList{\n Content[] stash\n int maxStash\n createFromServer()\n push(c: Content)\n pop()\n }\n class DnDManager{\n + dropTo(Fileview)\n + dropTo(Document)\n + dropTo(Stash)\n + dragFrom(Fileview)\n + dragFrom(Document)\n + dragFrom(Stash)\n }\n DnDManager <.. Fileview : param\n DnDManager <.. Document : param\n DnDManager <.. StashList : param\n class SearchManager{\n search(query,option)\n }\n SearchManager <.. Document : return\n```\n\n```mermaid\nclassDiagram\n class Management{\n login(auth:Auth)\n setServerConfigure(config: Configure)\n setLocale(lang: string)\n setTheme(theme: string)\n getLocale()\n getTheme()\n getServerConfigure()\n }\n class Extension{\n registerPlugin(plugin: Plugin)\n }\n```\n\nChunk는 문서를 이루는 기본적인 단위이다. 글의 문단이라고 생각 할 수 있다. Document는 그런 Chunk의 리스트이다. FileView는 파일에 접근하고 Document를 열 수 있는 파일 브라우저이다. StashList는 단순한 파일이나 텍스트 등을 임시로 저장하고 꺼내쓰는 컨테이너이다.\n\n### 3.7.2. 사용자 인터페이스 상세\n\n![interface](./interface.png)\n\n다음과 같이 컴포넌트가 배치될 것이다. 배치될 컴포넌트는 Treeview, Chunk, ChunkList, Appbar, GeneralDialogue, Drawer, ContextMenu, StashList가 있다.","number":[3],"sub_items":[],"path":"specific.md","source_path":"specific.md","parent_names":[]}},{"Chapter":{"name":"Supporting information","content":"# 추가 이력 (Supporting Information)\n\n## 4.1. 부록(Appendixes)\n\n내용 없음.\n\n## 4.2. 개발 환경(Development Environment)\n\n프론트엔드는 [Vite](https://vitejs-kr.github.io/)로 개발한다. 그리고 개발 언어로는 typescript를 사용한다. 그리고 react를 사용한다.\n\n백엔드는 Deno를 사용한다.\n\n## 4.3. 일정표(Schedule)\n\n<%\nconst getIssueByNumber = (n) => it.issues.filter(x=> x.number === n)[0];\n\nconst trId= (n)=>{\n const title = getIssueByNumber(n).title;\nreturn `(#${n}) ${title}`.replaceAll(/[^A-Za-z\\s0-9]/gi,\"\").toLocaleLowerCase().replaceAll(\" \",\"-\");\n}\nconst inTable = (arr) => {\n return arr.map(x=> `[#${x}](./specific.md#${trId(x)})`).join(', ')\n}\n%>\n<%\nconst timeTable = [\n [1,2,3,5,14,15,27],\n [4,6,7,8,11],\n [9,10,12,22,23],\n [13,16,17,19,20,21,30],\n [18,,24,25],\n [28,29]\n]\nconst Weeks = [\n'4.24~4.30', \n'5.1~5.7', \n'5.8~5.14', \n'5.15~5.21', \n'5.22~5.28', \n'5.29~6.4'\n]\n%>\n\n|주차|구현 기능|\n|----|--------|\n|<%= Weeks[0]%>|<%= inTable(timeTable[0]) %>|\n|<%= Weeks[1]%>|<%= inTable(timeTable[1]) %>|\n|<%= Weeks[2]%>|<%= inTable(timeTable[2]) %>|\n|<%= Weeks[3]%>|<%= inTable(timeTable[3]) %>|\n|<%= Weeks[4]%>|<%= inTable(timeTable[4]) %>|\n|<%= Weeks[5]%>|<%= inTable(timeTable[5]) %>|\n\n<% for(let weekIndex = 0; weekIndex < Weeks.length; weekIndex++) {%>\n### 주차 <%= Weeks[weekIndex]%>\n\n<%~ timeTable[weekIndex].map(n => {\n return `- [(#${n}) ${getIssueByNumber(n).title}](specific.md#${trId(n)})\\n`\n }).join('')\n%>\n\n<%}%>\n","number":[4],"sub_items":[],"path":"support.md","source_path":"support.md","parent_names":[]}},{"Chapter":{"name":"Architecture","content":"# 5. 설계\n\n## 5.1 UML\n\n### 5.1.1 Server Side UML\n\n```mermaid\nclassDiagram\nclass PermissionDescriptor {\n +canRead(path: string): boolean\n +canWrite(path: string): boolean\n +canCustom(path: string, options: any): boolean\n}\n<> PermissionDescriptor\nPermissionDescriptor <|.. PermissionImpl\nclass PermissionImpl {\n +basePath: string\n +writable: boolean\n +canRead(path: string): boolean\n +canWrite(path: string): boolean\n +canCustom(_path: string, _options: any): boolean\n}\nSessionStore o-- UserSession\nUserSession *-- PermissionDescriptor\nclass SessionStore~T~ {\n +sessions: Record\n +get(id: string): T\n +set(id: string, value: T): void\n +delete(id: string): void\n +saveToFile(path: string): Promise\n +loadFromFile(path: string): Promise\n}\nclass UserSession {\n +id: string\n +superuser: boolean\n +expiredAt: number\n +permissionSet: PermissionDescriptor\n}\n<> UserSession\n```\n```mermaid\nclassDiagram\nRouter <|.. FileServeRouter\nclass FileServeRouter {\n +fn: Handler\n +match(path: string, _ctx: MatchContext): any\n}\nclass MethodRouterBuilber {\n +handlers: MethodRouter\n +get(handler: Handler): this\n +post(handler: Handler): this\n +put(handler: Handler): this\n +delete(handler: Handler): this\n +build(): Handler\n}\nclass ResponseBuilder {\n +status: Status\n +headers: Record\n +body?: BodyInit\n +setStatus(status: Status): this\n +setHeader(key: string, value: string): this\n +setBody(body: BodyInit): this\n +redirect(location: string): this\n +build(): Response\n}\nclass MatchContext\n<> MatchContext\nclass Router~T~ {\n +match(path: string, ctx: MatchContext): T\n}\n<> Router\nRouter <|.. TreeRouter\nclass TreeRouter~T~ {\n -staticNode: Record>\n -simpleParamNode?: SimpleParamNode\n -regexParamNodes: Map>\n -fallbackNode?: Router\n -elem?: T\n -findRouter(path: string, ctx: MatchContext): [TreeRouter, string]\n +match(path: string, ctx?: MatchContext): T\n +register(path: string, elem: T): this\n +registerRouter(path: string, router: Router): void\n -setOrMerge(elem: RegElem): void\n -registerPath(path: string, elem: RegElem): void\n -singleRoute(p: string): SingleRouteOutput\n}\nMethodRouter <-- MethodRouterBuilber: Create\nRouter <|.. MethodRouter\nRouter <|.. FsRouter\nResponse <-- ResponseBuilder: Create\n```\n\n```mermaid\nclassDiagram\nclass ChunkMethodAction {\n +action: (doc: ActiveDocumentObject) => ChunkMethodHistory\n +checkConflict: (m: ChunkMethodHistory) => boolean\n +trySolveConflict: (m: ChunkMethodHistory) => boolean\n}\n<> ChunkMethodAction\nChunkMethodAction <|.. ChunkCreateAction\nclass ChunkCreateAction {\n +params: ChunkCreateMethod\n +action(doc: ActiveDocumentObject): ChunkCreateHistory\n +checkConflict(m: ChunkMethodHistory): boolean\n +trySolveConflict(m: ChunkMethodHistory): boolean\n}\nChunkMethodAction <|.. ChunkDeleteAction\nclass ChunkDeleteAction {\n +params: ChunkDeleteMethod\n +action(doc: ActiveDocumentObject): ChunkRemoveHistory\n +checkConflict(_m: ChunkMethodHistory): boolean\n +trySolveConflict(_m: ChunkMethodHistory): boolean\n}\nChunkMethodAction <|.. ChunkModifyAction\nclass ChunkModifyAction {\n +params: ChunkModifyMethod\n +action(doc: ActiveDocumentObject): ChunkModifyHistory\n +checkConflict(m: ChunkMethodHistory): boolean\n +trySolveConflict(_m: ChunkMethodHistory): boolean\n}\nChunkMethodAction <|.. ChunkMoveAction\nclass ChunkMoveAction {\n +params: ChunkMoveMethod\n +action(doc: ActiveDocumentObject): ChunkMoveHistory\n +checkConflict(_m: ChunkMethodHistory): boolean\n +trySolveConflict(_m: ChunkMethodHistory): boolean\n}\n```\n```mermaid\nclassDiagram\nDocumentObject <|.. FileDocumentObject\nclass FileDocumentObject {\n +docPath: string\n +chunks: Chunk[]\n +tags: string[]\n +updatedAt: number\n +tagsUpdatedAt: number\n +open(): Promise\n +parse(content: unknown[]): void\n +save(): Promise\n}\nclass Participant {\n +id: string\n +user: UserSession\n +send(data: string): void\n +addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void\n +removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void\n +close(): void\n}\n<> Participant\nParticipant <|.. Connection\nclass Connection {\n +id: string\n +user: UserSession\n +socket: WebSocket\n +send(data: string): void\n +addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void\n +removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void\n +close(code?: number, reason?: string): void\n}\nclass ParticipantList {\n +connections: Map\n +add(id: string, p: Participant): void\n +get(id: string): any\n +remove(id: string): void\n +unicast(id: string, message: string): void\n +broadcast(message: string): void\n}\nFileDocumentObject <|-- ActiveDocumentObject\nclass ActiveDocumentObject {\n +conns: Set\n +history: DocHistory[]\n +maxHistory: number\n +join(conn: Participant): void\n +leave(conn: Participant): void\n +updateDocHistory(method: ChunkMethodHistory): void\n +broadcastMethod(method: ChunkMethod, updatedAt: number, exclude?: Participant): void\n}\nclass DocumentStore {\n +documents: Inline\n +open(conn: Participant, docPath: string): Promise\n +close(conn: Participant, docPath: string): void\n +closeAll(conn: Participant): void\n}\nParticipantList o-- Participant\nDocumentStore o-- ActiveDocumentObject\n```\n```mermaid\nclassDiagram\nclass IFsWatcher{\n addEventListener(): void\n onNofity(e: FileWatchEvent): void\n}\n<> IFsWatcher\nclass FsWatcherImpl{\n onNotify(e: FileWatchEvent): void\n}\n\n```\n### 5.1.2 Client Side UML\n\n```mermaid\nclassDiagram\nError <|-- RPCErrorWrapper\nclass RPCErrorWrapper {\n +data?: unknown\n +code: RPCErrorCode\n +toJSON(): Inline\n}\nMessageEvent <|-- RPCNotificationEvent\nclass RPCNotificationEvent {\n +notification: ChunkNotification\n}\n```\n```mermaid\nclassDiagram\nclass ViewModelBase {\n +updateAsSource(path: string, updatedAt: number): void\n}\n<> ViewModelBase\nViewModelBase <|.. IViewModel\nclass IViewModel {\n +pageView: IPageViewModel\n}\n<> IViewModel\nIPageViewModel <|.. BlankPage\nclass BlankPage {\n +type: string\n +updateAsSource(_path: string, _updatedAt: number): void\n}\nIViewModel <|.. ViewModel\nclass ViewModel {\n +pageView: IPageViewModel\n +updateAsSource(path: string, updatedAt: number): void\n}\nViewModelBase <|.. IPageViewModel\nclass IPageViewModel {\n +type: string\n}\n<> IPageViewModel\nIPageViewModel <|.. IDocumentViewModel\nclass IDocumentViewModel {\n +updateOnNotification(notification: ChunkNotification): void\n}\n<> IDocumentViewModel\nclass ChunkListMutator {\n +add(i?: number | undefined, chunkContent?: ChunkContent | undefined): void\n +create(i?: number | undefined): void\n +addFromText(i: number, text: string): void\n +del(id: string): void\n +move(id: string, pos: number): void\n}\n<> ChunkListMutator\nclass ChunkListState {\n +chunks: Chunk[]\n +cloen(): ChunkListState\n}\nclass ChunkListStateMutator\n<> ChunkListStateMutator\nclass ChunkListHistory {\n +history: ChunkListHistoryElem[]\n +limit: number\n -applyLast(mutator: ChunkListStateMutator, updatedAt: number): void\n +current: ChunkListState\n +currentUpdatedAt: number\n +revoke(): void\n +apply(mutator: ChunkListStateMutator, updatedAt: number): boolean\n}\nclass ChunkMutator {\n +setType(t: ChunkContentType): void\n +setContent(s: string): void\n}\n<> ChunkMutator\nIDocumentViewModel <|.. DocumentViewModel\nclass DocumentViewModel {\n +type: \"document\"\n +docPath: string\n +chunks: Chunk[]\n +history: ChunkListHistory\n +buffer: Inline\n +seq: number\n +tags: string[]\n +updatedAt: number\n +tagsUpdatedAt: number\n +updateOnNotification(notification: ChunkNotification): void\n -updateMark(updatedAt: number): void\n +apply(mutator: ChunkListStateMutator, updatedAt: number, seq: number, refresh?: boolean): void\n +updateAsSource(_path: string, _updatedAt: number): void\n +useChunks(): [Chunk[], ChunkListMutator]\n +useTags(): [string[], (tags: string[]) => Promise]\n +useChunk(chunk_arg: Chunk): [Chunk, ChunkMutator]\n}\nIViewModel ..> \"1\" IPageViewModel\nViewModel ..> \"1\" IPageViewModel\nChunkListHistory ..> \"1\" ChunkListStateMutator\nChunkListHistory ..> \"1\" ChunkListState\nDocumentViewModel ..> \"1\" ChunkListHistory\nDocumentViewModel ..> \"1\" ChunkListStateMutator\nDocumentViewModel ..> \"1\" ChunkListMutator\nDocumentViewModel ..> \"1\" ChunkMutator\n```\n```mermaid\nclassDiagram\nclass ChunkListStateMutator\n<> ChunkListStateMutator\nclass ChunkListStateAddMutator\nclass ChunkListStateDeleteMutator\nclass ChunkListStateModifyMutator\nclass ChunkListStateMoveMutator\nChunkListStateMutator <|.. ChunkListStateAddMutator\nChunkListStateMutator <|.. ChunkListStateDeleteMutator\nChunkListStateMutator <|.. ChunkListStateModifyMutator\nChunkListStateMutator <|.. ChunkListStateMoveMutator\n```\n```mermaid\nclassDiagram\nclass IRPCMessageManager {\n +opened: boolean\n +close(): void\n +sendNotification(notification: ChunkNotification): void\n +addEventListener(name: \"notification\", listener: RPCMessageMessagerEventListener): void\n +addEventListener(name: string, listener: EventListenerOrEventListenerObject): void\n +removeEventListener(name: \"notification\", listener: RPCMessageMessagerEventListener): void\n +removeEventListener(name: string, listener: EventListenerOrEventListenerObject): void\n +invokeMethod(m: RPCMessageBody): Promise\n}\n<> IRPCMessageManager\nIRPCMessageManager <|.. RPCMessageManager\nclass RPCMessageManager {\n -callbackList: Map\n -curId: number\n -ws?: WebSocket | undefined\n +opened: boolean\n +open(url: string | URL, protocals?: string | undefined): Promise\n +close(): void\n -genId(): number\n +genHeader(): Inline\n +send(message: RPCMethod): Promise\n +sendNotification(message: ChunkNotification): void\n +addEventListener(type: \"notification\", callback: RPCMessageMessagerEventListener): void\n +removeEventListener(type: \"notification\", callback: RPCMessageMessagerEventListener): void\n +invokeMethod(m: RPCMessageBody): Promise\n}\nclass FsDirEntry {\n +name: string\n +isDirectory: boolean\n +isFile: boolean\n +isSymlink: boolean\n}\n<> FsDirEntry\nclass FsStatInfo {\n +isFile: boolean\n +isDirectory: boolean\n +isSymlink: boolean\n +size: number\n +mtime: Date | null\n +atime: Date | null\n +birthtime: Date | null\n}\n<> FsStatInfo\nFsStatInfo <|.. FsGetResult\nclass FsGetResult {\n +entries?: FsDirEntry[] | undefined\n}\n<> FsGetResult\nclass IFsEventMap {\n +modify: (this: IFsManager, event: MessageEvent) => void\n +create: (this: IFsManager, event: MessageEvent) => void\n +delete: (this: IFsManager, event: MessageEvent) => void\n}\n<> IFsEventMap\nclass IFsManager {\n +get(path: string): Promise\n +getStat(path: string): Promise\n +upload(filePath: string, data: BodyInit): Promise\n +delete(filePath: string): Promise\n +mkdir(path: string): Promise\n +addEventListener(name: string, listener: EventListenerOrEventListenerObject): void\n +removeEventListener(name: string, listener: EventListenerOrEventListenerObject): void\n +dispatchEvent(event: Event): boolean\n}\n<> IFsManager\nIFsManager <|.. FsManager\nclass FsManager {\n -manager: RPCMessageManager\n -prefix: string\n +get(path: string): Promise\n +getStat(filePath: string): Promise\n +upload(filePath: string, data: BodyInit): Promise\n +mkdir(filePath: string): Promise\n +delete(filePath: string): Promise\n}\nFsGetResult ..> \"*\" FsDirEntry\nIFsEventMap ..> \"1\" IFsManager\nIFsManager ..> \"1\" FsGetResult\nFsManager ..> \"1\" RPCMessageManager\nFsManager ..> \"1\" FsGetResult\n```\n\n## 5.2 의사코드\n\n의사코드는 다음과 같이 진행된다.\n\n### 서버 RPC 메세지 처리\n\n클라이언트는 RPC를 진행하기 위해 웹소켓을 연결합니다. 웹소켓의 주소 `/ws`에 \n도달하기 위해서 먼저 라우팅이 진행이 됩니다. 라우팅은 `TreeRouter` 클래스에서\n 진행됩니다.\n```\nInput: req Request\nOutput: Response\nfn Route(req){\n for node in HandlerNodes {\n if(req.url prefix is matching){\n node.Handler[matching](req)\n }\n }\n response with 404 Not Found\n}\n``` \n마침내 엔드포인트에 도달하게 되면 웹소켓을 얻어내고 새로운 연결을 등록합니다.\n```\nInput: req Request\nOutput: Response\nfn RPCHandleEndpoint(req){\n ws, res = upgradeWebSocket(req)\n user = getUserInfo(req.header)\n conn = new Connection(ws, user) \n registerParticipant(conn)\n res\n}\n```\n이제부터 메세지를 받을 수 있습니다.\n메세지가 오면 이 함수가 실행이 됩니다.\n```\nInput: message on send\nOutput: response\nfn Connection.handleMessage(msg: string){\n data = JSON.parse(msg)\n check format of data\n dispatch(data.method, data, this)\n}\n```\n\n디스패치가 성공적으로 이루어지면 RPC 작업를 처리하는 함수에 도착합니다.\n청크 작업을 예로 들겠습니다. 청크 작업에서는 권한을 확인하고\n명령의 충돌을 히스토리를 비교하며 다시 적용하며 해결합니다.\n그리고 요구된 작업을 처리하고 문서의 `updatedAt`을 업데이트하고\n문서 업데이트 사실을 이 문서를 보고 있던 참여자에게 전파합니다.\n```\nInput: conn Connection\nInput: m RPCChunkMethod\nOutput: response\n\nfn ChunkOperation(conn, m){\n doc = DocStore.getDocument(m.docPath);\n updatedAt = m.updatedAt;\n action = getAction(m);\n if !conn.userpermissionSet.canDo(Action) {\n response with PermissionError;\n }\n \n appliedList = doc.history.filter(x => x.updatedAt > updatedAt)\n for m in appliedList {\n if action.checkConflict(m) {\n resolvedAction = action.tryResolveConflict(m)\n if resolvedAction == Fail {\n response with ConflictError\n }\n action = resolvedAction\n }\n }\n \n res = action.act(doc);\n doc.updateHistory(res);\n \n subscribers = doc.getSubscribers();\n subscribers.broadcastNotification(m, exclude = conn);\n response with res\n}\n```\n\n문서 작업도 마찬가지로 이루어집니다.\n\n### 클라이언트의 메세지 처리 동기화\n\n클라이언트에서는 다음과 같은 일이 일어납니다.\n\n먼저 `notification`을 받습니다. 그러면 모든 `DocumentViewModel`에게 이벤트를 전달합니다.\n그리고 각각의 `DocumentViewModel`은 자기 문서에 일어난 일인지 확인하고 `ChunkListMutator`를 만들어서\n문서에 적용합니다.\n```\nInput: e RPCNotification\nInput: this document view model\n\nfn updateOnNotification(this,notification){\n { docPath, method, seq, updatedAt } = notification.params;\n if (docPath !== this.docPath) return;\n mutator = ChunkListMutatorFactory.createFrom(method);\n this.apply(mutator, updatedAt, seq);\n}\n```\n\n문서의 레디큐에 mutator를 집어넣고 `seq` 번호가 기다리는 것이면 실행하고 업데이트합니다.\n\n```\nInput: mutator ChunkListMutator\nInput: updatedAt Date\nInput: seq number\nInput: this Document View Model\n\n//readyQueue is priority queue.\nfn apply(this, mutator, updatedAt, seq){\n this.readyQueue.push({mutator, updatedAt, seq})\n\n if(this.readyQueue.lenght < some limit){\n while(!readyQueue.empty() &&\n this.readyQueue.top().seq === this.seq + 1)\n {\n mutator, updatedAt, seq = this.readyQueue.pop();\n mutator(this.chunks);\n this.seq = seq;\n this.updatedAt = updatedAt; \n }\n this.dispatch(new Event(\"chunksChange\"));\n }\n else {\n document.refresh();\n }\n}\n```\n\n### 다른 작업들\n\n```\nmodule chunk {\n type mode = Read | Write\n\n struct Chunk {\n id: string\n content: string\n type: string\n }\n\n newChunk() {\n { id = uuid()\n ; content = \"\"\n ; type = \"\"\n }\n }\n\n chunkViewer(chunk : Chunk, focusedChunk : State, deleteThis : () => void) : Component {\n var mode = Read\n\n var c = new Component(\n content { value = chunk.content }\n settypebutton\n editbutton\n deletebutton\n )\n\n when mode becomes Read { chunk.content = content }\n when mode becomes Write { focusedChunk = chunk.id }\n when focusedChunk is changed { mode = Read }\n\n when editbutton is clicked { mode = (mode = Read) ? Write : Read }\n when deletebutton is clicked { deleteThis() }\n when settypebutton is clicked { chunk.type = prompt() }\n\n return c\n }\n}\n```\n\n```\nmodule search {\n searchWord(chunks, word) {\n return doc.chunks.concat_map((s) => s.matchAll(word))\n }\n\n searchWordPrompt(chunks: Chunk.chunk list) {\n var word = prompt()\n var results = searchWord(chunks, word)\n\n var c = new Component(results)\n\n when result in results is selected {\n moveto(result.location)\n close()\n }\n }\n\n when Ctrl-F is pressed { searchWordPrompt() }\n}\n```\n\n```\nmodule document {\n struct Document {\n title: string\n path: Path\n tags: string set\n chunks: chunk.Chunk array\n }\n\n documentViewer(doc: document) : Component {\n var focusedChunk = null\n\n var c = new Component(\n taglist { value: tags }\n chunklist\n )\n\n delete(id) {\n i = doc.chunks.find((c) => c.id = id)\n doc.chunks.remove(i)\n }\n\n chunklist = doc.chunks.concat_map((c, i) =>\n [ divider(i), chuknViewer(c, focusedChunk, () => delete(c.id)) ])\n\n when divider(i) clicked { doc.chunks.insert(i, c) }\n when chunkViewer(c) is dropped on divider(i) { doc.chunks.move(c, i) }\n\n return c\n }\n}\n```\n\n```\nmodule filelist {\n fileList(dir : Directory, open: (File) => void) : Component {\n var c = new Component(\n filelist\n )\n\n filelist = dir.files().map((f) => button(f))\n\n when button(f) is clicked {\n open(f)\n }\n\n return c\n }\n}\n```\n\n```\nmodule settings {\n settings() : Component {\n var c = new Component(\n language = select(\"korean\", \"english\")\n theme = select(\"light\", \"dark\")\n )\n\n when language(l) is selected {\n global context.lang = l\n }\n\n when theme(t) is selected {\n global context.theme = t\n }\n\n return c\n }\n}\n\n```\nmodule frontend {\n main() : Component {\n var docv\n\n var open = (f) => {\n with doc = openfile(f) {\n docv = document.documentViewer(doc)\n }\n }\n\n var filelist = filelist.fileList(rootdir, open)\n\n var c = new Component(\n document = docv\n filelist = filelist\n )\n\n return c\n }\n}\n```\n","number":[5],"sub_items":[],"path":"architecture.md","source_path":"architecture.md","parent_names":[]}},{"Chapter":{"name":"Testing","content":"# Testing\r\n\r\n## 유닛 테스트\r\n\r\n유닛 테스트로 69.6%의 Line Coverage와 73.4%의 Function Coverage를 달성했다.\r\n다음과 같은 로그가 있다.\r\n\r\n```\r\nrunning 2 tests from ./src/auth/permission.test.ts\r\npermission.test ... ok (8ms)\r\npermission empty ... ok (16ms)\r\nrunning 4 tests from ./src/auth/session.test.ts\r\nSession ...\r\n set ... ok (9ms)\r\n delete ... ok (16ms)\r\nok (42ms)\r\nLogin Handler ...\r\n login with invalid format ... ok (15ms)\r\n login with invalid password ... ok (16ms)\r\n login ... ok (16ms)\r\n logout with no session ... ok (16ms)\r\n logout ... ok (16ms)\r\nok (96ms)\r\ngetSession ... ok (16ms)\r\ngetSession with invalid cookie ... ok (16ms)\r\nrunning 1 test from ./src/auth/user.test.ts\r\nuser.createAdminUser ... ok (15ms)\r\nrunning 4 tests from ./src/document/filedoc.test.ts\r\nreadDocFile ... ok (19ms)\r\nreadDocFile: not found ... ok (16ms)\r\nreadDocFile: invalid json ... ok (16ms)\r\nsaveDocFile ... ok (15ms)\r\nrunning 3 tests from ./src/router/methodHandle.test.ts\r\nmethodHandle: basic methods ... ok (8ms)\r\nmethodHandle: not found ... ok (16ms)\r\nmethodHandle: options ... ok (16ms)\r\nrunning 8 tests from ./src/router/route.test.ts\r\nroute: basic route ... ok (10ms)\r\nroute: double slash route ... ok (16ms)\r\nroute: double match ... ok (16ms)\r\nroute: test context ... ok (16ms)\r\nroute: test regex ... ok (16ms)\r\nroute: test not found ... ok (16ms)\r\nroute: encode_route ... ok (2ms)\r\nroute: router in router ... ok (13ms)\r\nrunning 4 tests from ./src/rpc/chunk.test.ts\r\nbasic chunk operation ...\r\n create chunk ... ok (19ms)\r\n delete chunk ... ok (15ms)\r\n modify chunk ... ok (15ms)\r\n move chunk ... ok (15ms)\r\n invalid chunk operation ... ok (17ms)\r\nok (98ms)\r\ntest chunk notification operation ... ok (15ms)\r\ntest chunk conflict ... ok (16ms)\r\ntest chunk conflict resolve with history ... ok (32ms)\r\nrunning 2 tests from ./src/rpc/doc.test.ts\r\nhandleDocumentMethod ... ok (4ms)\r\nhandleTagMethod ...\r\n setTag ... ok (13ms)\r\n getTag ... ok (15ms)\r\n conflict ... ok (15ms)\r\nok (61ms)\r\nrunning 3 tests from ./src/rpc/share.test.ts\r\nhandleShareGetInfo ... ok (18ms)\r\nhandleShareDocMethod ... ok (15ms)\r\nhandleShareMethod with no existing share token ... ok (16ms)\r\nrunning 1 test from ./src/server.test.ts\r\nserver rpc test ... ok (1s)\r\nrunning 3 tests from ./src/setting.test.ts\r\nsetting: basic ... ok (35ms)\r\nsetting: default value ... ok (7ms)\r\nsetting: defered register ... ok (16ms)\r\ntest result: ok. 35 passed (15 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (2s)\r\n```\r\n\r\n\r\n## 기능 테스트\r\n\r\n### Chunk\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
IDContentProcedureTest DataP/F
1Focus/Unfocus1. 청크를 클릭한다.P
2remove1. 청크를 삭제하는 버튼을 클릭한다.P
3-1render - markdown1. 마크다운 청크 렌더링을 확인한다. # 제목 P
3-2render - latex1. LaTex 청크 렌더링을 확인한다. sum^n_{n=0}n = \\frac{n(n+1)}2$$ P
3-3render - link1. Image 청크 렌더링을 확인한다.http://picsum.photosP
4previews1. Katex 청크의 미리보기를 본다. sum^n_{n=0}n = \\frac{n(n+1)}2$$ F
10autocomplete1. Ctrl+Space를 눌러 자동완성을 시도한다.F
11swap positions1. 청크의 위치를 바꾼다.P
27-1edit1. 청크를 수정한다.P
27-2edit chunk conflict1. 청크를 수정모드에 들어간다.F
\r\n\r\n### Document\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
IDContentProcedureTest DataP/F
5view Chunk1. 문서를 열어 청크가 렌더링되는지 본다.test.sydP
7add/delete tag1. 문서에 태그를 추가한다.
2. 문서에 태그를 삭제한다.
AP
8Drag And Drop Upload,1. 텍스트를 드래그한다.P
\r\n\r\n### File\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
IDContentProcedureTest DataP/F
14create/delete/rename file1. 파일을 만든다.test.txtP
15upload/download files1. 파일을 업로드한다.test.txtP
18export document1. export 버튼을 누른다.F
\r\n\r\n### Search\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
IDContentProcedureTest DataP/F
16Document Search1. 검색버튼을 눌러 검색을 한다.chunkF
\r\n\r\n### Stash\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
IDContentProcedureTest DataP/F
17render1. 스태시가 그려지는지 확인한다P
19add1. 청크를 추가한다P
20remove1. 청크를 삭제한다P
21Drag and Drop to Document1. 청크로부터 문서로 청크를 옮긴다.P
\r\n\r\n### Management\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
IDContentProcedureTest DataP/F
22Login1. 비밀번호를 입력한다.adminF
24Localization1. 다른언어를 지원하는지 언어를 바꿔 확인한다F
\r\n","number":[6],"sub_items":[],"path":"testing.md","source_path":"testing.md","parent_names":[]}}],"__non_exhaustive":null}] \ No newline at end of file diff --git a/tools/README.md b/tools/README.md index e589812..fb01f17 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,12 +2,15 @@ ## getIssue -Github token을 받아서 최신 100개의 Issues 들의 내용을 긁어옵니다. 그리고 그 내용을 json으로 출력합니다. +Github의 최신 100개의 Issues 들의 내용을 긁어옵니다. 그리고 그 내용을 json으로 출력합니다. +만약 private repository 이면 `GITHUB_TOKEN` 환경변수에 깃헙 token을 설정해주세요. `.env`를 지원합니다. + ```sh deno run --allow-net --allow-env --allow-write getIssue.ts --token 'YOUR GIHTUB TOKEN' ``` 으로 실행하면 됩니다. + 토큰을 발급받기 위해선 [다음 링크](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)를 참조하면 됩니다. 인자로 token과 path를 받습니다. token 인자가 지정되어 있지 않으면 `GITHUB_TOKEN` 환경변수에서 가져옵니다. path가 지정되어있지 않으면 `stdout`에 출력합니다.