- Alpine.js에서 :value="null"이 문자열로 변환되는 문제 수정
- option value를 빈 문자열("")로 변경
- JavaScript에서 null 값을 빈 문자열로 변환하여 UI 바인딩 개선
- 저장 시 빈 문자열과 null 모두 처리하도록 수정
200 lines
11 KiB
HTML
200 lines
11 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>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
|
|
<style>
|
|
.sortable-ghost {
|
|
opacity: 0.4;
|
|
}
|
|
.sortable-chosen {
|
|
background-color: #f3f4f6;
|
|
}
|
|
.sortable-drag {
|
|
background-color: white;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen" x-data="bookEditorApp()">
|
|
<!-- 헤더 -->
|
|
<div id="header-container"></div>
|
|
|
|
<!-- 메인 컨텐츠 -->
|
|
<main class="container mx-auto px-4 py-8">
|
|
<!-- 헤더 섹션 -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<button @click="goBack()"
|
|
class="flex items-center px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg transition-colors">
|
|
<i class="fas fa-arrow-left mr-2"></i>서적으로 돌아가기
|
|
</button>
|
|
|
|
<button @click="saveChanges()"
|
|
:disabled="saving"
|
|
class="flex items-center px-6 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded-lg transition-colors">
|
|
<i class="fas fa-save mr-2"></i>
|
|
<span x-text="saving ? '저장 중...' : '변경사항 저장'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border p-6">
|
|
<div class="flex items-center mb-4">
|
|
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center mr-6">
|
|
<i class="fas fa-book text-white text-2xl"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900" x-text="bookInfo.title"></h1>
|
|
<p class="text-gray-600 mt-1" x-show="bookInfo.author" x-text="bookInfo.author"></p>
|
|
<p class="text-sm text-gray-500 mt-2">
|
|
<span x-text="documents.length"></span>개 문서 편집
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로딩 상태 -->
|
|
<div x-show="loading" class="text-center py-12">
|
|
<i class="fas fa-spinner fa-spin text-3xl text-gray-400 mb-4"></i>
|
|
<p class="text-gray-500">데이터를 불러오는 중...</p>
|
|
</div>
|
|
|
|
<!-- 편집 섹션 -->
|
|
<div x-show="!loading" class="space-y-8">
|
|
<!-- 서적 정보 편집 -->
|
|
<div class="bg-white rounded-lg shadow-sm border">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
|
<i class="fas fa-info-circle mr-2 text-blue-600"></i>
|
|
서적 정보
|
|
</h2>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">서적 제목</label>
|
|
<input type="text"
|
|
x-model="bookInfo.title"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">저자</label>
|
|
<input type="text"
|
|
x-model="bookInfo.author"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
|
<textarea x-model="bookInfo.description"
|
|
rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 순서 및 PDF 매칭 편집 -->
|
|
<div class="bg-white rounded-lg shadow-sm border">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
|
<i class="fas fa-list-ol mr-2 text-green-600"></i>
|
|
문서 순서 및 PDF 매칭
|
|
</h2>
|
|
<div class="flex space-x-2">
|
|
<button @click="autoSortByName()"
|
|
class="px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors text-sm">
|
|
<i class="fas fa-sort-alpha-down mr-1"></i>이름순 정렬
|
|
</button>
|
|
<button @click="reverseOrder()"
|
|
class="px-3 py-1.5 bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors text-sm">
|
|
<i class="fas fa-exchange-alt mr-1"></i>순서 뒤집기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
<div id="sortable-list" class="space-y-3">
|
|
<template x-for="(doc, index) in documents" :key="doc.id">
|
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 cursor-move hover:bg-gray-100 transition-colors"
|
|
:data-id="doc.id">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center flex-1">
|
|
<!-- 드래그 핸들 -->
|
|
<div class="mr-4 text-gray-400">
|
|
<i class="fas fa-grip-vertical"></i>
|
|
</div>
|
|
|
|
<!-- 순서 번호 -->
|
|
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-medium mr-4">
|
|
<span x-text="index + 1"></span>
|
|
</div>
|
|
|
|
<!-- 문서 정보 -->
|
|
<div class="flex-1">
|
|
<h3 class="font-medium text-gray-900" x-text="doc.title"></h3>
|
|
<p class="text-sm text-gray-500" x-text="doc.description || '설명 없음'"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PDF 매칭 및 컨트롤 -->
|
|
<div class="flex items-center space-x-3">
|
|
<!-- PDF 매칭 드롭다운 -->
|
|
<div class="min-w-48 relative">
|
|
<select x-model="doc.matched_pdf_id"
|
|
:class="doc.matched_pdf_id ? 'border-green-300 bg-green-50' : 'border-gray-300'"
|
|
class="w-full px-3 py-2 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm">
|
|
<option value="">PDF 매칭 없음</option>
|
|
<template x-for="pdf in availablePDFs" :key="pdf.id">
|
|
<option :value="pdf.id" x-text="pdf.title"></option>
|
|
</template>
|
|
</select>
|
|
<!-- 매칭 상태 표시 -->
|
|
<div x-show="doc.matched_pdf_id" class="absolute -top-1 -right-1">
|
|
<div class="w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 이동 버튼 -->
|
|
<div class="flex flex-col space-y-1">
|
|
<button @click="moveUp(index)"
|
|
:disabled="index === 0"
|
|
class="p-1 text-gray-400 hover:text-blue-600 disabled:opacity-30 disabled:cursor-not-allowed">
|
|
<i class="fas fa-chevron-up text-xs"></i>
|
|
</button>
|
|
<button @click="moveDown(index)"
|
|
:disabled="index === documents.length - 1"
|
|
class="p-1 text-gray-400 hover:text-blue-600 disabled:opacity-30 disabled:cursor-not-allowed">
|
|
<i class="fas fa-chevron-down text-xs"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- 빈 상태 -->
|
|
<div x-show="documents.length === 0" class="text-center py-8">
|
|
<i class="fas fa-file-alt text-gray-400 text-3xl mb-4"></i>
|
|
<p class="text-gray-500">편집할 문서가 없습니다</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- JavaScript 파일들 -->
|
|
<script src="/static/js/api.js?v=2025012384"></script>
|
|
<script src="/static/js/auth.js?v=2025012351"></script>
|
|
<script src="/static/js/header-loader.js?v=2025012351"></script>
|
|
<script src="/static/js/book-editor.js?v=2025012461"></script>
|
|
</body>
|
|
</html>
|