자동 확장 가능한 콘서트 티켓 예매 사이트 구축하기

이재홍 http://www.pyrasis.com 2014.03.24 ~ 2014.06.30

Node.js로 웹 서버 작성하기

필요한 AWS 리소스를 생성하고 설정하였습니다. 이제 Node.js로 웹 서버를 작성해보겠습니다. 다음 예제 코드는 저의 GitHub 저장소에 있는 예제 코드를 받아서 사용합니다.

 

 

app.js

var express = require('express')
  , Sequelize = require('sequelize')
  , redis = require('redis')
  , EC2Metadata = require('ec2metadata')
  , http = require('http')
  , fs = require('fs')
  , app = express()
  , server = http.createServer(app)
  , io = require('socket.io').listen(server);

var redisEndpoint = {
  host: 'exampleticket.o5nouc.0001.apne1.cache.amazonaws.com',
  port: 6379
};
var rdsEndpoint = {
  host: 'exampleticket.cnlconsezo7y.ap-northeast-1.rds.amazonaws.com',
  port: 3306
};

// Redis Pub/Sub
var publisher = redis.createClient(redisEndpoint.port, redisEndpoint.host);
var subscriber = redis.createClient(redisEndpoint.port, redisEndpoint.host);

// MySQL DB 이름, 계정, 암호
var sequelize = new Sequelize('exampleticket', 'admin', 'adminpassword', {
  host: rdsEndpoint.host,
  port: rdsEndpoint.port,
  maxConcurrentQuries: 1024,
  logging: false
});

// MySQL DB 테이블 정의
var Seat = sequelize.define('Seat', {
  seatId: { type: Sequelize.STRING, allowNull: false, unique: true },
  actionType: { type: Sequelize.STRING, allowNull: false },
  userId: Sequelize.STRING
});

// MySQL DB 테이블 생성
sequelize.sync();

var ipAddress;

app.get(['/', '/index.html'], function (req, res) {
  fs.readFile('./index.html', function (err, data) {
    res.contentType('text/html');
    res.send(data);
  });
});

// 좌석 예약, 결제 상태 출력
app.get('/seats', function (req, res) {
  Seat.findAll({
    where: { actionType: { ne: 'cancel' } }
  }).success(function (seats) {
    var data = [];
    seats.map(function (seat) { return seat.values; }).forEach(function (e) {
      seat = e.seatId.split('-');
      data.push({
        row: seat[0],
        col: seat[1],
        actionType: e.actionType,
        userId: e.userId
      });
    });
    res.header('Cache-Control', 'max-age=0, s-maxage=0, public');
    res.send(data);
  });
});

// socket.io에 접속할 IP 주소 전달
app.get('/ip', function (req, res) {
  res.header('Cache-Control', 'max-age=0, s-maxage=0, public');
  if (!ipAddress) {
    EC2Metadata.get(['public-ipv4'], function (err, data) {
      ipAddress = data.publicIpv4;
      res.send(ipAddress);
    });
  }
  else {
    res.send(ipAddress);
  }
});

// 좌석 예약, 결제 처리
io.sockets.on('connection', function (socket) {
  socket.on('action', function (data) {
    Seat.find({
      where: { seatId: data.row + '-' + data.col }
    }).success(function (seat) {
      if (seat == null ||
          seat.userId == data.userId ||
          seat.actionType == 'cancel') {
        
        if (seat == null)
          seat = Seat.build();
        seat.seatId = data.row + '-' + data.col;
        seat.userId = data.userId;
        seat.actionType = data.actionType;
        seat.save().success(function () {
          publisher.publish('seat', JSON.stringify(data));
        });
      }
    });
  });
});

// 실시간으로 좌석 상태 개신
subscriber.subscribe('seat');
subscriber.on('message', function (channel, message) {
  io.sockets.emit('result', JSON.parse(message));
});

server.listen(80);

다음은 app.js에서 사용한 모듈들의 버전을 정의한 파일입니다.

package.json

{
  "name": "ExampleTicketWebServer",
  "version": "0.0.1",
  "description": "ExampleTicketWebServer",
  "dependencies": {
    "express": "4.4.x",
    "ec2metadata": "0.1.x",
    "socket.io": "1.0.x",
    "sequelize": "1.7.x",
    "mysql": "2.3.2",
    "redis": "0.10.x"
  }
}

다음 내용을 index.html로 저장합니다.

index.html

<!DOCTYPE HTML>
<html>
<head>
  <title>ExampleTicket</title>
  <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
  
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
  <script src="/socket.io/socket.io.js"></script>
