Skip to content

Commit

Permalink
Merge pull request #188 from boostcampwm-2024/Feature/#180_리치_텍스트_구현
Browse files Browse the repository at this point in the history
Feature/#180 리치 텍스트 구현
  • Loading branch information
github-actions[bot] authored Nov 26, 2024
2 parents 51f46f2 + 7a57f0a commit 911fbdf
Show file tree
Hide file tree
Showing 26 changed files with 1,837 additions and 140 deletions.
51 changes: 50 additions & 1 deletion @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
CRDTSerializedProps,
RemoteBlockReorderOperation,
RemoteBlockUpdateOperation,
RemoteCharUpdateOperation,
TextColorType,
BackgroundColorType,
} from "./Interfaces";

export class CRDT<T extends Node<NodeId>> {
Expand Down Expand Up @@ -217,14 +220,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 @@ -253,13 +269,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 @@ -278,6 +314,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
22 changes: 22 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 Down Expand Up @@ -42,6 +57,7 @@ export interface RemoteCharInsertOperation {
node: Char;
blockId: BlockId;
pageId: string;
style?: string[];
}

export interface RemoteBlockDeleteOperation {
Expand All @@ -57,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;
}
}
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
3 changes: 3 additions & 0 deletions client/src/constants/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export const COLOR = {
RED: "#F24150",
YELLOW: "#FEA642",
GREEN: "#1BBF44",
PURPLE: "#A142FE",
BROWN: "#8B4513",
BLUE: "#4285F4",
};
16 changes: 15 additions & 1 deletion client/src/constants/option.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnimationType, ElementType } from "@noctaCrdt/Interfaces";
import { AnimationType, ElementType, TextStyleType } from "@noctaCrdt/Interfaces";

export const OPTION_CATEGORIES = {
TYPE: {
Expand Down Expand Up @@ -36,4 +36,18 @@ export const OPTION_CATEGORIES = {
},
};

export const TEXT_OPTION_CATEGORIES = {
TYPE: {
id: "textType",
label: "글자",
options: [
{ id: "bold", label: "굵게" },
{ id: "italic", label: "기울임" },
{ id: "underline", label: "밑줄" },
{ id: "strikethrough", label: "취소선" },
] as { id: TextStyleType; label: string }[],
},
};

export type OptionCategory = keyof typeof OPTION_CATEGORIES;
export type TextOptionCategory = keyof typeof TEXT_OPTION_CATEGORIES;
Loading

0 comments on commit 911fbdf

Please sign in to comment.