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/#011 순서있는 리스트 숫자 구현 #214

Merged
merged 7 commits into from
Nov 27, 2024
15 changes: 3 additions & 12 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@
this.LinkedList = new LinkedListClass();
}

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

Check warning on line 29 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 29 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 29 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 29 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 29 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 29 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 29 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 29 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 29 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 29 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 34 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 34 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 34 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 34 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 34 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 34 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 34 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 34 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 39 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 39 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 @@ -114,7 +114,7 @@
updatedBlock.indent = block.indent;
updatedBlock.style = block.style;
updatedBlock.type = block.type;
// this.LinkedList.nodeMap[JSON.stringify(block.id)] = block;
updatedBlock.listIndex = block.listIndex || undefined;
return { node: updatedBlock, pageId };
}

Expand All @@ -125,7 +125,7 @@
updatedBlock.indent = block.indent;
updatedBlock.style = block.style;
updatedBlock.type = block.type;
// this.LinkedList.nodeMap[JSON.stringify(block.id)] = block;
updatedBlock.listIndex = block.listIndex || undefined;
return { node: updatedBlock, pageId };
}

Expand All @@ -136,14 +136,10 @@
newNode.next = operation.node.next;
newNode.prev = operation.node.prev;
newNode.indent = operation.node.indent;
newNode.listIndex = operation.node.listIndex || undefined;
this.LinkedList.insertById(newNode);

this.clock = Math.max(this.clock, operation.node.id.clock) + 1;
/*
if (this.clock <= newNode.id.clock) {
this.clock = newNode.id.clock + 1;
}
*/
}

remoteDelete(operation: RemoteBlockDeleteOperation): void {
Expand All @@ -153,11 +149,6 @@
this.LinkedList.deleteNode(targetNodeId);
}
this.clock = Math.max(this.clock, clock) + 1;
/*
if (this.clock <= clock) {
this.clock = clock + 1;
}
*/
}

