Sister Nosilv story

[TIL 11th] 나의 첫 팀 프로젝트를 마무리하며, 코드 메모 및 보완점

by 노실언니

진행상황 

 Complete | `웹개발종합반` 완강

 Complete | `엑셀보다 쉽고 빠른 SQL` 완강

 Complete | 사전캠프 미션 `SQL 퀘스트`완료

 Complete | 1차시 팀 미니 프로젝트 개발완료 NEW

 Complete | 내일배움캠프 Spring 5기 사전캠프 (24.11.18.月 ~ 24.12.20.金─총 33일 / 14:00~18:00 ─일일 4시간)

 In progress | 내일배움캠프 Spring 5기 본캠프 (24.12.23.月 ~ 25.05.06.火 ─총 135일 / 9:00~21:00─일일 12시간)

 In progress | 국취제Ⅰ2회차 (24.12.13 ~ 25.01.12)

 In progress | 자바 `Java 문법 종합반` 강의 듣기 NEW

 In progress | 자바 혼공자바 읽기 & 퀘스트 하기

 In progress | 코테 C++ 자료구조, 알고리즘 책 읽기

 In progress | GIT 얄코의 깃/깃허브 책 읽고 실습 / 깃 특강 복습 NEW

 To-do |얄코의 HTML/CSS/JS 책 읽기

 To-do | DB 입문/실무 - 국민대 김남규 교수님 강의 듣기 & SQLD자격증챌린지 & SQL 자격검정 실전문제 풀기 


조는 계속 바뀐다고 하니, 오늘이 마지막이 되는구나

 

12/23(월), 24(화), 26(목), 27(금) 총 4일에 걸쳐 진행했던 팀 미니프로젝트가 종료되었다.

(공휴일과 주말은 칼 같이 쉬었다.)

 

프로젝트 이름과 목표

- 프로젝트 명: 강의에서 배운 내용(HTML, CSS, JS, Firebase, JQuery)를 적극 활용한 팀 소개 웹 페이지 제작

- 프로젝트 목표 개인 결과평

 ① CSS의 미적구현 최소화 / JS, Firebase, JQuery를 활용하는 기능에 집중하기 → Perfect

 ② Git, Github를 협업툴로 사용하기 → OK (사용은 했지만 최소한의 기능만 사용)

 

구현한 기능 중에 메모해두고 기억하고 싶은 부분

 ① 헤더/푸터 개별 파일로 작성하고 JS로 모든 페이지의 맨 앞/맨 뒤에 이식

// member_add.html

<head>
<script type="module">
    // header, footer 로드
    $(function () {
        $("#member-detail__main-header").load("header.html");
        $("#member-detail__main-footer").load("footer.html");
    });
</script>
</head>

<body>
	<!-- 메인 헤더 -->
    <div id="member-detail__main-header"></div>

    <!-- 입력 전체 창: 생략-->
    
    <!-- 메인 푸터 -->
    <div id="member-detail__main-footer"></div>
</body>

 

 ② 원하는 페이지로 이동하는 JS | 일반, URL 활용

아래 코드는 내가 아는 코드

// member_add.html
// -- JS '등록취소' 버튼 클릭 시, 이전 페이지로 이동
$("#memberinfo-form__cancel-btn").click(async function () {
	window.location.href = "index.html";	// 선택1: 특정 페이지로 이동
    history.back();							// 선택2: 이전 페이지로 이동
});

아래 코드는 내가 플젝하며 배운 코드 → 분석해보좌

// index.html
// 멤버 카드 클릭시 상세페이지로 이동: 동적 이벤트 바인딩
$(document).on("click", ".card", function () {
	const docid = $(this).attr("data-id");
    location.href = `member_detail.html?uid=${docid}`;
});

1. $(document).on("click", ".card", function () { } ) ;

$(document) HTML 문서 전체

.on("click", "Id나 Class와 같은 셀렉터", ...) 동적으로 생성된 요소를 포함하여 셀렉한 모든 요소에 대해 click 이벤트를 등록

 └ 페이지가 로드 된 이후 추가된 요소에도 클릭 이벤트가 적용됨 → 동적 이벤트 바인딩

