잡다한 배똥월드

728x90

📝 기본 설명

 

참고 링크 : 2021 Dev-Matching: 웹 프론트엔드 개발자(하반기)' 기출 문제 해설

 

'2021 Dev-Matching: 웹 프론트엔드 개발자(하반기)' 기출 문제 해설

프로그래머스에서는 지난 2021년 9월 4일 '2021 Dev-Matching: 프론트엔드 개발자(하반기)'의 과제 테스트가 진행되었습니다. 과제 리뷰가 제공되지 않지만, 어떻게 하면 구현을 더 잘할 수 있었을까?

prgms.tistory.com

 

 

 

 

 

위의 공식 문제 해설을 기반으로 작성한 코드입니다.

제대로 작동이 되지 않아 개인적으로 수정한 내용도 있으며, 추가한 내용도 있습니다.

 

 

 

 

 

GitHub로 이동하기

 

GitHub - b-sseung/21_DevMatching_2

Contribute to b-sseung/21_DevMatching_2 development by creating an account on GitHub.

github.com

 

 

 

 

 



📌 index.html

<html>
  <head>
    <title>커피캣 스토어</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <main class="App"></main>
    <script src="src/index.js" type="module"></script>
  </body>
</html>

 

 

 

 

 


📌 api.js

const base = 'https://uikt6pohhh.execute-api.ap-northeast-2.amazonaws.com/dev';

export const request = async (url, options = {}) => {
  try {
    const fullUrl = `${base}${url}`
    const res = await fetch(fullUrl, options);

    if (res.ok) {
      return await res.json();
    }

    throw new Error('API 통신 실패');
  } catch (e) {
    new Error(e.message);
  }
}

 

 

 

 

 

📌 App.js

import ProductListPage from './ProductListPage.js';
import ProductDetailPage from './ProductDetailPage.js';
import CartPage from './CartPage.js';
import { init } from './router.js';

export default function App({ $target }) {
  //route() 함수임
  this.route = () => {
    const { pathname } = location;

    //route함수가 호출될 때마다 페이지 리셋
    $target.innerHTML = '';

    //pathname 수정 필요
    if (pathname === '/web/') {
      new ProductListPage({
        $target
      }).render();
    } else if (pathname.indexOf('/web/products/') === 0) {
      const [, , , productId] = pathname.split('/');
      new ProductDetailPage({
        $target,
        productId
      }).render();
    } else if (pathname === '/web/cart') {
      new CartPage({
        $target
      }).render();
    }
  }

  init(this.route);
  this.route();

  window.addEventListener('popstate', this.route);
}

 

 

 

 

 

 📌 Cart.js

import { routeChange } from './router.js';
import { removeItem } from './storage.js';

export default function Cart({ $target, initState }) {
  const $component = document.createElement('div');
  $component.className = 'Cart';
  this.state = initState;

  $target.appendChild($component);

  this.setState = nextState => {
    this.state = nextState;
    this.render();
  }

  this.getTotalPrice = () => {
    return this.state.reduce(
      (acc, option) => acc + ((option.productPrice + option.optionPrice) * option.quantity), 0)
  }

  this.render = () => {
    $component.innerHTML = `
      <ul>
        ${this.state.map(cartItem => `
          <li class="Cart__item">
            <img src="${cartItem.imageUrl}">
            <div class="Cart__itemDescription">
              <div>${cartItem.productName} ${cartItem.optionName} ${cartItem.quantity}개</div>
              <div>${cartItem.productPrice + cartItem.optionPrice}원</div>
            </div>
          </li>  
        `).join('')}
      </ul>
      <div class="Cart__totalPrice">
        총 상품가격 ${this.getTotalPrice()}원
      </div>
      <button class="OrderButton">주문하기</button>    
    `

    return $component;
  }

  this.render();

  $component.addEventListener('click', e => {
      if (e.target.className === 'OrderButton') {
      alert('주문 되었습니다!');
      removeItem('products_cart');
      routeChange('/web/');
    }
  })
}

 

 

 

 

 

📌 CartPage.js

import { request } from './api.js';
import { getItem } from './storage.js';
import { routeChange } from './router.js';
import Cart from './Cart.js';

