<repo_name>devjindev/our-emotions<file_sep>/passport/kakaoStrategy.js
//* passport(로그인) - 카카오 로그인 전략
// 처음 로그인할 때는 회원가입 처리 필요, 두 번째 로그인부터는 회원가입 처리 필요 X (바로 로그인 처리)

'use strict';

//! 패키지(미들웨어) 참조
const KakaoStrategy = require('passport-kakao').Strategy; // 카카오 로그인 전략

const { User } = require('../models'); // user DB

//! 모듈 생성
module.exports = (passport) => {
  passport.use(new KakaoStrategy({ // 전략 설정
    clientID: process.env.KAKAO_ID, // 카카오(에서 발급해주는) ID // 비밀키
    callbackURL: '/auth/kakao/callback', // 카카오(에서 주는) 인증 결과를 받을 라우터 주소
  },
  // routes/auth.js 로그인 라우터 passport.authenticated의 done() 콜백함수로 전달 
  async (accessToken, refreshToken, profile, done) => { // 전략 수행 // 위에서 accessToken, refreshToken, profile 받음
    try { // 성공하면
      const exUser = await User.findOne({ where: { snsId: profile.id, provider: 'kakao' } }); // DB 중 입력된 profile.id가 있는 사용자 정보 찾음
      if (exUser) { // 🟠 exUser가 있으면 (등록된 profile.id이면) => 첫 번째 로그인 이후
        done(null, exUser); // null: 서버 에러 발생 시 사용 // 사용자 정보 전달
      } else { //  🟠 exUser가 없으면 => 첫 번째 로그인 (회원가입 필요)
        const newUser = await User.create({  // 새로운 user DB 로우(객체) 생성
          // profile
          email: profile._json && profile._json.kaccount_email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser); // 새로운 사용자 정보 전달
      }
    } catch (error) { // 실패하면
      console.error(error);
      done(error); // 서버 에러
    }
  }));
};
<file_sep>/routes/user.js
//* router - 팔로우하기, 팔로우끊기

'use strict';

//! 모듈 참조 - 패키지(미들웨어)
const express = require('express');

const { isLoggedIn } = require('./middlewares'); // 로그인 한 상태
const { User } = require('../models'); // user DB

//! 라우터 객체 생성
const router = express.Router();

//? 팔로우하기
router.post('/:id/follow', isLoggedIn, async (req, res, next) => { // /:id/follow로 post(등록) 요청하면 // 로그인 한 상태
  try { // 성공하면
    const user = await User.find({ where: { id: req.user.id } }); // user DB 중 팔로우할 user id가 있는 user 찾음
    await user.addFollowing(parseInt(req.params.id, 10)); // 찾은 user에 팔로우 하기 // user : 팔로우 할 user 관계를 Follow에 넣음 // 팔로우할 user id 받아와서 정수(10진수)로 변환
    res.send('success'); // 요청 전송
  } catch (error) { // 실패하면
    console.error(error);
    next(error);
  }
});

//? 팔로우끊기
router.post('/:id/unfollow', isLoggedIn, async (req, res, next) => { // /:id/follow로 post(등록) 요청하면 // 로그인 한 상태
  try { // 성공하면
    const user = await User.find({ where: { id: req.user.id } }); // user DB 중 팔로우할 user id가 있는 user 찾음
    await user.addFollowing(parseInt(req.params.id, 10)); // 찾은 user에 팔로우 하기 // user : 팔로우 할 user 관계를 Follow에 넣음 // 팔로우할 user id 받아와서 정수(10진수)로 변환
    res.send('success'); // 요청 전송
  } catch (error) { // 실패하면
    console.error(error);
    next(error);
  }
});

//? 닉네임 변경하기
router.post('/profile', async(req, res, next) => { // /profile로 post(등록) 요청하면
  try{ // 성공하면
    await User.update({nick: req.body.nick}, { // user DB 수정 // 닉네임을 입력받은 닉네임으로 수정
      where: {id: req.user.id} // 입력 받은 ID(email) 에서
    });
    res.redirect('/profile'); // /profile로 페이지 이동
  }catch(error){ // 실패하면
    console.error(error);
    next(error);
  }
});

//! router 모듈 생성
module.exports = router;
<file_sep>/views/error.html
<!--* html - error (에러 내역) -->

{% extends 'layout.html' %}

{% block content %}
  <section class="errorArea">
    <h2>{{message}}</h2>
    <h3>{{error.status}}</h3>
    <pre>{{error.stack}}</pre>
  </section>
{% endblock %}<file_sep>/passport/index.js
//* passport(로그인) - serialize, deserialize

'use strict';

//! 파일 참조
const local = require('./localStrategy'); // 로컬 로그인 전략
const kakao = require('./kakaoStrategy'); // 카카오 로그인 전략
const { User } = require('../models'); // DB

//app.use(passport.session()); // 요청(로그인) 인증 // req.session 객체에 passport 정보를 저장함

//! 모듈 생성
module.exports = (passport) => {
  // routes/auth.js 로그인 라우터 passport.authenticated의 done() 콜백함수로 전달
  // done() : 로그인 시 사용
  //? 왜하냐? 세션에 사용자 정보를 모두 저장하면 용량이 커지므로 아이디만 저장하려고
  
  // req.session 객체에 어떤 데이터(passport 정보)를 저장할지 선택
  // 사용자 정보 객체를 세션에 ID로 저장
  passport.serializeUser((user, done) => { // 사용자 정보를 받음
    done(null, user.id); // null: 서버 에러 발생 시 사용 // 사용자 ID 전달
  });
  
  // 위에서 저장된 사용자 ID를 받아 DB에서 사용자 정보 조회
  // 세션에 저장한 ID를 통해 사용자 정보 객체를 불러옴
  passport.deserializeUser((id, done) => { // 위에서 사용자 ID를 받음
    User.findOne({ // user DB 중 받은 사용자 ID가 있는 사용자 정보 찾음
      where: { id },
      include: [ // 아래사항 포함
        {
          model: User, // user DB의
          attributes: ['id', 'nick'], // ID, 닉네임
          as: 'Followers', // 팔로워
        }, 
        {
          model: User, // user DB의
          attributes: ['id', 'nick'], // ID, 닉네임
          as: 'Followings', // 팔로잉
        }
      ],
    })
      .then(user => done(null, user)) // 성공하면 사용자 정보 전달
      .catch(err => done(err));  // 실패하면 에러 처리 (서버 에러)
  });

  //! 메소드 호출
  local(passport); // 로컬 로그인 전략
  kakao(passport); // 카카오 로그인 전략
};
<file_sep>/routes/post.js
//* router - post(게시글 업로드)
// single(하나의 이미지 있는 게시글 업로드), none(이미지 없는 게시글 업로드)

'use strict';

//! 모듈 참조 - 패키지(미들웨어)
const express = require('express');
const multer = require('multer'); // 파일 업로드
const path = require('path'); // 파일 및 폴더 경로 쉽게 조작
const fs = require('fs'); // 파일 및 폴더 CRUD

//! 모듈 참조 - 파일
const { Post, Hashtag, User } = require('../models'); // post, hashtag, user DB
const { isLoggedIn } = require('./middlewares'); // 로그인 한 상태

//! 라우터 객체 생성
const router = express.Router();

// 폴더 Read(읽기)
fs.readdir('uploads', (error) => { // uploads 폴더 읽음
  if (error) { // 에러 있으면 (uploads(이미지 업로드 할) 폴더 없으면)
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
    fs.mkdirSync('uploads'); // 폴더 Create(생성)
  }
});

//? 이미지 있는 게시글 업로드 미들웨어 객체
// cb(): 파일 업로드 시 사용
const upload = multer({
  // 이미지 업로드 옵션 설정
  storage: multer.diskStorage({ // 파일 저장 방식: 서버 디스크
    destination(req, file, cb) { // 파일 저장 경로(폴더): uploads/
      cb(null, 'uploads/'); // null: 서버 에러 발생 시 사용
    },
    filename(req, file, cb) { // 파일명: 기존 파일명 + 업로드 날짜 + 기존 확장자
      const ext = path.extname(file.originalname); // 기존 확장자
      cb(null, path.basename(file.originalname, ext) + new Date().valueOf() + ext);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 }, // 최대 이미지 파일 용량 허용치 (10MB)
});

//? 이미지 있는 게시글 업로드 라우터
// single: 하나의 이미지 있는 게시글 업로드할 때 사용 (이미지 => req.file, 나머지 정보 => req.body)
// single(req.body 속성)
router.post('/img', isLoggedIn, upload.single('img'), (req, res) => { // /img로 post(등록) 요청하면 // 로그인 한 상태
  console.log(req.file);
  res.json({ url: `/img/${req.file.filename}` }); // url 요청 전송
});

//? 이미지 없는 게시글 업로드 미들웨어 객체
const upload2 = multer();

//? 이미지 없는 게시글 업로드 라우터
// none: 이미지 없는 게시글 업로드할 때 사용 (모든 정보 => req.body)
router.post('/', isLoggedIn, upload2.none(), async (req, res, next) => { // /로 post(등록) 요청하면 // 로그인 한 상태
  try { // 성공하면
    const post = await Post.create({ // 새로운 post DB 로우 생성 (저장)
      content: req.body.content, // 게시글 내용
      img: req.body.url, // 게시글 이미지(이미지 데이터 온게 아니라 주소 온거)
      userId: req.user.id, // 게시글 작성자 ID
    });
    const hashtags = req.body.content.match(/#[^\s]*/g); // 게시글 내용 내 해시태그 추출
    if (hashtags) { // 해시태그 있으면
      const result = await Promise.all(hashtags.map(tag => Hashtag.findOrCreate({ // hashtag DB에 저장 // 배열 모두 한 번에 저장 // 새로운 배열 map
        where: { title: tag.slice(1).toLowerCase() }, // title: tag명에서 맨 첫번째꺼(#) 삭제, 모두 소문자로 해시태그 재설정 (에서)
      })));
      await post.addHashtags(result.map(r => r[0])); // post : hashtag 관계를 PostHashtag에 넣음
    }
    res.redirect('/'); // /home으로 페이지 이동
  } catch (error) { // 실패하면
    console.error(error);
    next(error);
  }
});

//? 게시글 삭제 라우터
router.delete('/:id', async(req, res, next) => { // /:id로 delete 요청하면
  try{ // 성공하면
    await Post.destroy({where: {id: req.params.id, userId: req.user.id}}); // post DB 중 삭제할 게시글 id와 삭제할 게시글 user id 찾아서 삭제
    res.send('OK'); // 요청 전송
  }catch(error){ // 실패하면
    console.error(error);
    return next(error);
  }
});

//? 해시태그 검색 라우터
router.get('/hashtag', async (req, res, next) => { // /hashtag로 get 요청하면
  const query = req.query.hashtag; // 입력된 해시태그명(문자열) 받아옴
  if (!query) { // 해시태그명 없으면 (빈 문자열이면)
    return res.redirect('/'); // /home으로 페이지 이동(돌려보냄)
  }
  try { // 성공하면
    const hashtag = await Hashtag.findOne({ where: { title: query } }); // hashtag DB 중 입력받은 해시태그명 있는거 찾음
    let posts = []; // 게시글들
    if (hashtag) { // 해시태그명 있으면 (등록된 해시태그명이면)
      posts = await hashtag.getPosts({ include: [{ model: User }] }); // 해시태그명 포함한 게시글들 가져옴 (user DB 포함)
    }
    return res.render('main', { // 응답 (렌더링)
      title: `${query} | 우리의 감성`,
      user: req.user,
      twits: posts, // twits에 해시태그명 포함한 게시글들 넣음
    });
  } catch (error) { // 실패하면
    console.error(error);
    return next(error);
  }
});

//? 좋아요 라우터
// 좋아요
router.post('/:id/like', async(req, res, next) => { // /:id/like로 post(등록) 요청하면
  try{ // 성공하면
    const post = await Post.findOne({where: {id: req.params.id}}); // post DB 중 좋아요한 post id가 있는 post 찾음
    await post.addLiker(req.user.id); // 찾은 post에 좋아요 누른 user id 추가
    res.send('OK'); // 요청 전송
  }catch (error) { // 실패하면
    console.error(error);
    return next(error);
  }
});
// 좋아요 취소
//! router 모듈 생성
router.delete('/:id/like', async(req, res, next) => { // /:id/like로 delete(삭제) 요청하면
  try{ // 성공하면
    const post = await Post.findOne({where: {id: req.params.id}}); // post DB 중 좋아요 취소할 post id가 있는 post 찾음
    await post.removeLiker(req.params.id); // 찾은 post에 좋아요 삭제
    res.send('OK'); // 요청 전송
  }catch (error) { // 실패하면
    console.error(error);
    return next(error);
  }
});
module.exports = router;
<file_sep>/public/js/main.js
//? header
window.addEventListener('load', () => { // 윈도우 로드되면
    setTimeout(() => { // 1초 뒤에 body에 'before-start' class 삭제
        document.body.classList.remove('before-start');
    }, 1000);
});

//? hambuger
const hambuger = document.querySelector('.hambuger');
const profile = document.querySelector('.leftSide');
if(hambuger){
    hambuger.addEventListener('click', () => { // hambuger click 하면
        profile.classList.toggle('hambuger-click'); // leftside에 'hambuger-click' class toggle
    });
}


//* /views/write.html (게시글 쓰기)

//? img
if (document.getElementById('img')) { // #img
    // id에 img 있는 것을 change(click...)하면
    document.getElementById('img').addEventListener('change', function (e) {
        var formData = new FormData(); // fromData 객체 생성 // form 태그(input) 데이터를 동적으로 제어
        console.log(this, this.files); // 이미지 파일들 출력
        formData.append('img', this.files[0]); // fromData.append(key, value) // 이미지 파일들 하나씩 저장
        var xhr = new XMLHttpRequest(); // AJAX(=> JSON 사용) 객체 생성
        xhr.onload = function () { // 요청
            if (xhr.status === 200) { // 상태 코드가 200이면 (성공)
            var url = JSON.parse(xhr.responseText).url; // 응답 메세지를 object -> JOSN // JSON url 객체 생성
            document.getElementById('img-url').value = url; // #img-url.value(값) = JSON url
            document.getElementById('img-preview').src = url; // #img-preview.src(경로) = JSON url
            document.getElementById('img-preview').style.display = 'inline'; // #img-preview.style.display = inline
            } else { // 그 외면 (실패)
                console.error(xhr.responseText); // error 출력
            }
        };
        xhr.open('POST', '/post/img'); // 요청 설정 // HTTP 요청 메서드(JSON 사용) : POST(등록), 주소 : /post/img
        xhr.send(formData); // 요청(from 태그 데이터) 전송
    });
}

//* /views/main.html (게시글 목록)

//? 팔로잉
document.querySelectorAll('.follow').forEach(function (tag) { // 팔로우하기 버튼들 배열로 생성해서 모두 선택 // 하나씩 가져와서 tag에 담음 (반복)
    tag.addEventListener('click', function () { // 팔로우하기 버튼 클릭 하면
        var isLoggedIn = document.querySelector('#my-id'); // user의 id
        if (isLoggedIn) { // user의 id가 있으면 (로그인 한 상태이면)
            if(tag.parentNode.querySelector('#twit-user-id')){
                var twitUserId = tag.parentNode.querySelector('#twit-user-id').value; // 팔로우하기 버튼이 있는 게시글 작성자의 id value
            }
            var userId = isLoggedIn.value; // user의 id value
            // if(twitUserId !== undefined){
                if (twitUserId !== userId) { // 게시글 작성자 ID != 사용자 ID
                    if (confirm('팔로잉 하시겠습니까?')) { // 확인창 출력
                        var xhr = new XMLHttpRequest(); // AJAX(=> JSON 사용) 객체 생성
                        xhr.onload = function () { // 요청
                            if (xhr.status === 200) { // 상태 코드가 200이면 (성공)
                                location.reload(); // 새로고침
                            } else { // 그 외면 (실패)
                                console.error(xhr.responseText); // error 출력
                            }
                        };
                        xhr.open('POST', '/user/' + twitUserId + '/follow'); // 요청 설정 // HTTP 요청 메서드(JSON 사용) : POST(등록), 주소 : /user/twittwitUserId/follow
                        xhr.send(); // 요청 전송
                    }
                }
            // }
        }
    });
});
//? 언팔로우
document.querySelectorAll('.unfollow').forEach(function (tag) { // 팔로우하기 버튼들 배열로 생성해서 모두 선택 // 하나씩 가져와서 tag에 담음 (반복)
    tag.addEventListener('click', function () { // 팔로우하기 버튼 클릭 하면
        var isLoggedIn = document.querySelector('#my-id'); // user의 id
        if (isLoggedIn) { // user의 id가 있으면 (로그인 한 상태이면)
            if(tag.parentNode.querySelector('#twit-user-id')){
                var twitUserId = tag.parentNode.querySelector('#twit-user-id').value; // 팔로우하기 버튼이 있는 게시글 작성자의 id value
            }
            var userId = isLoggedIn.value; // user의 id value
            // if(twitUserId !== undefined){
                if (twitUserId !== userId) { // 게시글 작성자 ID != 사용자 ID
                    if (confirm('팔로잉을 취소하시겠습니까?')) { // 확인창 출력
                        var xhr = new XMLHttpRequest(); // AJAX(=> JSON 사용) 객체 생성
                        xhr.onload = function () { // 요청
                            if (xhr.status === 200) { // 상태 코드가 200이면 (성공)
                                location.reload(); // 새로고침
                            } else { // 그 외면 (실패)
                                console.error(xhr.responseText); // error 출력
                            }
                        };
                        xhr.open('POST', '/user/' + twitUserId + '/unfollow'); // 요청 설정 // HTTP 요청 메서드(JSON 사용) : POST(등록), 주소 : /user/twittwitUserId/follow
                        xhr.send(); // 요청 전송
                    }
                }
            // }
        }
    });
});

//? 좋아요
document.querySelectorAll('.like').forEach((tag) => { // 좋아요 버튼들 배열로 생성해서 모두 선택 // 하나씩 가져와서 tag에 담음 (반복)
    tag.addEventListener('click', () => { // 좋아요 버튼 클릭하면
        tag.classList.toggle('like-click'); // 'like-click' class toggle
    });
});

// 좋아요
// document.querySelectorAll('.like').forEach(function (tag) { // 좋아요 버튼들 배열로 생성해서 모두 선택 // 하나씩 가져와서 tag에 담음 (반복)
//     tag.addEventListener('click', function () { // 좋아요 버튼 클릭 하면
//         var isLoggedIn = document.querySelector('#my-id'); // user의 id
//         var twitId = tag.parentNode.querySelector('#twit-id').value; // 좋아요 버튼이 있는 게시글의 id value
//         if (isLoggedIn) { // user의 id가 있으면 (로그인 한 상태이면)
//             var xhr = new XMLHttpRequest(); // AJAX(=> JSON 사용) 객체 생성
//             xhr.onload = function () { // 요청
//             if (xhr.status === 200) { // 상태 코드가 200이면 (성공)
//                 location.reload(); // 새로고침
//             } else { // 그 외면 (실패)
//                 console.error(xhr.responseText); // error 출력
//             }
//             };
//             xhr.open('POST', '/post/' + twitId + '/like'); // 요청 설정 // HTTP 요청 메서드(JSON 사용) : POST(등록), 주소 : /user/twitID/like
//             xhr.send(); // 요청 전송
//         }
//     });
// });
// 좋아요 취소
// document.querySelectorAll('.unlike').forEach(function (tag) { // 좋아요 취소 버튼들 배열로 생성해서 모두 선택 // 하나씩 가져와서 tag에 담음 (반복)
//     tag.addEventListener('click', function () { // 좋아요 취소 버튼 클릭 하면
//         var isLoggedIn = document.querySelector('#my-id'); // user의 id
//         var twitId = tag.parentNode.querySelector('#twit-id').value; // 좋아요 취소 버튼이 있는 게시글의 id value
//         if (isLoggedIn) { // user의 id가 있으면 (로그인 한 상태이면)
//             var xhr = new XMLHttpRequest(); // AJAX(=> JSON 사용) 객체 생성
//             xhr.onload = function () { // 요청
//             if (xhr.status === 200) { // 상태 코드가 200이면 (성공)
//                 location.reload(); // 새로고침
//             } else { // 그 외면 (실패)
//                 console.error(xhr.responseText); // error 출력
//             }
//             };
//             xhr.open('DELETE', '/post/' + twitId + '/like'); // 요청 설정 // HTTP 요청 메서드(JSON 사용) : DELETE(삭제), 주소 : /user/twitID/like
//             xhr.send(); // 요청 전송
//         }
//     });
// });

//? 게시글 삭제
document.querySelectorAll('.delete').forEach(function (tag) { // 삭제하기 버튼들 배열로 생성해서 모두 선택 // 하나씩 가져와서 tag에 담음 (반복)
    tag.addEventListener('click', function () { // 삭제하기 버튼 클릭 하면
        var isLoggedIn = document.querySelector('#my-id'); // user의 id
        if(tag.parentNode.querySelector('#twit-id')){
            var twitId = tag.parentNode.querySelector('#twit-id').value; // 삭제하기 버튼이 있는 게시글의 id value
        }
        if (isLoggedIn) { // user의 id가 있으면 (로그인 한 상태이면)
            if(tag.parentNode.querySelector('#twit-user-id')){
                var twitUserId = tag.parentNode.querySelector('#twit-user-id').value; // 삭제하기 버튼이 있는 게시글 작성자의 id value
            }
            var userId = isLoggedIn.value; // user의 id value
            if (twitUserId === userId) { // 게시글 작성자 ID == 사용자 ID (내 게시글에서만 삭제하기 버튼이 보여야 됨)
                if (confirm('게시글 삭제하시겠습니까?')) { // 확인창 출력
                    var xhr = new XMLHttpRequest(); // AJAX(=> JSON 사용) 객체 생성
                    xhr.onload = function () { // 요청
                        if (xhr.status === 200) { // 상태 코드가 200이면 (성공)
                            location.reload(); // 새로고침
                        } else { // 그 외면 (실패)
                            console.error(xhr.responseText); // error 출력
                        }
                    };
                    xhr.open('DELETE', '/post/' + twitId); // 요청 설정 // HTTP 요청 메서드(JSON 사용) : DELETE(삭제), 주소 : /post/twitID
                    xhr.send(); // 요청 전송
                }
            }
        }
    });
});
<file_sep>/routes/middlewares.js
//* router - isAuthenticated() 로그인 인증 (접근 권한 제한) => page에 사용 (❗ 라우터 내 미들웨어)
// isAuthenticated() = 요청이 인증 됐으면 true, 인증 안 됐으면 false => 로그인 여부 파악

'use strict';

//? 로그인 한 상태 => isAuthenticated() === true
exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) { // isAuthenticated() === true면
    next(); // 다음으로 넘어감
  } else { // 그 외면
    res.status(403).send('로그인 필요'); // 상태 코드(403(금지됨)) 요청 전송
  }
};

//? 로그인 안 한 상태 => isAuthenticated() === false
exports.isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) { // isAuthenticated() === false면
    next(); // 다음으로 넘어감
  } else { // 그 외면
    res.redirect('/'); // Home으로 페이지 이동
  }
};
<file_sep>/models/index.js
//todo MySQL table <-> sequelize model
//* DB 관계 정의하기 - model들의 관계를 시퀄라이즈에 등록

