#38 [서버 프로그래밍] (06.28)

sookyoung.k·2023년 6월 28일
0
post-thumbnail

🌱 express 서버 열기

  • sample_project 폴더 생성 (프로젝트 폴더 만들기)

  • npm init -y → npm 시작

  • npm install express ejs → 필요한 npm 모듈 설치
    * npm install yarn -g
    npm으로 오류가 나는 경우가 많은데 그럴 땐 yarn 사용해보자!
    npm보다 빠르고 안전하다~

    * npm, yarn은 자바스크립트 node.js 환경 기반의 Package Manager를 말한다.

  • yarn add caver-js → npm 모듈 설치 (yarn으로 설치)

  • index.js 파일 생성 → 가장 기본이 되는 자바스크립트 파일

// express 로드
const express = require('express');
const app = express();

// view 파일들의 기본 경로
app.set('views', __dirname+'/views');
// view engine 설정
app.set('view engine', 'ejs');

// post 방식으로 데이터가 들어오는 경우 json 형태로 데이터를 변환 
app.use(express.urlencoded({extended: false}));

// dotenv 설정
require('dotenv').config();

const server = app.listen(port, function() {
    console.log(port, "Server Start");
});

여기까지가 가장 기본적인 서버 구조!
(server 변수 같은 경우엔... 프로젝트 만들면서 서버가 하나면(?) 굳이 변수 설정을 해 줄 필요는 없다.)

🌱 솔리디티 파일 배포 (baobab 테스트넷)

  • 솔리디티... 까먹으셨죠? 프로젝트 파일 경로에 truffle init
    (init: 생성자 명령어, 기본 폴더구조 생성)

명령어 실행하면

폴더들 생성됨! (파일디렉토리 구조 확인) truffle-config.js 파일 들어가서 버전 바꿔주자!

  • contracts 파일에 board.sol 파일 생성
// SDPX-License-Identifier: MIT 
pragma solidity = 0.8.19;

contract board {
    
    // owner 주소 변수 선언
    address internal owner;
    // 글번호 초기 변수
    uint internal content_no;

    // 구조체 생성
    struct content_info {
        string title;
        string content;
        address writer;
        string image;
        string create_dt;
    }

    // mapping 변수 생성 
    mapping (uint => content_info) internal contents;

    // deploy가 될 때 최초로 한 번만 실행이 되는 함수(초기화 함수)
    constructor() {
        owner = msg.sender;
        content_no = 0;
    }

    // modifier 함수: 함수의 추가적인 행동을 지정할 때 사용 
    modifier onlyOwner {
        require(msg.sender == owner, "Only owner can call function");
        _;
    }
    
    modifier increament {
        _;
        content_no++;
    }

    // 게시판에 글을 추가하는 함수 생성
    function add_content(
        string memory _title,
        string memory _content,
        address _writer,
        string memory _image,
        string memory _create_dt
    ) public onlyOwner increament {
        // mapping data에 값들을 추가한다. 
        contents[content_no].title = _title;
        contents[content_no].content = _content;
        contents[content_no].writer = _writer;
        contents[content_no].image = _image;
        contents[content_no].create_dt = _create_dt;
        // 글 번호를 1 증가 → modifier로 처리해버림
        // content_no++;
    }

    // 글의 번호를 출력하는 함수 
    function view_content_no() public view returns(uint) {
        returns (content_no);
    }
    
    // 구조체를 리턴하는 함수 
    function view_content(
        // 키 값으로 불러와야!
        uint _no 
    ) public view returns (
        string memory,
        string memory,
        address writer,
        string memory,
        string memory
    ) {
        string memory title = contents[_no].title;
        string memory content = contents[_no].content;
        address writer = contents[_no].writer;
        string memory image = contents[_no].image;
        string memory create_dt = contents[_no].create_dt;
        return (title, content, writer, image, create_dt);
    }
}