function() 이름이 없는 함수로 재사용되지 않고 특정 이벤트가 발생했을 때만 호출되는 함수

 (this 키워드로 현재 이벤트가 발생한 특정 요소에 접근할 수 있다.)

 

2. const docid = $(this).attr("data-id");

$(this) 현재 이벤트가 발생한 요소 - .card

$(this).attr("data-id") 클릭된 요소(.card)에서 속성명이 data-id인 속성값을 가져오기 → "${docid}" 즉 doc.id 값을 가져옴

해당 요소의 속성 중 data-id의 값인 doc.id  변수 docid에 저장

 

3. location.href = `member_detail.html?uid=${docid}`;

location.href = `member_detail.html 지정된 URL로 이동

?uid=${docid}` docid의 값(doc.id)을 URL의 쿼리 파라미터로 추가

// index.html
// Firebase에서 데이터를 가져와서, 카드로 출력하는 구간
let docs = await getDocs(collection(db, "memberInfo"));
docs.forEach((doc) => {
  let row = doc.data();
  let docid = doc.id;			// docid는 자동생성된 doc.id (문서ID)가 저장됨

  let photo = row["photo"];
  let name = row["name"];

  let temp_html = `
  <div class="col">
    <div id="cards" class="card h-100" data-id="${docid}"> // 카드 등록시,요소의 data-id 속성에 문서ID 저장
      <img class="card-photo" src="${photo}">
      <div class="card-body">
        <h5 class="card-title">${name}</h5>
      </div>
    </div>
  </div>`;
  $("#card").append(temp_html);
});

이렇게 되면 member_detail.html에서는 이전 html이 URL을 통해 전달한 정보( ?uid=${docid} )를 다뤄야 한다

// member_detail.html
// 문서ID 추출 (멤버 식별 키)
const uid = window.location.search.substring(5);

const uid window.location.search.substring(5);

 window.location.search 현재 URL에서 ? 부터 그 이후의 값인 Query String만 가져옴 - 즉 ?uid=doc.id값 을 가져온다

.substring(n) 해당 문자열에서 n번째 문자부터 끝까지만 가져옴 - 즉 ?uid=doc.id값 만 가져온다

 

 ③ DB 읽기-그냥 읽기 VS 읽고 싶은 문서id 의 문서만 읽기 VS 쿼리 사용하기

// index.html Firebase 내용 그냥 읽기
let docs = await getDocs(collection(db, "memberInfo"));
docs.forEach((doc) => {
  let row = doc.data();
  let docid = doc.id;
  let photo = row["photo"];
  let name = row["name"];

  let temp_html = `
  <div class="col">
    <div id="cards" class="card h-100" data-id="${docid}">
      <img class="card-photo" src="${photo}">
      <div class="card-body">
        <h5 class="card-title">${name}</h5>
      </div>
    </div>
  </div>`;
  $("#card").append(temp_html);
});
// member_detail.html 원하는 문서ID의 문서만 읽기

// 함수: firebase에서 uid로 멤버 정보를 가져온 후 보여주는 함수
async function getUserByUID(uid) {
    // URL 파싱으로 나온 uid로 원하는 멤버의 테이블만 읽어오기 설정
    const userRef = doc(db, "memberInfo", uid);
    // 설정한 대로 읽어와서 docSnap에 저장
    const docSnap = await getDoc(userRef);
    // 테이블에 값이 존재한다면 읽어서 해당 요소의 content에 추가
    if (docSnap.exists()) {
        let name = docSnap.data().name;
        let age = docSnap.data().age;
        $("#name").append(name); 			// 해당 id 요소에 값 추가
        $("#age").append(age);
        
    } else {
        console.log("데이터 없음!");
    }
}
// 멤버정보 조회한 후 보여주는 위 함수실행
getUserByUID(uid);

