본문 바로가기
코딩과 알고리즘

AWS와 Node.js 책 본문 소스

처음부터 시작하는 AWS 와 Node.js 책자의 소스 모음입니다.

책자에서 보고 직접 따라 치시기는 어려우실 것 같아 관련 명령어를 본문에 수록하였습니다.
이 명령어를 복사해서 붙여넣어 사용하시면 됩니다.

Part 3-4

* yum 설치 도구 업데이트

sudo yum -y update

 

Part 4-1.

* NPM 설치 도구 설치 명령어

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

* node.js  설치 명령어

nvm install v16.13.1


Part 4-3.

* 첫번째 예제, 폴더 생성 및 권한 지정

sudo mkdir /home/nodejs1
sudo chown ec2-user:ec2-user /home/nodejs1
cd /home/nodejs1


* 예제 소스

const http = require('http'); 
http.createServer(function (req, res) { 
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    res.end('안녕! Node.js\n'); 
}).listen(8080);


Part 6-2.
Part1 - Part5 요약

1. aws.amazon.com 페이지 접속 및 콘솔 로그인

2. EC2 검색하여 EC2 콘솔창에 진입, 리전 선택 ( 한국에 설치하려면 ‘서울’ 선택 )

3. 서버 생성

단계1. 인스턴스 시작 버튼 ▶ Amazon Linux 2 AMI 선택
단계2. 서버 사양 선택, 프리티어요금제를 사용하려면 프리티어 서버 선택 후 다음 버튼
단계3. 다음 버튼
단계 4. SSD 디스크 용량 입력. 프리티어 요금제인 경우 8G 그대로 유지하고 다음 버튼
단계 5. 다음 버튼
단계 6. 검토 및 시작 버튼
단계 7. 시작하기
새 키 페어 선택 – 키페어 이름을 입력하여 새로운 인증서를 다운로드 저장하거나,
또는 기존 키 페어 선택, 체크상자를 체크 후 인스턴스 시작

4. EC2 콘솔에 다시 접근, 연필 모양 아이콘 클릭하여 서버 이름 알아보기 쉽게 변경

5. 서버 접속 세션 입력

MOBAXTERM에 새 세션 추가, 서버 퍼블릭 IP주소 입력
specifi user name 체크상자 체크하고 ec2-user 이라는 로그인 아이디 입력, 
Advanced SSH Setting 탭에서 use private key 체크, 인증서 첨부, OK 버튼 눌러 세션 저장
세션 탭에서 IP주소로 입력된 세션 이름을 F2키를 눌러서 알아보기 쉽게 변경

6. 서버 접속

세션을 더블클릭하여 서버에 접속, 창의 글씨가 작아 보이면 Ctrl + 마우스 휠로 조절
sudo yum -y update 명령 입력하여 yum 설치 도구 업데이트

7. Node.js 설치

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
MOBAXTER 창을 닫고 다시 서버에 접속
nvm install v16.13.1

8. node.js 샘플 제작

sudo mkdir /home/nodejs1
sudo chown ec2-user:ec2-user /home/nodejs1
cd /home/nodejs1
vi main.js
i키를 입력 ( 삽입 모드 )
아래 내용 입력하거나 또는 복사 붙여 넣음

const http = require('http'); 
http.createServer(function (req, res) { 
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    res.end('안녕! Node.js\n'); 
}).listen(8080);

ESC, :wq! ( 저장하고 vi 에디터 빠져 나옴 )

9. 샘플 소스 실행

node main.js

10. 방화벽 열기

EC2 의 서버 인스턴스 아이디 선택
보안 탭 – 보안그룹 아이디 선택
인바운드 규칙 – 인바운드 규칙 편집
규칙 추가 – 포트범위 8080, 돋보기 모양에 IP 대역 0.0.0.0/80 입력 후 규칙 저장 버튼

11. 크롬 브라우저를 열어 서버의 퍼블릭IP:8080 주소로 접속


Part 10-1.
puppy.html  소스

<html>
  <head>
    <title>우리집 초코</title>
  </head>
  <body>
    <h1>멍!!</h1>
  </body>
</html>

main.js 수정

const http = require('http'); 
const fs = require('fs');

http.createServer(function (req, res) {  
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  fs.readFile('.' + req.url, function(err, data) {
    res.write(data + '\n');
    res.end();
  });
}).listen(8080);


Part 10-2.

happynewyear.html

<html>
  <head>
    <title>행복한 새해!</title>
  </head>
  <body>
    <h1>😀해피뉴이어~🎵</h1><br/>
    저물어가는 한해를 보내며 신년을 맞이하는 모든 분들께<br/>
    하나님의 은혜와 평강이 가득하시기를 소망합니다.<br/>
    <h1>🍈🍉🍊🍋🍌🍍🥭🍎</h1>
    <h1>🍏🍐🍑🍒🍓🍅🍆🌽</h1><br/>
  </body>
</html>

 

Part 11-2. 설치 스크립트

sudo mkdir /home/nodejs2
sudo chown ec2-user:ec2-user /home/nodejs2
cd /home/nodejs2/
npm init -y
npm install express

Part 11-4.

main.js

const express=require('express');
const app=express();
app.use(express.static('public'));
app.listen(8080, function() {});

public 폴더 생성 스크립트

sudo mkdir public
sudo chown ec2-user:ec2-user public

puppy.html

<html>
  <head>
    <title>우리집 초코</title>
  </head>
  <body>
    <h1>멍!!</h1>
  </body>
</html>

happynewyear.html

<html>
  <head>
    <title>행복한 새해!</title>
  </head>
  <body>
    <h1>😀해피뉴이어~🎵</h1><br/>
    저물어가는 한해를 보내며 신년을 맞이하는 모든 분들께<br/>
    하나님의 은혜와 평강이 가득하시기를 소망합니다.<br/>
    <h1>🍈🍉🍊🍋🍌🍍🥭🍎</h1>
    <h1>🍏🍐🍑🍒🍓🍅🍆🌽</h1><br/>
  </body>
</html>

Part 11-5

웹사이트 2개 서비스하는 참고용 스크립트

const express=require('express');

const app=express();
app.use(express.static('public'));
app.listen(8080, function() {});

const app2=express();
app2.use(express.static('public2'));
app2.listen(8081, function() {});

 

Part 11-6

태극기 그림파일 ( 다운로드용 )

 

Part 12-2.

main.js 수정

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

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// 페이지를 찾을 수 없음 오류 처리
app.use(function(req, res, next) {
    res.status(404);  
    res.send(
    '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
    '<html><head><title>404 페이지 오류</title></head>' + 
    '<body><h1>찾을 수 없습니다</h1>' + 
    '<p>요청하신 URL ' + req.url + ' 을 이 서버에서 찾을 수 없습니다..</p><hr>' +
    '</body></html>'
  );
});

app.listen(8080, function() {});


영어 문장으로 오류를 띄울 경우

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

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// 페이지를 찾을 수 없음 오류 처리
app.use(function(req, res, next) {
    res.status(404);	
    res.send(
        '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
        '<html><head><title>404 Not Found</title></head>' + 
        '<body><h1>Not Found</h1>' + 
        '<p>The requested URL ' + req.url + ' was not found on this server.</p><hr>' +
        '</body></html>'
    );
});

app.listen(8080, function() {});

Part 12-3.

main.js 수정

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

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

app.get('/plus/:id/:id2', function (req, res, next) {
  let sum = parseInt(req.params.id) 
    + parseInt(req.params.id2);
  res.send(sum.toString());
})

// 페이지를 찾을 수 없음 오류 처리
app.use(function(req, res, next) {
    res.status(404);	
    res.send(
		'<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
		'<html><head><title>404 페이지 오류</title></head>' + 
		'<body><h1>찾을 수 없습니다</h1>' + 
		'<p>요청하신 URL ' + req.url + ' 을 이 서버에서 찾을 수 없습니다..</p><hr>' +
		'</body></html>'
	);
});

app.listen(8080, function() {});


Part 12-4

연습문제 1

// 뺄셈
app.get(
  '/minus/:id/:id2', 
  function (req, res, next) {
    (                )
    res.send("합계 : " + sum);
  }
);

연습문제 1 해답

let sum = parseInt(req.params.id) 
  - parseInt(req.params.id2);

연습문제 2

