ETC/Bootcamp

[패스트캠퍼스 X 야놀자] 프론트엔드 부트캠프 Javascript 과제

따봉치치 2023. 8. 28. 16:04
패스트 캠퍼스 X 야놀자 프론트엔드 부트캠프 5-6주차 과제 후기


이번 과제는 그동안 배운 Javascript를 사용해 직원들의 사진을 관리할 수 있는 사진 관리자 서비스를 만드는 것이였다!

firebase 혹은 AWS S3를 사용하여 페이지를 구축해야 하고, 디자인 부터 모두 새롭게 만들어야 했다!

 

본인은 사진을 저장할 storage로 firebase를 사용하였는데 그 이유는

1. 간단한 프로젝트 였기 때문에 사용이 편한 firebase를 사용

2. 예전에 aws를 사용했다가 freetier가 끝난지 모르고 냅뒀다가 과금이 되었던 아픔 경험...

3. 직원들의 정보는 local storage를 사용하여 관리할 것이기 때문

 

이러한 이유들로 오직 '이미지' 저장만을 위한 목적이였기 때문에 간단하게 firebase storage를 사용하였다

 

 

1. Firebase 시작하기

파이어 베이스는 다음과 같이 프로젝트 추가를 누르면  사용할 수 있다.

간단한 정보들만 입력해주면 된다!

 

오른쪽 네비바에서 프로젝트 개요를 들어가면 

다음과 같이 프로젝트를 연결할 수 있는 config를 제공해준다 이를 javscript를 module로 설정해주고 해당 파일에 넣어주면 된다!

 

2. 프로젝트 구현

 

시작 화면

처음 시작화면으로는 로고를 활용한 간단한 애니메이션을 구현해보았다!

버튼에도 hover 시에 변동을 줄 수 있게 해주었다.

 

-html 코드

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>COWORKERS</title>
    <link rel="stylesheet" href="styles/splash.css">
    <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital@1&family=Montserrat:wght@600&family=Pacifico&family=Roboto&family=Roboto+Condensed&family=Ubuntu:ital,wght@0,300;1,500&display=swap" rel="stylesheet">    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
</head>
<body>
    <h1>
        <span>C</span>
        <span>o</span>
        <span>w</span>
        <span>o</span>
        <span>r</span>
        <span>k</span>
        <span>e</span>
        <span>r</span>
      </h1>

      <button id="start_btn">START</button>
      <script src="js/splash.js"></script>
    
</body>
</html>

-CSS코드

html, body {
    width: 100%;  
    height: 100%;
    -webkit-font-smoothing: antialiased;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    position: relative;
}

h1 {
    height: 100px;
}

h1 span {
    padding-top: 20px;
    position: relative;
    top: 20px;
    display: inline-block;
    animation: bounce .3s ease infinite alternate;
    font-family: 'Pacifico', cursive;
    font-size: 80px;
    color: #4D76C1;
    text-shadow: 0 1px 0 #CCC,
                0 2px 0 #CCC,
                0 3px 0 #CCC,
                0 4px 0 #CCC,
                0 5px 0 #CCC,
                0 6px 0 transparent,
                0 7px 0 transparent,
                0 8px 0 transparent,
                0 9px 0 transparent,
                0 10px 10px rgba(0, 0, 0, .4);
}

h1 span:nth-child(2) { animation-delay: .1s; }
h1 span:nth-child(3) { animation-delay: .2s; }
h1 span:nth-child(4) { animation-delay: .3s; }
h1 span:nth-child(5) { animation-delay: .4s; }
h1 span:nth-child(6) { animation-delay: .5s; }
h1 span:nth-child(7) { animation-delay: .6s; }
h1 span:nth-child(8) { animation-delay: .7s; }

@keyframes bounce {
    100% {
    top: -20px;
    text-shadow: 0 1px 0 #CCC,
                    0 2px 0 #CCC,
                    0 3px 0 #CCC,
                    0 4px 0 #CCC,
                    0 5px 0 #CCC,
                    0 6px 0 #CCC,
                    0 7px 0 #CCC,
                    0 8px 0 #CCC,
                    0 9px 0 #CCC,
                    0 50px 25px rgba(0, 0, 0, .2);
    }
}
#start_btn {
    min-width: 150px;
    max-width: 350px;
    display: block;
    padding: .7em 8em;
    border: none;
    background: none;
    color: inherit;
    position: relative;
    font-family: 'Roboto Condensed', sans-serif;
    font-weight: 700;
    font-size: 1em;
    letter-spacing: 2px;
    margin-top: 200px;
    -moz-osx-font-smoothing: grayscale;
    border-radius: 50px;
    background: #4D76C1;
    color: #fff;
    padding: .5em .1em;
    -webkit-transition: background-color 0.3s, color 0.3s;
    transition: background-color 0.3s, color 0.3s;
}

