app.js

Download
javascript 317 lines 8.1 KB
  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();