개발/Node

[node] Chapter 7. 페이지 네이션 되는 게시판 만들기

ForrestPark 2025. 1. 21. 10:05

Chapter 7. 페이지 네이션 되는 게시판 만들기

📚 도서,Node.js백엔드 개발자 되기 Chapter 7

7.1 구조

  • MVC 패턴 구조 만들기
  • 컨트롤러 는 app.js 에서.
  • service 는 post-service.js 하나만 만듬.
  • chapter7 > board > npm init -y : 프로젝트 초기화

7.2.2 익스프레스 설치 및 플로젝트 디렉터리 구조 잡기

  1. 라이브러리 설치

    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 함.

  2. 구조별로 디렉터리를 먼저 만들어준다.

    mkdir configs
    mkdir services
    mkdir views
    vi app.js ## controller 역할

    7.2.3 핸들바 템플릿 엔진 설치 및 설정

    뷰로 웹페이지를 보기위한 템플릿 엔진 : 퍼그, ejs, 머스태시, 핸들바 가 있음.
    핸들바는 머스태시와 호환, 추가기능 제공 적절..
    express-handlebars

    npm 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>&lt;&lt;</a>
    <a>&lt;</a>
    <a>1</a>
    <a>2</a>
    <a>3</a>
    <a>4</a>
    <a>&gt;</a>
    <a>&gt;&gt;</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개의 댓글이 있습니다.



</form>
작성자 : 댓글 작성자
작성일시 : 2025-01-16 00:00:00
{{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 에서 사용 예시
    const helper = require("./handlebar-helpers.js")
    const isEqual = helper.eq(5.5);
    const dateStr = helper.dateString("2025-01-20")
    handlebar 에서 사용예시
    {{헬퍼함수 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. 리스트의 검색창 및 검색 버튼 수정
    검색어 입ㄹ겨 > 검색어 클릭 > 검색어 정보를 서버에 요청

(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 패키지 주요 내장 헬퍼 정리

  1. {{#if condition }} .. {{else}} ... {{/if}}
    ex)
    {{#if user.isLoggedIn}}
     <p>Welcome, {{user.name}}! </p>
    {{else}}
     <p> Please log in. </p>
    {{/if}}
  2. {{#each array}} ... {{/each}} : 반복 블록 핼퍼
  • 주어진 배열의 각 요소에 대해 블록 안의 내용을 반복해서 랜더링함.
  • 블록 내에서는 현재 요소의 데이터에 this 키워드로 접근할 수있음.
  • {{@index}} 를 사용하여 현재 요소의 인덱스를 가져 올 수있음.
<ul>
    {{#each user}}
        <li>{{this.name}} ({{this.age}})</li>
    {{/each}}
</ul>
  1. {{#with object}} ... {{/with}} : 컨텍스트 변경 블록 헬퍼
  • 컨텍스트 : 템플릿 에서 변수나 속성을 참조할 때 해당 변수나 속성이 어디에서 정의 되었는지 나타내는 범위
  • 최상위 컨텍스트가 data {user :{name, age}, posts} 인경우 name 값 사용
  • 블록 안에서 주어진 객체를 새로운 컨텍스트로 설정
  • 블록 내에서 객체의 속성을 바로 사용 가능.
{{#with user}}
    <p> Name : {{name}}</p>
    <p> Age : {{age}}</p>
{{/with}}
  1. {{lookup object key}} : 객체의 값을 가져오는 헬퍼
  • {{lookup user 'name'}}
  1. 사용자 정의 헬퍼
    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로 이동  (괄호는 &lt 사용)-->
    <a href="/?page=1&search={{@root.search}}">&lt;&lt;</a>
    {{#if hasPrev}}
    <a href="/?page={{prevPage}}&search={{@root.search}}"">&lt;</a>"
    <!-- (6) 1페이지 인 경우 이전 페이지 가 없으므로 링크가 없음 -->
    {{else}} 
    <a>&lt</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}}">&gt;</a>

    {{else}}
    <a>&gt;</a>
    {{/if}}
    <a href="/?page={{lastPage}}&search={{@root.search}}">&gt;&gt;</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>