#start_btn::before {
    content: '';
    position: absolute;
    top: -20px;
    left: -20px;
    bottom: -20px;
    right: -20px;
    background: inherit;
    border-radius: 50px;
    z-index: -1;
    opacity: 0.4;
    -webkit-transform: scale3d(0.8, 0.5, 1);
    transform: scale3d(0.8, 0.5, 1);
}
#start_btn:hover {
    -webkit-transition: background-color 0.1s 0.3s, color 0.1s 0.3s;
    transition: background-color 0.1s 0.3s, color 0.1s 0.3s;
    color: #ECEFF1;
    background-color: #3f51b5;
    -webkit-animation: anim-moema-1 0.3s forwards;
    animation: anim-moema-1 0.3s forwards;
}

#start_btn:hover::before {
    -webkit-animation: anim-moema-2 0.3s 0.3s forwards;
    animation: anim-moema-2 0.3s 0.3s forwards;
}

@-webkit-keyframes anim-moema-1 {
  60% {
    -webkit-transform: scale3d(0.8, 0.8, 1);
    transform: scale3d(0.8, 0.8, 1);
  }
  85% {
    -webkit-transform: scale3d(1.1, 1.1, 1);
    transform: scale3d(1.1, 1.1, 1);
  }
  100% {
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
  }
}
@-webkit-keyframes anim-moema-2 {
  to {
    opacity: 0;
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
  }
}

 

 

 

메인 화면

메인에서는 깔끔하게 디자인을 하고싶었고

이를 위해 list를 grid를 사용하여 구현하였다! 이에 대해서 적응형을 하면서 문제가 많았다..

상단에는 리스트 삭제 버튼과 전체 선택버튼을 구현하였고 옆에 search 바도 같이 구현하였다

 

 

 

직원 추가 페이지 & 직원 수정 페이지

 

그리드 가장 마지막에 위치한 추가 버튼을 누르면 새롭게 직원을 등록할 수 있는 페이지로 넘어간다!

select Image 버튼을 클릭하면 실제 파일에서 사진을 가져와 등록할 수 있다

이때 제출 버튼을 누르면 firebase에 저장하고 저장된 사진의 url을 받아와 직원들의 정보를 local storage에 저장할 수 있도록 구현하였다

 

동일한 페이지를 재사용하여 직원의 정보를 수정할 수 있게 하였다!

또한 현재는 API가 연결되어 있지는 않지만 제출 후 loading 애니메이션도 보일 수 있게 해주었다

 

-Javascript 코드

function uploadImg() {
    const file = input.files[0];
    if (file) {
        const reader = new FileReader();
        reader.onload = (e) => {
            profileImg.src = e.target.result;
            profileImg.style.display = 'block';
            profileImg.style.width = "200px";
            profileImg.style.height = "220px";
        };
        reader.readAsDataURL(file);
    }
}

해당 코드는 선택된 이미지 파일을 면에 뿌리는 코드이다

 

class Info {
    constructor(isChecked, profileImgUrl, name, email, phoneNumber, isActive) {
        this.isChecked = isChecked;
        this.profileImgUrl = profileImgUrl;
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.isActive = isActive;
    }
}

유저의 정보는 class 만들어 관리하기 쉽게 하였다!

 