// 곱셈
app.get(
  (                  ) , 
  function (req, res, next) {
    let sum = parseInt(req.params.id) 
      * parseInt(req.params.id2);
    res.send("결과 : " + sum);
  }
);

연습문제 2 해답

'/multiply/:id/:id2'

연습문제 3

app.get(
  (            ) , 
  function (req, res, next) {
    let sum = Math.pow(req.params.id, 2);
    res.send("결과 : " + sum);
  }
);

연습문제 3 정답

'/power/:id'

* /power 뒤에 입력 파라미터는 1개 뿐이므로 1개만 기술하여야 합니다.

연습문제 4

// reverse
app.get(
  (              ), 
  function (req, res, next) {
    let result = req.params.id.split("").reverse().join("");
    res.send(result);
  }
);

연습문제 4 정답

'/:id'

* 기본 파라미터 1개만 입력되어야 하므로 /:id만 기술하여야 합니다.

Part 13-2

폴더 생성, 소유자 변경 및 npm 초기화

sudo mkdir /home/nodejs3
sudo chown ec2-user:ec2-user /home/nodejs3
cd /home/nodejs3
npm init -y
npm install express ejs request
mkdir public
mkdir views

/home/nodejs3/main.js

const express=require('express');
const request=require('request');
const ejs=require("ejs");
const app=express();

app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

app.get('/randomdog', function(req,res) {
    request("https://random.dog/woof.json", 
function(error, response, body) {
        if (!error && response.statusCode == 200) {
            console.log(body);
            let object = JSON.parse(body);
            res.render('randomdog', {
                imagesize: object.fileSizeBytes,
                imageurl: object.url
            });
        }
    });
});

// 페이지를 찾을 수 없음 오류 처리
app.use(function(req, res, next) {
    res.status(404);
    res.send(
        '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
        '<html><head><title>404 페이지 오류</title></head>' +
        '<body><h1>찾을 수 없습니다</h1>' +
        '<p>요청하신 URL ' + req.url + ' 을 이 서버에서 찾을 수 없습니다..</p><hr>' +
        '</body></html>'
    );
});
app.listen(8080, function() {});

/home/nodejs3/views/randomdog.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>랜덤 강아지</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
        <h1>랜덤한 강아지가 화면에 표시가 되요</h1><br/>
        용량 : <%=imagesize%> bytes.<br/>
        <div style="border:5px solid black;padding:5px; width:550px;heght:550px;">
                <img src='<%=imageurl%>' width='500' height='500' style='text-align:center'>
        </div>
        API 정보 : https://random.dog/woof.json
  </body>
</html>

Part 16-2

HTML 문서 기본 형식

<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8">
   <title>제목</title>
</head>
<body>

</body>
</html>

Part 16-3

myhtml

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>제목</title>
</head>
<body>
안녕, HTML
</body>
</html>

Part 16-4

br 태그

<body>
안녕,<br/> HTML<br/>
Node.js 와의 만남
</body>

Part 16-5

SPAN 태그

<body>
안녕,<br/> HTML<br/>
<span style="color:red;font-size:20pt;">Node.js</span> 와의 만남
</body>

Part 16-6

스타일 시트

<body>
<style>
.emphasizeWord 
{ 
color:red;
font-size:20pt;
font-weight:bold;
text-shadow: 2px 2px 2px gray;
}
</style>
안녕,<br/> HTML<br/>
<span class="emphasizeWord">Node.js</span> 와의 만남<br>
<span class="emphasizeWord">C++</span> 과도 만남
</body>

Part 16-7

default.css

.emphasizeWord 
{ 
  color:red;
  font-size:20pt;
  font-weight:bold;
  text-shadow: 2px 2px 2px gray;
}

.blinkWord {
  animation: blinker 0.5s infinite;
}

@keyframes blinker {
  50% {
    opacity: 0;
  }
}

my.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>제목</title>
</head>
<body>
  <link rel="stylesheet" type="text/css" href="default.css">
  안녕,<br/> HTML<br/>
  <span class="emphasizeWord">Node.js</span> 와의 만남<br/>
  <span class="emphasizeWord">C++</span> 과도 만남<br/>
  블링 블링 <span class="blinkWord">블링크!</span>
</body>
</html>

Part 16-8

public/div.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>제목</title>
  <link rel="stylesheet" 
   type="text/css" 
   href="./default.css">
</head>
<body>
  <div class='divstyle1'>상자1</div>
  <div class='divstyle1'>상자2</div>
  <div class='divstyle1'>상자3</div>
  <div class='divstyle1'>상자4</div>
  <div class='divstyle1'>상자5</div>
  <div class='divstyle1'>상자6</div>
</body>
</html>

public/default.css

@import url('https://fonts.googleapis.com/css?family=Gugi:400');

.emphasizeWord 
{ 
  color:red;
  font-size:20pt;
  font-weight:bold;
  text-shadow: 2px 2px 2px gray;
}

.blinkWord {
  animation: blinker 0.5s infinite;
}

@keyframes blinker {
  50% {
    opacity: 0;
  }
}

.divstyle1
{
  border:5px solid #ccc;
  padding:5px;
  margin:5px;
  width:200px;
  height:100px;
  float:left;
  text-align:center;
  line-height:100px;
  font-family: 'Gugi';
  font-size:30pt;
}

default.css 추가

   :
.divstyle2
{
  animation: grain 0.5s steps(10) infinite;
  position: absolute;
  right: 10px;
  bottom: 10px;
  border:5px solid blue;
  padding:20px;
  background: linear-gradient( 45deg, yellow, red );
  color: white;
  font-family: 'Gugi';
  font-size:20px;
  line-height:200%;
}
@keyframes grain {
  0%, 100% { transform:translate(0, 0) }
  25%, 75% { transform:translate(0%, -0.5%) }
  40%, 60% { transform:translate(0%, -2%) }
  50% { transform:translate(0%, -5%) }
}

div.html 추가

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>제목</title>
  <link rel="stylesheet" 
   type="text/css" 
   href="./default.css">
</head>
<body>
  <div class='divstyle1'>상자1</div>
  <div class='divstyle1'>상자2</div>
  <div class='divstyle1'>상자3</div>
  <div class='divstyle1'>상자4</div>
  <div class='divstyle1'>상자5</div>
  <div class='divstyle1'>상자6</div>
  <div class='divstyle2'>
    둥둥 뜨는 레이어<br/>
    다채로운 그라데이션 배경까지!
  </div>
</body>
</html>

Part 16-9

public/button.html

<input type=button 
  value="클릭하세요1"
  onclick="alert('1')">

<button
  onclick="alert('2')">
  클릭하세요2
</button>

Part 16-10

1. 셋 중 먹고싶은 과일은?<br/>
<input type=radio name="choice" value="1"> 딸기
<input type=radio name="choice" value="2"> 포도
<input type=radio name="choice" value="3"> 키위
<br/>
2. 선호하는 음료를 선택하세요.(복수선택)<br/>
<input type=checkbox name="coffee"> 아메리카노
<input type=checkbox name="coffee"> 카페라떼
<input type=checkbox name="coffee"> 카푸치노
<input type=checkbox name="coffee"> 카라멜마끼아또
<br/>
3. 남기실 말씀<br/>
<input type=text name="msg">
<br/>

Part 17-3

public/jquery.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>jQuery</title>
  <link rel="stylesheet" 
   type="text/css" 
   href="./default.css">
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
  <!-- 이 부분은 입력상자 -->
  <input type=text class='craytext' value='입력상자1'/><br/>
  <input type=text class='craytext' value='입력상자2'/><br/>
  <input type=text class='craytext' value='입력상자3'/><br/>

  <!-- 이 부분은 스크립트 -->
  <script>
  $(".craytext")
  .focus(function(){
    $(this).css('background', '#eee');
  })
  .focusout(function(){
    $(this).css('background', '#fff');
  });
  </script>
</body>
</html>

Part 17-4

    :
<!-- 이 부분은 입력상자 -->
  <input type=button id='craybutton' value="클릭클릭!" />
  
  <!-- 이 부분은 스크립트 -->
  <script>
  $("#craybutton").click(function(){
    $(this).val("콜록콜록!");
  });
</script>
</body>
</html>

Part 17-5

    :
<input type=button id='craybutton2' value="jQuery Ajax" />
  
<script>
  $("#craybutton2")
  .click(async function(){
    let ret = await $.post(
      '/echo',
      { msg: '철수' }
    ); 
    alert(ret.result);
  });
