[내일배움캠프 - 노드 심화주차] 프로젝트 회고

sooyoung choi·2023년 12월 18일
0

내일배움캠프

목록 보기
5/19
post-thumbnail

👉 2거 주세요

아웃소싱 팀 프로젝트 - 음식 배달 서비스 구현하기

내가 맡았던 부분 중 기록하고 싶은 것들을 기록해놓으려 한다.

1. 소개

내일배움캠프 Node.js 심화주차 팀프로젝트입니다.

😺 https://github.com/choisooyoung-dev/this-one-please

팀명2거주세요
팀장최수영
팀원강다형, 고병옥, 신정선, 최연식

2. 사용 기술, 라이브러리

  • Node.js
  • Express
  • MySQL
  • Prisma
  • Redis
  • Cloud Type Server

나는 처음으로 Redis, refreshtoken 부분을 구현해보았다.

3. 프로젝트 소개

  • 음식 배달 서비스를 구현하는 아웃소싱 프로젝트

메인 페이지

로그인 전

  • 상단 네브바에 로그인 버튼
  • 매장 검색 기능
  • 카테고리 별 식당 분리

로그인 후

  • 현재 잔액과 마이페이지, 카트, 로그아웃 버튼을 보여준다.
  • access token과 refresh token을 발급해준다.
  • 이때 refresh token은 redis에 user의 아이디 값과 같이 저장해준다.
// 레디스 연결 redis.util.js

import redis from 'redis';
import dotenv from 'dotenv';
dotenv.config();

const redisClient = redis.createClient({
  host: process.env.REDIS_HOST, // 여기에 원하는 호스트를 설정하세요
  port:  process.env.REDIS_PORT,
  password: process.env.REDIS_PW,
  username: default
});

redisClient.on('connect', () => console.log('Connected to Redis!'));
redisClient.on('error', (err) => console.log('Redis Client Error', err));
redisClient.connect();
// auth controller

import redisClient from '../../../auth-utils/redis.util.js';
import { AuthService } from './auth.service.js';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import { loginSchemaValidation } from '../../middlewares/validation.middleware.js';
dotenv.config();

export class AuthController {
    authService = new AuthService();

    login = async (req, res, next) => {
        try {
            const { email, password } = await loginSchemaValidation.validateAsync(req.body);

            const user = await this.authService.login(email, password);

            if (!user) {
                return res.status(400).json({ success: false, error: '가입되어있지 않은 계정입니다. 회원가입 해주세요.' });
            }

            const user_id = user.id;

            if (email !== user.email) {
                return res.status(400).json({ success: false, error: '가입되어있지 않은 계정입니다. 회원가입 해주세요.' });
            }

            if (password !== user.password) {
                return res.status(401).json({ success: false, error: '비밀번호가 일치하지 않습니다.' });
            }

            const accessToken = jwt.sign({ user_id }, process.env.ACC_TOKEN_KEY, {
                expiresIn: process.env.ACCESS_EXP_IN,
            });
            const refreshToken = jwt.sign({ user_id }, process.env.REF_TOKEN_KEY, {
                expiresIn: process.env.REFRESH_EXP_IN,
            });

            // Accesstoken 쿠키 저장
            res.cookie('accessToken', accessToken);
            // Refreshtoken redis 저장 (key, value)
            redisClient.set(refreshToken, user_id);
            res.cookie('refreshToken', refreshToken);

            return res.status(200).json({
                success: true,
                message: '로그인 되었습니다.',
                data: { accessToken, refreshToken },
            });
        } catch (error) {
            console.log(error);
            next(error);
        }
    };

    logout = async (req, res, next) => {
        try {
            // AccessToken 및 RefreshToken 변수 선언
            const accessToken = req.cookies.accessToken;
            const refreshToken = req.cookies.refreshToken;

            res.clearCookie('accessToken');
            res.clearCookie('refreshToken');

            // Redis에서 키 삭제
            redisClient.del(refreshToken);
            redisClient.del(accessToken);

            return res.status(200).json({
                success: true,
                message: '로그아웃 성공',
            });
        } catch (err) {
            next(err);
        }
    };
}

로그인 버튼 누른 후 (로그인 페이지로 이동)

  • 아이디, 비밀번호 빈 값 체크 후 입력 유도하기 위해 경고창 띄어줌

