패스트캠퍼스X야놀자 프론트엔드 개발 부트캠프_2차 과제 리팩토링

양재혁(Jaehyuk-Yang)·2023년 8월 28일

프로젝트 소개

안녕하세요.

아이돌 멤버 관리 서비스 웹 페이지 입니다.

배포 주소

https://fastcampusxyanolja-assginment.web.app/


id : user
password : 1234


개발 기간

2023.08.08 ~ 2023.08.16

기술 스택

Enviroment & FrontEnd

Development & FrontEnd


구현 내용

필수 요구사항


  • AWS S3 / Firebase 같은 서비스”를 이용하여 사진을 관리할 수 있는 페이지를 구현하세요.
  • 프로필 페이지를 개발하세요.
  • 스크롤이 가능한 형태의 리스팅 페이지를 개발하세요.
  • 전체 페이지 데스크탑-모바일 반응형 페이지를 개발하세요.
  • 사진을 등록, 수정, 삭제가 가능해야 합니다.
  • 유저 플로우를 제작하여 리드미에 추가하세요.
  • CSS
    • 애니메이션 구현
    • 상대수치 사용(rem, em)
  • JavaScript
    • DOM event 조작

선택 요구사항

  • 사진 관리 페이지와 관련된 기타 기능도 고려해 보세요.
  • 페이지가 보여지기 전에 로딩 애니메이션이 보이도록 만들어보세요.
  • 직원을 등록, 수정, 삭제가 가능하게 해보세요.

화면 구성

초기 화면

초기화면

로그인

로그인

정보 등록

정보 등록

프로필

프로필

정보 변경

정보 변경

정보 삭제

삭제


주요 기능

로그인 게스트 / 매니저 권한 분리
gif-main-page gif-main-page
기본 타이틀 조작 방지 정보 등록
gif-main-page gif-main-page
정보 변경 정보 삭제
gif-main-page gif-main-page
모바일 반응형
gif-main-page

User Flow

제목 없는 다이어그램

리팩토링 전 주요 코드

로그인

const loginform = document.getElementById('login-form');
const loginbutton = document.getElementById('idbutton');
const loading = document.querySelector('.spin-container');
var nonvisible = getComputedStyle(loading).display;
var visible = "flex";
loading.style.display=nonvisible;
var down = getComputedStyle(loading).zIndex;
var up = 2;
loading.style.zIndex=down;
loginbutton.addEventListener('click', function (e) {
  e.preventDefault();
  const id = loginform.id.value;
  const password = loginform.password.value;
  if (id === "user" && password === "1234") {
    sessionStorage.setItem("nums",1);
    loading.style.display = visible;
    loading.style.zIndex = up;
    setTimeout(() => {
      loading.style.display = "none";
      loading.style.zIndex = 0;
      window.location.href = "index.html";
    }, 1000);
  } else {
    e.preventDefault();
    login();
  }
});

function login(){
  Swal.fire({
    title: '로그인 오류',
    text: "아이디 혹은 비밀번호를 다시 입력하세요.",
    icon: 'warning',
    confirmButtonColor: '#3085d6',
    confirmButtonText: '확인',
  })
}
  • getComputedStyle()를 활용하여 스타일을 가져옴

  • loginbutton.addEventListener 에서 로그인 버튼 클릭 시, e.preventDefault(); 고유 동작을 중단하고, 로그인 시 사용한 id와 password를 id와 password 변수로 받음

  • id값이 user, password 값이 각각 user, 1234인 경우 loading.style.display = visible;, loading.style.zIndex = up;을 선언하여 만들어둔 로딩 애니메이션이 보이도록 설정

  • sessionStorage.setItem("nums",1); 을 통해 SessionStorage에 키 값은 nums이고 해당하는 value값을 1로 설정

    • 추후 게스트와 매니저를 구분하기 위해 사용
  • setTimeout(() => {
          loading.style.display = "none";
          loading.style.zIndex = 0;
          window.location.href = "index.html";
        }, 1000);

    1초 후, 로딩 애니메이션의 displaynone으로 바꿔주고 index.html로 이동하도록 설정

  • id 혹은 password가 다른 경우 login(); 실행

  • login()은 외부 라이브러리인 SweetAlert 사용

    • swal의 기본 구성은 swal("제목", "내용", "아이콘")

게스트와 매니저 구분 로직

let nums=sessionStorage.getItem("nums");
if(nums!==null) {
  checkuser.innerHTML="매니저";
  checklogin.innerHTML="로그아웃";
}
else{
  checkuser.innerHTML="게스트";
  checklogin.innerHTML="로그인";
}
export const innerHTML = checklogin.innerHTML;
  • SessionStorage 내부의 nums키의 value값을 nums로 받아옴

  • nums가 null값이면 게스트, null값이 아니면 매니저로 설정