</script>
    :
app.post('/echo', function(req,res) {
  res.json({result: req.body.msg + ', ok'});
});

Part 18-1

sudo mkdir /home/nodejs4
sudo chown ec2-user:ec2-user /home/nodejs4
cd /home/nodejs4
npm init -y
npm install express ejs request 
npm install -g nodemon
mkdir public
mkdir views
// 쪽지 더미 데이터
let memo=[
  { x:20, y:20, rot:10, 
    msg:"걱정하는게 걱정이야"
  },
  { x:230, y:80, rot:-8, 
    msg:"항상 방법은 있어"
  },
  { x:420, y:50, rot:15, 
    msg:"오늘보다 더 크고 멋지게!"
  },
  { x:420, y:210, rot:12, 
    msg:"올해엔 정말 멋지게 해보자구!!"
  }
];
console.log(memo);

Part 18-2

// 쪽지 더미 데이터
let memo=[
  { x:20, y:20, rot:10, 
    msg:"걱정하는게 걱정이야"
  },
  { x:230, y:80, rot:-8, 
    msg:"항상 방법은 있어"
  },
  { x:420, y:50, rot:15, 
    msg:"오늘보다 더 크고 멋지게!"
  },
  { x:420, y:210, rot:12, 
    msg:"올해엔 정말 멋지게 해보자구!!"
  },
  { x:200, y:240, rot:32, 
    msg:"인생의 여정에 고난은 연단을 이룹니다."
  }
];

Part 18-3

console.log(memo);

// 라우팅 처리
app.get('/memoview', function(req,res) {
  res.render('memoviewtemplate', {
    data: memo
  });
});
   :

/home/nodejs4/public/default.css

@import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap');

.memolayer {
  background-color:#eff;
border-top:1px solid green;
  border-left:1px solid green;
  border-bottom:2px solid green;
  border-right:2px solid green;
  padding:3px;
  margin:5px; 
  width:100px;
  height:100px;
  font-family: 'Nanum Pen Script', cursive;
  font-size:16pt;
  color:green;
}

memoviewtemplate.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>메모장</title>
</head>
<body>
  <link rel="stylesheet" type="text/css" href="./default.css">
  <h1>크레이의 쪽지쉼터</h1>
  <hr/>
  <% for (let i=0; i<=data.length-1; i++){ %>
  <div class='memolayer' 
    style="
      position:absolute;
      left:<%= data[i].x %>px;
      top:<%= data[i].y + 75 %>px;
      transform: rotate(<%= data[i].rot %>deg)"
  >
    <%= data[i].msg %><br/>
  </div>
  <% } %>
</body>
</html>

Part 19-1

views/memotemplate.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>메모장</title>
</head>
<body>
  <link rel="stylesheet" type="text/css" href="./default.css">
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<h1>크레이의 쪽지쉼터</h1>
  <hr/>
  <% for (let i=0; i<=data.length-1; i++){ %>
  <div class='memolayer' 
    style="
      position:absolute;
      left:<%= data[i].x %>px;
      top:<%= data[i].y + 75 %>px;
      transform: rotate(<%= data[i].rot %>deg)">
    <%= data[i].msg %><br/>
  </div>
  <% } %>
  <div class='memolayer' style="position:absolute;top:0px; left:740px; width:60px; height:60px;">
  <input id="input_memo" type=button value="+" 
style="width:60px;height:60px;font-size:30pt;">
</div>

<script>
  $("#input_memo")
  .click(async ()=>{
    var msg=prompt("명언을 입력해 보세요", "");
    if(msg=='' || msg==undefined)return;
    
    let ret = await $.post(
      '/memoadd', 
      { msg: msg }
    );
    
    if(ret.result=='ok')location.reload();
  });
</script>
</body>
</html>

main.js

const express=require('express');
const ejs=require("ejs");
const app=express();

app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// json post 를 사용하려면 추가
app.use(express.json());
app.use(express.urlencoded({extended : true}));

// 쪽지 더미 데이터
let memo=[
  { x:20, y:20, rot:10, 
    msg:"걱정하는게 걱정이야"
  },
  { x:230, y:80, rot:-8, 
    msg:"항상 방법은 있어"
  },
  { x:420, y:50, rot:15, 
    msg:"오늘보다 더 크고 멋지게!"
  },
  { x:420, y:210, rot:12, 
    msg:"올해엔 정말 멋지게 해보자구!!"
  },
  { x:200, y:240, rot:32, 
    msg:"인생의 여정에 고난은 연단을 이룹니다."
  }
];
console.log(memo);

// 라우팅 처리
app.get('/memoview', function(req,res) {
  res.render('memoviewtemplate', {
    data: memo
  });
});

function getRandom(min,max){
    return Math.floor(Math.random()*(max-min+1)+min);
}

app.post('/memoadd', function(req,res) {
  memo.push({ 
    x:getRandom(0,700), 
    y:getRandom(0,500), 
    rot:getRandom(-20,20), 
    msg:req.body.msg
  });
  res.json({result: 'ok'});
});

// 페이지를 찾을 수 없음 오류 처리
app.use(function(req, res, next) {
    res.status(404);  
    res.send(
    '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
    '<html><head><title>404 페이지 오류</title></head>' + 
    '<body><h1>찾을 수 없습니다</h1>' + 
    '<p>요청하신 URL ' + req.url + ' 을 이 서버에서 찾을 수 없습니다..</p><hr>' +
    '</body></html>'
  );
});

app.listen(8080, function() {});

Part 19-1

main.js

    :
app.post('/memoadd', function(req,res) {
  let x=getRandom(0,700);
  let y=getRandom(0,500);
  let rot=getRandom(-20,20);  
  memo.push({ 
    x:x,
    y:y,
    rot:rot,
    msg:req.body.msg
  });
  res.json({x:x, y:y, rot:rot, result: 'ok'});
});
    :

memoviewtemplate.ejs

    :
$("#input_memo")
.click(async function (){
  var msg=prompt("명언을 입력해 보세요", "");
  if(msg=='' || msg==undefined)return;
    
  let ret = await $.post(
    '/memoadd', 
    { msg: msg }
  );
    
  if(ret.result=='ok'){
    $("body").append(
    `<div class='memolayer' 
      style="
        position:absolute;
        left:${ret.x}px; top:${ret.y}px;
        transform: rotate(${ret.rot}deg)">
      ${msg}<br/>
    </div>
    `);
}
});
    :

Part 21-1

설치스크립트

sudo su -
rpm -Uvh http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
yum-config-manager --disable mysql56-community
yum-config-manager --enable mysql57-community*
vi /etc/yum.repos.d/mysql-community.repo

   : ( 설정 수정 후 )

yum -y install mysql-community-server
service mysqld restart
grep 'temporary password' /var/log/mysqld.log
mysql_secure_installation

   : ( 설정 수정 후 )

vi /etc/my.cnf

   : ( 설정 수정 )
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
collation-server = utf8mb4_unicode_ci
character-set-server = utf8mb4
skip-character-set-client-handshake
   :

service mysqld restart

Part 21-2

스크립트

mysql -uroot -p

show databases;
use mysql;
show tables;
select * from user;
quit

Part 21-4

스크립트

sudo su -
mysql -uroot -p
create database postbox;
show databases;
use postbox;

create table memolist (
  number int primary key auto_increment,
  x int,
  y int,
  rot int,
  msg text
) CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

desc memolist;
describe memolist;

Part 21-5

스크립트

INSERT INTO memolist SET x=420,  y=50, rot=15, msg='오늘보다 더 크고 멋지게!🍕';
INSERT INTO memolist SET x=420,  y=210, rot=12, msg='올해엔 정말 멋지게 해보자구!!🍔';
INSERT INTO memolist SET x=200,  y=240, rot=32, msg='인생의 여정에 고난은 연단을 이룹니다.🍟';

SELECT * FROM memolist;

DELETE FROM memolist WHERE number=2;
SELECT * FROM memolist;

UPDATE memolist SET rot=-15 WHERE number=3;
SELECT * FROM memolist;
exit

mysql -uroot -p postbox

Part 22-1

스크립트

sudo mkdir /home/nodejs5
sudo chown ec2-user:ec2-user /home/nodejs5
cd /home/nodejs5
npm init -y
npm install express ejs request mysql2 
npm install -g nodemon
mkdir public
mkdir views