'use strict';

// 자동 생성
const Sequelize = require('sequelize'); // 참조할 패키지
const env = process.env.NODE_ENV || 'development'; // 시스템 환경 = node 환경 or 개발 환경
const config = require('../config/config')[env]; // 참조할 경로 (config)
const db = {}; // db 객체 생성

const sequelize = new Sequelize(
  config.database, config.username, config.password, config,
);

db.sequelize = sequelize;
db.Sequelize = Sequelize;
//

//! 파일 참조
db.User = require('./user')(sequelize, Sequelize); // user model(table)
db.Post = require('./post')(sequelize, Sequelize); // post model(table)
db.Hashtag = require('./hashtag')(sequelize, Sequelize); // hashtag model(table)

//! model들 관계 등록
//! 최종 등록 모델 : user, post, hashtag, PostHashtag, Follow
//? user : post = 1 : N
db.User.hasMany(db.Post); // hasmany
db.Post.belongsTo(db.User); // belongsto // post model에 userID 컬럼 추가 (시퀄라이즈가 자동으로)

//? post : hashtag = N : M (다대다)
db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' }); // belongsToMany // PostHashtag 모델 생성 (postID, hashtagID 컬럼 추가)
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' }); // belongsToMany // PostHashtag 모델 생성 (postID, hashtagID 컬럼 추가)