* 정적/동적 차이?
→ 정적 파일: 선언할 때 타입이 있음
Javascript, CSS, Image 등 서비스에서 사용하기 위해 미리 서버에 저장해 놓은 파일
→ 동적 파일: 타입이 없음 (있긴 한데 변환이 가능한지/가능하지 않은지만 있음)

* 클래스는 무엇일까?
→ 변수와 함수를 모아둔 집합체

→ 솔리디티에서 deploy를 하는 것이 클래스 생성과 비슷하다
→ 배포될 때 constructor(생성자 함수) 무조건 실행 - 최초로 무조건 실행되니까 이름이나 권한이 필요 없다

* modifier
→ 함수의 결합 (해당하는 함수와 modifier의 결합, modifier 자체도 함수)
modifier의 이름은 필요하지만 권한은 필요 없다 (독립적으로 사용할 수 없기 때문에 권한 필요 x)
→ 괄호를 넣지 않음! 함수인데! 왜? 함수에 추가적으로 붙어있는 것이기 때문에 매개변수를 만들 수 없다
→ 사용하는 이유: 반복되는 코드를 하나로 묶어서 함수로 호출하는 것이 훨씬 보기에 편하니깐~

* require
→ 요구를 하는 것! 뭐를? 조건을! (require = if)
requier도 함수임

* mapping data에 새로운 값을 집어넣으려면?
→ json 형태의 데이터를 넣는 것과 같음! (새로운) 키 값에 데이터 넣기
→ json안의 json이라고 생각하세용~ (파일 구조 확인)

* viewpure 차이
view - function 밖의 변수들을 볼 수 있지만 변경은 불가
pure - function 밖의 변수들을 볼 수도, 변경할 수도 없을 때 사용
view는 정적 파일의 끝을 달린다! 어떠한 파일을 return 해줄 것인지 알려줘야 함

  • 솔리디티 파일이 다 완성되었으면 해야 하는 일 → 컴파일! truffle compile

처음엔 매개변수에 _ 안 써서 컴파일 실패가 떴었다. 이건 놀랍게도 성공한 화면... 아마도? warning 메세지는 무시해도 된다고 하신 것 같다. 왤까...? 솔리디티는 이상해!

  • 컴파일 했으면! 배포를 해줘야 한다! 하기 전에 migrations 폴더에 01_deploy.js 파일 생성 (오타내지 말고 잘 만들어야 한다고 하셨다)
    ➡️ 배포 마이그레이션 (deploy migration)

    * migration - 계약을 배포하고 변경하는 일련의 과정을 프로그래밍 언어 중 하나인 자바스크립트(js)로 기술한 것

계약을 배포하는 migration 작성

const board = artifacts.require('board');

module.exports = function (deployer) {
   deployer.deploy(board).then(function () {
      console.log('Contract Deploy');
   });
};

✔️ artifacts.require('board') → 'board'라는 계약 정보를 읽어온다.
✔️ module.exports → 외부에서 모듈을 가져온다
✔️ deployer: truffle이 제공해주는 배포를 위한 툴
✔️ deployer.deploy(board)deploy() 함수 호출 후 인자로 위에서 읽어온 계약 정보인 board를 넘겨준다.

.then을 붙여서 콜백함수로 컨트랙트를 배포하기 전 테스트하면 좋다

  • 오늘은 가나슈가 아니라 클레이튼 바오밥 테스트넷에 배포할 것이기 때문에 설정을 바꿔주어야 한다 truffle-config.js에 아래 코드 추가
require('dotenv').config()
const HDWalletProvider = require('truffle-hdwallet-provider-klaytn')
const URL = "https://api.baobab.klaytn.net:8651"

const private_key = process.env.private_key