/home/nodejs5/main.js

const express=require('express');
const ejs=require("ejs");
const app=express();
const mysql = require('mysql2/promise');
const db = mysql.createPool({
  host: 'localhost',
  port: 3306,
  user: 'root',
  password: 'DB패스워드',
  database: 'postbox'
});

// 랜덤 각도를 구현할 함수
function getRandom(min,max){
  return Math.floor(Math.random()*(max-min+1)+min);
}
  
// 메모 목록 반환 함수
async function memo_list(db)
{
  const data = await db.query(
    'SELECT * FROM memolist'
  );
  return await data[0];
}

// 메모 추가 함수
async function memo_add(db, msg)
{
  let x=getRandom(0,700);
  let y=getRandom(0,500);
  let rot=getRandom(-20,20);
  // MYSQL 쿼리 실행
  const result = await db.query(
    "INSERT INTO memolist SET x = ?, y = ?, rot = ?, msg = ?",
    [ x, y, rot, msg ]
  );
  return await true;
}

// 템플릿 엔진과 폴더 설정
app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// json post 를 사용하려면 추가
app.use(express.json());
app.use(express.urlencoded({extended : true}));

// 쪽지 목록
app.get('/memoview', async function (req,res) {
  res.render('memoviewtemplate', {
    data: await memo_list(db)
  });
});

// 쪽지 추가
app.post('/memoadd', async function (req,res) {
  await memo_add(db, req.body.msg);
  res.json({result: 'ok'});
});

// 페이지를 찾을 수 없음 오류 처리
app.use((req, res, next) => {
  res.status(404);
  res.send(
    '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
    '<html><head><title>404 페이지 오류</title></head>' +
    '<body><h1>찾을 수 없습니다</h1>' +
    '<p>요청하신 URL ' + req.url + ' 을 이 서버에서 찾을 수 없습니다..</p><hr>' +
    '</body></html>'
  );
});

app.listen(8080, ()=> {});

/home/nodejs5/public/default.css

@import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap');

.memolayer {
  background-color:#eff;
  border-top:1px solid green;
  border-left:1px solid green;
  border-bottom:2px solid green;
  border-right:2px solid green;
  padding:3px;
  margin:5px; 
  width:100px;
  height:100px;
  font-family: 'Nanum Pen Script', cursive;
  font-size:16pt;
  color:green;
}

/home/nodejs5/views/memoviewtemplate.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>메모장</title>
</head>
<body>
  <link rel="stylesheet" type="text/css" href="./default.css">
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <h1>크레이의 쪽지쉼터</h1>
  <hr/>
  <% for (let i=0; i<=data.length-1; i++){ %>
  <div class='memolayer' 
    style="
      position:absolute;
      left:<%= data[i].x %>px;
      top:<%= data[i].y + 75 %>px;
      transform: rotate(<%= data[i].rot %>deg)">
    <%= data[i].msg %><br/>
  </div>
  <% } %>
  <div class='memolayer' style="position:absolute;top:0px; left:740px; width:60px; height:60px;">
    <input id="input_memo" type=button value="+" 
     style="width:60px;height:60px;font-size:30pt;">
  </div>
  
  <script>
  $("#input_memo")
  .click(async function (){
    var msg=prompt("명언을 입력해 보세요", "");
    if(msg=='' || msg==undefined)return;
    
    let ret = await $.post(
      '/memoadd', 
      { msg: msg }
    );
    
    if(ret.result=='ok'){
      location.reload();
    }
  });
  </script>
</body>
</html>

스크립트

cd /home/nodejs5
nodemon main.js

Part 23-1

/home/nodejs5/db.js

const mysql = require('mysql2/promise');
const db = mysql.createPool({
  host: 'localhost',
  port: 3306,
  user: 'root',
  password: 'DB패스워드',
  database: 'postbox'
});

module.exports = db;

/home/nodejs5/main.js

const express=require('express');
const ejs=require("ejs");
const app=express();
const db=require("./db.js");

// 랜덤 각도를 구현할 함수
function getRandom(min,max){
  return Math.floor(Math.random()*(max-min+1)+min);
}   
    :

/home/nodejs5/memo.js

// 랜덤 각도를 구현할 함수
function getRandom(min,max){
  return Math.floor(Math.random()*(max-min+1)+min);
}
  
// 쪽지 목록 반환 함수
async function memo_list(db)
{
  const data = await db.query(
    'SELECT * FROM memolist'
  );
  return await data[0];
}

// 메모 추가 함수
async function memo_add(db, msg)
{
  let x=getRandom(0,700);
  let y=getRandom(0,500);
  let rot=getRandom(-20,20);
  // MYSQL 쿼리 실행
  const result = await db.query(
    "INSERT INTO memolist SET x = ?, y = ?, rot = ?, msg = ?",
    [ x, y, rot, msg ]
  );
  return await true;
}

module.exports={ 
  memo_list,
  memo_add
};

/home/nodejs5/main.js

const express=require('express');
const ejs=require("ejs");
const app=express();
const db=require("./db.js");
const {memo_list, memo_add}=require("./memo.js");

// 템플릿 엔진과 폴더 설정
app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// json post 를 사용하려면 추가
app.use(express.json());
app.use(express.urlencoded({extended : true}));

// 쪽지 목록
app.get('/memoview', async function (req,res) {
  res.render('memoviewtemplate', {
    data: await memo_list(db)
  });
});

// 쪽지 추가
app.post('/memoadd', async function (req,res) {
  await memo_add(db, req.body.msg);
  res.json({result: 'ok'});
});

// 페이지를 찾을 수 없음 오류 처리
app.use((req, res, next) => {
  res.status(404);
  res.send(
    '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">' +
    '<html><head><title>404 페이지 오류</title></head>' +
    '<body><h1>찾을 수 없습니다</h1>' +
    '<p>요청하신 URL ' + req.url + ' 을 이 서버에서 찾을 수 없습니다..</p><hr>' +
    '</body></html>'
  );
});

app.listen(8080, ()=> {});

Part 23-2

/home/nodejs5/memo.js

class MemoClass {
  // 랜덤 각도를 구현할 함수
  getRandom(min,max){
    return Math.floor(Math.random()*(max-min+1)+min);
  }

  // 쪽지 목록 반환 함수
  async memo_list(db)
  {
    const data = await db.query(
      'SELECT * FROM memolist'
    );
    return await data[0];
  }

  // 메모 추가 함수
  async memo_add(db, msg)
  {
    let x=this.getRandom(0,700);
    let y=this.getRandom(0,500);
    let rot=this.getRandom(-20,20);
    // MYSQL 쿼리 실행
    const result = await db.query(
      "INSERT INTO memolist SET x = ?, y = ?, rot = ?, msg = ?",
      [ x, y, rot, msg ]
    );
    return await true;
  }
};

module.exports=MemoClass;

main.js 수정

    :
const db=require("./db.js");
const MemoClass = require("./memo.js");
const memo = new MemoClass();


// 템플릿 엔진과 폴더 설정
app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// json post 를 사용하려면 추가
app.use(express.json());
app.use(express.urlencoded({extended : true}));

// 쪽지 목록
app.get('/memoview', async function (req,res) {
  res.render('memoviewtemplate', {
    data: await memo.memo_list(db)
  });
});

// 쪽지 추가
app.post('/memoadd', async function (req,res) {
  await memo.memo_add(db, req.body.msg);
  res.json({result: 'ok'});
});
    :

Part 24-1

memo.js 수정

class MemoClass {
    :
// 메모 삭제 함수
  async memo_del(db, number)
  {
    // MYSQL 쿼리 실행
    const result = await db.query(
      "DELETE FROM memolist WHERE number = ?",
      [ number ]
    );
    return await true;
  }
};

main.js 수정

// 쪽지 삭제
app.post('/memodel', async function (req,res) {
  await memo.memo_del(db, req.body.number);
  res.json({result: 'ok'});
});

// 페이지를 찾을 수 없음 오류 처리

/home/nodejs5/default.css

.memodel {
  color:red;
  text-align:right;
  width:15px;
  height:15px;
  cursor:pointer;
  font-size:10pt;
  float:right;
}

