Fairy ' s

[5. Apr] 1st project 본문

Devops Bootcamp

[5. Apr] 1st project

berafairy 2023. 4. 5. 10:48
Start

 

 먼저 요구사항에 맞는 ERD 제작과 API 문서 작성으로 시작되었다.

 

 

요구사항

 

  • [✔] 사용자는 모든 상품을 조회할 수 있다.
  • [✔] 사용자는 특정 분류의 상품을 조회할 수 있다. (상품분류, 브랜드명, 가격, 상품명)
  • [✔] 사용자의 타입이 판매자인 경우 자신의 상품을 등록할 수 있다.
  • [✔] 사용자는 상품을 장바구니에 담을 수 있다.
  • [✔] 사용자는 자신의 장바구니를 조회할 수 있다.
  • [✔] 사용자는 자신의 장바구니에 있는 상품의 수량을 변경시킬 수 있다.
  • [✔] 사용자는 상품을 자신의 장바구니에서 제외할 수 있다.

TABLE

Shopping Mall Entity Relationship Diagram

 

필요한 테이블은 users, items, cart 세 가지 테이블입니다.

  • users 테이블은 프라이머리 키로 지정한 고유한 user_id 그리고
    username과 유저가 판매자인지 여부를 나타내는 is_seller를 불린 값으로 나타내는 정보들을 저장한다.
  • items 테이블은 프라이머리 키로 지정한 고유한 item_id와 category, brand, price, item_name을 포함하여 
    쇼핑몰의 상품 항목에 필요한 정보를 저장한다.
  • cart 테이블은 사용자의 장바구니를 나타내는 테이블이고 cart_id는 각 카트의 고유 식별자이고
    user_id와 item_id 필드는 users 테이블과 items 테이블의 해당 필드를 참조하는 외래 키이다.
    item_cnt는 사용자 장바구니에 있는 각 항목의 수량을 저장한다.

여기서 스키마에는 두 개의 외래 키 제약 조건이 포함됩니다. 

  • 첫 번째 제약 조건은 cart 테이블의 user_id 필드가 users 테이블의 user_id 필드를 참조하는 외래 키임을 지정했었는데, 
    이런 제약 조건을 통해 각 카트가 특정 사용자와 연결되도록 해서 아무나 특정 사용자의 장바구니에 접근 할 수 없도록 지정한다.

    두 번째 제약 조건은 방금 설명했던 첫 번째 제약조건과 같은 패턴이다.
    cart 테이블의 item_id 필드가 items 테이블의 item_id 필드를 참조하는 외래 키임을 지정한다.
    이 제약 조건을 통해 카트의 각 항목이 상품의 유효한 항목에 해당하도록 한다.

API 명세서

>> Swagger Link << 

/items : 상품
/cart : 장바구니

  • /items (GET)  : 상품은 전체 조회와, 항목 별로 선택 조회를 할 수 있다.
  •  /items (POST) : 사용자의 판매자 여부에 따른 상품 업로드가 가능해야하기 때문에 리퀘스트 헤더 부분에
    authorization에 토큰으로 판매자 여부를 구별하여 판매자가 아닌 경우 상품 업로드를 할 수 없다.
  • /cart?user_id=${user_id} (GET) : user_id에 해당하는 유저의 장바구니를 조회할 수 있다.
  • /cart?user_id=${user_id} (POST) : user_id에 해당하는 유저의 장바구니에 특정 상품을 원하는 개수만큼 추가할 수 있다.
  • /cart?user_id=${user_id} (PUT) : user_id에 해당하는 유저의 장바구니에 있는 상품의 개수를 수정할 수 있다.
  • /cart?user_id=${user_id} (DELETE) : user_id에 해당하는 유저의 장바구니의 상품들을 전부 삭제한다.

Implimentation
SQL 문


Table 생성

CREATE TABLE public.users (
	user_id integer NOT NULL,
	username  varchar NOT NULL,
	is_seller boolean NOT NULL,
	CONSTRAINT users_pk PRIMARY KEY (user_id)
);

CREATE TABLE public.items (
	item_id integer NOT NULL,
	category  varchar NOT NULL,
	brand varchar NOT NULL,
	price integer NOT NULL,
	item_name varchar NOT NULL,
	CONSTRAINT items_id_pk PRIMARY KEY (item_id)
);

CREATE TABLE public.cart (
	cart_id varchar NULL,
	user_id integer NOT NULL,
	item_id integer NOT NULL,
	item_cnt integer NOT NULL,
	CONSTRAINT cart_pk PRIMARY KEY (cart_id)
);

Table 조회

SELECT * FROM cart;

 


Table에 항목 추가

