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 패키지를 위한 폴더
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
'개인공부 > Node.js' 카테고리의 다른 글
nodejs로 SNS 만들기 #4 (1) | 2022.08.15 |
---|---|
User.find is not a function 오류 발생 시 해결 방법 (0) | 2022.08.15 |
nodejs로 SNS 만들기 #3 (0) | 2022.08.15 |
nodejs로 SNS 만들기 #2 (0) | 2022.08.09 |
[터미널 설정 문제] 'node' 용어가 cmdlet, 함수, 스크립트 파일 또는 실행할 수 있는 프로그램 이름으로 인식되지 않습니다. (0) | 2022.07.21 |