Jieunny์˜ ๋ธ”๋กœ๊ทธ

S2) Unit 11. [์‹ค์Šต] Coz’ Mini Hackathon (์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„) ๋ณธ๋ฌธ

CodeStates/Training

S2) Unit 11. [์‹ค์Šต] Coz’ Mini Hackathon (์•„๊ณ ๋ผ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„)

Jieunny 2023. 2. 9. 16:52

๐Ÿ“ฃ ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

Bare Minimum Requirement Self Checklist

1๏ธโƒฃ my-agora-states-server
โœ”๏ธ my-agora-states-server/app.js
โœ… ๋ชจ๋“  Origin, ๊ฒฝ๋กœ์— ๋Œ€ํ•ด CORS ์š”์ฒญ์„ ํ—ˆ์šฉํ•˜๊ฒŒ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
โœ… POST ์š”์ฒญ ๋“ฑ์— ํฌํ•จ๋œ body(payload)๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
โœ… ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ์„ ์œ„ํ•ด / ์—์„œ ์ƒํƒœ ์ฝ”๋“œ 200์œผ๋กœ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.
โœ… discussionRouter ๋ฅผ ์ด์šฉํ•˜์—ฌ /discussions ๊ฒฝ๋กœ๋กœ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค.
โœ”๏ธ my-agora-states-server/router/discussions.js
โœ… GET /discussions
โœ… GET /discussions/:id
โœ”๏ธ my-agora-states-server/controller/index.js
โœ… discussionsController.findAll
โœ… discussionsController.findById

2๏ธโƒฃ my-agora-states-server ๊ณผ์ œ ์ œ์ถœ(Pull request)
โœ”๏ธ Pull request๋กœ ๊ณผ์ œ ์ œ์ถœ

3๏ธโƒฃ my-agora-states-server ์‹œ์ž‘
โœ”๏ธ package.json ์„ ์ฐธ๊ณ ํ•˜์—ฌ ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„๋ฅผ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

4๏ธโƒฃ my-agora-states ์™€ ์—ฐ๋™ํ•˜๊ธฐ
โœ”๏ธ my-agora-states-server๊ฐ€ ์ผœ์ ธ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
โœ”๏ธ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ํ•œ ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„์—์„œ discussions ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

Optional Checklist

1๏ธโƒฃ my-agora-states-in-react
โœ”๏ธ create-react-app์œผ๋กœ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
โœ”๏ธ ๊ธฐ์กด์— ๋งŒ๋“  ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ ๋ฅผ React๋กœ ์˜ฎ๊ธฐ๊ธฐ
โœ… ๋””์Šค์ปค์…˜ ๋‚˜์—ด ๊ธฐ๋Šฅ
โœ”๏ธ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ํ•œ ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„์—์„œ discussions ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“ฃ ์„œ๋ฒ„ ๊ตฌํ˜„

โœ”๏ธ my-agora-states-server

1๏ธโƒฃ controller/index.js

const { agoraStatesDiscussions } = require("../repository/discussions");
const discussionsData = agoraStatesDiscussions;

const discussionsController = {
  findAll: (req, res) => {
    // TODO: ๋ชจ๋“  discussions ๋ชฉ๋ก์„ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.
    return res.status(200).json(discussionsData);
  },

  findById: (req, res) => {
    // TODO: ์š”์ฒญ์œผ๋กœ ๋“ค์–ด์˜จ id์™€ ์ผ์น˜ํ•˜๋Š” discussion์„ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.
    const { id } = req.params;
    if(id){
      const filteredList = discussionsData.filter((discussion) => {
        return discussion.id === Number(id);
        // id๋ฅผ number๋กœ ๋ฐ”๊ฟ”์ค˜์•ผ ๋น„๊ต ๊ฐ€๋Šฅํ•˜๋‹ค.
      })
      if(filteredList.length === 0) return res.status(404).send('Not found');
      else return res.status(200).json(filteredList[0]);
    }
  }

};

module.exports = {
  discussionsController,
};

2๏ธโƒฃ router/discussions.js

// TODO: discussions ๋ผ์šฐํ„ฐ๋ฅผ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค.
const { discussionsController } = require('../controller');
const { findAll, findById } = discussionsController;
const express = require('express');
const router = express.Router();

// TODO: ๋ชจ๋“  discussions ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
router.get('/', findAll);

// TODO: :id์— ๋งž๋Š” discussion์„ ์กฐํšŒํ•˜๋Š” ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
router.get('/:id', findById);

module.exports = router;

3๏ธโƒฃ App.js

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

const cors = require('cors');
const morgan = require('morgan');

