import { useState, useEffect, useRef } from "react"; const STORAGE_KEY = "yongdae_reading_marathon"; const MAX_BOOKS = 50; const ADMIN_PASSWORD = "yongdae2026"; const pastelColors = [ { bg: "#FFD6D6", accent: "#FF6B6B", text: "#c0392b" }, { bg: "#D6EDFF", accent: "#4A90E2", text: "#1a5fa0" }, { bg: "#D6FFE4", accent: "#27AE60", text: "#1a7a42" }, { bg: "#FFF3D6", accent: "#F39C12", text: "#c07a00" }, { bg: "#EDD6FF", accent: "#9B59B6", text: "#6c3483" }, { bg: "#D6FFFA", accent: "#1ABC9C", text: "#0e8a6e" }, { bg: "#FFE4D6", accent: "#E67E22", text: "#b35a10" }, { bg: "#F0FFD6", accent: "#82C91E", text: "#5a8c0e" }, ]; function getColor(index) { return pastelColors[index % pastelColors.length]; } // localStorage 저장/불러오기 function loadFromStorage() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); return parsed.participants || []; } } catch (e) { console.error("불러오기 실패", e); } return []; } function saveToStorage(participants) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ participants })); } catch (e) { console.error("저장 실패", e); } } function AdminLoginModal({ onClose, onSuccess }) { const [pw, setPw] = useState(""); const [error, setError] = useState(""); const [shake, setShake] = useState(false); function handleLogin() { if (pw === ADMIN_PASSWORD) { onSuccess(); } else { setError("비밀번호가 틀렸습니다."); setShake(true); setTimeout(() => setShake(false), 500); setPw(""); } } return (
{ if(e.target===e.currentTarget) onClose(); }}>
🔐

관리자 로그인

관리자만 내용을 수정할 수 있어요

{ setPw(e.target.value); setError(""); }} onKeyDown={e => e.key==="Enter" && handleLogin()} autoFocus style={{ width:"100%", padding:"13px 16px", border: error ? "2px solid #ef5350" : "2px solid #e0e0e0", borderRadius:12, fontSize:16, fontWeight:600, outline:"none", boxSizing:"border-box", letterSpacing:4, }} onFocus={e => e.target.style.border="2px solid #ff9800"} onBlur={e => e.target.style.border=error?"2px solid #ef5350":"2px solid #e0e0e0"} /> {error && (
❌ {error}
)}
); } function AddParticipantModal({ participants, onClose, onAdd }) { const [newName, setNewName] = useState(""); const [nameError, setNameError] = useState(""); function handleAdd() { const trimmed = newName.trim(); if (!trimmed) { setNameError("이름을 입력해주세요."); return; } if (participants.some(p => p.name === trimmed)) { setNameError("이미 참여 중인 이름입니다."); return; } onAdd(trimmed); } return (
{ if(e.target===e.currentTarget) onClose(); }}>
🏃‍♂️

참여자 추가

이름을 입력하고 참여 버튼을 눌러주세요

{ setNewName(e.target.value); setNameError(""); }} onKeyDown={e => e.key==="Enter" && handleAdd()} autoFocus style={{ width:"100%", padding:"13px 16px", border: nameError ? "2px solid #ef5350" : "2px solid #e0e0e0", borderRadius:12, fontSize:16, fontWeight:600, outline:"none", boxSizing:"border-box", }} onFocus={e => e.target.style.border="2px solid #ff9800"} onBlur={e => e.target.style.border=nameError?"2px solid #ef5350":"2px solid #e0e0e0"} /> {nameError &&
{nameError}
}
); } export default function App() { const [participants, setParticipants] = useState(() => loadFromStorage()); const [isAdmin, setIsAdmin] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(""); const [savedFlash, setSavedFlash] = useState(null); const [pendingEdits, setPendingEdits] = useState({}); // 저장 전 임시 수정값 const [saveAllFlash, setSaveAllFlash] = useState(false); // participants 변경 시 자동 저장 (참여자 추가/삭제만) // 독서량은 "저장" 버튼 클릭 시만 저장 function handleAddParticipant(name) { const updated = [...participants, { id: Date.now(), name, books: 0 }]; setParticipants(updated); saveToStorage(updated); setShowAddModal(false); } function handleEditStart(p) { setEditingId(p.id); setEditValue(String(p.books)); } // 개별 저장 function handleEditSave(id) { const val = Math.min(MAX_BOOKS, Math.max(0, Number(editValue) || 0)); const updated = participants.map(p => p.id === id ? { ...p, books: val } : p); setParticipants(updated); saveToStorage(updated); setEditingId(null); setSavedFlash(id); setTimeout(() => setSavedFlash(null), 1400); } function handleLogout() { setIsAdmin(false); setEditingId(null); } const sorted = [...participants].sort((a, b) => b.books - a.books); return (
{/* 헤더 */}
📚🏃

용대초등학교 독서 마라톤

🎯 목표: 50권 · 현재 참여자 {participants.length}명
{isAdmin ? (
🔓 관리자 모드
) : ( )}
{/* 관리자 전용 추가 버튼 */} {isAdmin && (
)} {/* 읽기 전용 안내 */} {!isAdmin && participants.length > 0 && (
👀 현재 읽기 전용 모드입니다
)} {/* 순위 목록 */} {participants.length === 0 ? (
📖
아직 참여자가 없어요
{isAdmin ? "위의 추가하기 버튼으로 참여자를 등록하세요!" : "관리자가 참여자를 등록하면 여기에 표시됩니다."}
) : ( <> {/* 1등 스포트라이트 */} {sorted[0] && (
🏆
🥇
{sorted[0].name}
{Array.from({length:MAX_BOOKS}).map((_,i) => (
))}
{sorted[0].books} / {MAX_BOOKS}권 완독
{sorted[0].books}권
)}
{sorted.map((p, idx) => { const color = getColor(idx); const pct = (p.books / MAX_BOOKS) * 100; const isEditing = editingId === p.id; const medals = ["🥇","🥈","🥉"]; return (
{idx < 3 ? medals[idx] : idx+1}
{p.name}
0?8:0, }}/>
{p.books} / {MAX_BOOKS}권
{isAdmin ? ( isEditing ? (
setEditValue(e.target.value)} onKeyDown={e => { if(e.key==="Enter") handleEditSave(p.id); if(e.key==="Escape") setEditingId(null); }} autoFocus style={{ width:60,padding:"6px 8px", border:`2px solid ${color.accent}`, borderRadius:8,fontSize:15,fontWeight:700, color:color.text,textAlign:"center",outline:"none", }} /> {/* ✅ 저장 버튼 */}
) : (
{savedFlash===p.id && ( ✅ 저장됨! )}
{p.books}
) ) : (
{p.books}
)}
); })}
)}
{showLoginModal && ( setShowLoginModal(false)} onSuccess={() => { setIsAdmin(true); setShowLoginModal(false); }} /> )} {showAddModal && isAdmin && ( setShowAddModal(false)} onAdd={handleAddParticipant} /> )}
); }