🌟 주요 기능: - 트리 구조 메모장 시스템 - 소설 분기 관리 (정사 경로 설정) - 중앙 배치 트리 다이어그램 - 정사 경로 목차 뷰 - 인라인 편집 기능 📚 백엔드: - MemoTree, MemoNode 모델 추가 - 정사 경로 자동 순서 관리 - 분기점에서 하나만 선택 가능한 로직 - RESTful API 엔드포인트 🎨 프론트엔드: - memo-tree.html: 트리 다이어그램 에디터 - story-view.html: 정사 경로 목차 뷰 - SVG 연결선으로 시각적 트리 표현 - Alpine.js 기반 반응형 UI - Monaco Editor 통합 ✨ 특별 기능: - 정사 경로 황금색 배지 표시 - 확대/축소 및 패닝 지원 - 드래그 앤 드롭 준비 - 내보내기 및 인쇄 기능 - 인라인 편집 모달
652 lines
30 KiB
HTML
652 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>트리 메모장 - Document Server</title>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Alpine.js 자동 시작 방지 -->
|
|
<script>
|
|
// Alpine.js 자동 시작 방지
|
|
document.addEventListener('alpine:init', () => {
|
|
console.log('Alpine.js 초기화됨');
|
|
});
|
|
</script>
|
|
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
<!-- Monaco Editor (VS Code 에디터) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* 트리 스타일 */
|
|
.tree-node {
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.tree-node:hover {
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.tree-node.selected {
|
|
background-color: #dbeafe;
|
|
border-left: 3px solid #3b82f6;
|
|
}
|
|
|
|
.tree-node.dragging {
|
|
opacity: 0.5;
|
|
transform: rotate(5deg);
|
|
}
|
|
|
|
/* 트리 연결선 */
|
|
.tree-node-item {
|
|
position: relative;
|
|
}
|
|
|
|
.tree-node-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
border-left: 1px solid #d1d5db;
|
|
}
|
|
|
|
.tree-node-item::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 50%;
|
|
width: 16px;
|
|
border-top: 1px solid #d1d5db;
|
|
}
|
|
|
|
/* 루트 노드는 연결선 없음 */
|
|
.tree-node-item.root::before,
|
|
.tree-node-item.root::after {
|
|
display: none;
|
|
}
|
|
|
|
/* 마지막 자식 노드는 세로선을 중간까지만 */
|
|
.tree-node-item.last-child::before {
|
|
height: 50%;
|
|
}
|
|
|
|
/* 트리 들여쓰기 */
|
|
.tree-indent {
|
|
width: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
/* 트리 라인 스타일 */
|
|
.tree-line {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
.tree-line.vertical::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
bottom: 0;
|
|
border-left: 1px solid #d1d5db;
|
|
}
|
|
|
|
.tree-line.branch::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
bottom: 0;
|
|
border-left: 1px solid #d1d5db;
|
|
}
|
|
|
|
.tree-line.branch::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 10px;
|
|
border-top: 1px solid #d1d5db;
|
|
}
|
|
|
|
.tree-line.corner::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
height: 50%;
|
|
border-left: 1px solid #d1d5db;
|
|
}
|
|
|
|
.tree-line.corner::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 10px;
|
|
border-top: 1px solid #d1d5db;
|
|
}
|
|
|
|
.tree-line.empty {
|
|
/* 빈 공간 */
|
|
}
|
|
|
|
/* 에디터 컨테이너 */
|
|
.editor-container {
|
|
height: calc(100vh - 200px);
|
|
min-height: 400px;
|
|
}
|
|
|
|
/* 스플리터 */
|
|
.splitter {
|
|
width: 4px;
|
|
background: #e5e7eb;
|
|
cursor: col-resize;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.splitter:hover {
|
|
background: #3b82f6;
|
|
}
|
|
|
|
/* 노드 타입별 아이콘 색상 */
|
|
.node-folder { color: #f59e0b; }
|
|
.node-memo { color: #6b7280; }
|
|
.node-chapter { color: #8b5cf6; }
|
|
.node-character { color: #10b981; }
|
|
.node-plot { color: #ef4444; }
|
|
|
|
/* 상태별 색상 */
|
|
.status-draft { border-left-color: #9ca3af; }
|
|
.status-writing { border-left-color: #f59e0b; }
|
|
.status-review { border-left-color: #3b82f6; }
|
|
.status-complete { border-left-color: #10b981; }
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-gray-50" x-data="memoTreeApp()" x-init="init()" x-cloak>
|
|
<!-- 헤더 -->
|
|
<header class="bg-white shadow-sm border-b">
|
|
<div class="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-16">
|
|
<!-- 로고 및 네비게이션 -->
|
|
<div class="flex items-center space-x-4">
|
|
<div class="flex items-center space-x-2">
|
|
<i class="fas fa-sitemap text-blue-600 text-xl"></i>
|
|
<h1 class="text-xl font-bold text-gray-900">트리 메모장</h1>
|
|
</div>
|
|
|
|
<!-- 네비게이션 링크 -->
|
|
<div class="hidden md:flex space-x-4">
|
|
<a href="index.html" class="text-gray-600 hover:text-gray-900">📖 문서 관리</a>
|
|
<a href="hierarchy.html" class="text-gray-600 hover:text-gray-900">📚 계층구조</a>
|
|
<span class="text-blue-600 font-medium">🌳 트리 메모장</span>
|
|
<a href="story-view.html" class="text-gray-600 hover:text-gray-900">📖 스토리 뷰</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 사용자 메뉴 -->
|
|
<div class="flex items-center space-x-4">
|
|
<!-- 트리 관리 버튼들 -->
|
|
<div x-show="currentUser" class="flex space-x-2">
|
|
<button
|
|
@click="showNewTreeModal = true"
|
|
class="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
title="새 트리 생성"
|
|
>
|
|
<i class="fas fa-plus mr-1"></i> 새 트리
|
|
</button>
|
|
|
|
<button
|
|
@click="showTreeSettings = !showTreeSettings"
|
|
x-show="selectedTree"
|
|
class="px-3 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
title="트리 설정"
|
|
>
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 사용자 정보 -->
|
|
<div x-show="currentUser" class="flex items-center space-x-3">
|
|
<span class="text-sm text-gray-600" x-text="currentUser?.full_name || currentUser?.email"></span>
|
|
<button @click="logout()" class="text-sm text-red-600 hover:text-red-800">로그아웃</button>
|
|
</div>
|
|
|
|
<!-- 로그인 버튼 -->
|
|
<button x-show="!currentUser" @click="openLoginModal()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
로그인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 메인 컨테이너 -->
|
|
<div class="h-screen pt-16" x-show="currentUser">
|
|
<!-- 상단 툴바 -->
|
|
<div class="bg-white border-b shadow-sm p-3">
|
|
<div class="flex items-center justify-between">
|
|
<!-- 트리 선택 -->
|
|
<div class="flex items-center space-x-3">
|
|
<select
|
|
x-model="selectedTreeId"
|
|
@change="loadTree(selectedTreeId)"
|
|
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
>
|
|
<option value="">📂 트리 선택</option>
|
|
<template x-for="tree in userTrees" :key="tree.id">
|
|
<option :value="tree.id" x-text="`${getTreeIcon(tree.tree_type)} ${tree.title}`"></option>
|
|
</template>
|
|
</select>
|
|
|
|
<button
|
|
@click="showNewTreeModal = true"
|
|
class="px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
|
|
title="새 트리"
|
|
>
|
|
<i class="fas fa-plus mr-1"></i> 새 트리
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 트리 액션 버튼들 -->
|
|
<div x-show="selectedTree" class="flex items-center space-x-2">
|
|
<button
|
|
@click="createRootNode()"
|
|
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
|
>
|
|
<i class="fas fa-plus mr-1"></i> 노드 추가
|
|
</button>
|
|
<button
|
|
@click="centerTree()"
|
|
class="px-3 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 text-sm"
|
|
title="트리 중앙 정렬"
|
|
>
|
|
<i class="fas fa-crosshairs"></i>
|
|
</button>
|
|
<button
|
|
@click="zoomIn()"
|
|
class="px-2 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 text-sm"
|
|
title="확대"
|
|
>
|
|
<i class="fas fa-search-plus"></i>
|
|
</button>
|
|
<button
|
|
@click="zoomOut()"
|
|
class="px-2 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 text-sm"
|
|
title="축소"
|
|
>
|
|
<i class="fas fa-search-minus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 메인 트리 다이어그램 영역 -->
|
|
<div class="flex h-full">
|
|
<!-- 트리 다이어그램 (중앙) -->
|
|
<div class="flex-1 relative overflow-hidden bg-gray-50">
|
|
<!-- 트리 없음 상태 -->
|
|
<div x-show="!selectedTree" class="flex items-center justify-center h-full">
|
|
<div class="text-center text-gray-500">
|
|
<i class="fas fa-sitemap text-6xl text-gray-300 mb-4"></i>
|
|
<h3 class="text-lg font-medium mb-2">트리를 선택하세요</h3>
|
|
<p class="text-sm">왼쪽에서 트리를 선택하거나 새로 만들어보세요</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 노드 없음 상태 -->
|
|
<div x-show="selectedTree && treeNodes.length === 0" class="flex items-center justify-center h-full">
|
|
<div class="text-center text-gray-500">
|
|
<i class="fas fa-plus-circle text-6xl text-gray-300 mb-4"></i>
|
|
<h3 class="text-lg font-medium mb-2">첫 번째 노드를 만들어보세요</h3>
|
|
<button
|
|
@click="createRootNode()"
|
|
class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
<i class="fas fa-plus mr-2"></i> 루트 노드 생성
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 트리 다이어그램 캔버스 -->
|
|
<div x-show="selectedTree && treeNodes.length > 0" class="relative w-full h-full" id="tree-canvas">
|
|
<!-- 전체 트리 컨테이너 (SVG + 노드 함께 스케일링) -->
|
|
<div
|
|
class="absolute inset-0 w-full h-full"
|
|
id="tree-container"
|
|
:style="`transform: scale(${treeZoom}) translate(${treePanX}px, ${treePanY}px); transform-origin: center center;`"
|
|
@mousedown="startPan($event)"
|
|
@wheel="handleWheel($event)"
|
|
>
|
|
<!-- SVG 연결선 레이어 -->
|
|
<svg class="absolute inset-0 w-full h-full pointer-events-none z-10" id="tree-connections">
|
|
<!-- 연결선들이 여기에 그려짐 -->
|
|
</svg>
|
|
|
|
<!-- 노드 레이어 -->
|
|
<div class="absolute inset-0 w-full h-full z-20" id="tree-nodes">
|
|
<!-- 노드들이 여기에 배치됨 -->
|
|
<template x-for="node in treeNodes" :key="node.id">
|
|
<div
|
|
class="absolute tree-diagram-node"
|
|
:style="getNodePosition(node)"
|
|
@mousedown="startDragNode($event, node)"
|
|
>
|
|
<div
|
|
class="bg-white rounded-lg shadow-md border-2 p-3 cursor-pointer min-w-32 max-w-48 relative"
|
|
:class="{
|
|
'border-blue-500 shadow-lg': selectedNode && selectedNode.id === node.id,
|
|
'border-gray-200': !selectedNode || selectedNode.id !== node.id,
|
|
'border-gray-400': node.status === 'draft',
|
|
'border-yellow-400': node.status === 'writing',
|
|
'border-blue-400': node.status === 'review',
|
|
'border-green-400': node.status === 'complete',
|
|
'bg-gradient-to-br from-yellow-50 to-amber-50 border-amber-400': node.is_canonical,
|
|
'shadow-amber-200': node.is_canonical
|
|
}"
|
|
@click="selectNode(node)"
|
|
@dblclick="editNodeInline(node)"
|
|
>
|
|
<!-- 정사 경로 배지 -->
|
|
<div x-show="node.is_canonical" class="absolute -top-2 -right-2 bg-amber-500 text-white text-xs px-2 py-1 rounded-full font-bold shadow-md">
|
|
<i class="fas fa-star mr-1"></i><span x-text="node.canonical_order || '?'"></span>
|
|
</div>
|
|
|
|
<!-- 노드 헤더 -->
|
|
<div class="flex items-center justify-between mb-2">
|
|
<div class="flex items-center">
|
|
<span class="text-lg mr-1" x-text="getNodeIcon(node.node_type)"></span>
|
|
<span x-show="node.is_canonical" class="text-amber-600 text-sm" title="정사 경로">⭐</span>
|
|
</div>
|
|
<div class="flex space-x-1">
|
|
<button
|
|
@click.stop="toggleCanonical(node)"
|
|
class="w-5 h-5 flex items-center justify-center rounded"
|
|
:class="node.is_canonical ? 'text-amber-600 hover:text-amber-700 hover:bg-amber-50' : 'text-gray-400 hover:text-amber-600 hover:bg-amber-50'"
|
|
:title="node.is_canonical ? '정사에서 제외' : '정사로 설정'"
|
|
>
|
|
<i class="fas fa-star text-xs"></i>
|
|
</button>
|
|
<button
|
|
@click.stop="addChildNode(node)"
|
|
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-green-600 hover:bg-green-50 rounded"
|
|
title="자식 노드 추가"
|
|
>
|
|
<i class="fas fa-plus text-xs"></i>
|
|
</button>
|
|
<button
|
|
@click.stop="showNodeMenu($event, node)"
|
|
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded"
|
|
title="더보기"
|
|
>
|
|
<i class="fas fa-ellipsis-h text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 노드 제목 -->
|
|
<div class="text-sm font-medium text-gray-800 truncate" x-text="node.title"></div>
|
|
|
|
<!-- 노드 정보 -->
|
|
<div class="flex items-center justify-between mt-2 text-xs text-gray-500">
|
|
<span x-text="node.node_type"></span>
|
|
<span x-show="node.word_count > 0" x-text="`${node.word_count}w`"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 오른쪽: 속성 패널 -->
|
|
<div class="w-80 bg-white border-l shadow-lg flex flex-col">
|
|
<!-- 에디터 헤더 -->
|
|
<template x-if="selectedNode">
|
|
<div class="p-4 border-b bg-white">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<input
|
|
type="text"
|
|
x-model="selectedNode.title"
|
|
@blur="saveNodeTitle()"
|
|
class="text-lg font-semibold bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-blue-500 rounded px-2 py-1 w-full"
|
|
placeholder="노드 제목을 입력하세요"
|
|
>
|
|
<div class="flex items-center space-x-4 mt-2 text-sm text-gray-600">
|
|
<div class="flex items-center space-x-2">
|
|
<span>타입:</span>
|
|
<select
|
|
x-model="selectedNode.node_type"
|
|
@change="saveNodeType()"
|
|
class="border border-gray-300 rounded px-2 py-1 text-xs"
|
|
>
|
|
<option value="memo">📝 메모</option>
|
|
<option value="folder">📁 폴더</option>
|
|
<option value="chapter">📖 챕터</option>
|
|
<option value="character">👤 캐릭터</option>
|
|
<option value="plot">📋 플롯</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<span>상태:</span>
|
|
<select
|
|
x-model="selectedNode.status"
|
|
@change="saveNodeStatus()"
|
|
class="border border-gray-300 rounded px-2 py-1 text-xs"
|
|
>
|
|
<option value="draft">📝 초안</option>
|
|
<option value="writing">✍️ 작성중</option>
|
|
<option value="review">👀 검토중</option>
|
|
<option value="complete">✅ 완료</option>
|
|
</select>
|
|
</div>
|
|
<div x-show="selectedNode?.word_count > 0" class="text-xs">
|
|
<span x-text="`${selectedNode?.word_count || 0} 단어`"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button
|
|
@click="saveNode()"
|
|
class="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"
|
|
>
|
|
<i class="fas fa-save mr-1"></i> 저장
|
|
</button>
|
|
<button
|
|
@click="deleteNode(selectedNode.id)"
|
|
class="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
|
|
>
|
|
<i class="fas fa-trash mr-1"></i> 삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 에디터 -->
|
|
<div x-show="selectedNode" class="flex-1 editor-container">
|
|
<div id="monaco-editor" class="h-full"></div>
|
|
</div>
|
|
|
|
<!-- 노드 미선택 상태 -->
|
|
<div x-show="!selectedNode" class="flex-1 flex items-center justify-center">
|
|
<div class="text-center text-gray-500">
|
|
<i class="fas fa-edit text-6xl text-gray-300 mb-4"></i>
|
|
<h3 class="text-lg font-medium mb-2">노드를 선택하세요</h3>
|
|
<p class="text-sm">왼쪽 트리에서 노드를 클릭하여 편집을 시작하세요</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그인이 필요한 상태 -->
|
|
<div x-show="!currentUser" class="flex items-center justify-center h-screen">
|
|
<div class="text-center">
|
|
<i class="fas fa-lock text-6xl text-gray-300 mb-4"></i>
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-2">로그인이 필요합니다</h2>
|
|
<p class="text-gray-600 mb-4">트리 메모장을 사용하려면 로그인해주세요</p>
|
|
<button @click="openLoginModal()" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
|
로그인하기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 새 트리 생성 모달 -->
|
|
<div x-show="showNewTreeModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
|
<h2 class="text-xl font-bold mb-4">새 트리 생성</h2>
|
|
<form @submit.prevent="createTree()">
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">제목</label>
|
|
<input
|
|
type="text"
|
|
x-model="newTree.title"
|
|
required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="트리 제목을 입력하세요"
|
|
>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
|
<textarea
|
|
x-model="newTree.description"
|
|
rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="트리에 대한 설명을 입력하세요"
|
|
></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">타입</label>
|
|
<select
|
|
x-model="newTree.tree_type"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value="general">📝 일반</option>
|
|
<option value="novel">📚 소설</option>
|
|
<option value="research">🔬 연구</option>
|
|
<option value="project">💼 프로젝트</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button
|
|
type="button"
|
|
@click="showNewTreeModal = false"
|
|
class="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
생성
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그인 모달 -->
|
|
<div x-show="showLoginModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
|
<h2 class="text-xl font-bold mb-4">로그인</h2>
|
|
<form @submit.prevent="handleLogin()">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
|
<input
|
|
type="email"
|
|
x-model="loginForm.email"
|
|
required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
|
<input
|
|
type="password"
|
|
x-model="loginForm.password"
|
|
required
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
</div>
|
|
<div x-show="loginError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
|
<span x-text="loginError"></span>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
@click="showLoginModal = false"
|
|
class="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
:disabled="loginLoading"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
<span x-show="!loginLoading">로그인</span>
|
|
<span x-show="loginLoading">로그인 중...</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JavaScript -->
|
|
<script>
|
|
// 스크립트 순차 로딩을 위한 함수
|
|
function loadScript(src) {
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement('script');
|
|
script.src = src;
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
// 스크립트들을 순차적으로 로드
|
|
async function loadScripts() {
|
|
try {
|
|
await loadScript('static/js/api.js?v=2025012290');
|
|
console.log('✅ API 스크립트 로드 완료');
|
|
await loadScript('static/js/auth.js?v=2025012240');
|
|
console.log('✅ Auth 스크립트 로드 완료');
|
|
await loadScript('static/js/memo-tree.js?v=2025012280');
|
|
console.log('✅ Memo Tree 스크립트 로드 완료');
|
|
|
|
// 모든 스크립트 로드 완료 후 Alpine.js 로드
|
|
console.log('🚀 Alpine.js 로딩...');
|
|
await loadScript('https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js');
|
|
console.log('✅ Alpine.js 로드 완료');
|
|
} catch (error) {
|
|
console.error('❌ 스크립트 로딩 실패:', error);
|
|
}
|
|
}
|
|
|
|
// DOM 로드 후 스크립트 로딩 시작
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', loadScripts);
|
|
} else {
|
|
loadScripts();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|