[node] Chapter 7. 페이지 네이션 되는 게시판 만들기
Chapter 7. 페이지 네이션 되는 게시판 만들기
📚 도서,Node.js백엔드 개발자 되기 Chapter 7
7.1 구조
- MVC 패턴 구조 만들기
- 컨트롤러 는 app.js 에서.
- service 는 post-service.js 하나만 만듬.
- chapter7 > board > npm init -y : 프로젝트 초기화
7.2.2 익스프레스 설치 및 플로젝트 디렉터리 구조 잡기
라이브러리 설치
npm i express@4.17.3 # npm i mongodb@4.13.0 npm i mongodb@latest
mongodb 버전을 4.13.0 으로 하면 4.7 이상 부터는 클라이언트 접속할때
version: ServerApiVersion.v1 으로 입력해야함. 이 버전이 안맞으면 db 가 생기지 않으므로 그냥 최신 버전의 mongodb 를 인스톨 한다.향후에 mongodb-connection.js 에서 setting 함.
구조별로 디렉터리를 먼저 만들어준다.
mkdir configs mkdir services mkdir views vi app.js ## controller 역할
7.2.3 핸들바 템플릿 엔진 설치 및 설정
뷰로 웹페이지를 보기위한 템플릿 엔진 : 퍼그, ejs, 머스태시, 핸들바 가 있음.
핸들바는 머스태시와 호환, 추가기능 제공 적절..
express-handlebarsnpm i express-handlebars@6.0.3
app.js
const express = require("express") const handlebars = require("express-handlebars") const app = express()
app.engine("handlebars", handlebars.engine({ layoutsDir:"views"})) // 템플릿 엔진으로 핸들바 등록
app.set("view engine","handlebars") // 웹 페이지 로드시 사용할 템플릿 엔진 설정
app.set("views", __dirname +"/views") // 뷰 디렉터리를 view 로 설정
// 라우터 설정
app.get("/", (req,res) => {
res.render("home", {title: " 안녕하세요", message: "만나서 반값습니다!"})
})
app.listen(80)
### **views > main.handlebars**
```HTML
<html>
<head>
<meta charset="utf-8"/>
<title> 게시판 프로젝트</title>
</head>
<body>
{{{body}}} {!-- --}
</body>
</html>
views > home.handlebars
<h2>{{title}}</h2>
<p>{{message}}</p>
랜더링에 들어갈 요소를 바디에 집어넣어준다.
7.3.1. 리스트 화면 기획
게시판 제목
게시판 검색 기능
게시판 글쓰기 기능
제목 ( 100자 까지 입력가능)
작성자 항목
조회수 항목 (게시글 클릭시 조회 수 증가)
등록일 항목 (yyyy.mm.dd)
페이징 기능 ( 한페이지에 게시글 10개)
7.3.2 글쓰기 화면 기획
- 게시판 제목 글 작성
- 제목 입력
- 이름 입력
- 비밀번호 박스
7.3.3 상세 화면 기획
- [게시판 이름] 제목
- 아무개 (수정/삭제)
- 내용
- n개의 댓글이 있습니다.
- 댓글 리스트
- 댓글 입력 창
- 목록으로
7.4 UI 화면 만들기
// app.js const express = require("express") const handlebars = require("express-handlebars") const app = express()
app.engine("handlebars", handlebars.engine({ layoutsDir:"views"})) // 템플릿 엔진으로 핸들바 등록
app.set("view engine","handlebars") // 웹 페이지 로드시 사용할 템플릿 엔진 설정
app.set("views", __dirname +"/views") // 뷰 디렉터리를 view 로 설정
// 라우터 설정
app.get("/", (req,res) => {
res.render("home", {title: " Test Board", message: "GRK Partners!"})
})
app.listen(80)
## 7.4.1 리스트 UI
```HTML
<!--home.handlebars-->
<h1>{{title}}</h1> <!--title 영역 -->
<!-- 검색어 영역 -->
<input type="text" name="search" id="search" value="" size="50" placeholder="검색어를 입력하세요."/>
<button>검색</button>
<br />
<a href="/write"> 글쓰기 </a>
<div>
<table>
<thead>
<tr>
<th width = "50%">제목</th>
<th>작성자</th>
<th>조회수</th>
<th>등록일</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/detail">타이틀</a></td>
<td align="center">작성자 이름</td>
<td align="center">9999</td>
<td align="center">2025.1.13</td>
</tr>
</tbody>
</table>
</div>
<!--페이징 영역-->
<div>
<a><<</a>
<a><</a>
<a>1</a>
<a>2</a>
<a>3</a>
<a>4</a>
<a>></a>
<a>>></a>
</div>
결과