// body 내부
<div class="member-detail__text-box"><span>이름 : </span><span id="name"></span></div>
<div class="member-detail__text-box"><span>나이 : </span><span id="age"></span></div>
// id가 name인 span요소의 값이 getUserByUID()함수에 의해 docSnap.data().name의 값으로 바뀜
// member_detail.html 쿼리 사용하여 테이블 내 원하는 행만 읽어오기
async function getCommentByUID(uid) {
    // 쿼리설정 : 댓글 테이블의 memberUid 필드값이 특정 uid값인 코멘트만 / 날짜 내림차순으로 읽기
    // Mysql로 치면 select * from comment where memberUid=uid order by date
    const q = query(
        collection(db, "comment"),
        where("memberUid", "==", uid), // 팀 전체의 경우 team2code 로 읽으면 됨
        orderBy("date")
    );
    
    // 위 쿼리사용해서 원하는 행만 인출
    const comments = await getDocs(q);
    let tempComment_html; // 코멘트 당 붙일 html
    if (comments.empty) {
        tempComment_html = `<li class="comment">
    <div class="non-comment" style="margin: 0px auto">코멘트가 없습니다</div></li>`;
        $("#comment-list").append(tempComment_html);
    } else {
        comments.forEach((comment) => {
            // 코멘트 하나씩 row에 할당하고
            let row = comment.data();
            let commentId = comment.id;
            // row의 정보들을 각각 컬럼별로 쪼개서 변수에 할당
            let content = row["comment"];
            let lastDate = row["date"].toDate();
            let year = lastDate.getFullYear();
            let month = ("0" + (lastDate.getMonth() + 1)).slice(-2);
            let days = ("0" + lastDate.getDate()).slice(-2);
            lastDate = year + "-" + month + "-" + days;
            let commenterId = row["id"];

            tempComment_html = `<li class="comment">
                <div class="comment-id">${commenterId}</div>
                <div class="comment-word">${content}</div>
                <div class="comment-btn-box">
                    <a class="comment-fix-btn" id="comment-fix-btn" data-id="${commentId}">수정</a>
                    <a class="comment-del-btn" id="comment-del-btn" data-id=${commentId}>삭제</a>
                    </div>
                    <div class="comment-date">${lastDate}</div>
                    </li>`;
                    $("#comment-list").append(tempComment_html);
        });
    }
}
// 코멘트를 DB에서 읽어서 원하는 코멘트만 보여주는 위 함수 호출
getCommentByUID(uid);

 

 ④ DB 테이블(문서)삭제 - doc.id를 사용한 테이블 삭제,  조건에 맞는 테이블만 삭제, 비밀번호 확인 후 삭제

왼쪽은 문서ID, 오른쪽은 테이블과 필드

1. doc.id(uid)를 사용한 테이블 단독 삭제 await deleteDoc(doc(db, "memberInfo", uid));

await JavaScript이 asyne/await 구문 "비동기 함수가 완료될 때까지 기다려라"

deleteDoc Firestore가 제공하는 지정된 경로의 문서(테이블)삭제 비동기 함수 ∴ await 필요

doc(db, "memberInfo", uid)

 doc() 특정 문서를 참조하는 객체 생성 : db-Firestore 데이터베이스 객체, "컬렉션 이름", 삭제할 문서ID

 

2. query를 사용한 테이블 몇몇 삭제 = memberUid필드값이 변수 uid와 같은 테이블만 삭제

const commentRef = collection(db, "comment"); Firestore DB 내의 comment 컬렉션을 참조하는 객체 생성

- collection 함수는 컬렉션 경로를 지정하여 Firestore에서 작업을 수행할 때 사용할 수 있는 참조 객체를 반환함

const q = query(commentRef, where("memberUid", "==", uid)); 쿼리 객체 생성 / memberUid 필드값이 uid인 테이블만

const querySnapshot = await getDocs(q); Firestore에서 실행하여 쿼리에 맞는 테이블만 가져오는 비동기 함수

