본문 바로가기

[Network] Airline 서버 구현 (2) : 서버 개발하기

[Network] Airline 서버 구현 (2) : 서버 개발하기

 

*  이전 글 : StatesAirline Server API document
*  Airline Server 개발하기
-  Airport API
-  Book API
-  Flight API

 

초기세팅

 

폴더 구조

 

1. statesairline

  • statesairline/app.js 는 서비스에 필요한 미들웨어와 웹 서버를 실행하는 코드가 작성되어 있다.

2. router

  • statesairline/router/ 안에는 airport API, book API, flight API 요청을 수행하는 라우터가 작성되어 있다. 작성된 라우터 내용을 통해 API 요청을 받을 수 있다.

3. controller

  • statesairline/controller/ 안에는 정의된 API 요청을 수행하는 코드가 작성되어 있다. 

4. repository

  • statesairline/repository/flightlist.js 는 서비스에서 제공하는 항공편 데이터가 작성되어 있다. (이 데이터로 서비스를 구현)
  • statesairline/repository/airportlist.js 는 서비스에서 제공하는 공항 데이터가 작성되어 있다.

5. test

  • statesairline/__test__/statesairline.test.js 은 Jest 스펙을 가지고 있으며 코드 테스트를 위한 테스트 케이스가 작성되어 있다.

 

 

 

 

 

 

 

초기 세팅 

더보기
  • npm install 을 통해서 package.json에 설정된 패키지를 설치한다.
  • npm start를 통해 서버를 실행한다.
  • Postman으로 API 동작 여부를 확인한다.
  • npm test를 통해 테스트를 실행한다.
  • 서버에서 사용되는 포트 번호로 실행되지 않는 경우, 관리자 권한을 부여하여 실행하거나 해당 포트를 사용 중인 프로그램을 종료 후 재시작을 한다.

 

미리 알아야 할 것

req.query와 req.params

  • Query와 Params를 기준으로 데이터를 필터링한다.

 


 

1. statesairline

 

app.js
// 서비스에 필요한 미들웨어와 웹 서버를 실행하는 코드
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 서버는 요청을 받을수 있는 포트 번호를 필요로 한다.
// HTTP server의 표준 포트는 보통 80 번 이지만, 보통 다른 서버에서 사용중이기 때문에 접근할 수 없다.
// 따라서 우리는 테스트 서버 포트로 3000, 8080, 1337 등을 활용한다.
// PORT는 아파트의 호수와도 같다. 서버로 요청을 받기 위해서는 다음과 같이 포트 번호를 설정 한다.
// (* 때에 따라 다른 포트번호를 열고 싶다면, 환경 변수를 활용 하기도 한다.)
// 환경변수 : 프로세스가 컴퓨터에서 동작하는 방식에 영향을 미치는 동적인 값들의 모임
const port = 3001;
const flightRouter = require('./router/flightRouter'); // router require
const bookRouter = require('./router/bookRouter');
const airportRouter = require('./router/airportRouter');

// cors 헤더를 모든 응답에 넣어준다.
app.use(cors());
// 모든 응답은 json으로 한다. Content-Type: application/json
app.use(express.json());

app.use('/flight', flightRouter);     // 비행 일정 조회는 /flight
app.use('/book', bookRouter);         // 항공권 예약는 /book
app.use('/airport', airportRouter);   // 공항 정보 조회는 /airport 

app.get('/', (req, res) => {
  res.status(200).send('Welcome, States Airline!');
});

app.use((req, res, next) => {          // 🟣 (설명1)
  res.status(404).send('Not Found!');
});

app.use((err, req, res, next) => {     // 🟣 (설명2)
  console.error(err.stack);
  res.status(500).send({
    message: 'Internal Server Error',
    stacktrace: err.toString()        // 🟣 (설명2)
  });
});

app.listen(port, () => {
  console.log(`[RUN] StatesAirline Server... | http://localhost:${port}`);
});

module.exports = app;

 

🟣 (설명 1), (설명 2)

Express.js 프레임워크에서 에러 핸들링과 예외 처리를 위한 미들웨어를 등록하는 방법 

(설명 1) 첫 번째 미들웨어 함수는 404 Not Found 오류를 처리한다. 이 미들웨어는 모든 요청에 대해 호출되며, 라우터 로직에서 해당 요청에 대한 핸들러를 찾지 못한 경우 실행된다. 이 경우, 클라이언트에게 "Not found!" 메시지와 함께 404 코드를 반환한다.

 

