Compare commits
No commits in common. "ef10264e8a1e2c5a93ba25754f45bc2b3207101c" and "f0c67b8bc4a142181f6598558a28f5e1499b8b79" have entirely different histories.
ef10264e8a
...
f0c67b8bc4
@ -6,31 +6,22 @@
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
class IPermissionDescriptor {
|
class PermissionDescriptor {
|
||||||
+canRead(path: string): boolean
|
+canRead(path: string): boolean
|
||||||
+canWrite(path: string): boolean
|
+canWrite(path: string): boolean
|
||||||
+canCustom(path: string, options: any): boolean
|
+canCustom(path: string, options: any): boolean
|
||||||
+toJSON(): any
|
|
||||||
}
|
}
|
||||||
<<Interface>> IPermissionDescriptor
|
<<Interface>> PermissionDescriptor
|
||||||
IPermissionDescriptor <|.. AdminPermission
|
PermissionDescriptor <|.. PermissionImpl
|
||||||
class AdminPermission {
|
|
||||||
+canRead(): boolean
|
|
||||||
+canWrite(): boolean
|
|
||||||
+canCustom(): boolean
|
|
||||||
+toJSON(): Inline
|
|
||||||
}
|
|
||||||
IPermissionDescriptor <|.. PermissionImpl
|
|
||||||
class PermissionImpl {
|
class PermissionImpl {
|
||||||
+basePath: string
|
+basePath: string
|
||||||
+writable: boolean
|
+writable: boolean
|
||||||
+toJSON(): Inline
|
|
||||||
+canRead(path: string): boolean
|
+canRead(path: string): boolean
|
||||||
+canWrite(path: string): boolean
|
+canWrite(path: string): boolean
|
||||||
+canCustom(_path: string, _options: any): boolean
|
+canCustom(_path: string, _options: any): boolean
|
||||||
}
|
}
|
||||||
SessionStore o-- UserSession
|
SessionStore o-- UserSession
|
||||||
UserSession *-- IUser
|
UserSession *-- PermissionDescriptor
|
||||||
class SessionStore~T~ {
|
class SessionStore~T~ {
|
||||||
+sessions: Record<string, T>
|
+sessions: Record<string, T>
|
||||||
+get(id: string): T
|
+get(id: string): T
|
||||||
@ -43,64 +34,9 @@ class UserSession {
|
|||||||
+id: string
|
+id: string
|
||||||
+superuser: boolean
|
+superuser: boolean
|
||||||
+expiredAt: number
|
+expiredAt: number
|
||||||
+permissionSet: IPermissionDescriptor
|
+permissionSet: PermissionDescriptor
|
||||||
}
|
}
|
||||||
<<Interface>> UserSession
|
<<Interface>> UserSession
|
||||||
IPermissionDescriptor <|.. IUser
|
|
||||||
class IUser {
|
|
||||||
+id: string
|
|
||||||
+expiredAt: number
|
|
||||||
+basepath: string
|
|
||||||
+joinPath(path: string): string
|
|
||||||
+relativePath(path: string): string
|
|
||||||
+setExpired(seconds: number): void
|
|
||||||
+isExpired(): boolean
|
|
||||||
+toJSON(): any
|
|
||||||
}
|
|
||||||
<<Interface>> IUser
|
|
||||||
IUser <|.. UserSessionImpl
|
|
||||||
class UserSessionImpl {
|
|
||||||
+id: string
|
|
||||||
+expiredAt: number
|
|
||||||
+permissionSet: IPermissionDescriptor
|
|
||||||
+basepath: string
|
|
||||||
+toJSON(): UserSessionImplJSON
|
|
||||||
+joinPath(path: string): string
|
|
||||||
+relativePath(path: string): string
|
|
||||||
+setExpired(seconds: number): void
|
|
||||||
+isExpired(): boolean
|
|
||||||
+canRead(path: string): boolean
|
|
||||||
+canWrite(path: string): boolean
|
|
||||||
+canCustom(path: string, options: any): boolean
|
|
||||||
}
|
|
||||||
```
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class DocumentContent {
|
|
||||||
+chunks: RPC.Chunk[]
|
|
||||||
+tags: string[]
|
|
||||||
+version: number
|
|
||||||
}
|
|
||||||
<<Interface>> DocumentContent
|
|
||||||
class DocReadWriter {
|
|
||||||
+read(path: string): Promise<DocumentContent>
|
|
||||||
+save(path: string, doc: DocumentContent): Promise<void>
|
|
||||||
}
|
|
||||||
<<Interface>> DocReadWriter
|
|
||||||
DocReadWriter <|.. MemoryDocReadWriterType
|
|
||||||
class MemoryDocReadWriterType {
|
|
||||||
-store: Map<string, DocumentContent>
|
|
||||||
+read(path: string): Promise<DocumentContent>
|
|
||||||
+save(path: string, doc: DocumentContent): Promise<void>
|
|
||||||
+clear(): void
|
|
||||||
}
|
|
||||||
DocReadWriter <|.. DocFileReadWriterType
|
|
||||||
class DocFileReadWriterType {
|
|
||||||
+rw: IReadWriter
|
|
||||||
+read(path: string): Promise<DocumentContent>
|
|
||||||
+save(path: string, doc: DocumentContent): Promise<void>
|
|
||||||
}
|
|
||||||
DocumentContent ..> DocReadWriter
|
|
||||||
```
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
@ -144,20 +80,11 @@ Router <|.. FsRouter
|
|||||||
classDiagram
|
classDiagram
|
||||||
class ResponseBuilder {
|
class ResponseBuilder {
|
||||||
+status: Status
|
+status: Status
|
||||||
+headers: Headers
|
+headers: Record<string, string>
|
||||||
+body?: BodyInit
|
+body?: BodyInit
|
||||||
+#response: Response
|
|
||||||
+#resolved: boolean
|
|
||||||
+resolved: boolean
|
|
||||||
+setStatus(status: Status): this
|
+setStatus(status: Status): this
|
||||||
+setHeader(key: string, value: string): this
|
+setHeader(key: string, value: string): this
|
||||||
+setHeaders(headers: Record<string, string>): this
|
|
||||||
+setBody(body: BodyInit): this
|
+setBody(body: BodyInit): this
|
||||||
+setResponse(response: Response, resolved?: boolean): this
|
|
||||||
+setCors(origin: string, credentials: boolean): this
|
|
||||||
+setCorsMethods(methods: string[]): this
|
|
||||||
+setContentType(contentType: string): this
|
|
||||||
+setJson(json: unknown): this
|
|
||||||
+redirect(location: string): this
|
+redirect(location: string): this
|
||||||
+build(): Response
|
+build(): Response
|
||||||
}
|
}
|
||||||
@ -206,10 +133,8 @@ class ChunkMoveAction {
|
|||||||
classDiagram
|
classDiagram
|
||||||
class Participant {
|
class Participant {
|
||||||
+id: string
|
+id: string
|
||||||
+user: IUser
|
+user: UserSession
|
||||||
+send(data: string): void
|
+send(data: string): void
|
||||||
+sendNotification(notification: RPC.RPCNotification): void
|
|
||||||
+responseWith(data: RPC.RPCResponse): void
|
|
||||||
+addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
+addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
||||||
+removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
+removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
||||||
+close(): void
|
+close(): void
|
||||||
@ -218,10 +143,8 @@ class Participant {
|
|||||||
Participant <|.. Connection
|
Participant <|.. Connection
|
||||||
class Connection {
|
class Connection {
|
||||||
+id: string
|
+id: string
|
||||||
+user: IUser
|
+user: UserSession
|
||||||
+socket: WebSocket
|
+socket: WebSocket
|
||||||
+sendNotification(notification: RPC.RPCNotification): void
|
|
||||||
+responseWith(res: RPC.RPCResponse): void
|
|
||||||
+send(data: string): void
|
+send(data: string): void
|
||||||
+addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
+addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
||||||
+removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
+removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void
|
||||||
@ -233,50 +156,33 @@ class ParticipantList {
|
|||||||
+get(id: string): any
|
+get(id: string): any
|
||||||
+remove(id: string): void
|
+remove(id: string): void
|
||||||
+unicast(id: string, message: string): void
|
+unicast(id: string, message: string): void
|
||||||
+unicastNotification(id: string, notification: RPC.RPCNotification): void
|
+broadcast(message: string): void
|
||||||
+broadcastNotification(notification: RPC.RPCNotification): void
|
|
||||||
}
|
}
|
||||||
ParticipantList o-- Participant
|
ParticipantList o-- Participant
|
||||||
```
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
class ISubscriptable {
|
DocumentObject <|.. FileDocumentObject
|
||||||
+join(participant: Participant): void
|
class FileDocumentObject {
|
||||||
+leave(participant: Participant): void
|
+docPath: string
|
||||||
+broadcastChunkMethod(method: ChunkNotificationParam, updatedAt: number, exclude?: Participant): void
|
+chunks: Chunk[]
|
||||||
+participantsCount: number
|
+tags: string[]
|
||||||
+participants: Participant[]
|
+updatedAt: number
|
||||||
|
+tagsUpdatedAt: number
|
||||||
|
+open(): Promise<void>
|
||||||
|
+parse(content: unknown[]): void
|
||||||
|
+save(): Promise<void>
|
||||||
}
|
}
|
||||||
<<Interface>> ISubscriptable
|
FileDocumentObject <|-- ActiveDocumentObject
|
||||||
ISubscriptable <|.. ActiveDocumentObject
|
|
||||||
class ActiveDocumentObject {
|
class ActiveDocumentObject {
|
||||||
-conns: RefCountSet<Participant>
|
+conns: Set<Participant>
|
||||||
-disposeHandlers: (() => void)[]
|
|
||||||
+history: DocHistory[]
|
+history: DocHistory[]
|
||||||
+maxHistory: number
|
+maxHistory: number
|
||||||
+docPath: string
|
|
||||||
+chunks: RPC.Chunk[]
|
|
||||||
+updatedAt: number
|
|
||||||
+seq: number
|
|
||||||
+#tags: string[]
|
|
||||||
+tagsUpdatedAt: number
|
|
||||||
+readWriter: DocReadWriter
|
|
||||||
+dispose(): void
|
|
||||||
+save(): Promise<void>
|
|
||||||
+setTags(tags: string[]): void
|
|
||||||
+tags: string[]
|
|
||||||
+participantsCount: any
|
|
||||||
+participants: Participant[]
|
|
||||||
+join(conn: Participant): void
|
+join(conn: Participant): void
|
||||||
+joined(conn: Participant): boolean
|
|
||||||
+leave(conn: Participant): void
|
+leave(conn: Participant): void
|
||||||
+open(): Promise<void>
|
|
||||||
+updateDocHistory(method: ChunkMethodHistory): void
|
+updateDocHistory(method: ChunkMethodHistory): void
|
||||||
+broadcastChunkMethod(method: ChunkNotificationParam, updatedAt: number, exclude?: Participant): void
|
+broadcastMethod(method: ChunkMethod, updatedAt: number, exclude?: Participant): void
|
||||||
+broadcastTagsNotification(exclude?: Participant): void
|
|
||||||
}
|
}
|
||||||
IDisposable <|.. ActiveDocumentObject
|
|
||||||
DocumentObject <|.. ActiveDocumentObject
|
|
||||||
class DocumentStore {
|
class DocumentStore {
|
||||||
+documents: Inline
|
+documents: Inline
|
||||||
+open(conn: Participant, docPath: string): Promise<ActiveDocumentObject>
|
+open(conn: Participant, docPath: string): Promise<ActiveDocumentObject>
|
||||||
@ -287,91 +193,16 @@ DocumentStore o-- ActiveDocumentObject
|
|||||||
```
|
```
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
class IDisposable {
|
class IFsWatcher{
|
||||||
+dispose(): void
|
addEventListener(): void
|
||||||
|
onNofity(e: FileWatchEvent): void
|
||||||
}
|
}
|
||||||
<<Interface>> IDisposable
|
<<interface>> IFsWatcher
|
||||||
IDisposable <|.. RefCountDisposable
|
class FsWatcherImpl{
|
||||||
class RefCountDisposable {
|
onNotify(e: FileWatchEvent): void
|
||||||
-refCount: number
|
|
||||||
-handlers: (() => void)[]
|
|
||||||
+dispose(): void
|
|
||||||
+disposeForced(): void
|
|
||||||
+addRef(): void
|
|
||||||
+addDisposeHandler(handler: () => void): void
|
|
||||||
}
|
}
|
||||||
class RefCountSet~T~ {
|
|
||||||
-items: Map<T, RefCountDisposable>
|
|
||||||
+add(item: T, disposeHandler?: () => void): void
|
|
||||||
+addDisposeHandler(item: T, handler: () => void): boolean
|
|
||||||
+delete(item: T): void
|
|
||||||
+deleteForced(item: T): void
|
|
||||||
+clear(): void
|
|
||||||
+size: number
|
|
||||||
+has(item: T): boolean
|
|
||||||
+values(): IterableIterator<T>
|
|
||||||
}
|
|
||||||
RefCountDisposable --o RefCountSet~T~
|
|
||||||
```
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
Event <|-- FsWatcherEvent
|
|
||||||
class FsWatcherEvent {
|
|
||||||
+paths: string[]
|
|
||||||
}
|
|
||||||
EventTarget <|-- FsWatcher
|
|
||||||
class FsWatcher {
|
|
||||||
-path: string
|
|
||||||
-watcher?: Deno.FsWatcher
|
|
||||||
-filterFns: ((path: string, kind: FsWatchEventType) => boolean)[]
|
|
||||||
+addFilter(fn: (path: string, kind: FsWatchEventType) => boolean): void
|
|
||||||
+removeFilter(fn: (path: string, kind: FsWatchEventType) => boolean): boolean
|
|
||||||
+startWatching(): void
|
|
||||||
+stopWatching(): void
|
|
||||||
+addEventListener(type: FsWatchEventType, handler: (e: FsWatcherEvent) => void): void
|
|
||||||
}
|
|
||||||
```
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class IReadWriter {
|
|
||||||
+read(path: string): Promise<string>
|
|
||||||
+write(path: string, content: string): Promise<void>
|
|
||||||
}
|
|
||||||
<<Interface>> IReadWriter
|
|
||||||
class MemoryReadWriter {
|
|
||||||
-data: Record<string, string>
|
|
||||||
+read(path: string): Promise<string>
|
|
||||||
+write(path: string, content: string): Promise<void>
|
|
||||||
}
|
|
||||||
IReadWriter <|.. MemoryReadWriter
|
|
||||||
IReadWriter <|.. AtomicReadWriter
|
|
||||||
class AtomicReadWriter {
|
|
||||||
+read(path: string): Promise<string>
|
|
||||||
+write(path: string, content: string): Promise<void>
|
|
||||||
}
|
|
||||||
IReadWriter <|.. WatchFilteredReadWriter
|
|
||||||
class WatchFilteredReadWriter {
|
|
||||||
-raw: AtomicReadWriter
|
|
||||||
-fsWatcher: FsWatcher
|
|
||||||
+read(path: string): Promise<string>
|
|
||||||
+write(path: string, content: string): Promise<void>
|
|
||||||
}
|
|
||||||
IReadWriter <|.. QueueReadWriter
|
|
||||||
class QueueReadWriter {
|
|
||||||
-queue: Command[]
|
|
||||||
+started: boolean
|
|
||||||
-waitedResolve: () => void
|
|
||||||
+delayCount: number
|
|
||||||
+baseReadWriter: IReadWriter
|
|
||||||
+read(path: string): Promise<string>
|
|
||||||
+write(path: string, content: string): Promise<void>
|
|
||||||
+save(path: string, content: string): void
|
|
||||||
+startTimer(): void
|
|
||||||
+flush(): Promise<void>
|
|
||||||
+wait(): Promise<void>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
```
|
||||||
### 5.1.2 Client Side UML
|
### 5.1.2 Client Side UML
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@ -777,7 +608,6 @@ fn apply(this, mutator, updatedAt, seq){
|
|||||||
|
|
||||||
### 5.2.3 다른 작업들
|
### 5.2.3 다른 작업들
|
||||||
|
|
||||||
다음은 청크 컴포넌트에 관한 의사코드이다.
|
|
||||||
```
|
```
|
||||||
module chunk {
|
module chunk {
|
||||||
type mode = Read | Write
|
type mode = Read | Write
|
||||||
@ -818,7 +648,7 @@ module chunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
다음은 검색 기능에 관한 의사코드이다.
|
|
||||||
```
|
```
|
||||||
module search {
|
module search {
|
||||||
searchWord(chunks, word) {
|
searchWord(chunks, word) {
|
||||||
@ -840,7 +670,7 @@ module search {
|
|||||||
when Ctrl-F is pressed { searchWordPrompt() }
|
when Ctrl-F is pressed { searchWordPrompt() }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
다음은 문서 컴포넌트에 관한 의사코드이다.
|
|
||||||
```
|
```
|
||||||
module document {
|
module document {
|
||||||
struct Document {
|
struct Document {
|
||||||
@ -873,7 +703,7 @@ module document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
다음은 파일리스트 컴포넌트에 관한 의사코드이다.
|
|
||||||
```
|
```
|
||||||
module filelist {
|
module filelist {
|
||||||
fileList(dir : Directory, open: (File) => void) : Component {
|
fileList(dir : Directory, open: (File) => void) : Component {
|
||||||
@ -891,7 +721,7 @@ module filelist {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
다음은 설정에 관한 의사코드이다.
|
|
||||||
```
|
```
|
||||||
module settings {
|
module settings {
|
||||||
settings() : Component {
|
settings() : Component {
|
||||||
@ -912,7 +742,6 @@ module settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
다음은 프론트엔드를 요약하는 의사코드이다.
|
|
||||||
```
|
```
|
||||||
module frontend {
|
module frontend {
|
||||||
main() : Component {
|
main() : Component {
|
||||||
@ -935,69 +764,3 @@ module frontend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5.3 동기화 정책(Synchronization Policy)
|
|
||||||
|
|
||||||
동기화 정책은 이렇다. 기본적으로 상대문서의 갱신 시간과 나의 갱신 시간을 비교하고 상대문서가 최신이 아니면 거부한다. 메소드가 실행 순서에
|
|
||||||
무관하면 언제나 실행하고 갱신한다. 메소드가 순서에 따라 조정될 수 있으면 조정한다.
|
|
||||||
|
|
||||||
예를 들어, 다음과 같은 상황이 있다.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant A as Alice
|
|
||||||
participant S as Server
|
|
||||||
participant B as Bob
|
|
||||||
S ->> A: document.open
|
|
||||||
S ->> B: document.open
|
|
||||||
A ->> S: chunk.create
|
|
||||||
B ->> S: chunk.create
|
|
||||||
S -->> A: response
|
|
||||||
S -->> B: conflict
|
|
||||||
S -) B: chunk.update
|
|
||||||
```
|
|
||||||
|
|
||||||
처음에 Server에서 Alice와 Bob이 문서를 받아왔다. 그리고 Alice가 `chunk.create` 메소드를 보내고 Bob이
|
|
||||||
`chunk.create`메소드를 보냈다. 그러면 Server는 Alice가 먼저 도착했으므로 Alice의 메소드을 처리하고 최근 갱신 시간을
|
|
||||||
갱신한다. 그리고 Bob의 메소드를 처리할때 Bob의 문서의 최근 갱신 시간이 Server 문서의 최근 갱신 시간보다 작으므로 거부한다.
|
|
||||||
|
|
||||||
별개로 Alice의 갱신사실을 알리기 위해서 Bob에게 `chunk.update`를 보낸다.
|
|
||||||
|
|
||||||
다른 예로 다음과 같은 상황에서는 이렇다.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant A as Alice
|
|
||||||
participant S as Server
|
|
||||||
participant B as Bob
|
|
||||||
S ->> A: document.open
|
|
||||||
A ->> S: chunk.create
|
|
||||||
S ->> B: document.open
|
|
||||||
S -->> A: response
|
|
||||||
S -) B: chunk.update
|
|
||||||
B ->> S: chunk.create
|
|
||||||
S -->> B: response
|
|
||||||
S -) A: chunk.update
|
|
||||||
```
|
|
||||||
|
|
||||||
여기서는 Bob의 문서가 최신이기 때문에(**문서 갱신 시간이 Server와 같기 떄문에**) 거부되지 않고 처리되는 것을 볼 수 있다.
|
|
||||||
그와 별개로 갱신 사실을 알리기 위해서 `chunk.update`가 Alice와 Bob에게 전달되는 것을 볼 수 있다.
|
|
||||||
|
|
||||||
메소드를 조정할 수 있으면 조정한다는 것은 인수를 바꾸어서 실행한다는 것이다. 예를 들어 다음의 예에 대해서
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant A as Alice
|
|
||||||
participant S as Server
|
|
||||||
participant B as Bob
|
|
||||||
S ->> A: document.open
|
|
||||||
S ->> B: document.open
|
|
||||||
A ->> S: chunk.create
|
|
||||||
B ->> S: chunk.create
|
|
||||||
S -->> A: response
|
|
||||||
S -->> B: response
|
|
||||||
S -) B: chunk.update
|
|
||||||
```
|
|
||||||
|
|
||||||
A는 1번째 위치에서 `chunk.create`에서 시도하고 B가 3번째 위치에서 `chunk.create`를 시도하면 A가 B보다 먼저 앞의 위치에서
|
|
||||||
삽입을 했으므로 B의 위치를 4번째로 조정하고 적용한다.
|
|
@ -47,10 +47,6 @@
|
|||||||
1. [Server Side UML](./architecture.md#511-server-side-uml)
|
1. [Server Side UML](./architecture.md#511-server-side-uml)
|
||||||
2. [Client Side UML](./architecture.md#512-client-side-uml)
|
2. [Client Side UML](./architecture.md#512-client-side-uml)
|
||||||
2. [의사코드(Pseudo Code)](./architecture.md#52-의사코드pseudo-code)
|
2. [의사코드(Pseudo Code)](./architecture.md#52-의사코드pseudo-code)
|
||||||
1. [서버 RPC 메세지 처리](./architecture.md#521-서버-rpc-메세지-처리)
|
|
||||||
2. [클라이언트의 메세지 처리 동기화](./architecture.md#522-클라이언트의-메세지-처리-동기화)
|
|
||||||
3. [다른 작업들](./architecture.md#523-다른-작업들)
|
|
||||||
3. [동기화 정책(Synchronization Policy)](./architecture.md#53-동기화-정책synchronization-policy)
|
|
||||||
6. [시험(Testing)](./testing.md)
|
6. [시험(Testing)](./testing.md)
|
||||||
1. [유닛 테스트(Unit test)](./testing.md#61-유닛-테스트unit-test)
|
1. [유닛 테스트(Unit test)](./testing.md#61-유닛-테스트unit-test)
|
||||||
2. [기능 테스트(Functional Test)](./testing.md#62-기능-테스트functional-test)
|
2. [기능 테스트(Functional Test)](./testing.md#62-기능-테스트functional-test)
|
Loading…
Reference in New Issue
Block a user