querySnapshot.forEach(async(doc)=> { await deleteDoc(doc.ref )});

 querySnapshot.forEach(async(doc) => 쿼리 결과로 반환된 행에 대해 반복 작업 수행

 doc 특정 문서의 데이터와 참조 정보 포함

 doc.ref 특정 문서의 참조 객체

 deleteDoc 는 위에 1에서 설명

member_detail.html
// 멤버 삭제
$("#member-detail__delete").click(async function () {
    const password = prompt("비밀번호를 입력하세요");

    if (password == "admin") {
      if (confirm("정말 삭제하시겠습니까?")) {
        // 멤버 삭제: 테이블 삭제
        await deleteDoc(doc(db, "memberInfo", uid));
        // 해당 멤버의 댓글 삭제: 테이블 속 행 삭제
        const commentRef = collection(db, "comment");
        const q = query(commentRef, where("memberUid", "==", uid));
        const querySnapshot = await getDocs(q); // 쿼리 결과 가져오기
        querySnapshot.forEach(async (doc) => {
          await deleteDoc(doc.ref); // 조건에 맞는 댓글 삭제
        });
        alert("삭제 완료!");
        window.location.href = "index.html";
      } else {
        alert("취소되었습니다");
      }
      window.location.href = `member_detail.html?uid=${memberUid}`;
    } else if (password == null) {
      alert("취소되었습니다.");
    } else {
      alert("잘못된 비밀번호입니다.");
      window.location.href = `member_detail.html?uid=${memberUid}`;
    }
});

 

3. 각 댓글 문서를 비밀번호 확인 후 삭제

1) $("#comment-list") .on("click", ".comment-del-btn", async function () { } ) ;

$("#comment-list") comment-list라는 id의 요소에서

.on("click", "Id나 Class와 같은 셀렉터", ...) 동적으로 생성된 요소를 포함하여 셀렉한 모든 요소에 대해 click 이벤트를 등록

 └ 페이지가 로드 된 이후 추가된 요소에도 클릭 이벤트가 적용됨 → 동적 이벤트 바인딩

 └ async function 함수는 비동기 익명 함수로, 특정 이벤트 발생 시 호출되며 await와 함께 사용한다

  └ this 키워드로 현재 이벤트가 발생한 특정 요소에 접근할 수 있다.)

// member_detail.html
// 수정&삭제 버튼은 data-id="${commentID}" 특정 댓글의 문서ID를 가진다
comments.forEach((comment) => {
    // 코멘트 하나씩 row에 할당하고
    let row = comment.data();
    let commentId = comment.id;
    // row의 정보들을 각각 컬럼별로 쪼개서 변수에 할당
    let content = row["comment"];
    let lastDate = row["date"].toDate();
    let year = lastDate.getFullYear();
    let month = ("0" + (lastDate.getMonth() + 1)).slice(-2);
    let days = ("0" + lastDate.getDate()).slice(-2);
    lastDate = year + "-" + month + "-" + days;
    let commenterId = row["id"];
    tempComment_html = `<li class="comment">
                        <div class="comment-id">${commenterId}</div>
                        <div class="comment-word">${content}</div>
                        <div class="comment-btn-box">
                            <a class="comment-fix-btn" id="comment-fix-btn" data-id="${commentId}">수정</a>
                            <a class="comment-del-btn" id="comment-del-btn" data-id=${commentId}>삭제</a>
                            </div>
                            <div class="comment-date">${lastDate}</div>
                            </li>`;
            $("#comment-list").append(tempComment_html);
          });

2) const docid = $(this).attr("data-id");

$(this) 현재 이벤트가 발생한 요소 - .comment-del-btn

$(this).attr("data-id") 클릭된 요소(.comment-del-btn)에서 속성명이 data-id인 속성값을 가져오기 → "${commentId}" 즉 comment.id 값(특정 댓글 문서ID)을 가져옴

 

3) const password = prompt ("비밀번호 입력 : ") 비밀번호값 입력받기

4) const cmtDoc = doc(db, "comment", docid);

5) const cmtSnap = await getDoc(cmtDoc);

6) const memberUid = cmtSnap.data().memberUid 멤버정보문서의 Uid

7) cmtSnap.data().password 와 사용자가 입력한 값이 맞는지 if문으로 구현

// member_detail.html
// 댓글 삭제
$("#comment-list").on("click", ".comment-del-btn", async function () {
const docid = $(this).attr("data-id");
const password = prompt("비밀번호를 입력하세요");
// member_detail.html
// script 태그 속
// db에서 객체 가져오기
const cmtDoc = doc(db, "comment", docid);
const cmtSnap = await getDoc(cmtDoc);

// 멤버정보의 uid
const memberUid = cmtSnap.data().memberUid;

if (password == cmtSnap.data().password || password == "admin") {
  await deleteDoc(doc(db, "comment", docid));
  alert("댓글이 삭제되었습니다");
  window.location.href = `member_detail.html?uid=${memberUid}`;
} else if (password == null) {
  alert("취소되었습니다.");
} else {
  alert("잘못된 비밀번호입니다.");
  window.location.href = `member_detail.html?uid=${memberUid}`;
}
});