//? (user model 내) user(팔로잉) : user(팔로워) = N : M (다대다)
// 팔로잉
db.User.belongsToMany(db.User, {
  foreignKey: 'followingId',
  as: 'Followers', // 시퀄라이즈가 JSON 작업 시 사용하는 이름
  through: 'Follow', // Follow 모델 생성 (followingId, followerId 컬럼 추가)
});
// 팔로워
db.User.belongsToMany(db.User, {
  foreignKey: 'followerId',
  as: 'Followings', // 시퀄라이즈가 JSON 작업 시 사용하는 이름
  through: 'Follow', // Follow 모델 생성 (followingId, followerId 컬럼 추가)
});

//? (user model 내) user(좋아요) : post(좋아요) = N : M (다대다)
// (게시글) 좋아요
db.User.belongsToMany(db.Post, {
  through: 'Like' // Like 모델 생성 (postID, userID 컬럼 추가)
});
// (게시글) 좋아요 취소
db.Post.belongsToMany(db.User, {
  as: 'Liker', // 시퀄라이즈가 JSON 작업 시 사용하는 이름
  through: 'Like' // Like 모델 생성 (postID, userID 컬럼 추가)
});

//! DB 모듈 생성
module.exports = db;
<file_sep>/routes/auth.js
//* router - 회원가입, 로그인, 로그아웃 인증
// isLoggedIn: 로그아웃
// isNotLoggedIn: 회원가입, 로그인