/* */ 
// abi 안에 있음 
   networks: {
      baobab : {
        provider : new HDWalletProvider(private_key, URL),
        network_id : 1001,
        gas : 2000000,
        gasPrice : null
      }

➡️ truffle-hdwallet-provider-klaytn: truffle-hdwallet-provider에서 파생된 자바스크립트 HD 지갑 제공자

* 'HD 지갑'이 뭔데?
→ Hierarchiacal Deterministic Wallet, 하나의 마스터 시드(seed) 키를 사용하여 무수히 많은 주소를 생성할 수 있는 암호화폐 지갑 (=계층적 결정 지갑)
→ 왜 사용하는지? 보안적 측면 때문이다
단 하나의 주소값만 사용하면 내가 보내고 받는 트랜잭션을 누군가 지켜보거나 감시할 수도 있기 때문이다. 그래서 HD 지갑에서는 하나의 지갑 내에 여러 개의 주소를 생성하여 알아서 분산 보관해 이러한 점을 어느정도 방지할 수 있다.
https://steemit.com/kr/@goldenman/hd-wallet
https://velog.io/@dik654/HDwallet

  • .envprivate_key 환경변수 설정
private_key = '0xde6c6b789783bd02381a71b671f545e435452c86d13395831f419e4dd918a692'

# mysql server 정보
host = '127.0.0.1'
port = 3306
user = 'root'
password = '1234'
database = 'new_deal2'

# session secret key
secret = 'rosencrantz'

지난번 수업 때 작성했던 거 복사해옴 (어차피 똑같이 쓸 것)

  • require 한 모듈 설치 npm install truffle-hdwallet-provider-klaytn

  • 이제 배포를 합시다 truffle migrate라고 그냥 하면 에러가 뜬다! (우리는 바오밥에 할 거니까) ➡️ truffle migrate --network baobab

(자동으로 컴파일 후) 배포 완료

  • 가나슈를 킨 후 truffle migrate(가나슈에 배포하는 명령어)를 한 후 build 폴더에 board.json을 확인해보면

abinetworks >> 1001, 5777 생성

클레이튼의 바보밥 테스트넷, 가나슈에 각각 배포된 컨트랙트 정보를 보여주는 것 (→ 두 개의 address 값이 다르다)

🌱 뷰(화면), 라우팅 처리

  • views, routes 폴더 생성 + 지난번에 만든 token 폴더 쌔벼오기

  • 라우트 나누기 → index.js
    → mysql(DB), 컨트랙트에 접근하는 라우트를 두 개 만들 것임
    → 라우팅: 모듈화 → 기능별로 파일을 나눠서 사용하겠다
    → 주소를 기준으로 해당하는 파일을 가져올 것

// localhost:3000 [get] 요청이 들어왔을 때 
app.get('/', function(req, res) {
    res.redirect("/user");
});

// routing 사용 
const sql = require('./routes/sql.js')();
app.use('/user', sql);

const contract = require('./routes/contract.js')();
app.use('/contract', contract)

mysql 계정 연결을 관리하는 라우터와 contract를 관리하는 라우터로 나눠준다

🌱 contract api 관리 (1) 글 목록 보여주기

  • routes에 contract.js 파일 생성
// express 로드
const express = require('express');
// Route() 함수를 변수에 대입
const router = express.Router();

// baobab network에 배포한 컨트랙트를 연동하기 위한 모듈을 로드
const Caver = require('caver-js');
// 컨트랙트의 정보 로드
const contract_info = require('../build/contracts/board.json');

// baobab 네트워크 주소를 입력
const caver = new Caver('https://api.baobab.klaytn.net:8651');

// 배포된 컨트랙트를 연동 
const smartcontract = new caver.klay.Contract(
    contract_info.abi,
    contract_info.networks['1001'].address
)

// 수수료 지불할 지갑을 등록
const account = caver.klay.accouts.createWithAccountKey(
    process.env.public_key,
    process.env.private_key
) 

caver.klay.accounts.wallet.add(account);

➡️ abi = Application Binary Interface : 스마트 컨트랙트 안에 존재하는 함수와 매개변수들을 json 형식으로 나타낸 리스트이다. abi를 사용해 컨트랙트 내의 함수를 호출하거나 컨트랙트로 부터 데이터를 얻을 수 있다.
➡️ createWithAccountKey(address, accountKey): 주어진 AccountKey로 Account 인스턴스를 생성한다 (Account는 계정 주소와 AccountKey를 관리하기 위한 클래스)

public_key = '0xA36e88DF42D4Ac17ed695B56502a01e499Bd9982'
  • contract.js에 api 생성
module.exports = function() {
    // api들 생성 
    // 해당하는 파일의 api의 기본 경로: loaclhost:3000/contract 
    
    // localhost:3000/contract 요청 시 등록된 게시글의 목록을 보여준다 
    router.get('/', async function(req, res) {
        // contract를 이용하여 저장된 모든 데이터를 로드 (오래 걸릴 것 같으니 블로킹 코드 적어주기 → async)
        // 저장된 데이터의 개수를 확인하는 함수를 호출 
        const contents_length = await smartcontract
                                    .methods
                                    .view_content_no()
                                    // view함수는 수수료 발생 x
                                    // 수수료를 낼 지갑의 정보가 필요하지 않다.
                                    .call(); // 바로 콜~ 
        console.log(contents_length); 
        // 비어 있는 배열을 하나 생성 
        const data = new Array();
        for(let i=0; i<contents_length; i++) {
            // view_content() 함수를 호출해서 
            // return 받을 결과값을 data라는 비어있는 배열에 push
            const result = await smartcontract
                                .methods
                                .view_content(i)
                                .call();
            data.push(result);
        }
        console.log(data);
        res.render('content_list.ejs', {
            'data' : data
        })
    })
    return router;
}
  • viewscontent_list.ejs 파일 생성 → 컨트랙트에 저장되어 있는 글 목록을 불러올 화면 (간단한 테이블 구성)

🌱 부트스트랩, 제이쿼리 설정

https://getbootstrap.com/docs/5.3/getting-started/download/

압축 푼 다음 public 폴더 만들어서 파일 옮겨두기

* 설정 할 때 파일 디렉토리 참고

  • 제이쿼리

https://jquery.com/download/

여기에서 코드 복사

https://code.jquery.com/jquery-3.7.0.min.js 복사 후

content_list.ejs 헤드에 코드 추가

  • 부트스트랩 사용하기 위한 설정도 index.js에서!
//  외부의 js, css와 같은 정적 파일 
app.use(express.static('public'));

그리고 content_list.ejs 헤드 부분에 코드 추가

* 아래 코드에는 고쳤는데.. css 링크에서 ../public까지는 자동으로 설정이 되기 때문에 (index.js에서 설정해뒀으니까) 생략! /css~부터 시작하면 된다. 이거 때문에 부트스트랩 오류남...

  • 하지만 잘라냈습니다 ... views 폴더에 header.ejs 만들고 붙여넣기!
      <!-- 제이쿼리 -->
      <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
      <!-- 부트스트랩 css-->
      <link rel="stylesheet" href="/css/bootstrap.min.css" />

그리고 자바스크립트 코드로 붙여넣어준다.

content_list.ejs

<!DOCTYPE html>
<html lang="ko">
   <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>
      <%- include('header.ejs')%>
   </head>
   <body></body>
</html>

→ 관리하기 쉽게 하려고 header.ejs로 분리해서 사용해준다.

🌱 contract api 관리 (1) 글 목록 보여주기 - 화면 구현

  • content_list.ejs 본격적으로 작성!
<!DOCTYPE html>
<html lang="ko">
   <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>
      <%- include('header.ejs')%>
   </head>
   <body>
        <!-- 글목록 table -->
        <!-- 서버에서 글목록 data라는 변수에 담아서 유저에게 보내는 상황
        data 변수의 데이터 타입 : list
        [
            {
                '0' : title,
                '1' : content,
                '2' : writer,
                '3' : image,
                '4' : create_dt
            }, ...
        ] -->
        <div class="container">
            <table class="table table-dark">
                <tr>
                    <th>No</th>
                    <th>Title</th>
                    <th>Writer</th>
                    <th>Create_dt</th>
                </tr>
                <!-- 데이터가 존재하지 않는다면 -->
                <%if (data.length == 0) {%>
                    <tr>
                        <td colspan="4">저장된 게시글이 존재하지 않습니다.</td>
                    </tr>
                <%} else {%>
                    <!-- 데이터가 존재하는 경우 -->
                    <%for (let i=0; i<data.length; i++) {%>
                        <tr>
                            <td><%=i+1%></td>
                            <td><%=data[i]['0']%></td>
                            <td><%=data[i]['2']%></td>
                            <td><%=data[i]['4']%></td>
                        </tr>
                    <%}%>
                <%}%>
            </table>
        </div>
   </body>
</html>

* 숫자 키 값은 꼭 [''] 이 형태로 불러와야 한다! 안그러면 에러가 나요~

* class를 사용하는 이유? - css 선택자
1. 같은 클래스 이름을 중복적으로 사용할 수 있다
2. 여러 개의 클래스를 하나의 태그로 로드할 수 있다 (두 개 이상 아마도 다섯 개 이하의 클래스를 동시에 사용할 수 있다)
→ 부트스트랩으로 테이블 예쁘게 꾸며줬다!

  • 서버 한 번 실행해봅시다! nodemon index.js

잘 나옴... 사실 오타 개많았어서 많이 고침 ㅎㅎ

🌱 contract api 관리 (2) 글 쓰기

  • contract.js module.export 부분에 코드 추가
   // 글의 내용을 추가할 수 있는 페이지
   // localhost:3000/contract/add [get]
   router.get('/add', function(req, res) {
    res.render('add_content.ejs');
   });
  • content_list.ejs에 글쓰기 버튼 추가
<div class="container">
                <button onclick="location.href = '/container/add'">
                    글쓰기
                </button>
            </div>
  • add_content.ejs 파일 생성
<!DOCTYPE html>
<html lang="ko">
   <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>
      <%- include('header.ejs')%>
   </head>
   <body>
      <div class="container">
         <form action="/contract/add2" method="post">
            <span class="input-group-text" id="basic1">Title</span>
            <input type="text" name="_title" class="form-control" aria-describedby="basic1" />

            <span class="input-group-text" id="basic2">Content</span>
            <input type="text" name="_content" class="form-control" aria-describedby="basic2" />

            <span class="input-group-text" id="basic3">Writer</span>
            <input type="text" name="_content" class="form-control" aria-describedby="basic3" />

            <span class="input-group-text" id="basic4">Image</span>
            <input type="text" name="_image" class="form-control" aria-describedby="basic4" />

            <input type="submit" value="글 등록" class="btn-primary" />
         </form>
      </div>
   </body>
</html>
  • contract.js에 라우터 추가
// moment 모듈 로드
const moment = require('moment');
const date = moment();

/* */
   // 글의 내용들을 contract를 이용하여 저장하는 api
   // localhost:3000/contract/add2 [post]
   router.post('/add2', async function (req, res) {
      // 유저가 보낸 데이터를 변수에 대입, 확인
      const input_title = req.body._title;
      const input_content = req.body._content;
      const input_writer = account.address;
      const input_image = req.body._image;
      console.log(input_title, input_content, input_writer, input_image);
      // 현재시간
      const create_dt = date.format('YYYY-MM-DD HH:mm');
      console.log(create_dt);

      // smartcontract에 있는 함수를 호출하여 데이터를 저장 
      const receipt = await smartcontract
                        .methods
                        .add_content(
                            input_title,
                            input_content,
                            input_writer,
                            input_image,
                            create_dt
                        )
                        .send(
                            {
                                from : account.address,
                                gax : 2000000
                            }
                        )
    console.log(receipt);
    res.redirect('/contract')
   });

➡️ moment: 자바스크립트에서 사용되는 날짜 관련 라이브러리 중 가장 많이 사용되었던 라이브러리
https://jsikim1.tistory.com/195

  • npm install moment 해서 모듈 설치 후 입력값을 주면

등록되어 테이블에 목록이 뜬다! 끔찍한 css는 넘어가도록 해요...

콘솔 창의 결과도 확인!

moment 라이브러리 시간 설정 주의

  • 근데 어라...? 시간이 이상하다...? 시간차를 두고 올렸는데도 시간이 처음 올린 그대로 고정 ㅠ

contract.js에서 const로 정의한 datelet으로 바꾸고

이쪽에 복붙해주세요 let date = moment();

그럼 문제 해결~!

데이터 구조 한 번 확인~!

🌱 contract api 관리 (3) 글 내용 보이기

  • 제목 누르면 콘텐츠 내용 보이게 해야 하니까 코드 수정 (content_list.ejs)

이동함

  • contract.js에 라우터 추가
   // 글의 상세정보를 보여주는 페이지 api
   // localhost:3000/contract/view [get]
   router.get('/view/:_no', function (req, res) {
      const input_no = req.params._no;
      console.log(input_no);
      res.send(input_no);
   });

들어온당~~!!!

   // 글의 상세정보를 보여주는 페이지 api
   // localhost:3000/contract/view [get]
   router.get('/view/:_no', async function (req, res) {
      const input_no = req.params._no;
      console.log(input_no);
    //   res.send(input_no); // 확인 했으니까 주석처리 

    const data = await smartcontract
                    .methods        
                    .view_content(input_no)
                    .call();
    res.render('view_content.ejs', {
        'data' : data
    })
   });

마저 완성 후 화면을 만들러 간다~

  • viewsview_content.ejs 파일 생성 (글 정보 보여줄 화면)
<!DOCTYPE html>
<html lang="ko">
<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>
    <%- include('header.ejs')%>
</head>
<body>
    <div class="container">
        <ul class="list-group">
            <li class="list-group-item">Title : <%=data['0']%></li>
            <li class="list-group-item">Content: <%=data['1']%></li>
            <li class="list-group-item">Writer: <%=data['2']%></li>
            <li class="list-group-item">Image: <img src="<%=data['3']%>"></img>
            <li class="list-group-item">Create_dt: <%=data['4']%></li>
        </ul>
        <button onclick="location.href='/contract'" class="btn btn-danger">글 목록</button>
        <button onclick="window.history.back()">뒤로가기</button>
    </div>
</body>
</html>

하면~!!

이렇게 뜬다...
뭐야 내가 가져온 사진이지만 개이쁨... 저도 마실래요

🌱 이미지 링크가 아닌 이미지 파일 업로드로 바꿔주기

  • 이미지 업로드! add_content.ejs 수정

  • contract.js에 파일 업로드 설정
// 파일업로드 시 사용하는 모듈 (multer)
const multer = require('multer');

const storage = multer.diskStorage(
    {
        destination: function(req, file, cb) {
            cb(null, 'public/upload/');
        },
        filename: function(req, file, cb) {
            cb(null, file.originalname);
        }
    }
);

const upload = multer({
    storage: storage
})

➡️ multer: 파일 업로드를 위해 사용되는 node.js의 미들웨어
→ multipart/form-data 형식으로 단일 및 다중 파일 업로드를 지원하기 때문에 가장 많이 사용한다
https://www.npmjs.com/package/multer

  • 그리고 api 경로도 수정

그리고 다시 테스트! (당연히 모듈 설치 해줘야 함 npm install multer)

콘솔에 데이터 업로드 잘 됐는지 확인!
(주소 안 바꿔줘서... 안뜨는거 당연함...)

view_content.ejs 파일 업로드 코드 수정해준 담에 보면~

대신 앞에 주소로 해둔 건 안 뜬다. (당연함 수정했으니까) 암튼 끝~~~

profile
영차영차 😎

0개의 댓글