(설명 2) 두 번째 미들웨어 함수는 500 Internal Server Error 오류를 처리한다. 이 미들웨어는 다른 미들웨어나 라우터 로직에서 예외가 발생했을 때 실행된다. 이 경우 서버 내부 오류를 나타내는 500 상태 코드를 반환하고, 서버 로그에 에러 스택 트레이스를 기록한다.

 

-> 클라이언트에게 적절한 오류 메시지와 함께 적절한 HTTP 코드를 반환하여 웹 애플리케이션의 안정성과 신뢰성을 높이는 역할

 

🟣 (설명 3) : 500 Internal Server Error

 

하이퍼텍스트 전송 프로토콜 (HTTP) 500 Internal Server Error 서버 에러 응답 코드는 요청을 처리하는 과정에서 서버가 예상하지 못한 상황에 놓였다는 것을 나타낸다.

  • HTTP 상태 코드 500은 다른 오류 코드가 적합하지 않을 때 서버에서 반환되는 일반적인 오류 응답이다.
  • 서버 언어의 구문 에러(스크립트 문법 오류), 서버 통신의 Timeout 시간 지연 오류, 서버 트래픽 과부하 등의 이유가 있다.
  • 아래의 경우, POST 요청의 body 부분의 SyntaxError가 이유이다. ( 4라인 마지막에 , 를 지워야함) 

stacktrace: err일 때,

stacktrace: err.toString()일 때,


 

2. router

 

1) airport router & airport controller

airport router
const { findAll } = require('../controller/airportController');
const express = require('express');
const router = express.Router();       // **

router.get('/', findAll);              // airportController의 findAll 기능을 / 주소로 연결 

module.exports = router;               // **
airport controller
const airports = require('../repository/airportList');   // 공항 정보 데이터

module.exports = {
  // [GET] /airport?query={query} 요청을 수행
  // 공항 이름 자동완성 기능을 수행
  findAll: (req, res) => {
    if (req.query.query) { // 🟣 (설명1)
      // console.log(req.query.query);
      const filteredAirports = airports.filter((airport) => {
        return airport.code.includes(req.query.query.toUpperCase());
      });
      return res.status(200).json(filteredAirports);
    }
    res.json(airports);      
  }
};

🟣 (설명 1) req.query.query?

http://localhost:3001/airport?query=a 일 때

1) req

request 콘솔에 찍어봄. query 파라미터가 보인다.

2) req.query와 req.query.query

req.query.query 와 req.query

4) postman에서의 res

Postman으로 잘 받아와진다. 

airportList (데이터... 중간 생략)
module.exports = [
  {
    name: '제주',
    code: 'CJU'
  },
  
.
.
(생략)
.

];

 


 

2) book router & book controller

bookRouter.js
const { findAll, findByPhone, findByPhoneAndFlightId, create, deleteByBookingId } = require('../controller/bookController');
const express = require('express');
const router = express.Router();

router.get('/', findAll);

router.get('/:phone', findByPhone);

router.get('/:phone/:flight_uuid', findByPhoneAndFlightId);

router.post('/', create);

router.delete('/:booking_uuid', deleteByBookingId);

module.exports = router;
bookController.js
// POST /book에서 사용할 uuid 
const { v4: uuid, validate } = require('uuid');
// 항공편 예약 데이터를 저장
let booking = [];

const isKoreanPhoneNumber = (str) => {
  return /^01([0|1|6|7|8|9]?)-?([0-9]{3,4})-?([0-9]{4})$/.test(str);
};

