개인공부/Node.js

nodejs로 SNS 만들기 #1

psys 2022. 8. 9. 15:56
728x90

프로젝트 세팅

1. package.json 생성하기

$npm init

 

2. package.json 작성하기

{
  "name": "nodeinsta",
  "version": "1.0.0",
  "description": "노드 인스타",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "PSY",
  "license": "MIT"
}

※ start 속성은 잊지 말고 넣어줘야함

 

3. 시퀄라이즈 설치하기

$npm i -g sequelize-cli

//node_modules와 package-lock.json 폴더 생성
$npm i sequelize mysql2

//config, migrations, models, seeders 폴더 생성
$sequelize init

 

4. 폴더추가

views - 템플릿 넣을 폴더

routes - 라우터 넣을 폴더

public - 정적 파일 넣을 폴더

passport - passport 패키지를 위한 폴더

nodeInsta 폴더 구조

5. 필요한 npm 패키지 설치

$npm i express cookie-parser express-session morgan connect-flash pug

$npm i -g nodemon

$npm i -D nodemon

 

6. app.js 작성

// app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const flash = require('connect-flash');

const pageRouter = require('./routes/page'); // 라우터 가져오기

const app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.set('port', process.env.PORT || 8001);

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser('nodeInstasecret'));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: 'nodeInstasecret',
  cookie: {
    httpOnly: true,
    secure: false,
  },
}));

app.use(flush());

app.use('/', pageRouter);

app.use((req, res, next) => {
  const error =  new Error(`Not Found`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});

 

7. dotenv 패키지

cookieParser와 express-session의 nodeInstasecret같은 비밀키는 직접 하드코딩하지 않는다.

키 유출 방지를 위해 별도로 관리해야한다.

이를 위한 패키지는 dotenv이다.

 

비밀키는 .env 파일에 모아두고

dotenv가 .env파일을 읽어 process.env 객체에 넣는다.

 

$npm i dotenv
//.env
//키=파일형식으로 비밀키 추가
COOKIE_SECRET=nodeInstasecret

 

8. 비밀키 불러오기

// app.js
...
const flash = require('connect-flash');
require('dotenv').config(); // 추가1-비밀키 불러오기

const pageRouter = require('./routes/page'); // 라우터 가져오기
...
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET)); // 변경1-비밀키
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET, // 변경2-비밀키
  cookie: {
    httpOnly: true,
...

 

9. 라우터와 템플릿 엔진 만들기

routes폴더 안 page.js 추가

views 폴더 안 layout.pug, main.pug, profile.pug, join.pug, error.pug 추가

public 폴더 안 main.css 추가

 

1) page.js

// routes/page.js
const express = require('express');

const router = express.Router();

// http://localhost:8001/profile
router.get('/profile', (req, res) => {
  res.render('profile', { title: '내 정보 - NodeInsta', user: null });
});

// http://localhost:8001/join
router.get('/join', (req, res) => {
  res.render('join', {
  		title: '회원가입 - NodeInsta',
        user: null,
        joinError: req.flash('joinError'),
	});
});

// http://localhost:8001/
router.get('/', (req, res, next) => {
  res.render('main', {
        title: 'NodeBird',
        twits: [],
        user: null,
        loginError: req.flash('loginError'),
	});
});

module.exports = router;

 

2) layout.pug

doctype
html
  head
    meta(charset='UTF-8')
    title= title
    meta(name='viewport' content='width=device-width, user-scalable=no')
    meta(http-equiv='X-UA-Compatible' content='IE=edge')
    link(rel='stylesheet' href='/main.css')
  body
    .container
      .profile-wrap
        .profile
          if user && user.id
            .user-name= '안녕하세요! ' + user.nick + '님'
            .half
              div 팔로잉
              .count.following-count= user.Followings && user.Followings.length || 0
            .half
              div 팔로워
              .count.follower-count= user.Followers && user.Followers.length || 0
            input#my-id(type='hidden' value=user.id)
            a#my-profile.btn(href='/profile') 내 프로필
            a#logout.btn(href='/auth/logout') 로그아웃
          else
            label nodeInsta
            form#login-form(action='/auth/login' method='post')
              .input-group
                label(for='email') 이메일
                input#email(type='email' name='email' required autofocus)
              .input-group
                label(for='password') 비밀번호
                input#password(type='password' name='password' required)
              if loginError
                .error-message= loginError
              a#join.btn(href='/join') 회원가입
              button#login.btn(type='submit') 로그인
              a#kakao.btn(href='/auth/kakao') 카카오톡
      block content

 

3) main.pug

extends layout

