WIL 팀 프로젝트

이진호·2023년 10월 7일
0

TIL

목록 보기
9/66
post-thumbnail

레이아웃

전체 레이아웃

팀소개 부분

팀원별 소개 부분

레이아웃


처음에 해당 레이아웃을 어떻게 구현할까 고민을 많이 했었는데 .bubble-container라는 div를 만든 뒤에 .bubble들을 한 공간에 넣어서 각각 left와 right를 position을 지정해서 위치를 잡아주려고 했지만 내가 원하는대로 left와 right가 지정이 되지 않고 원하는 그림이 나오지 않아서 bubble-speech-container 내부에 3개의 아이템들을 넣고 flex를 통해서 구현하였다.

말풍선 꼬리 CSS

각 말풍선 꼬리들이 아바타를 가르킬 수 있도록 CSS를 구성하기 위해서 고민을 했는데 아직 초기 단계여서 나중에 말풍선이 추가되더라도 특정 css를 빼거나 추가하는 방식으로 쉽게 바꿀 수 있을 것 같아서 공통된 부분들을 최대한 뽑아내는 방식으로 구현했다. 총 6개에 말풍선 박스 기본 모양(.bubble)과 말풍선 꼬리부분(.bubble:after)을 공통요소로 잡은 뒤에 왼쪽과 오른쪽(.left,.right)을 기준으로 나누고 마지막으로 top,center,bottom형식으로 나눠서 css를 작성했다.

.bubble {
  position: relative;
  background: #a2a4a4;
  border-radius: 0.4em;
  margin: 30px;
  width: 200px;
  height: 100px;
  padding: 1px;
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1), 0 10px 20px rgba(0, 0, 0, 0.06);
}
.bubble:after {
  content: '';
  position: absolute;
  border-style: solid;
  border-width: 12px 9px 0;
  border-color: #a2a4a4 transparent;
  display: block;
  width: 0;
  z-index: 1;
}
.bubble.right:after {
  left: 95%;
}
.bubble.left:after {
  right: 95%;
}
.bubble.bottom:after {
  bottom: 0;
}
.bubble.top:after {
  top: 0;
}
.bubble.right.bottom:after {
  transform: rotate(180deg);
}
.bubble.right.top:after {
  transform: rotate(360deg);
}
.bubble.left.bottom:after {
  transform: rotate(-180deg);
}
.bubble.left.top:after {
  transform: rotate(360deg);
}
.bubble.right.center:after {
  top: 50%;
  transform: translateY(-50%) rotate(180deg);
}
.bubble.left.center:after {
  top: 50%;
  transform: translateY(-50%) rotate(180deg);
}

말풍선 내용

말풍선에 종류에는 텍스트로 이뤄진 말풍선이 있고 여러 관심분야를 하나의 말풍선에 넣기 위해서 리스트로 이뤄진 말풍선이 있다.
이를 위해서 동적으로 넣을 때 아예 태그 자체를 다르게 넣었고 다른 class를 부여해서 보여지는 것이 달라지게끔 구현했다.

기능

팀 및 팀원별 소개 부분

버튼

기본적으로 팀 소개 부분이 main에 나타나게 되고 각 팀원 별로 버튼을 각각 만들어서 해당 버튼을 눌르면 해당 팀원의 정보들이 main부분에 나타날 수 있도록 구현하기 위해서 팀 소개 부분은 정적으로 html에 직접적으로 넣고 팀원 소개 부분은 json 데이터를 가져와서 동적으로 넣을 수 있게 구현하였다.
팀원 정보 같은 경우에는 한번 설정하면 바꿀 일이 많지 않을 것 같아서 로컬 상에서 가져올 수 있도록 하였다.

팀원별 데이터 가져오기

우선, 팀원 별로 공통된 주제에 대해서 내용을 채워넣고 아래와 같은 json파일을 만든 이후에 javascript 파일에서 해당 데이터를 가져왔다.

// data.json 내용
{
	id: string,
    name: string,
    tmi: string,
    mbti: string,
    intersting : string[],
    introduce : string,
    blog: string,
    img: string(url),
    advantage : string
} 
// url을 입력으로 해당 url의 데이터를 가져오는 함수
async function getDataPromise(url) {
	const result = await fetch(url).then((result) => result.json());
    return result;
}

데이터를 동적으로 채워넣기

makeInfoItem(data)

