2015년 12월 23일 수요일

[Express.js] Node.js, socket.io 채팅 예제 (2)

채팅에 기능을 더 추가한다.

글을 작성하면서 개발한게 아니라 진행 과정은 추후에 정리해야겠다.



아래는 결과 소스



index.js
==========
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});



var userList = [];


io.on('connection', function(socket){
  var joinedUser = false;
  var nickname;

  // 유저 입장
  socket.on('join', function(data){
    if (joinedUser) { // 이미 입장 했다면 중단
      return false; 
    }

    nickname = data;
    userList.push(nickname);
    socket.broadcast.emit('join', { 
      nickname : nickname
      ,userList : userList
    });

    socket.emit('welcome', { 
      nickname : nickname
      ,userList : userList
    });

    joinedUser = true;
  });


  // 메시지 전달
  socket.on('msg', function(data){
    console.log('msg: ' + data);
    io.emit('msg', { 
      nickname : nickname
      ,msg : data
    });
  });


  // 접속 종료
  socket.on('disconnect', function () {
    // 입장하지 않았다면 중단
    if ( !joinedUser) { 
      console.log('--- not joinedUser left'); 
      return false;
    }

    // 접속자목록에서 제거
    var i = userList.indexOf(nickname);
    userList.splice(i,1);

    socket.broadcast.emit('left', { 
      nickname : nickname 
      ,userList : userList
    });    
  });
});
==========




index.html
==========
<!doctype html>
<html>
<head>
  <title>Socket.IO chat</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font: 13px Helvetica, Arial; }
    form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
    form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
    form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
    #messages { list-style-type: none; margin: 0; padding: 0; }
    #messages li { padding: 5px 10px; }
    #messages li:nth-child(odd) { background: #eee; }

    #messages span.nickname { font-weight: bold; font-size: 120%; display: inline-block; width: 100px; }


    div.userList { text-align: center; width: 200px; min-height: 200px; border: 1px solid #999;}
    #userList { list-style-type: none; margin: 0; padding: 0; }
    #userList li { }

    #before { text-align: center; margin-top: 50%; }
    #after { display: none; }
    .noti { text-align: center; color: blue; }
  </style>
</head>
<body>

<section id="before">
  <p>닉네임을 입력하세요</p>
  <input id="nickname"><button id="joinBtn">들어가기</button>
</section>


<section id="after">
  <div class="userList">
    <h2>현재 접속자</h2>
    <ul id="userList"></ul>
  </div>

  <hr>
  <ul id="messages"></ul>
  <form>
    <input id="m" autocomplete="off" /><button>Send</button>
  </form>
</section>

<script src="/socket.io/socket.io.js"></script>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script>

var nickname;
var socket = io();

// 이벤트: join 클릭 
$('#joinBtn').click(function(e){  
  fnNickname(e);
});

// 이벤트: nickname 엔터키 
$('#nickname').keypress(function(e) { 
  if (e.which == 13) {
    fnNickname(e);
  }
});

// 송신: 닉네임
function fnNickname(e) {
  if ($('#nickname').val().trim() == '') {
    alert('Input your nickname!');
    return false;
  }
  nickname = $('#nickname').val().trim();
  socket.emit('join', nickname);  // 접속 이벤트
}



// 수신: 환영인사
socket.on('welcome', function(data){
  // 유저리스트 업데이트
  fnUpdateUserList(data.userList);

  $('#before').hide();
  $('#after').show();
  $('#messages').append($('<li class="noti">').text(nickname + '님 환영합니다.'));  
});


// 유저리스트 업데이트
function fnUpdateUserList(userList) {
  $('#userList').text('');
  for (i = 0; i < userList.length; i++) {
    $('#userList').append($('<li>').text(userList[i]));
  };
}

// 수신: 신규자 접속
socket.on('join', function(data){
  // 입장 알림
  $('#messages').append($('<li class="noti">').text(data.nickname + '님이 입장하셨습니다'));
  
  // 유저리스트 업데이트
  fnUpdateUserList(data.userList);
});

// 수신: 퇴장
socket.on('left', function(data){
  // 종료 알림
  $('#messages').append($('<li class="noti">').text(data.nickname + '님이 퇴장하셨습니다'));
  
  // 유저리스트 업데이트
  fnUpdateUserList(data.userList);
});


// 송신: 메시지
$('form').submit(function(){
  socket.emit('msg', $('#m').val());
  $('#m').val('');
  return false;
});
  

// 수신: 메시지
socket.on('msg', function(data){
  var span = $('<span class="nickname">').text(data.nickname);
  var li = $('<li>').append(span).append(data.msg);
  $('#messages').append(li);
});

</script>

</body>
</html>
==========





















2015년 12월 21일 월요일

[Express.js] Node.js, socket.io 채팅 예제


socket.io 채팅 구현
(http://socket.io/get-started/chat/ 을 요약 번역)




Node.js 서버 구현

*Node.js가 설치 되어있지 않다면 먼저 설치가 필요하다. (http://nodejs.org/)


1. 작업할 빈 폴더 생성, 아래 내용의 package.json 파일 생성
{
  "name": "socket-chat-example",
  "version": "0.0.1",
  "description": "my first socket.io app",
  "dependencies": {}
}


2. express 설치
- '--save' 옵션을 사용하면 설치시 package.json 파일 dependencies에도 추가해준다
$ npm install --save express


3. 아래 내용으로 index.js 파일을 생성해서 서버 구동을 확인한다.
var app = require('express')();
var http = require('http').Server(app);

app.get('/', function(req, res){
  res.send('<h1>Hello world</h1>');
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});
- 서버 구동
$ node index.js

- 접속 확인



Serving HTML

1. index.js 파일 수정

..
app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});
..


2. index.html 파일 생성
<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>
  </body>
</html>

3. 서버 재시동 후 확인
- Control+C로 서버 종료 후 node index.js로 재구동 후 접속 확인



nodemon 설치

- 소스 수정시마다 재구동이 번거롭다면 nodemon을 설치하면 매우 편리하게 쓸 수 있다.
(http://nodemon.io/)
- '-g' 옵션으로 글로벌 설치
$ npm install -g nodemon

- 이후 서버 구동은 nodemon으로 한다. 파일 수정시 자동으로 서버를 재시작해준다.
$ nodemon



Integrating Socket.IO

1. socket.io 설치
$ npm install --save socket.io
*Windows 환경의 경우 python이 설치 되어있지 않으면 node-gyp 관련된 socket.io의 dependency중 bufferutil, utf-8-validate 빌드 오류가 발생될 수 있다.
*이번 예제 진행에는 영향을 미치지 않으니 일단 무시하고 진행한다.


2. 서버 소스 수정
- index.js 파일 수정
- io에 'connection' 이벤트가 발생하면 콜백 함수를 실행하는 형태이다.
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
  res.sendfile('index.html');
});