7.4.2 글쓰기 UI 생성
- 글쓰기 페이지 form 태그 , input, textarea 태그 사용하여 데이터 입력
- app.js 에 라우터 함수 없음. 라우터 함수 추가 필요.
(2) 글쓰기 ui html(handlebars) 작성
chapter7/board/views/write.handlebars
<h1>[{{title}}] 글 작성 </h1>
<div>
<!-- (1) 글쓰기 폼 -->
<form name="boardForm" method="post" action="/write">
<!-- (2) 제목입력 칸 -->
<div>
<label> title </label>
<input type="text" name = "title" placeholder="input title" value="" />
</div>
<!-- (3)이름 입력 칸 -->
<div>
<label>project name</label>
<input type="text" name="writer" placeholder="input your name" value="" />
</div>
<!-- (4) 비밀 번호 입력 칸 -->
<div>password</div>
<input type="password" name="password" placeholder="please input password" value="" />
<!-- (5) 본문 입력 -->
<div>
<label>insert main content</label></div><br>
<textarea placeholder="main" name="content" cols=" "50" rows="10" ><</textarea>
</br>
</div>
<div>
<!--(6) 버튼 영역-->
<button type="submit">저장</button>
<button type="button" onclick="location.href='/'">취소</button>
</div>
</form>
</div>
- form 태그 : 다른곳에서 form 찾을 수 있게 name 지정
- post 통신으로 데이터를 전송함.
- action : 서버의 주소값 입력
- 서버에서는 post 이며 url이 write 인 핸들러 함수가 필요함.
(2) app.js 에 핸들러 함수 추가
// write( 글쓰기 )
app.get("/write", (req,res) => {
res.render("write", {title : "Project board "})
})
7.4.3 상세페이지 UI 생성
- write 로 작성한 게시물 표시, 수정, 삭제 , 댓글 추가, 표시,삭제 기능
<!--chapter7/board/views/detail.handlebars--> <h1>{{title}}</h1> <!-- (1) 게시글 제목 --> <h2 class="text-xl"> Project title</h2> <!-- (2)) 작성자 이름 --> <div> Writer : <b> Writer name</b> </div> <!-- (3) 조회수 와 작성일시--> <div> 조회수 : 999 | 작성일 시 : 2025-01-16 099:03:00 <button onclick="modifyPost()">수정</button> <button onclick="deletePost()">삭제</button> </div>
THIS IS A TEST HO!
3개의 댓글이 있습니다.
{{comment}}
(2) app.js 에서 핸들러 함수를 만듬.
// detail page
app.get("/detail/:id", async (req,res) => {
res.render("detail", {
title: "Test board"
})
})
- forrestest.site/detail/1 로 아무 id 나 집어넣어도 상세페이지를 확인 할수 있다.
결과
7.5 API 만들기
- 인증 기능 생략, 게시글 마다 패스워드 넣어서 수정 삭제
- UI 단의 자바 스크립트 최소 유지
- 백엔드 포커스 맞춰 진행
- 글쓰기 -> 리스트 -> 상세페이지 -> 글 수정,삭제,댓글 추가 -> 댓글 삭제 순서
7.5.1 몽고 디비 연결을 위한 유틸리티 생성
(1) config 폴더 > mongodb-connection.js 파일 생성
// configs/mongodb-connection.js
const { MongoClient } = require("mongodb")
// 몽고디비 연결 주소
// MongoDB ACCESS
const MongoClient = require('mongodb').MongoClient;
//url 마지막 board 로 기본 db 생성( 첫데이터 추가시 지정한 데이터베이스 자동 생성 )
const url = "mongodb+srv://pulpilisory:qwe123@cluster0.kbog4.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0/board";
const client = new MongoClient(url, {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true,
}
});
module.exports = function (callback){
// 몽고디비 커넥션 연결 함수 반환
return client.connect(url,callback)
}
(2) app.js 에 추가
// (0) 몽고 디비 연결 함수
const mongodbConnection = require("./configs/mongodb-connection")
//~ 생략 ~//
let collection ;
app.listen(80, async () =>{
console.log("Server started")
// (1) mongodbConnection() 의 결과 mongoClient
const mongoClient = await mongodbConnection()
// (2) mongoClient.db() 로 db 선택 collection() 으로 컬랙션 선택후 collection에 할당.
collection = mongoClient.db().collection("post")
})
- mongodbConnection(콜백)과 같은 형태로 사용. 본문 코드에 콜백이 없으므로 콜백 실행 없이 MongoClient 객체를 반환
- (2) mongoClient 에서 db()를 사용해 데이터 베이스 선택함. collection('post') 를 사용해 컬랙션을 선택ㅎ마.
- db() 대신 명시적으로 db('board')를 사용 해도 됨.
- 데이터 베이서 설정파일에서 이미 기본 데이터베이스를 board 로 넣어 두었으므로 빈값을 넣어도 됨.
- collection 변수는 글로벌 변수임. mdb 라이브러리 내부 커낵션 풀 관리하므로 글로벌 변수 사용 문제 안됨
7.5.2 page 에서 사용할 핸들바 커스텀 헬퍼 만들기
핸들 바에서 each, if 등 기본적인 헬퍼 함수를 제공
그외에 모든 것은 커스텀 헬퍼 함수를 구현해서 사용해야함.
커스텀 헬퍼 사용시 설정도 조금 변경해야함.
(1) config 폴더 안에 헬퍼 함수 작성
// chapter/board/configs/handlebars-helpers.js
module.exports = {
// (1) 주어진 배열의 list 길이 반환 list 객체가 null 인경우 빈값 -> 0이나오도록 설정.
lengthOfList: (list = []) => list.length,
// (2) 두값 비교하여 같은지 여부 반환
eq: (val1, val2) => val1 === val2,
// (3) ISO 날짜 문자열에서 날짜만 반환
dateString: (isoString) => new Date(isoString).toLocaleDateString(),
}
- (3) : 날짜 데이터 저장시 2024-01-20T10:00:00.000Z 같은 ISO 문자열(표준시)로 저장함.(우리나라 +9)
JS 에서 사용 예시
handlebar 에서 사용예시const helper = require("./handlebar-helpers.js") const isEqual = helper.eq(5.5); const dateStr = helper.dateString("2025-01-20")
{{헬퍼함수 1 (헬퍼함수2 변수1 변수2) 변수11}} {{lengthOfList comments}} 개의 댓글이 있습니다. 작성일시 : {{dateString createdDt }} {{#if (eq .@root.paginator.page)}}eq 테스트 {{/if}}
- '.' 과 root 는 각각 현재 객체와 최상의 객체를 의미함.
(2) 핸들바 커스텀 함수 설정
app.js > app.engine("handlebars", handlebars.enginee());
설정을 변경
// app.js 핸들바 커스텀 함수 설정 추가
app.engine(
"handlebars",
handlebars.create({
helpers: requestAnimationFrame("./configs/handlebars-helpers"),
}).engine,
handlebars.engine({ layoutsDir : "views"})
);
- handlebar.create()는 handlebars 객체를 만들때 사용함. 옵션에서 헬퍼함수를 추가 할수잇음.
7.5.3 nodemon 설정
cd chapter7/board
npm i nodemon@2.0.20
package.json 의 script 에 'start' 명령어에 nodemon 을 통해 app.js 를 실행하도록 설정한다.
파일이 저장될 떄 서버를 재기동 시켜줌.
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "npx nodemon app.js"
},
7.5.4 글쓰기 api 만들기
글쓰기 api 는 http post 메서드를 사용시 데이터를 req.body 로 넘김.
(1) 미들웨어 설정 추가
// 미들 웨어 설정 추가 req.body post 요청 해석 설정
app.use(express.json())
app.use(express.urlencoded({extended: true}))
(2) write post 라우팅 추가
service file 폴더(계층) 에서 글 posting 하는 기능을 만들어서 사용한다.
post-service.js 에는 writePost 함수가 있고 이 함수는 collection(post doc) 과 요청의 body 값을 가지고 함수를 실행한다.
// 글쓰기 저장 post write
// (1) 서비스 파일 로딩
const postService = require("./services/post-service")
app.post("/write", async (req, res) => {
const post = req.body;
// (2) 글쓰기 후 결과 반환
const result = await postService.writePost(collection, post)
// (3) 생성된 document 의 _id 를 사용해 상세 페이지로 이동
res.redirect(`/detail/${result.insertedId}`)
})
- post 에 저장된 내용을 몽고디비 저장하고 결과 반환함.
- writePost() 함수에서 promise 를 넘기므로 함수 앞에 async 를 붙여야함.
- 저장 결과인 result 에는 도큐먼트의 식별자로 사용가능한 insertedId 값이 있음.
- 글을 쓰고 난뒤에는 해당 값을 통해 상세 페이지로 넘어가게됨
(3) services/post-service.js 파일을 생성하고 writePost 작성
// chapter7/board/services/post-service.js
// 글쓰기 함수
async function writePost(collection, post) {
// (1) 생성일시와 조회수를 입력
post.hits = 0
// (2) 날짜는 ISO 포맷으로 저장
post.createdDt = new Date().toISOString()
// (3) 몽고 디비에 post 를 저장 후 결과 반환
return await collection.insertOne(post)
}
//(4) require() 로 파일을 임포트 시 외부로 노출하는 객체
module.exports ={
writePost,
}
(4) mongoDB compas 를 사용해서 db 에 post 가 들어오는지 확인

7.5.5 리스트 api 만들기
조회, 검색과 페이지 네이션
- 리스트의 검색창 및 검색 버튼 수정
검색어 입ㄹ겨 > 검색어 클릭 > 검색어 정보를 서버에 요청
(1) home.handlebars 검색 버튼 수정.
<!-- 검색어 영역 -->
<!-- (1) value 에 검색어 데이터를 넣음. -->
<input type="text" name="search" id="search" value="{{search}}"
size="50" placeholder="Input search word"/>
<!-- (2) 버튼 클릭시 search 변수에 검색어 데이터를 담아서 서버로 보냄. -->
<button onclick ="location.href=`/?search=${document.getElementById('search').value}`">검색</button>
<br />
- 버튼 클릭시 이벤트 추가. 자바스크립트 최소화 를 위해 onclick 속성 안에 js 함수를 한줄로 넣음.
- 별도 자바스크립트 함수 만들어 추가 해도됨
- 클릭시 input 박스에 있는 데이터를 담아서 서버로 요청을 보냄.
(2) 리스트 api 백엔드 코드 작성. app.get("/" ...) 부분 수정
// root page
app.get("/", async (req,res) => {
// (1) 현재 페이지 데이터
const page = parseInt(req.query.page) || 1;
const search = req.query.search || "";
try {
// (2) postService.list 에서 글 목록과 페이지네이터를 가져옴
const [posts,paginator] = await postService.list(collection, page, search);
// (3) list page 랜더링
res.render("home", {
title: " GRK partners project management",
message: "GRK Partners!",
search,
paginator,
posts
})
} catch (error) {
console.log(error)
// error 가 나는 경우 빈값으로 렌더링
res.render("home", {
title: " GRK partners project management",
message: "GRK Partners!",
})
}
})
- (1) : get 으로 url 뒤에 변수 추가 하는 경우 req.query 객체로 변수의 값을 받아 올수 있음.
- 리스트 페이지 이므로 현재 페이지 데이터와 검색어 데이터를 req.query의 page, search 로 각각 가지고 있음.
- || 는 이전값이 빈값이거나 null 인경우 뒤위 값을 기본값으로 설정.
- (2) : 리스트 데이터를 가져오는 구체적인 로직은 모두 postService.list 에 있음.
- (3) : 객체에 값을 할당시 값으로 사용하는 변수명과 키의 이름이 같다면 변수만 바로넣어도 됨.
(2) postService 에 list() 함수 추가
// paginator util 객체를 생성
const paginator = require("../utils/paginator")
// post 목록 보여주는 함수 list
async function list(collection, page, search){
const perPage = 10;
// (1) title search 와 부분일치 하는지 확인
const query = {title: new RegExp(search, "i")}
// (2) limit 은 10개 만 가져온다는의미 skip은 설정된 개수만큼 건너 뛴다(skip)
// 생성일 역순으로 정렬
const cursor = collection.find(query, {limit : perPage, skip:(page-1) * perPage}).sort({
createdDt: -1,
})
// (3) 검색어에 걸리는 게시물의 총합
const totalCount = await collection.count(query)
// (4) 커서로 받아온 데이터를 리스트로 변경
const posts = await cursor.toArray()
// (5) 페이지네이터 생성
const paginatorObj = paginator({ totalCount, page, perPage: perPage })
return [posts, paginatorObj]
}
(1) : list() 함수는 collection, page, search 3개의 매개변수를 받음. perPage : 한 페이지에 노출할 글 개수.
(2) :글 목록 리스트를 가져올 시 collection의 find() 함수를 사용. 뒤에 sort를 사용하여 정렬
find() 는 cursor 를 반환함
cursor 의 toArray 메서드를 사용해 게시글 데이터를 리스트로 변경함. 옵션으로 limit, skip 을 줌.
(3) totalCount 는 페이지네이터에서 사용함.
(4) cursor 의 메서드는 대부분이 promise 이다
** toArray(), next(), forEach(), hasNext(), close(), count() 모두 비동기적, promise 반환 , await 나 then 을 사용하여 결과를 처리해야함.
** forEach : 커서의 모든 문서를 수노히하며 콜백함수를 실행하는 것도 시간이 걸리는 작업.
limit(), skip(), sort(), project() 메서드는 객체 자체를 반환하여 메서드 체이닝을 지원함.
커서 객체의 설정을 ㅂ련경할 뿐 데이터베이스에 직접 접근하지 않으므로 promise 를 반환하지 않음.
** 몽고디비 쿼리는 js 문법과 매우 유사. sql의 like 와 같은 형식의 검색은 정규 표현식을 사용함.
(3) pagination 유틸 작성
JavaScript 내장함수 ( new 가 필요없이 그냥 씀.)
Math : 수학 관련 객체
String: 문자열 관련 객체
Number: 숫자 관련 객체
Array: 배열 관련 객체
Object: 객체 관련 객체
Date: 날짜 및 시간 관련 객체
Function: 함수 관련 객체
RegExp: 정규 표현식 관련 객체
JSON: JSON 처리 관련 객체
utils 계층을 만들고 paginator.js 생성 > paginator 객체 에 페이지에 대한 정보를 모두 담아서 출력한다.
/* utils/paginator.js*/
// (1) lodash : 배열 등 계산 해주는 객체 npm i lodash
const lodash = require("lodash")
const PAGE_LIST_SIZE = 10 // (2) 최대 몇 개의 페이지를 보여줄지 설정
//(3) 총 개수, 페이지, 한 페이지에 표시하는 게시물 개수를 매개변수로 받음.
module.exports = ({totalCount, page, perPage = 10})=> {
const PER_PAGE = perPage
// (4) 총 페이지 계산
const totalPage = Math.ceil(totalCount /PER_PAGE)
// 시작 페이지 : 몫 * PAGE_LIST_SIZE +1
let quotient = parseInt(page / PAGE_LIST_SIZE)
if (page % PAGE_LIST_SIZE === 0){
quotient -= 1;
}
// (5) 시작 페이지 구하기
const startPage = quotient * PAGE_LIST_SIZE +1
// (6) 끝 페이지 구하기
const endPage = startPage + PAGE_LIST_SIZE -1 < totalPage ? startPage + PAGE_LIST_SIZE : totalPage
const isFirstPage = page === 1;
const isLastPage = page === totalPage;
const hasPrev = page > 1;
const hasNext = page < totalPage
const paginator = {
// (7) 표시할 페이지 번호 리스트 만들어 줌.
pageList : lodash.range(startPage, endPage +1),
page,
prevPage: page -1,
nextPage : page +1,
startPage,
lastPage : totalPage,
hasPrev,
hasNext,
isFirstPage,
isLastPage,
}
return paginator;
}
- (1) : 10 페이지가 나오도록 하려면 시작부터 끝 페이지까지의 숫자가 들어있는 리스트를 만들어야함.
- lodash.range()함수 사용
- (2) : PAGE_LIST_SIZE
- (3) : 페이지네이터는 하나의 함수. totalCount, page, perPage 를 받음.
- (4) : 전체 글을 페이지당 글수 로 나누면 전체 페이지의 수를 알 수 있음.
- 시작, 끝 페이지 를 구함.
(4) home.handlebars 리스트및 페이지네이션 추가 작업.
** handlerbars 패키지 주요 내장 헬퍼 정리
- {{#if condition }} .. {{else}} ... {{/if}}
ex){{#if user.isLoggedIn}} <p>Welcome, {{user.name}}! </p> {{else}} <p> Please log in. </p> {{/if}}
- {{#each array}} ... {{/each}} : 반복 블록 핼퍼
- 주어진 배열의 각 요소에 대해 블록 안의 내용을 반복해서 랜더링함.
- 블록 내에서는 현재 요소의 데이터에 this 키워드로 접근할 수있음.
- {{@index}} 를 사용하여 현재 요소의 인덱스를 가져 올 수있음.
<ul>
{{#each user}}
<li>{{this.name}} ({{this.age}})</li>
{{/each}}
</ul>
- {{#with object}} ... {{/with}} : 컨텍스트 변경 블록 헬퍼
- 컨텍스트 : 템플릿 에서 변수나 속성을 참조할 때 해당 변수나 속성이 어디에서 정의 되었는지 나타내는 범위
- 최상위 컨텍스트가 data {user :{name, age}, posts} 인경우 name 값 사용
- 블록 안에서 주어진 객체를 새로운 컨텍스트로 설정
- 블록 내에서 객체의 속성을 바로 사용 가능.
{{#with user}}
<p> Name : {{name}}</p>
<p> Age : {{age}}</p>
{{/with}}
- {{lookup object key}} : 객체의 값을 가져오는 헬퍼
- {{lookup user 'name'}}
- 사용자 정의 헬퍼
const Handlebars = require('handlebars') Handlebars.registerHelper('formatDate', function(dateString) { const date = new Date(dateString) return date.toLocaleDateString() })
// 템플릿 내에서 사용
// {{ formatDate createdDate}}
Handlebars 컨텍스트와 @root
{{@root}}는 템플릿에 전달된 최상위 레벨의 데이터 객체를 참조한느데 사용되는 특별 변수임.
{{@root.search}} 는 with 블록 내부에서 컨텍스트가 변경되었음에도 불구하고 템플릿에 전달된 최상위 레벨 데이터 객체에서 가져옴.
{{.}} : . 은 현재 컨텍스트를 나타냄. {{each array}} {{/each}} 블록 내에서 . 은 현재 반복중인 배열요소를 가리킴
## pagination 되는 home
```HTML
<!--home.handlebars-->
<!--title 영역 -->
<h1>{{title}}</h1>
<!-- 검색어 영역 -->
<!-- (1) value 에 검색어 데이터를 넣음. -->
<input type="text" name="search" id="search" value="{{search}}"
size="50" placeholder="Input search word"/>
<!-- (2) 버튼 클릭시 search 변수에 검색어 데이터를 담아서 서버로 보냄. -->
<button onclick ="location.href=`/?search=${document.getElementById('search').value}`">검색</button>
<br />
<a href="/write"> 글쓰기 </a>
<br>
<!-- <p> 이페이지에서 사용하는 변수들 </p>
<p> id : {{_id}}</p>
<p> root :{{@root}}</p>
<p> posts :{{posts}}</p> -->
<div>
<table>
<thead>
<tr>
<th width = "50%">제목</th>
<th>작성자</th>
<th>조회수</th>
<th>등록일</th>
</tr>
</thead>
<!-- (1) 게시글 데이터 표시 -->
<tbody>
{{#each posts}}
<tr>
<!-- (2) 상세 페이지 링크 -->
<td><a href="/detail/{{_id}}">{{title}}</a></td>
<td align="center">{{writer}}</td>
<td align="center">{{hits}}</td>
<!-- (3) dateString 헬퍼 함수 사용 -->
<td align="center">{{dateString createdDt}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
<!--페이징 영역-->
<div>
<!--(4) with 내장 헬퍼 함수 paginator 객체 가 컨택스트가 됨.-->
{{#with paginator}}
<!-- (5) @root :서버의 최상위 컨텍스트 참조, '<<' 링크를 통해 page 1로 이동 (괄호는 < 사용)-->
<a href="/?page=1&search={{@root.search}}"><<</a>
{{#if hasPrev}}
<a href="/?page={{prevPage}}&search={{@root.search}}""><</a>"
<!-- (6) 1페이지 인 경우 이전 페이지 가 없으므로 링크가 없음 -->
{{else}}
<a><</a>
{{/if}}
<!-- 현재 페이지 번호 리스트 나타내기 -->
{{#each pageList}}
{{#if (eq . @root.paginator.page)}}
<a>{{.}}</a>
{{else}}
<a href="/?page={{.}}&search={{@root.search}}">{{.}}</a>
{{/if}}
{{/each}}
<!-- '>>' 부분 구현 -->
{{#if hasNext}}
<a href="/?page={{nextPage}}&search={{@root.search}}">></a>
{{else}}
<a>></a>
{{/if}}
<a href="/?page={{lastPage}}&search={{@root.search}}">>></a>
{{/with}}
</div>
7.5.6 상세페이지 api 만들기
(1) app.js에 상세페이지 라우터 설정, 로직 추가
// detail page 상세 페이지로 이동
app.get("/detail/:id", async (req,res) => {
// (1) 게시글 정보 가져오기
try {
const post = await postService.getDetailPost(collection, req.params.id)
console.log("data check : ",post)
res.render("detail", {
title: "게시글 상세 ",
post : post
})
} catch (error) {
console.error("상세 글 조회 중 에러 ",error)
res.status(500).send('Error')
}
})
(2) detail.handbars 수정
<!--views/detail.handlebars-->
<h1>{{title}}</h1>
{{#with post}}
<h2 class="text-xl"> {{title}}</h2>
<div>
작성자 : <b>{{writer}}</b>
</div>
<div>
조회수 : {{hits}} | 작성일시 :{{dateString createdDt}}
{{!-- <button onclick="modifyPost()">수정</button>
<button onclick="deletePost()">삭제</button> --}}
<button type="button" id="modifyButton">수정</button>
<button type="button" id="deleteButton">삭제</button>
</div>
<div>
<pre>{{content}}</pre>
</div>
<section>
<div>
<h3> 3개의 댓글이 있습니다. </h3>
</div>
<!-- (7) 댓글 작성폼 -->
<form method="post" action="/write-comment">
<div>
<input type="text" name="name" placeholder="input name" />
<input type="password" name="password" placeholder="input password" />
</div>
<div>
<textarea cols="40" rows="3" name="comment" placeholder="댓글 입력해주세요"></textarea>
<!--댓글 전송 버튼-->
<br/> <br/> <button> 댓글 쓰기</button>
</div>
</form>
</section>
{{/with}}
<footer>
<div>
<a href="/">목록으로</a>
</div>
</footer>
<script>
async function modifyPost() { }
async function deletePost() { }
</script>