하나를 샘플로 만든 뒤에 그에 맞게끔 데이터들을 가공해서 엘리먼트들을 만들어서 하나의 InfoItem을 만들어서 반환하게끔 구현하였다.

  • textInfo, infoMap, bubbleObject 객체들을 이용을 했는데 key를 통해서 각각 필요한 데이터들을 가져올 수 있게끔 구현하였다. 이를 통해서 key값만 가지고도 각각 값과 엘리먼트들이 매칭이 가능했다.
  • 관심분야의 경우 string[] 형식으로 데이터가 들어왔고 이를 절반으로 나누고 각각의 관심분야 아이템들은 해시태그 형식으로 표현하기 위해서 따로 처리 했다.

    $().append(content,[,content])

    Type: htmlString or Element or Text or Array or jQuery
    와 같이 사용할 수 있어서 배열을 통해 한번에 넣어줬다.

    const [parent,items] = data;

    구조 분해 할당을 통해 변수를 선언한 뒤에 넣어줬다.

// 소개 페이지 동적 생성
function makeInfoItem(data) {
  const { id, name, tmi, mbti, interesting, introduce, img, advantage, blog } = data;
  const $infoItem = $(`<div class="${id} user info-item"></div>`);
  const $heading = $(`<h3>안녕하세요.<br/>저는 <strong>${name}</strong>입니다.</h3>`);
  const $introduceContainer = $('<div class="introduce-container"></div>');
  const $introduce = $(`<p>${introduce}</p>`);
  const $bubbleSpeechContainer = $('<div class="bubble-speech-container"></div>');
  const $bubbleLeft = $('<div class="bubble-left"></div>');
  const $bubbleRight = $('<div class="bubble-right"></div>');
  const $img = $('<img />');

  const $bubbleMBTI = $('<div class="bubble right bottom"></div>');
  const $bubbleinteresting1 = $('<div class="bubble right center"></div>');
  const $bubbleAdvantage = $('<div class="bubble right top"></div>');

  const $bubbleTMI = $('<div class="bubble left bottom"></div>');
  const $bubbleBlog = $('<div class="bubble left top blog"></div>');
  const $bubbleinteresting2 = $('<div class="bubble left center"></div>');

  /* ------------ 버블 시작 ------------  */
  const textInfo = {
    MBTI: mbti,
    TMI: tmi,
    advantage: advantage,
    blog: blog,
  };
  const infoMap = {
    MBTI: 'MBTI',
    TMI: 'TMI',
    advantage: '장점',
    blog: '블로그',
  };
  const bubbleObject = {
    MBTI: $bubbleMBTI,
    TMI: $bubbleTMI,
    advantage: $bubbleAdvantage,
    blog: $bubbleBlog,
  };

  // 데이터 동적 넣기 (text)
  Object.keys(textInfo).forEach((key) => {
    bubbleObject[key].append(makeTextBubble(infoMap[key], textInfo[key]));
  });
  // 데이터 동적 넣기 (list)
  const interesting1 = interesting.slice(0, interesting.length / 2),
    interesting2 = interesting.slice(interesting.length / 2);

  [
    [$bubbleinteresting1, interesting1],
    [$bubbleinteresting2, interesting2],
  ].forEach((data) => {
    const [parent, items] = data;

    parent.append(makeListBubble('관심분야', items));
  });

  // 버블 left, right에 아이템들 넣기
  $bubbleLeft.append([$bubbleMBTI, $bubbleinteresting1, $bubbleAdvantage]);
  $bubbleRight.append([$bubbleTMI, $bubbleinteresting2, $bubbleBlog]);
  $bubbleSpeechContainer.append([$bubbleLeft, $img, $bubbleRight]);

  /* ------------ 이미지 ------------  */
  $img.attr('src', img);

  /* ------------ 소개글 ------------  */
  $introduceContainer.append($introduce);

  $infoItem.append([$heading, $bubbleSpeechContainer, $introduceContainer]);
  return $infoItem;
}
makeTextBubble(label,desc)

해당 함수는 텍스트 말풍선을 만들기 위한 함수로 왼쪽 상단에 들어갈 팀원들의 정보와 안에 내용을 넣을 수 있게 해주었고, 특히 blog의 경우에는 a tag를 넣어 주었다.