io.on('connection', function(socket){
  console.log('a user connected');
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});

3. 클라이언트 소스 수정
- index.html 파일 수정. body 맨 아래에 추가한다.
- socket.io.js를 로드해서 io() 라는 함수를 실행해서 연결한다.
...
  <script src="/socket.io/socket.io.js"></script>
  <script>
    var socket = io();
  </script>

  </body>
...
- nodemon을 사용했다면 자동으로 서버가 재시작 되었을테니 브라우저만 새로고침하면 확인 가능하다.
- 클라이언트는 직접적인 변화가 없으며, 접속시 서버 콘솔에 'a user connected'가 출력된다.


4. disconnect 이벤트 구현
- connection 콜백에서 disconnect 이벤트 리스너를 추가했다.
- 접속시, 해제시 서버측에 콘솔이 출력된다.
...
io.on('connection', function(socket){
  console.log('a user connected');
  socket.on('disconnect', function(){
    console.log('user disconnected');
  });
});
...



Emitting events

*Socket.IO를 이용해서 원하는 데이터를 원하는 이름의 이벤트로 전송이 가능하다.

1. index.html 수정
- 폼데이터 전송을 편하게 하기 위해 jquery를 추가했다.
- send 버튼을 누르면 input에 사용자가 입력한 값을 'chat message'라는 이름의 이벤트로 전송하는 내용이다.
...
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script>
  var socket = io();
  $('form').submit(function(){
    socket.emit('chat message', $('#m').val());
    $('#m').val('');
    return false;
  });
</script>
...


2. index.js 수정
- 'chat message'라는 이벤트가 발생하면 메시지를 콘솔로 출력하는 내용이다.
...
io.on('connection', function(socket){
  socket.on('chat message', function(msg){
    console.log('message: ' + msg);
  });
});
...


3. 메시지 전송 확인
- 클라이언트 소스 수정이 있었으니 브라우저 새로고침 후 메시지 전송시 서버 콘솔에 출력되는지 확인한다.