module.exports = {

  // 🟡 [GET] /book 요청을 수행 
  // 전체 예약 데이터를 조회 
  findAll: (req, res) => {
    return res.status(200).json(booking);
  },
  
  // 🟡 [GET] /book/:phone 요청을 수행 
  // 요청 된 phone과 동일한 phone 예약 데이터를 조회 
  findByPhone: (req, res) => {
    const {phone} = req.params;
    if (!isKoreanPhoneNumber(phone)) return res.status(400).json('Bad request');
    const filteredPhone = booking.filter((info)=> info.phone === phone )
    if (filteredPhone.length === 0) return res.status(404).json('Not found');
    return res.status(200).json(filteredPhone)
  },
  
  // 🟡 [GET] /book/:phone/:flight_uuid 요청 
  // 요청 된 id, phone과 동일한 uuid, phone 예약 데이터를 조회 
  findByPhoneAndFlightId: (req,res) => {
    const {phone, flight_uuid} = req.params;
    if (!isKoreanPhoneNumber(phone) || !validate(flight_uuid)) return res.status(400).json("Bad request")
    const filteredPhoneAndId = booking.filter((info)=> 
    info.phone === phone && info.flight_uuid === flight_uuid );
    if (filteredPhoneAndId.length === 0) return res.status(404).json('Not found');
    return res.status(200).json(filteredPhoneAndId)
  },
  
  // 🟡 [POST] /book 요청 
  // 요청 된 예약 데이터를 저장 
  create: (req, res) => {
    const booking_uuid = uuid();      // POST /book에서 사용할 booking_uuid 
    const {phone, flight_uuid, name} = req.body;
    if (!isKoreanPhoneNumber(phone)) return res.status(400).json('Bad request');
    if (booking.find((book) => book.phone === phone && book.flight_uuid === flight_uuid)) {
      return res.status(409).json("It's Already booked.");
    } else {
      booking.unshift({ booking_uuid, flight_uuid, name, phone });
      res.location(`/book/${booking_uuid}`);       // 🟣 (설명 1) 
      return res.status(201).json(booking[0]);
    }
  },

  // 🟡 [DELETE] /book/:booking_uuid 요청을 수행
  // 요청 된 id, phone 값과 동일한 예약 데이터를 삭제
  deleteByBookingId: (req, res) => {
    const {booking_uuid} = req.params;
    booking = booking.filter((item) => item.booking__uuid !== booking_uuid);
    return res.status(200).json(booking_uuid)    // 🟣 (설명 2) 
  }
};

🟣 (설명 1) res.location(`/book/${booking_uuid}`);

res.location(`/book/${booking_uuid}`); 을 추가했을 때

 

🟣 (설명 2) return res.status(200).json(booking_uuid);

  • 204 No Content : 삭제 요청으로 자원을 삭제하여 더 이상 존재하지 않고 그 자원을 참조하는 모든 자원도 삭제되어 더 이상 HTTP body를 응답하는 것이 무의미해졌을 때 사용한다. 
  • 즉, express에서 200과 body에 null, false 등으로 응답하는 것과 달리 204는 HTTP Response body가 아예 존재하지 않는다.
  • 그러나 API 문서에 미리 작성해 놓은 것처럼 응답 메시지에 삭제가 완료된 booking_uuid를 포함하였다.
  • 아래는 GitHub Pages site를 삭제할 때의 REST API 예시이다. 

응답 코드는 Status: 204

 


 

3) flight router & flight controller

flightRouter.js
const { findAll, findById, update } = require('../controller/flightController');
const express = require('express');
const router = express.Router();

router.get('/', findAll);

router.get('/:uuid', findById);

router.put('/:uuid', update);

module.exports = router;
flightController.js
(1) findAll - 첫 번째 방법
const flights = require('../repository/flightList');
const fs = require('fs');