// TEXT 세팅(MBTI, TMI, 장점, 블로그 주소) 함수
function makeTextBubble(label, desc) {
  const $label = $(`<div class="label">${label}</div>`);
  const $desc = $(`<div class="desc">${desc}</div>`);
  if (label === '블로그') {
    $desc.text('');
    $desc.append($(`<a href="${desc}">${desc}</a>`));
  }
  const $wrapper = $('<div class="balloon-text"></div>');

  $wrapper.append([$label, $desc]);

  return $wrapper;
}
makeListBubble(label,listItems)

해당 함수는 리스트 말풍선을 만들기 위한 함수로 ul 태그에 Array.map()메서드를 이용하여 구현하였다.

// LIST 세팅 함수
function makeListBubble(label, listItems) {
  const $label = $(`<div class="label">${label}</div>`);
  const $list = $('<ul class="list"></ul>');
  // list item 내용들 $list에 추가
  $list.append(
    listItems.map((item) => {
      return $(`<li class="list-item">#${item}</li>`);
    }),
  );

  const $wrapper = $('<div class="balloon-list"></div>');

  $wrapper.append([$label, $list]);

  return $wrapper;
}

방명록

방명록은 firebase의 firestore를 통해서 데이터들을 저장할 수 있도록 했다.

방명록 데이터 생김새

collection은 'guestbooks'로 설정하였고 각 document들은 아래와 같은 형식으로 저장된다.

{
	text: string,
    nickname: string,
    email : string
}

CRUD

방명록 생성

에러가 발생했는지 체크하기 위해서 try-catch문을 사용하였다.
addDoc 함수는 firestore에서 제공하는 함수로 컬렉션과 데이터를 입력으로 넣는다.

addDoc(collectionRef, data)

/**
 * 방명록을 작성하는 함수
 * @param {string} email
 * @param {string} nickname
 * @param {string} text
 * @returns {object} {msg : 방명록 작성 성공 실패 메시지}
 */
export async function writeGuestBook(email, nickname, text) {
  try {
    await addDoc(collection(db, 'guestbooks'), { email, nickname, text });
    // ref에 id 자동 저장
  } catch (e) {
    console.error(e);
    return { msg: 'write-fail' };
  }
  return { msg: 'write-success' };
}

방명록 읽기

나중에 방명록을 수정하거나 삭제할 수 있게끔 해야하는데 해당 기능을 구현하기 위해서 각 방명록 document에 id를 이용하여 구현하기 위해서 읽어올 때 id값을 추가해서 방명록 목록을 반환한다.

/**
 * 방명록 목록을 가져오는 함수
 * @returns {object} {data: [{email,text,nickname, id}] , msg : 방명록 성공 실패 메시지}
 */
export async function readGuestBooks() {
  try {
    const docSnap = await getDocs(collection(db, 'guestbooks'));
    const docs = docSnap.docs;
    return { data: docs.map((v) => ({ id: v.id, ...v.data(), msg: 'read-success' })) };
  } catch (e) {
    console.error(e);
    return { data: [], msg: 'read-fail' };
  }
}

방명록 업데이트

/**
 * 특정 방명록을 업데이트 하는 함수
 * @param {string} id
 * @param {string} text
 * @returns {object} {msg: 방명록 업데이트 성공 실패 메시지}
 */
export async function updateGuestBook(id, text) {
  try {
    const docRef = doc(db, 'guestbooks', id);
    updateDoc(docRef, { text });
    return { msg: 'update-success' };
  } catch {
    return { msg: 'update-fail' };
  }
}

방명록 지우기

/**
 * 특정 방명록을 제거하는 함수
 * @param {string} id
 * @returns {object} {msg: 방명록 삭제 성공 실패 메시지}
 */
export async function removeGuestBook(id) {
  try {
    await deleteDoc(doc(db, 'guestbooks', id));
    console.log('삭제완료');
    return { msg: 'remove-success' };
  } catch (e) {
    console.error(e);
    return { msg: 'remove-fail' };
  }
}

방명록 지우는 함수는 만들어 놓고 방명록이 지워지지 않아 팀원분과 얘기를 나눴던 기억이 있는데... docRef와 docSnap은 차이가 있었다. deleteDoc(docRef)가 들어가야지 deleteDoc(docSnap)을 하면 안된다라는 것을 이때 알게됐다.

사용자 인증

