NEW!!
- HTML의
select
태그 - 콤보박스~
- 각 메뉴는
option
태그로 생성하여 자식으로. label
속성이나textContent
로 option 이름 지정value
속성으로 값 지정. (추후에 식별할 수 있음)selected
속성으로 대표 옵션 지정 ⇒ 자식 옵션 중 하나만 ㄱㄴ- 없으면 option의 textContent를 대신 사용
disabled
속성으로 비활성화 (값을 따로 줄 필요 x)
- render 함수의 매개변수는 있으면 안된다.
- element.closest(
태그이름
) ⇒ 상위로 올라가서 해당하는 태그 중 가장 가까운 태그를 찾는다. - 태그 이름 대신 id, class 등을 넣어도 됨

API url
호스트 : https://kdt-frontend.programmers.co.kr 상품 리스트 API : /products 상품 정보 API : /products/{id} 상품 옵션 API : /product-options?product.id={productId} 상품 옵션 수량 API : /product-option-stocks?productOption.id={productOptionId}
명세
- 상품, 옵션, 상품 내 cart 컴포넌트 만들기~
- 상품 API 형태 [ { id, name } ] - API는 옵션 따로, 스톡 따로이다. - 옵션 API 형태 [ { id, optionName, optionPrice } ] - 스톡 API 형태 => 객체가 배열로 한번 더 감싸져있음에 주의 [ [ { "id" :1, "stock": 5, ,, } ], [{}], [{}], ] <상품> - 패치하고, 옵션과 카트에 뿌려주는 역할 - 무족건 옵션과 카트 상태는 여기 state만 조작한다!, 나머지 state는 여기 setState에서만 조작 - ProductPage state 형식 { productId, product : { basePrice, id, name }, optionData : [], selectOptions } <옵션> - 상품 옵션 렌더링 시 `옵션명 (옵션가격 | 재고 : n)` 이런 형식으로 보여줘야 함 - 옵션 가격이 0보다 클 때 옵션가격을, 재고가 0보다 클 때 재고를 표시 - 재고가 0인 상품의 경우 옵션을 선택하지 못하게 함 - ProductOption state 형식 [ { optionId, optionName, optionPrice, stock }, ] <cart> - `상품명 - 옵션명 | 옵션가격, 선택수량 [x버튼]` 형식으로 보여줘야 하고, 총 가격을 맨 밑에 보여줘야 함 - state 형식 { productName, //상품명 basePrice, //상품 기본 가격 selectedOptions : [{ optionId, optionName, optionPrice, ea }] }
코드
- main.js
import ProductPage from "./component/ProductPage.js"; const $app = document.querySelector('.app'); new ProductPage({ $target : $app, initialState : { productId : 1 } })
- api.js
- fetch처리 하는 로직이 반복되므로 함수로 따로 빼주자!
const API_END_POINT = "https://kdt-frontend.programmers.co.kr"; export default function request(url) { if (!url.startsWith('/')) { throw new Error('url은 /로 시작해야합니다!'); } return fetch(API_END_POINT+url) .then(res => { if (res.ok) { return res.json(); } throw new Error(`${res.status} Error`) }) .catch(e=> console.log(e.message)) }
- ProductPage.js
- 패치 > 상태로 등록 > option 상태, cart 상태 등록시키기
- state가 바뀌면 선택한 상품 및 옵션이 바뀌는 것이다.
- (밖에서 setState할 때)setState할 때, 새로운 상태의 productId가 지금과 다르다면 다시 fetch
- render는 형식상 있는 것, 이 페이지에서 그려주는 것은 없고,
- fetch할 때 옵션과 스톡 api 호출이 따로인데, 스톡 호출할 때 옵션도 promise로 묶어서 같이 return 하면 좋다.
- ⇒ 옵션 호출 return문에서 then으로 스톡 처리를 해줄 수도 있겠지만, 그러면 depth가 또 깊어지기 때문이다.
여기서 option을 생성할 때 target으로 이 페이지의 div를 넣어주는 형식
⇒ Promise.resolve와 Promise.all 사용!
import request from "../api.js"; import ProductOptions from "./ProductOption.js"; import Cart from "./Cart.js"; export default function ProductPage({ $target, initialState }) { const $product = document.createElement('div') $target.appendChild($product) this.state = initialState this.setState = (newState) => { if (this.state.productId !== newState.productId) { fetchOptionData(this.state.productId) return; } this.state = newState; const { optionData, product, selectedOptions } = this.state; productOptions.setState(optionData) cart.setState({ productName : product.name, basePrice: product.basePrice, selectedOptions: selectedOptions }) } this.render = () => {} this.render() const productOptions = new ProductOptions({ $target: $product, initialState: [], onSelect: (option) => { const { selectedOptions } = this.state; const newOptions = [...selectedOptions]; const selectIdx = selectedOptions.findIndex(selected => option.optionId === selected.optionId); if (selectIdx != -1) { newOptions[selectIdx].ea++; } else { newOptions.push({ optionId : option.optionId, optionName : option.optionName, optionPrice : option.optionPrice, ea : 1 }) } this.setState({ ...this.state, selectedOptions : newOptions }) } }) const cart = new Cart( { $target: $product, initialState : { productName : '', basePrice : 1000, selectedOptions : [] }, onRemove : (index) => { const { selectedOptions } = this.state; const newOptions = [...selectedOptions]; newOptions.splice(index,1); this.setState({ ...this.state, selectedOptions: newOptions }) } }) const fetchOptionData = (productId) => { return request(`/products/${productId}`) .then(product => { this.setState({ ...this.state, product, optionData : [], selectedOptions : [] }) return request(`/product-options?product.id=${product.id}`) }) .then(options => { return Promise.all([ Promise.resolve(options), Promise.all(options.map(option => request(`/product-option-stocks?productOption.id=${option.id}`)))]) }) .then(([options, stocks]) => { const optionData = options.map((option, idx) => { const optionStock = stocks[idx][0].stock; return ( { optionId: option.id, optionName: option.optionName, optionPrice: option.optionPrice, stock: optionStock } )}) this.setState({ ...this.state, optionData, }) }) } fetchOptionData(this.state.productId) }
- ProductOption.js
export default function ProductOptions({ $target, initialState, onSelect }) { const $select = document.createElement('select'); $target.appendChild($select); this.state = initialState; this.setState = (newState) => { this.state = newState; this.render(); } const optionFullName = ({optionName, optionPrice, stock}) => { return `${optionName} ${optionPrice > 0 ?`(옵션가 ${optionPrice}` : ''} | ${stock > 0 ? `재고 : ${stock}` : '재고없음'})`; } $select.addEventListener('change', (event) => { const selectedOptionId = parseInt(event.target.value); const selectedOption = this.state.find(({optionId}) => optionId === selectedOptionId); onSelect(selectedOption); }) this.render = () => { if (this.state && Array.isArray(this.state)) { $select.innerHTML = ` <option>선택해주세요</option> ${this.state.map(option => `<option value=${option.optionId} ${option.stock <= 0 ? 'disabled' : ''}>${optionFullName(option)}</option>`)} ` } } this.render(); }
- Cart.js
export default function Cart ({ $target, initialState, onRemove }) { const $cart = document.createElement('div'); $target.appendChild($cart); this.state = initialState; this.setState = (newState) => { this.state = newState; this.render(); } const cartFullName = ({optionName, optionPrice, ea}) => { return `${this.state.productName} - ${optionName} | ${optionPrice}, ${ea}개` } const caculateTotalPrice = () => { const { selectedOptions, basePrice } = this.state; return selectedOptions.reduce((acc, current) => acc + (basePrice + current.optionPrice)*current.ea, 0) } this.render = () => { const selectedOptions = this.state.selectedOptions; if (Array.isArray(selectedOptions)) { $cart.innerHTML = `${Array.isArray(selectedOptions) && `<ul> ${selectedOptions.map((option) => `<li>${cartFullName(option)} <button class="remove">X</button> </li>`).join('')} </ul>`} <div> ${caculateTotalPrice()}원 </div> ` } $cart.querySelectorAll('.remove').forEach(($button,idx) => { $button.onclick = () => { onRemove(idx); }}) } this.render(); }