localReorder(params: {
Expand Down
170 changes: 112 additions & 58 deletions @noctaCrdt/LinkedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,65 +55,9 @@ export abstract class LinkedList<T extends Node<NodeId>> {
delete this.nodeMap[JSON.stringify(id)];
}

reorderNodes({ targetId, beforeId, afterId }: ReorderNodesProps): void {
const targetNode = this.getNode(targetId);
if (!targetNode) return;

// 1. 기존 연결 해제
if (targetNode.prev) {
const prevNode = this.getNode(targetNode.prev);
if (prevNode) {
prevNode.next = targetNode.next;
}
} else {
this.head = targetNode.next;
}

if (targetNode.next) {
const nextNode = this.getNode(targetNode.next);
if (nextNode) {
nextNode.prev = targetNode.prev;
}
}

// 2. 새로운 위치에 연결
if (!beforeId) {
// 맨 앞으로 이동
const oldHead = this.head;
this.head = targetId;
targetNode.prev = null;
targetNode.next = oldHead;

if (oldHead) {
const headNode = this.getNode(oldHead);
if (headNode) {
headNode.prev = targetId;
}
}
} else if (!afterId) {
// 맨 끝으로 이동
const beforeNode = this.getNode(beforeId);
if (beforeNode) {
beforeNode.next = targetId;
targetNode.prev = beforeId;
targetNode.next = null;
}
} else {
// 중간으로 이동
const beforeNode = this.getNode(beforeId);
const afterNode = this.getNode(afterId);

if (beforeNode && afterNode) {
targetNode.prev = beforeId;
targetNode.next = afterId;
beforeNode.next = targetId;
afterNode.prev = targetId;
}
}
updateAllOrderedListIndices() {}

// 노드맵 갱신
this.setNode(targetId, targetNode);
}
reorderNodes({ targetId, beforeId, afterId }: ReorderNodesProps): void {}

findByIndex(index: number): T {
if (index < 0) {
Expand Down Expand Up @@ -299,6 +243,116 @@ export abstract class LinkedList<T extends Node<NodeId>> {
}

export class BlockLinkedList extends LinkedList<Block> {
updateAllOrderedListIndices() {
let currentNode = this.getNode(this.head);
let currentIndex = 1;

while (currentNode) {
if (currentNode.type === "ol") {
const prevNode = currentNode.prev ? this.getNode(currentNode.prev) : null;

if (!prevNode || prevNode.type !== "ol") {
// 이전 노드가 없거나 ol이 아닌 경우 1부터 시작
currentIndex = 1;
} else if (prevNode.indent !== currentNode.indent) {
// indent가 다른 경우
if (currentNode.indent > prevNode.indent) {
// indent가 증가한 경우 1부터 시작
currentIndex = 1;
} else {
// indent가 감소한 경우 같은 indent를 가진 이전 ol의 번호 다음부터 시작
let prevSameIndentNode = prevNode;
while (
prevSameIndentNode &&
(prevSameIndentNode.indent !== currentNode.indent || prevSameIndentNode.type !== "ol")
) {
if (prevSameIndentNode.prev) {
prevSameIndentNode = this.getNode(prevSameIndentNode.prev)!;
} else {
break;
}
}

if (prevSameIndentNode && prevSameIndentNode.type === "ol") {
currentIndex = prevSameIndentNode.listIndex! + 1;
} else {
currentIndex = 1;
}
}
} else {
// 같은 indent의 연속된 ol인 경우 번호 증가
currentIndex = prevNode.listIndex!;
currentIndex += 1;
}

currentNode.listIndex = currentIndex;
}

currentNode = currentNode.next ? this.getNode(currentNode.next) : null;
}
}

reorderNodes({ targetId, beforeId, afterId }: ReorderNodesProps) {
const targetNode = this.getNode(targetId);
if (!targetNode) return;

// 1. 현재 위치에서 노드 제거
if (targetNode.prev) {
const prevNode = this.getNode(targetNode.prev);
if (prevNode) prevNode.next = targetNode.next;
}

if (targetNode.next) {
const nextNode = this.getNode(targetNode.next);
if (nextNode) nextNode.prev = targetNode.prev;
}

if (this.head === targetId) {
this.head = targetNode.next;
}

// 2. 새로운 위치에 노드 삽입
if (!beforeId) {
// 맨 앞으로 이동
const oldHead = this.head;
this.head = targetId;
targetNode.prev = null;
targetNode.next = oldHead;

if (oldHead) {
const headNode = this.getNode(oldHead);
if (headNode) headNode.prev = targetId;
}
} else if (!afterId) {
// 맨 끝으로 이동
const beforeNode = this.getNode(beforeId);
if (beforeNode) {
beforeNode.next = targetId;
targetNode.prev = beforeId;
targetNode.next = null;
}
} else {
// 중간으로 이동
const beforeNode = this.getNode(beforeId);
const afterNode = this.getNode(afterId);

if (beforeNode && afterNode) {
targetNode.prev = beforeId;
targetNode.next = afterId;
beforeNode.next = targetId;
afterNode.prev = targetId;
}
}

// 노드맵 갱신
this.setNode(targetId, targetNode);

// ordered list가 포함된 경우 전체 인덱스 재계산
if (targetNode.type === "ol") {
this.updateAllOrderedListIndices();
}
}

deserializeNodeId(data: any): BlockId {
return BlockId.deserialize(data);
}
Expand Down
3 changes: 3 additions & 0 deletions @noctaCrdt/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class Block extends Node<BlockId> {
style: string[];
icon: string;
crdt: BlockCRDT;
listIndex?: number;

constructor(value: string, id: BlockId) {
super(value, id);
Expand All @@ -70,6 +71,7 @@ export class Block extends Node<BlockId> {
style: this.style,
icon: this.icon,
crdt: this.crdt.serialize(),
listIndex: this.listIndex ? this.listIndex : null,
};
}

Expand All @@ -84,6 +86,7 @@ export class Block extends Node<BlockId> {
block.style = data.style;
block.icon = data.icon;
block.crdt = BlockCRDT.deserialize(data.crdt);
block.listIndex = data.listIndex ? data.listIndex : null;
return block;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { iconContainerStyle, iconStyle } from "./IconBlock.style";

interface IconBlockProps {
type: ElementType;
index?: number;
index: number | undefined;
}

export const IconBlock = ({ type, index = 1 }: IconBlockProps) => {
Expand Down
4 changes: 2 additions & 2 deletions client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import { setInnerHTML, getTextOffset } from "../../utils/domSyncUtils";
import { IconBlock } from "../IconBlock/IconBlock";
import { MenuBlock } from "../MenuBlock/MenuBlock";
import { TextOptionModal } from "../TextOptionModal/TextOptionModal";
import { TypeOptionModal } from "../TypeOptionModal/TypeOptionModal";
import { blockAnimation } from "./Block.animation";
import { textContainerStyle, blockContainerStyle, contentWrapperStyle } from "./Block.style";
import { TypeOptionModal } from "../TypeOptionModal/TypeOptionModal";

interface BlockProps {
id: string;
Expand Down Expand Up @@ -246,7 +246,7 @@ export const Block: React.FC<BlockProps> = memo(
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
/>
<IconBlock type={block.type} index={1} />
<IconBlock type={block.type} index={block.listIndex} />
<div
ref={blockRef}
onKeyDown={(e) => onKeyDown(e, blockRef.current, block)}
Expand Down
4 changes: 4 additions & 0 deletions client/src/features/editor/hooks/useBlockOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const useBlockOptionSelect = ({
});

editorCRDT.currentBlock = copiedParent;
editorCRDT.LinkedList.updateAllOrderedListIndices();
setEditorState({
clock: editorCRDT.clock,
linkedList: editorCRDT.LinkedList,
Expand Down Expand Up @@ -178,6 +179,9 @@ export const useBlockOptionSelect = ({
editorCRDT.currentBlock = nextBlock || prevBlock || null;
}

// ol 노드의 index를 다시 설정
editorCRDT.LinkedList.updateAllOrderedListIndices();

setEditorState({
clock: editorCRDT.clock,
linkedList: editorCRDT.LinkedList,
Expand Down
Loading