기존에는 패스워드를 통해서 방명록을 작성하고 본인이 작성한 방명록을 수정하거나 삭제할 때 패스워드만을 이용해서 수정, 삭제가 가능하게끔 구현할려고 했는데 시간이 많으니 로그인 기능도 넣었으면 좋겠다라는 튜터님의 말씀에 해당 기능을 넣었다.
그 중에서 대부분이 github계정이 있을거라고 생각해서 github 로그인 서비스를 이용하기로 생각했다.

GitHub 로그인 기능

userInfo라는 전역 변수를 이용해서 현재 유저 로그인 상태를 확인할 수 있도록 하였고 로그인하거나 로그아웃할 때 해당 변수를 수정할 수 있게끔 구현하였다.
그리고 firebase에서는 로그인 상태가 변하면 이에 따라 조작할 수 있는 onAuthStateChanged함수를 제공하고 있고, 이를 이용하여 사용자가 로그아웃 하거나 현재 로그인됐구나를 알리기 위해 DOM을 조작할 필요성이 있었고 로그인 성공과 실패 콜백 함수들을 설정하여 DOM을 조작할 수 있도록 하였다.

// firebase.js 
/* Auth 관련 함수들... */
export let userInfo = null;

/**
 * 회원가입 하는 함수
 */
export async function signUser() {
  signInWithPopup(auth, provider)
    .then((result) => {
      userInfo = result.user;
    })
    .catch((error) => {
      // Handle Errors here.
      const errorCode = error.code;
      const errorMessage = error.message;
      console.error(errorCode, errorMessage);
    });
}
/**
 * 로그아웃 하는 함수
 */
export async function signOutUser() {
  signOut(auth);
}
export function onAuthChange(onSuccess, onFail) {
  onAuthStateChanged(auth, (user) => {
    if (user) {
      userInfo = user;
      onSuccess(user);
    } else {
      userInfo = null;
      onFail();
    }
  });
}

// common.js
onAuthChange(
    (user) => {
      // 로그인 상태일 시
      $('.logout-btn').show();
      $('.welcome h4').text(`안녕하세요! ${user.reloadUserInfo.screenName}님!`);
    },
    () => {
      // 로그인 상태가 아닐 시
      $('.logout-btn').hide();
      $('.welcome h4').text('');
    },
  );

사용자 인증 기능 추가에 따른 수정, 삭제, 등록 기능 수정

사용자 인증 기능이 추가됨에 따라서 수정, 삭제, 등록 기능을 수정해야 했고, 방명록을 처음 불러올 때 CRUD 대한 내용을 많이 수정하였다.

읽기

서버에서 방명록 데이터를 가져온 뒤에 그 내용들을 랜더링 하는 과정을 거쳐야 한다. 또한 이후에 방명록 데이터가 변할 때면 또 랜더링을 해줘야 한다. 기존에는 reload를 시켜서 다시 읽어오는 방식으로 했었는데 굳이 그럴 필요 없이 서버 데이터의 사본을 가지고 있다가 그 내용들을 업데이트 하면서 사용자들이 바로바로 볼 수 있게 하고, 불필요한 서버 통신을 할 필요가 없다고 생각했기 때문에 처음 읽어올 때 서버에서 데이터를 가지고 오고 GguestBooks라는 전역 변수를 이용해 저장을 하였다.
어차피 읽어와도 랜더링해야하고 수정된 이후에도 랜더링을 해야했기 때문에 renderGuestBook(type, guestBooks)이라는 함수를 만들어줬다.
type을 통해서 서버와 통신을 통해 데이터를 가져와야하는지 아니면 로컬 데이터를 사용할 것인지를 구분해줬다.
그리고 서버와 통신하는데 있어 문제가 생긴다면 기존에 불러왔던 전역변수 GguestBooks를 이용하여 랜더링 할 수 있게 하였다.
GguestBooks의 기본값은 []이다.