</head>
<body>
  
  <div id="stage" class="btn btn-default" style="width:600px; margin-left:265px;">Stage</div>
  <div><span>ID: </span><span id="userId"></span></div>
  <div id="seats" class="col-xs-12"></div>
  
  <div id="dialog" class="modal fade">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h4 class="modal-title">결제</h4>
        </div>
        <div class="modal-body">
          <p><span id="seatId"></span> 좌석을 결제하시겠습니까?</p>
        </div>
        <div class="modal-footer">
          <button id="pay" type="button"
                  class="btn btn-primary" data-dismiss="modal">결제</button>
          <button id="cancel" type="button"
                  class="btn btn-default" data-dismiss="modal">취소</button>
        </div>
      </div>
    </div>
  </div>

  <script>    
    $(function () {
      var currentSeat;
      var socket;
      var userId = prompt('ID를 입력하세요', '');
      $('#userId').text(userId);

      function action(seat, actionType) {
        socket.emit('action', {
          actionType: actionType,
          userId: userId,
          row: seat.data('row'),
          col: seat.data('col')
        });
      }

      function openDialog(seat) {
        $('#seatId').text(seat.data('row') + '-' + seat.data('col'));
        $('#dialog').modal({ keyboard: false, backdrop: 'static' });
      }

      function updateSeat(data) {
        var seat = $('div[data-row="' + data.row + '"][data-col="' + data.col + '"]');
        if (data.actionType == 'reserve')
          seat.removeClass('btn-default').addClass('btn-warning');
        else if (data.actionType == 'cancel')
          seat.removeClass('btn-warning').addClass('btn-default');
        else if (data.actionType == 'pay')
          seat.removeClass('btn-warning').addClass('btn-success');

        if (data.userId == userId && (data.actionType == 'reserve' || data.actionType == 'pay'))
          seat.addClass('active');
        else
          seat.removeClass('active');
      }

      function drawSeats() {
        var seat;
        var rowSymbols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
                             'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
                             'U', 'V', 'W', 'X', 'Y', 'Z'];
        $('#seats').css({ 'height': '550px', 'width': '1100px' });

        var colDiv = $('<div>');
        colDiv.css({ 'width': '35px', 'height': '550px', 'float': 'left',  });
        for (var row = 1; row <= 26; row++) {
          var rowSymbol = $('<div>');
          rowSymbol.css({
            'width': '30px',
            'height': '25px',
            'padding-top': '7px',
            'margin-top': '12px',
            'margin-bottom': '1px' });
          rowSymbol.addClass('badge').text(rowSymbols[row - 1]);
          colDiv.append(rowSymbol);
        }
        $('#seats').append(colDiv);

        for (var col = 1; col <= 32; col++) {
          var colDiv = $('<div>');
          for (var row = 1; row <= 26; row++) {
            colDiv.css({ 'height': '550px', 'width': '30px', 'float': 'left' });

            seat = $('<div>');
            seat.css({ 'width': '30px',
                       'height': '30px',
                       'padding-left': '5px',
                       'padding-top': '5px',
                       'margin-bottom': '8px' })
            .addClass('btn btn-default seat').text(col);
            seat.attr('data-row', rowSymbols[row - 1]).attr('data-col', col);
            colDiv.append(seat);
          }

          if (col <= 8)
            colDiv.css('margin-top', col * 10);
          else if (col <= 24 && col > 8)
            colDiv.css('margin-top', '80px');
          else if (col > 24)
            colDiv.css('margin-top', (33 - col) * 10);

          $('#seats').append(colDiv);

          if (col % 4 == 0) {
            var way = $('<div>');
            way.css({ 'width': '10px', 'height': '550px', 'float': 'left' });
            $('#seats').append(way);
          }
        }
      }

      $.get('/ip', function (ip) {
        socket = io.connect(ip);
        socket.on('connect', function () {
          $('.seat').click(function () {
            if ($(this).hasClass('btn-warning') || $(this).hasClass('btn-success'))
              return;

            currentSeat = $(this);
            action($(this), 'reserve');
          });

          $('#pay').click(function () {
            action(currentSeat, 'pay');
            currentSeat = null;
          });

          $('#cancel').click(function () {
            action(currentSeat, 'cancel');
            currentSeat = null;
          });

          socket.on('result', function (data) {
            if (currentSeat && data.userId == userId && data.actionType == 'reserve')
              openDialog(currentSeat);
            updateSeat(data);
          });
        });
      });

      drawSeats();

      $.getJSON('/seats', function (data) {
        $.each(data, function (i, e) {
          updateSeat(e);
        });
      });
    });
  </script> 
</body>
</html>

앞에서 생성한 <프로젝트 이름>.src S3 버킷에 ExampleTicketWebServer라는 디렉터리를 생성하고 app.js, package.json, index.html 파일을 올립니다.


그림 32-13 S3 버킷에 웹 서버 Node.js 소스 올리기


저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.