본문 바로가기

Project

Project 마무리(2) - Board 사이트 구현

728x90
반응형

1) 마무리

    1-1) 개요

    1-2) 결과물

    1-3) 진행 중 이슈 및 해결

    1-4) 후기

 

 

 

 

 

 

1) 마무리

1-1) 개요

2022년 12월 26일부터 12월 30일까지 5일간 진행한 게시판 사이트 구현 프로젝트!

이번에는 지금까지 내 자신이 얼마나 route, controller, service, repository를 이해(express 모듈을 비롯한

cookie-parser, MVC 패턴 등)하고 있는지를 확인하는데 가장 큰 목적을 두고 작업을 진행했다.

이를 위해 함께 프로젝트를 진행한 팀원 분과 논의하여 담당 파트를 구분하고,

단독으로 코드를 구현할 수 없고 기능이 이어져 있다고 판단되는 부분에 대해서는

협업을 통해 구현해나가기로 결정했다.

이렇게 묘한 설렘과 긴장을 안고 첫날 To Do List를 작성하는데 처음에는 우리가 작성한 List대로

프로젝트의 상황이 전반적으로 순조롭게 흘러갈 것만 같았다.

물론 구현 과정에서 새로운 이슈가 발생하기도, 또는 처음 구현방식을 시도하는 과정에서

장애물을 만나 해당 구현계획을 수정하리라 예상하였다.

하지만 이번 5일 간의 프로젝트는 순조롭다 생각하는 순간 예기치 못한 어려움을 만났고,

이를 해결하고자 대안을 세우면 이후 다른 기능을 구현하는 과정에서

해당 대안이 에러를 발생시키는 요인으로 작용하기도 하였다.

이처럼 여러 방면으로 고민하고 노력한 나의 경험을 다른 분들께도 소개하고자

이렇게 회고를 시작하게 되었다.

 

 

 

1-2) 결과물

 

로그인/회원가입(ID 중복체크 기능), 회원정보 수정(Profile), 무한루프 슬라이드/자동 슬라이드 변환, 팀원 소개(About us), 게시판 CRUD(write, view, modify, delete), admin 기능 등 최대한 게시판과 관련된 다양한 기능들을 구현하려 노력했다.

 

github : https://github.com/nazzzo/project_OB_board

 

 

 

1-3) 진행 중 이슈 및 해결

(1) 회원가입(join.html) 상 ID 중복체크 이슈

회원가입 입력폼 중 ID의 경우, 입력한 ID가 중복인지 아닌지를 체크하기 위해

"아이디 중복" 버튼을 클릭하면 아이디 중복 확인 창이 떠서 해당 아이디가 중복인지 아닌지를 체크할 수 있도록 설계하였다.

이에 ID가 데이터베이스에 이미 존재하는지 아닌지, 즉 중복체크를 하는 것까지는 무난히 작업이 완료되었다.

 

하지만 ID 중복 확인 창에서 입력한 값이 중복 ID가 아닌 경우에는 회원가입 페이지로 해당 입력 데이터를

어떻게 넘길  수 있을지에 대한 문제에 부딪혔다.

처음에는 값을 한 번 더 인위적으로 찍어주는 코드를 추가하면 되지 않을까하고 생각했지만

ID 중복 확인 창과 회원가입 창은 그 관계과 자식창과 부모창이기에

동일한 페이지에서 데이터를 주고받듯이 쉽게 구현할 수 있는 것이 아니었다.

그렇다고 입력값을 대상으로 ID 중복체크를 구현하기 위해 route를 따로 하나 만들 수도 없었다.

이렇게 되면 해당 회원가입 페이지가 중복 체크를 할때마다 다른 페이지로 넘어가기 때문에

만일 회원가입을 진행하다 ID를 나중에 다시 수정하고자 하면 그때까지 입력한 데이터가

전부 날아가기에 사용자의 불편(회원가입 정보를 다시 처음부터 입력해야 하는 불편)을 초래하게 된다.

 

이후 여러 방안을 물색하던 중 "opener" 함수를 발견하였고,

이는 자식창(ID 중복 확인 창)에서 부모창(회원가입 창)으로 데이터(ID 입력값)를 넘길 때 사용하는 window 객체 내의 함수였다.

이를 통해 자식창에서 입력한 값을 부모창으로 넘기는 것까지 무사히 구현하였다.

 

 

