섹션 레이아웃 및 템플릿 스타일 개선
This commit is contained in:
@@ -114,30 +114,369 @@ export interface ButtonBlockProps {
|
||||
export interface ImageBlockProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
// 이미지 정렬: 기본은 center
|
||||
align?: "left" | "center" | "right";
|
||||
// 너비 모드: auto(콘텐츠에 맞춤) 또는 fixed(px 지정)
|
||||
widthMode?: "auto" | "fixed";
|
||||
widthPx?: number;
|
||||
// 모서리 둥글기
|
||||
borderRadius?: "none" | "sm" | "md" | "lg" | "full";
|
||||
}
|
||||
|
||||
// 구분선 블록 속성
|
||||
export interface DividerBlockProps {
|
||||
align: "left" | "center" | "right";
|
||||
thickness: "thin" | "medium";
|
||||
// 길이/너비
|
||||
widthMode?: "auto" | "full" | "fixed";
|
||||
widthPx?: number;
|
||||
// 색상
|
||||
colorHex?: string;
|
||||
// 상하 여백 (토큰, 호환용)
|
||||
marginY?: "sm" | "md" | "lg";
|
||||
// 상하 여백(px) 숫자 값
|
||||
marginYPx?: number;
|
||||
}
|
||||
|
||||
// 리스트 아이템 트리(중첩 리스트) 노드 정의
|
||||
export interface ListItemNode {
|
||||
id: string;
|
||||
text: string;
|
||||
children?: ListItemNode[];
|
||||
}
|
||||
|
||||
export interface ListLine {
|
||||
depth: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
// 리스트 아이템 트리 조작 유틸 (pure helper)
|
||||
|
||||
type ListTreeOpResult = {
|
||||
tree: ListItemNode[];
|
||||
changed: boolean;
|
||||
};
|
||||
|
||||
const cloneNode = (node: ListItemNode): ListItemNode => ({
|
||||
id: node.id,
|
||||
text: node.text,
|
||||
children: node.children ? node.children.map(cloneNode) : [],
|
||||
});
|
||||
|
||||
export const buildItemsTreeFromItems = (blockId: string, items: string[]): ListItemNode[] =>
|
||||
items.map((text, index) => ({
|
||||
id: `${blockId}_item_${index + 1}`,
|
||||
text,
|
||||
children: [],
|
||||
}));
|
||||
|
||||
export const flattenItemsTreeToItems = (tree: ListItemNode[]): string[] => {
|
||||
const result: string[] = [];
|
||||
|
||||
const walk = (nodes: ListItemNode[]) => {
|
||||
for (const node of nodes) {
|
||||
result.push(node.text);
|
||||
if (node.children && node.children.length > 0) {
|
||||
walk(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(tree);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const linesToItemsTree = (blockId: string, lines: ListLine[]): ListItemNode[] => {
|
||||
const roots: ListItemNode[] = [];
|
||||
const stack: ListItemNode[] = [];
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const depth = Number.isFinite(line.depth) && line.depth > 0 ? Math.floor(line.depth) : 0;
|
||||
|
||||
const node: ListItemNode = {
|
||||
id: `${blockId}_item_${index + 1}`,
|
||||
text: line.text,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (depth === 0 || stack.length === 0) {
|
||||
roots.push(node);
|
||||
stack.length = 1;
|
||||
stack[0] = node;
|
||||
return;
|
||||
}
|
||||
|
||||
// depth 가 현재 스택 깊이보다 크면, 스택 마지막을 부모로 사용한다.
|
||||
const parentDepth = Math.min(depth - 1, stack.length - 1);
|
||||
const parent = stack[parentDepth];
|
||||
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(node);
|
||||
|
||||
// 현재 노드를 해당 depth 위치에 두고, 그 이후 스택은 잘라낸다.
|
||||
stack.length = parentDepth + 2;
|
||||
stack[parentDepth + 1] = node;
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
export const itemsTreeToLines = (tree: ListItemNode[]): ListLine[] => {
|
||||
const lines: ListLine[] = [];
|
||||
|
||||
const walk = (nodes: ListItemNode[], depth: number) => {
|
||||
for (const node of nodes) {
|
||||
lines.push({ depth, text: node.text });
|
||||
if (node.children && node.children.length > 0) {
|
||||
walk(node.children, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(tree, 0);
|
||||
return lines;
|
||||
};
|
||||
|
||||
export const parseTextareaToLines = (text: string): ListLine[] => {
|
||||
const rawLines = text.split(/\r?\n/);
|
||||
|
||||
return rawLines.map((raw) => {
|
||||
let depth = 0;
|
||||
let i = 0;
|
||||
|
||||
while (i < raw.length) {
|
||||
const ch = raw[i];
|
||||
if (ch === "\t") {
|
||||
depth += 1;
|
||||
i += 1;
|
||||
} else if (ch === " ") {
|
||||
let spaceCount = 0;
|
||||
while (i < raw.length && raw[i] === " ") {
|
||||
spaceCount += 1;
|
||||
i += 1;
|
||||
}
|
||||
depth += Math.floor(spaceCount / 2);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const textWithoutIndent = raw.slice(i);
|
||||
return {
|
||||
depth,
|
||||
text: textWithoutIndent,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const stringifyLinesToTextarea = (lines: ListLine[]): string => {
|
||||
return lines
|
||||
.map((line) => {
|
||||
const indent = " ".repeat(Math.max(0, Math.floor(line.depth)));
|
||||
return `${indent}${line.text}`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
// 내부 재귀 유틸: siblings 배열과 targetId 를 받아 조작을 수행한다.
|
||||
const withSiblings = (
|
||||
siblings: ListItemNode[],
|
||||
targetId: string,
|
||||
op: (args: {
|
||||
siblings: ListItemNode[];
|
||||
index: number;
|
||||
parentPath: ListItemNode[];
|
||||
}) => ListTreeOpResult,
|
||||
parentPath: ListItemNode[] = [],
|
||||
): ListTreeOpResult => {
|
||||
for (let i = 0; i < siblings.length; i += 1) {
|
||||
const node = siblings[i];
|
||||
if (node.id === targetId) {
|
||||
return op({ siblings, index: i, parentPath });
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const childResult = withSiblings(node.children, targetId, op, [...parentPath, node]);
|
||||
if (childResult.changed) {
|
||||
const cloned = siblings.map(cloneNode);
|
||||
const idx = cloned.findIndex((n) => n.id === node.id);
|
||||
if (idx >= 0) {
|
||||
cloned[idx].children = childResult.tree;
|
||||
}
|
||||
return { tree: cloned, changed: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tree: siblings, changed: false };
|
||||
};
|
||||
|
||||
export const indentListItem = (tree: ListItemNode[], targetId: string): ListItemNode[] => {
|
||||
const { tree: next } = withSiblings(
|
||||
tree,
|
||||
targetId,
|
||||
({ siblings, index }) => {
|
||||
// 첫 번째 아이템은 들여쓰기 불가
|
||||
if (index === 0) {
|
||||
return { tree: siblings, changed: false };
|
||||
}
|
||||
|
||||
const prev = siblings[index - 1];
|
||||
const target = siblings[index];
|
||||
|
||||
const newPrev: ListItemNode = cloneNode(prev);
|
||||
const newTarget: ListItemNode = cloneNode(target);
|
||||
const newSiblings = siblings.map(cloneNode);
|
||||
|
||||
// siblings 에서 target 제거
|
||||
newSiblings.splice(index, 1);
|
||||
|
||||
// prev 위치의 노드를 newPrev 로 교체
|
||||
const prevIndex = index - 1;
|
||||
const prevNode = newSiblings[prevIndex];
|
||||
const prevChildren = prevNode.children ? [...prevNode.children] : [];
|
||||
prevChildren.push(newTarget);
|
||||
newSiblings[prevIndex] = { ...newPrev, children: prevChildren };
|
||||
|
||||
return { tree: newSiblings, changed: true };
|
||||
},
|
||||
);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const outdentListItem = (tree: ListItemNode[], targetId: string): ListItemNode[] => {
|
||||
// 부모의 children 에서 targetId 를 찾아 제거하고,
|
||||
// 부모가 속한 siblings 배열에서 부모 바로 다음 위치에 target 을 삽입한다.
|
||||
|
||||
const liftFromChildren = (nodes: ListItemNode[]): ListTreeOpResult => {
|
||||
const clonedSiblings = nodes.map(cloneNode);
|
||||
|
||||
for (let i = 0; i < clonedSiblings.length; i += 1) {
|
||||
const node = clonedSiblings[i];
|
||||
const children = node.children ?? [];
|
||||
|
||||
// 1단계: 현재 node 의 children 에 target 이 있는지 확인한다.
|
||||
const childIndex = children.findIndex((c) => c.id === targetId);
|
||||
if (childIndex >= 0) {
|
||||
const child = children[childIndex];
|
||||
const newChildren = [...children];
|
||||
newChildren.splice(childIndex, 1);
|
||||
|
||||
// 부모의 children 에서 target 제거
|
||||
clonedSiblings[i] = {
|
||||
...node,
|
||||
children: newChildren,
|
||||
};
|
||||
|
||||
// 부모 바로 뒤에 target 을 siblings 레벨로 삽입
|
||||
clonedSiblings.splice(i + 1, 0, cloneNode(child));
|
||||
|
||||
return { tree: clonedSiblings, changed: true };
|
||||
}
|
||||
|
||||
// 2단계: 더 깊은 레벨에서 검색한다.
|
||||
if (children.length > 0) {
|
||||
const childResult = liftFromChildren(children);
|
||||
if (childResult.changed) {
|
||||
clonedSiblings[i] = {
|
||||
...node,
|
||||
children: childResult.tree,
|
||||
};
|
||||
return { tree: clonedSiblings, changed: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { tree: nodes, changed: false };
|
||||
};
|
||||
|
||||
const { tree: next } = liftFromChildren(tree);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const moveListItemUp = (tree: ListItemNode[], targetId: string): ListItemNode[] => {
|
||||
const { tree: next } = withSiblings(
|
||||
tree,
|
||||
targetId,
|
||||
({ siblings, index }) => {
|
||||
if (index === 0) {
|
||||
return { tree: siblings, changed: false };
|
||||
}
|
||||
const cloned = siblings.map(cloneNode);
|
||||
const tmp = cloned[index - 1];
|
||||
cloned[index - 1] = cloned[index];
|
||||
cloned[index] = tmp;
|
||||
return { tree: cloned, changed: true };
|
||||
},
|
||||
);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const moveListItemDown = (tree: ListItemNode[], targetId: string): ListItemNode[] => {
|
||||
const { tree: next } = withSiblings(
|
||||
tree,
|
||||
targetId,
|
||||
({ siblings, index }) => {
|
||||
if (index >= siblings.length - 1) {
|
||||
return { tree: siblings, changed: false };
|
||||
}
|
||||
const cloned = siblings.map(cloneNode);
|
||||
const tmp = cloned[index + 1];
|
||||
cloned[index + 1] = cloned[index];
|
||||
cloned[index] = tmp;
|
||||
return { tree: cloned, changed: true };
|
||||
},
|
||||
);
|
||||
return next;
|
||||
};
|
||||
|
||||
// 리스트 블록 속성
|
||||
export interface ListBlockProps {
|
||||
items: string[];
|
||||
items: string[]; // 1단계 하위호환용 단순 배열
|
||||
itemsTree?: ListItemNode[]; // 중첩 리스트용 트리 구조
|
||||
ordered: boolean;
|
||||
align: "left" | "center" | "right";
|
||||
// 타이포/색상
|
||||
fontSizeCustom?: string;
|
||||
lineHeightCustom?: string;
|
||||
textColorCustom?: string;
|
||||
// 불릿/간격
|
||||
bulletStyle?:
|
||||
| "disc"
|
||||
| "circle"
|
||||
| "square"
|
||||
| "decimal"
|
||||
| "none"
|
||||
| "lower-alpha"
|
||||
| "upper-alpha"
|
||||
| "lower-roman"
|
||||
| "upper-roman";
|
||||
// 토큰 기반 간격(호환용)
|
||||
gapY?: "sm" | "md" | "lg";
|
||||
// 아이템 간 여백(px) 숫자 값
|
||||
gapYPx?: number;
|
||||
}
|
||||
|
||||
// 섹션 블록 속성
|
||||
export interface SectionBlockProps {
|
||||
background: "default" | "muted" | "primary";
|
||||
paddingY: "sm" | "md" | "lg";
|
||||
// px 단위 세로 패딩(있으면 토큰 대신 우선 적용)
|
||||
paddingYPx?: number;
|
||||
// 레이아웃 컬럼 정의 (12 그리드 기준 span)
|
||||
columns: Array<{
|
||||
id: string;
|
||||
span: number;
|
||||
}>;
|
||||
// 섹션 레이아웃 스타일
|
||||
maxWidthMode?: "narrow" | "normal" | "wide";
|
||||
// px 단위 최대 폭(있으면 토큰 기반 maxWidth 대신 우선 적용)
|
||||
maxWidthPx?: number;
|
||||
gapX?: "sm" | "md" | "lg";
|
||||
// px 단위 컬럼 간 간격(있으면 토큰 기반 gap 대신 우선 적용)
|
||||
gapXPx?: number;
|
||||
alignItems?: "top" | "center" | "bottom";
|
||||
backgroundColorCustom?: string;
|
||||
}
|
||||
|
||||
// 폼 입력/셀렉트/체크박스/라디오 공통 스타일 속성
|
||||
@@ -285,6 +624,7 @@ export interface Block {
|
||||
export interface EditorState {
|
||||
blocks: Block[];
|
||||
selectedBlockId: string | null;
|
||||
selectedListItemId?: string | null;
|
||||
history: Block[][];
|
||||
future: Block[][];
|
||||
addTextBlock: () => void;
|
||||
@@ -307,6 +647,11 @@ export interface EditorState {
|
||||
addBlogTemplateSection: () => void;
|
||||
addTeamTemplateSection: () => void;
|
||||
addFooterTemplateSection: () => void;
|
||||
selectListItem: (itemId: string | null) => void;
|
||||
indentSelectedListItem: (blockId: string) => void;
|
||||
outdentSelectedListItem: (blockId: string) => void;
|
||||
moveSelectedListItemUp: (blockId: string) => void;
|
||||
moveSelectedListItemDown: (blockId: string) => void;
|
||||
updateBlock: (
|
||||
id: string,
|
||||
partial:
|
||||
@@ -354,52 +699,53 @@ const getNextFormFieldName = (blocks: Block[], prefix: string): string => {
|
||||
// 에디터 스토어 생성 함수 (테스트 및 앱에서 공유 사용)
|
||||
// set/get 은 zustand 내부 구현에 의해 주입되므로, 여기서는 any 로 완화해 사용한다.
|
||||
const createEditorState = (set: any, get: any): EditorState => ({
|
||||
blocks: [],
|
||||
selectedBlockId: null,
|
||||
history: [],
|
||||
future: [],
|
||||
blocks: [],
|
||||
selectedBlockId: null,
|
||||
selectedListItemId: null,
|
||||
history: [],
|
||||
future: [],
|
||||
|
||||
// 텍스트 블록 추가: 기본 텍스트와 함께 생성 후 선택 상태로 만든다
|
||||
addTextBlock: () => {
|
||||
const id = createId();
|
||||
// 텍스트 블록 추가: 기본 텍스트와 함께 생성 후 선택 상태로 만든다
|
||||
addTextBlock: () => {
|
||||
const id = createId();
|
||||
|
||||
const { selectedBlockId, blocks } = get();
|
||||
let sectionId: string | null = null;
|
||||
let columnId: string | null = null;
|
||||
const { selectedBlockId, blocks } = get();
|
||||
let sectionId: string | null = null;
|
||||
let columnId: string | null = null;
|
||||
|
||||
if (selectedBlockId) {
|
||||
const target = blocks.find((b: Block) => b.id === selectedBlockId);
|
||||
if (target) {
|
||||
if (target.type === "section") {
|
||||
const sProps = target.props as SectionBlockProps;
|
||||
sectionId = target.id;
|
||||
columnId = sProps.columns?.[0]?.id ?? null;
|
||||
} else {
|
||||
sectionId = (target as any).sectionId ?? null;
|
||||
columnId = (target as any).columnId ?? null;
|
||||
}
|
||||
if (selectedBlockId) {
|
||||
const target = blocks.find((b: Block) => b.id === selectedBlockId);
|
||||
if (target) {
|
||||
if (target.type === "section") {
|
||||
const sProps = target.props as SectionBlockProps;
|
||||
sectionId = target.id;
|
||||
columnId = sProps.columns?.[0]?.id ?? null;
|
||||
} else {
|
||||
sectionId = (target as any).sectionId ?? null;
|
||||
columnId = (target as any).columnId ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newBlock: Block = {
|
||||
id,
|
||||
type: "text",
|
||||
props: {
|
||||
text: "새 텍스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
sectionId,
|
||||
columnId,
|
||||
};
|
||||
const newBlock: Block = {
|
||||
id,
|
||||
type: "text",
|
||||
props: {
|
||||
text: "새 텍스트",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
sectionId,
|
||||
columnId,
|
||||
};
|
||||
|
||||
set((state: EditorState) => ({
|
||||
blocks: [...state.blocks, newBlock],
|
||||
selectedBlockId: id,
|
||||
history: [...state.history, blocks],
|
||||
future: [],
|
||||
}));
|
||||
},
|
||||
set((state: EditorState) => ({
|
||||
blocks: [...state.blocks, newBlock],
|
||||
selectedBlockId: id,
|
||||
history: [...state.history, blocks],
|
||||
future: [],
|
||||
}));
|
||||
},
|
||||
|
||||
// Hero 템플릿 섹션 추가: 섹션 1개와 기본 텍스트/버튼 블록들을 첫 컬럼에 배치한다
|
||||
addHeroTemplateSection: () => {
|
||||
@@ -419,6 +765,11 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
});
|
||||
},
|
||||
|
||||
// 리스트 아이템 선택 상태를 업데이트한다.
|
||||
selectListItem: (itemId) => {
|
||||
set({ selectedListItemId: itemId ?? null });
|
||||
},
|
||||
|
||||
// Features 템플릿 섹션 추가: 3컬럼(4/4/4) 섹션과 각 컬럼의 제목/설명 텍스트를 생성한다
|
||||
addFeaturesTemplateSection: () => {
|
||||
const sectionId = createId();
|
||||
@@ -566,7 +917,7 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
});
|
||||
},
|
||||
|
||||
// 이미지 블록 추가: 기본 플레이스홀더와 함께 생성 후 선택 상태로 만든다
|
||||
// 이미지 블록 추가: 기본 플레이스홀더와 스타일 기본값과 함께 생성 후 선택 상태로 만든다
|
||||
addImageBlock: () => {
|
||||
const id = createId();
|
||||
|
||||
@@ -594,6 +945,9 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
props: {
|
||||
src: "",
|
||||
alt: "이미지 설명",
|
||||
align: "center",
|
||||
widthMode: "auto",
|
||||
borderRadius: "md",
|
||||
},
|
||||
sectionId,
|
||||
columnId,
|
||||
@@ -820,6 +1174,13 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
type: "list",
|
||||
props: {
|
||||
items: ["리스트 아이템 1"],
|
||||
itemsTree: [
|
||||
{
|
||||
id: `${id}_item_1`,
|
||||
text: "리스트 아이템 1",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
ordered: false,
|
||||
align: "left",
|
||||
},
|
||||
@@ -970,6 +1331,130 @@ const createEditorState = (set: any, get: any): EditorState => ({
|
||||
}));
|
||||
},
|
||||
|
||||
// 선택된 리스트 아이템을 들여쓰기한다.
|
||||
indentSelectedListItem: (blockId) => {
|
||||
const { blocks, selectedListItemId } = get();
|
||||
if (!selectedListItemId) return;
|
||||
|
||||
const target = blocks.find((b: Block) => b.id === blockId && b.type === "list");
|
||||
if (!target) return;
|
||||
|
||||
const listProps = target.props as ListBlockProps & { itemsTree?: ListItemNode[] };
|
||||
const currentTree: ListItemNode[] =
|
||||
(listProps.itemsTree as ListItemNode[] | undefined) && listProps.itemsTree!.length > 0
|
||||
? (listProps.itemsTree as ListItemNode[])
|
||||
: [];
|
||||
|
||||
const nextTree = indentListItem(currentTree, selectedListItemId);
|
||||
|
||||
set((state: EditorState) => ({
|
||||
blocks: state.blocks.map((block: Block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
props: {
|
||||
...(block.props as any),
|
||||
itemsTree: nextTree,
|
||||
},
|
||||
}
|
||||
: block,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 선택된 리스트 아이템을 부모의 다음 형제로 내어쓰기한다.
|
||||
outdentSelectedListItem: (blockId) => {
|
||||
const { blocks, selectedListItemId } = get();
|
||||
if (!selectedListItemId) return;
|
||||
|
||||
const target = blocks.find((b: Block) => b.id === blockId && b.type === "list");
|
||||
if (!target) return;
|
||||
|
||||
const listProps = target.props as ListBlockProps & { itemsTree?: ListItemNode[] };
|
||||
const currentTree: ListItemNode[] =
|
||||
(listProps.itemsTree as ListItemNode[] | undefined) && listProps.itemsTree!.length > 0
|
||||
? (listProps.itemsTree as ListItemNode[])
|
||||
: [];
|
||||
|
||||
const nextTree = outdentListItem(currentTree, selectedListItemId);
|
||||
|
||||
set((state: EditorState) => ({
|
||||
blocks: state.blocks.map((block: Block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
props: {
|
||||
...(block.props as any),
|
||||
itemsTree: nextTree,
|
||||
},
|
||||
}
|
||||
: block,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 선택된 리스트 아이템을 같은 레벨에서 한 칸 위로 이동시킨다.
|
||||
moveSelectedListItemUp: (blockId) => {
|
||||
const { blocks, selectedListItemId } = get();
|
||||
if (!selectedListItemId) return;
|
||||
|
||||
const target = blocks.find((b: Block) => b.id === blockId && b.type === "list");
|
||||
if (!target) return;
|
||||
|
||||
const listProps = target.props as ListBlockProps & { itemsTree?: ListItemNode[] };
|
||||
const currentTree: ListItemNode[] =
|
||||
(listProps.itemsTree as ListItemNode[] | undefined) && listProps.itemsTree!.length > 0
|
||||
? (listProps.itemsTree as ListItemNode[])
|
||||
: [];
|
||||
|
||||
const nextTree = moveListItemUp(currentTree, selectedListItemId);
|
||||
|
||||
set((state: EditorState) => ({
|
||||
blocks: state.blocks.map((block: Block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
props: {
|
||||
...(block.props as any),
|
||||
itemsTree: nextTree,
|
||||
},
|
||||
}
|
||||
: block,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 선택된 리스트 아이템을 같은 레벨에서 한 칸 아래로 이동시킨다.
|
||||
moveSelectedListItemDown: (blockId) => {
|
||||
const { blocks, selectedListItemId } = get();
|
||||
if (!selectedListItemId) return;
|
||||
|
||||
const target = blocks.find((b: Block) => b.id === blockId && b.type === "list");
|
||||
if (!target) return;
|
||||
|
||||
const listProps = target.props as ListBlockProps & { itemsTree?: ListItemNode[] };
|
||||
const currentTree: ListItemNode[] =
|
||||
(listProps.itemsTree as ListItemNode[] | undefined) && listProps.itemsTree!.length > 0
|
||||
? (listProps.itemsTree as ListItemNode[])
|
||||
: [];
|
||||
|
||||
const nextTree = moveListItemDown(currentTree, selectedListItemId);
|
||||
|
||||
set((state: EditorState) => ({
|
||||
blocks: state.blocks.map((block: Block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
props: {
|
||||
...(block.props as any),
|
||||
itemsTree: nextTree,
|
||||
},
|
||||
}
|
||||
: block,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 선택된 블록 ID를 변경
|
||||
selectBlock: (id) => {
|
||||
const { blocks } = get();
|
||||
|
||||
Reference in New Issue
Block a user