feat: 소설 분기 시스템 및 트리 메모장 구현
🌟 주요 기능: - 트리 구조 메모장 시스템 - 소설 분기 관리 (정사 경로 설정) - 중앙 배치 트리 다이어그램 - 정사 경로 목차 뷰 - 인라인 편집 기능 📚 백엔드: - MemoTree, MemoNode 모델 추가 - 정사 경로 자동 순서 관리 - 분기점에서 하나만 선택 가능한 로직 - RESTful API 엔드포인트 🎨 프론트엔드: - memo-tree.html: 트리 다이어그램 에디터 - story-view.html: 정사 경로 목차 뷰 - SVG 연결선으로 시각적 트리 표현 - Alpine.js 기반 반응형 UI - Monaco Editor 통합 ✨ 특별 기능: - 정사 경로 황금색 배지 표시 - 확대/축소 및 패닝 지원 - 드래그 앤 드롭 준비 - 내보내기 및 인쇄 기능 - 인라인 편집 모달
This commit is contained in:
98
backend/database/migrations/006_add_canonical_path.sql
Normal file
98
backend/database/migrations/006_add_canonical_path.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- 006_add_canonical_path.sql
|
||||
-- 정사 경로 표시를 위한 필드 추가
|
||||
|
||||
-- memo_nodes 테이블에 정사 경로 관련 필드 추가
|
||||
ALTER TABLE memo_nodes
|
||||
ADD COLUMN is_canonical BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN canonical_order INTEGER DEFAULT NULL,
|
||||
ADD COLUMN story_path TEXT DEFAULT NULL; -- 정사 경로 저장 (예: /1/3/7)
|
||||
|
||||
-- 정사 경로 순서를 위한 인덱스 추가
|
||||
CREATE INDEX idx_memo_nodes_canonical_order ON memo_nodes(tree_id, canonical_order) WHERE is_canonical = TRUE;
|
||||
|
||||
-- 트리별 정사 경로 통계를 위한 뷰 생성
|
||||
CREATE OR REPLACE VIEW memo_tree_canonical_stats AS
|
||||
SELECT
|
||||
t.id as tree_id,
|
||||
t.title as tree_title,
|
||||
COUNT(n.id) as total_nodes,
|
||||
COUNT(CASE WHEN n.is_canonical = TRUE THEN 1 END) as canonical_nodes,
|
||||
MAX(n.canonical_order) as max_canonical_order,
|
||||
STRING_AGG(
|
||||
CASE WHEN n.is_canonical = TRUE THEN n.title END,
|
||||
' → '
|
||||
ORDER BY n.canonical_order
|
||||
) as canonical_story_path
|
||||
FROM memo_trees t
|
||||
LEFT JOIN memo_nodes n ON t.id = n.tree_id
|
||||
GROUP BY t.id, t.title;
|
||||
|
||||
-- 정사 경로 순서 자동 업데이트 함수 (분기점에서 하나만 선택 가능)
|
||||
CREATE OR REPLACE FUNCTION update_canonical_order()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- 정사로 설정될 때
|
||||
IF NEW.is_canonical = TRUE AND (OLD.is_canonical IS NULL OR OLD.is_canonical = FALSE) THEN
|
||||
-- 같은 부모를 가진 다른 형제 노드들의 정사 상태 해제 (분기점에서 하나만 선택)
|
||||
IF NEW.parent_id IS NOT NULL THEN
|
||||
UPDATE memo_nodes
|
||||
SET is_canonical = FALSE, canonical_order = NULL, story_path = NULL
|
||||
WHERE tree_id = NEW.tree_id
|
||||
AND parent_id = NEW.parent_id
|
||||
AND id != NEW.id
|
||||
AND is_canonical = TRUE;
|
||||
END IF;
|
||||
|
||||
-- 부모 노드의 순서를 기준으로 순서 계산
|
||||
IF NEW.parent_id IS NULL THEN
|
||||
-- 루트 노드는 항상 1
|
||||
NEW.canonical_order = 1;
|
||||
ELSE
|
||||
-- 부모 노드의 순서 + 1
|
||||
SELECT COALESCE(parent.canonical_order, 0) + 1
|
||||
INTO NEW.canonical_order
|
||||
FROM memo_nodes parent
|
||||
WHERE parent.id = NEW.parent_id AND parent.is_canonical = TRUE;
|
||||
|
||||
-- 부모가 정사가 아니면 순서 할당 안함
|
||||
IF NEW.canonical_order IS NULL THEN
|
||||
NEW.canonical_order = NULL;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 정사 경로 업데이트
|
||||
NEW.story_path = COALESCE(NEW.path, '');
|
||||
END IF;
|
||||
|
||||
-- 정사에서 제외될 때 순서 제거
|
||||
IF NEW.is_canonical = FALSE AND OLD.is_canonical = TRUE THEN
|
||||
NEW.canonical_order = NULL;
|
||||
NEW.story_path = NULL;
|
||||
|
||||
-- 뒤의 순서들을 앞으로 당기기
|
||||
UPDATE memo_nodes
|
||||
SET canonical_order = canonical_order - 1
|
||||
WHERE tree_id = NEW.tree_id
|
||||
AND is_canonical = TRUE
|
||||
AND canonical_order > OLD.canonical_order;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 트리거 생성
|
||||
DROP TRIGGER IF EXISTS trigger_update_canonical_order ON memo_nodes;
|
||||
CREATE TRIGGER trigger_update_canonical_order
|
||||
BEFORE UPDATE ON memo_nodes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_canonical_order();
|
||||
|
||||
-- 기존 루트 노드들을 정사로 설정 (기본값)
|
||||
UPDATE memo_nodes
|
||||
SET is_canonical = TRUE, canonical_order = 1
|
||||
WHERE parent_id IS NULL AND is_canonical = FALSE;
|
||||
|
||||
COMMENT ON COLUMN memo_nodes.is_canonical IS '정사 경로 여부 (소설의 메인 스토리라인)';
|
||||
COMMENT ON COLUMN memo_nodes.canonical_order IS '정사 경로에서의 순서 (1부터 시작)';
|
||||
COMMENT ON COLUMN memo_nodes.story_path IS '정사 경로 문자열 표현';
|
||||
Reference in New Issue
Block a user