idcheck.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="/css/idcheck.css" />
  </head>
  <body>
    <form action="/join/idcheck" method="post" id="form_area">
      <h3><label for="user_id">아이디 중복 확인하기</label></h3>
      <span id="inputbox" class="box">
        <input
          type="text"
          id="chlidId"
          name="user_id"
          style="background-color: #fff"
          class="inbox"
        />
        <button id="closeButton" onclick="closeAndSendValue()" class="btn">
          중복 확인하기
        </button>
      </span>
      <span id="failId" style="display: none; color: red">
        <p>아이디가 이미 존재합니다.다른 아이디를 사용해주세요</p>
      </span>
      <span id="successId" style="display: none; color: green">
        <p>입력하신 아이디는 사용할 수 있습니다.</p>
      </span>
    </form>
    <script src="/js/idcheck.js"></script>
    <script>
      function closeAndSendValue() {
        console.log("실행");
        let childId = document.getElementById("chlidId").value;
        window.opener.document.getElementById("user_id").value = childId;
        // window.close()
      }
    </script>
  </body>
</html>

 

join.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>:: Old_Boys 회원가입 ::</title>
    <link rel="stylesheet" href="/css/join.css" />
  </head>
  <body>
    <div id="wrap">
      <div id="logo">
        <a href="/">
          <img
            src="https://i.ibb.co/RTjgWsX/imageedit-22-4204026569.png"
            id="logoimg"
          />
        </a>
      </div>
      <form method="post" id="join" action="/join/join">
        <div id="joinidpw">
          <div id="joinid" class="form_area4">
            <h3><label for="user_id" class="icon">아이디</label></h3>
            <span id="inputbox" class="box">
              {{token.value}}
              <input
                value=""
                class="inbox"
                type="text"
                onclick="openIdCheckPopup()"
                name="user_id"
                id="user_id"
                placeholder="아이디 중복 확인을 완료하면 자동으로 입력됩니다."
              />
              <span>
                <button
                  type="button"
                  class="btn"
                  id="idCheckBtn"
                  onclick="openIdCheckPopup()"
                >
                  아이디 중복
                </button>
              </span>
            </span>
            <span id="alertid" class="alertmain"
              >아이디 형식을 다시 확인해주세요</span
            >
          </div>
          <div id="joinpw" class="form_area">
            <h3><label for="user_pw" class="icon">비밀번호</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="password"
                name="user_pw"
                id="user_pw"
                placeholder="영문/숫자 8자리 이상으로 입력해주세요"
              />
            </span>
            <span id="alertpw1" class="alertmain"
              >영문,숫자,특수문자를 포함하여 8글자 이상</span
            >
          </div>
          <div id="joinpw2" class="form_area">
            <h3><label for="user_pw2" class="icon">비밀번호 재확인</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="password"
                id="user_pw2"
                placeholder="비밀번호를 다시한번 입력해주세요"
              />
            </span>
            <span id="alertpw2" class="alertmain"
              >비밀번호가 일치하지 않습니다. 다시 입력해주세요</span
            >
          </div>
        </div>
        <div id="joininfo">
          <div id="joinname" class="form_area">
            <h3><label for="user_name" class="icon">이름</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="text"
                minlength="2"
                id="user_name"
                name="user_name"
                placeholder="이름을 입력해주세요"
              />
            </span>
            <span id="alertname" class="alertmain"
              >올바른 형식의 이름을 입력하세요</span
            >
          </div>
          <div id="joinnick" class="form_area">
            <h3><label for="user_nick" class="icon">닉네임</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="text"
                id="user_nick"
                name="user_nickname"
                placeholder="별명을 입력해주세요"
              />
            </span>
            <span id="alertnick" class="alertmain"
              >올바른 형식의 별명을 입력하세요</span
            >
          </div>
          <div id="joinbirth" class="form_area">
            <h3><label for="user_birth" class="icon">생년월일</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="date"
                id="user_birth"
                name="user_birth"
              />
            </span>
            <span id="alertbirth1" class="alertmain"
              >만 14세 미만은 가입이 불가능합니다. 나이먹고 다시오세요</span
            >
            <span id="alertbirth2" class="alertmain"
              >생년월일은 필수항목입니다.</span
            >
          </div>
          <div id="joingender" class="form_area">
            <h3><label for="user_gender" class="icon">성별</label></h3>
            <span class="box">
              <select id="user_gender" class="inbox" name="user_gender">
                <option value="default" checked>선택</option>
                <option value="남성">남성</option>
                <option value="여성">여성</option>
              </select>
              <span id="alertgender" class="alertmain"
                >필수 입력항목입니다. 성별을 선택해주세요</span
              >
            </span>
          </div>
        </div>
        <div id="joincontact">
          <div id="joinmobile" class="form_area">
            <h3><label for="user_mobile">휴대전화</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="text"
                id="user_mobile"
                name="user_mobile"
                placeholder="-을 제외한 번호만 입력해주세요"
              />
            </span>
            <span id="alertmobile" class="alertmain"
              >010으로 시작하는 "-"를 제외한 번호만 입력해주세요</span
            >
          </div>
          <div class="form_area">
            <h3><label for="user_email">이메일</label></h3>
            <span id="inputbox" class="box">
              <input
                class="inbox"
                type="text"
                id="user_email"
                name="user_email"
                placeholder="example@example.com"
              />
            </span>
            <span id="alertemail" class="alertmain"
              >이메일은 example@example.com 형식으로 입력해주세요</span
            >
          </div>
        </div>
        <div class="form_area5" id="bottom_area">
          <button type="submit" class="btn" id="confirmBtn">가입하기</button>
        </div>
      </form>
      <div id="footer_wrap">
        <div id="footer">
          <ul>
            <li>
              <a class="team_ob" href="#">TEAM : old boys</a>
            </li>
            <li>
              <a class="team_ob" href="#">Joo Hyung Kim</a>
            </li>
            <li>
              <a class="team_ob" href="#">Hyung Uoong Choi</a>
            </li>
            <li>
              <a class="team_ob" href="#">Sang Beom Hwang</a>
            </li>
            <li>
              <a class="personal" href="#">행복하자 우리</a>
            </li>
          </ul>
          <div id="footer_info">
            <h1 id="footer_logo">
              <img
                src="https://i.ibb.co/MpWgbfY/team-logo.png"
                alt="footer_logo"
              />
            </h1>
            <div id="team_info">
              <p>https://github.com/sangbeomhwang/board_old_boys.git</p>
              <p>
                1번 뇌섹남 : 김주형 / 2번 뇌섹남 : 최현웅 / 3번 뇌섹남 : 황상범
              </p>
              <p>old boys</p>
              <p class="copyright">
                Copyright © 2022. Kyungil Technical College. All rights
                reserved.
              </p>
            </div>
          </div>
        </div>
      </div>
    </div>
    <script src="/js/join.js" type-="text/javascript"></script>
  </body>
