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
'코딩테스트 > 프로그래머스' 카테고리의 다른 글
[Java] 프로그래머스 - 행렬과 연산 (4단계) (1) | 2022.09.23 |
---|---|
[프론트엔드/과제테스트] 고양이 사진첩 애플리케이션 기출 문제 해설 총 정리 (0) | 2022.08.18 |
[JavaScript] 프로그래머스 - 단어 변환 (3단계) (0) | 2022.08.04 |
[Java] 프로그래머스 - 단어 변환 (3단계) (0) | 2022.08.01 |
[Java] 프로그래머스 - 사칙연산 (4단계) (0) | 2022.08.01 |