export default function CartPage({ $target }) {
  const $page = document.createElement('div');
  $page.className = 'CartPage';

  $page.innerHTML = '<h1>장바구니</h1>'

  let cartComponent = null;

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  }

  const cartData = getItem('products_cart', []);
  this.state = {
    products: null
  }

  this.render = () => {
    if (cartData.length === 0) {
      alert('장바구니가 비어있습니다.')
      routeChange('/web/');
    } else {
      $target.appendChild($page);
      if (this.state.products && !cartComponent) {
        cartComponent = new Cart({
          $target: $page,
          initState: this.state.products
        })
      }
    }
  }

  this.fetchProducts = async () => {
    const products = await Promise.all(cartData.map(async (cartItem) => {
      const product = await request(`/products/${cartItem.productId}`);
      const selectedOption = product.productOptions.find(option => option.id === cartItem.optionId);

      return {
        imageUrl: product.imageUrl,
        productName: product.name,
        quantity: cartItem.quantity,
        productPrice: product.price,
        optionName: selectedOption.name,
        optionPrice: selectedOption.price
      }
    }))

    this.setState({ products });
  }

  this.fetchProducts();
}

 

 

 

 

 

📌 index.js

import App from './App.js';

new App({$target: document.querySelector('.App')});

 

 

 

 

 

📌 ProductDetail.js

import SelectedOptions from './SelectedOptions.js';

export default function ProductDetail({ $target, initState }) {
    const $productDetail = document.createElement('div');
    $productDetail.className = 'ProductDetail';

    $target.appendChild($productDetail);

    this.state = initState;
    let selectedOptions = null;
    let isInitialized = false;

    this.setState = nextState => {
        this.state = nextState;
        this.render();

        if (selectedOptions) {
            selectedOptions.setState({
                ...this.state,
                selectedOptions: this.state.selectedOptions
            });
        }
    }

    this.render = () => {
        const { product } = this.state;

        if (!isInitialized) {
            $productDetail.innerHTML = `
                <img src="${product.imageUrl}">
                <div class="ProductDetail__info">
                    <h2>${product.name}</h2>
                    <div class="ProductDetail__price">${product.price}원~</div>
                    <select>
                        <option>선택하세요.</option>
                        ${product.productOptions.map(option => `
                            <option value="${option.id}" ${option.stock === 0 ? 'disable' : ''}>
                                ${option.stock === 0 ? '(품절) ' : ''}${product.name} ${option.name} ${option.price > 0 ? `(+${option.price}원)` : ''}
                            </option>
                        `).join('')}
                    </select>
                    <div class="ProductDetail__selectedOptions"></div>
                </div>
            `
            selectedOptions = new SelectedOptions({
                $target: $productDetail.querySelector('.ProductDetail__selectedOptions'),
                initState: {
                    product: this.state.product,
                    selectedOptions: this.state.selectedOptions
                }
            });
            isInitialized = true;
        }
    }

    this.render();

    $productDetail.addEventListener('change', (e) => {
        if (e.target.tagName === 'SELECT') {
            const selectedOptionId = parseInt(e.target.value);
            const { product, selectedOptions } = this.state;
            const option = product.productOptions.find(option => option.id === selectedOptionId)
            const selectedOption = selectedOptions.find(selectedOption => selectedOption.optionId === selectedOptionId)

            if (option && !selectedOption) {
                const nextSelectedOptions = [
                    ...selectedOptions,
                    {
                        productId: product.id,
                        optionId: option.id,
                        optionName: option.name,
                        optionPrice: option.price,
                        quantity: 1
                    }
                ];
                this.setState({
                    ...this.state,
                    selectedOptions: nextSelectedOptions
                });
            }
        }
    })
}

 

 

 

 

 

📌 ProductDetailPage.js

import { request } from './api.js';
import ProductDetail from './ProductDetail.js';

export default function ProductDetailPage({ $target, productId }) {
  this.state = {
    productId,
    product: null
  }

  const $page = document.createElement('div');
  $page.className = 'ProductDetailPage';

  $page.innerHTML = '<h1>상품 정보</h1>'

  this.setState = nextState => {
    this.state = nextState;
    this.render();
  }

  this.render = () => {
    if (!this.state.product) {
      $target.innerHTML = 'Loading...';
    } else {
      $target.innerHTML = '';
      $target.appendChild($page);
      //ProductDetail 렌더링하기

      new ProductDetail({
        $target: $page,
        initState: {
          product: this.state.product,
          selectedOptions: []
        }
      })
    }
  }

  this.fetchProduct = async () => {
    const { productId } = this.state;
    const product = await request(`/products/${productId}`);
    this.setState({
      ...this.state,
      product
    });
  }

  this.fetchProduct();
}

 

 

 

 

 

📌 ProductList.js

import { routeChange } from "./router.js";