</html>

 

join.js

const valueId = document.getElementById("user_id");
const valuePw1 = document.getElementById("user_pw");
const valuePw2 = document.getElementById("user_pw2");
const valueName = document.getElementById("user_name");
const valueNick = document.getElementById("user_nick");
const valueBirth = document.getElementById("user_birth");
const valueGender = document.getElementById("user_gender");
const valueMobile = document.getElementById("user_mobile");
const valueEmail = document.getElementById("user_email");
const submitBtn = document.getElementById("join");

const alertPw1 = document.getElementById("alertpw1");
const alertPw2 = document.getElementById("alertpw2");
const alertName = document.getElementById("alertname");
const alertNick = document.getElementById("alertnick");
const alertBirth1 = document.getElementById("alertbirth1");
const alertBirth2 = document.getElementById("alertbirth2");
const alertGender = document.getElementById("alertgender");
const alertMobile = document.getElementById("alertmobile");
const alertEmail = document.getElementById("alertemail");

const submitJoinBtn = document.getElementById("confirmBtn");

function openIdCheckPopup() {
  window.open(
    "idcheck",
    "Popup Window",
    "height=300, width=400, top=300, left=600"
  );
}

// ID입력값이 없으면 이벤트 발동불가
// Value나 Length 조건부여시, 공백 및 -1 로 표시되기때문에, Trim 으로 value 공백 제거 하여 "" 와 비교할 수 있도록 지역변수 선언

submitBtn.addEventListener("submit", function (event) {
  const valueIdValue = valueId.value.trim();
  if (valueIdValue === "") {
    event.preventDefault();
  }
});

// 정규표현식 문자 정의
// Contains at least one English uppercase letter (?=.*[A-Z])
// Contains at least one English lowercase letter (?=.*[a-z])
// Contains at least one number (?=.*\d)
// Contains at least one special character (?=.*[$@$!%*?&])
// Contains at least one Chinese character (?=.*[\u4E00-\u9FA5])
// Has a minimum length of 8 characters and maximum length of 20 characters ({8,20})

