import React, { useState, useEffect } from 'react'
import { Loader2, RefreshCw, Plus, X, Play, BookOpen, ChevronDown, ChevronUp } from 'lucide-react'
function ProgressBar({ value, max }) {
const pct = max > 0 ? Math.min(100, Math.round(value / max * 100)) : 0
const color = pct >= 80 ? '#3a7d5c' : pct >= 40 ? '#c8a84e' : '#4a5abf'
return (
)
}
function NewNovelModal({ onClose, onCreated }) {
const [form, setForm] = useState({
novel_id: '',
title: '',
title_ko: '',
genre: '',
setting: '',
characters: '',
base_story: '',
publish_schedule: '매주 월/목 09:00',
episode_count_target: 50,
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError('')
try {
const res = await fetch('/api/novels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...form, episode_count_target: Number(form.episode_count_target) }),
})
if (!res.ok) {
const err = await res.json()
throw new Error(err.detail || '생성 실패')
}
const data = await res.json()
onCreated(data)
onClose()
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const field = (name, label, placeholder = '', type = 'text', rows = 0) => (
{rows > 0 ? (
)
return (
)
}
function NovelCard({ novel, onGenerate }) {
const [expanded, setExpanded] = useState(false)
const [generating, setGenerating] = useState(false)
const handleGenerate = async () => {
if (!confirm(`"${novel.title_ko}" 다음 에피소드를 생성할까요? 수 분이 걸릴 수 있습니다.`)) return
setGenerating(true)
try {
const res = await fetch(`/api/novels/${novel.novel_id}/generate`, { method: 'POST' })
const data = await res.json()
if (data.success) {
alert(`에피소드 ${data.episode_num}화 생성 완료!`)
onGenerate()
} else {
alert('생성 실패: ' + (data.detail || '알 수 없는 오류'))
}
} catch (e) {
alert('생성 실패: ' + e.message)
} finally {
setGenerating(false)
}
}
return (
{novel.title_ko}
{novel.genre}
{novel.status === 'active' ? '연재중' : '중단'}
{novel.publish_schedule && (
연재 일정: {novel.publish_schedule}
)}
{/* 에피소드 테이블 토글 */}
{(novel.episodes?.length > 0) && (
{expanded && (
| 화수 |
제목 |
생성일 |
분량 |
{novel.episodes.map((ep, idx) => (
| {ep.episode_num}화 |
{ep.title} |
{ep.generated_at} |
{ep.word_count?.toLocaleString()}자 |
))}
)}
)}
)
}
export default function Novel() {
const [novels, setNovels] = useState([])
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const fetchNovels = async () => {
setLoading(true)
try {
const res = await fetch('/api/novels')
const data = await res.json()
setNovels(data.novels || [])
} catch (e) {
console.error('Novel 로드 실패:', e)
} finally {
setLoading(false)
}
}
useEffect(() => { fetchNovels() }, [])
if (loading) {
return (
)
}
return (
소설 연재 관리
{novels.length === 0 ? (
등록된 소설이 없습니다.
) : (
{novels.map(novel => (
))}
)}
{showModal && (
setShowModal(false)}
onCreated={() => fetchNovels()}
/>
)}
)
}