- ToDo List EXP 미션
- 기존 ToDo 앱을 확장성을 가지고 개발 진행
- 새롭게 추가한 내용
- 백로그, 체크리스트 본문, 완료된 태스크 총 3가지 항목으로 분리
- 다크모드 기능
- 검색, 필터링 기능
- 진행 과정
- 기능 요약
- 패키지 모듈화 작업 (최적화 및 유지 보수성을 지키기 위해 파일 분리 후 모듈화 실시)
- index.html : 기본적인 웹 페이지 구조
- css : html css 파일 패키지
- style.css : html 스타일 css 파일
- darkMode.css : 다크모드 기능을 위한 css 파일
- scripts : Javascript 파일 패키지
-
backlogTask.js : 백로그 데이터 셋 생성, 리스트 정렬, 검색, CRUD 기능을 포함한 기능의 js 파일
sortTodos(): 백로그 리스트 정렬 함수createTask(): 새로운 태스크 데이터 생성 함수addBackLogElement(): 백로그 요소 생성 함수
-
completedTask.js : 완료된 태스크 처리 js 파일
renderCompletedTasks(): 완료된 태스크 처리 함수
-
currentTask.js : 백로그를 통해 넘어온 데이터 기반 체크리스트 본문 CRUD 및 정렬 기능 js 파일
checkListBody(): 체크리스트 본문 랜더링 및 정렬 기능 함수addCheckListBodyElement(): 체크리스트 요소 생성 함수finishEdit(): 체크리스트 본문 수정 처리 함수
-
script.js : 로컬스토리지 활용, 데이터 삭제 함수 등 데이터 및 최소 실행 작업에 대한 js 파일
saveToLocalStorage(): 로컬 스토리지 저장 함수loadLocalStorage(): 로컬 스토리지 load 함수displayTodoList(): 초기 실행 시 로컬 스토리지 데이터 기반 UI 송출todoDelete(): 데이터 삭제 함수
-
subTask.js : 체크리스트 본문의 각 데이터의 하위 태스크 CRUD 기능 js 파일
renderInitialSubTasks(): 전체 하위 태스크 초기 랜더링 함수renderSubTaskWrapper(): html wrapper에 대한 하위 태스크 랜더링 함수clearSubTaskEl(): 하위 태스크 요소 전체 삭제 함수findBacklogId(): 백로그 ID 찾기 함수insertSubTaskAddButton(): 하위 태스크 요소 삽입 함수createSubTaskElement(): 하위 태스크 요소 생성 함수initSubtaskAddButtons(): 하위 태스크 버튼 이벤트 연결 함수toggleSubtask(): 체크리스트 본문 토글 버튼 연결
-
element.js : html element 생성 js 파일
-
mypage.js : 사용자 페이지 요소 생성 js 파일
mypageModal(): 아이콘, 모달, 닫기 버튼 생성 함수
-
backUplist.js : 작성했던 할일 목록을 보여주기 위한 js 파일
saveToBackUpStorage(): Backup 리스트 로컬 스토리지 저장 함수loadToBackUpStorage(): Backup 리스트 로컬 스토리지 로드 함수
-
completedDeleteList.js : 사용자 페이지에서 작성 했던 할일 목록을 보여주기 위한 js 파일
modalCompletedList(): 작성 했던 할일 목록 정렬 및 랜더링 함수completedDeleteListEl(): 작성 했던 할일 목록 요소 생성 함수
-
modalBackLog.js : 사용자 페이지의 현재 할일 목록 랜더링 및 생성 js 파일
modalBacklogListBody(): 할일 목록 정렬 및 랜더링 함수addModalBacklogListBodyElement(): 할일 목록 요소 생성 함수
-
initEventListeners.js : 각 파일의 이벤트 관리 js 파일
-
백로그
initBackLogEvents(): 전체 백로그 초기화 이벤트editBtnEvent(): 수정 버튼 이벤트deleteBtnEvent(): 삭제 버튼 이벤트selectedEvent(): 중요도 변경 이벤트arrowEvent(): 백로그 <-> 체크리스트 이동 화살표 버튼 이벤트searchBtnEvent(): 검색 기능 이벤트initBackLogButtons(): 새로운 백로그 생성 이벤트 (데이터 생성)highlightUrgentTasks(): 종료일 마감 임박 이벤트
-
체크리스트
modBtnEvent(); 수정 버튼 이벤트taskBtnEvent(): 하위 태스크 토글 버튼 이벤트addBtnEvent(): 하위 태스크 추가 버튼 이벤트saveEvent(): 수정 후 저장 이벤트
-
하위 태스크
initSubTaskEvents(): 전체 하위 태스크 이벤트 초기화checkboxEvent(): 체크박스 이벤트deleteEvent(): 삭제 버튼 이벤트inputConfirmEvent(): 하위 태스크 입력 확인 이벤트
-
완료된 태스크
completedTaskrestore(): 완료된 태스크 -> 체크리스트 복귀 이벤트initCompletedTaskEvents(): 완료된 태스크 삭제 이벤트
-
사용자 페이지
openMypageModalEvents(): 사용자 페이지 모달 창 열기 이벤트closeMypageModalEvents(): 사용자 페이지 모달 창 닫기 이벤트
-
-
- 패키지 모듈화 작업 (최적화 및 유지 보수성을 지키기 위해 파일 분리 후 모듈화 실시)
- 주요 기능 코드
로컬 스토리지 사용
메인 페이지 데이터 저장 로컬 스토리지// localStorage에 List 저장
const saveToLocalStorage = () => {
localStorage.setItem("todoList", JSON.stringify(todos));
};
// 리로드 했을 시 localStorage에 todoList 가 있다면 불러와서 JSON 형태로 만든 후 todos 에 초기화
const loadLocalStorage = () => {
const data = localStorage.getItem("todoList");
console.log(JSON.parse(data));
if (data) {
todos = JSON.parse(data);
}
};
// 처음 로드 되었을 때 localStorage 를 확인 후 있다면 todoList를 생성
const displayTodoList = () => {
loadLocalStorage();
sortTodos();
highlightUrgentTasks();
};사용자 페이지 데이터(작성 했던 할일 목록) 저장 로컬 스토리지
export let backUpdata = [];
export const saveToBackUpStorage = (item) => {
const backUpdata = {
id: item.id,
title: item.title,
date: item.date,
};
// 기존 백업 데이터를 불러오거나 새 배열 생성
const existing = JSON.parse(localStorage.getItem("backUpDataList")) || [];
existing.push(backUpdata);
// 다시 저장
localStorage.setItem("backUpDataList", JSON.stringify(existing));
};
export const loadToBackUpStorage = () => {
const data = localStorage.getItem("backUpDataList");
if (data) {
backUpdata = JSON.parse(data);
}
};백로그 정렬 및 CRUD
const sortTodos = (keyword = "") => {
const filtered = todos.filter((todo) => !todo.complete && (keyword ? todo.title.includes(keyword) : true));
filtered.sort((a, b) => {
// 날짜가 없는지 여부 확인 (빈 문자열 혹은 falsy 값인 경우)
const noDateA = !a.date || a.date === "";
const noDateB = !b.date || b.date === "";
// 한쪽만 날짜가 없으면, 날짜가 없는 항목이 앞에 오도록 함
if (noDateA && !noDateB) {
return -1; // a가 b보다 앞에 오도록 함
} else if (!noDateA && noDateB) {
return 1; // b가 a보다 앞에 오도록 함
} else {
// 둘 다 날짜가 있는 경우: 날짜를 비교하여 정렬
const dateA = new Date(a.date);
const dateB = new Date(b.date);
if (dateA.getTime() === dateB.getTime()) {
// 날짜가 같으면 importance 값을 비교
return a.importance - b.importance;
} else {
// 날짜를 기준으로 정렬
return dateA - dateB;
}
}
});
backLogList.innerHTML = "";
filtered.forEach((todo) => {
const { backLogContainer } = addBackLogElement(todo);
backLogList.appendChild(backLogContainer);
});
saveToLocalStorage();
highlightUrgentTasks();
};
// 백로그 이벤트 - 검색 기능 이벤트
const searchBtnEvent = () => {
const keyword = document.querySelector(".searchBar");
keyword.addEventListener("keydown", (e) => {
const word = keyword.value.trim();
if (e.key == "Enter") {
sortTodos(word);
}
});
};
const initBackLogButtons = () => {
const addTaskBtn = document.querySelector(".addTask");
const searchBtn = document.querySelector(".searchButton");
addTaskBtn.addEventListener("click", () => {
createTask();
saveToLocalStorage();
console.log(todos);
});
searchBtn.addEventListener("click", () => {
const keyword = document.querySelector(".searchBar").value.trim();
sortTodos(keyword);
});
};
// 백로그 이벤트 - edit 버튼 이벤트
const editBtnEvent = ({ state, finishDateContent, backLogTaskContent, editBtn, items }) => {
editBtn.addEventListener("click", () => {
backLogTaskContent.removeAttribute("disabled");
finishDateContent.removeAttribute("disabled");
backLogTaskContent.focus();
if (backLogTaskContent.value !== "" && finishDateContent.value !== "") state.editing = true;
});
// 날짜를 변경 했을 시
finishDateContent.addEventListener("change", (e) => {
items.date = e.target.value;
state.date = true;
});
// 제목을 입력 시
backLogTaskContent.addEventListener("input", (e) => {
items.title = e.target.value;
});
//제목 엔티 눌렀을 떄
backLogTaskContent.addEventListener("keydown", (e) => {
if (e.key == "Enter") {
if (state.editing) {
finishDateContent.disabled = true;
state.editing = false;
sortTodos();
}
e.target.disabled = true;
state.title = true;
saveToLocalStorage();
}
});
backLogTaskContent.addEventListener("blur", () => {
if (!state.editing) {
backLogTaskContent.disabled = items.title === "" ? false : true;
state.title = true;
}
saveToLocalStorage();
window.dispatchEvent(new CustomEvent("updateChecklist"));
});
};
// 백로그 이벤트 - 삭제 버튼 이벤트
const deleteBtnEvent = ({ backLogContainer, deleteBtn, items }) => {
deleteBtn.addEventListener("click", (e) => {
backLogList.removeChild(backLogContainer);
todoDelete(items);
window.dispatchEvent(new CustomEvent("updateChecklist"));
});
};체크리스트 본문 정렬 및 CRUD
const checkListBody = () => {
checkList.innerHTML = "";
todos
.filter(todo => todo.moveCheck && !todo.complete)
.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
if (dateA < dateB) return -1;
if (dateA > dateB) return 1;
return a.importance - b.importance;
})
.forEach(todo => checkList.appendChild(addCheckListBodyElement(todo)));
};
// 수정 완료시 적용
const finishEdit = ({ isEditing, titleSpan, titleInput, dateSpan, dateInput, todo }) => {
if (!isEditing) return;
titleSpan.innerText = titleInput.value;
dateSpan.innerText = dateInput.value;
titleInput.style.display = "none";
dateInput.style.display = "none";
titleSpan.style.display = "inline";
dateSpan.style.display = "inline";
todo.title = titleInput.value;
todo.date = dateInput.value;
window.dispatchEvent(new CustomEvent("updateBackLog"));
saveToLocalStorage();
};
export const modBtnEvent = ({ titleSpan, titleInput, dateSpan, dateInput, modBtnEl, todo }) => {
let isEditing = false;
modBtnEl.addEventListener("click", (e) => {
e.stopPropagation();
isEditing = true;
titleInput.style.display = "inline";
dateInput.style.display = "inline";
titleSpan.style.display = "none";
dateSpan.style.display = "none";
titleInput.style = "display: inline; padding: 8px; border-radius: 8px; border: 1px solid #ccc; font-size: 14px; width: 90%; margin-bottom: 6px;";
dateInput.style = "display: inline; padding: 6px; border-radius: 6px; border: 1px solid #ccc; font-size: 14px;";
titleInput.focus();
saveEvent({ isEditing, titleSpan, titleInput, dateSpan, dateInput, todo });
});
};
// 체크리스트 본문 - 추가(+) 버튼 이벤트
export const addBtnEvent = ({ addBtnEl, todo, wrapper }) => {
addBtnEl.addEventListener("click", (e) => {
const container = wrapper.querySelector(".subtaskContainer");
initSubtaskAddButtons(todo.id, container, addBtnEl);
});
};
// 체크리스트 본문 - save 이벤트
const saveEvent = ({ isEditing, titleSpan, titleInput, dateSpan, dateInput, todo }) => {
// 외부 클릭 시 저장
const clickHandler = (e) => {
if (isEditing && ![titleInput, dateInput].includes(e.target)) {
finishEdit({ isEditing, titleSpan, titleInput, dateSpan, dateInput, todo });
document.removeEventListener("click", clickHandler);
}
};
document.addEventListener("click", clickHandler);
// 엔터 키 클릭 시 저장
titleInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
finishEdit({ isEditing, titleSpan, titleInput, dateSpan, dateInput, todo });
document.removeEventListener("click", clickHandler);
}
});
dateInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
finishEdit({ isEditing, titleSpan, titleInput, dateSpan, dateInput, todo });
document.removeEventListener("click", clickHandler);
}
});
};
//하위 태스크 이벤트
// 삭제 버튼 이벤트
const deleteEvent = ({ div, backlogId, subTask, delBtn }) => {
delBtn.addEventListener("click", () => {
const backlog = todos.find((item) => item.id === backlogId);
if (!backlog) return;
backlog.list = backlog.list.filter((item) => item.id !== subTask.id);
saveToLocalStorage();
div.remove();
});
};
// 입력 확인 이벤트
const inputConfirmEvent = ({ div, backlogId, subTask, checkbox, delBtn, input }) => {
let isConfirmed = false;
const confirm = () => {
if (isConfirmed) return;
isConfirmed = true;
const value = input.value.trim();
if (!value) {
const backlog = todos.find((item) => item.id === backlogId);
if (backlog) {
backlog.list = backlog.list.filter((item) => item.id !== subTask.id);
saveToLocalStorage();
}
div.remove();
return;
}
subTask.text = value;
const span = document.createElement("span");
span.className = "subtaskText";
span.textContent = subTask.text;
if (subTask.check) {
span.style.textDecoration = "line-through";
span.style.opacity = "0.6";
}
input.replaceWith(span);
initSubTaskEvents({ div, backlogId, subTask, textEl: span, checkbox, delBtn, input: null });
saveToLocalStorage();
};
input.addEventListener("keydown", (e) => e.key === "Enter" && confirm());
input.addEventListener("blur", confirm);
};
// 하위 태스크 요소 생성
const createSubTaskElement = (backlogId, subTask, editable = false) => {
const div = addEl("div", "subtaskItem");
div.setAttribute("data-sub-id", subTask.id);
const checkbox = addEl("input", "subtaskCheck", "", "", "checkbox");
checkbox.checked = !!subTask.check;
let textSpan = null;
let input = null;
if (editable) {
const style = "width: 100%; text-align: center; border: none; outline: none; background: transparent;";
input = addEl("input", "subtaskText", "", "", "text", style);
input.value = subTask.text || "";
} else {
textSpan = addEl("span", "subtaskText", subTask.text);
if (subTask.check) {
textSpan.style.textDecoration = "line-through";
textSpan.style.opacity = "0.6";
}
}
const delBtn = addEl("button", "subtaskDelete", "🗑︎");
if (editable) {
div.append(checkbox, input, delBtn);
} else {
div.append(checkbox, textSpan, delBtn);
}
initSubTaskEvents({ div, backlogId, subTask, textSpan, checkbox, delBtn, input });
return div;
};
// 버튼 이벤트 연결
const initSubtaskAddButtons = (backlogId, container, addBtn) => {
const backlog = todos.find((b) => b.id === backlogId);
if (!backlog) return;
const newId = Date.now();
const newTask = { id: newId, text: "", check: false };
backlog.list.push(newTask);
const div = createSubTaskElement(backlogId, newTask, true);
container.insertBefore(div, addBtn);
const input = div.querySelector('input[type="text"]');
if (input) input.focus();
};완료 태스크 CRUD
// 완료된 태스크 아래로 옮기는 함수
export const renderCompletedTasks = (todos) => {
// .completedTaskContainer라는 클래스를 가진 요소를 찾아서 container 변수에 저장
const container = document.querySelector(".completedTaskContainer");
// 중복 생성 방지를 위해 화면 초기화
container.innerHTML = "";
// complete: true인 항목을 completed 베열에 넣기
const completed = todos.filter(item => item.complete);
// 배열을 하나씩 순회하면서 item이라는 이름으로 꺼내옴
completed.forEach(item => {
// div, className 만든다
const taskItem = addEl("div", "completedTaskItem");
const infoDiv = addEl("div", "completedTaskInfo");
const titleDiv = addEl("div", "completedTaskTitle");
const deleteAndButton = addEl("div", "deleteAndbutton");
const restoreEl = addEl("button", "restore", "↩︎");
const delBtn = addEl("button", "delete", "🗑︎", "", "");
completedTaskrestore({ restoreEl, backlogId: item.id });
// 완료된 항목에 하이픈 처리
titleDiv.innerHTML = `<del>${item.title || "(제목 없음)"}</del>`;
const dateDiv = addEl("div", "completedTaskDate", item.date);
infoDiv.appendChild(titleDiv);
infoDiv.appendChild(dateDiv);
deleteAndButton.append(delBtn);
deleteAndButton.append(restoreEl);
taskItem.appendChild(infoDiv);
taskItem.appendChild(deleteAndButton);
container.appendChild(taskItem);
initCompletedTaskEvents({ item, delBtn });
});
};-
팀워크 및 활동 하이라이트
- 피드백 타임
- 기능과 코드 중점
- git - PR 코드 리뷰
- 카톡 캡쳐본
- 트러블 슈팅
- 기능과 코드 중점
- 파트별 분배(백로그/체크리스트)
- 백로그 팀, 체크리스트 본문 팀 나누어서 작업 진행한 부분
- ex : 브랜치 작업 설명
- 백로그 : backLogBody + backLogSearch + backLogHighlightTask = totalBackLog
- 체크리스트 본문 : checklistBody + subTaskController = checkList_subTask
- 병합 : totalBackLog + checkList_subTask + completedTask = ToDoApp
- 버전 관리 : ToDoApp(Dev) = Main
- 피드백 타임
-
느낀점
- 개인 생각하는 아쉬운점, 느낀점, 개선할 점
- 우진 : 팀원 분들과 첫 프로젝트를 진행하여 재밌었고, 부족한 부분이 너무 많았지만 팀원 분들의 코드 및 피드백을 들으며 내가 생각하지 못한 부분까지 생각나게 하며 내가 성장할 수 있는 프로젝트였습니다.
- 나영 : “진짜” 프로젝트를 해본 거 같아서 너무 신기하고, 부족한 점이 많아서 더 열심히 해야겠다고 느꼈다. 그리고 하나의 프로젝트 하는데 많은 시간과 수정사항이 들어간다는 점을 깨닫게 되었다 !!
- 지연 : 다른 팀원 분들로부터 배울 점이 많은 프로젝트였다. 전에 했던 프로젝트에서는 이렇게 꼼꼼하게 피드백 하지 않았었는데 꼼꼼한 피드백을 통해 작은 프로젝트라도 더욱 완성도 있는 결과가 나오는 것 같다.
- 유림 : 오랜만에 팀 프로젝트를 하며 즐거운 시간을 보냈고, 의사소통의 중요성을 다시끔 느낄 수 있었다. 다만, 초기에 Git을 활용한 형상 관리의 기반을 제대로 마련하지 못한 점은 아쉬움으로 남아, 다음 프로젝트에서는 이를 보완해야겠다.
- 용선 : 팀원 분들과 첫 프로젝트를 진행하면서 부족한 점들이 많았지만 이를 계기로 새롭게 배운 점이 많았고 다음 프로젝트에는 보다 나은 프로젝트가 될 수 있게 노력해야겠다.
- 개인 생각하는 아쉬운점, 느낀점, 개선할 점