checklogin.addEventListener('click',()=>{
  if(checklogin.innerHTML==="로그인"){
    loading.style.display = visible;
    loading.style.zIndex = up;
    setTimeout(() => {
      loading.style.display = "none";
      loading.style.zIndex = 0;
    }, 1000);
    setTimeout(() => {
      hreflink();
    }, 2000);
  }
  else{
    loading.style.display = visible;
    loading.style.zIndex = up;
    setTimeout(() => {
      loading.style.display = "none";
      loading.style.zIndex = 0;
      checkuser.innerHTML="게스트";
      checklogin.innerHTML="로그인";
    }, 1000);
    setTimeout(() => {
      loading.style.display = visible;
      loading.style.zIndex = up;
    }, 2000);
    setTimeout(() => {
      hreflink();
    }, 3000);
    sessionStorage.clear();
  }
  })

export const innerHTML = checklogin.innerHTML;
  • 버튼에 해당하는 innerHTML값이 로그인일 때 클릭 시, 로딩 애니메이션을 띄우고 2초 뒤, login.html로 이동

  • 버튼에 해당하는 innerHTML값이 로그아웃일 때 클릭 시, 로딩 애니메이션을 띄우면서 게스트, 로그인으로 바꿈

  • 보안을 위해 자동으로 로그인 화면으로 이동

  • 게스트이므로 SessionStorage를 비워줌

import { innerHTML } from "./header.js";
if(innerHTML==="로그아웃"){
  insertbutton.addEventListener('click',()=>{
    // 모달창 띄우기
    modalOn();

  })
  deletebutton.addEventListener('click',()=>{
    deleteBoard();
  })

}
else{
  insertbutton.addEventListener('click',()=>{
    login();
  })
  deletebutton.addEventListener('click',()=>{
    login();
  })

}


export function login(){
  Swal.fire({
    title: '로그인 오류',
    text: "로그인 후 사용 가능합니다.",
    icon: 'warning',
    confirmButtonColor: '#3085d6',
    confirmButtonText: '확인',
  })
}
  • header.js의 checklogin.innerHTML를 불러옴

  • innerHTML값이 로그아웃 즉, 매니저인 경우에만 정보 등록, 정보 삭제 가능

  • 반대인 경우, swal의 login() 실행


정보 등록

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
  • 먼저 firebase를 초기화
if (image && name && group) {
    const storage = getStorage();
    const storageRef = ref(storage, 'images');

    const listResult = await listAll(storageRef);
    for (const item of listResult.items) {
      const fileName = item.name;
      if (fileName === image.name) {
        cantupload();
        clearInputValues();
        setTimeout(() => {
          location.reload();
        }, 1000);
        return;
      }
    }
  • 이미지 선택, 이름 입력, 그룹 입력 시 listAll()를 사용해 FireStorage image의 모든 파일과 하위 파일을 검색

  • Firebase 내 동일한 이미지를 등록하려고 하면 2개의 swal을 띄우고 return

const storage = getStorage();
const storageRef = ref(storage, 'images/' + image.name);
const uploadTask = uploadBytes(storageRef, image);
await uploadTask;
const downloadURL = await getDownloadURL(storageRef);
const imagesCollection = collection(db, 'images');
await addDoc(imagesCollection, {
          id: id,
          name: name,
          group: group,
          imageUrl: downloadURL
});
modalOff();
  • images 폴더에는 사진을 업로드

  • db에는 id와 이름, 그룹, imageurl을 업로드

  • 전부 다 등록 시 모달창 종료

정보 삭제

const imageUrl = completeditems.querySelector('.image img').src;
const imagesCollection = collection(db, 'images');
const querySnapshot = await getDocs(imagesCollection);
querySnapshot.forEach(async (doc) => {
    const docData = doc.data();
    if (docData.imageUrl === imageUrl) {
      await deleteDoc(doc.ref);
    }
  });
  • db내 imageurl과 체크된 src값이 같다면 deleteDoc를 이용해 삭제

프로필 페이지 이동

// 정보를 포함한 쿼리 문자열 생성
        const queryParams = new URLSearchParams({
          id: id,
          image: image,
          name: name,
          group: group
        });

        // 쿼리 매개변수를 다음 페이지 URL에 추가
        const nextPageUrl = `profile.html?${queryParams}`;
        window.location.href = nextPageUrl;
      } 
      else {
        const queryParams = new URLSearchParams({
          id: 'example',
          image: clickedImage.src,
          name: clickedName.textContent,
          group: clickedGroup.textContent,
        });

        const nextPageUrl = `profile.html?${queryParams}`;
        window.location.href = nextPageUrl;
      }
    }
  • 쿼리 문자열을 통해 프로필 페이지로 이동

  • example의 경우 미리 설정한 default 프로필들에만 해당


정보 수정

