Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1주차] 김철흥 미션 제출합니다. #8

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2d113f2
Vanilla JS Setup
DefineXX Mar 15, 2025
92794b7
style: 스타일 가이드 및 스타일링 환경 세팅
DefineXX Mar 15, 2025
b347e1f
feat: To Do List 추가 기능 및 Task List UI 구현
DefineXX Mar 15, 2025
4867e4f
feat: 글자 수에 따른 컨테이너 초과 UX 보완
DefineXX Mar 15, 2025
b3c07d5
style: To Do 와 Done Section 레이아웃 세팅
DefineXX Mar 15, 2025
4f2a7d6
feat: Task에 대한 Check 기능 구현
DefineXX Mar 15, 2025
667f8b3
fix: Check Icon 언마운트 로직 추가
DefineXX Mar 15, 2025
3d752cb
feat: 현재 날짜 추가
DefineXX Mar 15, 2025
e28b796
feat: Task 삭제 버튼 UI 추가
DefineXX Mar 15, 2025
dd2b65c
feat: Task 삭제 로직 및 삭제 버튼 애니메이션 추가
DefineXX Mar 15, 2025
aee61f2
feat: Task Input 내 autofocus 추가 및 mobile phone 내 스크롤바 제거
DefineXX Mar 15, 2025
5effcda
feat: Task Count 구현
DefineXX Mar 15, 2025
82cde2b
fix: Vercel 정적 배포를 위해 디렉토리 구조 변경
DefineXX Mar 15, 2025
fd5045c
fix: script 및 stylesheet 경로 변경
DefineXX Mar 15, 2025
37c7760
docs: Update README.md
DefineXX Mar 15, 2025
60c2e62
chore: public 디렉토리로 index.html 이동
DefineXX Mar 15, 2025
b5d9d4d
fix: script 및 stylesheet 경로 변경
DefineXX Mar 15, 2025
f796480
chore: public 디렉토리 내 모든 소스 파일 위치
DefineXX Mar 15, 2025
e20ca56
chore: 디렉토리 구조 변경
DefineXX Mar 15, 2025
9f1b512
chore: src 디렉토리로 소스 파일 이동
DefineXX Mar 15, 2025
3856ed4
chore: 루트 디렉토리로 소스 파일 이동
DefineXX Mar 15, 2025
1aca14c
chore: 빌드 시 소스 파일 포함 위해 vercel.json 설정
DefineXX Mar 15, 2025
0968126
chore: vercel.json 수정
DefineXX Mar 15, 2025
fcb9b56
fix: svg 아이콘 경로 수정
DefineXX Mar 15, 2025
610beb3
fix: Completed Task에 대한 클래스명 변경 누락 수정
DefineXX Mar 15, 2025
ad0cece
feat: 모바일 화면에 대한 반응형 추가
DefineXX Mar 15, 2025
f83052b
feat: background에 대한 반응형 추가
DefineXX Mar 15, 2025
6f05b05
fix: 반응형 로직 수정
DefineXX Mar 15, 2025
ba21f13
fix: backgroud 반응형 수정
DefineXX Mar 15, 2025
521d0af
feat: 캘린더 기능 추가 및 UI 스타일링 수정
DefineXX Mar 16, 2025
3cef316
fix: Task 추가 form 태그로 변경 및 submit 핸들러로 변경
DefineXX Mar 16, 2025
ea45e49
refactor: Task CRUD 로직 refactor
DefineXX Mar 16, 2025
2724f5e
feat: 선택된 날짜에 대한 Task 렌더링 로직 추가
DefineXX Mar 16, 2025
113763e
fix: 새로고침시 발생하는 Task List 분류 버그 수정
DefineXX Mar 16, 2025
10535b8
fix: 모바일 기기 내 date input 렌더링 테스트
DefineXX Mar 17, 2025
2b31ee7
fix: 모바일 환경 대응 ver.1
DefineXX Mar 17, 2025
de5b2f3
fix: 불필요한 이벤트 리스너 삭제
DefineXX Mar 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions README.md

This file was deleted.