// ---------
// HTML 파트의 body 속
<div class="comment-box">
    <div class="comment-text">댓글</div>
    <ul class="comment-list" id="comment-list"></ul>

    <div class="input-comment-box">
      <form id="input-form">
        <div class="form-container">
          <div class="left-box">
            <input type="text" id="id" required placeholder="닉네임" />
            <input type="password" id="password" maxlength="4" required placeholder="비밀번호" />
            <button type="button" id="add-comment" class="comment-submit-btn">등록</button>
          </div>
          <div class="right-box">
            <textarea id="content" required></textarea>
          </div>
        </div>
      </form>
    </div>
  </div>

 

 

 ⑤ 수정 : DB읽어서 값을 웹 페이지에 보여준 후 수정

// member_detail.html
// 1) 수정버튼 클릭 시, 특정 멤버의 문서ID를 쿼리스트링으로 URL에 넘김
$("#member-detail_modify").click(async function () {
        const password = prompt("비밀번호를 입력해주세요");
        if (password == "admin") {
          window.location.href = `./member_modify.html?uid=${uid}`;
        } else if (password == null) {
          alert("취소되었습니다.");
        } else {
          alert("잘못된 비밀번호입니다.");
          window.location.href = `member_detail.html?uid=${memberUid}`;
        }
});
// member_modify.html
// 2) DB값을 읽고 웹페이지로 보여주기

// <script> 태그 속 일부
const uid = window.location.search.substring(5); // 쿼리스트링 파싱 -> 특정 멤버 문서id 저장

const userRef = doc(db, "memberInfo", uid);
const docSnap = await getDoc(userRef);

// 정보 인출 시도 후, 값이 있다면 일단 내용 보여주기
if (docSnap.exists()) {
  $("#memberinfo_name").val(docSnap.data().name);
  $("#memberinfo_age").val(docSnap.data().age);
}

// <body> 태그 속 일부
<div class="memberinfo-form__inputset">
	<label for="memberinfo_name" class="memberinfo-form__label">이름</label>
	<input type="text" class="memberinfo-form__input" id="memberinfo_name" placeholder="이름을 입력하세요" required />
</div>

<div class="memberinfo-form__inputset">
	<label for="memberinfo_age" class="memberinfo-form__label">나이</label>
	<input type="text" class="memberinfo-form__input" id="memberinfo_age" placeholder="나이를 입력하세요" required />
</div>
// member_modify.html
// 3) 수정 클릭시, 입력받은 내용 DB에 쓰기

$("#modify").click(async function () {
  let name = $("#memberinfo_name").val();
  let age = $("#memberinfo_age").val();
  let doc = {
    name: name,
    age: age,
  };
  // 예외 고려 : 수정실패
  try {
    await updateDoc(userRef, doc);
  } catch (error) {
    console.error(error);
    alert("수정 실패!");
  }
  // DB에 덮어씌우는 작업이 완료되면, 멤버상세페이지로 이동
  window.location.href = `member_detail.html?uid=${uid}`;
});

// 이전 버튼 클릭시, 멤버상세페이지로 이동
$("#before").click(async function () {
  window.location.href = `member_detail.html?uid=${uid}`;
});

 

 ⑥ 다양한 버튼 클릭 이벤트 코드 : 동적 / 정적 이벤트 메소드

- 동적 메소드 : 아직 DOM에 존재하지 않거나 첫 로드 이후에 추가된 요소에도 이벤트 적용가능

- 정적 메소드 : DOM이 처음 로드되었을 때 존재하는 요소에만 직접 이벤트 핸들러를 등록함

 

$(document 혹은 "셀렉터1").on("click", "셀렉터2", function () { function 정의 } );

  $(document 혹은 "셀렉터1") HTML문서 로드 후 전체 문서 혹은 특정 요소를 대상으로 선택하여

 .on() 이벤트를 동적으로 등록하는 메서드

 "click", "셀렉터2", function() { function 정의 } 특정 요소(이벤트 트리거)에 대해 "click" 클릭 이벤트 처리

  만일 해당 요소가 동적으로 추가되더라도 이벤트가 작동함 → 동적 바인딩

 