function submitBtnClick() {
    let profile = profileImg.src;
    let name = nameInput.value;
    let email = emailInput.value;
    let phoneNumber = phoneNumberInput.value;

   

    if (name && email && phoneNumber) {
        if (selectedItem) {
            
            // Case 1: selectedItem가 있고 사진도 변경되지 않은 경우
            if (!input.files[0]) {
                console.log("1");
                // 기존 사진 URL 유지
                profile = selectedItem.profileImgUrl;
                updateInfo(profile, name, email, phoneNumber);
                console.log(infoList);
                localStorage.setItem('infoList', JSON.stringify(infoList));
                history.back();
                console.log("success!");
            } else {
                console.log("2");
                // 기존 사진 대신 새로운 사진 업로드
                const file = input.files[0];
                const reader = new FileReader();
                reader.onload = (e) => {
                    const imageBase64 = e.target.result;
                    const storageRef = ref(storage, 'profile_images');
                    const fileExtension = file.type.split('/').pop();
                    const filename = Date.now().toString() + '.' + fileExtension;
                    const imageRef = ref(storageRef, 'profile_images/' + filename);
        
                    uploadString(imageRef, imageBase64, 'data_url').then(snapshot => {
                        getDownloadURL(imageRef).then(url => {
                            profile = url;
                            updateInfo(profile, name, email, phoneNumber);
                            console.log(infoList);
                            localStorage.setItem('infoList', JSON.stringify(infoList));
                            history.back();
                            console.log("success!");
                        }).catch(error => {
                            console.error('Error getting download URL: ', error);
                        });
                    }).catch(error => {
                        console.error('Error uploading image: ', error);
                    });
                };
                reader.readAsDataURL(file);
            }
        } else {
            // Case 3: selectedItem이 없고 사진이 선택된 경우
            if (input.files[0]) {
                console.log("3");
                const file = input.files[0];
                const reader = new FileReader();
                reader.onload = (e) => {
                    const imageBase64 = e.target.result;
                    const storageRef = ref(storage, 'profile_images');
                    const fileExtension = file.type.split('/').pop();
                    const filename = Date.now().toString() + '.' + fileExtension;
                    const imageRef = ref(storageRef, 'profile_images/' + filename);

                    uploadString(imageRef, imageBase64, 'data_url').then(snapshot => {
                        getDownloadURL(imageRef).then(url => {
                            profile = url;
                            addNewInfo(profile, name, email, phoneNumber);
                            console.log(infoList);
                            localStorage.setItem('infoList', JSON.stringify(infoList));
                            history.back();
                            console.log("success!");
                        }).catch(error => {
                            console.error('Error getting download URL: ', error);
                        });
                    }).catch(error => {
                        console.error('Error uploading image: ', error);
                    });
                };
                reader.readAsDataURL(file);
            } else {
                // Case 4: selectedItem이 없고 사진도 선택되지 않은 경우
                console.log("4");
                profile = " "; // " " 값 대체
                addNewInfo(profile, name, email, phoneNumber);
                console.log(infoList);
                localStorage.setItem('infoList', JSON.stringify(infoList));
                history.back();
                console.log("success!");
            }
        }
    } else {
        alert("값을 입력해주세요");
    }
}

유저 정보가 생성 & 수정되는 함수이다

이때, 4가지 경우가 있다

1. 이미 있는 유저의 정보를 수정 + 이미지또한 수정

2. 이미 있는 유저의 정보를 수정 + 이미지 수정 X

3. 새롭게 유저의 정보를 등록

4. 새롭게 유저의 정보를 등록 + 이미지 등록

 

 

 

직원 정보 검색

상단 오른쪽의 input 태그를 사용하여

사용자의 이름을 검색하면 해당 사용자를 보여주게 하였다

 

-Javascript 코드

searchBtn.addEventListener("click", () => {
    const searchText = searchInput.value.toLowerCase().trim(); // 검색어를 소문자로 변환하여 공백 제거

    // 검색어가 비어있으면 모든 아이템 표시
    if (!searchText) {
        showAllItems();
        return;
    }

    // 검색어를 포함하는 아이템 필터링
    filteredItems(searchText);

    searchInput.value = "";
});

function showAllItems() {
    const itemEls = list.querySelectorAll(".list_item");
    itemEls.forEach(itemEl => {
        itemEl.style.display = "block"; // 모든 아이템 표시
    });
}

function filteredItems(searchText) {
    const itemEls = list.querySelectorAll(".list_item");
    itemEls.forEach(itemEl => {
        const nameEl = itemEl.querySelector(".name");
        const name = nameEl.textContent.toLowerCase();
        if (name.includes(searchText)) {
            itemEl.style.display = "block"; // 검색어가 포함되면 보이기
        } else {
            itemEl.style.display = "none"; // 검색어가 포함되지 않으면 숨기기
        }
    });
}

 

 

 

직원 전체 선택

