FastCampas X Yanolja BootCamp

두 번째 과제 - JS를 이용한 사진 관리 웹사이트 만들기

취업하고싶다! 2023. 8. 29. 00:07

이번 과제로는 JS를 이용한 사진 관리 웹사이트를 만들어보았다.

먼저 기본 화면을 봐보도록 하자. 

로그인 화면

 

관리 시스템 화면

 

학생 정보 등록 화면

 

정보 입력 화면

 

정보 등록 완료 시 화면

 

학생 프로필 및 수정 화면

 

학생 정보 수정 화면

 

수정된 학생 정보 화면

 

선택 박스 체크 화면

 

선택한 객체 삭제 화면

 

학생 정보 부분 리스트(스크롤) 페이지

 

CSS를 이용한 애니매이션 화면

 

반응형 화면

[흐름]

 

다음과 같이 구성했다.

과제 기간은 약 10일로 지난 과제 기간보다 길었지만 생각없이 신청한 공모전, 교내 경진대회 등으로 시간을 다 날려먹어서 과제 할 시간이 약 4일정도밖에 없어서 요구한 필수 기능들만 구현을 했다.

 

파이어베이스의 파이어스토어 데이터베이스를 사용했고 웹페이지에서 등록하면 해당 객체 정보가 파이어베이스에 정상적으로 잘 등록된다.

 

하지만 해결 못한 문제가 존재했다.

먼저, 객체 수정을 하면 수정이 잘 되나 싶었으나... 아니었다.

 

다음과 같이 객체가 2개 있을 때 1번 객체(김아무개)를 수정해보자.

 

다음과 같이 이름을 '김아무개요'로 바꿨을 때 

이렇게 수정이 잘 됨을 알 수 있다. 그래서 수정 기능이 잘 동작하는 줄 알았는데...!

 

다음과 같이 두 번째 객체를 수정(이름을 '김승문이에요'로)하면

 

이렇게 다른 객체의 정보가 전부 수정된다...

즉, 내가 객체1을 수정하고 객체2를 수정하면, 객체1의 모든 정보가 객체2의 모든 정보로 바뀌는 것이다.

 

하나의 객체만 수정하면 전혀 문제가 없으나 연속해서 여러 번 다른 객체의 정보를 수정하면 바로 그 전에 수정한 객체의 정보로 전부 바뀌는 것이다. 

 

기존 코드를 보자.