'use strict';

//! 모듈 참조 - 패키지(미들웨어)
const express = require('express');
const passport = require('passport'); // 로그인 인증
const bcrypt = require('bcrypt'); // 암호화

//! 모듈 참조 - 파일
const { isLoggedIn, isNotLoggedIn } = require('./middlewares'); // 접근 권한 제한 (로그인 여부 파악)
const { User } = require('../models'); // DB

//! 라우터 객체 생성
const router = express.Router();

//! 라우터
//? 회원가입 라우터
router.post('/join', isNotLoggedIn, async (req, res, next) => { // /join으로 post(등록) 요청하면 // 로그인 안 한 상태
  const { email, nick, password } = req.body; // 이메일, 닉네임, 비밀번호 객체 생성 // req.body에 등록
  try { // 성공하면
    const exUser = await User.findOne({ where: { email } }); // user DB 중 입력 받은 email 있는거 찾음
    if (exUser) { // 🟠 exUser가 있으면 (이미 등록된 email이면)
      req.flash('joinError', '이미 가입된 이메일입니다.'); // joinError 메세지 출력
      return res.redirect('/join'); // join으로 페이지 이동(되돌려 보냄)
    }
    // 🟠 exUser 없으면 (회원가입 성공)
    const hash = await bcrypt.hash(password, 12); // 비밀번호 해시 암호화 // 12번 반복
    await User.create({ // 새로운 user DB 로우(객체) 생성 (저장) // 이메일, 닉네임, 비밀번호(해시)
      email,
      nick,
      password: <PASSWORD>,
    });
    return res.redirect('/'); // /home으로 페이지 이동
  } catch (error) { // 실패하면
    console.error(error);
    return next(error);
  }
});