// 비밀번호 정규표현식 조건 만족
// 구현기능
// 1. valuePw1 의 값이 영어,대소문자,숫자를 포함하여 최소 8글자 및 20글자 이내 암호 입력
// 2. 불일치할 경우, alertpw1 HTML style 속성 부여
// 3. Submit Button 조건에 따라 활성화 비활성화 결정

submitBtn.addEventListener("submit", function (event) {
  const isValid =
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[$@$!%*?&])[a-zA-Z\d$@$!%*?&]{8,20}$/.test(
      value
    );
  if (!isValid) {
    event.preventDefault();
  }
});

valuePw1.addEventListener("keyup", function (event) {
  const value = valuePw1.value;
  const isValid =
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[$@$!%*?&])[a-zA-Z\d$@$!%*?&]{8,20}$/.test(
      value
    );

  if (!isValid) {
    alertPw1.style.display = "block";
  } else {
    alertPw1.style.display = "none";
  }
});

//비밀번호 Part 2
// 구현기능
// 1. valuePw1 과 valuePw2 의 값이 다를 경우, 불일치 HTML Style 속성 부여
// 2. Submit Button 조건에 따라 활성화 비활성화 결정
// ** ValuePw 1 과 2의 순서가 중요,
// 1이 먼저일 경우, 마지막에 입력된 값이 1이여야 조건이 완성, 2가 먼저오면 2가 마지막으로 입력된 값에 따라 조건 완성 확인

valuePw2.addEventListener("keyup", function (event) {
  const valuePw1Value = valuePw1.value;
  const valuePw2Value = valuePw2.value;

  if (valuePw1Value === valuePw2Value) {
    alertPw2.style.display = "none";
  } else {
    alertPw2.style.display = "block";
  }
});

submitBtn.addEventListener("submit", function (event) {
  const valuePw1Value = valuePw1.value;
  const valuePw2Value = valuePw2.value;

  if (valuePw2Value !== valuePw1Value) {
    event.preventDefault();
  }
});

//Name&Nick Area
//Length 점 표기법으로 아래와 같이 표기해야 나타남
//Name 은 String Type 이기때문.

valueName.addEventListener("keyup", function (event) {
  const valueNameLength = valueName.value.length;

  if (valueNameLength < 2) {
    alertName.style.display = "block";
  } else {
    alertName.style.display = "none";
  }
});

submitBtn.addEventListener("submit", function (event) {
  const valueNameLength = valueName.value.length;

  if (valueNameLength < 2) {
    event.preventDefault();
  }
});

valueNick.addEventListener("keyup", function (event) {
  const valueNickLength = valueNick.value.length;

  if (valueNickLength < 2) {
    alertNick.style.display = "block";
  } else {
    alertNick.style.display = "none";
  }
});

submitBtn.addEventListener("submit", function (event) {
  const valueNickLength = valueNick.value.length;

  if (valueNickLength < 2) {
    event.preventDefault();
  }
});

//Birth & Gender Area

valueBirth.addEventListener("change", function (event) {
  const valueBirthValue = valueBirth.value;
  const birthDate = new Date(valueBirthValue);
  const minDate = new Date("2007-01-01");

  if (birthDate > minDate) {
    alertBirth1.style.display = "block";
  } else {
    alertBirth1.style.display = "none";
  }

  if (valueBirthValue === null || valueBirthValue === undefined) {
    alertBirth2.style.display = "block";
  } else {
    alertBirth2.style.display = "none";
  }
});

submitBtn.addEventListener("submit", function (event) {
  const valueBirthValue = valueBirth.value;
  const birthDate = new Date(valueBirthValue);
  const minDate = new Date("2007-01-01");

  if (birthDate > minDate) {
    event.preventDefault();
  }

  if (valueBirthValue === null || valueBirthValue === undefined) {
    event.preventDefault();
  }
});

valueGender.addEventListener("select", function (event) {
  const valueGenderValue = valueGender.value;

  if (valueGenderValue === "남성" || valueGenderValue === "여성") {
    alertGender.style.display = "none";
  } else if (valueGenderValue === "선택") {
    alertGender.style.display = "block";
  }
});

submitBtn.addEventListener("submit", function (event) {
  const valueGenderValue = valueGender.value;

  if (valueGenderValue === "선택") {
    event.preventDefault();
  }
});

