import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Settings, X, Plus, Check, ChevronLeft, ChevronRight, Sparkles, Star, BrainCircuit, Type, Maximize, Layout, Calendar as CalendarIcon, Edit2, Trash2, Quote, Lightbulb, PlusCircle, Users } from 'lucide-react'; const App = () => { const apiKey = ""; // Gemini API Key // --- Character Definitions --- const CHARACTERS = { cat: { id: 'cat', name: '치즈냥이', render: () => ( ) }, fox_code: { id: 'fox_code', name: '강아지', // '미니 여우'에서 '강아지'로 명칭 변경 render: () => ( ) }, rabbit: { id: 'rabbit', name: '검은 토끼', render: () => ( ) }, fox_head: { id: 'fox_head', name: '여우 얼굴', render: () => (
Fox Face { // Fallback if image fails - Use a simplified SVG Fox Face e.target.style.display = 'none'; e.target.parentNode.innerHTML = ` `; }} />
) } }; // --- Animalese Voice Logic --- const audioCtx = useRef(null); const playAnimalese = (text) => { if (!text || typeof window === 'undefined') return; if (!audioCtx.current) { audioCtx.current = new (window.AudioContext || window.webkitAudioContext)(); } const ctx = audioCtx.current; const charDelay = 0.05; text.split('').forEach((char, i) => { if (char === ' ' || char === '\n') return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); const code = char.charCodeAt(0); const freq = 400 + (code % 600); osc.type = 'triangle'; osc.frequency.setValueAtTime(freq, ctx.currentTime + i * charDelay); gain.gain.setValueAtTime(0, ctx.currentTime + i * charDelay); gain.gain.linearRampToValueAtTime(0.06, ctx.currentTime + i * charDelay + 0.01); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + i * charDelay + charDelay); osc.connect(gain); gain.connect(ctx.destination); osc.start(ctx.currentTime + i * charDelay); osc.stop(ctx.currentTime + i * charDelay + charDelay); }); }; // --- Constants --- const CATEGORY_COLORS = [ { id: 'red', hex: '#FF5F5F', label: '긴급' }, { id: 'blue', hex: '#5EA9FF', label: '업무' }, { id: 'green', hex: '#66D19E', label: '일상' }, { id: 'purple', hex: '#B388FF', label: '약속' }, { id: 'orange', hex: '#FFB347', label: '학습' } ]; const FONT_OPTIONS = [ { id: 'Pretendard', name: '기본 고딕' }, { id: 'Nanum Myeongjo', name: '나눔 명조' }, { id: 'Gaegu', name: '개구쟁이체' } ]; // --- State Management --- const [tasks, setTasks] = useState([ { id: 1, title: '디자인 시스템 검토', category: 'blue', time: '오전 10:00', date: new Date().toISOString().split('T')[0], memo: '전체적인 컬러 팔레트 수정 필요', completed: false, showMemo: false, important: true }, { id: 2, title: '친구와 저녁 식사', category: 'purple', time: '오후 07:00', date: new Date().toISOString().split('T')[0], memo: '맛있는 파스타를 먹기로 함', completed: false, showMemo: false, important: false }, ]); const [displayMessage, setDisplayMessage] = useState('무슨 일을 도와줄까옹? 오늘 할 일을 확인해봐냥!'); const [isMenuOpen, setIsMenuOpen] = useState(false); const [activeTab, setActiveTab] = useState('tasks'); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [viewDate, setViewDate] = useState(new Date()); // Customization Settings const [catScale, setCatScale] = useState(1); const [uiScale, setUiScale] = useState(1); const [fontFamily, setFontFamily] = useState('Pretendard'); const [activeChar, setActiveChar] = useState('cat'); const [position, setPosition] = useState({ x: 50, y: 150 }); const [isDragging, setIsDragging] = useState(false); const [hasMoved, setHasMoved] = useState(false); const [aiAnalysis, setAiAnalysis] = useState(null); const [isAiLoading, setIsAiLoading] = useState(false); const [aiRecommendation, setAiRecommendation] = useState(null); const [isRecLoading, setIsRecLoading] = useState(false); // Task Form State const [isFormOpen, setIsFormOpen] = useState(false); const [editingTaskId, setEditingTaskId] = useState(null); const [formTitle, setFormTitle] = useState(''); const [formCategory, setFormCategory] = useState('blue'); const [isPM, setIsPM] = useState(false); const [hour, setHour] = useState(12); const [minute, setMinute] = useState(0); const dragOffset = useRef({ x: 0, y: 0 }); // --- AI Logic (Gemini API) --- const analyzeTasksWithAI = async () => { setIsAiLoading(true); const todayTasksData = tasks .filter(t => t.date === selectedDate) .map(t => `- [${t.time}] ${t.title}${t.memo ? ` (메모: ${t.memo})` : ''} [상태: ${t.completed ? '완료' : '미완료'}]`) .join('\n'); const systemPrompt = ` 당신은 사용자의 일정을 관리하는 귀여운 비서입니다. 사용자가 작성한 할 일 제목과 메모 내용을 바탕으로 '하루 요약'을 수행하세요. JSON 형식으로만 응답하세요: { "summary": "전체적인 하루 분위기를 담은 귀여운 말투의 요약", "recommendation": "메모 내용을 참고하여 사용자에게 해줄 따뜻한 격려나 조언", "mood": "분석된 사용자의 기분이나 태도 (열정적, 여유로움 등)" } `; const userQuery = `날짜: ${selectedDate}\n기록:\n${todayTasksData || '일정 없음'}`; try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json" } }) }); const data = await response.json(); const result = JSON.parse(data.candidates[0].content.parts[0].text); setAiAnalysis(result); setDisplayMessage(result.summary); } catch (error) { setDisplayMessage("분석하다가 졸음이 왔다옹... 다시 시도해줘냥!"); } finally { setIsAiLoading(false); } }; const getTaskRecommendation = async () => { setIsRecLoading(true); const taskList = tasks.filter(t => t.date === selectedDate).map(t => t.title).join(', '); const systemPrompt = ` 사용자의 오늘 할 일을 보고, 추가로 하면 좋을 만한 활동 하나를 추천하세요. JSON 형식으로 응답하세요: { "suggestion": "추천할 활동 내용", "reason": "왜 이 활동을 추천하는지 이유", "category": "red, blue, green, purple, orange 중 하나 선택" } `; const userQuery = `현재 할 일: ${taskList || '없음'}. 오늘 더 하면 좋을 게 있을까?`; try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json" } }) }); const data = await response.json(); const result = JSON.parse(data.candidates[0].content.parts[0].text); setAiRecommendation(result); } catch (error) { console.error(error); } finally { setIsRecLoading(false); } }; // --- Actions --- const addTaskFromAi = () => { if (!aiRecommendation) return; const now = new Date(); const isPMNow = now.getHours() >= 12; const currentHour = now.getHours() % 12 || 12; const currentMinute = Math.ceil(now.getMinutes() / 5) * 5; const newTask = { id: Date.now(), title: aiRecommendation.suggestion.trim(), category: aiRecommendation.category || 'green', time: `${isPMNow ? '오후' : '오전'} ${currentHour.toString().padStart(2, '0')}:${currentMinute.toString().padStart(2, '0')}`, date: selectedDate, memo: `AI 추천 사유: ${aiRecommendation.reason}`, completed: false, showMemo: false, important: false }; setTasks([newTask, ...tasks]); setAiRecommendation(null); setDisplayMessage(`AI 추천 일정을 추가했다옹!`); }; const openAddForm = () => { setEditingTaskId(null); setFormTitle(''); setFormCategory('blue'); setIsPM(false); setHour(12); setMinute(0); setIsFormOpen(true); }; const openEditForm = (task) => { setEditingTaskId(task.id); setFormTitle(task.title); setFormCategory(task.category || 'blue'); const [ampm, time] = task.time.split(' '); const [h, m] = time.split(':'); setIsPM(ampm === '오후'); setHour(parseInt(h)); setMinute(parseInt(m)); setIsFormOpen(true); }; const handleSaveTask = () => { if (!formTitle.trim()) return; const taskData = { title: formTitle, category: formCategory, time: `${isPM ? '오후' : '오전'} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`, date: selectedDate, }; if (editingTaskId) { setTasks(tasks.map(t => t.id === editingTaskId ? { ...t, ...taskData } : t)); setDisplayMessage(`수정 완료했다옹!`); } else { const newTask = { ...taskData, id: Date.now(), memo: '', completed: false, showMemo: false, important: false }; setTasks([newTask, ...tasks]); setDisplayMessage(`${formTitle} 등록 완료냥!`); } setIsFormOpen(false); }; const toggleComplete = (id) => setTasks(tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t)); const toggleImportant = (id) => setTasks(tasks.map(t => t.id === id ? { ...t, important: !t.important } : t)); const toggleMemo = (id) => setTasks(tasks.map(t => t.id === id ? { ...t, showMemo: !t.showMemo } : t)); const updateMemo = (id, memo) => setTasks(tasks.map(t => t.id === id ? { ...t, memo } : t)); const deleteTask = (id) => { setTasks(tasks.filter(t => t.id !== id)); setDisplayMessage("삭제 완료했다옹!"); }; useEffect(() => { if (displayMessage) playAnimalese(displayMessage); }, [displayMessage]); // --- Interaction Logic --- const handleMouseDown = (e) => { if (e.target.closest('button')) return; setIsDragging(true); setHasMoved(false); dragOffset.current = { x: e.clientX - position.x, y: e.clientY - position.y }; }; const handleMouseUp = () => { if (isDragging && !hasMoved) { setIsMenuOpen(prev => !prev); } setIsDragging(false); }; useEffect(() => { const handleMove = (e) => { if (isDragging) { setHasMoved(true); setPosition({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y }); } }; window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, hasMoved]); const handleMonthChange = (offset) => { const next = new Date(viewDate); next.setMonth(viewDate.getMonth() + offset); setViewDate(next); }; const VerticalDragValue = ({ label, value, min, max, onChange, step = 1 }) => { const isDraggingValue = useRef(false); const startY = useRef(0); const startValue = useRef(0); const handleStart = (e) => { isDraggingValue.current = true; startY.current = e.clientY; startValue.current = value; document.body.style.cursor = 'ns-resize'; }; useEffect(() => { const handleMove = (e) => { if (!isDraggingValue.current) return; const diff = Math.round((startY.current - e.clientY) / 10) * step; let newValue = startValue.current + diff; if (newValue < min) newValue = min; if (newValue > max) newValue = max; onChange(newValue); }; const handleEnd = () => { isDraggingValue.current = false; document.body.style.cursor = 'default'; }; window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleEnd); return () => { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleEnd); }; }, [value, min, max, onChange, step]); return (
{label} {value.toString().padStart(2, '0')}
); }; const menuStyles = useMemo(() => { const baseWidth = 460; const baseHeight = 720; const menuWidth = baseWidth * uiScale; const menuHeight = baseHeight * uiScale; let menuX = position.x + (200 * catScale) + 20; let menuY = position.y - 50; if (menuX + menuWidth > window.innerWidth) menuX = position.x - menuWidth - 20; if (menuY < 20) menuY = 20; if (menuY + menuHeight > window.innerHeight - 20) menuY = window.innerHeight - menuHeight - 20; return { left: menuX, top: menuY, width: baseWidth, height: baseHeight, transform: `scale(${uiScale})`, transformOrigin: 'top left' }; }, [position, catScale, uiScale]); return (
{/* Character Container */}
{/* Bubble Message */}

{displayMessage}

{/* Selected Character Render */}
{CHARACTERS[activeChar].render()}
{/* Main Menu UI */} {isMenuOpen && (

AI POCKET SECRETARY

{selectedDate}

{[ {id: 'tasks', icon: , label: 'TASKS'}, {id: 'calendar', icon: , label: 'CALENDAR'}, {id: 'settings', icon: , label: 'SETTINGS'} ].map(tab => ( ))}
{activeTab === 'tasks' && (
AI Suggestion
{isRecLoading ? (
) : aiRecommendation ? (

{aiRecommendation.suggestion}

{aiRecommendation.reason}

) : (

버튼을 눌러 AI의 추천을 받아보라냥!

)}
{isFormOpen ? (
{editingTaskId ? 'Edit Task' : 'Create Task'}
setFormTitle(e.target.value)} placeholder="무슨 일인가옹?" className="w-full border-b-2 border-black pb-3 mb-8 text-lg font-black focus:outline-none" />

Category

{CATEGORY_COLORS.map(color => (

Time

{editingTaskId && }
) : ( tasks.filter(t => t.date === selectedDate).map(t => (
c.id === t.category)?.hex }}>
{ e.stopPropagation(); toggleMemo(t.id); }}>

{t.time}

{t.title}

{t.showMemo && (
e.stopPropagation()}>