//? 로그인 라우터
router.post('/login', isNotLoggedIn, (req, res, next) => { // /login으로 post(등록) 요청하면 // 로그인 안 한 상태
  // 미들웨어 내의 미들웨어
  passport.authenticate('local', (authError, user, info) => { // 로컬 로그인 인증 전략 수행
    if (authError) { // authError값 있으면 (서버 에러)
      console.error(authError);
      return next(authError);
    }
    if (!user) { // 🟠 user값 없으면 (로그인 실패하면)
      req.flash('loginError', info.message); // 로그인 에러 출력
      return res.redirect('/'); // /home으로 페이지 이동
    }
    // 🟠 user값 있으면 (로그인 성공하면)
    return req.login(user, (loginError) => {
      if (loginError) {
        console.error(loginError);
        return next(loginError);
      }
      return res.redirect('/');
    });
  })(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});

//? 로그아웃 라우터
router.get('/logout', isLoggedIn, (req, res) => { // /logout get 요청하면 // 로그인 한 상태
  req.logout(); // req.user 객체 제거
  req.session.destroy(); // req.session 객체 내용 제거
  res.redirect('/'); // /home으로 페이지 이동
});

//? 카카오 로그인 라우터
router.get('/kakao', passport.authenticate('kakao')); // /kakao로 get 요청 // 카카오 로그인 인증 전략 시작
// 위에 카카오(에서 주는) 인증 결과를 받을 라우터 주소
router.get('/kakao/callback', passport.authenticate('kakao', { // /kakao/callback로 get 요청하면 // 카카오 로그인 인증 전략 수행
  failureRedirect: '/', // 실패하면  /home으로 페이지 이동
}), (req, res) => { // 성공하면
  res.redirect('/'); // /home으로 페이지 이동
});

//! router 모듈 생성
module.exports = router;
<file_sep>/README.md
# SNS 기반 웹 애플리케이션 (21.02)

#### 본 프로젝트는 <Node.js 교과서(제로초) - '익스프레스로 SNS 서비스 만들기'>를 참고하였습니다.

`Node.js, mySQL을 사용하여 제작한 SNS 기반 웹 애플리케이션입니다.`  

![](https://img.shields.io/badge/-HTML5-F1662B)
![](https://img.shields.io/badge/-CSS3-2FA9DC)
![](https://img.shields.io/badge/-JavaScript-E3A127)
![](https://img.shields.io/badge/-Node.js-8BC500)  

## 💡 Application Description
 나와 우리의 감성, 감정, 하루를 함께 공유하고 공감하는 '우리의 감성' SNS 서비스 어플리케이션입니다!  

## ✨ Preview
* 로그인
![login](https://user-images.githubusercontent.com/74370531/110148047-b0c94b00-7e1f-11eb-9497-2b5eb16bb26d.JPG)
* 회원가입
![join](https://user-images.githubusercontent.com/74370531/110148046-b030b480-7e1f-11eb-8efd-a844de933c13.JPG)
* 홈
![main](https://user-images.githubusercontent.com/74370531/110148048-b0c94b00-7e1f-11eb-8716-1a03c1671b88.JPG)
* 프로필
![profile](https://user-images.githubusercontent.com/74370531/110148050-b161e180-7e1f-11eb-861f-f62f5cd78116.JPG)
* 게시글 작성
![write](https://user-images.githubusercontent.com/74370531/110148052-b161e180-7e1f-11eb-8f90-3e656e5f0513.JPG)
* 해시태그 검색
![hashtag](https://user-images.githubusercontent.com/74370531/110150168-436ae980-7e22-11eb-862a-bffd41b1a26b.JPG)  

## 🔎 File Description
* 📁 models : DB 설정
  * 📄 index.js : 모델(테이블)들 시퀄라이즈에 등록, 관계 설정
  * 📄 user.js : 사용자 테이블
  * 📄 post.js : 게시글 테이블
  * 📄 hastag.js : 해시태그 테이블
* 📁 passport : 로그인 구현
  * 📄 index.js
```js
// routes/auth.js 로그인 라우터 passport.authenticated의 done() 콜백함수로 전달
// done() : 로그인 시 사용
//? 왜하냐? 세션에 사용자 정보를 모두 저장하면 용량이 커지므로 아이디만 저장하려고

// req.session 객체에 어떤 데이터(passport 정보)를 저장할지 선택
// 사용자 정보 객체를 세션에 ID로 저장
passport.serializeUser((user, done) => { // 사용자 정보를 받음
  done(null, user.id); // null: 서버 에러 발생 시 사용 // 사용자 ID 전달
});

// 위에서 저장된 사용자 ID를 받아 DB에서 사용자 정보 조회
// 세션에 저장한 ID를 통해 사용자 정보 객체를 불러옴
passport.deserializeUser((id, done) => { // 위에서 사용자 ID를 받음
  User.findOne({ // user DB 중 받은 사용자 ID가 있는 사용자 정보 찾음
    where: { id },
    include: [ // 아래사항 포함
      {
        model: User, // user DB의
        attributes: ['id', 'nick'], // ID, 닉네임
        as: 'Followers', // 팔로워
      }, 
      {
        model: User, // user DB의
        attributes: ['id', 'nick'], // ID, 닉네임
        as: 'Followings', // 팔로잉
      }
    ],
  })
    .then(user => done(null, user)) // 성공하면 사용자 정보 전달
    .catch(err => done(err));  // 실패하면 에러 처리 (서버 에러)
});
```
  * 📄 localStrategy.js : 로컬로그인 구현
  * 📄 kakaoStrategy.js : 카카오로그인 구현
* 📁 public
  * 📄 main.js
    * XMLHttpRequest 객체를 사용하여 서버로부터 XML 데이터를 전달받아 처리
    * 서버에 요청 전송
```js
xhr.open('POST', '/post/img'); // 게시글 작성 (이미지 선택)
xhr.open('POST', '/user/' + twitUserId + '/follow'); // 팔로잉
xhr.open('POST', '/user/' + twitUserId + '/unfollow'); // 언팔로우
xhr.open('DELETE', '/post/' + twitId); // 게시글 삭제
```
* 📁 routes : 라우터
  * 📄 middlewares.js : req.isAuthenticated()
  * 📄 auth.js : 회원가입, 로그인, 로그아웃 라우터
  * 📄 user.js : 팔로잉, 언팔로우, 닉네임 변경 라우터
  * 📄 post.js : 게시글 업로드, 해시태그 검색 라우터
  * 📄 page.js : 홈, 회원가입, 게시글 작성, 프로필 페이지 라우터
* 📁 views : 화면
  * 📄 layout.html : 로그인, 내정보, 해시태그 검색
  * 📄 join.html : 회원가입
  * 📄 main.html : 게시글 목록
```html
<!--! 사용자 팔로잉들(배열)을 사용자 팔로잉들 ID(새로운 배열)로 만듬 - 거기에 게시글 작성자 ID 포함하냐 => true/flase -->
<!--! 사용자가 접속했고, 사용자 ID가 게시글 작성자 ID가 아니고, ❗!true = false(팔로잉 안 했으면)면 -->
{% if user and user.id !== twit.user.id and not followingsIdList.includes(twit.user.id) %}
  <li><button class="follow btn">팔로잉</button></li>
<!--! 사용자가 접속했고, 사용자 ID가 게시글 작성자 ID가 아니고, true(팔로잉 했으면)면 -->
{% elseif user and user.id !== twit.user.id and followingsIdList.includes(twit.user.id) %}
  <li><button class="unfollow btn">언팔로우</button></li>
{% endif %}
```
  * 📄 write.html : 게시글 작성
  * 📄 profile.html : 내프로필
  * 📄 error.html  

## 🔧 Install
```
npm install
```  

## 🚀 Usage
```
npm start
```
```
http://localhost:8001/
```
<file_sep>/models/hashtag.js
//* DB 모델 정의하기 - hashtag(해시태그) for 검색

'use strict';

//! 모듈 생성
module.exports = (sequelize, DataTypes) => (
  // 테이블 컬럼 설정
  sequelize.define('hashtag',
  {
    // 해시태그명
    title: {
      type: DataTypes.STRING(15), // 길이 0~15 문자열
      allowNull: false, // 빈칸 허용 X
      unique: true, // 해당 값이 고유해야 됨
    },
  }, 
  { // 테이블 설정
    timestamps: true, // createdAt와 updatedAt 컬럼 추가 // 로우가 생성, 수정될 때 시간이 자동 입력됨
    paranoid: true, // deletedAt 컬럼 추가 // 로우가 삭제될 때 시간이 자동 입력됨
  })
);
<file_sep>/app.js
'use strict';

//! 모듈 참조 - 패키지(미들웨어)
const favicon = require('serve-favicon'); // favicon
const express = require('express');
const cookieParser = require('cookie-parser'); // 요청에 동봉된 쿠키 해석
const morgan = require('morgan'); // 요청에 대한 정보를 콘솔에 기록
const path = require('path'); // 파일 및 폴더 경로 쉽게 조작
const session = require('express-session'); // 세션 관리
const nunjucks = require('nunjucks');
const flash = require('connect-flash'); // 일회성 메세지 출력
const passport = require('passport'); // 요청(로그인) 인증
require('dotenv').config(); // 쿠키 비밀키

//! 모듈 참조 - 파일
const pageRouter = require('./routes/page'); //? (로그인) page 라우터
const authRouter = require('./routes/auth'); //? (로그인) 인증 라우터
const postRouter = require('./routes/post'); //? 게시글 업로드 라우터
const userRouter = require('./routes/user'); //? 팔로우하기 라우터
const { sequelize } = require('./models'); //? DB
const passportConfig = require('./passport'); //? 로그인

//! 메소드 호출
const app = express();
sequelize.sync(); //? sequelize를 통해 model을 mySQL(DB=서버)에 연결 // 서버 실행 시 알아서 mySQL에 연결
passportConfig(passport); //? 요청(로그인) 인증 // 경로 <-> 패키지

//! app 설정
// app.set('views', path.join(__dirname, 'views')); // views 폴더 지정
// app.set('view engine', 'pug'); // view engine을 pug로 지정
app.set('view engine', 'html'); // view engine을 html으로 지정
nunjucks.configure('views', { // views 폴더 지정
  express: app,
  watch: true,
});
app.set('port', process.env.PORT || 8001); // port // 포트 번호 : 포트 비밀키 or 8001

//! favicon
app.use(favicon(path.join(__dirname, 'public/images', 'favicon.ico')));

//! 이미지 연결
app.use('/logoLoading', express.static(path.join(__dirname, 'public/images', 'logo_01.png'))); // loading logo
app.use('/snsKakao', express.static(path.join(__dirname, 'public/images', 'sns_01.png'))); // loginArea kakao
app.use('/logoHeader', express.static(path.join(__dirname, 'public/images', 'logo_02.png'))); // header logo

//! app에 패키지(미들웨어) 연결(사용)
app.use(morgan('dev')); // morgan // 요청에 대한 정보를 콘솔에 기록
// static // 정적 파일 제공
app.use(express.static(path.join(__dirname, 'public'))); // public 폴더 지정
app.use('/img', express.static(path.join(__dirname, 'uploads'))); // uploads 폴더 내 img들이 /img 주소로 제공
// body-parser // 요청 본문 해석
app.use(express.json()); // json 형식의 데이터 전달 방식 
app.use(express.urlencoded({ extended: false })); // 주소 형식의 데이터 전달 방식 // querystring 모듈 사용
app.use(cookieParser(process.env.COOKIE_SECRET)); // 요청에 동봉된 쿠키 해석 // 해석된 쿠키들 req.cookies에 들어감 // 쿠키 비밀키 (복호화 할 때 사용하는 키)
app.use(session({ // 세션 관리
  resave: false, // 세션에 수정사항 없어도 다시 저장할거냐? no
  saveUninitialized: false, // 세션에 저장할 사항 없어도 저장할거냐? no
  secret: process.env.COOKIE_SECRET, // 쿠키 비밀키
  cookie: { // 쿠키 설정
    httpOnly: true, // 클라이언트에서 쿠키 못보게? yes
    secure: false, // https가 아닌 환경에서 사용할 수 없게? no
  },
}));
app.use(flash()); // 일회성 메세지 출력 // req.flash(key, value) 생성
app.use(passport.initialize()); // 요청(로그인) 인증 // req(요청) 객체에 passport 설정을 심음
app.use(passport.session()); // 요청(로그인) 인증 // req.session 객체에 passport 정보를 저장함 // deserializeUser() 호출

app.use('/', pageRouter); //? 라우터 연결 // 주소가 /로 시작하면, routes/page 호출
app.use('/auth', authRouter); //? 라우터 연결 // 주소가 /auth로 시작하면, routes/auth 호출
app.use('/post', postRouter); //? 라우터 연결 // 주소가 /post로 시작하면, routes/post 호출
app.use('/user', userRouter); //? 라우터 연결 // 주소가 /user로 시작하면, routes/user 호출

//? 라우터가 요청 처리 실패하면 (요청된 주소를 찾을 수 없으면)
app.use((req, res, next) => { // 404 처리 미들웨어
  const err = new Error('Not Found'); // 에러 객체 생성
  err.status = 404; // 에러 상태
  next(err); // 404(찾을 수 없음) 에러 상태코드를 에러 핸들러로 넘김
});

app.use((err, req, res, next) => { // 에러 핸들러
  res.locals.message = err.message; // 응답 메세지 = 에러 메세지
  // app.get(key) => app.set(key)으로 설정한 것을 가져옴
  res.locals.error = req.app.get('env') === 'development' ? err : {}; // 시스템 환경 === 개발 환경? 맞으면 error 아니면 ok
  res.status(err.status || 500); // 응답 상태 = 에러 상태 or 500(내부 서버 오류) 
  res.render('error'); // 에러 응답
});

//! app에 port 연결
app.listen(app.get('port'), () => { // app.set('port') 받아옴
  console.log(app.get('port'), '번 포트에서 대기중');
});