block content
  .timeline
    if user
      div
        form#twit-form(action='/post' method='post' enctype='multipart/form-data')
          .input-group
            textarea#twit(name='content' maxlength=140)
          .img-preview
            img#img-preview(src='' style='display: none;' width='250' alt='미리보기')
            input#img-url(type='hidden' name='url')
          div
            label#img-label(for='img') 사진 업로드
            input#img(type='file' accept='image/*')
            button#twit-btn.btn(type='submit') 짹짹
    .twits
      form#hashtag-form(action='/post/hashtag')
        input(type='text' name='hashtag' placeholder='태그 검색')
        button.btn 검색
      for twit in twits
        .twit
          input.twit-user-id(type='hidden' value=twit.user.id)
          input.twit-id(type='hidden' value=twit.id)
          .twit-author= twit.user.nick
          -const follow = user && user.Followings.map(f => f.id).includes(twit.user.id);
          if user && user.id !== twit.user.id && !follow
            button.twit-follow 팔로우하기
          .twit-content= twit.content
          if twit.img
            .twit-img
              img(src=twit.img alt='섬네일')
  script.
    if (document.getElementById('img')) {
      document.getElementById('img').addEventListener('change', function (e) {
        var formData = new FormData();
        console.log(this, this.files);
        formData.append('img', this.files[0]);
        var xhr = new XMLHttpRequest();
        xhr.onload = function () {
          if (xhr.status === 200) {
            var url = JSON.parse(xhr.responseText).url;
            document.getElementById('img-url').value = url;
            document.getElementById('img-preview').src = url;
            document.getElementById('img-preview').style.display = 'inline';
          } else {
            console.error(xhr.responseText);
          }
        };
        xhr.open('POST', '/post/img');
        xhr.send(formData);
      });
    }
    document.querySelectorAll('.twit-follow').forEach(function (tag) {
      tag.addEventListener('click', function () {
        var isLoggedIn = document.querySelector('#my-id');
        if (isLoggedIn) {
          var userId = tag.parentNode.querySelector('.twit-user-id').value;
          var myId = isLoggedIn.value;
          if (userId !== myId) {
            if (confirm('팔로잉하시겠습니까?')) {
              var xhr = new XMLHttpRequest();
              xhr.onload = function () {
                if (xhr.status === 200) {
                  location.reload();
                } else {
                  console.error(xhr.responseText);
                }
              };
              xhr.open('POST', '/user/' + userId + '/follow');
              xhr.send();
            }
          }
        }
      });
    });

 

4) profile.pug

extends layout

block content
  .timeline
    .followings.half
      h2 팔로잉 목록
      if user.Followings
        for following in user.Followings
          div= following.nick
    .followers.half
      h2 팔로워 목록
      if user.Followers
        for follower in user.Followers
          div= follower.nick

 

5) join.pug

extends layout

block content
  .timeline
    form#join-form(action='/auth/join' method='post')
      .input-group
        label(for='join-email') 이메일
        input#join-email(type='email' name='email')
      .input-group
        label(for='join-nick') 닉네임
        input#join-nick(type='text' name='nick')
      .input-group
        label(for='join-password') 비밀번호
        input#join-password(type='password' name='password')
      if joinError
        .error-message= joinError
      button#join-btn.btn(type='submit') 회원가입

 

6) error.pug

extends layout

block content
  h1= message
  h2= error.status
  pre #{error.stack}

 

7) main.css 

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
.btn {
  display: inline-block;
  padding: 0 5px;
  text-decoration: none;
  cursor: pointer;
  border-radius: 4px;
  background: #0095f6;
  border: 1px solid silver;
  color: rgb(255, 255, 255);
  height: 37px;
  line-height: 37px;
  vertical-align: top;
  font-size: 12px;
}
input[type='text'], input[type='email'], input[type='password'], textarea {
  border-radius: 4px;
  height: 37px;
  padding: 10px;
  border: 1px solid silver;
}
.container { width: 100%; height: 100%; }
@media screen and (min-width: 800px) {
  .container { width: 800px; margin: 0 auto; }
}
.input-group { margin-bottom: 15px; }
.input-group label { width: 25%; display: inline-block; }
.input-group input { width: 70%; }
.half { float: left; width: 50%; margin: 10px 0; }
#join { float: right; }
.profile-wrap {
  width: 100%;
  display: inline-block;
  vertical-align: top;
  margin: 10px 0;
}
@media screen and (min-width: 800px) {
  .profile-wrap { width: 290px; margin-bottom: 0; }
}
.profile {
  text-align: left;
  padding: 10px;
  margin-right: 10px;
  border-radius: 4px;
  border: 1px solid silver;
  background: rgb(255, 255, 255);
}
.user-name { font-weight: bold; font-size: 18px; }
.count { font-weight: bold; color: crimson; font-size: 18px; }
.timeline {
  margin-top: 10px;
  width: 100%;
  display: inline-block;
  border-radius: 4px;
  vertical-align: top;
}
@media screen and (min-width: 800px) { .timeline { width: 500px; } }
#twit-form {
  border-bottom: 1px solid silver;
  padding: 10px;
  background: lightcoral;
  overflow: hidden;
}
#img-preview { max-width: 100%; }
#img-label {
  float: left;
  cursor: pointer;
  border-radius: 4px;
  border: 1px solid crimson;
  padding: 0 10px;
  color: white;
  font-size: 12px;
  height: 37px;
  line-height: 37px;
}
#img { display: none; }
#twit { width: 100%; min-height: 72px; }
#twit-btn {
  float: right;
  color: white;
  background: crimson;
  border: none;
}
.twit {
  border: 1px solid silver;
  border-radius: 4px;
  padding: 10px;
  position: relative;
  margin-bottom: 10px;
}
.twit-author { display: inline-block; font-weight: bold; margin-right: 10px; }
.twit-follow {
  padding: 1px 5px;
  background: #fff;
  border: 1px solid silver;
  border-radius: 5px;
  color: crimson;
  font-size: 12px;
  cursor: pointer;
}
.twit-img { text-align: center; }
.twit-img img { max-width: 75%; }
.error-message { color: red; font-weight: bold; }
#search-form { text-align: right; }
#join-form { padding: 10px; text-align: center; }
#hashtag-form { text-align: right; }
footer { text-align: center; }

 

10. 서버 실행

$npm start

http://localhost:8001/에 접속하면 아래와 같은 화면이 출력된다.

 


다음편(데이터베이스 연결)은 요기👇👇

https://yeon960.tistory.com/271

 

nodejs로 SNS 만들기 #2

https://yeon960.tistory.com/270 nodejs로 SNS 만들기 #1 1. 프로젝트 세팅 시작 1. package.json 생성하기 $npm init 2. package.json 작성하기 { "name": "nodeinsta", "version": "1.0.0", "description": "노..

yeon960.tistory.com