// morgan ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์„ธํŒ…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
// HTTP ์š”์ฒญ logger๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฏธ๋“ค์›จ์–ด ์ž…๋‹ˆ๋‹ค.
app.use(morgan('tiny'));

// TODO: cors๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
app.use(cors());

// TODO: Express ๋‚ด์žฅ ๋ฏธ๋“ค์›จ์–ด์ธ express.json()์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
app.use(express.json());


const port = 4000;
const discussionsRouter = require('./router/discussions');

// TODO: app.use()๋ฅผ ํ™œ์šฉํ•˜์—ฌ /discussions ๊ฒฝ๋กœ๋กœ ๋ผ์šฐํŒ…ํ•ฉ๋‹ˆ๋‹ค. 
app.use('/discussions', discussionsRouter);


app.get('/', (req, res) => {
  // ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ์„ ์œ„ํ•ด ์ƒํƒœ ์ฝ”๋“œ 200๊ณผ ํ•จ๊ป˜ ์‘๋‹ต์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  res.status(200).send('fe-sprint-my-agora-states-server');
});

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

module.exports.app = app;
module.exports.server = server;

๐Ÿ“ฃ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ  ๋ฆฌ์•กํŠธ ๊ตฌํ˜„

โœ”๏ธ ์ฃผ์š” ๊ธฐ๋Šฅ

โžฐ ์ปดํฌ๋„ŒํŠธ๋กœ ๋‚˜๋ˆ„๊ธฐ
โžฐ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
โžฐ ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ 10๊ฐœ์”ฉ ํŽ˜์ด์ง€๋„ค์ด์…˜ ํ•˜๊ธฐ
โžฐ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ํ•˜๊ธฐ
โžฐ localStroage์— ์ €์žฅํ•˜๊ธฐ (๊ตฌํ˜„ ํ–ˆ๋Š”๋ฐ ์ฒ˜์Œ ์ถ”๊ฐ€ํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ €์žฅ์ด ์•ˆ๋˜๊ณ  ๋‘๋ฒˆ์งธ ๋ฐ์ดํ„ฐ๋ถ€ํ„ฐ ์ €์žฅ๋˜๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ)

โœ”๏ธ ํŒŒ์ผ ๊ตฌ์กฐ

๏น’my-agora-states-react
แ„‚ src
แ„‚ components
แ„‚ Content.js
แ„‚ Discussions.js
แ„‚ DiscussionsContainer.js
แ„‚ Form.js
แ„‚ Main.js
แ„‚ Menubar.js
แ„‚ Paging.js
แ„‚ Tab.js
แ„‚ styles
แ„‚ images
แ„‚ App.js
แ„‚ App.css

โœ”๏ธ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

1๏ธโƒฃ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ -> Contents.js

import Form from '../components/Form';
import '../styles/Contents.css';
import { useState, useEffect, useRef } from 'react';
import DiscussionsContainer from '../components/DiscussionsContainer';

function Contents() {
  const [storageData, setStorageData] = useState([]);
  
  useEffect(() => {
  // ์ฒ˜์Œ ๋ Œ๋”๋ง ํ•  ๋•Œ๋งŒ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ํ•˜๊ธฐ
    fetch('http://localhost:4000/discussions')  // ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ ๋ฐ›์•„์˜ค๊ธฐ
      .then(res => res.json())
      .then(json => { 
        if(localStorage.length===0){
          localStorage.setItem('Discussions', JSON.stringify(json));
          setStorageData(JSON.parse(localStorage.getItem('Discussions')));
        }
        else{
          setStorageData(JSON.parse(localStorage.getItem('Discussions')));
        }
      }); 

  }, []);

  //console.log(storageData);

  return (
    <div className="Contents">
      <Form discussions={storageData} setDiscussions={setStorageData}/>
      <DiscussionsContainer discussions={storageData}/>
    </div>
  );
}

export default Contents;

โžฐ ๋‚˜๋งŒ์˜ ์•„๊ณ ๋ผ ์Šคํ…Œ์ด์ธ  ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜จ ๋‹ค์Œ, ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ storageData์— ๋‹ด์•„์„œ Form๊ณผ DiscussionsContainer ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•ด์ค€๋‹ค.

2๏ธโƒฃ ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ 10๊ฐœ์”ฉ ํŽ˜์ด์ง€๋„ค์ด์…˜ ํ•˜๊ธฐ -> Paging.js / Discussions.js

๏น’ Paging.js

import React, { useState } from 'react';
import '../styles/Paging.css';
import Discussions from '../components/Discussions';
import Pagination from "react-js-pagination";