270 changes: 270 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
document.addEventListener('DOMContentLoaded', () => {
// Date-Picker trigger
const calendarButton = document.querySelector('.calendar-button');
const datePicker = document.getElementById('date-picker');

// 캘린더 열기
calendarButton.addEventListener('click', () => {
datePicker.showPicker ? datePicker.showPicker() : datePicker.click();
});

// 렌더링할 날짜 형식 변환
const formatSelectedDate = (selectedDate) => {
const year = selectedDate.getFullYear();
const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1
const day = String(selectedDate.getDate()).padStart(2, '0');

const weekdays = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
const weekday = weekdays[selectedDate.getDay()];

return `${year}.${month}.${day} (${weekday})`;
};

let selectedDateStr =
localStorage.getItem('selectedDate') ||
new Date().toISOString().split('T')[0];
let selectedDateObj = new Date(selectedDateStr);

// 초기 날짜 설정 (현재 날짜)
document.querySelector('.today-date').textContent =
formatSelectedDate(selectedDateObj);

// 날짜 선택 이벤트 처리
datePicker.addEventListener('change', (e) => {
selectedDateStr = e.target.value;

// 선택한 날짜 저장
localStorage.setItem('selectedDate', selectedDateStr);

const dateParts = selectedDateStr.split('-');
const selectedDateObj = new Date(
Number(dateParts[0]), // 연도
Number(dateParts[1]) - 1, // 월 (0부터 시작)
Number(dateParts[2]) // 일
);

document.querySelector('.today-date').textContent =
formatSelectedDate(selectedDateObj);

renderTasksForSelectedDate();
updateTaskCount();
});

// Task 추가 관련 요소들
const addTaskForm = document.getElementById('add-task-form');
const addTaskInput = document.querySelector('.add-task-input');
const toDoList = document.getElementById('to-do-list');
const doneList = document.getElementById('done-list');
const noTasksToDo = document.getElementById('no-tasks-to-do');
const noTasksDone = document.getElementById('no-tasks-done');
Comment on lines +54 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 리액트에서 전체 코드 순서를 변수 -> 함수 -> useEffect -> 렌더링과 같이 작성해 왔는데요, 바닐라에서도 마찬가지로 변수 -> 함수 순서로 작성하게 되더라고요. 그런 점에서 변수 -> 함수 -> 변수 -> 함수 같은 흐름은 관심사를 분리할 때라는 좋은 신호인 것 같습니다.

이 위로는 date 관련 코드이니 date.js에, 아래는 task와 관련된 코드이니 task.js로 분리해서, app.js에서는 그 함수들을 import하여 document에 이벤트 리스너를 등록하는 형식은 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 코드를 작성하면서 모듈화의 필요성을 느꼈는데, 이 부분 반영해서 리팩토링해보겠습니다 :)


// Task 개수 업데이트
const updateTaskCount = () => {
const todoListCount = toDoList.childElementCount;
const doneListCount = doneList.childElementCount;

if (todoListCount === 0) {
noTasksToDo.style.display = 'block';
noTasksToDo.textContent = 'Add Your Task!';
} else {
noTasksToDo.style.display = 'none';
}

if (doneListCount === 0) {
noTasksDone.style.display = 'block';
noTasksDone.textContent = 'No Tasks Done Yet!';
} else {
noTasksDone.style.display = 'none';
}

document.getElementById(
'to-do-list-title'
).textContent = `To Do (${todoListCount})`;
document.getElementById(
'done-list-title'
).textContent = `Done (${doneListCount})`;
};

// Task 목록
let tasks = JSON.parse(localStorage.getItem('tasks')) || [];

// Task 추가
const addTask = () => {
const taskValue = addTaskInput.value.trim();
if (taskValue === '') return;

// Task 객체 생성
const task = {
id: Date.now(),
text: taskValue,
completed: false,
date: selectedDateStr,
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 task 객체 unique ID로 관리하게 되면 장점이 많다고 생각합니다. 다만 Date.now()로 값을 할당하게 되면 date와 겹치는 부분이 있다고 생각해 hash 등을 사용해 암호화하는 방식은 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 시간을 고유한 ID로 사용한 후에, 날짜 선택 기능을 추가하면서 id의 값이 중복된 의미의 필드가 된 것 같네요!
고유 ID로 사용할 수 있는 더 좋은 방식을 채택해서 리팩토링해보겠습니다 :)


tasks.push(task);
localStorage.setItem('tasks', JSON.stringify(tasks));

// Task 렌더링
if (task.date === selectedDateStr) {
renderTask(task);
}

addTaskInput.value = '';
updateTaskCount();
};

// Task 렌더링
const renderTask = (taskObj) => {
const { id, text, completed } = taskObj;

// 성능 최적화를 위해 DocumentFragment 사용
const fragment = document.createDocumentFragment();

// Task Box + Delete Button
const taskDeleteContainer = document.createElement('li');
taskDeleteContainer.classList.add('task-delete-container');
taskDeleteContainer.dataset.id = id; // Task ID 저장

// Task Box
const taskItem = document.createElement('div');
taskItem.classList.add('task-item', 'to-do-item');

// Checkbox
const checkboxContainer = document.createElement('label');
checkboxContainer.classList.add('checkbox-container');

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.classList.add('task-checkbox');
checkbox.checked = completed;

// Task 내용
const taskText = document.createElement('span');
taskText.classList.add('task-text');
taskText.textContent = text;

if (completed) {
taskItem.classList.add('done-item');
taskText.classList.add('done-text');

// 완료된 Task에 체크 아이콘 추가
const checkIcon = document.createElement('img');
checkIcon.src = 'icons/check.svg';
checkIcon.alt = 'Check Icon';
checkIcon.classList.add('check-icon');
checkboxContainer.appendChild(checkIcon);
}

// 삭제 버튼
const taskDeleteButton = document.createElement('button');
taskDeleteButton.classList.add('task-delete-button');
taskDeleteButton.innerHTML =
'<img src="icons/trash.svg" alt="Delete Icon">';

// Task 삭제 Event Listener
taskDeleteButton.addEventListener('click', () => {
removeTask(id);
});

// Task 완료 상태 변경 Event Listener
checkbox.addEventListener('change', () => {
toggleTask(
id,
taskDeleteContainer,
taskItem,
taskText,
checkboxContainer
);
});

checkboxContainer.appendChild(checkbox);
taskItem.appendChild(checkboxContainer);
taskItem.appendChild(taskText);
taskDeleteContainer.appendChild(taskItem);
taskDeleteContainer.appendChild(taskDeleteButton);

fragment.appendChild(taskDeleteContainer);

if (completed) {
doneList.appendChild(fragment);
} else {
toDoList.appendChild(fragment);
}

updateTaskCount();
};

// Task 삭제
const removeTask = (id) => {
tasks = tasks.filter((task) => task.id !== id);
localStorage.setItem('tasks', JSON.stringify(tasks));

document.querySelector(`[data-id="${id}"]`).remove();
updateTaskCount();
};

// Task 완료 상태 변경
const toggleTask = (
id,
taskDeleteContainer,
taskItem,
taskText,
checkboxContainer
) => {
const task = tasks.find((task) => task.id === id);
task.completed = !task.completed;
localStorage.setItem('tasks', JSON.stringify(tasks));

taskItem.classList.toggle('done-item');
taskText.classList.toggle('done-text');

if (task.completed) {
const checkIcon = document.createElement('img');
checkIcon.src = 'icons/check.svg';
checkIcon.alt = 'Check Icon';
checkIcon.classList.add('check-icon');
checkboxContainer.appendChild(checkIcon);

doneList.appendChild(taskDeleteContainer);
} else {
const checkIcon = document.querySelector('.check-icon');
if (checkIcon) checkIcon.remove();

toDoList.appendChild(taskDeleteContainer);
}

updateTaskCount();
};

// 선택된 날짜에 대한 Task 렌더링
const renderTasksForSelectedDate = () => {
toDoList.innerHTML = '';
doneList.innerHTML = '';

const filteredTasks = tasks.filter((task) => task.date === selectedDateStr);
filteredTasks.forEach((task) => renderTask(task));
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 과제에서 그럴 일은 없겠지만, date 값을 객체 속성으로 두면 O(N)이 걸릴 것 같습니다. {id, date, text, completed} -> date: {id, text, completed} 이렇게 2차원으로 만드시는 건 어떠실까요? (보통 N >> k 이기 때문에0

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에서 date 필드와 id 필드를 리팩토링할 소요가 있었는데, date 필드 안에 task 객체 배열을 두는 방식으로 리팩토링하면 코드가 더 깔끔해질 것 같네요!

좋은 피드백 감사합니다 :)


renderTasksForSelectedDate();

addTaskForm.addEventListener('submit', (e) => {
e.preventDefault();

if (addTaskInput.value.trim() === '') {
alert('Please Enter Your Task!');
return;
} else {
addTask();
}
});

addTaskForm.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTask();
}
});

// 초기 화면 로딩 시 Task 개수 업데이트
updateTaskCount();
});
5 changes: 5 additions & 0 deletions icons/calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions icons/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading