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: () => (

{
// 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 */}
{/* 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' && (
{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()}>
)}
))
)}
)}
{activeTab === 'calendar' && (
{viewDate.getFullYear()}. {(viewDate.getMonth() + 1).toString().padStart(2, '0')}
{['SUN','MON','TUE','WED','THU','FRI','SAT'].map(d =>
{d}
)}
{Array.from({ length: new Date(viewDate.getFullYear(), viewDate.getMonth(), 1).getDay() }).map((_, i) =>
)}
{Array.from({ length: new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 0).getDate() }).map((_, i) => {
const dateStr = `${viewDate.getFullYear()}-${(viewDate.getMonth() + 1).toString().padStart(2, '0')}-${(i + 1).toString().padStart(2, '0')}`;
const hasTasks = tasks.some(t => t.date === dateStr);
return (
);
})}
)}
{activeTab === 'settings' && (
Daily Deep Analysis
하루 요약 분석
{aiAnalysis ? (
{aiAnalysis.mood}
"{aiAnalysis.recommendation}"
{aiAnalysis.summary}
) : (
오늘의 할 일을 분석해 피드백을 드릴게요!
)}
{/* Character Selection */}
Characters
{Object.values(CHARACTERS).map(char => (
))}
Typography
{FONT_OPTIONS.map(font => (
))}
)}
{!isFormOpen && activeTab === 'tasks' && (
)}
)}
);
};
export default App;