[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를 사용한 테이블 삭제, 조건에 맞는 테이블만 삭제, 비밀번호 확인 후 삭제
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
'Today I Learned' 카테고리의 다른 글
[TIL 13th] JAVA 2byte 정수형 char, short의 가장 큰 차이, 그리고 큰/작은 따옴표의 차이 (0) | 2025.01.09 |
---|---|
[TIL 12th] SQL코드카타, Case when이 최선이 아닌 문제 (0) | 2024.12.31 |
[TIL 10th] 코드카타 까먹은 코드들 / 파이어베이스 쿼리사용법 / 협업시 유의점 (0) | 2024.12.30 |
[TIL 7th] Spring 5기 온보딩 | 배운게 없는데 많달까 (1) | 2024.12.23 |
[TIL 6th] SQL 완강-빠르게 핥은 SQL / 애증의 group by (1) | 2024.12.12 |
블로그의 정보
노력하는 실버티어
노실언니