/home/nodejs5/memoviewtemplate.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>메모장</title>
</head>
<body>
  <link rel="stylesheet" type="text/css" href="./default.css">
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <h1>크레이의 쪽지쉼터</h1>
  <hr/>
  <% for (let i=0; i<=data.length-1; i++){ %>
  <div class='memolayer' 
    style="
      position:absolute;
      left:<%= data[i].x %>px;
      top:<%= data[i].y + 75 %>px;
      transform: rotate(<%= data[i].rot %>deg)">
    <div class='memodel' number="<%= data[i].number %>">💥</div>
    <%= data[i].msg %><br/>
  </div>
  <% } %>
  <div class='memolayer' style="position:absolute;top:0px; left:740px; width:60px; height:60px;">
    <input id="input_memo" type=button value="+" 
     style="width:60px;height:60px;font-size:30pt;">
  </div>
  
  <script>
  $("#input_memo")
  .click(async function (){
    var msg=prompt("명언을 입력해 보세요", "");
    if(msg=='' || msg==undefined)return;
    
    let ret = await $.post(
      '/memoadd', 
      { msg: msg }
    );
    
    if(ret.result=='ok'){
      location.reload();
    }
  });
  $(".memodel")
  .click(async function (){
    let ret = await $.post(
      '/memodel', 
      { number: $(this).attr('number') }
    );    
    if(ret.result=='ok'){
      location.reload();
    }
  });
  </script>
</body>
</html>

Part 24-3

$(".memodel")
.click(async function (){
  let ret = await $.post(
    '/memodel', 
    { number: $(this).attr('number') }
  );
  $(this).parent().remove();
});

Part 25-4

sudo mkdir /home/multichat2
sudo chown ec2-user:ec2-user /home/multichat2
cd /home/multichat2
npm init -y
npm install express ejs request socket.io
npm install -g nodemon
mkdir public
mkdir views

Part 25-6

/home/multichat2/index.js

// 웹소켓을 사용하기 위한 기본 모듈 구성
const express = require('express');
const app = express();
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);

// chat.ejs 템플릿을 위한 사전 정의
app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// 웹 브라우저에서 퍼블릭IP:8080 주소를 호출하면 chat.ejs 템플릿을 띄웁니다.
app.get('/', (req, res) => {
  res.render('chat');
});

// 접속이 발생하면 함수가 살행됩니다. 모든 사용자를 대상으로 합니다.
io.on('connection', (socket) => {
  console.log(socket.id);
});

// 8080 포트에서 접속을 대기합니다.
http.listen(8080, () => {
  console.log('Connect at 8080');
});

/home/multichat2/views/chat.ejs

<html>
  <head>
    <title></title>
  </head>
  <body>
    <script src="/socket.io/socket.io.js"></script>
    <script>
// 웹소켓 연결
    const socket = io({
      transports: ['websocket'], 
      upgrade: false
    });
    
    // 서버 연결됨
    socket.on("connect", function () {
      console.log("소켓아이디 : " + socket.id);
    });
    
    // 서버 연결 끊김 & 다운
    socket.on('disconnect', (data) => {
      location.reload();
    });
    </script>
  </body>
</html>

Part 25-7

index.js

    :
io.on('connection', (socket) => {
  console.log(socket.id);
  socket.leave(socket.id);
  socket.join("world");
  console.log(socket.adapter.rooms);
  
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });
});
    :

Part 25-8

chat.ejs

<html>
  <head>
    <title></title>
  </head>
  <body>
    <!-- 닉네임 입력 영역 -->
    <div>
      닉네임 : <input type=text id='NickName'>
      <input type=button id="LoginButton" value="접속">
    </div>
  
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
    // 웹소켓 연결
    const socket = io({
      transports: ['websocket'], 
      upgrade: false
    });

    // 접속자 닉네임
    var nickname="";

    // 서버 연결됨
    socket.on("connect", function () {
      console.log("소켓아이디 : " + socket.id);
    });
    
    // 서버 연결 끊김 & 다운
    socket.on('disconnect', (data) => {
      location.reload();
    });

    // 로그인 버튼
    $("#LoginButton").click(function(){
      if($("#NickName").val()==""){
        alert("닉네임을 입력하세요.");
        return false;
      }
      socket.emit('TryLogin', {
        NickName: $("#NickName").val()
      });
    });

    // 로그인 응답
    socket.on('FeedLogin', (data) => {
      if(data.result!='ok'){
        alert('로그인이 실패하였습니다.');
        return;
      }
      nickname=$("#NickName").val();
      $("#NickName").css('border', '0px');
      $("#LoginButton").hide();
    });
    </script>
  </body>
</html>

index.js

    :
io.on('connection', (socket) => {
  console.log(socket.id);
  socket.leave(socket.id);
  socket.join("world");
  console.log(socket.adapter.rooms);
  
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });

  // 로그인 시도
  // 입력)
  //   NickName : 닉네임
  socket.on('TryLogin', (data) => {
    try {
      console.log('TryLogin : ' + data.NickName);
      socket.NickName=data.NickName;
      
      socket.emit('FeedLogin', {
        result: 'ok'      
      }); // 로그인 성공
    }
    catch(e)
    {
      console.log(e);
    }
  });

});
    :

Part 25-10

chat.ejs

<html>
  <head>
    <title></title>
  </head>
  <body>
    <link rel="stylesheet" type="text/css" href="default.css">
    = 크레이 멀티 채팅방 =<br/>
    <br/>
    
    <!-- 닉네임 입력 영역 -->
    <div>
      닉네임 : <input type=text id='NickName'>
      <input type=button id="LoginButton" value="접속">
    </div>
    
    <!-- 방 목록을 담을 컨테이너 -->
    <div id='rooms' class='roomContainer'></div>
    
    <!-- 방 생성 버튼 -->
    <div class='roomCls' roomName='*New*'>+개설</div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="chat.js"></script>
  </body>
</html>

/home/multichat2/public/default.css

.roomContainer {
  width:500px;
  border:1px solid blue;
  padding:10px;
}

.roomCls {
  margin:10px;
  width:80px;
  height: 80px;
  line-height:80px;
  border:1px solid green;
  text-align:center;
  vertical-align:middle;
  display: inline-block;
  cursor:pointer;
}

/home/multichat2/public/chat.js

// 웹소켓 연결
const socket = io({
  transports: ['websocket'], 
  upgrade: false
});

// 닉네임 저장 변수
var nickname="";

// 접속 월드 저장 변수
var roomname="";

// 서버 연결됨
socket.on("connect", function () {
  console.log("소켓아이디 : " + socket.id);
});

// 서버 연결 끊김 & 다운
socket.on('disconnect', (data) => {
  location.reload();
});

// 로그인 버튼 클릭
$("#LoginButton").click(function(){
  if($("#NickName").val()==""){
    alert("닉네임을 입력하세요.");
    return false;
  }
  socket.emit('TryLogin', {
    NickName: $("#NickName").val()
  });
});

// 로그인 응답    
// 서버의 FeedLogin 메시지를 받아 처리
socket.on('FeedLogin', (data) => {
  // 응답이 ok 가 아니면 로그인 실패 처리
  if(data.result!='ok'){
    alert('로그인이 실패하였습니다.');
    return;
  }
  // 닉네임 변수에 닉네임 보관
  nickname=$("#NickName").val();
  // 닉네임 입력상자의 경계선을 없앰
  $("#NickName").css('border', '0px');
  // 접속 버튼을 숨김
  $("#LoginButton").hide();
  
  socket.emit('TryRoomList');
});

// 방 개설 버튼 클릭시
$(".roomCls").click(function (){
  // 로그안 여부를 우선 검사
  if(nickname==""){
    alert("먼저 로그인하세요");
    return false;
  }
  
  // 신규 룸 개설
  if($(this).attr('roomName')=='*New*'){
    // 방 이름을 입력 받습니다.
    roomName = prompt("방이름을 입력하세요");
    // 방 이름이 제대로 입력되지 않았다면 중지
    if(roomName=="" || roomName==undefined)return;
    // 서버에 방 생성을 요청하는 TryNewRoom 메시지를 보냅니다.
    socket.emit('TryNewRoom', {
      roomName: roomName
    });
  }
});

// 신규방 피드백
socket.on('FeedNewRoom', (data) => { 
  console.log(data);
  if(data.result=='duplicate'){
    alert("이미 방이 존재합니다.");
    return;
  }
  if(data.result!='ok'){
    alert('신규 방을 생성하는데 실패하였습니다.');
    return;
  }
  socket.emit('TryRoomList');
});