function Paging({ discussions }) {
  const [page, setPage] = useState(1);
  //console.log(discussions);
  let discussionsCount = discussions.length;
  let discussionPerPage = 10;
  let totalPage = discussionsCount/discussionPerPage;

  let dataPerPage = [];

  for(let i=0; i<totalPage; i++){ //10๊ฐœ์”ฉ ๋‚˜๋ˆ ์„œ ๋ฐฐ์—ด์— ๋„ฃ๊ธฐ
    let temp = [];
    for(let j=0; j<10; j++){
      if(i*10 + j < discussionsCount) temp.push(discussions[i*10 + j])
    }
    dataPerPage.push(temp);
  }
  //console.log(discussions[0]);

  const handlePageChange = (page) => {
    setPage(page);
  };

  let pageData = dataPerPage[page-1];
  // ๊ทธ ํŽ˜์ด์ง€์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ 10๊ฐœ๋ฅผ Discussions ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณด๋‚ด์ค€๋‹ค.

  return (
    <div className='Paging'>
      <Pagination
        activePage={page}
        itemsCountPerPage={discussionPerPage}
        totalItemsCount={discussionsCount}
        pageRangeDisplayed={totalPage}
        prevPageText={"<"}
        nextPageText={">"}
        onChange={handlePageChange}
      />
      <Discussions currentPageData={pageData}/>
    </div>
  );
}
export default Paging;

โžฐ ๋ฆฌ์•กํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ react-js-pagination ์„ ์‚ฌ์šฉํ–ˆ๋‹ค.
โžฐ props๋กœ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฐ์—ด์— 10๊ฐœ์”ฉ ๋‚˜๋ˆ ์„œ ๋„ฃ์–ด์ค€๋‹ค(1๋ฒˆ์งธ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋Š” ๋ฐฐ์—ด์˜ [0]๋ฒˆ์งธ์—, 2๋ฒˆ์งธ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋Š” ๋ฐฐ์—ด์˜ [1]๋ฒˆ์งธ ์ด๋Ÿฐ์‹์œผ๋กœ ๋“ค์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค)
โžฐ page ๋ณ€์ˆ˜์—๋Š” ํ˜„์žฌ ๋‚ด๊ฐ€ ๋ˆ„๋ฅธ ํŽ˜์ด์ง€์˜ ๊ฐ’์ด ๋‹ด๊ฒจ ์žˆ์œผ๋ฏ€๋กœ, ๊ทธ ๊ฐ’์˜ -1์— ํ•ด๋‹นํ•˜๋Š” ์ธ๋ฑ์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ Discussions ์ปดํฌ๋„ŒํŠธ์— ๋ณด๋‚ด์ค€๋‹ค.
โžฐ Pagination ์ปดํฌ๋„ŒํŠธ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒƒ์œผ๋กœ, ํŽ˜์ด์ง€ ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™”๋ฉด์— ๋„์›Œ์ค€๋‹ค.

๏น’ Discussions.js

function Discussions({ currentPageData }) {
  //์Šคํ…Œ์ดํŠธ๋กœ ๋ฐ›์€ ํŽ˜์ด์ง€ ๋ณด์—ฌ์ฃผ๊ธฐ.
  //๋ฐ์ดํ„ฐ๋ฅผ ํŽ˜์ด์ง€์— ๋„ฃ์„ ๋งŒํผ๋งŒ ๋ฐ›์•„์˜ค๊ธฐ.
  //console.log(currentPageData)

  return (
    <div className="Discussions">
      <ul className="discussions__container">
        {currentPageData && currentPageData.map((data) => {
          return(
          <li className="discussion__container" key={data.id}>
            <div className="discussion__avatar--wrapper"><img className="discussion__avatar--image" alt="avatarImg" src={data.avatarUrl}/></div>
            <div className="discussion__content">
              <h2 className="discussion__title"><a href={data.url}>{data.title}</a></h2>
              <div className="discussion__information">{data.author + '/' + new Date(data.createdAt).toLocaleString()}</div>
            </div>
            {data.answer !== null ? <div className="answered__check"><a href={data.answer.url}>๐Ÿ’ฌ</a></div> : <div className="answered__check"></div>}     
          </li>
          )
          })
        }
      </ul>
    </div>
  );
}

export default Discussions;

โžฐ Discussions ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” 10๊ฐœ์”ฉ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ์ด๋ฆ„, ๋‚ ์งœ ๋“ฑ๋“ฑ์„ ๋ณด์—ฌ์ค€๋‹ค.

3๏ธโƒฃ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ํ•˜๊ธฐ -> Form.js

