Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…into Feature/#183_게스트,_유저별_workspace를_생성하고_불러오기
  • Loading branch information
hyonun321 committed Nov 26, 2024
2 parents 301046d + 911fbdf commit 2207a7b
Show file tree
Hide file tree
Showing 33 changed files with 2,055 additions and 164 deletions.
51 changes: 50 additions & 1 deletion @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
RemoteBlockReorderOperation,
RemoteBlockUpdateOperation,
serializedEditorDataProps,
RemoteCharUpdateOperation,
TextColorType,
BackgroundColorType,
} from "./Interfaces";

export class CRDT<T extends Node<NodeId>> {
Expand Down Expand Up @@ -218,14 +221,27 @@ export class BlockCRDT extends CRDT<Char> {
value: string,
blockId: BlockId,
pageId: string,
style?: string[],
color?: TextColorType,
backgroundColor?: BackgroundColorType,
): RemoteCharInsertOperation {
const id = new CharId(this.clock + 1, this.client);
const { node } = this.LinkedList.insertAtIndex(index, value, id);
const { node } = this.LinkedList.insertAtIndex(index, value, id) as { node: Char };
if (style && style.length > 0) {
node.style = style;
}
if (color) {
node.color = color;
}
if (backgroundColor) {
node.backgroundColor = backgroundColor;
}
this.clock += 1;
const operation: RemoteCharInsertOperation = {
node,
blockId,
pageId,
style: node.style || [],
};

return operation;
Expand Down Expand Up @@ -254,13 +270,33 @@ export class BlockCRDT extends CRDT<Char> {
return operation;
}

localUpdate(node: Char, blockId: BlockId, pageId: string): RemoteCharUpdateOperation {
const updatedChar = this.LinkedList.nodeMap[JSON.stringify(node.id)];
if (node.style && node.style.length > 0) {
updatedChar.style = [...node.style];
}
if (node.color) {
updatedChar.color = node.color;
}
if (node.backgroundColor !== updatedChar.backgroundColor) {
updatedChar.backgroundColor = node.backgroundColor;
}
return { node: updatedChar, blockId, pageId };
}

remoteInsert(operation: RemoteCharInsertOperation): void {
const newNodeId = new CharId(operation.node.id.clock, operation.node.id.client);
const newNode = new Char(operation.node.value, newNodeId);

newNode.next = operation.node.next;
newNode.prev = operation.node.prev;

if (operation.style && operation.style.length > 0) {
operation.style.forEach((style) => {
newNode.style.push(style);
});
}

this.LinkedList.insertById(newNode);

if (this.clock <= newNode.id.clock) {
Expand All @@ -279,6 +315,19 @@ export class BlockCRDT extends CRDT<Char> {
}
}

remoteUpdate(operation: RemoteCharUpdateOperation): void {
const updatedChar = this.LinkedList.nodeMap[JSON.stringify(operation.node.id)];
if (operation.node.style && operation.node.style.length > 0) {
updatedChar.style = [...operation.node.style];
}
if (operation.node.color) {
updatedChar.color = operation.node.color;
}
if (operation.node.backgroundColor) {
updatedChar.backgroundColor = operation.node.backgroundColor;
}
}

serialize(): CRDTSerializedProps<Char> {
return {
...super.serialize(),
Expand Down
28 changes: 28 additions & 0 deletions @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ export type ElementType = "p" | "h1" | "h2" | "h3" | "ul" | "ol" | "li" | "check

export type AnimationType = "none" | "highlight" | "gradation";

export type TextStyleType = "bold" | "italic" | "underline" | "strikethrough";

export type BackgroundColorType =
| "black"
| "red"
| "green"
| "blue"
| "white"
| "yellow"
| "purple"
| "brown"
| "transparent";

export type TextColorType = Exclude<BackgroundColorType, "transparent">;

export interface InsertOperation {
node: Block | Char;
}
Expand All @@ -22,6 +37,12 @@ export interface RemotePageCreateOperation {
page?: Page;
}

export interface RemotePageDeleteOperation {
clientId: number;
workspaceId: string;
pageId: string;
}

export interface RemoteBlockUpdateOperation {
node: Block;
pageId: string;
Expand All @@ -36,6 +57,7 @@ export interface RemoteCharInsertOperation {
node: Char;
blockId: BlockId;
pageId: string;
style?: string[];
}

export interface RemoteBlockDeleteOperation {
Expand All @@ -51,6 +73,12 @@ export interface RemoteCharDeleteOperation {
pageId: string;
}

export interface RemoteCharUpdateOperation {
node: Char;
blockId: BlockId;
pageId: string;
}

export interface CursorPosition {
clientId: number;
position: number;
Expand Down
46 changes: 27 additions & 19 deletions @noctaCrdt/LinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export abstract class LinkedList<T extends Node<NodeId>> {
return node;
}

insertAtIndex(index: number, value: string, id: T["id"]): InsertOperation {
insertAtIndex(index: number, value: string, id: T["id"]) {
try {
const node = this.createNode(value, id);
this.setNode(id, node);
Expand Down Expand Up @@ -219,52 +219,60 @@ export abstract class LinkedList<T extends Node<NodeId>> {
this.setNode(node.id, node);
}

stringify(): string {
getNodesBetween(startIndex: number, endIndex: number): T[] {
if (startIndex < 0 || endIndex < startIndex) {
throw new Error("Invalid indices");
}

const result: T[] = [];
let currentNodeId = this.head;
let result = "";
let currentIndex = 0;

while (currentNodeId !== null) {
// 시작 인덱스까지 이동
while (currentNodeId !== null && currentIndex < startIndex) {
const currentNode = this.getNode(currentNodeId);
if (!currentNode) break;
result += currentNode.value;
currentNodeId = currentNode.next;
currentIndex += 1;
}

// 시작 인덱스부터 끝 인덱스까지의 노드들 수집
while (currentNodeId !== null && currentIndex < endIndex) {
const currentNode = this.getNode(currentNodeId);
if (!currentNode) break;
result.push(currentNode);
currentNodeId = currentNode.next;
currentIndex += 1;
}

return result;
}

spread(): T[] {
stringify(): string {
let currentNodeId = this.head;
const result: T[] = [];
let result = "";

while (currentNodeId !== null) {
const currentNode = this.getNode(currentNodeId);
if (!currentNode) break;
result.push(currentNode!);
result += currentNode.value;
currentNodeId = currentNode.next;
}

return result;
}

/*
spread(): T[] {
const visited = new Set<string>();
let currentNodeId = this.head;
const result: T[] = [];
while (currentNodeId !== null) {
const nodeKey = JSON.stringify(currentNodeId);
if (visited.has(nodeKey)) break; // 순환 감지
visited.add(nodeKey);
const currentNode = this.getNode(currentNodeId);
if (!currentNode) break;
result.push(currentNode);
result.push(currentNode!);
currentNodeId = currentNode.next;
}
return result;
}
*/
}

serialize(): any {
return {
Expand Down
21 changes: 19 additions & 2 deletions @noctaCrdt/Node.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
// Node.ts
import { NodeId, BlockId, CharId } from "./NodeId";
import { AnimationType, ElementType } from "./Interfaces";
import { AnimationType, ElementType, TextColorType, BackgroundColorType } from "./Interfaces";
import { BlockCRDT } from "./Crdt";

export abstract class Node<T extends NodeId> {
id: T;
value: string;
next: T | null;
prev: T | null;
style: string[];

constructor(value: string, id: T) {
this.id = id;
this.value = value;
this.next = null;
this.prev = null;
this.style = [];
}

precedes(node: Node<T>): boolean {
Expand All @@ -32,6 +34,7 @@ export abstract class Node<T extends NodeId> {
value: this.value,
next: this.next ? this.next.serialize() : null,
prev: this.prev ? this.prev.serialize() : null,
style: this.style,
};
}

Expand Down Expand Up @@ -86,19 +89,33 @@ export class Block extends Node<BlockId> {
}

export class Char extends Node<CharId> {
style: string[];
color: TextColorType;
backgroundColor: BackgroundColorType;

constructor(value: string, id: CharId) {
super(value, id);
this.style = [];
this.color = "black";
this.backgroundColor = "transparent";
}

serialize(): any {
return super.serialize();
return {
...super.serialize(),
color: this.color,
backgroundColor: this.backgroundColor,
};
}

static deserialize(data: any): Char {
const id = CharId.deserialize(data.id);
const char = new Char(data.value, id);
char.next = data.next ? CharId.deserialize(data.next) : null;
char.prev = data.prev ? CharId.deserialize(data.prev) : null;
char.style = data.style ? data.style : [];
char.color = data.color ? data.color : "black";
char.backgroundColor = data.backgroundColor ? data.backgroundColor : "transparent";
return char;
}
}
10 changes: 10 additions & 0 deletions @noctaCrdt/WorkSpace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,17 @@ export class WorkSpace {
this.pageList.push(newPage);
return newPage;
}
remotePageDelete(operation: { pageId: string; workspaceId: string; clientId: number }): void {
const { pageId } = operation;

// pageList에서 해당 페이지의 인덱스 찾기
const pageIndex = this.pageList.findIndex((page) => page.id === pageId);

// 페이지가 존재하면 삭제
if (pageIndex !== -1) {
this.pageList.splice(pageIndex, 1);
}
}
getPage(data: string) {
const page = this.pageList.find((page) => page.id === data);
return page;
Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"dompurify": "^3.2.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
Expand Down
22 changes: 21 additions & 1 deletion client/src/components/sidebar/PageItem.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,29 @@ export const pageItemContainer = css({
"&:hover": {
background: "white/50",
cursor: "pointer",
"& .delete_box": {
visibility: "visible",
opacity: 1,
},
},
});
export const deleteBox = css({
display: "flex",
visibility: "hidden",
position: "absolute",
right: "md",
justifyContent: "center",
alignItems: "center",
borderRadius: "xs",
width: "24px",
height: "24px",
opacity: 0,
transition: "all 0.2s ease-in-out",
cursor: "pointer",
"&:hover": {
background: "gray.100",
},
});

export const iconBox = css({
display: "flex",
flexShrink: 0,
Expand Down
15 changes: 13 additions & 2 deletions client/src/components/sidebar/PageItem.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { pageItemContainer, iconBox, textBox } from "./PageItem.style";
import CloseIcon from "@assets/icons/close.svg?react";
import { pageItemContainer, iconBox, textBox, deleteBox } from "./PageItem.style";

interface PageItemProps {
id: string;
title: string;
icon?: string;
onClick: () => void;
onDelete?: (id: string) => void; // 추가: 삭제 핸들러
}

export const PageItem = ({ icon, title, onClick }: PageItemProps) => {
export const PageItem = ({ id, icon, title, onClick, onDelete }: PageItemProps) => {
// 삭제 버튼 클릭 핸들러
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation(); // 상위 요소로의 이벤트 전파 중단
onDelete?.(id);
};

return (
<div className={pageItemContainer} onClick={onClick}>
<span className={iconBox}>{icon}</span>
<span className={textBox}>{title}</span>
<span className={`delete_box ${deleteBox}`} onClick={handleDelete}>
<CloseIcon width={16} height={16} />
</span>
</div>
);
};
Loading

0 comments on commit 2207a7b

Please sign in to comment.