3. 회원가입 페이지

  • 이메일 인증 기능

    • 노드메일러 사용
    • 인증 번호는 redis에 넣고 전송된 후 3분 뒤 그리고 인증이 되면 만료된다.
  • 빈값 체크

  • 프론트에서 값을 넘겨줘야 하기 때문에 fetch를 썼다.

  • 각 버튼마다 라우터로 가서 해야할 일을 하고 해당 페이지로 돌아오게끔 설계하였다.

...

// 메일 보내주는 로직
const sendEmail = async () => {
    const email = document.getElementById('email').value;

    try {
        const response = await fetch('/api/users/signup/send-mail', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ email }),
        });
        const data = await response.json();
        // console.log(data);

        if (data.error) {
            alert(data.error);
            window.location.href = '/login';
        }
        if (data.message) {
            alert(data.message);
        }
    } catch (error) {
        console.log('Error!', error);
    }
};


// 메일 인증 로직
const authEmail = async () => {
    const authEmailNum = document.getElementById('email-verification').value;

    try {
        const response = await fetch('/api/users/signup/verify-mail', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ authEmailNum }),
        });

        const data = await response.json();
        console.log(data);

        if (data.error) {
            alert(data.error);
        } else if (data.message) {
            signupBtn.removeAttribute('disabled');
            signupBtn.classList.add('hover:bg-blue-700');
            emailBtn.setAttribute('disabled', '');
            authMailBtn.setAttribute('disabled', '');
            // console.log('signupBtn: ', signupBtn);
            alert(data.message);
        } else {
            alert('인증 번호 확인에 실패했습니다.');
        }
    } catch (error) {
        console.log('에러 발생', error);
    }
};

// 회원가입 로직
const signup = async () => {
    const email = document.getElementById('email').value;
    const password = document.getElementById('password').value;
    const confirmPassword = document.getElementById('password-confirm').value;
    const name = document.getElementById('nickname').value;
    const userType = document.querySelector('input[name="userType"]:checked').value;
    const address = document.getElementById('address').value;
    let type = 0;
    try {
        if (userType === 'business') {
            type = 1;
        }
        const response = await fetch('/api/users/signup', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                email,
                password,
                confirmPassword,
                name,
                type,
                address,
            }),
        });
        const data = await response.json();
        console.log(data);

        if (data.error) {
            alert(data.error);
        } else if (data.message) {
            alert(data.message);
            window.location.href = '/';
        }
    } catch (error) {
        console.log('에러 발생', error);
    }
};
  • 컨트롤러에서의 로직

export class UsersController {
    usersService = new UsersService();

    // 회원 가입 시 인증 이메일 보내기
    sendEmail = async (req, res, next) => {
        const { email } = req.body;

        const userEmail = await this.usersService.getUserEmail(email);
        console.log('userEmail: ', userEmail);

        if (userEmail) {
            return res.status(400).json({
                success: false,
                error: '이미 등록되어있는 이메일 입니다. 로그인 해주세요',
            });
        }

        try {
            const transporter = nodemailer.createTransport({
                service: 'gmail', // gmail 사용
                auth: {
                    user: process.env.MAILS_EMAIL, // env 파일 내 보내는 사람의 메일 주소
                    pass: process.env.MAILS_PWD, // env 파일 내 생성된 앱 비밀번호 16자리
                },
            });

            // 랜덤 인증번호 생성 함수
            const randomStrFunc = (num) => {
                const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
                let result = '';
                const randomMaxLength = characters.length;
                for (let i = 0; i < num; i++) {
                    result += characters.charAt(Math.floor(Math.random() * randomMaxLength));
                }

                return result;
            };

            let randomStr = randomStrFunc(10);
            redisClient.set(randomStr, randomStr);
            if (!email) {
                return res.status(400).json({ success: false, error: '이메일을 입력해주세요.' });
            }
            async function main() {
                await transporter.sendMail({
                    from: process.env.MAILS_EMAIL, // env 파일 내 보내는 사람의 메일 주소
                    to: email, // 받는 사람
                    subject: '👋 2거주세요 가입 인증번호입니다.', // 제목
                    text: `인증번호는 ${randomStr} 입니다. 3분 내로 입력해주세요.`, // 메일 내용
                    // html: "<b>Hello world?</b>", // html 보내줄 수도 있음
                });
                await redisClient.expire(randomStr, 180);
            }
            await main();

            res.status(200).json({
                success: true,
                message: '입력해주신 이메일 주소로 전송되었습니다. 확인해주세요.',
            });

            res.redirect('/');
        } catch (error) {
            console.log(error);
            next(error);
        }
    };