import '../styles/Form.css';
import Menubar from '../components/Menubar';
import { useEffect } from 'react';

function Form({discussions, setDiscussions}) {
  function inputDiscussion(){
    let nameValue = document.querySelector('#name').value;
    let titleValue = document.querySelector('#title').value;
    let storyValue = document.querySelector('#story').value;
    let currentTime = new Date();

    const newDiscussion = {
      id: discussions.length + 10,
      createdAt: currentTime,
      author: nameValue,
      title: titleValue,
      body: storyValue,
      url: "https://github.com/codestates-seb/agora-states-fe/discussions/45",
      avatarUrl: "https://avatars.githubusercontent.com/u/79903256?s=64&v=4",
      answer: null  // ์•ˆ ๋„ฃ์–ด์ฃผ๋ฉด answer null ์•„๋‹Œ๊ฑฐ if๋ฌธ์— ๊ฑธ๋ ค์„œ ์˜ค๋ฅ˜๋‚œ๋‹ค.
    };
    setDiscussions([newDiscussion, ...discussions]);
    // ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ์›๋ž˜ ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
    localStorage.setItem('Discussions', JSON.stringify(discussions));
  }

  return (

      <div className="Form">
        
        <Menubar text={'Your Question'}/>
        <form action="" method="get" className="form">
            <div className="form__text">
              <p>ENTER HERE</p>
            </div>
            <div className="form__input--wrapper">

              <div className="form__input--name">
                <label htmlFor="name"></label>
                <input type="text" placeholder="name" name="name" id="name" autoComplete="off" required></input>
              </div>

              <div className="form__input--title">
                <label htmlFor="name"></label>
                <input type="text" placeholder="title" name="title" id="title" autoComplete="off" required></input>
              </div>

              <div className="form__textbox">
                <label htmlFor="story"></label>
                <textarea id="story" name="story" placeholder="Write your Question" autoComplete="off" required></textarea>
              </div>

              <div className="form__submit">
                <input onClick={inputDiscussion} className="inputBtn" type="button" value="Question"></input>
              </div>
          </div>
        </form>
      </div>

  );
}

export default Form;

โžฐ spread ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•ด์„œ, ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ์›๋ž˜ ๋ฐ์ดํ„ฐ์— ๋„ฃ์–ด์ค€๋‹ค.

4๏ธโƒฃ localStorage ์— ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐ -> Content.js, Form.js

โžฐ Content.js ์—์„œ ์ฒ˜์Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ๋Š”๋ฐ ๋งŒ์•ฝ localStorage์— ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฑธ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์•„๋‹ˆ๋ฉด, ๋ถˆ๋Ÿฌ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ localStroage์— ์ €์žฅํ•œ๋‹ค.
โžฐ Form.js ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•˜๊ณ , ๊ทธ ๋ฐฐ์—ด์„ ๋‹ค์‹œ localStroage์˜ ์›๋ž˜ ๋ฐ์ดํ„ฐ์— ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.
โžฐ ์ถ”๊ฐ€๊ฐ€ ๋˜๊ธด ํ•˜๋Š”๋ฐ, ์ฒซ ๋ฒˆ์งธ ์ถ”๊ฐ€ํ•œ ๋ฐ์ดํ„ฐ๋Š” ๋ฌด์‹œ๋˜๊ณ  ๋‘ ๋ฒˆ์งธ ์ถ”๊ฐ€ํ•œ ๋ฐ์ดํ„ฐ๋ถ€ํ„ฐ ์ •์ƒ์ ์œผ๋กœ ์ถ”๊ฐ€๋˜๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
โžฐ ์•„์ง ํ•ด๊ฒฐ์„ ํ•˜์ง€ ๋ชปํ–ˆ๋‹ค..๋ญ”๊ฐ€ ๋ฆฌ์•กํŠธ ๋ Œ๋”๋ง ๊ณผ์ •์—์„œ ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ ๊ฐ™์€๋ฐใ… ใ… ใ… ใ… 


๐Ÿ“ฃ ์‹œ์—ฐ ํ™”๋ฉด

'CodeStates > Training' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

S3) Unit 1. [์‹ค์Šต] Tree UI  (0) 2023.02.14
S3) Unit 1. [์‹ค์Šต] JSON.stringify  (0) 2023.02.14
S2) Unit 10. [์‹ค์Šต] StatesAirline Server  (0) 2023.02.08
S2) Unit 10. [์‹ค์Šต] Mini Node Server  (0) 2023.02.06
S2) Unit 9. [์‹ค์Šต] StateAirline Client  (0) 2023.02.03