Broadcasting

이제 다른 모든 유저에게 메시지를 전달해보자.


1. index.js 수정
- 'client message' 이벤트가 발생하면 메시지를 서버 콘솔에 출력하고,
emit 함수를 이용해 'server message' 라는 이벤트를 모든 클라이언트에게 전달하는 내용이다.
- chat message 라는 이름을 공통으로 사용해도 되지만, 구분하기 쉽게 client message / server message 라는 이름으로 구분해서 사용한다.
...
io.on('connection', function(socket){
  socket.on('client message', function(msg){
   console.log('message: ' + msg);
    io.emit('server message', msg);
  });
});
...


2. index.html 수정
- 'chat message' 이벤트 이름을 'client message' 로 변경했다.
- 'server message' 라는 이벤트가 발생하면 브라우저에 수신된 메시지를 출력하는 내용을 추가했다.
...
<script>
  var socket = io();
  $('form').submit(function(){
    socket.emit('client message', $('#m').val());
    $('#m').val('');
    return false;
  });
  
  socket.on('server message', function(msg){
    $('#messages').append($('<li>').text(msg));
  });
</script>
...


3. 메시지 전송확인
- 브라우저 탭이나 창을 여러개 띄워서 메시지 전송을 확인한다.
- 메시지 전송시 서버와 접속중인 모든 클라이언트에 해당 메시지가 출력되는것을 확인할 수 있다.





2015년 12월 18일 금요일

[Express.js] 게시판 만들기 예제


generator로 생성되는 기본 view engine은 jade인데,
익숙해 지면 간편할듯 하지만 바로 적응하면서 쓰기엔 시간이 더 걸릴것 같다.

jsp와 비슷한 문법의 ejs도 옵션으로 있으니 이걸 사용한다.
$ express --ejs board

역시 폴더로 이동해서 dependencies를 설치한다.
$ cd board
$ npm install

정상 구동 확인
$ npm start
http://localhost:3000/



테이블 생성
CREATE TABLE `board` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `title` varchar(255) NOT NULL,
   `description` text NOT NULL,
   `img_url` varchar(255) DEFAULT NULL,
   `created` datetime DEFAULT NULL,
   `modifed` datetime DEFAULT NULL,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4

테스트 데이터 삽입
INSERT INTO board (title, description, created) 
VALUES ('테스트1', '본문 내용', sysdate())
   , ('테스트2', '본문 내용', sysdate())
   , ('테스트3', '본문 내용', sysdate());


MySQL 모듈 설치
$ npm install mysql

개발중 소스 수정시마다 서버 재시작하려면 불편하니 nodemon을 이용한다.
(http://nodemon.io/)
$ npm install -g nodemon

이후 서버 구동은
$ nodemon



app.js에 routes를 추가한다.
...
app.use('/user', users);
// board route 추가
app.use('/board', require('./routes/board')); 
...

routes 폴더에 board.js를 생성한다 (users.js를 복사해서 필요부분을 수정한다.)

var express = require('express');
var router = express.Router();

var mysql      = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'express',
  password : '1234',
  database : 'express'
});

router.get('/', function(req, res, next) {
 connection.query('SELECT * FROM board', function(err, rows, fields) {
   if (err) throw err;
   res.send(rows);
 });

});

module.exports = router;



접속확인
http://localhost:3000/board


이제 view를 입히자.
board.js 수정
...
   //res.send(rows);
   res.render('board/index', { rows: rows });
...


view 폴더에 board 폴더를 만들고 index.ejs를 생성한다.

<!DOCTYPE html>
<html>
  <head>
    <title>test board</title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
   <table border="1px">
    <tr>
      <th width="30px">id</th>
      <th width="100px">제목</th>
    </tr>
    <% for(var i=0; i<rows.length; i++) {%>
    <tr>
     <td><%= rows[i].id %></td>
     <td><%= rows[i].title %></td>
    </tr>
    <% } %>
  <table>
  </body>
</html>


접속확인
http://localhost:3000/board


DB 접속과 view 적용이 확인 되었으니 이걸 활용해서 추가/수정/삭제를 구현해보자.



[Express.js] Express application generator

Express application generator