const queryid = getQueryParam('id');
const queryimage = getQueryParam('image');
const queryname = getQueryParam('name');
const querygroup = getQueryParam('group');
const listitems = await listAll(storageRef);
const numericId = parseInt(queryid, 10);
const idValues = profiles.map(item => item.id);
    // 리스트 내 중복 사진은 수정 불가
    for(const item of listitems.items){
      const fileName = item.name;
      if(fileName == image.name){
        cantupload();
        clearInputValues();
        setTimeout(() => {
          location.reload();
        }, 1000);
        return;
      }
    }
    // Firestorage
      if (idValues.includes(numericId)) {
        const matchingProfile = profiles.find(profile => profile.id === numericId);
    
        if (matchingProfile) {
          const storage = getStorage();
    
          // Firestorage 내의 기존 이미지 삭제
          const matchingImage = matchingProfile.image;
          const matchingId = matchingProfile.id;
          await deleteMatchingImageFromFirestorage(matchingImage);
    
          // 새로운 이미지 Firestorage에 업로드
          const newStorageRef = ref(storage, 'images/' + image.name);
          const uploadTask = uploadBytes(newStorageRef, image);
          await uploadTask;
          const newDownloadURL = await getDownloadURL(newStorageRef);
    
          // Firestore 데이터 업데이트
          await updateItemFields(matchingId, groupInput.value, nameInput.value, newDownloadURL);
    
          // URL 파라미터 업데이트
          const queryParams = new URLSearchParams({
            id: queryid,
            image: newDownloadURL,
            name: nameInput.value,
            group: groupInput.value
          });
          setTimeout(() => {
            const newUrl = `${window.location.pathname}?${queryParams.toString()}`;
            window.history.pushState(null, '', newUrl);
            location.reload();
          }, 1500);
        }
      }
  }
  else{
    uploadError();
    modalOff();
  }
}



imagecontainer.src = queryimage;
namecontainer.innerHTML = queryname;
groupcontainer.innerHTML = querygroup;
  • 리스트 내 중복 사진은 수정 불가

  • 수정을 위해 삭제하고 등록하는 형식 사용

  • 수정 시 url 파라미터를 업로드 하므로 수정 후 , window.history.pushStatelocation.reload() 사용


리팩토링 후 코드

const checklogin=document.querySelector('.checklogin');
const checkuser=document.querySelector('.checkuser');
let nums=sessionStorage.getItem("nums");
const link='login.html';
const loading = document.querySelector('.spin-container');
const nonvisible = getComputedStyle(document.querySelector('.spin-container')).display;
const down = getComputedStyle(document.querySelector('.spin-container')).zIndex;
const visible = "flex";
loading.style.display=nonvisible;
const up = 2;
loading.style.zIndex=down;
  • 재할당 되는 내용이 없으므로 const로 변경
class changeLoading{
  constructor(loadingEl) {
    this.loadingEl = loadingEl
  }

  changeDisplay(display, zIndex) {
    this.loadingEl.style.display = display;
    this.loadingEl.style.zIndex = zIndex;
  }

}

checklogin.addEventListener('click',()=>{
  let change = new changeLoading(loading)
  if(checklogin.innerHTML==="로그인"){
    change.changeDisplay(visible, up)
    setTimeout(() => {
      change.changeDisplay("none",0)
    }, 1000);
    setTimeout(() => {
      hreflink();
    }, 2000);
  }
  else{
    change.changeDisplay(visible, up)
    setTimeout(() => {
      change.changeDisplay("none",0)
      checkuser.innerHTML="게스트";
      checklogin.innerHTML="로그인";
    }, 1000);
    setTimeout(() => {
      change.changeDisplay(visible, up)
    }, 2000);
    setTimeout(() => {
      hreflink();
    }, 3000);
    sessionStorage.clear();
  }
  })

export const innerHTML = checklogin.innerHTML;
  • 클래스로 로그인/로그아웃 로딩 구현

추가 코드 ( 검색 기능 )

const search = document.querySelector('.material-symbols-outlined');
const searchbox = document.querySelector('.searchbox')
var check = false;
search.addEventListener('click', ()=>{
  
  const profileList = document.querySelector('.profile-list')
  const names = profileList.querySelectorAll('.item .name')
  const allItems = profileList.querySelectorAll('.item');
    allItems.forEach(item => {
      item.style.display = 'flex';
    });
  names.forEach(name => {
    const item = name.closest('.item');
    if(name.innerHTML === searchbox.value){
      check=true
    }
    else{
      item.style.display='none';
    }
  })
  if (check === false) {
    const allItems = profileList.querySelectorAll('.item');
    allItems.forEach(item => {
      item.style.display = 'flex';
    });
    cantsearch();
  }

  check=false
})


function cantsearch(){
  Swal.fire({
    title: '결과 없음',
    text: "검색된 결과가 없습니다.",
    icon: 'warning',
    confirmButtonColor: '#3085d6',
    confirmButtonText: '확인',
  })
}
  • check의 값은 T/F 방식으로 사용

  • 검색한 값과 일치하면 해당하는 값을 가진 item만 보이고 나머지는 display: 'none'처리

  • 검색한 값이 없다면 alert 띄우고 모든 item들이 보이도록 설정


느낀점

firebase를 처음으로 사용해 보았는 데 난이도가 꽤 있었고, 휴가로 인해 하루 빨리 제출해야했기에 다른 공부를 못하고 이것만 몰두해 진행했습니다. 결과적으로 데이터 등록, 삭제, 수정 부분이 완벽하게 구현되어 있어 뿌듯했지만, 디자인 적 측면과 검색 기능 구현이 없어 아쉽습니다. 이번 프로젝트를 통해 firebase를 다룰 수 있게 된 점과 js스킬이 향상된 것 같아 의미있었습니다 !

profile
Frontend developer

0개의 댓글