에디터 MVP 골격 및 프로젝트 저장/로드 기능 구현

This commit is contained in:
2025-11-18 06:04:39 +09:00
commit 4b00e9a3f9
26 changed files with 11179 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
import { createStore } from "zustand";
import { create } from "zustand";
// 블록 타입 정의: 1단계에서는 텍스트 블록만 사용
export type BlockType = "text"; // 이후 image/button/section 으로 확장 예정
// 텍스트 블록 속성
export interface TextBlockProps {
text: string;
// 텍스트 정렬: 기본은 left
align: "left" | "center" | "right";
// 텍스트 크기: 기본은 base
size: "sm" | "base" | "lg";
}
// 공통 블록 모델
export interface Block {
id: string;
type: BlockType;
props: TextBlockProps;
}
// 에디터 상태 인터페이스
export interface EditorState {
blocks: Block[];
selectedBlockId: string | null;
addTextBlock: () => void;
updateBlock: (id: string, partial: Partial<TextBlockProps>) => 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,
}));
},
// 특정 블록의 텍스트 속성을 부분 업데이트
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);