전체 선택을 누르면 모든 유저들의 checkbox를 check상태로 변경 해 주 었다

 

-Javascript 코드

const totalcheckBox = document.querySelector(".ex_select_all_btn");

totalcheckBox.addEventListener("click", () => {
    const checkBoxes = document.querySelectorAll("#check_btn");
    
    checkBoxes.forEach(checkbox => {
        checkbox.checked = !checkbox.checked; // Toggle the checked state
    });

    // Toggle the class name
    totalcheckBox.classList.toggle("ex_selected_all_btn_clicked");
});
;

 

 

 

 

직원 삭제

선택된 직원들을 delete 버튼을 누르면 삭제할 수 있게 구현하였다

여러개의 직원들의 정보를 동시에 삭제할 수 있고

삭제 버튼을 누르면 모달을 띄어 잘못 삭제하는 것을 방지하였다!

 

-Javascript 코드

<dialog id="dialog">
        <span>Do You Want Delete?</span>
        <div>
            <button id="yesBtn"value="confirm">YES</button>
            <button id="noBtn" value="cancle">NO</button>
        </div>

    </dialog>
deleteBtn.addEventListener("click", () => {
    dialog.showModal();
 
});
yesBtn.addEventListener("click",(e)=> {
    dialog.close();
    checkboxItemdelete();
})
noBtn.addEventListener("click",(e)=> {
    dialog.close();
})

모달을 생성해주고 Yes를 누르면 모달이 닫히고 No를 누르면 삭제를 진행하고 모달이 닫힌다.

 

function checkboxItemdelete() {
    const checkBoxes = document.querySelectorAll("#check_btn");
    const itemsToDelete = [];

    checkBoxes.forEach((checkbox, index) => {
        if (checkbox.checked) {
            itemsToDelete.push(infos.length - index - 1);
        }
    });

    
    itemsToDelete.forEach(index => {
        deleteItem(index);
    });

    // 체크박스 초기화
    totalcheckBox.checked = false;
}

function deleteItem(index) {
    if (index >= 0 && index < infos.length) {
        const imagePath = infos[index].profileImgUrl;
        
        // 이미지를 먼저 Firebase Storage에서 삭제
        deleteImageFromStorage(imagePath);
        infos.splice(index, 1);
        updateLocalStorage();
        updateList();
    }
}

function deleteImageFromStorage(filePath) {
    const storageRef = ref(storage, filePath);
    
    deleteObject(storageRef)
        .then(() => {
            console.log("이미지가 성공적으로 삭제되었습니다.");
        })
        .catch((error) => {
            console.error("이미지 삭제 중 오류 발생:", error);
        });
}

또한 firebase에 저장된 이미지 또한 삭제해주었다!

 

3. 트러블 슈팅

(1).

🙅🏻 이슈  : script 파일에서 firebase 권한 오류 발생!

 

➡️ 해결 방법  : Storage -> Rule에서 권한을 read, write를 모두 허용해 주면 된다!

 

 

(2).

🙅🏻 이슈  : 유저 리스트를 grid로 작성하였더니 화면이 작아질 때 아이템의 크기가 제각각으로 변하는 오류 발생

 

➡️ 해결 방법  : flex를 사용해주면 flex : 1 를 사용하여 반응형에서 사용이 용이하다. 근데 내가 원했던 디자인은 아니였어서 그냥 grid 아이템들의 크기를 지정해주는 방식으로 타협하였다.. 왜 repeat(3,1fr)을 사용하였는데 크기가 제각각으로 변하는지 확실히 모르겠어서 조금 더 공부를 해봐야 할것 같다

 

 

4. 느낀점

 

처음부터 끝까지 내가 혼자하는 프로젝트는 조금 오랜만이였는데 오히려 조금 힘든 부분이 있었다..!

사실 프로젝트의 시간이 디자인에 8할이 할애되었다고 해도 과언이 아니다..

정말 나름대로 font 도 여러개 바꾸고 색상도 정말 여러가지로 맞춰보고

또 몇번이고 디자인을 갈아엎었으나!!.... 디자인은 정말 쉽지 않다는 것을 느꼈다.

그래도 이번에 Javascript랑 많이 친해진 느낌이다. 여러가지 애니메이션도 써보고 local storage랑 firebase를 사용해 볼 수 있어서 한 층 성장힌 기분이다^~^