async function renderGuestBook(type, guestBooks) {
  // 1. 방명록을 가져온다
  let docs = guestBooks;
  if (type === 'server') {
    const { msg, data } = await readGuestBooks();
    if (msg === 'read-fail') {
      console.error('read guest book fail');
      renderGuestBook('local', GgusetBooks);
      return;
    }
    docs = data;
    GgusetBooks = data;
  }
  // 2. 가져온 방명록을 동적으로 넣어준다.
  const $guestBooksBoxes = docs.map(({ email, nickname, text, id }) => {
    const $guestBooksBox = $(`<div class="guestbooks-box" data-id="${id}"></div>`);
    const $text = $(`<p class="guestbook-text">${text}</p>`);
    const $controlContainer = $('<div class="control-container"></div>');
    const $nickname = $(`<span class="nickname">${nickname}</span>`);
    const $warningBtn = $(`<button type="button" class="warning-btn" >삭제</button>`);
    const $updateBtn = $(`<button type="button" data-type="update" class="success-btn">수정</button>`);

    $controlContainer.append([$nickname, $warningBtn, $updateBtn]);

    $guestBooksBox.append([$text, $controlContainer]);
    
    ...
 
    return $guestBooksBox;
  });

  $('.guestbooks-container').html('').append($guestBooksBoxes);
}

수정,삭제

수정과 삭제는 기본적으로 로그인 상태(userInfo)가 존재해야지만 할 수 있고, 또한 userInfo의 email값과 해당 방명록의 email값이 동일해야지 기능을 수행할 수 있도록 구현해야 했다.

삭제 같은 경우에는 id와 email값만 알면 되서 밖으로 함수를 뺄 수 있었다.

async function renderGuestBook(type, guestBooks) {
    ...
    const $warningBtn = $(`<button type="button" class="warning-btn" >삭제</button>`);
	...

    // 삭제 이벤트

    $warningBtn.on('click', handleRemoveGuestBook(id,email));
	...

    return $guestBooksBox;
  });

  $('.guestbooks-container').html('').append($guestBooksBoxes);
}
function handleRemoveGuestBook(id, email) {
  return () => {
    if (!userInfo) {
      return alert('로그인 후 이용하실 수 있습니다.');
    }
    if (userInfo.email !== email) {
      return alert('방명록을 남긴 사용자가 아니면 삭제할 수 없습니다!');
    }
    GgusetBooks = GgusetBooks.filter((guestBook) => guestBook.id !== id);

    renderGuestBook('local', GgusetBooks);
    removeGuestBook(id);
  };
}

다만 수정의 경우에는 기능이 몇개 더 있어야 했는데 수정 버튼을 눌르면 수정할 수 있게 #text태그가 input 태그로 변경을 할 수 있게 하는 등 수정 버튼 엘리먼트에 접근할 이유가 있었다. $(this)를 사용하거나 다시 수정 버튼을 지정하여 진행할 수 있지만 $(this)는 가독성이 좀 떨어질 것 같고 다시 수정 버튼을 지정하는 것은 혹여나 여러 개의 수정버튼이 선택될 우려도 있을 것 같아서 내부에 작성해서 수정 버튼을 한정했다. 그리고 또 하나의 차이점은 삭제 함수에서는 다시 render 함수를 실행했는데 수정 함수에서는 따로 렌더링 하지 않았다. 공부하는 입장에서 2가지 구현 방법이 있었기 때문에 서로 다르게 구현해 봤다.

async function renderGuestBook(type, guestBooks) {
	...
    const $updateBtn = $(`<button type="button" data-type="update" class="success-btn">수정</button>`);
	...

    $updateBtn.on('click', () => {
      if (!userInfo) {
        return alert('로그인 후 이용하실 수 있습니다.');
      }
      if (userInfo.email !== email) {
        return alert('방명록을 남긴 사용자가 아니면 수정할 수 없습니다!');
      }
      const type = $updateBtn.data('type');
      if (type === 'update') {
        // '수정'버튼 상태라면
        // p태그를 input태그로 변경해주고
        $guestBooksBox.find('.guestbook-text').replaceWith(`<input type="text" value="${text}" required/>`);
        // 수정 버튼의 data-type을 complete로 변경해 주고 내부 텍스트를 수정 완료로 변경해준다.
        $updateBtn.data('type', 'complete').text('수정 완료');
      }
      if (type === 'complete') {
        // '수정 완료' 버튼 상태라면
        const $input = $guestBooksBox.find('input');
        const nextText = $input.val();

        $input.replaceWith(`<p class="guestbox-text">${nextText}</p>`);
        $updateBtn.data('type', 'update').text('수정');

        GgusetBooks = GgusetBooks.map((guestBook) => {
          if (guestBook.id !== id) return guestBook;
          guestBook.text = nextText;
        });
        updateGuestBook(id, nextText);
      }
    });

    return $guestBooksBox;
  });

  $('.guestbooks-container').html('').append($guestBooksBoxes);
}
profile
dygmm4288

0개의 댓글