// 광장의 유저들에게 방목록이 바뀌었음을 알리는 피드백 메시지
socket.on('FeedRefreshRoomList', (data) => { 
  // 룸 정보 요청
  socket.emit('TryRoomList');
});    

// 방 목록 
socket.on('FeedRoomList', (data) => { 
  if(data.result!='ok'){
    alert('방 목록을 받아오는데 실패하였습니다.');
    return;
  }
  console.log('FeedRoomList');
  var html="";
  for(var i=0;i<data.rooms.length;++i){
    console.log(data.rooms[i]);
    roomNameThis=data.rooms[i]['roomName'];
    html +=`
      <div class='roomCls' roomname='${roomNameThis}'>
        ${roomNameThis}
      </div>
      `;
  }
  
  $("#rooms").html(html);
});

index.js

// 웹소켓을 사용하기 위한 기본 모듈 구성
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);

// chat.ejs 템플릿을 위한 사전 정의
app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// 방 정보를 보관합니다.
let rooms = [];

// 예측 불가한 예외처리를 합니다. 갑작스런 node.js의 중지를 막아줍니다.
process.on('uncaughtException', function (err) {
  console.log('uncaughtException 발생 : ' + err);
});

// 웹 브라우저에서 퍼블릭IP:8080 주소를 호출하면 chat.ejs 템플릿을 띄웁니다.
app.get('/', (req, res) => {
  res.render('chat');
});

// 접속이 발생하면 함수가 살행됩니다. 모든 사용자를 대상으로 합니다.
io.on('connection', (socket) => {
  console.log('사용자 1명의 연결이 추가되었습니다.');
  // 월드로 보내주고
  socket.join("world");
  // 현재 접속중인 방에서는 나오게 합니다.
  socket.leave(socket.id);
  // 소켓 오브젝트에 현재 방의 이름을 입력
  socket.roomName="world";
  // 그리고 닉네임은 정해지기 전까지 공백을 넣어 둡니다
  socket.NickName="";
  
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });

  // 로그인 시도
  // 입력)
  //   NickName : 닉네임
  socket.on('TryLogin', (data) => {
    try {
      console.log('TryLogin : ' + data.NickName);
      socket.NickName=data.NickName;
      
      socket.emit('FeedLogin', {
        result: 'ok'      
      }); // 로그인 성공
    }
    catch(e)
    {
      console.log(e);
    }    
  });
  
  // 방목록
  // 웹 브라우저에서 방목록을 요청하는 메시지가 전달되면
  socket.on('TryRoomList', () => {
    try {
      console.log("FeedRoomList");
      //  rooms 변수값을 웹 브라우저에 전달합니다.
      socket.emit("FeedRoomList", {
        result:'ok',
        rooms:rooms
      });
    }
    catch(e)
    {
      console.log(e);
    }
  });

  // 신규 방 생성
  // 입력)
  //   roomName :  방의 이름
  // 웹 브라우저에서 요청받은 방의 개설을 처리합니다.
  socket.on('TryNewRoom', (data) => {
    try {
      console.log("TryNewRoom");
      // 모든 방에서 중복된 방이름 검사하기 위해 반복을 돌립니다.
      for(var i=0;i<rooms.length;++i)
      {
        // 동일 방 이름이 있으면
        if(rooms[i].roomName==data.roomName){
          console.log("FeedNewRoom");
          // 웹 브라우저에서 이름이 중복되었다는 사실을 알립니다.
          socket.emit("FeedNewRoom", {
            result:'duplicate'
          });
          return;
        }
      }
      
      // 앞에서 중복된 이름이 없다면 방 개설이 가능합니다.
      // 방이름과 채팅내역을 묶어서 방 목록에 정보에 추가해 줍니다.
      // 채팅 내역은 새로 만드는 방이니 비어 있습니다. []
      rooms.push({
        roomName: data.roomName,
        chat: []
      });
      
      console.log("FeedNewRoom");
      // 웹 브라우저에 방에 잘 입장했다고 신호를 보냅니다.
      socket.emit("FeedNewRoom", {
        result:'ok'
      });
      
      // 이어서 월드에 있는 다른 유저들에게 방목록을 새로 고치라고 알려줍니다.
      // 방이 새로 개설되었기 때문입니다.
      socket.broadcast.to('world').emit("FeedRefreshRoomList", {
        result:'ok'
      });      
      console.log(rooms);
    }
    catch(e){
      console.log(e);
    }
  });

});

// 8080 포트에서 접속을 대기합니다.
http.listen(8080, () => {
  console.log('Connect at 8080');
});

Part 25-11

/home/multichat2/index.js

// 웹소켓을 사용하기 위한 기본 모듈 구성
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);

// chat.ejs 템플릿을 위한 사전 정의
app.set('view engine', 'ejs');
app.set('views', './views');

// public 폴더하위의 파일들을 기본으로 서비스
app.use(express.static('public'));

// 방 정보를 보관합니다.
let rooms = [];

// 예측 불가한 예외처리를 합니다. 갑작스런 node.js의 중지를 막아줍니다.
process.on('uncaughtException', function (err) {
  console.log('uncaughtException 발생 : ' + err);
});

// 웹 브라우저에서 퍼블릭IP:8080 주소를 호출하면 chat.ejs 템플릿을 띄웁니다.
app.get('/', (req, res) => {
  res.render('chat');
});