INSERT INTO cart (cart_id, user_id, item_id, item_cnt)
VALUES ('${request.body.cart_id}', '${request.body.user_id}', '${request.body.item_id}','${request.body.item_cnt}'

INSERT INTO items (item_id, category, brand, price, item_name)
VALUES ('${request.body.item_id}','${request.body.category}','${request.body.brand}', '${request.body.price}', '${request.body.item_name}'

Table 항목 수정

UPDATE public.cart SET item_cnt=${item_cnt} 
WHERE cart_id='${cart_id}' and item_id=${item_id}

Table 항목 삭제

DELETE FROM public.cart WHERE user_id = ${userId};

GET Items 

 

내가 구현했던 get items를 칼럼 별로 검색 하는 파트에서 검색 기능을 어떤 식으로 구현 해야할지 고민이 많았다.

fastify를 잘 이해하지 못하고 사용할 줄 몰랐던 초기에는,
검색해서 받아오는 get의 엔드포인트와 전체 조회 get 의 엔드포인트를 ' / ' 로 똑같이 했었는데,
방법을 찾아보니 그렇게 하고 sql 문을 바꾸어도 선택 조회를 하는 것은 쉽지 않았다.

fastify.get('/', async function(request, reply) {
    fastify.pg.connect()
    const client = await fastify.pg.connect();
    try {
      const { rows } = await client.query(
        'SELECT item_id FROM items'
      )
      reply.code(200).send(rows)
    } finally {
      client.release()
    }
  })
}

위 코드가 초기에 items 테이블에서 item_id를 선택해서 가져오려고 작성했던 코드였다.
하지만 두 경로가 동일한 엔드포인트 "/"를 사용하고 Fastify의 두 번째 경로가 첫 번째 경로를 덮어쓰므로 작성된 코드는 다른 쿼리를 사용하여도 제대로 작동하지 않는다.
두 쿼리를 모두 사용할 수 있게 하려면 각 경로에 대해 서로 다른 엔드포인트를 사용해야 했다.

fastify.get('/:column/:value', async function(request, reply) {
    const column = request.params.column;
    const value = request.params.value;
    const client = await fastify.pg.connect();
    try {
      const { rows } = await client.query(
        `SELECT * FROM items WHERE ${column} = $1`, [value]);

      const tableRows = rows.map(row => `
      ...
      `).join('');

      const html = `
      ...
      `;

      reply.header('Content-Type', 'text/html').send(html);
    } finally {
      client.release()
    }
  })

그렇게 바뀐 코드이다.
URL 엔드포인트에 /:column/:value 를 배치하여 column에 원하는 칼럼과 value에 검색하고 싶은 키워드를
입력해서 조회하면 선택한 칼럼의 키워드에 해당하는 내용만 받아올 수 있게 구현하였다.

 

검색 결과

어떤 칼럼을 선택해서 검색해도 정상적으로 검색 기능이 작동한다.


POST Item (Authorization)

 

module.exports = {
    //aaa는 유저1, bbb는 유저2로 가정 
    tokenValidator: (token) => {
        let result;
        if (token === "Bearer aaa") { 
            result = false;
            // userId 1은 DB 에서 구매자 (is_seller = false)
        } else if (token === "Bearer bbb") {
            result = true;
            // userId 2은 DB 에서 판매자 (is_seller = true)
        } else if (token === "Bearer ccc") {
            result = false;
            // userId 3은 DB 에서 구매자 (is_seller = false)
        } 
        return result
    }
}

위 코드로 사용자마다 토큰을 부여해서 판매자 여부를 판단한다.

  fastify.post('/', async function (request, reply) {
    try{ 
      const client = await fastify.pg.connect()
      let check_seller;
      if (check_seller = tokenValidator(request.headers.authorization)) {
        const{ rows1 } = await client.query(
          `SELECT user_id FROM users WHERE is_seller = '${check_seller}'`
        )
        const { rows } = await client.query(
          `INSERT INTO items (item_id, category, brand, price, item_name)
          VALUES ('${request.body.item_id}','${request.body.category}','${request.body.brand}', '${request.body.price}', '${request.body.item_name}')`
        )
        reply.code(201).send('성공적으로 등록되었습니다!');
      }   
      else {
        console.log("Bed Request");
        reply.code(400).send('잘못된 요청 입니다!');
      }
    }
    catch(error) {
      //잘못된 유저 에러
      console.log("판매자만 상품을 등록할 수 있습니다!");
      reply.code(401).send('Un authorized');
    }
  });

check_seller에 tokenValidator 의 boolean 값을 담아서 토큰이 판매자의 토큰이면
성공적으로 등록되었다는 201 상태코드와 메시지를 출력하며 쿼리 문을 통해 아이템을 데이터 베이스에 등록한다.

 

 

상품 등록 시, Authorization이 판매자의 토큰과 일치하는 사용자만 상품을 등록할 수 있다.

왼쪽 사진을 보면 판매자의 토큰을 가지지 않은 사용자가 상품을 등록하였을 때 잘못된 요청이라는 로그를 반환하며
상품이 등록되지 않는다.

오른쪽 사진은 판매자의 토큰을 가진 사용자가 상품을 등록하였을 때 성공적으로 등록되었다는 로그를 반환하며
상품이 정상적으로 등록된다.

등록 결과


Cart


 cart 부분은 items에서 구현했던 것과 다르게 엔드포인트에 쿼리문을 사용하였다.

 

fastify.post('/cart', async function(request, reply) {
    const client = await fastify.pg.connect(); 
    const userId = request.body.user_id;
    const query = `SELECT * FROM public.cart WHERE user_id = ${userId}`;
    const { rows } = await client.query(
        `INSERT INTO cart (cart_id, user_id, item_id, item_cnt)
        VALUES ('${request.body.cart_id}', '${request.body.user_id}', '${request.body.item_id}','${request.body.item_cnt}')`
    )
    const result = await client.query(query);
    return await reply.code(201).send(result.rows);
})

URL /cart?user_id=${user_id} 의 json 구문에 POST 요청으로 user_id에 해당하는 사용자의 장바구니에 상품을 추가하는 방식이다.

 

POST로 body의 json 내용에 장바구니에 추가하고 싶은 아이템의 정보와 cart_id를 입력하면 정상적으로 장바구니에 상품이 추가된다.


fastify.put('/cart', async function(request, reply) {
        const client = await fastify.pg.connect(); 
        const userId = request.body.user_id;
        const query = `SELECT * FROM public.cart WHERE user_id = '${userId}'`;
        const item_id = request.body.item_id;
        const item_cnt = request.body.item_cnt;
        const cart_id = request.body.cart_id;
        const { rows } = await client.query(
            `UPDATE public.cart SET item_cnt=${item_cnt} WHERE cart_id='${cart_id}' and item_id=${item_id}`
        )
        const result = await client.query(query);
	return await reply.code(201).send(result.rows);
})

URL /cart?user_id=${user_id} 의 json 구문에 PUT 요청으로 user_id에 해당하는 사용자의 장바구니에 상품 개수를 수정하는 방식이다.

PUT으로 body에 json 내용의 item_cnt 수정 후 SEND하면 장바구니에 해당 item의 개수가 수정된다.


fastify.delete('/cart', async function(request, reply) {
    const client = await fastify.pg.connect(); 
    const userId = request.body.user_id;
    const query = `SELECT * FROM public.cart WHERE user_id = '${userId}'`;
    const { rows } = await client.query(
        `DELETE FROM public.cart WHERE user_id = ${userId};`
    )
    const result = await client.query(query);
    return await reply.code(201).send(result.rows);
})

URL /cart?user_id=${user_id} 의 json 구문에 DELETE 요청으로 user_id에 해당하는 사용자의 모든 장바구니 내용을 삭제하는 방식이다.


 

구현을 마치고 사전에 작성했던 API 문서와 내용이 달라진 것들을 수정하여 제출하였다.


과제 후기

 

과제 시작부터는 이렇게 많은 시간을 필요할 줄은 예상하지 못했다 ,,,

그래도 부트 캠프에서 배워온 서버 구축 방법을 배웠고, sql 문을 활용해 데이터베이스 테이블을 만들어 json 파일로 데이터를 post하는 방법도 배웠기 때문에 어느정도 수월할 줄 알았다.

그치만 현실은 참담했다 ... get 하나 제대로 만드는 것에 2일차의 반을 투자했다.

더 어려운 토큰을 활용한 post부분 그리고 나머지 put과 delete 부분 또한 정말 get 구현 보다 더욱 많은 시간이 필요했다. 

부트캠프에서 사전에 배운 지식들을 활용해서 해보려했지만, 비슷하게 응용해서 하는 방법으로 진행하면 끊임 없는 이슈가 발생했고.... 정해진 일과에 프로젝트를 끝내는 것은 시간이 터무니없이 부족했다.

너무 어려운 실습과제 프로젝트를 따라가는 것이 너무 어려웠고 머리가 아팠다.

모든 팀원이 프로젝트를 진행하는 이틀동안 아침 9시부터 자정까지 풀로 이 프로젝트만 붙들고 있었고, post와 put 부분을 맡고 있었던 팀원은 2일차 밤을 새서 끝낼 수 있었다.
, , , , 진짜진짜 너무 고생 많으셨습니다 ㅠㅠ, , , , , , ,

과제가 끝난 뒤에도 따로 시간을 투자해서 구현한 내용들의 로직과 코드 이해가 필요할 것 같다.

이후에 진행한 프로젝트에 관한 정리를 블로그에 남길 예정이다.

'Devops Bootcamp' 카테고리의 다른 글

[6. Apr] 발표  (0) 2023.04.06
[6. Apr] TCP/IP 4계층,OSI 7계층 / TIL  (0) 2023.04.06
[30. March] 로그 파이프라인 / TIL #2  (0) 2023.03.30
[30. March] 데이터 파이프라인 / TIL #1  (0) 2023.03.30
[29. March] 발표 / TIL  (0) 2023.03.29
Comments