Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#053 블럭내 복사 붙여넣기 구현 #192

Merged
merged 48 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ac12282
feat: Char style 속성 추가
Ludovico7 Nov 23, 2024
5563a35
build: dompurify 패키지 의존성 추가
Ludovico7 Nov 23, 2024
6427329
feat: 텍스트 타입 카테고리 추가
Ludovico7 Nov 23, 2024
8a3543f
feat: useBlockOption 서윤님 피드백 반영
Ludovico7 Nov 23, 2024
35c6e55
feat: 텍스트 옵션 모달 구현
Ludovico7 Nov 23, 2024
2dc8e0b
feat: 공통 style 속성 추가
Ludovico7 Nov 23, 2024
189d3e9
feat: 텍스트 드래그 했을 때 선택된 텍스트들 찾는 로직 추가
Ludovico7 Nov 23, 2024
20787b9
feat: sendCharUpdateOperation 소켓 연결 추가
Ludovico7 Nov 23, 2024
c2fd847
feat: 서버에서 스타일 변경 처리 추가
Ludovico7 Nov 23, 2024
07315a0
feat: 구현한 텍스트 수정 로직 에디터와 블록 구조에 반영
Ludovico7 Nov 23, 2024
ea97d1a
feat: 스타일 태그를 위한 텍스트 렌더링 방식 변경
Ludovico7 Nov 24, 2024
a70d430
fix: currentBlock과 currentCaret 에디터에서 값을 입력할때마다 갱신
Ludovico7 Nov 24, 2024
90d0e2d
feat: 텍스트 감지 방식 변경
Ludovico7 Nov 24, 2024
f0d48cd
design: 기본 글자 굵기 normal로 변경
Ludovico7 Nov 24, 2024
d646a1f
fix: 스타일 추가, 삭제 로직 변경
Ludovico7 Nov 24, 2024
dfc4d59
feat: 스타일 속성 있는 경우 처리하기 위한 파라미터 추가
Ludovico7 Nov 24, 2024
14cc2f0
fix: 불필요한 파라미터 제거
Ludovico7 Nov 24, 2024
8214475
feat: 캐럿 관리 방식 수정
Ludovico7 Nov 24, 2024
7100ac3
chore: 불필요한 console.log 제거
Ludovico7 Nov 24, 2024
b1b83f8
진행상황 저장
Ludovico7 Nov 24, 2024
685a161
feat: 텍스트 스타일 클래스 구현
Ludovico7 Nov 25, 2024
cb2cc39
fix: span기반 스타일링 방식으로 수정
Ludovico7 Nov 25, 2024
25ced5b
fix: span 구조에 맞게 렌더링 함수 수정
Ludovico7 Nov 25, 2024
6008698
fix: 캐럿 이상하게 이동하는 문제 수정
Ludovico7 Nov 25, 2024
776ed88
fix: 스타일 적용된 텍스트 삭제하도록 수정
Ludovico7 Nov 25, 2024
2df4eb6
fix: 변경된 리치 텍스트 구조에 맞게 수정
Ludovico7 Nov 25, 2024
5fceaa0
feat: 스타일 있는 텍스트 병합하거나 분할할 때, 스타일 유지되게 데이터 전송
Ludovico7 Nov 25, 2024
a630562
chore: lint 설정
Ludovico7 Nov 25, 2024
087720c
Merge branch 'dev' into Feature/#180_리치_텍스트_구현
Ludovico7 Nov 25, 2024
00f1bc5
feat: 선택된 텍스트의 스타일에 따라 버튼 디자인 변경
Ludovico7 Nov 25, 2024
fe6c112
fix: 서윤님 피드백 반영
Ludovico7 Nov 25, 2024
a21ba10
feat: 색상 변경을 위한 속성 추가
Ludovico7 Nov 25, 2024
1eeb4c3
design: 색상 변경을 위한 토큰 추가
Ludovico7 Nov 25, 2024
56006b9
feat: 텍스트 색상, 배경 update 메서드 구현
Ludovico7 Nov 25, 2024
5212733
feat: 텍스트 색상, 배경 스타일 반영을 위한 렌더링 로직 수정
Ludovico7 Nov 25, 2024
63a2ac8
feat: 텍스트 모달창 구현
Ludovico7 Nov 25, 2024
c22551b
chore: lint 적용
Ludovico7 Nov 25, 2024
3247d93
design: 색상 버튼 디자인 수정
Ludovico7 Nov 25, 2024
1e5a3fd
feat: 블록 복제, 병합, 분할 할때 스타일 같이 적용되게 수정
Ludovico7 Nov 25, 2024
e66fd21
design: 제목과 에디터 사이에 스페이서 추가
Ludovico7 Nov 25, 2024
fb88335
fix: 서윤님 피드백 반영
Ludovico7 Nov 25, 2024
bfa4393
feat: 일반 텍스트 붙여넣기 기능 구현
Ludovico7 Nov 25, 2024
f153fe7
feat: 서버로 받은 char 삽입시 스타일 적용하도록 변경
Ludovico7 Nov 25, 2024
8a28c17
feat: remoteCharInsertOperation에 스타일 속성 추가
Ludovico7 Nov 25, 2024
1e874e2
feat: 서버 인스턴스에서 operation 생성 방식 변경
Ludovico7 Nov 25, 2024
97e5093
feat: getTextOffset 유틸리티 함수로 분리
Ludovico7 Nov 25, 2024
4e43800
feat: 복사 붙여넣기 구현
Ludovico7 Nov 25, 2024
19e9844
feat: 텍스트 드래그한 후 백스페이스 클릭시 한번에 삭제하도록 구현
Ludovico7 Nov 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
CRDTSerializedProps,
RemoteBlockReorderOperation,
RemoteBlockUpdateOperation,
RemoteCharUpdateOperation,
TextColorType,
BackgroundColorType,
} from "./Interfaces";

export class CRDT<T extends Node<NodeId>> {
Expand All @@ -22,17 +25,17 @@
this.LinkedList = new LinkedListClass();
}

localInsert(index: number, value: string, blockId?: BlockId, pageId?: string): any {

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'value' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'value' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 28 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

localDelete(index: number, blockId?: BlockId, pageId?: string): any {

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'index' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'blockId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'pageId' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 33 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

remoteInsert(operation: any): void {

Check warning on line 38 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'operation' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 38 in @noctaCrdt/Crdt.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

'operation' is defined but never used. Allowed unused args must match /^_/u
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}
Expand Down Expand Up @@ -217,14 +220,27 @@
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,41 @@
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);
});
}

if (operation.color) {
newNode.color = operation.color;
}

if (operation.backgroundColor) {
newNode.backgroundColor = operation.backgroundColor;
}

this.LinkedList.insertById(newNode);

if (this.clock <= newNode.id.clock) {
Expand All @@ -278,6 +322,19 @@
}
}

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
24 changes: 24 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 @@ -36,6 +51,9 @@ export interface RemoteCharInsertOperation {
node: Char;
blockId: BlockId;
pageId: string;
style?: string[];
color?: TextColorType;
backgroundColor?: BackgroundColorType;
}

export interface RemoteBlockDeleteOperation {
Expand All @@ -51,6 +69,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
Loading