// 정보 엘리먼트 생성
function createDogInfoElement(dogData) {

  const dogInfo = document.createElement("div");
  dogInfo.className = "dog-info";

  dogInfo.addEventListener("click", () => {
    openEditDialog(dogData); // 수정 다이얼로그 열기
  });

  // 기존의 엘리먼트가 있는 경우 해당 엘리먼트를 반환
  if (dogData.element) {
    return dogData.element;
  }

   // 체크박스 생성 및 추가
   const checkbox = document.createElement("input");
   checkbox.type = "checkbox";
   checkbox.className = "dog-checkbox";

   if (dogData.isChecked) {
     checkbox.checked = true;
   }
   dogInfo.appendChild(checkbox);
   checkbox.addEventListener("change", () => {
    dogData.isChecked = checkbox.checked; // 체크박스 상태를 데이터와 동기화
  });
  dogInfo.appendChild(checkbox);
  dogCheckboxes.push({ checkbox, dogData }); // 체크박스와 데이터를 연결하여 추적

  // 이미지 컨테이너 생성
  const imageContainer = document.createElement("div");
  imageContainer.className = "image-container";


  // 이미지
  const dogImage = document.createElement("img");
  dogImage.src = dogData.imageUrl; // 이미지 URL을 가져와서 설정
  dogImage.alt = "강아지 사진";
  dogImage.style.borderRadius = "50%";
  imageContainer.appendChild(dogImage);
  dogInfo.appendChild(dogImage);

  // 이름
  const dogName = document.createElement("span");
  dogName.textContent = dogData.name;
  dogInfo.appendChild(dogName);

  // 나이
  const dogBreed = document.createElement("span");
  dogBreed.textContent = dogData.breed;
  dogInfo.appendChild(dogBreed);

  // 생년월일
  const dogBirthday = document.createElement("span");
  dogBirthday.textContent = dogData.birthday;
  dogInfo.appendChild(dogBirthday);

  // 성별
  const dogGender = document.createElement("span");
  dogGender.textContent = dogData.gender;
  dogInfo.appendChild(dogGender);
  
  return dogInfo;
}
dialogRegisterDogButton.addEventListener("click", () => {
  //  정보 생성
  const dogInfo = document.createElement("div");
  dogInfo.className = "dog-info";

  //  체크박스 생성
  const checkbox = document.createElement("input");
  checkbox.type = "checkbox";
  checkbox.className = "dog-checkbox";

  //  정보 컨테이너 생성
  const infoContainer = document.createElement("div");
  infoContainer.className = "info-container";

  // 이미지 컨테이너 생성
  const imageContainer = document.createElement("div");
  imageContainer.className = "image-container";

  //  정보 내용 생성
  const dogImage = document.createElement("img");
  dogImage.src = imagePreview.src;
  dogImage.alt = "강아지 사진";
  dogImage.style.borderRadius = "50%";

  const dogName = document.createElement("span");
  dogName.textContent = dogNameInput.value;

  const dogBreedInput = document.getElementById("dogBreedInput");

  const dogBreed = document.createElement("span");
  dogBreed.textContent = dogBreedInput.value;

  const dogBirthdayInput = document.getElementById("dogBirthdayInput");
  const dogBirthday = document.createElement("span");
  dogBirthday.textContent = dogBirthdayInput.value;

  const dogGenderInput = document.getElementById("dogGenderInput");
  const dogGender = document.createElement("span");
  dogGender.textContent = dogGenderInput.value;

  // 생성한 엘리먼트들을 이미지 컨테이너와 정보 컨테이너에 추가
  imageContainer.appendChild(dogImage);
  infoContainer.appendChild(checkbox);
  infoContainer.appendChild(imageContainer);
  infoContainer.appendChild(dogName);
  infoContainer.appendChild(dogBreed);
  infoContainer.appendChild(dogBirthday);
  infoContainer.appendChild(dogGender);

  //  정보 컨테이너를 강아지 정보 엘리먼트에 추가
  dogInfo.appendChild(infoContainer);

  //  정보 엘리먼트를 dogInfoContainer에 추가
  const dogInfoContainer = document.getElementById("dogInfoContainer");
  dogInfoContainer.appendChild(dogInfo);


  const selectedImage = dogImageInput.files[0];

  if (!selectedImage) {
    console.error("이미지가 선택되지 않았습니다.");
    return; // 이미지가 선택되지 않은 경우 함수 종료
  }


   // Firebase의 Cloud Storage에 이미지 업로드
   const storage = getStorage(app); // Firebase Storage 객체 가져오기
   const storageRef = ref(storage, `dog_images/${dogImageInput.files[0].name}`);
   const uploadTask = uploadBytes(storageRef, dogImageInput.files[0]);
 
   console.log(storageRef);

   //  정보 등록 후에 Firestore 데이터 다시 가져와서 표시
  const db = getFirestore(app);
  const dogsCollection = collection(db, "dogs");

  getDocs(dogsCollection).then((querySnapshot) => {
    const dogInfoContainer = document.getElementById("dogInfoContainer");
    dogInfoContainer.innerHTML = ""; // 기존의  정보 엘리먼트 모두 삭제

    querySnapshot.forEach((doc) => {
      const dogData = doc.data();
      dogData.id = doc.id; // Firestore 문서의 ID 값을 dogData 객체에 추가
      const dogInfo = createDogInfoElement(dogData);
      dogInfoContainer.appendChild(dogInfo);
    });
  }).catch((error) => {
    console.error("Firestore 데이터 가져오기 실패:", error);
  });
 
   let imageUrl; // imageUrl 변수를 블록 밖에서 선언

   // 이미지 업로드가 완료된 후, 이미지 URL을 가져오기 위한 작업 수행
   uploadTask
    .then((snapshot) => {
      return getDownloadURL(snapshot.ref).then((url) => {
        imageUrl = url; // 이미지 URL을 가져와서 설정
        const db = getFirestore(app);
        return addDoc(collection(db, "dogs"), {
          name: dogNameInput.value,
          breed: dogBreedInput.value,
          birthday: dogBirthdayInput.value,
          gender: dogGenderInput.value,
          imageUrl: imageUrl,
          isChecked: checkbox.checked
        }).then((docRef) => {
          dogCheckboxes.push({ checkbox, dogData: {
            name: dogNameInput.value,
            breed: dogBreedInput.value,
            birthday: dogBirthdayInput.value,
            gender: dogGenderInput.value,
            imageUrl: imageUrl,
            isChecked: checkbox.checked,
            id: docRef.id
          }});
        });
      });
    })
     .then(() => {
       // 데이터 추가가 완료된 경우
       console.log("강아지 정보가 성공적으로 등록되었습니다.");
 
       // 입력한 정보 초기화
       dogNameInput.value = "";
       dogBreedInput.value = "";
       dogBirthdayInput.value = "";
       dogGenderInput.value = "male";
       imagePreview.src = "#";
       imagePreviewText.style.display = "block";
       modalContainer.style.display = "none";
     })
     .catch((error) => {
       console.error("강아지 정보 등록에 실패했습니다:", error);
     });

    // 체크박스와 데이터를 연결하여 추적
    dogCheckboxes.push({ checkbox, dogData: {
      name: dogNameInput.value,
      breed: dogBreedInput.value,
      birthday: dogBirthdayInput.value,
      gender: dogGenderInput.value,
      imageUrl: imageUrl,
      isChecked: checkbox.checked
    }});
 
  // 다이얼로그 숨기기
  modalContainer.style.display = "none";
});
// 정보 업데이트 및 화면 업데이트
function updateDogInfoOnScreen() {
  const dogInfoContainer = document.getElementById("dogInfoContainer");
  const db = getFirestore(app);
  const dogsCollection = collection(db, "dogs");

  getDocs(dogsCollection)
    .then((querySnapshot) => {
      // 기존 정보를 모두 삭제
      dogInfoContainer.innerHTML = "";
      querySnapshot.forEach((doc) => {
        const dogData = doc.data();
        dogData.id = doc.id; // Firestore 문서의 ID 값을 dogData 객체에 추가
        const dogInfo = createDogInfoElement(dogData);
        dogInfoContainer.appendChild(dogInfo);
      });
    })
    .catch((error) => {
      console.error("Firestore 데이터 가져오기 실패:", error);
    });

정보 등록과 업데이트를 다음과 같이 코드를 짰었는데, 웹이 익숙하지도 않고 파이어베이스도 사용한 지 좀 오래돼서 뭐가 문제인지 정말 모르겠어서 gpt의 힘을 좀 빌려보았다.

(참고로 dogData, dogInfoContainer 이런식으로 이름을 정한 이유는 원래 강아지 등록 시스템을 만들다가 과제 이름 자체가 직원 사진 관리 시스템이길래 바꾸었다... 나중에 시간 있을 때 변수 이름들 다 바꿔야겠다,,,)

 

gpt에게 내 코드에서 뭐가 문제고 어디를 어떻게 고쳐야 할 지 물어봤는데 다음과 같은 답변을 내놓았다.

 

기존의 문제점:

기존 코드에서는 dialogRegisterDogButton 클릭 이벤트 핸들러에서 새로운 강아지 정보 엘리먼트를 생성하고 
Firestore에 업로드한 다음, updateDogInfoOnScreen 함수를 호출하여 화면을 업데이트하고 있었습니다. 
그런데 updateDogInfoOnScreen 함수에서는 기존에 생성한 엘리먼트를 삭제하고 새로운 엘리먼트를 생성하여 
추가하는 방식을 사용하였습니다. 이로 인해 중복된 엘리먼트가 화면에 추가되는 문제가 발생하였습니다.

문제 해결 방법:

객체와 엘리먼트 관리를 위한 Map 생성: dogDataMap이라는 Map을 생성하였습니다. 
이 맵은 강아지 객체의 ID를 키로 하고, 해당 객체에 대응하는 엘리먼트를 값으로 갖습니다.

createDogInfoElement 함수 수정: createDogInfoElement 함수를 수정하여, 
이미 생성한 엘리먼트가 있다면 해당 엘리먼트를 반환하도록 변경하였습니다. 
만약 이미 생성한 엘리먼트가 없다면, 새로운 엘리먼트를 생성하고 dogDataMap에 추가한 후 반환합니다.

dialogRegisterDogButton 클릭 이벤트 핸들러 내부 수정: 클릭 이벤트 핸들러에서는 dogDataMap에 이미 해당
객체의 엘리먼트가 있는지 확인하고, 없으면 새로운 엘리먼트를 생성하여 화면에 추가합니다.

updateDogInfoOnScreen 함수 수정: 화면 업데이트 함수에서는 dogDataMap을 활용하여 이미 생성한 엘리먼트가
있는 경우 해당 엘리먼트의 내용을 수정하고, 없는 경우에만 새로운 엘리먼트를 생성하여 추가합니다.

 

음.. 답변을 읽어보니 기존의 내 코드의 updateDogInfoOnScreen 함수에서 기존에 생성한 엘리먼트를 삭제하고 새로운 엘리먼트를 생성하여 추가하는 방식을 사용해서 중복된 엘리먼트가 화면에 추가되는 문제가 발생했다고 한다.

getDocs(dogsCollection)
    .then((querySnapshot) => {
      // 기존 정보를 모두 삭제
      dogInfoContainer.innerHTML = "";
      querySnapshot.forEach((doc) => {
        const dogData = doc.data();
        dogData.id = doc.id; // Firestore 문서의 ID 값을 dogData 객체에 추가
        const dogInfo = createDogInfoElement(dogData);
        dogInfoContainer.appendChild(dogInfo);
      });
    })

 

이부분에 문제가 있었다는건데, 기존 정보를 모두 삭제하는 코드로 작성해서 내가 객체1을 수정하고 객체2를 수정하면 객체2의 내용이 모두 수정 되었던건가..? 

 

아무튼 답변을 기준으로 다시 코드를 고쳐보았다.

// 객체와 엘리먼트 관리를 위한 맵
const dogDataMap = new Map();

// createDogInfoElement 함수 수정
function createDogInfoElement(dogData) {
  // 이미 생성한 엘리먼트가 있는 경우 해당 엘리먼트를 반환
  if (dogDataMap.has(dogData.id)) {
    return dogDataMap.get(dogData.id);
  }

  // ... (기존 코드 내용)

  // 새로운 엘리먼트를 맵에 추가
  dogDataMap.set(dogData.id, dogInfo);
  return dogInfo;
}

// dialogRegisterDogButton 클릭 이벤트 핸들러 내부 수정
dialogRegisterDogButton.addEventListener("click", () => {
  // ... (기존 코드 내용)

  // 기존에 생성한 엘리먼트가 없으면 새로 생성
  if (!dogDataMap.has(dogData.id)) {
    const dogInfo = createDogInfoElement(dogData);
    dogInfoContainer.appendChild(dogInfo);
  }

  // ... (기존 코드 내용)
});

// updateDogInfoOnScreen 함수 수정
function updateDogInfoOnScreen() {
  const dogInfoContainer = document.getElementById("dogInfoContainer");
  const db = getFirestore(app);
  const dogsCollection = collection(db, "dogs");

  getDocs(dogsCollection)
    .then((querySnapshot) => {
      querySnapshot.forEach((doc) => {
        const dogData = doc.data();
        dogData.id = doc.id;

        // 이미 생성한 엘리먼트가 있는 경우 해당 엘리먼트를 활용하여 업데이트
        if (dogDataMap.has(dogData.id)) {
          const dogInfo = dogDataMap.get(dogData.id);
          // 엘리먼트의 내용을 수정
          // 예: dogInfo.querySelector(".dog-name").textContent = dogData.name;
        } else {
          // 기존 코드와 동일하게 엘리먼트 생성
          const dogInfo = createDogInfoElement(dogData);
          dogInfoContainer.appendChild(dogInfo);
        }
      });
    })
    .catch((error) => {
      console.error("Firestore 데이터 가져오기 실패:", error);
    });
}

Map을 사용하면 중복 생성을 피하면서도 객체와 엘리먼트 간의 매핑 관계를 간단하게 관리할 수 있는 장점이 있다고 한다.

dogDataMap이라는 변수를 Map()을 사용해 생성하고 객체를 생성할 때(함수: createDogInfoElement) dogDataMap에 객체를 저장한다. 그리고 updateDogInfoOnScreen 함수에서 기존에 생성한 엘리먼트를 수정해 중복된 엘리먼트가 추가되지 않게 한다.

이를 통해 데이터와 엘리먼트가 일치하게 유지되고, 문제가 해결되었다.

코드를 자세히 읽어보면 느낌은 알 것도 같은데 나 스스로 혼자 이정도의 코드를 짤 정도의 실력이 되려면 아직 정말 많이 부족한 것 같다.

이 외에도 검색 기능, 전체 삭제 기능 등 추가적으로 구현해야 할 부분들이 아직 남아있는데 이번 주에 경진대회 발표랑 공모전 2차 과제가 몰려있어서 일단 1차 리팩토링은 여기까지하고 시간을 내서 2차 리팩토링 시간에 위의 기능들을 추가해볼 예정이다.