// 접속이 발생하면 함수가 살행됩니다. 모든 사용자를 대상으로 합니다.
io.on('connection', (socket) => {
  console.log('사용자 1명의 연결이 추가되었습니다.');
  // 월드로 보내주고
  socket.join("world");
  // 현재 접속중인 방에서는 나오게 합니다.
  socket.leave(socket.id);
  // 소켓 오브젝트에 현재 방의 이름을 입력
  socket.roomName="world";
  // 그리고 닉네임은 정해지기 전까지 공백을 넣어 둡니다
  socket.NickName="";

  // 웹 브라우저에서 유저 접속이 없으면 끊김 처리를 합니다.
  socket.on('disconnect', () => {
    try {
      console.log('사용자 1명의 연결이 끊겼습니다');
      // 먼저 현재 방에서 나오게 합니다.
      socket.leave(socket.roomName);
      // 그리고 어느 채팅방에 입장했던 경우라면
      if(socket.roomName!="" && socket.roomName!="world"){
        // 방에 있는 다른 사용자들에게 퇴장을 알립니다. ( FeedLeaveUser )
        socket.broadcast.to(socket.roomName).emit("FeedLeaveUser", {
          result:'ok',
          NickName: socket.NickName
        });
      }
    }
    catch(e)
    {
      console.log(e);
    }
  });

  // 로그인 시도
  // 입력)
  //   NickName : 닉네임
  // 웹 브라우저에서 로그인 메시지가 전달되면
  socket.on('TryLogin', (data) => {
    try {
      console.log('TryLogin : ' + data.NickName);
      // 소켓오브젝트에 닉네임 정보를 저장합니다.
      socket.NickName=data.NickName;
      // 웹 브라우저에 로그인 결과를 전달합니다.
      socket.emit('FeedLogin', {
        result: 'ok'      
      }); // 로그인 성공
    }
    catch(e)
    {
      console.log(e);
    }    
  });
  
  // 방목록
  // 웹 브라우저에서 방목록을 요청하는 메시지가 전달되면
  socket.on('TryRoomList', () => {
    try {
      console.log("FeedRoomList");
      // 방 목록인 rooms 변수값을 웹 브라우저에 전달합니다.
      socket.emit("FeedRoomList", {
        result:'ok',
        rooms:rooms
      });
    }
    catch(e)
    {
      console.log(e);
    }
  });

  // 신규 방 생성
  // 입력)
  //   roomName :  방의 이름
  // 웹 브라우저에서 요청받은 방의 개설을 처리합니다.
  socket.on('TryNewRoom', (data) => {
    try {
      console.log("TryNewRoom");
      // 모든 방에서 중복된 방이름 검사하기 위해 반복을 돌립니다.
      for(let i=0;i<rooms.length;++i)
      {
        // 동일 방 이름이 있으면
        if(rooms[i].roomName==data.roomName){
          console.log("FeedNewRoom");
          // 웹 브라우저에서 이름이 중복되었다는 사실을 알립니다.
          socket.emit("FeedNewRoom", {
            result:'duplicate'
          });
          return;
        }
      }
      
      // 앞에서 중복된 이름이 없다면 방 개설이 가능합니다.
      // 방이름과 채팅내역을 묶어서 방 목록에 정보에 추가해 줍니다.
      // 채팅 내역은 새로 만드는 방이니 비어 있습니다. []
      rooms.push({
        roomName: data.roomName,
        chat: []
      });
      
      // 방을 개설한 경우 방에 자동으로 입장합니다.
      socket.join(data.roomName);    
      // 이전 방에서 퇴장합니다.
      socket.leave(socket.roomName);
      // 소켓 오브젝트에 방의 이름을 보관하고,
      socket.roomName=data.roomName;
      console.log("FeedNewRoom");
      // 웹 브라우저에 방에 잘 입장했다고 신호를 보냅니다.
      socket.emit("FeedNewRoom", {
        result:'ok'
      });
      
      // 이어서 월드에 있는 다른 유저들에게 방목록을 새로 고치라고 알려줍니다.
      // 방이 새로 개설되었기 때문입니다.
      socket.broadcast.to('world').emit("FeedRefreshRoomList", {
        result:'ok'
      });      
      console.log(rooms);
    }
    catch(e){
      console.log(e);
    }
  });
  
  // 기존방 입장
  // 입력)
  //   roomName :  방의 이름
  // 웹 브라우저에서 존재하는 방의 입장 요구를 처리합니다.
  socket.on('TryEnterRoom', (data) => {
    try {
      console.log("TryEnterRoom");
      console.log(data);
      
      // 그동안 사라졌을수도 있기 때문에 방이 있나 검사합니다.
      find=false;
      // 모든방을 찾아봐서
      for(let i=0;i<rooms.length;++i)
      {
        // 동일한 방의 이름이 있다면
        if(rooms[i].roomName==data.roomName){
          // 찾았다고 체크해 놓습니다.
          find=true;
        }
      }
      
      // 만일 방을 찾지 못해다면
      if(find==false){
        console.log("FeedEnterRoom");
        // 웹 브라우저에 찾지못함 notfound 을 알려줍니다.
        socket.emit("FeedEnterRoom", {
          result:'notfound'
        });
        return;
      }
      
      // 방을 찾았다면 이쪽으로 넘어옵니다.
      // 방에 입장하고
      socket.join(data.roomName);    
      // 이전 방에서는 탈퇴합니다.
      socket.leave(socket.roomName);
      // 소켓 오브젝트에 방의 이름을 보관합니다.
      socket.roomName=data.roomName;
      console.log("FeedEnterRoom");
      // 방에 잘 입장되었다고 유저에게 회신 후,
      socket.emit("FeedEnterRoom", {
        result:'ok'
      });
      
      // 방의 있는 다른 유저에게도 입장 알림
      socket.broadcast.to(data.roomName).emit("FeedEnterUser", {
        result:'ok',
        NickName: socket.NickName
      });
    }
    catch(e){
      console.log(e);
    }
  });

  // 방 정보 요청
  // 입력)
  //   roomName :  방의 이름
  // 웹 브라우저에서 방의 정보를 요청받으면 실행
  socket.on('TryRoomInfo', () => {
    try {
      // 현재 방의 유저들 닉네임을 알아내기 위해 Socket.io 내장 소켓 목록을 받아옵니다.
      clients=io.sockets.adapter.rooms.get(socket.roomName);
      
      // 닉네임 배열 변수를 선언하고,
      let nicknames=[];
      // 클라이언트 목록을 반복을 돌리면서
      for (const clientId of clients) {
        // 클라이언트별 소켓을 구하고
        const clientSocket = io.sockets.sockets.get(clientId);
        // 닉네임을 꺼내서 닉네임 배열 변수에 추가해 줍니다.
        nicknames.push(clientSocket.NickName);
      }
      
      // 채팅 내역을 받아올 배열 변수를 선언하고
      let roomChat=[];
      // 방 목록을 순회하다가
      for(let i=0;i<rooms.length;++i){
        // 동일한 방 이름을 찾으면
        if(rooms[i].roomName==socket.roomName){
          // 채팅 내역을 roomChat 배열변수에 저장합니다.
          roomChat = rooms[i].chat;
        }
      }
      
      // 방의 이름, 참가자, 채팅 내역을 웹 브라우저에 회신합니다.
      socket.emit("FeedRoomInfo", {
        roomName: socket.roomName,
        roomUser: nicknames,
        roomChat: roomChat
      });
    }
    catch(e)
    {
      console.log(e);
    }
  });

  // 사용자의 채팅방 나가기 요청
  socket.on('TryLeaveRoom', () => {
    try {
      console.log("TryLeaveRoom");
      // 현재 방에서 나간 다음
      socket.leave(socket.roomName);
      // 월드방에 진입합니다.
      socket.join('world');
      
      // 방의 다른 유저들에게 퇴장 알림 메시지 전송
      socket.broadcast.to(socket.roomName).emit("FeedLeaveUser", {
        result:'ok',
        NickName: socket.NickName
      });
      
      // 소켓의 현재 위치를 world 로 변경
      socket.roomName='world';      
      console.log("FeedLeaveRoom");
      
      // 방에서 나가는데 완료되었음을 사용자에게 통보합니다.
      socket.emit("FeedLeaveRoom", {
        result:'ok'
      });
    }
    catch(e)    
    {
      console.log(e);
    }
  });
  
  // 채팅메시지가 웹 브라우저에서 전달되면,
  socket.on('TryChat', (data) => {
    try {
      console.log("TryChat");
      // 채팅방을 순회하며
      for(var i=0;i<rooms.length;++i){
        // 동일한 방을 찾은 경우
        if(rooms[i].roomName==socket.roomName){
          // "닉네임 : 채팅메시지" 문장을 채팅방의 chat 배열 변수에 추가합니다.
          rooms[i].chat.push(socket.NickName + " : " + data.msg);
          // 그리고 사용자에게 성공했음을 알리는 동시에, 닉네임과 채팅 메시지를 전달합니다.
          socket.emit("FeedChat", {
            result:'ok',
            NickName: socket.NickName,
            msg: data.msg
          });
          // 같은 채팅방의 다른 사용자들에게도 동일한 메시지를 알리는 동시에, 닉네임과 채팅 메시지를 전달합니다.
          socket.broadcast.to(socket.roomName).emit("FeedChat", {
            result:'ok',
            NickName: socket.NickName,
            msg: data.msg
          });
          return;
        }
      }
    }
    catch(e)
    {
      console.log(e);
    }
  });
  
});

// 8080 포트에서 접속을 대기합니다.
http.listen(8080, () => {
  console.log('Connect at 8080');
});

/home/multichat2/views/chat.ejs

<html>
  <head>
    <title></title>
  </head>
  <body>
    <link rel="stylesheet" type="text/css" href="default.css">    
    = 크레이 멀티 채팅방 =<br/>
    <br/>
    
    <!-- 닉네임 입력 영역 -->
    <div>
      닉네임 : <input type=text id='NickName'>
      <input type=button id="LoginButton" value="접속">
    </div>
    
    <!-- 방 목록을 담을 컨테이너 -->
    <div id='rooms' class='roomContainer'></div>
    
    <!-- 방 생성 버튼 -->
    <div class='roomCls' roomName='*New*'>+개설</div>

    <!-- 채팅 메시지 입력창과 전송 버튼 -->
    <div>
        <input id="msg" autocomplete="off"/>
        <input type=button id="btnChat" value="전송">
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="chat.js"></script>
  </body>
</html>

/home/multichat2/public/default.css

.roomContainer {
  width:500px;
  border:1px solid blue;
  padding:10px;
}

.roomCls {
  margin:10px;
  width:80px;
  height: 80px;
  line-height:80px;
  border:1px solid green;
  text-align:center;
  vertical-align:middle;
  display: inline-block;
  cursor:pointer;
}

/home/multichat2/public/chat.js

// 웹소켓 연결
const socket = io({
  transports: ['websocket'], 
  upgrade: false
});

// socket.emit 함수에 콘솔창에 메시지를 표시하는 기능을 추가하여
// 새로운 socket.emit2 함수를 정의
socket.emit2 = function(eventname, data){
  this.emit(eventname, data);
  console.log(eventname);
  console.log(data);
}

// 닉네임 저장 변수
var nickname="";

// 접속 월드 저장 변수
var roomname="";