$(document 혹은 "셀렉터1").click( function () { function 정의 } );

  $(document 혹은 "셀렉터1") HTML문서 로드 후 전체 문서 혹은 특정 요소를 대상으로 선택하여

 .click() 이벤트를 정적으로 등록하는 메서드

 function() { function 정의 } 클릭 이벤트를 처리하는 함수 정의

  └ async function() 으로 작성시, 비동기함수 → await 키워드로 비동기 작업처리가능

// -- 보고싶은 코드만 잘라서 가져왔으므로 문법적으로는 옳지 않습니다 !

// 멤버 등록 버튼 클릭 시 등록페이지로 이동
$(document).on("click", ".main-page__saveBtn", function () {

// 멤버 카드 클릭 시 상세페이지로 이동
$(document).on("click", ".card", function () {
	const docid = $(this).attr("data-id");
    
// 멤버 등록 버튼 클릭 시 입력값 → DB저장 & 이전 페이지로 이동
$("#memberinfo-form__addMember-btn").click(async function () {
    await addDoc(collection(db, "memberInfo"), doc);
    
// 멤버 등록 취소 버튼 클릭 시 이전 페이지로 이동
$("#memberinfo-form__cancel-btn").click(async function () {

// 멤버 삭제 버튼 클릭 시 멤버DB 삭제
$("#member-detail__delete").click(async function () {
	await deleteDoc(doc(db, "memberInfo", uid));
    querySnapshot.forEach(async (doc) => {
      await deleteDoc(doc.ref);

// 멤버 수정 버튼 클릭 시 멤버DB 수정
$("#member-detail_modify").click(async function () {
      
// 방명록 등록 버튼 클릭 시 댓글DB 테이블 추가 쓰기
$("#add-comment").click(async function () { ...
    await addDoc(collection(db, "comment"), doc);
    
// 댓글 삭제 버튼 클릭 시 댓글DB 테이블 삭제
$("#comment-list").on("click", ".comment-del-btn", async function () {
	const docid = $(this).attr("data-id");
    await deleteDoc(doc(db, "comment", docid));
    
// 댓글 수정 버튼 클릭 시 댓글DB 읽기&덮어쓰기
$(document).on("click", "#comment-fix-btn", async function () {
	const commentUid = $(this).attr("data-id");
	const docSnap = await getDoc(commentRef);
    let commentWordDiv = $(this).closest(".comment").find(".comment-word");
    $(this).attr("id", "fix-btn=" + commentUid + "&" + commentDoc.password);

// 댓글 수정 버튼 클릭 시
$(document).on("click", `[id^="fix-btn="]`, async function () {
    const id = $(this).attr("id").split("=")[1];
    await updateDoc(commentRef, commentDoc);
    const commentWordDiv = $(this).closest(".comment").find(".comment-word");
    $(this).attr("id", "comment-fix-btn");

 

느낀 점 계속 기억해야할 것

- 팀플젝은 시간계획, 역할분담 모두 세부적으로 잘 세워야 제한시간 내에 개발이 가능하고 회색지대를 최소화할 수 있는 것 같다.

 (회색지대: 구현은 해야하는데 맡은 사람이 없는 경우)

- 깃 커밋 컨벤션 계속 사용하기

- Pull Request와 Issue창 적극 활용하기

- 트러블 슈팅 문서화 → 담당자가 필요하나

- README.md 작성하기 → 담당자가 필요하나(2)

 → 그러려면, 매 작업을 문서화 해야 함

- 깃 허브에 올라가는 코드 중 숨기고 싶은 코드구간은 어떻게 숨겨야 할까 ( gitignore 처럼 파일통째로 숨기는 것 말고)

- Git 브랜칭 전략 사용하여 개발 브랜치와 기능 브랜치 활용하기

https://youtu.be/wtsr5keXUyE?si=jFsteROePfqsIF7a

 

 

반응형

블로그의 정보

노력하는 실버티어

노실언니

활동하기