(http://expressjs.com/en/starter/generator.html)을 간단히 번역함.


express-generator 를 사용하면 웹 앱의 구조를 쉽게 잡을 수 있다.

npm 명령어로 간단하게 설치가 가능하다.
$ npm install express-generator -g

실행도 간단하다. myapp이란 이름으로 만들고 싶다면
$ express myapp

기본 파일들과 package.json가 생성되었다.
아래 명령어로 구동에 필요한 dependencies를 설치한다.
$ cd myapp
$ npm install

이제 앱을 실행한다.
디버그 없이 하려면 npm start만 입력해도 된다.
> set DEBUG=myapp:* & npm start

*MacOS나 Linux의 경우 set을 빼고 입력한다.
$ DEBUG=myapp:* npm start


이제 브라우저로 접속해본다.
http://localhost:3000/


생성된 폴더 구조
.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade























2015년 12월 16일 수요일

[Express.js] Node.js, Express.js 설치



환경 : 
- Windows 7
- OS X 10.10 (Yosemite)



1. Node.js 설치

(https://nodejs.org/en/)
- v4.2.3
msi (맥은 pkg) 실행 후 별다른 변경 없이 'Next'만 눌러도 설치 완료

*이후 윈도우에서 node나 npm 관련 커맨드 사용하려면
시작>모든 프로그램>Node.js>Node.js command prompt를 실행해서 사용


2. Express 설치

(http://expressjs.com/en/starter/installing.html)

- 앱에 사용할 폴더를 만들고 안으로 이동
$ mkdir myapp
$ cd myapp


- 패키지 의존성 관리등에 사용할 package.json 파일 생성
- 아래 명령어를 입력하면 문답형식으로 쉽게 생성할 수 있도록 해준다.
(기본값으로 하려면 엔터만 연타해도 된다)
$ npm init

- 아래 명령어로 package.json 파일의 dependencies에 express를 추가하고 설치한다.
$ npm install express --save



3. Hello world example

(http://expressjs.com/en/starter/hello-world.html)

- 폴더에 아래 내용으로 app.js 파일을 생성

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});

var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

- 앱 실행
$ node app.js


- 브라우저로 접속 해서 Hello World! 출력 확인
http://localhost:3000/











2015년 9월 3일 목요일

[Javascript] string byte length









 // 최대 바이트 제한
function fnMaxByte(e, max) {
    var $target = $(e.target);  
    var byteCount = getByteLength($target.val());  
    if (byteCount > max) {
        var rtnStr = $target.val();      
        var rtnByte = getByteLength(rtnStr);
        var leng = rtnStr.length;
        while (rtnByte > max) {
            rtnStr = rtnStr.substr(0, leng--);
            rtnByte = getByteLength(rtnStr);
        }
        $target.val(rtnStr);
    } else {
        $($target.closest('div.row')).find('.limitTextNum span').text(byteCount);    }
}


// 바이트 카운트
function getByteLength(s,b,i,c){
    for(b=i=0;c=s.charCodeAt(i++);b+=c>>11?3:c>>7?2:1);
    return b;
}








2015년 7월 6일 월요일

[Javascript] milliseconds to time string




function fnMsToTime(duration) {
    var milliseconds = parseInt((duration%1000)/100)
        , seconds = parseInt((duration/1000)%60)
        , minutes = parseInt((duration/(1000*60))%60)
        , hours = parseInt((duration/(1000*60*60))%24)
        , days = parseInt(duration/(1000*60*60*24));
    hours = (hours < 10) ? "0" + hours : hours;    minutes = (minutes < 10) ? "0" + minutes : minutes;    seconds = (seconds < 10) ? "0" + seconds : seconds;
    return days + "일 " + hours + "시간 " + minutes + "분 " + seconds + "초 " + milliseconds + ' 남음';}






2015년 7월 1일 수요일

[cakephp] .htaccess mod_rewrite edit for exclude folder or url


I making cakephp 3.0 web app.

I want to cakephp exclude url for static html folder.


/app/webroot/exclude/some.html


/app/.htaccess
<IfModule mod_rewrite.c>
    RewriteEngine on    RewriteRule    ^$    webroot/    [L]    RewriteRule    (.*) webroot/$1    [L]</IfModule>

webroot/.htaccess
<IfModule mod_rewrite.c>
    RewriteEngine On    RewriteCond %{REQUEST_URI} !^/exclude/(.*) # add this Line on default    RewriteCond %{REQUEST_FILENAME} !-f    RewriteRule ^ index.php [L]</IfModule>



http://stackoverflow.com/questions/2249885/cakephp-htaccess-mod-rewrite-configuration-to-exclude-a-particular-folder-url

2015년 6월 24일 수요일

[php] increase max upload size (wordpress)




PHP_INI_PERDIR Entry can be set in php.ini, .htaccess, httpd.conf or .user.ini (since PHP 5.3)

php.ini

upload_max_filesize = 32M
post_max_size = 32M

then httpd restart




http://stackoverflow.com/questions/13442270/ini-setupload-max-filesize-200m-not-working-in-php

[apache] change run user, group

cakephp3 Permission denied error




/etc/apache2/envvars

# Since there is no sane way to get the parsed apache2 config in scripts, some
# settings are defined via environment variables and then used in apache2ctl,
# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.

##export APACHE_RUN_USER=www-data
##export APACHE_RUN_GROUP=www-data
## 아파치 유저 변경
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data


service apache restart







[cakephp] not found the requested url (apache)



/etc/apache2/sites-available/default


<Directory />
    Options FollowSymLinks
    AllowOverride All
</Directory>
<Directory /var/www>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order Allow,Deny
    Allow from all
</Directory>



http://book.cakephp.org/3.0/en/installation.html#url-rewriting



[vagrant] synced folder owner change (apache)




vagrant file edit

## 공유 폴더 권한 변경

#config.vm.synced_folder "./source", "/var/www/source", create: true
config.vm.synced_folder "./source", "/var/www/source", create: true,
      :owner => "vagrant",
      :group => "www-data",
      :mount_options => ["dmode=775","fmode=664"]



$ vagrant reload



2015년 5월 13일 수요일

[php] 기본 페이징 코드


// 전체 게시물수
$totalCnt = $row['cnt'];
// 한 페이지 게시글 수 
$onePagePer = 15;
// 전체 페이지 수
$totalPageCnt = ceil($totalCnt / $onePagePer);
if ($page < 1 && $page > $totalPageCnt) {
?>
 <script type="text/javascript">
 alert('존재하지 않는 페이지 입니다.');
 history.back();
 </script>
<?php
 exit;
}
// 한번에 보여줄 총 페이지 개수
$oneSectionPer = 10;
// 현재 섹션
$currentSection = ceil($page / $oneSectionPer);
// 전체 섹션의 수
$totalSectionCnt = ceil($totalPageCnt / $oneSectionPer);
// 현재 섹션의 처음 페이지
$firstPage = ($currentSection * $oneSectionPer) - ($oneSectionPer - 1);
if ($currentSection == $totalSectionCnt) {
 // 현재 섹션이 마지막 섹션이라면 $totalPageCnt가 마지막 페이지가 된다
 $lastPage = $totalPageCnt;
} else {
 $lastPage = $currentSection * $oneSectionPer;
}
// 이전페이지
$prevPage = (($currentSection - 1) * $oneSectionPer);
// 다음 페이지
$nextPage = (($currentSection + 1) * $oneSectionPer) - ($oneSectionPer - 1);
// 페이징을 저장할 변수
$paging = '<ul>';
// 첫페이지가 아니라면 처음 버튼을 생성
if ($page != 1) {
 $paging .= '<li class="page page_start"><a href="./index.php?page=1">처음</a></li>';
}
// 첫 섹션이 아니라면 이전 버튼을 생성
if ($currentSection != 1) {
 $paging .= '<li class="page page_prev"><a href="./index.php?page=' .$prevPage. '">이전</a></li>';
}

for ($i = $firstPage; $i <= $lastPage; $i++) {
 if ($i == $page) {
  $paging .= '<li class="page current">' .$i. '</li>';
 } else {
  $paging .= '<li class="page"><a href="./index.php?page=' .$i. '">' .$i. '</a></li>';
 }
}
//마지막 섹션이 아니라면 다음 버튼을 생성
if ($currentSection != $totalSectionCnt) {
 $paging .= '<li class="page page_next"><a href="./index.php?page=' .$nextPage. '">다음</a></li>';
}
// 마지막 페이지가 아니라면 끝 버튼을 생성
if ($page != $totalPageCnt) {
 $paging .= '<li class="page page_end"><a href="./index.php?page='.$totalPageCnt.'">끝</a></li>';
}
$paging .= '</ul>';
/* 페이징 끝 */
$currentLimit = ($onePagePer * $page) - $onePagePer;
$sqlLimit = ' LIMIT ' .$currentLimit. ', '.$onePagePer;
$sql = 'SELECT * FROM board_free ORDER BY b_no DESC' .$sqlLimit;
$result = $db->query($sql);