목차
1. ORM ( Object-Relational Mapping )
- ORM(Object-Relational Mapping)은 객체지향 패러다임을 활용하여 관계형 데이터베이스(RDB)의 데이터를 조작하게 하는 기술
- 이를 활용하면 쿼리를 작성하지 않고도 객체의 메서드를 활용하는 것처럼 쿼리 로직을 작성할 수 있다.
- Sequelizes는 Node.js의 대표적인 ORM
- Sequelize는 MySQL, PostgreSQL, MariaDB 등 많은 RDBMS를 지원하고 'Promise 기반으로 구현'되었기 때문에 비동기 로직을 편리하게 작성할 수 있다.
1.1. 사전 설정
1.1.1. mysql 스키마 생성
mysql workbench에서 nodejs 스키마를 생성한다.
( Charset/Collation : utf8mb4 / utf8mb4_general_ci )
1.1.2. 모듈 다운로드
express, nunjucks, sequelize, sequelize-cli, mysql2를 받는다.
그리고 npx sequelize init을 하면 초기설정이 실행된다.
그럼 다음과 같이 몇 가지 폴더와 파일들이 생성된다.
1.1.3. config/config.json
"development":{ } 가 개발할 때 필요한 부분이므로, 이 부분만 다음과 같이 수정하면 된다.
{
"development": {
"username": "root",
"password": "adminuser",
"database": "nodejs",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
...
},
"production": {
...
}
}
1.1.4. public, routers, views 폴더 생성
다음과 같이 3개의 폴더를 추가한다.
- views : html 파일에 해당하는 view가 담길 디렉토리
- routers : 분리한 라우터가 담길 디렉토리
- public : view에서 script 태그로 호출되는 sequelize.js가 담기는 디렉토리
1.2. models 파일 구성
1.2.1. index.js
- index.js 파일에서 반복문을 돌면서 models 폴더 내에 있는 파일들을 읽고, 그것들을 모델로 정의한다.
- models에 js 파일로 모델을 정의 하면 원하는 테이블이 만들 수 있다.
const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
const User = require('./user');
const Comment = require('./comment');
let sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize; // db에 연결하기 위한 연결객체를 db객체에 담는다.
db.Sequelize = Sequelize; // 현재 파일에 require한 Sequelize를 db객체에 담는다.
// db = {sequelize:sequelize, Sequelize:Sequelize } 한다는 것
// require한 user모델과 comment 모델도 db에 담는다.
db.User = User;
db.Comment = Comment;
// '모델객체 초기화 함수'와 '관계 형성 함수'를 실행한다.
User.init(sequelize);
Comment.init(sequelize);
User.associate(db);
Comment.associate(db);
// 여기까지의 코드가 테이블 생성 내용을 구성하는 코드
// 이제 db가 export가 되고, app.js에 require하면 require된 db에서 sequelize를 꺼내어 sync함수를 실행하게 되고, 이때 테이블이 생성된다.
module.exports = db;
1.2.2. user.js
- users 테이블을 생성/조작하는 테이블모델
-
sequelize를 이용해서 mysql에 테이블을 생성하거나 조작할 수 있는 "테이블모델"을 만든다.
module.exports = class User extends Sequelize.Model {
// 테이블을 생성하고 초기화하는 함수
static init ( sequelize ){
return super.init({
// 스키마 정의
...
},
{
// 테이블 옵션 설정
...
});
}
// 테이블간 관계 설정 함수
static associate(db){
...
}
};
const Sequelize = require('sequelize');
// 외부에서 User를 requier하고, User.init( Sequelize ); 로 같이 호출될 예정
module.exports = class User extends Sequelize.Model {
// '테이블을 생성'하고 초기화하는 함수
static init ( sequelize ){
return super.init({
// init함수에 각 필드의 이름(키)과 객체속성들(값)이 매칭된 객체가 전달된다.
// 각 필드를 객체 멤버 형식으로 나열한다.
// 각 멤버들의 값들도 객체로 구성된다.
name : {
type:Sequelize.STRING(20),
allowNull:false, // null을 허용하겠는가 ( false = NOTNULL )
unique:false, // unique 여부
},
age : {
type:Sequelize.INTEGER.UNSIGNED,
allowNull:false,
},
married : {
type:Sequelize.BOOLEAN,
allowNull:true,
},
comment : {
type:Sequelize.TEXT,
allowNull:true,
},
created_at : { // 레코드의 insert 시점( 날짜 시간 )
type:Sequelize.DATE,
allowNull:true,
defaultValue:Sequelize.NOW,
},
// 첫번째 필드 (id)는 따로 기술하지 않아도 자동증가 필드로 추가된다.
},
{ // 테이블의 옵션들이 멤버형식으로 정의된다.
sequelize,
timestamps:false, // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
underscored:false, // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드의 이름이 create_at, update_at으로 바뀐다.
modelName:'User', // sequelize가 사용할 모델(테이블) 이름
tableName:'users', // 데이터베이스의 자체 테이블의 이름
paranoid:false, // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
// createAt(생성시간) : 레코드 insert된 시간
// updateAt(수정시간) : 레코드update된 시간
// deleteAt(삭제시간) : 레코드가 삭제된 시간 (실제 데이터는 삭제되지 않고 시간만 기록)
});
}
// 테이블간 관계 설정 함수
static associate(db){
// User모델의 필드값이 Comment모델에 같은 필드값으로 여러번 나오도록 설정(1:N 관계)
db.User.hasMany(db.Comment, {foreignKey:'commenter', sourceKey:'id'} );
// User 모델의 id필드를 Comment 모델에 commenter 필드로 복사하고, 관계설정을 한다.
}
};
1.2.3. comment.js
// comment.js
const Sequelize = require('sequelize');
module.exports = class Comment extends Sequelize.Model {
static init(sequelize){
return super.init({
// id필드는 현재 테이블에 기본키로 자동생성(값이 자동증가)
// 누가 댓글을 썼는지에 대한 필드 : commenter정도의 이름으로 생성될 필드
// 외래키 설정시 역시 자동생성된다. -> users테이블의 id가 복사되어, 현재 테이블의 필드로 삽입생성된다.
// 외래키로 설정될 필드는 따로 기술하지 않고, 외래키에서 설정함과 동시에 자동생성되도록 한다. (associate에서 설정)
comment:{
type:Sequelize.STRING(100),
allowNull:false,
},
create_at:{
type:Sequelize.DATE,
allowNull:true,
defaultValue:Sequelize.NOW,
},
},
{
sequelize,
timestamps:false,
underscored:false,
modelName:'Comment',
tableName:'comments',
paranoid:false,
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
});
}
static associate(db){
// Comment모델의 commenter필드가 User모델의 id필드를 참조하면서 복사 & 생성된다.
db.Comment.belongsTo(db.User, {foreignKey:'commenter', sourceKey:'id'} );
}
};
1.2.4. associate( ) ( 테이블 간 관계 설정 )
- 한 명의 유저(user)는 여러 개의 댓글(comment)을 작성할 수 있다.
- 따라서 user테이블과 comment테이블은 '1 : N' 관계가 된다.
따라서 user테이블의 id를 <- comment테이블의 id가 참조한다.
// user table 모델
module.exports = class User extends Sequelize.Model {
static init ( sequelize ){
return super.init(
{
// 멤버필드 설정
},
{
// 테이블 옵션 설정
});
}
// 테이블간 관계 설정 함수
static associate(db){
// User모델의 필드값이 Comment모델에 같은 필드값으로 여러번 나오도록 설정(1:N 관계)
db.User.hasMany(db.Comment, {foreignKey:'commenter', sourceKey:'id'} );
// User 모델의 id필드를 Comment 모델에 commenter 필드로 복사하고, 관계설정을 한다.
}
};
// comment table 모델
module.exports = class Comment extends Sequelize.Model {
static init(sequelize){
return super.init(
{
// 멤버필드 설정
},
{
// 테이블 옵션 설정
});
}
// 테이블간 관계 설정 함수
static associate(db){
// Comment모델의 commenter필드가 User모델의 id필드를 참조하면서 복사 & 생성된다.
db.Comment.belongsTo(db.User, {foreignKey:'commenter', sourceKey:'id'} );
}
};
1.3. app.js 설정
1.3.1. 핵심내용
- 여기서는 'static 디렉토리 설정'과,
- '분리된 라우터를 수집/활용'하는 방법,
- 그리고, 'sequelize모듈로 db와 연결하는 것'이 포인트가 된다.
const express = require('express');
...
// static 폴더 설정 ( 여기에는 뷰에서 사용되는 sequelize.js가 위치한다 )
app.use(express.static(path.join(__dirname, 'public')));
// config.json의 내부정보로 전달하기 위한 db객체를 require한다.
const{ sequelize } = require('./models');
// 데이터베이스 연결
// 모델 제작 후 데이터베이스 연결시 해당 모델에 매핑되는 테이블이 없으면 새로 테이블을 만들라는 옵션. (force)
// force값이 true면 기존 테이블도 지우고 강제로 만들게 되니 주의한다.
sequelize.sync({ force:false })
.then(()=>{console.log('데이터베이스 연결 성공');})
.catch((err)=>{console.error(err);});
// 라우터들을 수집한다
const indexRouter = require('./routers');
const usersRouter = require('./routers/users');
const commentsRouter = require('./routers/comments');
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comments', commentsRouter);
...
1.3.2. 전체내용
const express = require('express');
const path = require('path');
const nunjucks = require('nunjucks');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(express.json());
app.use(express.urlencoded({extended:false}));
// nunjucks 사용환경 구성
app.set('view engine', 'html');
nunjucks.configure('views',{express:app, watch:true,});
// static 폴더 설정 ( 여기에는 뷰에서 사용되는 sequelize.js가 위치한다 )
app.use(express.static(path.join(__dirname, 'public')));
// config.json의 내부정보로 전달하기 위한 db객체를 require한다.
const{ sequelize } = require('./models');
// 데이터베이스 연결
// 모델 제작 후 데이터베이스 연결시 해당 모델에 매핑되는 테이블이 없으면 새로 테이블을 만들라는 옵션. (force)
// force값이 true면 기존 테이블도 지우고 강제로 만들게 되니 주의한다.
sequelize.sync({ force:false })
.then(()=>{console.log('데이터베이스 연결 성공');})
.catch((err)=>{console.error(err);});
// 라우터들을 수집한다
const indexRouter = require('./routers');
const usersRouter = require('./routers/users');
const commentsRouter = require('./routers/comments');
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comments', commentsRouter);
/*
app.get('/', (req,res)=>{
res.send('<h1>안녕하세요</h1>');
})
*/
app.use((req,res,next)=>{
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res,next)=>{
res.locals.message = res.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
console.error(err);
res.render('error');
});
app.listen(app.get('port'), ()=>{
console.log(app.get('port'),'번 포트에서 대기중');
});
1.4. index.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>index.html</title>
</head>
<body>
<!-- 사용자 -->
<div>
<form id="user-form">
<fieldset>
<legend>사용자 등록</legend>
<div><input name="username" type="text" placeholder="이름"> </div>
<div><input name="age" type="number" placeholder="나이"> </div>
<div><input name="married" type="checkbox" ><label for="married">결혼 여부</label> </div>
<button type="submit">등록</button>
</fieldset>
</form>
</div>
<table id="user-list" border="1">
<thead><tr><th>아이디</th><th>이름</th><th>나이</th><th>결혼여부</th></tr></thead>
<tbody>
{% for user in users %}
<tr><td>{{user.id}}</td><td>{{user.name}}</td><td>{{user.age}}</td>
<td>{{'기혼' if user.married else '미혼'}}</td></tr>
{% endfor%}
</tbody>
</table><br>
<!-- 댓글 -->
<div>
<form id="comment-form">
<fieldset>
<legend>댓글 등록</legend>
<div><input name="userid" type="text" placeholder="사용자 아이디" </div>
<div><input name="comment" type="text" placeholder="댓글" </div>
<button type="submit">등록</button>
</fieldset>
</form>
</div><br>
<table id="comment-list" border="1">
<thead><tr><th>아이디</th><th>작성자</th><th>댓글</th><th>수정</th><th>삭제</th></tr></thead>
<tbody></tbody>
</table>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/sequelize.js"></script>
</body>
</html>
1.5. 라우터 분리
// 전달되는 값들로 레코드가 추가된다.
// 1. 레코드 삽입
// 모델명.create({
// 필드명1 : 입력값,
// 필드명2 : 입력값,
// 필드명3 : 입력값,
// });
User.create({
name : 'hong',
age : 24,
married : false,
comment : '일반회원',
});
// 2. 일반조회(모든필드, 모든레코드)
// 모델명.findAll({});
User.findAll({});
// 3. 일부 필드만 조회 (select name, married from users)
User.findAll({
attributes:['name', 'married'],
});
// 4. 일부필드 & 일부 레코드(where조건) 조회
// ( select name, age from users where married=1 and age>30 )
User. findAll({
attributes:['name', 'age'],
where:{
married:1,
age:{[Op.gt] : 30},
},
});
// where절에 두 개의 조건이 별도의 언급없이 ','로 이어졋다면 그 둘을 and로 묶여 있는 것이다.
// or를 쓰려면
// ( select id, name from users where marreied=0 or age<30 )
User.findAll({
attributes:['id', 'name'],
where:{
[Op.or] : [{ married:1 }, { age:{ [Op.lt]:30 } }],
},
});
// 5.
// Select id, name from users order by age desc;
User.findAll({
attributes:['id', 'name';],
order:[['age', 'desc']],
});
// select id, name from users order by age desc, id asc;
User.findAll({
attributes:['id', 'name';],
order:[['age', 'desc'],['id','asc']],
});
// 6. 수정 update users set comment = '바꿀내용' where id=2;
User.Update({
comment : '바꿀내용',
},
{
where : {id:2},
});
// 7. 삭제 delete from users where id=2
User.destroy({
where : {id:2},
});
1.5.1. index.js
const express = require('express');
// 데이터베이스 조작(insert, update, delete, select)를 위해 require한다.
const User = require('../models/user');
const Comment = require('../models/comment');
const router = express.Router();
router.get('/', (req,res)=>{
res.render('index', { }); //최초 서버 실행시 첫페이지 (sequelize.html로 응답)
});
module.exports = router;
1.5.2. users.js
const express = require('express');
const User = require('../models/user');
const Comment = require('../models/comment');
const router = express.Router();
// 사용자 등록 동작을 하는 미들웨어
router.post('/', async (req, res, next)=>{
try{
const user = await User.create({
name:req.body.name,
age:req.body.age,
married:req.body.married,
});
// console.log(user);
res.json(user);
}catch(err){
console.error(err);
next(err); // 에러 루틴이 있는 라우터로 이동
}
});
// 등록된 사용자를 조회하는 미들웨어
router.get('/', async (req, res, next)=>{
try{
// User 객체를 통해 users 테이블의 모든 데이터 조회
const users = await User.findAll({
});
// 결과를 json형식으로 리턴
res.json(users);
}catch(err){
console.error(err);
next(err); //에러 루틴이 있는 라우터로 이동
}
});
module.exports = router;
1.5.3. comments.js
const express = require('express');
const User = require('../models/user');
const Comment = require('../models/comment');
const router = express.Router();
// 댓글 등록 동작하는 미들웨어
router.post('/', async (req, res, next)=>{
try{
const comment = await Comment.create({
userid:req.body.userid,
comment:req.body.comment,
});
res.json(comment);
}catch(err){
console.error(err);
next(err);
}
});
// 등록된 댓글을 조회하는 미들웨어
router.get('/', async (req,res,next)=>{
try{
const comments = await Comment.findAll({
// join을 위해 주인공테이블과 외래키관계(1:N)관계 테이블의 모델을 include한다.
// 이렇게 join 효과를 볼 수 있다.
include :{
model:User,
},
});
res.json(comments);
}catch(err){
console.error(err);
next(err);
}
});
// user테이블과 comments테이블의 조인
// User.findAll({
// include:{
// model:comment
// },
// });
// 댓글 수정 라우터
router.patch('/update/:id', async (req,res)=>{
try{
const result = await Comment.update({
comment:req.body.comment,
},
{
where:{ id:req.params.id },
});
res.json(result);
}catch(err){
console.error(err);
next(err);
}
});
// 댓글 삭제 라우터
router.delete('/delete/:id', async(req, res)=>{
try{
const result = await Comment.destroy({
where:{ id : req.params.id }
});
res.json(result);
}catch(err){
console.error(err);
next(err);
}
});
// 조건부 조회 라우터
router.get('/search/:id', async (req, res,next)=>{
try{
const comments = await Comment.findAll({
// join을 위해 주인공테이블과 외래키관계(1:N)관계 테이블의 모델을 include한다.
// 이렇게 join 효과를 볼 수 있다.
include :{
model:User,
},
where : {commenter : req.params.id}
});
res.json(comments);
}catch(err){
console.error(err);
next(err);
}
} );
module.exports = router;
1.6. public/sequelize.js
// index.html에 submit했을 때 반응
getUsers();
getComments()
//회원추가 : 사용자 등록 - user-form이 submit이벤트를 일으키면 실행.
document.getElementById('user-form').addEventListener('submit', async (e)=>{
e.preventDefault();
// 이름, 나이, 결혼여부를 변수에 저장
const name = e.target.username.value;
const age = e.target.age.value;
const married = e.target.married.checked;
if(!name){ return alert('이름을 입력하세요');}
if(!age){ return alert('나이을 입력하세요');}
try{
await axios.post('/users',{name, age, married});
// 레코드를 추가하고 되돌아오면, user를 모두 조회하여 user-list테이블에 모두 표시
// user들을 조회하여 user-list에 행단위로 추가하는 함수 호출
getUsers();
}catch(err){
console.error(err);
}
e.target.username.value = '';
e.target.age.value = '';
e.target.married.checked = false;
});
async function getUsers(){
// 모든 user를 조회해서 user-list 테이블을 표시한다.
try{
// '/users'의 get 방식으로 모든 사용자 정보를 조회하고 리턴된 데이터를 res에 담는다.
const res = await axios.get('/users');
// 결과를 사용하기 위해 변수에 담고 데이터를 추출
const users = res.data;
const tbody = document.querySelector('#user-list tbody');
tbody.innerHTML='';
// users에 있는 user들을 하나씩 user변수(함수의 매개변수)에 넣으면서 인원 수 만큼 반복실행한다.
users.map(function(user){
const row = document.createElement('tr'); // tr 태그 생성
let td = document.createElement('td'); // td 태그 생성
td.textContent = user.id; // 생성된 태그에 user의 id 삽입
row.appendChild(td); // tr안에 td 삽입
td = document.createElement('td');
td.textContent = user.name;
row.appendChild(td);
td = document.createElement('td');
td.textContent = user.age;
row.appendChild(td);
td = document.createElement('td');
td.textContent = user.married ? '기혼' : '미혼';
row.appendChild(td);
// row를 클릭하면 이벤트가 발생하도록 설정
row.addEventListener('click', ()=>{
getCommentOne(user.id);
});
tbody.appendChild(row); // 완성된 tr을 tbody에 추가
});
}catch(err){
}
}
// 작성자를 클릭하면 해당 댓글만 표시하는 기능
async function getCommentOne(id){
try{
const res = await axios.get(`/comments/search/${id}`);
const comments = res.data;
const tbody = document.querySelector('#comment-list tbody');
tbody.innerHTML = '';
comments.map(function(comment){
const row = document.createElement('tr');
let td = document.createElement('td');
td.textContent = comment.id;
row.appendChild(td);
td = document.createElement('td');
td.textContent = comment.User.name; //comment 포함된 user모델의 필드를 표시
row.appendChild(td),
td = document.createElement('td');
td.textContent = comment.comment;
row.appendChild(td);
// 수정버튼
const edit = document.createElement('button');
edit.textContent='수정';
// 수정버튼 생성
td = document.createElement('td'); //td버튼 생성
td.appendChild(edit); // 버튼을 td에 추가
row.appendChild(td); // 버튼이 든 td를 tr에 추가
//삭제버튼
const remove = document.createElement('button');
remove.textContent = '삭제';
//삭제버튼 생성
td = document.createElement('td');
td.appendChild(remove);
row.appendChild(td);
tbody.appendChild(row);
});
}catch(err){
console.error(err);
}
}
//댓글 등록 : comment-form이 submit이벤트를 일으키면 실행.
document.getElementById('comment-form').addEventListener('submit', async (e)=>{
e.preventDefault();
const userid = e.target.userid.value;
const comment = e.target.comment.value;
if(!userid){ return alert('아이디를 입력하세요');}
if(!comment){ return alert('댓글을 입력하세요');}
try{
await axios.post('/comments',{userid, comment});
getComments();
}catch(err){
console.error(err);
}
e.target.userid.value = '';
e.target.comment.value = '';
});
// 저장된 댓글 조회
async function getComments(){
try{
const res = await axios.get('/comments');
const comments = res.data;
const tbody = document.querySelector('#comment-list tbody');
tbody.innerHTML='';
comments.map(function(comment){
const row = document.createElement('tr');
let td = document.createElement('td');
td.textContent = comment.id;
row.appendChild(td);
td = document.createElement('td');
td.textContent = comment.User.name; //comment 포함된 user모델의 필드를 표시
row.appendChild(td),
td = document.createElement('td');
td.textContent = comment.comment;
row.appendChild(td);
// 수정버튼
const edit = document.createElement('button');
edit.textContent='수정';
// 수정버튼 생성
td = document.createElement('td'); //td버튼 생성
td.appendChild(edit); // 버튼을 td에 추가
row.appendChild(td); // 버튼이 든 td를 tr에 추가
//삭제버튼
const remove = document.createElement('button');
remove.textContent = '삭제';
//삭제버튼 생성
td = document.createElement('td');
td.appendChild(remove);
row.appendChild(td);
tbody.appendChild(row);
// 수정버튼에 이벤트리스너 설정
edit.addEventListener('click', async ()=>{
// 댓글 id와 입력받은 내용으로 comment를 수정하시고, 다시 댓글들을 검색하여 댓글을 표시해주세요.
const newComment = prompt('바꿀 내용을 입력하세요.');
if(!newComment) { return alert('내용을 반드시 입력해야 합니다.');}
try{
// http://localhost:3000/comments/3
await axios.patch(`/comments/update/${comment.id}`, {comment:newComment});
getComments();
}catch(err){
console.error(err);
}
});
// 삭제버튼 이벤트 리스너 설정
remove.addEventListener('click', async ()=>{
try{
await axios.delete(`/comments/delete/${comment.id}`);
getComments();
}catch(err){
console.error(err);
}
});
});
}catch(err){
console.error(err);
}
};
2. 게시판 만들기
2.1. 사전설정
- 해당 프로젝트로 이동 후 npm을 환경을 구성한다.
2.1.1. npm환경 구성 및 모듈 내려받기 + sequelize 환경설정
- npm 환경 구성
- npm init
- package.json에서 scripts 속성 수정 : start": "nodemon app"
- 모듈 내려받기
- npm i express nunjucks nunjucks-date-filter sequelize sequelize-cli cookie-parser express-session multer
- nodemon 설치
- npm i -D nodemon
- sequelize 환경 구성
- npx sequelize init
2.1.2. config/config.json 수정
- development에서 db설정을 수정한다.
2.1.3. models 구성
2.1.4. routers 구성
2.2. models 작성하기
2.2.1. index.js
const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize;
db.Sequelize = Sequelize;
// 각 테이블에 대한 모델을 만들고, require
const Member = require('./member');
const Board = require('./Board');
const Reply = require('./Reply');
db.Member = Member;
db.Board = Board;
db.Reply = Reply;
Member.init(sequelize);
Board.init(sequelize);
Reply.init(sequelize);
Member.associate(db);
Board.associate(db);
Reply.associate(db);
module.exports = db;
2.2.2. Member.js
// userid, pwd, name, phone, email, create_at들을 필드로 하는 모델을 만들어주세요.
// board의 write와 member의 userid와 N:1 관계를 설정해주세요
// 기타 설정은 board와 같은 설정이거나 맞춰 설정한다.
// userid에 기본키설정이 되어있기 떄문에 자동으로 생성되는 id(일련번호)필드는 생성되지 않는다.
const Sequelize = require('sequelize');
module.exports = class Member extends Sequelize.Model{
static init( sequelize ){
return super.init({
userid:{
type:Sequelize.STRING(30),
allowNull:false,
primaryKey:true,
unique:true,
},
pwd:{
type:Sequelize.STRING(30),
allowNull:false,
},
name:{
type:Sequelize.STRING(100),
allowNull:false,
},
phone:{
type:Sequelize.STRING(30),
allowNull : false,
},
email:{
type:Sequelize.STRING(100),
allowNull : true,
},
created_at:{
type:Sequelize.DATE,
allowNull : true,
defaultValue:Sequelize.NOW,
},
},
{
sequelize,
timestamps:false, // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
modelName:'Member', // sequelize가 사용할 모델(테이블) 이름
tableName:'members', // 데이터베이스의 자체 테이블의 이름
paranoid:false, // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
});
}
static associate(db){
db.Member.hasMany(db.Board, {foreignKey:'writer', sourceKey:'userid', onDelete:'cascade'})
}
};
2.2.3. Board.js
const Sequelize = require('sequelize');
module.exports = class Board extends Sequelize.Model{
static init( sequelize ){
return super.init({
subject:{
type:Sequelize.STRING(100),
allowNull:false,
},
content:{
type:Sequelize.STRING(1000),
allowNull:false,
},
readCount:{
type:Sequelize.INTEGER.UNSIGNED,
allowNull:false,
defaultValue:0,
},
created_at:{
type:Sequelize.DATE,
defaultValue:Sequelize.NOW,
allowNull:true,
},
filename:{
type:Sequelize.STRING(100),
allowNull:true,
},
realfilename:{
type:Sequelize.STRING(100),
allowNull:true,
},
},
{
sequelize,
timestamps:false, // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
modelName:'Board', // sequelize가 사용할 모델(테이블) 이름
tableName:'boards', // 데이터베이스의 자체 테이블의 이름
paranoid:false, // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
});
}
static associate(db){
db.Board.belongsTo(db.Member, {foreignKey:'writer', targetKey:'userid'})
// 해당 게시글이 지워지면 해당 boardnum인 댓글이 같이 지워짐 (onDelete:'cascade')
db.Board.hasMany(db.Reply, {foreignKey:'boardnum', sourceKey:'id', onDelete:'cascade' })
}
};
2.2.4. Reply.js
const Sequelize = require('sequelize');
module.exports = class Reply extends Sequelize.Model{
static init( sequelize ){
return super.init({
writer:{
type:Sequelize.STRING(30),
allowNull:false,
},
content:{
type:Sequelize.STRING(200),
allowNull:false,
},
created_at:{
type:Sequelize.DATE,
defaultValue:Sequelize.NOW,
allowNull:false,
}
},
{
sequelize,
timestamps:false, // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
modelName:'Reply', // sequelize가 사용할 모델(테이블) 이름
tableName:'replys', // 데이터베이스의 자체 테이블의 이름
paranoid:false, // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
charset:'utf8mb4',
collate:'utf8mb4_general_ci',
});
}
static associate(db){
db.Reply.belongsTo(db.Board, {foreignKey:'boardnum', targetKey:'id'});
}
};
2.3. app.js 설정
const express = require('express');
// 추가모듈
const nunjucks = require('nunjucks');
const path = require('path');
const cookieParser = require('cookie-parser');
const session = require('express-session');
// import * as deteFilter from 'nunjucks-date-filter';
const dateFilter = require('nunjucks-date-filter'); //넌적스에서 사용할 날짜 양식 필터 사용을 위한 모듈
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(express.json());
app.use(express.urlencoded({extended:false}));
// static
app.use(express.static(path.join(__dirname, 'public')));
// nunjucks 템플릿 설정
app.set('view engine', 'html');
let env = nunjucks.configure('views', { express:app, watch:true,});
env.addFilter('date', dateFilter);
// 쿠키, 세션 설정
app.use(cookieParser());
app.use(session({
resave:false,
saveUninitialized:false,
secret:'dlrjsgml',
}));
// sequelize 설정
const{ sequelize } = require('./models'); // config전달을 위한 db객체 require
sequelize.sync({force:false})
.then(()=>{console.log('데이터베이스 연결 성공');})
.catch((err)=>{
console.error(err);
});
// 분리된 라우터 수집
const indexRouter = require('./routers');
const memberRouter = require('./routers/members');
const boardRouter = require('./routers/boards');
// 라우팅
app.use('/', indexRouter);
app.use('/members', memberRouter);
app.use('/boards', boardRouter);
// 에러처리 미들웨어
app.use((req, res, next)=>{
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res,next)=>{
res.locals.message = res.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'),'번 포트에서 대기중');
});
2.4. views & public
2.4.1. views
2.4.1.1. views/login.html
axios를 내려받고, static인 public디렉토리의 login.js가 호출된다.
<!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>login.html</title>
</head>
<body>
<div>
<form id="login-form">
<fieldset>
<legend>Login</legend>
<div><input id="userid" type="text" placeholder="userid"></div>
<div><input id="pwd" type="password" placeholder="password"></div>
<button type="submit">로그인</button>
<input type="button" onclick="location.href='/members/joinform'" value="회원가입">
<span id="msg"></span>
</fieldset>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/login.js"></script>
</body>
</html>
2.4.1.2. views/main.html
axios를 내려받고, static인 public디렉토리의 main.js가 호출된다.
<!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>main.html</title>
<!-- public디렉토리에 css 추가함 -->
<link rel="stylesheet" type="text/css" href="/board.css">
<style>
#boardnum{text-align: center;}
#readCount{text-align: center;}
#writer{text-align: center;}
</style>
</head>
<body>
{{lUser.userid}}({{lUser.name}})님 어서오세요
<input type="button" value="회원수정" onclick="location.href='/members/updateForm/{{lUser.userid}}'">
<input type="button" value="로그아웃" onclick="location.href='/members/logout/logout'">
<input type="button" value="글쓰기" onclick="location.href='/members/boards/writeForm'"><br><br>
<table id="board-list" align="left">
<thead>
<tr><th width="100">번호</th><th width="400">제목</th><th width="100">작성자</th><th width="100">조회수</th></tr>
</thead>
<tbody><tr><td></td><td></td><td></td></tr></tbody>
</table>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/main.js"></script>
</body>
</html>
2.4.2. public
2.4.2.1. login.js
document.getElementById('login-form').addEventListener('submit', async (e)=>{
e.preventDefault();
const userid = e.target.userid.value;
const pwd = e.target.pwd.value;
if(!userid){ return alert("아이디를 입력하셈");}
if(!pwd){ return alert("비번을 입력하셈");} // 여기까지는 작동함
try{
const res = await axios.post('/members/login', {userid,pwd});
const mem = res.data;
let m = document.getElementById('msg');
if(mem == null){
m.innerHTML = '아이디가 없습니다';
}else if(mem.pwd != pwd){
m.innerHTML = '비밀번호를 확인하세요';
}else if(mem.pwd == pwd){
location.href = '/boards';
}else{
m.innerHTML = '알 수 없는 오류입니다.';
}
}catch(err){
}
})
2.4.2.2. main.js
main으로 이동시 모든 게시물을 조회하는 동작.
getBoard_list();
// 데이터베이스에서 게시물들을 읽어와 table의 tbody와 tr과 td로 삽입하는 함수
async function getBoard_list(){
try{
const res = await axios.get('/boards/boardLilst');
const boards = res.data;
//테이블의 tbody 안을 비운다.
const tbody = document.querySelector('#board-list tbody');
tbody.innerHTML='';
boards.map( async function(board){
const row = document.createElement('tr');
let td = document.createElement('td');
td.textContent = board.id;
td.id = 'boardnum';
row.append(td);
td = document.createElement('td');
let tContent = board.subject;
td.innerHTML = tContent;
row.appendChild(td);
td = document.createElement('td');
td.textContent = board.writer;
td.id = 'writer';
row.appendChild(td);
td = document.createElement('td');
td.textContent = board.readCount;
td.id = 'readCount';
row.appendChild(td);
tbody.appendChild(row);
})
}catch(err){
}
}
2.5. routers
2.5.1. index.js
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
// login.html을 표시하는 동작
router.get('/', (req, res, next)=>{
try{
res.render('login',{});
}catch(err){
console.error(err);
next(err);
}
});
module.exports = router;
2.5.2. members.js
const express = require('express');
const Member = require('../models/member');
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
// 로그인 동작
router.post('/login', async (req, res, next)=>{
try{
const luser = await Member.findOne({
// 전달된 아이디와 같은 레코드 검색 후 luser변수에 저장
where:{userid : req.body.userid },
});
if( (luser != null) && (luser.pwd == req.body.pwd) ){
req.session.loginUser = luser;
}
res.json(luser);
}catch(err){
console.error(err);
next(err);
}
});
module.exports = router;
2.5.3. boards.js
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
// 로그인 후 보여질 main화면 (main.html)
router.get('/', (req,res)=>{
const loginUser = req.session.loginUser;
res.render('main', {lUser:loginUser});
});
router.get('/boardList',(req,res)=>{
})
module.exports = router;
3. 기능 추가
3.1. 회원가입 폼으로 이동
3.1.1. views/login.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>login.html</title>
</head>
<body>
<div>
<form id="login-form">
<fieldset>
<legend>Login</legend>
<div><input id="userid" type="text" placeholder="userid"></div>
<div><input id="pwd" type="password" placeholder="password"></div>
<button type="submit">로그인</button>
<input type="button" onclick="location.href='/members/joinform'" value="회원가입">
<span id="msg"></span>
</fieldset>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/login.js"></script>
</body>
</html>
3.1.2. routers/members.js - router.get('/joinform')
const express = require('express');
const Member = require('../models/member');
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
...
router.get('/joinform', (req, res, next)=>{
res.render('memberInsert',{});
});
module.exports = router;
3.1.3. views/memberInsert.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">
<link rel="stylesheet" type="text/css" href="/board.css">
<title>memberInsert.html</title>
</head>
<body>
<div id="wrap">
<form id="join-form">
<fieldset>
<legend>회원가입 - 사용자 등록</legend>
<div><input id="userid" type="text" placeholder="userid"> </div>
<div><input id="pwd" type="password" placeholder="password"> </div>
<div><input id="name" type="text" placeholder="name"> </div>
<div><input id="phone" type="text" placeholder="phone"> </div>
<div><input id="email" type="text" placeholder="email"> </div>
<button type="submit">등록완료</button>
</fieldset>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/insertmember.js"></script>
</body>
</html>
3.2. 회원가입 동작
3.2.1. views/memberInsert.html
<!DOCTYPE html>
...
<link rel="stylesheet" type="text/css" href="/board.css">
<title>memberInsert.html</title>
</head>
<body>
<div id="wrap">
<form id="join-form">
<fieldset>
<legend>회원가입 - 사용자 등록</legend>
<div><input id="userid" type="text" placeholder="userid"> </div>
<div><input id="pwd" type="password" placeholder="password"> </div>
<div><input id="name" type="text" placeholder="name"> </div>
<div><input id="phone" type="text" placeholder="phone"> </div>
<div><input id="email" type="text" placeholder="email"> </div>
<button type="submit">등록완료</button>
</fieldset>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/insertmember.js"></script>
</body>
</html>
3.2.2. public/insertmember.js
document.getElementById('join-form').addEventListener('submit', async (e)=>{
e.preventDefault();
// 폼에서 입력된 값을 변수에 담기
const userid = e.target.userid.value;
const pwd = e.target.pwd.value;
const name = e.target.name.value;
const phone = e.target.phone.value;
const email = e.target.email.value;
// 유효성검사
if(!userid) { return alert('아이디를 입력하세요');}
if(!pwd) { return alert('비밀번호를 입력하세요');}
if(!name) { return alert('이름을 입력하세요');}
if(!phone) { return alert('전화번호를 입력하세요');}
if(!email) { return alert('이메일을 입력하세요');}
// axios를 활용해 라우터로 값 전달 (insert라서 리턴받는 변수 X)
try{
await axios.post('/members/insertMember',{userid,pwd,name,phone,email});
location.href='/';
}catch(err){
}
});
3.2.3. routers/members.js - router.post('/insertMember')
const express = require('express');
const Member = require('../models/member');
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
...
router.post('/insertMember', async (req,res,next)=>{
try{
await Member.create({
userid:req.body.userid,
pwd:req.body.pwd,
name:req.body.name,
phone:req.body.phone,
email:req.body.email,
});
res.end();
}catch(err){
console.error(err);
next(err);
}
});
module.exports = router;
3.3. 회원수정 폼으로 이동
3.3.1. views/main.html
<!DOCTYPE html>
...
<title>main.html</title>
<style>
#boardnum{text-align: center;}
#readCount{text-align: center;}
#writer{text-align: center;}
</style>
</head>
<body>
<div id="wrap">
{{lUser.userid}}({{lUser.name}})님 어서오세요
<input type="button" value="회원수정" onClick="location.href='/members/updateForm/{{lUser.userid}}'">
<input type="button" value="로그아웃" onClick="location.href='/members/logout'">
<input type="button" value="글쓰기" onClick="location.href='/boards/writeForm/'"><br><br>
...
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/main.js"></script>
</div>
</body>
</html>
3.3.2. routers/members.js - router.get('/updateForm/:userid')
const express = require('express');
const Member = require('../models/member');
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
...
router.get('/updateForm/:userid', async (req,res,next)=>{
// userid로 검색해서 검색 결과를 member라는 이름으로 같이 memberUpdateForm.html로 전송 이동.
try{
const member = await Member.findOne({
where : { userid : req.params.userid, }
});
res.render('memberUpdateForm', {member});
}catch(err){
console.error(err);
next(err);
}
});
module.exports = router;
3.3.3. views/memberUpdateForm.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>memberUpdateForm.html</title>
<link rel="stylesheet" type="text/css" href="/board.css">
</head>
<body>
<div id="wrap">
<form id="memberUpdate">
<fieldset><legend>회원정보 수정</legend>
<div>사용자 아이디 : {{member.userid}}
<input id="userid" type="hidden" value="{{member.userid}}"></div>
<div>비밀번호 : <input id="pwd" type="password" placeholder="password"></div>
<div>이름 : <input id="name" type="text" value="{{member.name}}"></div>
<div>전화번호 : <input id="phone" type="text" value="{{member.phone}}"></div>
<div>이메일 : <input id="email" type="text" value="{{member.email}}"></div>
<input type="submit" value="수정완료">
<input type="button" value="취소" onclick="location.href='/'">
</fieldset>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/updatemember.js"></script>
</div>
</body>
</html>
3.4. 회원수정 동작
3.4.1. views/memberUpdateForm.html
<!DOCTYPE html>
...
<title>memberUpdateForm.html</title>
<link rel="stylesheet" type="text/css" href="/board.css">
</head>
<body>
<div id="wrap">
<form id="memberUpdate">
<fieldset><legend>회원정보 수정</legend>
<div>사용자 아이디 : {{member.userid}}
<input id="userid" type="hidden" value="{{member.userid}}"></div>
<div>비밀번호 : <input id="pwd" type="password" placeholder="password"></div>
<div>이름 : <input id="name" type="text" value="{{member.name}}"></div>
<div>전화번호 : <input id="phone" type="text" value="{{member.phone}}"></div>
<div>이메일 : <input id="email" type="text" value="{{member.email}}"></div>
<input type="submit" value="수정완료">
<input type="button" value="취소" onclick="location.href='/'">
</fieldset>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/updatemember.js"></script>
</div>
</body>
</html>
3.4.2. public/updatemember.js
document.getElementById('memberUpdate').addEventListener('submit', async (e)=>{
e.preventDefault();
// 폼에서 입력된 값을 변수에 담기
const userid = e.target.userid.value;
const pwd = e.target.pwd.value;
const name = e.target.name.value;
const phone = e.target.phone.value;
const email = e.target.email.value;
// 유효성검사
if(!pwd) { return alert('비밀번호를 입력하세요');}
if(!name) { return alert('이름을 입력하세요');}
if(!phone) { return alert('전화번호를 입력하세요');}
if(!email) { return alert('이메일을 입력하세요');}
// axios를 활용해 라우터로 값 전달 후 리턴값 변수에 담기
try{
await axios.post('/members/update', {userid,pwd,name,phone,email});
location.href='/boards';
}catch(err){
}
});
3.4.3. routers/members.js - router.post('/update')
const express = require('express');
const Member = require('../models/member');
const Board = require('../models/board');
const Reply = require('../models/reply');
const router = express.Router();
...
router.post('/update', async(req, res, next)=>{
try{
// 회원정보 수정
const result = await Member.update({ // result에는 변경된 건수가 담김
pwd:req.body.pwd,
name:req.body.name,
phone:req.body.phone,
email:req.body.email,
},
{
where : { userid : req.body.userid, }
});
// 회원정보 조회하여 세션 저장
const member = await Member.findOne({
where : { userid : req.body.userid, }
});
req.session.loginUser = member;
res.json(member); // 종료의 의미 ( 보내는 것에는 크게 의미없음. )
}catch(err){
console.error(err);
next(err);
}
});
module.exports = router;
3.5. 게시글 작성 폼으로 이동
3.5.1. views/main.html
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
{{lUser.userid}}({{lUser.name}})님 어서오세요
<input type="button" value="회원수정" onClick="location.href='/members/updateForm/{{lUser.userid}}'">
<input type="button" value="로그아웃" onClick="location.href='/members/logout'">
<input type="button" value="글쓰기" onClick="location.href='/boards/writeForm/'"><br><br>
...
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/main.js"></script>
</div>
</body>
</html>
3.5.2. routers/boards.js - router.get('/writeForm')
multer, path, cookie, session 등 모듈을 import/setting한다.
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시글 등록 폼으로 이동
router.get('/writeForm', (req, res, next)=>{
const luser = req.session.loginUser;
res.render('writeForm', {luser});
});
module.exports = router;
3.5.3. views/writeForm.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>writeForm.html</title>
<link rel="stylesheet" type="text/css" href="/board.css">
</head>
<body>
<div id="wrap">
<form id="write-form">
<h2 style="text-align:center;">게시글 등록</h2>
<table id="board-list">
<tr><th width="100">제목</th>
<td width="600"> <input type="text" id="subject" size="95"></td>
</tr>
<tr><th width="100">작성자</th>
<td width="600"> {{luser.userid}}
<input type="hidden" id="userid" value="{{luser.userid}}"></td>
</tr>
<tr><th width="100">내용</th>
<td width="600"> <textarea id="text" rows="10" cols="95"></textarea></td>
</tr>
<tr><th width="100">이미지</th>
<td width="500"> <input type='file' name="image" id="image" /></td>
</tr>
<tr height="80"><td align="center" colspan="2">
<button type="submit">글쓰기</button>
<input type="button" value="돌아가기" onClick="location.href='/boards'"/></td>
</tr>
</table><br>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/boardWrite.js"></script>
</body>
</html>
3.6. 게시글 등록
3.6.1. views/writeForm.html
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
<form id="write-form">
<h2 style="text-align:center;">게시글 등록</h2>
<table id="board-list">
<tr><th width="100">제목</th>
<td width="600"> <input type="text" id="subject" size="95"></td>
</tr>
<tr><th width="100">작성자</th>
<td width="600"> {{luser.userid}}
<input type="hidden" id="userid" value="{{luser.userid}}"></td>
</tr>
<tr><th width="100">내용</th>
<td width="600"> <textarea id="text" rows="10" cols="95"></textarea></td>
</tr>
<tr><th width="100">이미지</th>
<td width="500"> <input type='file' name="image" id="image" /></td>
</tr>
<tr height="80"><td align="center" colspan="2">
<button type="submit">글쓰기</button>
<input type="button" value="돌아가기" onClick="location.href='/boards'"/></td>
</tr>
</table><br>
</form>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/boardWrite.js"></script>
</body>
</html>
3.6.2. public/boardWrite.js
document.getElementById('write-form').addEventListener('submit', async (e)=>{
e.preventDefault();
const subject = e.target.subject.value;
const writer = e.target.userid.value;
const text = e.target.text.value;
if(!subject){return alert('제목을 입력히셈');}
if(!text){return alert('내용을 입력히셈');}
const formData = new FormData();
formData.append('image', e.target.image.files[0]);
formData.append('subject', subject);
formData.append('writer', writer);
formData.append('text', text);
try {
await axios.post('/boards/writeBoard', formData)
location.href = '/boards';
} catch (err) {
console.error(err);
}
});
3.6.3. routers/boards.js - router.post('/writeBoard')
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시글 등록 동작
router.post('/writeBoard', upload.single('image'), async (req,res,next)=>{
// 파일 업로드와 게시글 insert를 완성.
// filename필드명에는 서버에 저장되는 파일명(현재날짜와 시간이 밀리초로 변환된 값 + 파일명
// realfilename 필드명에는 원래 파일명
try {
const board = await Board.create({
subject : req.body.subject,
writer : req.body.writer,
content : req.body.text,
filename : req.file.originalname,
realfilename : req.file.filename,
});
res.json(board);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.7. 게시글 보기
3.7.1. views/main.html
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
...
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/main.js"></script>
</div>
</body>
</html>
3.7.2. public/main.js
게시글 행 내부가 시작되는 부분에 클릭이벤트를 추가한다.
getBoard_list();
// 데이터베이스에서 게시물들을 읽어와 table의 tbody와 tr과 td로 삽입하는 함수
async function getBoard_list(){
try{
const res = await axios.get('/boards/boardList');
const boards = res.data;
//테이블의 tbody 안을 비운다.
const tbody = document.querySelector('#board-list tbody');
tbody.innerHTML='';
boards.map( async function(board){
const row = document.createElement('tr');
// 게시글 클릭시 boardView로 이동
row.addEventListener('click',()=>{
location.href="/boards/boardView/"+board.id;
});
let td = document.createElement('td');
td.textContent = board.id;
td.id = 'boardnum';
row.append(td);
td = document.createElement('td');
let tContent = board.subject;
td.innerHTML = tContent;
row.appendChild(td);
...
tbody.appendChild(row);
});
}catch(err){
}
}
3.7.3. routers/boards.js - router.get('/boardView/:id')
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시물 보기
router.get('/boardView/:id', async (req, res,next)=>{
try {
// 게시물 검색
const result = await Board.findOne({
attributes:['readCount'],
where:{id:req.params.id},
});
// 검색한 게시물의 조회수를 추출하여 +1 연산
const cnt = result.readCount + 1;
// 조회수 연산결과를 게시물에 update
await Board.update({
readCount:cnt,
},
{
where:{id:req.params.id},
});
// 게시물을 재검색
const board = await Board.findOne({
where:{id:req.params.id},
});
// render로 전송
const luser = req.session.loginUser;
const dt = new Date();
res.render('boardView', {board, luser, dt});
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.7.4. views/boardView.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>boardView.html</title>
<link rel="stylesheet" type="text/css" href="/board.css">
<style>
#time{text-align: center;}
#writer{text-align: center;}
#remove{text-align: center;}
</style>
</head>
<body>
<div id="wrap">
<h1 style="text-align: center;">게시글 내용 </h1>
<table>
<tr>
<th width="150" align="cneter">번호</th>
<td width="200" align="cneter">{{board.id}}
<input type="hidden" id="boardnum" value="{{board.id}}">
</td>
<th width="150" align="cneter">작성자</th>
<td width="200" align="cneter">{{board.writer}}</td>
</tr>
<tr>
<th align="center">작성일</th>
<td align="center">{{board.create_at | date("YYYY/MM/DD")}}</td>
<th align="center">조회수</th>
<td align="center">{{board.readCount}}</td>
<tr>
<th align="center" width="150">제목</th>
<td colspan="3"> {{board.subject}}</td>
</tr>
</tr>
<tr height="300"><th align="center" width="150">내용</th>
<td colspan="2" height="300" width="300"><pre>{{board.content}}</pre><br></td>
<td width="150" align="center">
{% if board.filename %}
<img src="/upload/{{board.realfilename}}" width="150"/>
{% endif %}
</td>
</tr>
<tr height="50"><td colspan="4" align="center">
<input type="button" value="수정" onclick="location.href='/boards/UpdateForm/{{board.id}}'"/>
<input type="button" value="삭제" onclick="location.href='/boards/deleteBoard/{{board.id}}'"/>
<input type="button" value="돌아가기" onclick="location.href='/boards/'"/>
</td></tr>
</table><br><br>
<form id="reply-list">
<table>
<thead>
<tr>
<th width="140" align="center">작성일시</th>
<th width="90" align="center">작성자</th>
<th width="400" align="center">내용</th>
<th width="70" align="center"> </th>
</tr>
<tr>
<td align="center">{{dt | date("MM/DD HH:mm")}}</td>
<td align="center">{{luser.userid}}
<input type="hidden" id="writer" value="{{luser.userid}}"></td>
<input type="hidden" id="boardnum" value="{{board.id}}">
<td> <input type="text" size="50" id="reply"></td>
<td align="center"><input type="submit" value="작성"></td>
</tr>
</thead>
<tbody><tr><td></td><td></td><td></td><td></td></tr></tbody>
</table>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
getReplys('{{board.id}}','{{luser.userid}}');
async function getReplys(boardnum, loginUser){
try {
const result = await axios.get( `/boards/replyList/${boardnum}`);
const replys = result.data;
const tbody = document.querySelector('#reply-list tbody');
tbody.innerHTML='';
replys.map(function(reply){
const row = document.createElement('tr');
let td = document.createElement('td');
td.textContent = reply.created_at.substring(5,7) + "/"
+ reply.created_at.substring(8,10) + " "
+ reply.created_at.substring(11,13) + ":"
+ reply.created_at.substring(14,16);
td.id = 'time';
row.append(td);
td=document.createElement('td')
td.textContent = reply.writer;
td.id = 'writer';
row.appendChild(td);
td = document.createElement('td');
td.textContent = reply.content;
td.id = 'content';
row.appendChild(td);
const remove1 = document.createElement('input');
remove1.setAttribute('type', 'button');
remove1.value = '삭제';
td = document.createElement('td');
td.id = 'remove';
if(reply.writer == loginUser){
td.appendChild(remove1);
} else {
td.innerHTML = ' ';
}
row.appendChild(td);
tbody.appendChild(row);
});
} catch (err) {
console.error(err);
}
}
</script>
</div>
</body>
</html>
3.7.5. routers/boards.js - router.get('replyList/:boardnum')
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시물의 댓글들 조회
router.get('/replyList/:boardnum', async (req, res, next)=>{
try {
const replys = await Reply.findAll({
where:{boardnum:req.params.boardnum},
order:[['id','DESC']],
});
res.json(replys);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.8. 댓글 작성
3.8.1. views/boardView.html
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
<h1 style="text-align: center;">게시글 내용 </h1>
<table>
...
</table><br><br>
<form id="reply-list">
<table>
<thead>
<tr>
<th width="140" align="center">작성일시</th>
<th width="90" align="center">작성자</th>
<th width="400" align="center">내용</th>
<th width="70" align="center"> </th>
</tr>
<tr>
<td align="center">{{dt | date("MM/DD HH:mm")}}</td>
<td align="center">{{luser.userid}}
<input type="hidden" id="writer" value="{{luser.userid}}"></td>
<input type="hidden" id="boardnum" value="{{board.id}}">
<td> <input type="text" size="50" id="reply"></td>
<td align="center"><input type="submit" value="작성"></td>
</tr>
</thead>
<tbody><tr><td></td><td></td><td></td><td></td></tr></tbody>
</table>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
...
document.getElementById('reply-list').addEventListener('submit', async (e)=>{
e.preventDefault();
const writer = e.target.writer.value;
const boardnum = e.target.boardnum.value;
const reply = e.target.reply.value;
if(!reply){return alert('댓글내용을 입력하세요');}
try{
await axios.post('/boards/addReply', {writer, boardnum, reply});
getReplys(boardnum, writer);
}catch(err){
console.error(err);
}
e.target.reply.value='';
});
</script>
</div>
</body>
</html>
3.8.2. routers/boards.js - router.post('/addReply')
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 댓글 작성
router.post('/addReply', async (req, res, next)=>{
try {
await Reply.create({
writer:req.body.writer,
content:req.body.reply,
boardnum:req.body.boardnum,
});
res.end();
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.9. 댓글 삭제
3.9.1. views/boardView.html
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
<h1 style="text-align: center;">게시글 내용 </h1>
<table>
...
</table><br><br>
<form id="reply-list">
<table>
<thead>
<tr>
<th width="140" align="center">작성일시</th>
<th width="90" align="center">작성자</th>
<th width="400" align="center">내용</th>
<th width="70" align="center"> </th>
</tr>
<tr>
<td align="center">{{dt | date("MM/DD HH:mm")}}</td>
<td align="center">{{luser.userid}}
<input type="hidden" id="writer" value="{{luser.userid}}"></td>
<input type="hidden" id="boardnum" value="{{board.id}}">
<td> <input type="text" size="50" id="reply"></td>
<td align="center"><input type="submit" value="작성"></td>
</tr>
</thead>
<tbody><tr><td></td><td></td><td></td><td></td></tr></tbody>
</table>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
getReplys('{{board.id}}','{{luser.userid}}');
async function getReplys(boardnum, loginUser){
try {
const result = await axios.get( `/boards/replyList/${boardnum}`);
const replys = result.data;
const tbody = document.querySelector('#reply-list tbody');
tbody.innerHTML='';
replys.map(function(reply){
const row = document.createElement('tr');
let td = document.createElement('td');
td.textContent = reply.created_at.substring(5,7) + "/"
+ reply.created_at.substring(8,10) + " "
+ reply.created_at.substring(11,13) + ":"
+ reply.created_at.substring(14,16);
td.id = 'time';
row.append(td);
td=document.createElement('td')
td.textContent = reply.writer;
td.id = 'writer';
row.appendChild(td);
td = document.createElement('td');
td.textContent = reply.content;
td.id = 'content';
row.appendChild(td);
const remove1 = document.createElement('input');
remove1.setAttribute('type', 'button');
remove1.value = '삭제';
// 삭제 버튼 클릭시 이벤트
remove1.addEventListener('click', async ()=>{
try {
await axios.delete(`/boards/deleteReply/${reply.id}`);
getReplys(boardnum, loginUser);
} catch (err) {
console.error(err);
}
});
td = document.createElement('td');
td.id = 'remove';
if(reply.writer == loginUser){
td.appendChild(remove1);
} else {
td.innerHTML = ' ';
}
row.appendChild(td);
tbody.appendChild(row);
});
} catch (err) {
console.error(err);
}
}
...
</script>
</div>
</body>
</html>
3.9.2. routers/boards.js - router.delete('/deleteReply/:id')
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 댓글 삭제
router.delete('/deleteReply/:id', async (req, res, next)=>{
try {
const result = await Reply.destroy({
where:{id:req.params.id}
});
res.json(result);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.10. 게시글 제목에 댓글count 표시하기
3.10.1. views/main.html
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div id="wrap">
{{lUser.userid}}({{lUser.name}})님 어서오세요
<input type="button" value="회원수정" onClick="location.href='/members/updateForm/{{lUser.userid}}'">
<input type="button" value="로그아웃" onClick="location.href='/members/logout'">
<input type="button" value="글쓰기" onClick="location.href='/boards/writeForm/'"><br><br>
<table id="board-list" align="left">
<thead>
<tr><th width="100">번호</th><th width="400">제목</th><th width="100">작성자</th><th width="100">조회수</th></tr>
</thead>
<tbody><tr><td></td><td></td><td></td></tr></tbody>
</table>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/main.js"></script>
</div>
</body>
</html>
3.10.2. public/main.js
getBoard_list();
// 데이터베이스에서 게시물들을 읽어와 table의 tbody와 tr과 td로 삽입하는 함수
async function getBoard_list(){
try{
const res = await axios.get('/boards/boardList');
const boards = res.data;
//테이블의 tbody 안을 비운다.
const tbody = document.querySelector('#board-list tbody');
tbody.innerHTML='';
boards.map( async function(board){
const row = document.createElement('tr');
// 게시글 클릭시 boardView로 이동
row.addEventListener('click',()=>{
location.href="/boards/boardView/"+board.id;
});
let td = document.createElement('td');
td.textContent = board.id;
td.id = 'boardnum';
row.append(td);
td = document.createElement('td');
// 현재 게시물의 댓글 개수를 조회해 제목 옆에 표시한다.
// 갯수 : 조회된 객체.length
let tContent = board.subject;
try {
const result = await axios.get(`/boards/replycnt/${board.id}`);
const data = result.data;
//let cnt = data.length;
let cnt = data.cnt;
if(cnt!=0){
tContent = tContent + ' <span style="color:red; font-weight:bold">['+ cnt +']</span>';
}
} catch (err) {
console.error(err);
}
td.innerHTML = tContent;
row.appendChild(td);
td = document.createElement('td');
td.textContent = board.writer;
td.id = 'writer';
row.appendChild(td);
td = document.createElement('td');
td.textContent = board.readCount;
td.id = 'readCount';
row.appendChild(td);
tbody.appendChild(row);
});
}catch(err){
}
}
3.10.3. routers/boards.js - router.get('/replycnt/:id')
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 댓글개수 조회
router.get('/replycnt/:id', async (req, res, next)=>{
try {
const result = await Reply.findAll({
where:{boardnum:req.params.id},
});
//res.json(result);
res.json({cnt:result.length})
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.10. 게시물 수정 폼으로 이동
3.10.1. boardView.html / html부분
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
<h1 style="text-align: center;">게시글 내용 </h1>
<table>
<tr>
<th width="150" align="cneter">번호</th>
<td width="200" align="cneter">{{board.id}}
<input type="hidden" id="boardnum" value="{{board.id}}">
</td>
<th width="150" align="cneter">작성자</th>
<td width="200" align="cneter">{{board.writer}}</td>
</tr>
<tr>
<th align="center">작성일</th>
<td align="center">{{board.create_at | date("YYYY/MM/DD")}}</td>
<th align="center">조회수</th>
<td align="center">{{board.readCount}}</td>
<tr>
<th align="center" width="150">제목</th>
<td colspan="3"> {{board.subject}}</td>
</tr>
</tr>
<tr height="300"><th align="center" width="150">내용</th>
<td colspan="2" height="300" width="300"><pre>{{board.content}}</pre><br></td>
<td width="150" align="center">
{% if board.filename %}
<img src="/upload/{{board.realfilename}}" width="150"/>
{% endif %}
</td>
</tr>
<tr height="50"><td colspan="4" align="center">
<input type="button" value="수정" onclick="location.href='/boards/updateForm/{{board.id}}'"/>
<input type="button" value="삭제" onclick="location.href='/boards/deleteBoard/{{board.id}}'"/>
<input type="button" value="돌아가기" onclick="location.href='/boards/'"/>
</td></tr>
</table><br><br>
<form id="reply-list">
...
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
...
</script>
</div>
</body>
</html>
3.10.2. boards.js - router.get( '/updateForm/:id' )
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시물 수정폼으로 이동
router.get('/updateForm/:id', async (req,res,next)=>{
// 전달된 아이디로 게시물을 조회한 후 updateForm.html로 렌더링,
// 세션에 있는 '유저아이디'와 조회한 게시물이 같이 이동한다.
try{
const board = await Board.findOne({
where:{id:req.params.id},
});
const luser = req.session.loginUser; // loginUser 세션을 가져와 저장
res.render('updateForm',{board, luser});
}catch(err){
console.error(err);
next(err);
}
});
module.exports = router;
3.10.3. updateForm.html / 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" type="text/css" href="/board.css">
</head>
<body>
<form id ="update-form">
<table>
<tr><th width="150" align="center">번호</th>
<td width="150" align="center">{{board.id}} <input type="hidden" name="id" value="{{board.id}}"/> </td><th width="150" align="center">작성자</th>
<td width="150" align="center">{{board.writer}} <input type="hidden" name="writer" value="{{board.writer}}"/> </td></tr>
<tr><th width="150" align="center">작성일</th>
<td width="150" align="center">{{board.created_at | date("YYYY/MM/DD")}}</td>
<th align="center">조회수</th> <td align="center">{{board.readCount}}</td></tr>
<tr><th align="center" width="150">제목</th><td width="150" colspan="3"> <input type="text" name="subject" size="50" value="{{board.subject}}"> </td>
<tr><th align="center" width="150">내용</th><td width="150" colspan="3"> <textarea name="text" rows="15" cols="60">{{board.content}}</textarea> </td></tr>
<tr><th align="center" width="150">이미지</th><td colspan="3"> <img src="/upload/{{board.realfilename}}" width="50"/><br><input type="file" name="image" id="image"/><br>이미지를 수정하고자 할 때만 선택하세요 </td></tr>
<tr height="80"><td colspan="4" align="center">
<input type="submit" value="수정">
<input type="button" value="돌아가기" onclick="location.href='/boards/boardView2/{{board.id}}'"></td></tr>
</tr>
</table>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
...
</script>
</body>
</html>
3.11. 게시물 수정 동작
3.11.1. updateForm.html / html 부분
<!DOCTYPE html>
...
</head>
<body>
<form id ="update-form">
<table>
<tr><th width="150" align="center">번호</th>
<td width="150" align="center">{{board.id}} <input type="hidden" name="id" value="{{board.id}}"/> </td><th width="150" align="center">작성자</th>
<td width="150" align="center">{{board.writer}} <input type="hidden" name="writer" value="{{board.writer}}"/> </td></tr>
<tr><th width="150" align="center">작성일</th>
<td width="150" align="center">{{board.created_at | date("YYYY/MM/DD")}}</td>
<th align="center">조회수</th> <td align="center">{{board.readCount}}</td></tr>
<tr><th align="center" width="150">제목</th><td width="150" colspan="3"> <input type="text" name="subject" size="50" value="{{board.subject}}"> </td>
<tr><th align="center" width="150">내용</th><td width="150" colspan="3"> <textarea name="text" rows="15" cols="60">{{board.content}}</textarea> </td></tr>
<tr><th align="center" width="150">이미지</th><td colspan="3"> <img src="/upload/{{board.realfilename}}" width="50"/><br><input type="file" name="image" id="image"/><br>이미지를 수정하고자 할 때만 선택하세요 </td></tr>
<tr height="80"><td colspan="4" align="center">
<input type="submit" value="수정">
<input type="button" value="돌아가기" onclick="location.href='/boards/boardView2/{{board.id}}'"></td></tr>
</tr>
</table>
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
...
</script>
</body>
</html>
3.11.2. updateForm.html / script 태그 부분
<!DOCTYPE html>
...
</head>
<body>
<form id ="update-form">
...
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
document.getElementById('update-form').addEventListener('submit', async (e)=>{
e.preventDefault();
const id = e.target.id.value;
const subject = e.target.subject.value;
const text = e.target.text.value;
if(!subject){return alert('제목을 입력하세요');}
if(!text){return alert('내용을 입력하세요');}
const formData = new FormData();
formData.append('id', id); //게시글 아이디(번호)
formData.append('subject', subject);
formData.append('text', text);
formData.append('image', e.target.image.files[0]);
try{
await axios.post('/boards/update', formData);
// boardView2는 조회수 카운트를 안하는 라우터
location.href='/boards/boardView2/{{board.id}}';
}catch(err){
}
});
</script>
</body>
</html>
3.11.3. boards.js - router.post( '/update' )
조회수를 올리지 않고, 게시물을 조회하는 라우터, 이 라우터가 수행을 완료하면 다시 await했던 스크립트로 돌아가고, 다음 줄의 코드를 수행한다.
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시물 수정
router.post('/update', upload.single('image'), async (req,res,next)=>{
try {
if(req.file != undefined){
// 업로드된 파일이 있다면 (undefined가 아니라면)
await Board.update({
subject:req.body.subject,
content:req.body.text,
filename:req.file.originalname,
realfilename:req.file.filename,
},{
where : {id:req.body.id},
});
}else{
// 업로드된 파일이 없다면 (undefined라면)
await Board.update({
subject:req.body.subject,
content:req.body.text,
},{
where : {id:req.body.id},
});
}
res.end();
// axios를 사용했기 때문에 redirect코드는 의미 없음
// res.redirect('/boards/boardView2/'+ req.body.id);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.11.4. boards.js - router.get( '/boardView2/:id' )
- update 라우터를 완료하고 다음 줄 코드인 location.href='/boards/boardView2/{{board.id}}';로 이 라우터가 동작한다.
- 게시물을 조회하지만 조회수를 올리지 않는 라우터이다.
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시물 보기 ( 조회수 카운트X )
router.get('/boardView2/:id', async (req, res,next)=>{
try {
// 게시물을 검색
const board = await Board.findOne({
where:{id:req.params.id},
});
// render로 전송
const luser = req.session.loginUser;
const dt = new Date();
res.render('boardView', {board, luser, dt});
} catch(err) {
console.error(err);
next(err);
}
});
module.exports = router;
3.12. 게시물 Write 동작 수정
게시물 작성 동작 시 이미지 업로드를 하지 않을 때 처리부분 추가
3.12.1. boards.js - router.post('/writeBoard')
- request에서 넘겨받은 file이 'undefined(정의되지 않음)'가 아니라면 ( = 업로드한 파일이 있다면 )
- filename, realfilename을 포함하여 데이터를 db에 담는다.
- request에서 넘겨받은 file이 'undefined(정의되지 않음)'라면 ( = 업로드한 파일이 없다면 )
- filename, realfilename을 제외하고 db에 담는다.
// 게시글 등록 동작
router.post('/writeBoard', upload.single('image'), async (req,res,next)=>{
// 파일 업로드와 게시글 insert를 완성.
// filename필드명에는 서버에 저장되는 파일명(현재날짜와 시간이 밀리초로 변환된 값 + 파일명
// realfilename 필드명에는 원래 파일명
try {
if(req.file != undefined){
const board = await Board.create({
subject : req.body.subject,
writer : req.body.writer,
content : req.body.text,
filename : req.file.originalname,
realfilename : req.file.filename,
});
res.json(board);
}else{
const board = await Board.create({
subject : req.body.subject,
writer : req.body.writer,
content : req.body.text,
});
res.json(board);
}
} catch (err) {
console.error(err);
next(err);
}
});
3.13. 게시물 삭제 동작
3.13.1. boardView.html / html 부분
<!DOCTYPE html>
...
</head>
<body>
<div id="wrap">
<h1 style="text-align: center;">게시글 내용 </h1>
<table>
<tr>
<th width="150" align="cneter">번호</th>
<td width="200" align="cneter">{{board.id}}
<input type="hidden" id="boardnum" value="{{board.id}}">
</td>
<th width="150" align="cneter">작성자</th>
<td width="200" align="cneter">{{board.writer}}</td>
</tr>
<tr>
<th align="center">작성일</th>
<td align="center">{{board.create_at | date("YYYY/MM/DD")}}</td>
<th align="center">조회수</th>
<td align="center">{{board.readCount}}</td>
<tr>
<th align="center" width="150">제목</th>
<td colspan="3"> {{board.subject}}</td>
</tr>
</tr>
<tr height="300"><th align="center" width="150">내용</th>
<td colspan="2" height="300" width="300"><pre>{{board.content}}</pre><br></td>
<td width="150" align="center">
{% if board.filename %}
<img src="/upload/{{board.realfilename}}" width="150"/>
{% endif %}
</td>
</tr>
<tr height="50"><td colspan="4" align="center">
<input type="button" value="수정" onclick="location.href='/boards/updateForm/{{board.id}}'"/>
<input type="button" value="삭제" onclick="location.href='/boards/deleteBoard/{{board.id}}'"/>
<input type="button" value="돌아가기" onclick="location.href='/boards/'"/>
</td></tr>
</table><br><br>
<form id="reply-list">
...
</form>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
...
</script>
</div>
</body>
</html>
3.13.2. boards.js - router.get( '/deleteBoard/:id' )
const express = require('express');
const Member = require('../models/member'); // 경로는 소문자인데 왜 확인하면 대문자지?
const Board = require('../models/board');
const Reply = require('../models/reply');
// 직접 사용할 라우터파일에서 필요한 require를 사용하는 것이 효율적일 수 있다.
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 파일이 업로드 될 폴더 설정(없으면 새로 만듦)
// upload폴더를 public에 넣는 이유는, public이 static이기 때문
try{
fs.readdirSync('public/upload');
}catch(error){
console.error('upload폴더가 없어 upload폴더를 생성');
fs.mkdirSync('public/upload');
}
// multer객체 설정
const upload = multer({
storage: multer.diskStorage({
destination(req,file,done){
done(null, 'public/upload/');
},
filename(req,file,done){
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname,ext)+Date.now() + ext);
},
}),
limits:{fileSize:5*1024*1024},
});
...
// 게시물 삭제
router.get('/deleteBoard/:id', async (req,res,next)=>{
try {
await Board.destroy({
where:{id:req.params.id},
});
res.redirect('/boards');
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;