195 lines
5.4 KiB
TypeScript
195 lines
5.4 KiB
TypeScript
import { createStore } from "zustand";
|
|
import { create } from "zustand";
|
|
|
|
// 블록 타입 정의: 텍스트/버튼/이미지/섹션 블록
|
|
export type BlockType = "text" | "button" | "image" | "section";
|
|
|
|
// 텍스트 블록 속성
|
|
export interface TextBlockProps {
|
|
text: string;
|
|
// 텍스트 정렬: 기본은 left
|
|
align: "left" | "center" | "right";
|
|
// 텍스트 크기: 기본은 base
|
|
size: "sm" | "base" | "lg";
|
|
}
|
|
|
|
// 버튼 블록 속성
|
|
export interface ButtonBlockProps {
|
|
label: string;
|
|
href: string;
|
|
// TODO: variant, size 등은 추후 확장
|
|
}
|
|
|
|
// 이미지 블록 속성
|
|
export interface ImageBlockProps {
|
|
src: string;
|
|
alt: string;
|
|
}
|
|
|
|
// 섹션 블록 속성
|
|
export interface SectionBlockProps {
|
|
background: "default" | "muted" | "primary";
|
|
paddingY: "sm" | "md" | "lg";
|
|
}
|
|
|
|
// 공통 블록 모델
|
|
export interface Block {
|
|
id: string;
|
|
type: BlockType;
|
|
props: TextBlockProps | ButtonBlockProps | ImageBlockProps | SectionBlockProps;
|
|
}
|
|
|
|
// 에디터 상태 인터페이스
|
|
export interface EditorState {
|
|
blocks: Block[];
|
|
selectedBlockId: string | null;
|
|
addTextBlock: () => void;
|
|
addButtonBlock: () => void;
|
|
addImageBlock: () => void;
|
|
addSectionBlock: () => void;
|
|
updateBlock: (id: string, partial: Partial<TextBlockProps & ButtonBlockProps>) => void;
|
|
selectBlock: (id: string | null) => void;
|
|
replaceBlocks: (blocks: Block[]) => void;
|
|
reorderBlocks: (activeId: string, overId: string) => void;
|
|
}
|
|
|
|
// 간단한 ID 생성기 (추후 uuid 라이브러리로 교체 가능)
|
|
let idCounter = 0;
|
|
const createId = () => `blk_${Date.now()}_${idCounter++}`;
|
|
|
|
// 에디터 스토어 생성 함수 (테스트 및 앱에서 공유 사용)
|
|
// set/get 은 zustand 내부 구현에 의해 주입되므로, 여기서는 any 로 완화해 사용한다.
|
|
const createEditorState = (set: any, get: any): EditorState => ({
|
|
blocks: [],
|
|
selectedBlockId: null,
|
|
|
|
// 텍스트 블록 추가: 기본 텍스트와 함께 생성 후 선택 상태로 만든다
|
|
addTextBlock: () => {
|
|
const id = createId();
|
|
const newBlock: Block = {
|
|
id,
|
|
type: "text",
|
|
props: {
|
|
text: "새 텍스트",
|
|
align: "left",
|
|
size: "base",
|
|
},
|
|
};
|
|
|
|
set((state: EditorState) => ({
|
|
blocks: [...state.blocks, newBlock],
|
|
selectedBlockId: id,
|
|
}));
|
|
},
|
|
|
|
// 이미지 블록 추가: 기본 플레이스홀더와 함께 생성 후 선택 상태로 만든다
|
|
addImageBlock: () => {
|
|
const id = createId();
|
|
const newBlock: Block = {
|
|
id,
|
|
type: "image",
|
|
props: {
|
|
src: "",
|
|
alt: "이미지 설명",
|
|
},
|
|
};
|
|
|
|
set((state: EditorState) => ({
|
|
blocks: [...state.blocks, newBlock],
|
|
selectedBlockId: id,
|
|
}));
|
|
},
|
|
|
|
// 섹션 블록 추가: 배경/패딩 기본값과 함께 생성 후 선택 상태로 만든다
|
|
addSectionBlock: () => {
|
|
const id = createId();
|
|
const newBlock: Block = {
|
|
id,
|
|
type: "section",
|
|
props: {
|
|
background: "default",
|
|
paddingY: "md",
|
|
},
|
|
};
|
|
|
|
set((state: EditorState) => ({
|
|
blocks: [...state.blocks, newBlock],
|
|
selectedBlockId: id,
|
|
}));
|
|
},
|
|
|
|
// 버튼 블록 추가: 기본 라벨/링크와 함께 생성 후 선택 상태로 만든다
|
|
addButtonBlock: () => {
|
|
const id = createId();
|
|
const newBlock: Block = {
|
|
id,
|
|
type: "button",
|
|
props: {
|
|
label: "버튼",
|
|
href: "#",
|
|
},
|
|
};
|
|
|
|
set((state: EditorState) => ({
|
|
blocks: [...state.blocks, newBlock],
|
|
selectedBlockId: id,
|
|
}));
|
|
},
|
|
|
|
// 특정 블록의 속성을 부분 업데이트 (텍스트/버튼 공통 사용)
|
|
updateBlock: (id, partial) => {
|
|
set((state: EditorState) => ({
|
|
blocks: state.blocks.map((block: Block) =>
|
|
block.id === id
|
|
? {
|
|
...block,
|
|
props: { ...block.props, ...partial },
|
|
}
|
|
: block,
|
|
),
|
|
}));
|
|
},
|
|
|
|
// 선택된 블록 ID를 변경
|
|
selectBlock: (id) => {
|
|
const { blocks } = get();
|
|
// 존재하지 않는 ID를 선택하려고 할 경우, 그냥 null 로 초기화
|
|
if (id && !blocks.some((b: Block) => b.id === id)) {
|
|
set({ selectedBlockId: null });
|
|
return;
|
|
}
|
|
set({ selectedBlockId: id });
|
|
},
|
|
|
|
replaceBlocks: (blocks) => {
|
|
set({
|
|
blocks,
|
|
selectedBlockId: blocks.length > 0 ? blocks[0].id : null,
|
|
});
|
|
},
|
|
|
|
reorderBlocks: (activeId, overId) => {
|
|
set((state: EditorState) => {
|
|
const current = state.blocks;
|
|
const oldIndex = current.findIndex((b: Block) => b.id === activeId);
|
|
const newIndex = current.findIndex((b: Block) => b.id === overId);
|
|
|
|
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) {
|
|
return { blocks: current };
|
|
}
|
|
|
|
const updated = [...current];
|
|
const [moved] = updated.splice(oldIndex, 1);
|
|
updated.splice(newIndex, 0, moved);
|
|
|
|
return { blocks: updated };
|
|
});
|
|
},
|
|
});
|
|
|
|
// React 컴포넌트에서 사용하는 전역 훅 스토어
|
|
export const useEditorStore = create<EditorState>()(createEditorState);
|
|
|
|
// 테스트 등에서 독립적인 스토어 인스턴스를 만들 때 사용하는 팩토리
|
|
export const createEditorStore = () => createStore<EditorState>(createEditorState);
|