1/*
2 * Todo App
3 * 기능: 추가, 삭제, 수정, 완료, 필터링, 로컬스토리지 저장
4 */
5
6// ============================================
7// State
8// ============================================
9let todos = [];
10let currentFilter = 'all';
11
12// ============================================
13// DOM Elements
14// ============================================
15const todoInput = document.getElementById('todoInput');
16const addBtn = document.getElementById('addBtn');
17const todoList = document.getElementById('todoList');
18const todoCount = document.getElementById('todoCount');
19const clearCompletedBtn = document.getElementById('clearCompleted');
20const filterBtns = document.querySelectorAll('.filter-btn');
21const currentDateEl = document.getElementById('currentDate');
22
23// ============================================
24// Initialize
25// ============================================
26function init() {
27 // 날짜 표시
28 displayCurrentDate();
29
30 // 로컬 스토리지에서 데이터 로드
31 loadTodos();
32
33 // 이벤트 리스너 등록
34 addEventListeners();
35
36 // 초기 렌더링
37 render();
38}
39
40function displayCurrentDate() {
41 const now = new Date();
42 const options = {
43 year: 'numeric',
44 month: 'long',
45 day: 'numeric',
46 weekday: 'long'
47 };
48 currentDateEl.textContent = now.toLocaleDateString('ko-KR', options);
49}
50
51// ============================================
52// Event Listeners
53// ============================================
54function addEventListeners() {
55 // 추가 버튼 클릭
56 addBtn.addEventListener('click', addTodo);
57
58 // Enter 키로 추가
59 todoInput.addEventListener('keypress', (e) => {
60 if (e.key === 'Enter') {
61 addTodo();
62 }
63 });
64
65 // 완료 항목 삭제
66 clearCompletedBtn.addEventListener('click', clearCompleted);
67
68 // 필터 버튼
69 filterBtns.forEach(btn => {
70 btn.addEventListener('click', () => {
71 setFilter(btn.dataset.filter);
72 });
73 });
74
75 // Todo 리스트 이벤트 위임
76 todoList.addEventListener('click', handleTodoClick);
77 todoList.addEventListener('change', handleTodoChange);
78}
79
80// ============================================
81// Todo CRUD Operations
82// ============================================
83function addTodo() {
84 const text = todoInput.value.trim();
85
86 if (!text) {
87 todoInput.focus();
88 return;
89 }
90
91 const newTodo = {
92 id: Date.now(),
93 text: text,
94 completed: false,
95 createdAt: new Date().toISOString()
96 };
97
98 todos.unshift(newTodo);
99 saveTodos();
100 render();
101
102 todoInput.value = '';
103 todoInput.focus();
104}
105
106function deleteTodo(id) {
107 todos = todos.filter(todo => todo.id !== id);
108 saveTodos();
109 render();
110}
111
112function toggleTodo(id) {
113 todos = todos.map(todo =>
114 todo.id === id ? { ...todo, completed: !todo.completed } : todo
115 );
116 saveTodos();
117 render();
118}
119
120function editTodo(id) {
121 const todoItem = document.querySelector(`[data-id="${id}"]`);
122 const todo = todos.find(t => t.id === id);
123
124 if (!todoItem || !todo) return;
125
126 // 편집 모드로 변경
127 todoItem.innerHTML = `
128 <input type="checkbox" ${todo.completed ? 'checked' : ''} disabled>
129 <input type="text" class="edit-input" value="${escapeHtml(todo.text)}">
130 <div class="todo-actions" style="opacity: 1;">
131 <button class="save-btn" data-action="save">저장</button>
132 <button class="cancel-btn" data-action="cancel">취소</button>
133 </div>
134 `;
135
136 const editInput = todoItem.querySelector('.edit-input');
137 editInput.focus();
138 editInput.select();
139
140 // Enter 키로 저장
141 editInput.addEventListener('keypress', (e) => {
142 if (e.key === 'Enter') {
143 saveTodoEdit(id, editInput.value);
144 }
145 });
146
147 // Escape 키로 취소
148 editInput.addEventListener('keydown', (e) => {
149 if (e.key === 'Escape') {
150 render();
151 }
152 });
153}
154
155function saveTodoEdit(id, newText) {
156 const text = newText.trim();
157
158 if (!text) {
159 render();
160 return;
161 }
162
163 todos = todos.map(todo =>
164 todo.id === id ? { ...todo, text: text } : todo
165 );
166 saveTodos();
167 render();
168}
169
170function clearCompleted() {
171 todos = todos.filter(todo => !todo.completed);
172 saveTodos();
173 render();
174}
175
176// ============================================
177// Event Handlers
178// ============================================
179function handleTodoClick(e) {
180 const todoItem = e.target.closest('.todo-item');
181 if (!todoItem) return;
182
183 const id = parseInt(todoItem.dataset.id);
184 const action = e.target.dataset.action;
185
186 switch (action) {
187 case 'delete':
188 deleteTodo(id);
189 break;
190 case 'edit':
191 editTodo(id);
192 break;
193 case 'save':
194 const editInput = todoItem.querySelector('.edit-input');
195 if (editInput) {
196 saveTodoEdit(id, editInput.value);
197 }
198 break;
199 case 'cancel':
200 render();
201 break;
202 }
203}
204
205function handleTodoChange(e) {
206 if (e.target.type === 'checkbox') {
207 const todoItem = e.target.closest('.todo-item');
208 if (todoItem) {
209 const id = parseInt(todoItem.dataset.id);
210 toggleTodo(id);
211 }
212 }
213}
214
215// ============================================
216// Filter
217// ============================================
218function setFilter(filter) {
219 currentFilter = filter;
220
221 // 버튼 활성화 상태 업데이트
222 filterBtns.forEach(btn => {
223 btn.classList.toggle('active', btn.dataset.filter === filter);
224 });
225
226 render();
227}
228
229function getFilteredTodos() {
230 switch (currentFilter) {
231 case 'active':
232 return todos.filter(todo => !todo.completed);
233 case 'completed':
234 return todos.filter(todo => todo.completed);
235 default:
236 return todos;
237 }
238}
239
240// ============================================
241// Render
242// ============================================
243function render() {
244 const filteredTodos = getFilteredTodos();
245
246 if (filteredTodos.length === 0) {
247 todoList.innerHTML = `
248 <li class="empty-state">
249 <p>${getEmptyMessage()}</p>
250 </li>
251 `;
252 } else {
253 todoList.innerHTML = filteredTodos.map(todo => `
254 <li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
255 <input type="checkbox" ${todo.completed ? 'checked' : ''}>
256 <span class="todo-text">${escapeHtml(todo.text)}</span>
257 <div class="todo-actions">
258 <button class="edit-btn" data-action="edit">편집</button>
259 <button class="delete-btn" data-action="delete">삭제</button>
260 </div>
261 </li>
262 `).join('');
263 }
264
265 updateCounter();
266}
267
268function getEmptyMessage() {
269 switch (currentFilter) {
270 case 'active':
271 return '진행중인 할 일이 없습니다! 🎉';
272 case 'completed':
273 return '완료된 할 일이 없습니다.';
274 default:
275 return '할 일을 추가해보세요! ✏️';
276 }
277}
278
279function updateCounter() {
280 const activeCount = todos.filter(todo => !todo.completed).length;
281 const totalCount = todos.length;
282 todoCount.textContent = `${activeCount}개 항목 남음 (전체 ${totalCount}개)`;
283}
284
285// ============================================
286// Local Storage
287// ============================================
288function saveTodos() {
289 localStorage.setItem('todos', JSON.stringify(todos));
290}
291
292function loadTodos() {
293 const stored = localStorage.getItem('todos');
294 if (stored) {
295 try {
296 todos = JSON.parse(stored);
297 } catch (e) {
298 console.error('Failed to load todos:', e);
299 todos = [];
300 }
301 }
302}
303
304// ============================================
305// Utility
306// ============================================
307function escapeHtml(text) {
308 const div = document.createElement('div');
309 div.textContent = text;
310 return div.innerHTML;
311}
312
313// ============================================
314// Start App
315// ============================================
316init();