diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8cdd36e1..9fe255ce 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,4 @@ -## ๐ ๊ฐ์ - -- ์ด๋ฒ PR์์ ํด๊ฒฐํ๋ ค๋ ๋ฌธ์ ๋๋ ๊ตฌํํ ๊ธฐ๋ฅ์ ๋ํ ๊ฐ๋จํ ์ค๋ช ์ ์์ฑํฉ๋๋ค. - -## ๐ ๋ณ๊ฒฝ ์ฌํญ ์์ฝ +## ๐ ๋ณ๊ฒฝ ์ฌํญ - ๋ณ๊ฒฝ๋ ์ฃผ์ ์ฌํญ์ bullet point๋ก ์ ๋ฆฌํ์ฌ ์์ฑํฉ๋๋ค. diff --git a/.github/workflows/auto_merge_approved_pr.yml b/.github/workflows/auto_merge_approved_pr.yml new file mode 100644 index 00000000..b6c57d4b --- /dev/null +++ b/.github/workflows/auto_merge_approved_pr.yml @@ -0,0 +1,77 @@ +name: "Auto Merge Approved PRs" + +on: + pull_request_review: + types: [submitted] + +jobs: + auto_merge: + runs-on: ubuntu-latest + steps: + - name: "Check Approvals" + id: check + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const pull_number = context.payload.pull_request.number; + const reviews = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull_number, + }); + const approvals = reviews.data.filter(review => review.state === 'APPROVED'); + const approvalCount = approvals.length; + core.info(`PR #${pull_number} has ${approvalCount} approval(s).`); + return approvalCount >= 2; + - name: "Merge PR" + if: ${{ steps.check.outputs.result == 'true' }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pull_number = context.payload.pull_request.number; + + // PR ์ ๋ณด ๋ฐ ๋ณํฉ ๊ฐ๋ฅ ์ํ ํ์ธ + let pr; + for (let i = 0; i < 5; i++) { // ์ต๋ 5ํ ์ฌ์๋ + const { data } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull_number, + }); + pr = data; + + if (pr.state !== 'open') { + core.info(`PR #${pull_number} is not open (state: ${pr.state}).`); + return; + } + + if (pr.mergeable === true) { + break; // ๋ณํฉ ๊ฐ๋ฅํ๋ฉด ๋ฃจํ ์ข ๋ฃ + } else if (pr.mergeable === false) { + core.info(`PR #${pull_number} cannot be merged due to conflicts or other issues.`); + return; + } else { + // mergeable์ด null์ธ ๊ฒฝ์ฐ (๊ณ์ฐ ์ค), ์ ์ ๋๊ธฐ ํ ์ฌ์๋ + await new Promise(resolve => setTimeout(resolve, 2000)); // 2์ด ๋๊ธฐ + } + } + + if (pr.mergeable !== true) { + core.info(`PR #${pull_number} mergeable status is unknown after retries.`); + return; + } + + // PR ๋ณํฉ ์๋ + try { + await github.rest.pulls.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull_number, + }); + core.info(`PR #${pull_number} has been merged successfully.`); + } catch (error) { + core.info(`Failed to merge PR #${pull_number}: ${error.message}`); + } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..e38910c8 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,27 @@ +name: Deploy on Server + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: [self-hosted, boost-was] # ๋ผ๋ฒจ์ ํด๋นํ๋ runner๋ก ์คํ + + steps: + # 1. ๋ ํฌ์งํ ๋ฆฌ ํด๋ก + - name: Checkout Repository + uses: actions/checkout@v4 + + # 2. Docker Compose๋ก ์๋น์ค ๋น๋ ๋ฐ ์ฌ์์ + - name: Build and Deploy Docker Images + env: + NODE_ENV: production + MONGODB_URI: ${{ secrets.MONGODB_URI }} + run: | + docker-compose up -d --build + + # 3. Clean up Old Images + - name: Remove Dangling Images + run: docker image prune -f diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml new file mode 100644 index 00000000..63f6decc --- /dev/null +++ b/.github/workflows/lint_and_test.yml @@ -0,0 +1,43 @@ +name: Lint and Test + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + lint_and_test: + name: Lint and Test + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Install pnpm + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + run_install: true + + # Set up Node.js + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + # Run lint + - name: Run lint + run: pnpm eslint . + + # Run tests + - name: Run tests + run: pnpm test diff --git a/.gitignore b/.gitignore index 8140d698..07237331 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /node_modules **/node_modules -/dist +*/dist /build .DS_Store .env \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index aae9fd17..487f5a3f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,10 +1,12 @@ { - "semi": true, - "trailingComma": "all", - "singleQuote": true, + "arrowParens": "always", "printWidth": 100, "tabWidth": 2, - "arrowParens": "always", - "endOfLine": "auto", - "bracketSpacing": true -} \ No newline at end of file + "jsxSingleQuote": false, + "singleQuote": false, + + "plugins": ["@pandabox/prettier-plugin"], + "pandaFirstProps": ["as", "className", "layerStyle", "textStyle"], + "pandaStylePropsFirst": true, + "pandaSortOtherProps": true +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..721890e6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.workingDirectories": [ + { + "pattern": "./packages/*/" + } + ], + "eslint.useFlatConfig": true, + "eslint.validate": ["javascript", "typescript", "javascriptreact", "html", "typescriptreact"], + "eslint.enable": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } +} diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts new file mode 100644 index 00000000..153bb8fc --- /dev/null +++ b/@noctaCrdt/Crdt.ts @@ -0,0 +1,129 @@ +import { LinkedList } from "./LinkedList"; +import { NodeId, Node } from "./Node"; +import { RemoteInsertOperation, RemoteDeleteOperation, SerializedProps } from "./Interfaces"; + +export class CRDT { + clock: number; + client: number; + textLinkedList: LinkedList; + + constructor(client: number) { + this.clock = 0; // ์ด CRDT์ ๋ ผ๋ฆฌ์ ์๊ฐ ์ค์ + this.client = client; + this.textLinkedList = new LinkedList(); + } + + /** + * ๋ก์ปฌ์์ ์ฝ์ ์ฐ์ฐ์ ์ํํ๊ณ , ์๊ฒฉ์ ์ ํํ ์ฐ์ฐ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค. + * @param index ์ฝ์ ํ ์ธ๋ฑ์ค + * @param value ์ฝ์ ํ ๊ฐ + * @returns ์๊ฒฉ์ ์ ํํ ์ฝ์ ์ฐ์ฐ ๊ฐ์ฒด + */ + localInsert(index: number, value: string): RemoteInsertOperation { + const id = new NodeId((this.clock += 1), this.client); + const remoteInsertion = this.textLinkedList.insertAtIndex(index, value, id); + return { node: remoteInsertion.node }; + } + + /** + * ๋ก์ปฌ์์ ์ญ์ ์ฐ์ฐ์ ์ํํ๊ณ , ์๊ฒฉ์ ์ ํํ ์ฐ์ฐ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค. + * @param index ์ญ์ ํ ์ธ๋ฑ์ค + * @returns ์๊ฒฉ์ ์ ํํ ์ญ์ ์ฐ์ฐ ๊ฐ์ฒด + */ + localDelete(index: number): RemoteDeleteOperation { + // ์ ํจํ ์ธ๋ฑ์ค์ธ์ง ํ์ธ + if (index < 0 || index >= this.textLinkedList.spread().length) { + throw new Error(`์ ํจํ์ง ์์ ์ธ๋ฑ์ค์ ๋๋ค: ${index}`); + } + + // ์ญ์ ํ ๋ ธ๋ ์ฐพ๊ธฐ + const nodeToDelete = this.textLinkedList.findByIndex(index); + if (!nodeToDelete) { + throw new Error(`์ญ์ ํ ๋ ธ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ์ธ๋ฑ์ค: ${index}`); + } + + // ์ญ์ ์ฐ์ฐ ๊ฐ์ฒด ์์ฑ + const operation: RemoteDeleteOperation = { + targetId: nodeToDelete.id, + clock: this.clock + 1, + }; + + // ๋ก์ปฌ ์ญ์ ์ํ + this.textLinkedList.deleteNode(nodeToDelete.id); + + // ํด๋ก ์ ๋ฐ์ดํธ + this.clock += 1; + + return operation; + } + + /** + * ์๊ฒฉ์์ ์ฝ์ ์ฐ์ฐ์ ์์ ํ์ ๋ ์ฒ๋ฆฌํฉ๋๋ค. + * @param operation ์๊ฒฉ ์ฝ์ ์ฐ์ฐ ๊ฐ์ฒด + */ + remoteInsert(operation: RemoteInsertOperation): void { + const newNodeId = new NodeId(operation.node.id.clock, operation.node.id.client); + const newNode = new Node(operation.node.value, newNodeId); + newNode.next = operation.node.next; + newNode.prev = operation.node.prev; + this.textLinkedList.insertById(newNode); + // ๋๊ธฐํ ๋ ผ๋ฆฌ์ ์๊ฐ + if (this.clock <= newNode.id.clock) { + this.clock = newNode.id.clock + 1; + } + } + + /** + * ์๊ฒฉ์์ ์ญ์ ์ฐ์ฐ์ ์์ ํ์๋ ์ฒ๋ฆฌํฉ๋๋ค. + * @param operation ์๊ฒฉ ์ญ์ ์ฐ์ฐ ๊ฐ์ฒด + */ + remoteDelete(operation: RemoteDeleteOperation): void { + const { targetId, clock } = operation; + if (targetId) { + this.textLinkedList.deleteNode(targetId); + } + // ๋๊ธฐํ ๋ ผ๋ฆฌ์ ์๊ฐ + if (this.clock <= clock) { + this.clock = clock + 1; + } + } + + /** + * ํ์ฌ ํ ์คํธ๋ฅผ ๋ฌธ์์ด๋ก ๋ฐํํฉ๋๋ค. + * @returns ํ์ฌ ํ ์คํธ + */ + read(): string { + return this.textLinkedList.stringify(); + } + + /** + * ํ์ฌ ํ ์คํธ๋ฅผ ๋ฐฐ์ด๋ก ๋ฐํํฉ๋๋ค. + * @returns ํ์ฌ ํ ์คํธ ๋ฐฐ์ด + */ + spread(): string[] { + return this.textLinkedList.spread(); + } + + /** + * textLinkedList๋ฅผ ๋ฐํํ๋ getter ๋ฉ์๋ + * @returns LinkedList ์ธ์คํด์ค + */ + public getTextLinkedList(): LinkedList { + return this.textLinkedList; + } + + /** + * CRDT์ ์ํ๋ฅผ ์ง๋ ฌํ ๊ฐ๋ฅํ ๊ฐ์ฒด๋ก ๋ฐํํฉ๋๋ค. + * @returns ์ง๋ ฌํ ๊ฐ๋ฅํ CRDT ์ํ + */ + serialize(): SerializedProps { + return { + clock: this.clock, + client: this.client, + textLinkedList: { + head: this.textLinkedList.head, + nodeMap: this.textLinkedList.nodeMap, + }, + }; + } +} diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts new file mode 100644 index 00000000..f3359df6 --- /dev/null +++ b/@noctaCrdt/Interfaces.ts @@ -0,0 +1,32 @@ +import { NodeId, Node } from "./Node"; + +export interface InsertOperation { + node: Node; +} + +export interface DeleteOperation { + targetId: NodeId | null; + clock: number; +} +export interface RemoteInsertOperation { + node: Node; +} + +export interface RemoteDeleteOperation { + targetId: NodeId | null; + clock: number; +} + +export interface CursorPosition { + clientId: number; + position: number; +} + +export interface SerializedProps { + clock: number; + client: number; + textLinkedList: { + head: NodeId | null; + nodeMap: { [key: string]: Node }; + }; +} diff --git a/@noctaCrdt/LinkedList.ts b/@noctaCrdt/LinkedList.ts new file mode 100644 index 00000000..84b589c8 --- /dev/null +++ b/@noctaCrdt/LinkedList.ts @@ -0,0 +1,239 @@ +import { NodeId, Node } from "./Node"; +import { InsertOperation } from "./Interfaces"; + +export class LinkedList { + head: NodeId | null; + nodeMap: { [key: string]: Node }; + + constructor(initialStructure?: LinkedList) { + if (initialStructure) { + this.head = initialStructure.head; + this.nodeMap = { ...initialStructure.nodeMap }; + } else { + this.head = null; + this.nodeMap = {}; + } + } + + // ๋ ธ๋๋งต์ ๋ ธ๋ ์ถ๊ฐ ๋ฉ์๋ + setNode(id: NodeId, node: Node): void { + this.nodeMap[JSON.stringify(id)] = node; + } + + // ๋ ธ๋๋งต์์ ๋ ธ๋ ์กฐํ ๋ฉ์๋ + getNode(id: NodeId | null): Node | null { + if (!id) return null; + return this.nodeMap[JSON.stringify(id)] || null; + } + + // ๋งํฌ๋ ๋ฆฌ์คํธ์์ ๋ ธ๋๋ฅผ ์ ๊ฑฐํ๊ณ nodeMap์์ ์ญ์ + deleteNode(id: NodeId): void { + const nodeToDelete = this.getNode(id); + if (!nodeToDelete) return; + + // ์ญ์ ํ ๋ ธ๋๊ฐ ํค๋์ธ ๊ฒฝ์ฐ + if (this.head && this.head.equals(id)) { + this.head = nodeToDelete.next; + if (nodeToDelete.next) { + const nextNode = this.getNode(nodeToDelete.next); + if (nextNode) { + nextNode.prev = null; + } + } + } else { + // ์ญ์ ํ ๋ ธ๋์ ์ด์ ๋ ธ๋๋ฅผ ์ฐพ์ ์ฐ๊ฒฐ์ ๋๋๋ค. + if (nodeToDelete.prev) { + const prevNode = this.getNode(nodeToDelete.prev); + if (prevNode) { + prevNode.next = nodeToDelete.next; + if (nodeToDelete.next) { + const nextNode = this.getNode(nodeToDelete.next); + if (nextNode) { + nextNode.prev = nodeToDelete.prev; + } + } + } + } + } + + // nodeMap์์ ๋ ธ๋ ์ญ์ + delete this.nodeMap[JSON.stringify(id)]; + } + + /** + * ๋งํฌ๋ ๋ฆฌ์คํธ ์์ ํน์ ์ธ๋ฑ์ค์ ํด๋นํ๋ ๋ ธ๋๋ฅผ ์ฐพ์ต๋๋ค. + * @param index ์ฐพ์ ์ธ๋ฑ์ค (0-๋ถํฐ ์ถ๋ฐํ๋ค.) + * @returns ํด๋น ์ธ๋ฑ์ค์ ๋ ธ๋ + */ + findByIndex(index: number): Node { + if (index < 0) { + throw new Error(`๋งํฌ๋ ๋ฆฌ์คํธ์์ ํน์ ์ธ๋ฑ์ค${index}๊ฐ ์์๊ฐ ์ ๋ ฅ๋์์ต๋๋ค.`); + } + + let currentNodeId = this.head; + let currentIndex = 0; + + while (currentNodeId !== null && currentIndex < index) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) { + throw new Error( + `๋งํฌ๋ ๋ฆฌ์คํธ์์ ํน์ ์ธ๋ฑ์ค์ ํด๋นํ๋ ๋ ธ๋๋ฅผ ์ฐพ๋ค๊ฐ ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค. ${currentIndex}`, + ); + } + currentNodeId = currentNode.next; + currentIndex += 1; + } + + // ์ ํจ์ฑ ๊ฒ์ฌ + if (currentNodeId === null) { + throw new Error(`๋งํฌ๋ ๋ฆฌ์คํธ์์ ${index}๋ฅผ ์กฐํํ์ง๋ง ๋งํฌ๋ ๋ฆฌ์คํธ๊ฐ ๋น์ด์์ต๋๋ค. `); + } + const node = this.getNode(currentNodeId); + if (!node) { + throw new Error(`๋งํฌ๋ ๋ฆฌ์คํธ์์ ์ธ๋ฑ์ค ${index}์์ ๋ ธ๋๋ฅผ ๊ฐ์ ธ์ค์ง ๋ชปํ์ต๋๋ค. `); + } + + return node; + } + + /** + * ์ธ๋ฑ์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ ธ๋๋ฅผ ์ฝ์ ํฉ๋๋ค. + * ๊ธ์๋ฅผ ์์ฑํ ๋ ํน์ ์ธ๋ฑ์ค์ ์ฝ์ ํด์ผ ํ๊ธฐ ๋๋ฌธ. + * @param index ์ฝ์ ํ ์ธ๋ฑ์ค (0-based) + * @param value ์ฝ์ ํ ๊ฐ + * @param id ์ฝ์ ํ ๋ ธ๋์ ์๋ณ์ + * @returns ์ฝ์ ๋ ๋ ธ๋ + */ + insertAtIndex(index: number, value: string, id: NodeId): InsertOperation { + try { + const node = new Node(value, id); + this.setNode(id, node); + + // ํค๋์ ์ฝ์ ํ๋ ๊ฒฝ์ฐ + if (!this.head || index === -1) { + node.next = this.head; + node.prev = null; + if (this.head) { + const oldHead = this.getNode(this.head); + if (oldHead) { + oldHead.prev = id; + } + } + + this.head = id; + return { node }; + } + + // ์ฝ์ ํ ์์น์ ์ด์ ๋ ธ๋ ์ฐพ๊ธฐ + const prevNode = this.findByIndex(index - 1); + + node.next = prevNode.next; + prevNode.next = id; + node.prev = prevNode.id; + + // ๋ ธ๋์ ๋ค์๊ป ์์ผ๋ฉด node๋ฅผ ์ป๊ณ ๋ค์ ๋ ธ๋์ prev๊ฐ ์๋ก ์ถ๊ฐ๋ ๋ ธ๋๋ก ์ ๋ฐ์ดํธ + if (node.next) { + const nextNode = this.getNode(node.next); + if (nextNode) { + nextNode.prev = id; + } + } + + return { node }; + } catch (e) { + throw new Error(`๋งํฌ๋ ๋ฆฌ์คํธ ๋ด์์ insertAtIndex ์คํจ\n${e}`); + } + } + + /** + * ์๊ฒฉ ์ฝ์ ์ฐ์ฐ์ ์ฒ๋ฆฌํฉ๋๋ค. + * ์๊ฒฉ ์ฐ์ฐ์ด ์์๋๋ ์ด๋ฏธ node์ ๋ณด๊ฐ ์์ฑ๋ ์ํ๋ก ์์ ํ์ฌ ํฐ ์ฐ์ฐ์ด ํ์ ์๋ค. + * @param node ์ฝ์ ํ ๋ ธ๋ ๊ฐ์ฒด + * @returns ์์ ๋ ์ธ๋ฑ์ค (์ ํ์ฌํญ) + */ + insertById(node: Node): void { + // ์ด๋ฏธ ์กด์ฌํ๋ ๋ ธ๋๋ผ๋ฉด ๋ฌด์ + if (this.getNode(node.id)) { + return; + } + + // ๋ ธ๋์ prev๊ฐ null์ด๋ฉด ํค๋์ ์ฝ์ + if (!node.prev) { + node.next = this.head; + node.prev = null; + + if (this.head) { + const oldHead = this.getNode(this.head); + if (oldHead) { + oldHead.prev = node.id; + } + } + + this.head = node.id; + this.setNode(node.id, node); + return; + } + + // ์ฝ์ ํ ์์น์ ์ด์ ๋ ธ๋ ์ฐพ๊ธฐ + const prevNode = this.getNode(node.prev); + if (!prevNode) { + throw new Error( + `์๊ฒฉ ์ฝ์ ์, ์ด์ ๋ ธ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. prevId: ${JSON.stringify(node.prev)}`, + ); + } + + // ์ ๋ ธ๋์ ๋ค์์ ์ด์ ๋ ธ๋์ ๋ค์์ผ๋ก ์ค์ + node.next = prevNode.next; + node.prev = prevNode.id; + + // ์ด์ ๋ ธ๋์ ๋ค์์ ์ ๋ ธ๋๋ก ์ค์ + prevNode.next = node.id; + + // ์ ๋ ธ๋์ ๋ค์ ๋ ธ๋๊ฐ ์๋ค๋ฉด, ๊ทธ ๋ ธ๋์ prev๋ฅผ ์ ๋ ธ๋๋ก ์ ๋ฐ์ดํธ + if (node.next) { + const nextNode = this.getNode(node.next); + if (nextNode) { + nextNode.prev = node.id; + } + } + + // ์ ๋ ธ๋๋ฅผ nodeMap์ ์ถ๊ฐ + this.setNode(node.id, node); + } + + /** + * ํ์ฌ ๋ฆฌ์คํธ๋ฅผ ๋ฌธ์์ด๋ก ๋ณํํฉ๋๋ค. + * @returns ๋งํฌ๋ ๋ฆฌ์คํธ๋ฅผ ์ํํ์ฌ ์ป์ ๋ฌธ์์ด + */ + stringify(): string { + let currentNodeId = this.head; + let result = ""; + + while (currentNodeId !== null) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) break; + result += currentNode.value; + currentNodeId = currentNode.next; + } + + return result; + } + + /** + * ํ์ฌ ๋ฆฌ์คํธ๋ฅผ ๋ฐฐ์ด๋ก ๋ณํํฉ๋๋ค. + * @returns ๋ฐฐ์ด๋ก ๋ณํ๋ ๋ฆฌ์คํธ + */ + spread(): string[] { + let currentNodeId = this.head; + const result: string[] = []; + + while (currentNodeId !== null) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) break; + result.push(currentNode.value); + currentNodeId = currentNode.next; + } + + return result; + } +} diff --git a/@noctaCrdt/Node.ts b/@noctaCrdt/Node.ts new file mode 100644 index 00000000..2b874457 --- /dev/null +++ b/@noctaCrdt/Node.ts @@ -0,0 +1,43 @@ +export class NodeId { + clock: number; + client: number; + + constructor(clock: number, client: number) { + this.clock = clock; + this.client = client; + } + + equals(other: NodeId): boolean { + return this.clock === other.clock && this.client === other.client; + } +} + +export class Node { + id: NodeId; + value: string; + next: NodeId | null; + prev: NodeId | null; + + constructor(value: string, id: NodeId) { + this.id = id; + this.value = value; + this.next = null; + this.prev = null; + } + + /** + * ๋ ๋ ธ๋์ ์์๋ฅผ ๋น๊ตํ์ฌ, ์ด ๋ ธ๋๊ฐ ๋ค๋ฅธ ๋ ธ๋๋ณด๋ค ๋จผ์ ์์ผ ํ๋์ง ์ฌ๋ถ๋ฅผ ๋ฐํํฉ๋๋ค. + * @param node ๋น๊ตํ ๋ ธ๋ + * @returns ์์ ๊ฒฐ์ ๊ฒฐ๊ณผ + */ + precedes(node: Node): boolean { + // prev๊ฐ ๋ค๋ฅด๋ฉด ๋น๊ต ๋ถ๊ฐ + if (!this.prev || !node.prev) return false; + if (!this.prev.equals(node.prev)) return false; + + if (this.id.clock < node.id.clock) return true; + if (this.id.clock === node.id.clock && this.id.client < node.id.client) return true; + + return false; + } +} diff --git a/@noctaCrdt/dist/Crdt.d.ts b/@noctaCrdt/dist/Crdt.d.ts new file mode 100644 index 00000000..c18fa772 --- /dev/null +++ b/@noctaCrdt/dist/Crdt.d.ts @@ -0,0 +1 @@ +//# sourceMappingURL=Crdt.d.ts.map \ No newline at end of file diff --git a/@noctaCrdt/dist/Crdt.d.ts.map b/@noctaCrdt/dist/Crdt.d.ts.map new file mode 100644 index 00000000..296f43c5 --- /dev/null +++ b/@noctaCrdt/dist/Crdt.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Crdt.d.ts","sourceRoot":"","sources":["../Crdt.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/Crdt.js b/@noctaCrdt/dist/Crdt.js new file mode 100644 index 00000000..dde9413f --- /dev/null +++ b/@noctaCrdt/dist/Crdt.js @@ -0,0 +1,2 @@ +"use strict"; +//# sourceMappingURL=Crdt.js.map \ No newline at end of file diff --git a/@noctaCrdt/dist/Crdt.js.map b/@noctaCrdt/dist/Crdt.js.map new file mode 100644 index 00000000..dea3b8d8 --- /dev/null +++ b/@noctaCrdt/dist/Crdt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Crdt.js","sourceRoot":"","sources":["../Crdt.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/Interfaces.d.ts b/@noctaCrdt/dist/Interfaces.d.ts new file mode 100644 index 00000000..00f43a55 --- /dev/null +++ b/@noctaCrdt/dist/Interfaces.d.ts @@ -0,0 +1 @@ +//# sourceMappingURL=Interfaces.d.ts.map \ No newline at end of file diff --git a/@noctaCrdt/dist/Interfaces.d.ts.map b/@noctaCrdt/dist/Interfaces.d.ts.map new file mode 100644 index 00000000..2c99ef16 --- /dev/null +++ b/@noctaCrdt/dist/Interfaces.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Interfaces.d.ts","sourceRoot":"","sources":["../Interfaces.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/Interfaces.js b/@noctaCrdt/dist/Interfaces.js new file mode 100644 index 00000000..04b8a87d --- /dev/null +++ b/@noctaCrdt/dist/Interfaces.js @@ -0,0 +1,2 @@ +"use strict"; +//# sourceMappingURL=Interfaces.js.map \ No newline at end of file diff --git a/@noctaCrdt/dist/Interfaces.js.map b/@noctaCrdt/dist/Interfaces.js.map new file mode 100644 index 00000000..f5f7494b --- /dev/null +++ b/@noctaCrdt/dist/Interfaces.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Interfaces.js","sourceRoot":"","sources":["../Interfaces.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/LinkedList.d.ts b/@noctaCrdt/dist/LinkedList.d.ts new file mode 100644 index 00000000..5e7fec9e --- /dev/null +++ b/@noctaCrdt/dist/LinkedList.d.ts @@ -0,0 +1 @@ +//# sourceMappingURL=LinkedList.d.ts.map \ No newline at end of file diff --git a/@noctaCrdt/dist/LinkedList.d.ts.map b/@noctaCrdt/dist/LinkedList.d.ts.map new file mode 100644 index 00000000..22f7d5a6 --- /dev/null +++ b/@noctaCrdt/dist/LinkedList.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"LinkedList.d.ts","sourceRoot":"","sources":["../LinkedList.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/LinkedList.js b/@noctaCrdt/dist/LinkedList.js new file mode 100644 index 00000000..bca2e8bd --- /dev/null +++ b/@noctaCrdt/dist/LinkedList.js @@ -0,0 +1,2 @@ +"use strict"; +//# sourceMappingURL=LinkedList.js.map \ No newline at end of file diff --git a/@noctaCrdt/dist/LinkedList.js.map b/@noctaCrdt/dist/LinkedList.js.map new file mode 100644 index 00000000..e9833e5c --- /dev/null +++ b/@noctaCrdt/dist/LinkedList.js.map @@ -0,0 +1 @@ +{"version":3,"file":"LinkedList.js","sourceRoot":"","sources":["../LinkedList.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/Node.d.ts b/@noctaCrdt/dist/Node.d.ts new file mode 100644 index 00000000..5dc1fc3c --- /dev/null +++ b/@noctaCrdt/dist/Node.d.ts @@ -0,0 +1 @@ +//# sourceMappingURL=Node.d.ts.map \ No newline at end of file diff --git a/@noctaCrdt/dist/Node.d.ts.map b/@noctaCrdt/dist/Node.d.ts.map new file mode 100644 index 00000000..97d301b8 --- /dev/null +++ b/@noctaCrdt/dist/Node.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Node.d.ts","sourceRoot":"","sources":["../Node.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/Node.js b/@noctaCrdt/dist/Node.js new file mode 100644 index 00000000..27b1e7e8 --- /dev/null +++ b/@noctaCrdt/dist/Node.js @@ -0,0 +1,2 @@ +"use strict"; +//# sourceMappingURL=Node.js.map \ No newline at end of file diff --git a/@noctaCrdt/dist/Node.js.map b/@noctaCrdt/dist/Node.js.map new file mode 100644 index 00000000..22bd2e45 --- /dev/null +++ b/@noctaCrdt/dist/Node.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Node.js","sourceRoot":"","sources":["../Node.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/@noctaCrdt/dist/tsconfig.tsbuildinfo b/@noctaCrdt/dist/tsconfig.tsbuildinfo new file mode 100644 index 00000000..9b9c312d --- /dev/null +++ b/@noctaCrdt/dist/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"program":{"fileNames":["../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.scripthost.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/typescript@5.3.3/node_modules/typescript/lib/lib.es2021.full.d.ts","../crdt.ts","../interfaces.ts","../linkedlist.ts","../node.ts"],"fileInfos":[{"version":"f33e5332b24c3773e930e212cbb8b6867c8ba3ec4492064ea78e55a524d57450","affectsGlobalScope":true},"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","26f2f787e82c4222710f3b676b4d83eb5ad0a72fa7b746f03449e7a026ce5073","9a68c0c07ae2fa71b44384a839b7b8d81662a236d4b9ac30916718f7510b1b2d","5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569",{"version":"21e41a76098aa7a191028256e52a726baafd45a925ea5cf0222eb430c96c1d83","affectsGlobalScope":true},{"version":"35299ae4a62086698444a5aaee27fc7aa377c68cbb90b441c9ace246ffd05c97","affectsGlobalScope":true},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true},{"version":"138fb588d26538783b78d1e3b2c2cc12d55840b97bf5e08bca7f7a174fbe2f17","affectsGlobalScope":true},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true},{"version":"bc47685641087c015972a3f072480889f0d6c65515f12bd85222f49a98952ed7","affectsGlobalScope":true},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true},{"version":"93495ff27b8746f55d19fcbcdbaccc99fd95f19d057aed1bd2c0cafe1335fbf0","affectsGlobalScope":true},{"version":"6fc23bb8c3965964be8c597310a2878b53a0306edb71d4b5a4dfe760186bcc01","affectsGlobalScope":true},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true},{"version":"bb42a7797d996412ecdc5b2787720de477103a0b2e53058569069a0e2bae6c7e","affectsGlobalScope":true},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true},{"version":"b541a838a13f9234aba650a825393ffc2292dc0fc87681a5d81ef0c96d281e7a","affectsGlobalScope":true},{"version":"e0275cd0e42990dc3a16f0b7c8bca3efe87f1c8ad404f80c6db1c7c0b828c59f","affectsGlobalScope":true},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true},{"version":"49ed889be54031e1044af0ad2c603d627b8bda8b50c1a68435fe85583901d072","affectsGlobalScope":true},{"version":"e93d098658ce4f0c8a0779e6cab91d0259efb88a318137f686ad76f8410ca270","affectsGlobalScope":true},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true},{"version":"ec0104fee478075cb5171e5f4e3f23add8e02d845ae0165bfa3f1099241fa2aa","affectsGlobalScope":true},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true},{"version":"acae90d417bee324b1372813b5a00829d31c7eb670d299cd7f8f9a648ac05688","affectsGlobalScope":true},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true},{"version":"51e547984877a62227042850456de71a5c45e7fe86b7c975c6e68896c86fa23b","affectsGlobalScope":true},{"version":"62a4966981264d1f04c44eb0f4b5bdc3d81c1a54725608861e44755aa24ad6a5","affectsGlobalScope":true},{"version":"33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","affectsGlobalScope":true},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true},"c1e8d979afc15d66e2bd5a58c732d5a2ba3ccaae41ac7d5a2c539e6de66a8e51","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],"root":[[54,57]],"options":{"allowSyntheticDefaultImports":true,"composite":true,"declaration":true,"declarationMap":true,"emitDecoratorMetadata":true,"experimentalDecorators":true,"module":99,"outDir":"./","removeComments":true,"rootDir":"..","skipLibCheck":true,"sourceMap":true,"strict":true,"target":8},"referencedMap":[],"exportedModulesMap":[],"semanticDiagnosticsPerFile":[54,55,56,57,51,52,9,10,14,13,2,15,16,17,18,19,20,21,22,3,4,23,27,24,25,26,28,29,30,5,31,32,33,34,6,38,35,36,37,39,7,40,45,46,41,42,43,44,8,53,50,47,48,49,1,12,11],"latestChangedDtsFile":"./Node.d.ts"},"version":"5.3.3"} \ No newline at end of file diff --git a/@noctaCrdt/package.json b/@noctaCrdt/package.json new file mode 100644 index 00000000..8abfc264 --- /dev/null +++ b/@noctaCrdt/package.json @@ -0,0 +1,27 @@ +{ + "name": "@noctaCrdt", + "version": "1.0.0", + "main": "dist/Crdt.js", + "types": "dist/Crdt.d.ts", + "scripts": { + "build": "tsc -b" + }, + "exports": { + ".": { + "types": "./dist/Crdt.d.ts", + "default": "./dist/Crdt.js" + }, + "./Node": { + "types": "./dist/Node.d.ts", + "default": "./dist/Node.js" + }, + "./LinkedList": { + "types": "./dist/LinkedList.d.ts", + "default": "./dist/LinkedList.js" + }, + "./Interfaces": { + "types": "./dist/Interfaces.d.ts", + "default": "./dist/Interfaces.js" + } + } +} diff --git a/@noctaCrdt/tsconfig.json b/@noctaCrdt/tsconfig.json new file mode 100644 index 00000000..f1f0224a --- /dev/null +++ b/@noctaCrdt/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/client/.gitignore b/client/.gitignore index a547bf36..fa147c3e 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -22,3 +22,7 @@ dist-ssr *.njsproj *.sln *.sw? + +## Panda +styled-system +styled-system-studio \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 00000000..e3802265 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,18 @@ +# 1. Node.js 20 ๊ธฐ๋ฐ ๋น๋ ์ด๋ฏธ์ง +FROM node:20 AS build +WORKDIR /app + +# 2. ๋ชจ๋ ธ๋ ํฌ ๋ฃจํธ์์ ํ์ํ ํ์ผ ๋ณต์ฌ +COPY ./package.json ./pnpm-lock.yaml ./pnpm-workspace.yaml ./ +COPY ./client/package.json ./client/ + +# 3. pnpm ์ค์น ๋ฐ ์์กด์ฑ ์ค์น +RUN npm install -g pnpm +RUN pnpm install + +# 4. ์ ํ๋ฆฌ์ผ์ด์ ๋น๋ +COPY . . +RUN pnpm --filter client run build + +# 5. ๋น๋๋ ์ ์ ํ์ผ ์ค๋น (Nginx์์ ์ ๊ณต) +CMD ["echo", "Build completed. Use the Nginx container to serve the files."] \ No newline at end of file diff --git a/client/Dockerfile.dev b/client/Dockerfile.dev new file mode 100644 index 00000000..61f2f870 --- /dev/null +++ b/client/Dockerfile.dev @@ -0,0 +1,20 @@ +# 1. Node.js 20 ๊ธฐ๋ฐ ์ด๋ฏธ์ง +FROM node:20 +WORKDIR /app + +# 2. ๋ชจ๋ ธ๋ ํฌ ๋ฃจํธ์์ ํ์ํ ํ์ผ ๋ณต์ฌ +COPY ./package.json ./pnpm-lock.yaml ./pnpm-workspace.yaml ./ +COPY ./client/package.json ./client/ + +# 3. pnpm ์ค์น ๋ฐ ์์กด์ฑ ์ค์น +RUN npm install -g pnpm +RUN pnpm install + +# 4. ์์ค ์ฝ๋ ๋ณต์ฌ +COPY . . + +# 5. ์ ํ๋ฆฌ์ผ์ด์ ํฌํธ ๋ ธ์ถ +EXPOSE 5173 + +# 6. Vite ๊ฐ๋ฐ ์๋ฒ ์คํ +CMD ["pnpm", "--filter", "client", "run", "dev"] \ No newline at end of file diff --git a/client/eslint.config.js b/client/eslint.config.js index 5621eb7a..3c45a308 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -1,9 +1,11 @@ -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import jsxA11y from 'eslint-plugin-jsx-a11y'; -import { fileURLToPath } from 'url'; -import { dirname, resolve } from 'path'; -import rootConfig from '../eslint.config.js'; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import jsxA11y from "eslint-plugin-jsx-a11y"; +import { fileURLToPath } from "url"; +import { dirname, resolve } from "path"; +import rootConfig from "../eslint.config.js"; +import panda from "@pandacss/eslint-plugin"; +import pandabox from "@pandabox/prettier-plugin"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -13,15 +15,18 @@ export default [ ...rootConfig, { - files: ['src/**/*.{ts,tsx}'], + files: ["src/**/*.{ts,tsx}"], + ignores: ["styled-system"], plugins: { react, - 'react-hooks': reactHooks, - 'jsx-a11y': jsxA11y, + "react-hooks": reactHooks, + "jsx-a11y": jsxA11y, + "@pandacss": panda, + "@pandabox": pandabox, }, languageOptions: { parserOptions: { - project: resolve(__dirname, './tsconfig.json'), + project: resolve(__dirname, "./tsconfig.json"), ecmaFeatures: { jsx: true, }, @@ -34,53 +39,90 @@ export default [ }, rules: { // Airbnb React ๊ท์น - 'react/boolean-prop-naming': ['error', { rule: '^(is|has)[A-Z]([A-Za-z0-9]?)+' }], - 'react/function-component-definition': [ - 'warn', + "react/boolean-prop-naming": ["error", { rule: "^(is|has)[A-Z]([A-Za-z0-9]?)+" }], + "react/function-component-definition": [ + "warn", { - namedComponents: 'arrow-function', - unnamedComponents: 'arrow-function', + namedComponents: "arrow-function", + unnamedComponents: "arrow-function", }, ], - 'react/jsx-boolean-value': ['error', 'never'], - 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], - 'react/jsx-closing-tag-location': 'error', - 'react/jsx-curly-spacing': ['error', { when: 'never', children: true }], - 'react/jsx-equals-spacing': ['error', 'never'], - 'react/jsx-first-prop-new-line': ['error', 'multiline'], - 'react/jsx-handler-names': 'warn', - 'react/jsx-indent': ['error', 2], - 'react/jsx-key': 'error', - 'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }], - 'react/jsx-no-bind': 'warn', - 'react/jsx-no-duplicate-props': 'error', - 'react/jsx-pascal-case': 'error', + "react/jsx-boolean-value": ["error", "never"], + "react/jsx-closing-bracket-location": ["error", "line-aligned"], + "react/jsx-closing-tag-location": "error", + "react/jsx-curly-spacing": ["error", { when: "never", children: true }], + "react/jsx-equals-spacing": ["error", "never"], + "react/jsx-first-prop-new-line": ["error", "multiline"], + "react/jsx-handler-names": "warn", + "react/jsx-indent": ["error", 2], + "react/jsx-key": "error", + "react/jsx-max-props-per-line": ["error", { maximum: 1, when: "multiline" }], + "react/jsx-no-bind": "off", + "react/jsx-no-duplicate-props": "error", + "react/jsx-pascal-case": "error", // ๊ฐ๋ฐ ์ด๊ธฐ๋ฅผ ์ํ ๊ท์น ์ํ - 'react/react-in-jsx-scope': 'off', - 'react/jsx-props-no-spreading': 'off', - 'react/require-default-props': 'off', - 'react/prop-types': 'off', + "react/react-in-jsx-scope": "off", + "react/jsx-props-no-spreading": "off", + "react/require-default-props": "off", + "react/prop-types": "off", // React Hooks - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", // JSX A11y - ๊ฐ๋ฐ ์ด๊ธฐ์๋ ๊ฒฝ๊ณ ๋ก๋ง - 'jsx-a11y/click-events-have-key-events': 'warn', - 'jsx-a11y/no-static-element-interactions': 'warn', - 'jsx-a11y/label-has-associated-control': 'warn', + "jsx-a11y/click-events-have-key-events": "warn", + "jsx-a11y/no-static-element-interactions": "warn", + "jsx-a11y/label-has-associated-control": "warn", + + // import ์์ + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type", + ], + pathGroups: [ + { + pattern: "@/**", + group: "internal", + }, + ], + alphabetize: { + order: "asc", + }, + }, + ], + + ...panda.configs.recommended.rules, + "@pandacss/no-config-function-in-source": "off", + "@pandacss/prefer-longhand-properties": "error", }, settings: { - 'import/resolver': { + "import/resolver": { typescript: { alwaysTryTypes: true, - project: resolve(__dirname, './tsconfig.json'), + project: "./tsconfig.json", }, }, react: { - version: 'detect', + version: "detect", + }, + node: { + extensions: [".js", ".jsx", ".ts", ".tsx"], + }, + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"], }, + "import/internal-regex": "^@/", }, }, ]; diff --git a/client/index.html b/client/index.html index e4b78eae..53b30377 100644 --- a/client/index.html +++ b/client/index.html @@ -1,13 +1,16 @@ -
- - - -