export default function ProductList({ $target, initState }) {
  this.state = initState;

  const $productList = document.createElement('ul');
  $target.appendChild($productList);

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  }

  this.render = () => {
    if (!this.state) return;

    $productList.innerHTML = `
      ${this.state.map((product) => 
        `<li class="Product" data-product-id="${product.id}">
          <a href="/web/products/${product.id}">
            <img src="${product.imageUrl}">
            <div class="Product__info">
              <div>${product.name}</div>
              <div>${product.price}원~</div>
            </div>
          </a>
        </li>
        `
      ).join('')}
    `
  }

  this.render();

  $productList.addEventListener('click', (e) => {
    const $li = e.target.closest('li');
    const { productId } = $li.dataset;

    if (productId) routeChange(`/web/products/${productId}`);
  });

}

 

 

 

 

 

📌 ProductListPage.js

import { request } from './api.js';
import ProductList from './ProductList.js';

export default function ProductListPage({ $target }) {
  //이 때 $target은 document.querySelector('.App')를 뜻함

  const $page = document.createElement('div');
  $page.className = 'ProductListPage';

  $page.innerHTML = '<h1>상품 목록</h1>';

  this.setState = (nextState) => {
    this.state = nextState
    this.render();
  }

  const fetchProducts = async () => {
    const products = await request('/products');
    this.setState(products);
    
      const productList = new ProductList({
        $target: $page,
        initState: this.state
    });
  }

  fetchProducts();

  this.render = () => {
    $target.appendChild($page);
  }
}

 

 

 

 

 

📌 router.js

const Router_CHANGE_EVENT = 'ROUTE_CHANGE';

export const init = (onRouteChange) => {
    window.addEventListener(Router_CHANGE_EVENT, () => {
        onRouteChange();
    });
}

export const routeChange = (url, params) => {
    history.pushState(null, null, url);
    window.dispatchEvent(new CustomEvent(Router_CHANGE_EVENT, params));
}

 

 

 

 

 

📌 SelectedOptions.js

import { getItem, setItem } from './storage.js';
import { routeChange } from './router.js';

export default function SelectedOptions({ $target, initState }) {
  const $component = document.createElement('div');
  $target.appendChild($component);

  this.state = initState;

  this.getTotalPrice = () => {
    const { product, selectedOptions } = this.state;
    const { price: productPrice } = product;

    return selectedOptions.reduce(
      (acc, option) => acc + ((productPrice + option.optionPrice) * option.quantity), 0
    );
  }

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  }

  this.render = () => {
    const { product, selectedOptions = [] } = this.state;
    if (product && selectedOptions) {
      $component.innerHTML = `
        <h3>선택된 상품</h3>
        <ul>
          ${selectedOptions.map(selectedOption => `
            <li>
              ${selectedOption.optionName} ${product.price + selectedOption.optionPrice}원
              <input type="text" data-optionId="${selectedOption.optionId}" value="${selectedOption.quantity}">
            <li>
          `).join('')}
        </ul>
        <div class="ProductDetail__totalPrice">${this.getTotalPrice()}원</div>
        <button class="OrderButton">주문하기</button>
      `
    }
  }

  this.render();

  $component.addEventListener('click', (e) => {
    const { selectedOptions } = this.state;
    if (e.target.className === 'OrderButton') {
      const cartData = getItem('products_cart', []);
      setItem('products_cart', cartData.concat(selectedOptions.map(selectedOption => ({
        productId: selectedOption.productId,
        optionId: selectedOption.optionId,
        quantity: selectedOption.quantity
      }))));
      
      routeChange('/web/cart');
    }
  })

  $component.addEventListener('change', e => {
    if (e.target.tagName === 'INPUT') {
      try {
        const nextQuantity = parseInt(e.target.value);
        const nextSelectedOptions = [...this.state.selectedOptions]
        
        if (typeof nextQuantity === 'number') {
          const { product } = this.state;

          const optionId = parseInt(e.target.dataset.optionid);
          const option = product.productOptions.find(option => option.id === optionId);
          const selectedOptionIndex = nextSelectedOptions.findIndex(selectedOption => selectedOption.optionId === optionId);

          nextSelectedOptions[selectedOptionIndex].quantity = option.stock >= nextQuantity ? nextQuantity : option.stock;
          
          this.setState({
            ...this.state,
            selectedOptions: nextSelectedOptions
          })
        }
      } catch (e) {
        console.log(e);
      }
    }
  })
}

 

 

 

 

 

📌 storage.js

const storage = localStorage;

export const getItem = (key, defaultValue) => {
  try {
    const value = storage.getItem(key);
    return value ? JSON.parse(value) : defaultValue
  } catch {
    return defaultValue
  }
}

export const setItem = (key, value) => {
  try {
    storage.setItem(key, JSON.stringify(value));
  } catch {
    //ignore
  }
}

export const removeItem = (key) => {
  try {
    storage.removeItem(key);
  } catch {
    //ignore
  }
}

 

 

 

 

 

 

728x90

+ Recent posts