// 서버 연결됨
socket.on("connect", function () {
  console.log("소켓아이디 : " + socket.id);
});

// 서버 연결 끊김 & 다운
socket.on('disconnect', (data) => {
  // 새로 고침
  location.reload();
});

// 로그인 버튼 클릭 이벤트
$("#LoginButton").click(function(){
  if($("#NickName").val()==""){
    alert("닉네임을 입력하세요.");
    return false;
  }
  socket.emit2('TryLogin', {
    NickName: $("#NickName").val()
  });
});

// 로그인 응답    
// 서버의 FeedLogin 메시지를 받아 처리
socket.on('FeedLogin', (data) => {
  // 응답이 ok 가 아니면 로그인 실패 처리
  if(data.result!='ok'){
    alert('로그인이 실패하였습니다.');
    return;
  }
  // 닉네임 변수에 닉네임 보관
  nickname=$("#NickName").val();
  // 닉네임 입력상자의 경계선을 없앰
  $("#NickName").css('border', '0px');
  // 접속 버튼을 숨김
  $("#LoginButton").hide();
  
  socket.emit2('TryRoomList');
});

// 서버의 방 목록 회신 처리 이벤트 함수
// 응답)
//   data.result : ok(정상)
//   data.rooms : 방 목록
socket.on('FeedRoomList', (data) => { 
  // ok 가 아닌 메시지가 수신되면 오류를 알리고 중지합니다.
  if(data.result!='ok'){
    alert('방 목록을 받아오는데 실패하였습니다.');
    return;
  }
  console.log('FeedRoomList');
  // 방목록을 표현할 HTML 코드를 하나씩 완성합니다.
  // HTML 코드를 저장할 변수를 정의한 다음
  var html="";
  // 방의 갯수만큼 순환을 돌며
  for(var i=0;i<data.rooms.length;++i){
    console.log(data.rooms[i]);
    roomNameThis=data.rooms[i]['roomName'];
    // html 변수에 방이름을 하나씩 넣어 DIV 태그를 추가합니다.
    html +=`
      <div class='roomCls' roomname='${roomNameThis}' onclick="EnterRoom('${roomNameThis}')">
        ${roomNameThis}
      </div>
      `;
  }
  
  // 반복이 끝났지만 아직 화면에 반영한 것은 아니기 때문에,
  // rooms 라는 태그 안쪽에 만들어진 html 변수값를 넣어줍니다.
  // 참고로 이렇게 하는 것은 속도상의 이점 때문입니다.
  $("#rooms").html(html);
});

// 방 개설 버튼 클릭시 이벤트 호출 함수 ( 신규룸 개설 )
$(".roomCls").click(function (){
  // 로그인 여부를 우선 검사
  if(nickname==""){
    alert("먼저 로그인하세요");
    return false;
  }
  
  // 신규방 개설인 경우라면
  if($(this).attr('roomName')=='*New*'){
    // 방 이름을 입력 받습니다.
    roomName = prompt("방이름을 입력하세요");
    // 방 이름이 제대로 입력되지 않았다면 중지
    if(roomName=="" || roomName==undefined)return;
    // 서버에 방 생성을 요청하는 TryNewRoom 메시지를 보냅니다.
    socket.emit2('TryNewRoom', {
      roomName: roomName
    });
  }
});

// 채팅방 선택시 호출 함수, 방에 입장합니다.
function EnterRoom(roomName)
{
  // 선택한 채팅방 입장 메시지를 서버에 전달합니다.
  socket.emit2('TryEnterRoom', {
    roomName: roomName
  });
}

// 신규방 피드백
// 응답)
//   data.result : duplicate (방 이름이 중복되었습니다), ok(방 개설 성공)
socket.on('FeedNewRoom', (data) => { 
  console.log(data);
  // 방 이름이 이미 있는 경우 진행 중지
  if(data.result=='duplicate'){
    alert("이미 방이 존재합니다.");
    return;
  }
  // 응답메시지가 ok가 아닌 경우도 중지합니다.
  if(data.result!='ok'){
    alert('신규 방을 생성하는데 실패하였습니다.');
    return;
  }
  // 방의 개설이 성공한 경우 이쪽으로 넘어옵니다
  // 방의 이름을 roomname 변수에 보관하고,
  roomname=data.roomName;
  // 자동 입장하였기 때문에 현재 방의 정보를 요청하는 메시지를 전송합니다.
  socket.emit2('TryRoomInfo');
});

// 서버에서 방입장 피드백이 온 경우
socket.on('FeedEnterRoom', (data) => { 
  console.log(data);
  // 방이 없어진 경우라면 알리고 중지합니다.
  if(data.result=='notfound'){
    alert("입장하려는 방을 찾지 못했습니다.");
    return;
  }
  // 결과가 성공이 아닌 경우도 알리고 중지합니다.
  if(data.result!='ok'){
    alert('방 입장에 실패하였습니다.');
    return;
  }
  // 여기까지 오면 방의 입장에 성공한 것입니다.
  // 현재 방의 이름을 보관하고
  roomname=data.roomName;
  // 방의 상세 정보를 서버에 요청합니다.
  socket.emit2('TryRoomInfo');
});

// 서버의 방정보 회신처리 이벤트 함수
socket.on('FeedRoomInfo', (data) => {
  // 사용자 내역 보여줄 html 문서를 html변수에 구성합니다.
  let html=`
    방이름 : ${data.roomName}<br/>
    <div id='userlist'>`;
  for(let i=0;i<data.roomUser.length;++i){
    html+=`😀 ${data.roomUser[i]}<br/>`;
  }
  html+=`</div>`;
  // 채팅자 내역 보여줄 html 문서를 html변수에 추가 구성합니다.
  for(let i=0;i<data.roomChat.length;++i){
    html+=`🚀 ${data.roomChat[i]}<br/>`;
  }
  // 채팅방 나가기 버튼을 추가로 구성합니다.
  html += `<input type=button value='나가기' onclick='LeaveRoom()'>`;
  // html 변수의 내용은 아직 화면에 반영되지 않았습니다.
  // rooms 컨테이너 안에 내용을 치환합니다.
  // 이렇게 함으로 속도의 이점이 있습니다.
  $("#rooms").html(html);
});

// 서버의 다른유저입장 메시지 처리 함수
socket.on('FeedEnterUser', (data) => { 
  // 룸 정보를 요청합니다. 나머지는 알아서 처리됩니다.
  socket.emit2('TryRoomInfo');
});

// 나가기 버튼 클릭시 서버에 방 떠나기 메시지를 전송합니다.
function LeaveRoom(){
  socket.emit2('TryLeaveRoom');
}

// 서버의 방 나가기 성공피드백 메시지 처리 함수
socket.on('FeedLeaveRoom', (data) => {
  console.log('FeedLeaveRoom');
  // 현재 방의 위치를 world 로 변경하고
  roomname='world';
  // 방 목록을 서버에 요청합니다.
  socket.emit2('TryRoomList');
});

// 서버의 다른유저퇴장 메시지 처리
socket.on('FeedLeaveUser', (data) => { 
  // 방의 정보를 다시 요청합니다. 나머지는 알아서 처리됩니다.
  socket.emit2('TryRoomInfo');
});

// 광장의 유저들에게 방목록이 바뀌었음을 알리는 피드백 메시지
socket.on('FeedRefreshRoomList', (data) => {
  // 로그인하지 않은 유저는 제외합니다.
  if($("#NickName").val()=="")return;
  // 방의 목록을 달라고 서버측에 요청하는 TryRoomList 메시지를 보냅니다.
  socket.emit2('TryRoomList');
});    

// 채팅 전송 버튼 클릭시 호출함수
$("#btnChat").click(function(){
  // 특정 방에 입장한 경우 외에는 채팅을 제한합니다.
  if(roomname=="" || roomname=="world"){
    alert("방 안에서만 채팅 가능합니다.");
    return;
  }
  // 여기까지 온 경우 채팅방 안에 있는 것이며,
  // 서버에 채팅메시지를 전송합니다.
  socket.emit2('TryChat', {
    msg: $('#msg').val()
  });
  // 채팅을 전송하고 나서 채팅 입력 상자를 깨끗이 비웁니다.
  $("#msg").val("");
});

// 채팅메시지 피드백
socket.on('FeedChat', (data) => { 
  // 룸 정보 요청
  socket.emit2('TryRoomInfo');
});