https://fastcampus.co.kr/b2g_MegabyteSchool_frontend
패스트캠퍼스 X 야놀자 : 프론트엔드 개발 부트캠프 | 패스트캠퍼스
프론트엔드 개발자로 취업까지 함께 가는 패스트캠퍼스 X 야놀자 부트캠프를 만나보세요!
fastcampus.co.kr
패스트캠퍼스X야놀자 프론트엔드 부트캠프 두 번째 과제로 JS를 활용해 직원 관리 서비스를 구현하는 과제를 받았다.
2023.08.08 ~ 2023.08.18 약 10일간 진행 됐고 나는 맨유 선수단에 선수를 추가하고 수정, 삭제 하는 기능을 구현 했다.
1. 과제 내용
1) 과제 설명
: 주어진 기간 동안 필수 요구사항 및 선택 요구사항을 고려해서 직원 관리 서비스를 구현하는 내용이다.
2) 과제 요구 사항 (구현 한 것들)
☆필수 요구사항
- “AWS S3 / Firebase 같은 서비스”를 이용하여 사진을 관리할 수 있는 페이지를 구현하세요.
- 프로필 페이지를 개발하세요.
- 스크롤이 가능한 형태의 리스팅 페이지를 개발하세요.
- 전체 페이지 데스크탑-모바일 반응형 페이지를 개발하세요.
- 사진을 등록, 수정, 삭제가 가능해야 합니다.
- 유저 플로우를 제작하여 리드미에 추가하세요.
- CSS
- 애니메이션 구현
- 상대수치 사용(rem, em)
- JavaScript
- DOM event 조작
☆선택 요구사항
- 사진 관리 페이지와 관련된 기타 기능도 고려해 보세요.
- 직원을 등록, 수정, 삭제가 가능하게 해보세요.
- 직원 검색 기능을 추가해 보세요.
- LocalStorage 사용
2. 과제 구현
https://jinjongsufastcampus.web.app/index.html
맨유 선수 관리 서비스
jinjongsufastcampus.web.app
3. 제출 후 느낀 점
"막연히 어렵기만 했던 자바스크립트가 정리가 되어가는 느낌이 들었지만 아직 부족하다는 점"
사실 자바스크립트라는 언어가 다른 공부해왔던 언어들에 비해 손에 익지 않고 계속 개념에 대한 이해가 붕 뜨는 기분이였는데 이번 과제와 함께 들었던 강의를 계기로 조금은 익숙해져가고 틀이 잡혀가는 기분이다. 그러나 아직은 부족하다는 점 또한 절실히 배운 과제였다.
4. 피드백
: 멘토님과 스터디원들이 코드리뷰를 통해 피드백을 해주셨다. 이전 과제처럼 이번 과제도 상당히 자세하고 구체적으로 써주셔서 피드백 요소가 무척이나 많다는 점과 또 많은 것을 배울 수 있었다. 팀원들의 코드 또한 상당히 배울 점이 많았다. 운이 좋게도 그룹스터디 조에서 우수 과제 제출자로 뽑혔지만 다른 팀원들의 과제에 비하면 부족한데 운 좋게도 다들 좋게 봐주신 것같아 감사하다. 피드백 내용은 아래와 같다.
- 코드 들여쓰기에 좀 더 신경썼으면 좋겠다.
- 이벤트리스너를 한 번에 묶는 함수를 만들어주면 관리하기 편할 것같다.
- 복잡한 함수는 별도로 빼주는게 유지보수 및 확장성 측면에서 유리하다,
- 전체 선택 , 전체 해제 같은 상수 데이터는 별도의 상수를 선언해주는 것이 좋다.
- window.location.href 함수도 별도의 함수로 만들어주면 좋다.
- style.css에 모든 페이지의 스타일을 넣었는데 각 페이지 별로 스타일을 나누는게 좋다.
- input 태그의 이름을 btn 으로 지어서 헷갈리다. 클래스명과 변수명을 input으로 적어주는 것이 좋다.
- input 값과 같은 단일데이터는 querySelector보단 getElementById를 사용하는 것이 좋다.
- 동일한 로직 있는 코드를 하나로 개선하면 좋겠다.
5. 리팩토링
: 2023.08.28 리팩토링 시간을 가졌고 위의 피드백을 토대로 코드를 수정했다.
1) 코드 들여쓰기
: 코드 들여쓰기 부분은 당연히 잘 하고 있다 생각해서 생각치 못한 부분인데 이 부분을 놓쳤던 점이 조금 아쉬웠다. 지금이라도 이 부분에 대해 생각해볼 수 있는 좋은 기회라 생각하고 기본부터 다잡아야겠다고 다짐했다.
※ 기존 코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./styles/styles.css">
<link rel="favicon" type="image/x-icon" href="./assets/icon/favicon.png">
<title>맨유 선수 관리 서비스</title>
</head>
<body class="index__body">
<div class="container">
<div class="index__border">
<header class="header">
<div class="header__maintitle">
<h1>맨유 선수단 관리 서비스</h1>
</div>
<div class="header__subtitle">
<h2>내가 원하는 선수로 선수단을 꾸려보세요!</h2>
</div>
</header>
<section class="start__tab">
<button class="start__button">시작하기</button>
</section>
</div>
</div>
<script src="./js/index.js"></script>
</body>
</html>
<header class="playerInfo__header">
<Nav class="nav">
<div class="nav__item">
<div class="logo-tab__menu">
<img class="logo__icon" src="./assets/icon/team_logo1.png" alt="team-logo">
<ul class="logo-tab__button">
<li><a href="playerList.html">선수단 관리 페이지</a></li>
<li><a href="playerInfo.html">프로필 정보</a></li>
<li><a href="playerUpload.html">선수 업로드 페이지</a></li>
</ul>
</div>
</div>
</Nav>
</header>
※ 수정 후
<body class="index__body">
<div class="container">
<div class="index__border">
<header class="header"> // header class 내부 들여쓰기 완료
<div class="header__maintitle">
<h1>맨유 선수단 관리 서비스</h1>
</div>
<div class="header__subtitle">
<h2>내가 원하는 선수로 선수단을 꾸려보세요!</h2>
</div>
</header>
<section class="start__tab">
<button class="start__button">시작하기</button>
</section>
</div>
</div>
<script src="./js/index.js"></script>
</body>
<header class="playerInfo__header">
<Nav class="nav">
<div class="nav__item">
<div class="logo-tab__menu">
<img class="logo__icon" src="./assets/icon/team_logo1.png" alt="team-logo"/>
<ul class="logo-tab__button">
<li><a href="playerList.html">선수단 관리 페이지</a></li>
<li><a href="playerInfo.html">프로필 정보</a></li>
<li><a href="playerUpload.html">선수 업로드 페이지</a></li>
</ul>
</div>
</div>
</Nav>
</header>
/* firebase 데이터 반영 */
selectedPlayerId = selectedPlayerData.id;
const db = firebase.firestore();
const storage = firebase.storage();
if(newImage){
let storageRef = storage.ref();
let storagePath = storageRef.child('image/'+ newImage.name);
let updateWork = storagePath.put(newImage)
updateWork.on('state_changed',
null,
(error)=>{
console.error('Error : ', error);
},
()=>{
updateWork.snapshot.ref.getDownloadURL().then((updateUrl)=>{
updatedData.image = updateUrl;
db.collection('Player').doc(selectedPlayerId).update(updatedData)
.then(()=>{
updatePage(updatedData);
closeModal();
alert('선수 정보 수정이 완료됐습니다.');
console.log('업데이트 완료');
})
.catch((error)=>{
alert('선수 정보 수정을 실패했습니다.')
console.error('선수 정보 수정 실패', error);
});
localStorage.setItem('selectedPlayer',JSON.stringify(updatedData));
});
}
)
}else{
db.collection('Player').doc(selectedPlayerId).update(updatedData)
.then(()=>{
updatePage(updatedData);
closeModal();
alert('선수 정보 수정이 완료됐습니다.');
console.log('업데이트 완료');
})
.catch((error)=>{
alert('선수 정보 수정을 실패했습니다.')
console.error('선수 정보 수정 실패', error);
});
localStorage.setItem('selectedPlayer',JSON.stringify(updatedData));
}
}
const SavePage = ()=>{
const saveBtn = document.querySelector('.save__btn');
saveBtn.addEventListener('click',()=>{
window.location.href="playerList.html";
})
}
saveModalBtn.addEventListener('click',saveModal)
SavePage();
};
2) 함수 정리(복잡한 함수 따로 빼기 + 이벤트리스너 함수 통합 setup 함수 생성)
: 복잡한 함수를 따로 component로 빼고 같은 파일 내 이벤트리스너의 반복되는 것을 모아서 통합 하는 함수를 만들어줬다. 코드의 가독성 및 확장성과 유지보수에 용이하도록 코드를 수정했다. component 폴더를 따로 만들어서 Logo.js와 SearchPlayer.js를 만들어서 import 해주었다.
※ 기존 코드
SearchBtn.addEventListener('keyup',()=>{
const filterPlayer = SearchBtn.value.toLowerCase();
const filterRow = document.querySelectorAll('.player__cell');
filterRow.forEach((cell)=>{
let cellText = cell.textContent.toLowerCase();
if(cellText.includes(filterPlayer)){
cell.style.display='';
}else{
cell.style.display = 'none';
}
});
SearchBtn.addEventListener('keyup',()=>{
const filterPlayer = SearchBtn.value.toLowerCase();
const filterRow = document.querySelectorAll('.player__cell');
filterRow.forEach((cell)=>{
let cellText = cell.textContent.toLowerCase();
if(cellText.includes(filterPlayer)){
cell.style.display='';
}else{
cell.style.display = 'none';
}
});
const moveToPlayerUpload = () =>{
addBtn.addEventListener("click", ()=>{
window.location.href="playerUpload.html;
});
}
removeBtn.addEventListener('click',()=>{
removePlayer();
});
allSelectBtn.addEventListener('click',()=>{
const allCheckboxes = document.querySelectorAll('.checkbox__container input[type="checkbox"]');
const isSelected = allCheckboxes[0].checked;
allCheckboxes.forEach(checkbox => {
checkbox.checked = !isSelected;
});
※ 수정 후
import {searchPlayer} from './component/SearchPlayer.js';
function setup(){
addBtn.addEventListener("click", ()=> moveToExternalPage('playerUpload.html'));
removeBtn.addEventListener('click', removePlayer);
searchBtn.addEventListener('keyup', ()=> searchPlayer(searchBtn));
allSelectBtn.addEventListener('click', allSelectedPlayers);
}
setup();
3) 상수 처리
: 전체 선택 및 전체 삭제는 TextContent로 내용만 바꿔주는 삼항연산자로 작성했는데 나중에 코드가 비대해질 때를 대비해서 이를 상수처리 하였다. selectAllPlayer과 deleteAllPlayer 변수를 새로 만들어 주었다.
※ 기존 코드
const allSelectedPlayers = ()=>{
const allCheckboxes = document.querySelectorAll('.checkbox__container input[type="checkbox"]');
const isSelected = allCheckboxes[0].checked;
allCheckboxes.forEach(checkbox => {
checkbox.checked = !isSelected;
});
allSelectBtn.textContent = isSelected ? '전체 선택' : '전체 해제';
}
※ 수정 후
const selectAllPlayer = '전체 선택';
const deleteAllPlayer = '전체 해제';
const allSelectedPlayers = ()=>{
const allCheckboxes = document.querySelectorAll('.checkbox__container input[type="checkbox"]');
const isSelected = allCheckboxes[0].checked;
allCheckboxes.forEach(checkbox => {
checkbox.checked = !isSelected;
});
allSelectBtn.textContent = isSelected ? selectAllPlayer : deleteAllPlayer;
}
4) window 함수 생성 및 변수명 재정의
: window.location.href 코드를 따로 함수로 만들어서 다른 페이지로 넘어갈 때 page 변수를 통해 넘어가게 해주었고, 나도 모르게 input 요소를 Btn이라고 이름을 정의해서 이를 수정하고 input 값과 같은 단일데이터에는 querySelector보단 getElementById를 추천해주셔서 이 부분 또한 수정했다.
※ 기존 코드
const lookBtn = document.querySelector('.transfer__btn');
const nameBtn = document.querySelector('.name__btn');
const nationBtn = document.querySelector('.nation__btn');
const ageBtn = document.querySelector('.age__btn');
const positionBtn = document.querySelector('.position__btn');
const imgBtn = document.querySelector('.img__btn');
const uploadBtn = document.querySelector('.upload__btn');
const moveToPlayerUpload = () =>{
addBtn.addEventListener("click", ()=>{
window.location.href="playerUpload.html";
});
}
※ 수정 후
const lookBtn = document.querySelector('.transfer__btn');
const nameInput = document.getElementById('name__input');
const nationInput = document.getElementById('nation__input');
const ageInput = document.getElementById('age__input');
const positionInput = document.getElementById('position__input');
const imgInput = document.getElementById('img__input');
const uploadBtn = document.querySelector('.upload__btn');
function moveToExternalPage(page) {
window.location.href = page;
}
const createPlayer = (player)=>{
const row = document.createElement("tr");
row.classList.add("player__cell");
row.setAttribute('data-id', player.id);
row.innerHTML = `
<td>
<div class="player__info">
<div class="checkbox__container">
<input type="checkbox">
</div>
<img class="player__image" src="${player.image}" alt="${player.image}">
<span class="player__name">${player.Name}</span>
</div>
</td>
<td>${player.Nation}</td>
<td>${player.Age}</td>
<td>${player.position}</td>
`;
const playerImage = row.querySelector(".player__image");
playerImage.addEventListener('click',(event)=>{
event.stopPropagation();
localStorage.setItem('selectedPlayer', JSON.stringify(player));
moveToExternalPage('playerInfo.html')// window.location.href= "playerInfo.html";
})
addPlayer.appendChild(row);
};
5) CSS 분리 및 동일 로직 개선
: styles.css에 모아둔 것을 playerInfo.css , playerList.css 등으로 구분해주었고 modal 창 부분에서 동일 로직 부분을 개선 했다 멘토님이 알려주신 방법으로 조금 수정 했다.
※ 기존 코드
const closeModal = () =>{
openModal.style.display= "none";
document.body.style.overflow = "auto";
}
window.addEventListener('click',(event)=>{
if(event.target===openModal){
openModal.style.display= "none";
document.body.style.overflow = "auto";
console.log('click');
}
})
※ 수정 후
const closeModal = () =>{
openModal.style.display= "none";
document.body.style.overflow = "auto";
}
window.addEventListener('click',(event)=>{
if(event.target===openModal){
closeModal();
}
})
6. 느낀 점
: 두 번의 과제를 거치고 나니 어느 정도 기본이 다져지는 느낌을 받았다. 역시 가장 중요한 건 내가 얼마나 구현을 위한 생각을 하고 또 얼만큼의 정성을 쏟아 코드를 치는 지에 따라 결과물이 다르다는 점을 배웠다. 그리고 강의를 듣고 실제로 응용해보는 시간이 매우 도움이 된다는 점을 다시 한 번 느꼈다. 다른 스터디원 및 사람들의 구현물을 보면서 자극을 받고 아직 많이 부족함을 느꼈다. 누군가에게 도움이 될 수 있는 그런 실력을 쌓고자 더더욱 노력해야 함을 느꼈다. 그래도 JS에 대해 무지했던 내가 조금의 만족감을 가질 수 있던 뜻 깊은 기회였던 것 같다. 앞으로의 팀 프로젝트 및 지금 개인적으로 하고 있는 프로젝트의 팀원들에게 민폐가 되지 않도록 현실에 안주하지말고 노력해야 함을 느꼈다.
앞서 첫 번째 리팩토링 과제 포스팅 때 적었던 마지막 문구가 생각 났다.
"항상 부족하다 느끼는 이 마음가짐을 원동력으로 삼아 점점 더 발전하는 사람으로 기억되고 싶다. "
아직 잘하는 사람은 아니지만 이 때 적었던 문구처럼 발전해 나가는 것 같아 조금의 만족감을 가지고 이 만족감을 더 원동력으로 삼아 더 노력하는 사람이 될 것이다.
'패스트캠퍼스 프론트엔드 > 과제' 카테고리의 다른 글
패스트캠퍼스X야놀자 프론트엔드 부트캠프 과제 리팩토링(1) (0) | 2023.08.08 |
---|