module.exports = {
  // 🟡 [GET] /flight
  // 요청 된 파라미터 departure_times, arrival_times 값과 동일한 값을 가진 항공편 데이터를 조회
  // 요청 된 파라미터 departure, destination 값과 동일한 값을 가진 항공편 데이터를 조회
  findAll: (req, res) => {
    const { departure_times, arrival_times, destination, departure } = req.query;
  
    if(departure_time && arrival_times) {
      const filteredTime = flights.filter((item) => {
                            return item.departure_times === departure_times &&
                                    item.arrival_times === arrival_times} )
      return res.status(200).json(filteredTime);
    }
    if(destination && departure) {
      const filteredPlace = flights.filter((item) => {
                            return item.destination === destination &&
                                    item.departure === departure} )
      return res.status(200).json(filteredPlace);
    }
    res.status(200).json(flights);
  },
(2) findAll - 두 번째 방법
 findAll: (req, res) => {
    const {
      departure_times, arrival_times,
      destination, departure} = req.query;

   // 유효한 입력인지를 먼저 확인한다.
    const isValidAirportCode = (code) => {
      // Check if the code is a string of three uppercase letters
      return (
        typeof code === "string" && code.length === 3 && /^[A-Z]{3}$/.test(code)
      );
    };

    const isValidDate = (timestamp) => {   // 🟣 (설명1)
      const date = new Date(timestamp);
      return !isNaN(date.getTime()) && date.toISOString().split('T')[0] === timestamp.split('T')[0];
    };


    if (Object.keys(req.query).length === 0) return res.json(flights);
    else if (isValidDate(departure_times) && isValidDate(arrival_times)) {
      const data = flights.filter(
        (flight) =>
          flight.departure_times === departure_times &&   // 🟣 (설명2) 출발시간만 있을 경우 추가
          flight.arrival_times === arrival_times
      );
      return res.status(200).json(data);
    } else if (isValidAirportCode(departure) && isValidAirportCode(destination)) {
      const data = flights.filter(
        (flight) =>
          flight.departure === departure && flight.destination === destination
      );
      return res.status(200).json(data);
    } else {
      return res.status(400).json("Incorrect request");
    }
  },

🟣 (설명 1) isValidDate() 함수

  • isValidDate() 함수는 입력받은 timestamp를 Date 객체로 변환한 다음, 날짜 형식이 유효한지 확인한다. 따라서 입력받은 timestamp가 Date 객체로 정확하게 변환될 수 있는 형식이어야 한다.
  • 입력한 departure_times 값을 보면 '2023-04-09T10:00:00.000Z'로, ISO 8601 형식을 따르고 있다. 이 형식은 유효한 날짜 및 시간 값을 나타내기 위한 국제 표준 형식 중 하나이며, 대부분의 프로그래밍 언어에서 지원한다. 그러므로 이 형식에 맞춰서 값을 입력하면 isValidDate() 함수에서는 ture를 반환할 것이다.
  • 다만, 입력한 값이 직접 생성한 문자열이라면, 올바른 날짜 값을 가지고 있지 않을 가능성이 있다. 이 경우, 입력된 값을 직접 확인하거나, 라이브러리를 이용하여 날짜를 생성하는 것이 좋다. 예를 들어, moment.js와 같은 라이브러리를 이용하면, 다양한 날짜 형식을 쉽게 다룰 수 있다.

 

🟣 (설명 2) 출발 시간만 입력할 경우

-> 실제 비슷한 코드를 짜게 된다면 조금 더 나은 사용자 경험을 위해서, 비행기 출발 시간만 입력해도 도착시간이 다양한 비행기편이 나오도록 하는 것이 좋을 것 같다.
else if (isValidDate(departure_times)) {
   if(isValidDate(arrival_times) && arrival_times)
     const data = flights.filter((flight) => 
     flight.departure_times === departure_times && flight.arrival_times === arrival_times);
     return res.status(200).json(data);
    } else {
     const data = flights.filter((flight) => 
     flight.departure_times === departure_times);
     return res.status(200).json(data);
    }

 

 // 🟡 [GET] /flight/:uuid
  // 요청 된 uuid 값과 동일한 uuid 값을 가진 항공편 데이터를 조회
  findById: (req, res) => {
    const { uuid } = req.params;
    if( uuid ) {
      const filteredID = flights.filter((item) => item.uuid === uuid) // 데이터가 없으면 []
    // const filteredID = flights.find((item) => item.uuid === uuid) 이렇게도 쓸 수 있다. 데이터가 없으면 undefined
      return res.status(200).json(filteredID);
    }
  },

  // 🟡 [PUT] /flight/:uuid 요청을 수행
  // 요청 된 uuid 값과 동일한 uuid 값을 가진 항공편 데이터를 요쳥된 Body 데이터로 수정
  update: (req, res) => {
    const { uuid } = req.params;  // 비행기 ID
    const bodyData = req.body; // 업데이트 희망하는 객체 

    if( uuid ) {
      flightFiltered = flights.filter((flight) => flight.uuid === uuid) // 해당하는 비행편이 나왔음
      let updateList = {}
      // 🟣 (1) 필요하면 어느 부분이 업데이트 되었는지도 알려주기 위한 코드
      for(let key in bodyData) {
        if(bodyData[key] !== flightFiltered[0][key]) {    // 요청 중 기존 데이터와 다른 부분이 있다면
          updateList[key] = bodyData[key]                 // 업데이트에 추가
          flightFiltered[0][key] = bodyData[key]          // 갱신된 정보로 덮어쓴다.
        }
      }
      
      // 🟣 (2) 업데이트가 된 부분을 알려줄 필요가 없다면 아래의 코드로도 충분하다. 
      // flightFiltered[0] = {...flightFiltered[0], ...bodyData} 또는 Object.assign(flightFiltered[0], bodyData)
      return res.status(202).json(flightFiltered[0]);
      
      // 🟣 (3) 또는 이렇게도 가능하다.
      // const beUpdatedIdx = flights.findIndex((flight) => flight.uuid === uuid);
      // const updatedFlight = { ...flights[beUpdatedIdx], ...bodyData }; //최종적으로 업데이트된 flight
      // flights.splice(beUpdatedIdx, 1, updatedFlight); // 덮어쓰기 
      // return res.status(200).json(updatedFlight); 
    }
      
  }
};
728x90
⬆︎