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}
/>
)}
);
}