//휴대전화 Area
// startsWith("value") 는 시작 문자를 지정.
// 만약 if 문 표현시, 아래와 같은 상황일때는 조건앞 괄호에 !를 부여하여 거짓조건을 부여할수 있음.

valueMobile.addEventListener("keyup", function (event) {
  if (valueMobile.value.startsWith("010") && valueMobile.value.length === 11) {
    alertMobile.style.display = "none";
  } else {
    alertMobile.style.display = "block";
  }
});

submitBtn.addEventListener("submit", function (event) {
  if (
    !(valueMobile.value.startsWith("010") && valueMobile.value.length === 11)
  ) {
    event.preventDefault();
  }
});

//이메일 Area
// 정규표현식 사용
// 휴대전화와 마찬가지로 If문에 !로 감싸 반대로 표현하여 코드 간소화

valueEmail.addEventListener("keyup", function (event) {
  const emailRegex =
    /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

  if (emailRegex.test(valueEmail.value)) {
    alertEmail.style.display = "none";
  } else {
    alertEmail.style.display = "block";
  }
});

submitBtn.addEventListener("submit", function (event) {
  const emailRegex =
    /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

  if (!emailRegex.test(valueEmail.value)) {
    event.preventDefault();
  }
});

 

 

(2) view 페이지의 좋아요(추천) 중복체크 가능 이슈

좋아요(추천)의 경우, 단순히 좋아요 버튼을 클릭할 때마다 1씩 증가하게 하는 것은 SQL query 문을 사용하여 쉽게 구현하였다.

하지만 좋아요를 ID별로 좋아요 버튼을 클릭할 때마다 중복하여 증가하게 하는 것은 실제 사이트들과 비교했을 때

부족한 부분이라고 판단되었다.

 

이에 팀원 분들과 함께 고민하여 SQL query 문을 활용하여 기존 좋아요 count를 삭제(DELETE 문 활용)하고,

새로 좋아요 count를 증가(INSERT INTO 문 활용)시키도록 구문을 작성하여 해당 중복체크 가능 이슈를 해결하였다.

 

board.repository.js

exports.addLike = async (likeInfo) => {
  const { user_id, boardIdx } = likeInfo
  const values = [user_id, boardIdx];
  const dropLike = `DELETE FROM likes WHERE user_id = ? AND boardIdx = ?`;
  await pool.query(dropLike, values);
  const addLike = `INSERT INTO likes(user_id, boardIdx) 
  values('${user_id}', '${boardIdx}');`;
  await pool.query(addLike);
}

 

board.service.js

exports.postLike = async (likeInfo) => {
  return await board.addLike(likeInfo)
}

 

board.controller.js

exports.postLike = async (req, res) => {
  console.log('req.body :',req.body);
  await boardService.postLike(req.body);
  res.redirect(`/board/view?index=${req.body.boardIdx}`);
}
exports.getWrite = (req, res) => {
  res.render("board/write.html", { token: req.cookies.token });
};

 

 

 

1-4) 후기

이번 프로젝트를 계기로 내가 route, express 모듈,

MVC 패턴(controller, service, repository) 등에서 어느 부분이

부족한지 그리고 어떤 부분을 잘못 이해하고 있었는지를 확인할 수 있었다.

이와 더불어 기존에 코드로 직접 구현해보지 않고 머릿속으로만

이러면 되겠지하고 넘겼던 생각들이 실제로는 어떤 이슈를

발생시킬 수 있는지 한 번 더 눈으로 실감하게 되었다.

현재 나는 정적 페이지의 route 간 요청/응답을 주고 받음으로써

페이지 간 관계 정의를 해내는 영역까지는 도달했지만

아직 이 과정에서 필요 데이터를 넘기거나 받을 시 해당 데이터의

타입을 즉각적으로 판단하는 것에 있어서는 미숙한 부분이 있는 것으로

판단되었다.

이에 앞으로도 다양한 아이디어를 직접 구현하고

여러 이슈를 맞닥뜨림으로써 이후 Ajax 등을 기반으로

향후 심화 프로젝트를 진행함에 있어 반드시 필요한 데이터 이해 및 조작 역량을

증진시키기 위해 열심히 노력할 계획이다.

그럼 지금까지 글을 읽어주신 모든 개발자 분들, 혹은 개발에 관심이 있는

학생 분들 모두 파이팅해서 각자 목표로 하는 바를 꼭 이루시길 바래요~