    // 회원 가입 시 인증 번호 검증
    verifyEmail = async (req, res, next) => {
        try {
            const { authEmailNum } = req.body;

            const redisAuthEmailNum = await redisClient.get(authEmailNum);

            if (!redisAuthEmailNum) {
                res.status(400).json({
                    success: false,
                    error: '일치하지 않은 인증번호 입니다. 다시 입력해주세요.',
                });
            } else {
                redisClient.del(redisAuthEmailNum);
                res.status(200).json({
                    success: true,
                    message: '인증되었습니다. 가입을 진행해주세요.',
                });
            }
            res.redirect('/');
        } catch (error) {
            console.log(error);
            next(error);
        }
    };

    // 회원 가입
    signup = async (req, res, next) => {
        try {
            const { email, password, name, type, address, confirmPassword } = req.body;
            // 회원가입 입력 폼에서 받아 온 데이터들로 변경
            // const { email, name, type, address, password, confirmPassword } =
            //     res.data;

            if (password !== confirmPassword) {
                return res.status(400).json({
                    success: false,
                    error: '비밀번호와 확인 비밀번호가 일치하지 않습니다. 다시 적어주세요.',
                });
            } else {
                await this.usersService.signup(email, password, name, type, address);
                res.status(201).json({
                    success: true,
                    data: { email, password, name, type, address },
                    message: '회원가입이 되었습니다. 로그인 해주세요.',
                });
            }
        } catch (error) {
            next(error);
        }
    };

...

4. 검색 기능


  • url 쿼리스트링으로 검색값을 메인페이지에서 받아와 데이터베이스에 저장되어있는 값과 비교후 보여준다.
const container = document.getElementById(`store-container`);

// url 파라미터를 가져와준다.
const urlParams = new URLSearchParams(window.location.search);
const searchInputValue = urlParams.get('searchInputValue');

document.addEventListener('DOMContentLoaded', (e) => {
    e.preventDefault();
    searchStore();
});

const searchStore = async () => {
    try {
        const response = await fetch('/api/stores', {
            method: 'GET',
        });
        const data = await response.json();
        const stores = data.data;
        const findStoresNameArr = [];
        const matchingStores = stores.filter((store) => store.name.includes(searchInputValue));

        if (matchingStores.length > 0) {
            // Display the matching stores
            findStoresNameArr.push(...matchingStores);
        }
        findStoresNameArr.forEach((store) => {
            const newItem = document.createElement('a');
            newItem.href = `/store/${store.id}`;
            container.appendChild(newItem);
       		...

            const image = document.createElement('img');
            image.src = `${store.image_url}`;
            image.className = 'w-full h-full object-cover';
            cdev4.appendChild(image);

         	 ...
            const storeName = document.createElement('label');
            storeName.className = 'block text-sm font-medium text-gray-700';
            storeName.innerText = `${store.name}`;
            cdev6.appendChild(storeName);

            const storeAddress = document.createElement('label');
            storeAddress.className = 'mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm';
            storeAddress.innerText = `${store.address}`;
            cdev6.appendChild(storeAddress);
        });
    } catch (error) {
        console.log('Error!', error);
    }
};

5. 스토어 화면

  • 원하는 가게 클릭하면 메뉴와 리뷰를 볼 수 있는 화면 보고있나 헤뻐치 ㅋ

  • 장바구니 추가
    • 다른 가게 메뉴 담으면 해당 가게 메뉴 삭제 후 추가됨

이번 프로젝트 23년 12월 12일 ~ 23년 12월 18일 간 진행하는거였는데 정말 촉박했지만, 피드백은 좋았던 편(팀원들을 정말 잘 만났다..)
팀장을 맡긴 했지만 1인분을 못한 느낌이니 다음번엔 2인분 하도록 노력해야지 냅다 달려~
같이 노력해준 팀원분들에게 감사하다 ㅠㅠ

1개의 댓글

comment-user-thumbnail
2023년 12월 19일

너무잘봤어요!
코드는 제가 이해를 못하겠지만 언젠가 공부하다가
문득 이글을 다시 보러왔을때 이해할 수 있는 제가됐으면 좋겠어요
함께해서 너무 고맙고 감사했습니다

답글 달기