Skip to content

Commit

Permalink
4일차
Browse files Browse the repository at this point in the history
  • Loading branch information
roy-jung committed Mar 10, 2022
1 parent 2a8529d commit 85d62d9
Show file tree
Hide file tree
Showing 17 changed files with 303 additions and 38 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</head>
<body>
<div id="root"></div>
<div id="modal"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
69 changes: 55 additions & 14 deletions src/components/cart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,70 @@
import { SyntheticEvent, useRef } from 'react'
import { createRef, SyntheticEvent, useEffect, useRef, useState } from 'react'
import { useRecoilState } from 'recoil'
import { CartType } from '../../graphql/cart'
import { checkedCartState } from '../../recoils/cart'
import CartItem from './item'
import WillPay from '../willPay'
import { useNavigate } from 'react-router-dom'

const CartList = ({ items }: { items: CartType[] }) => {
const navigate = useNavigate()
const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState)
const formRef = useRef<HTMLFormElement>(null)
const checkboxRefs = items.map(() => createRef<HTMLInputElement>())
const [formData, setFormData] = useState<FormData>()

const handleCheckboxChanged = (e: SyntheticEvent) => {
const setAllCheckedFromItems = () => {
// 개별아이템 선택시
if (!formRef.current) return
const checkboxes = formRef.current.querySelectorAll<HTMLInputElement>('.cart-item__checkbox')
const targetInput = e.target as HTMLInputElement
const data = new FormData(formRef.current)
const selectedCount = data.getAll('select-item').length
const allChecked = selectedCount === items.length
formRef.current.querySelector<HTMLInputElement>('.select-all')!.checked = allChecked
}

const setItemsChckedFromAll = (targetInput: HTMLInputElement) => {
const allChecked = targetInput.checked
checkboxRefs.forEach(inputElem => {
inputElem.current!.checked = allChecked
})
}

if (targetInput.classList.contains('select-all')) {
// select-all 선택시
const allChecked = targetInput.checked
checkboxes.forEach(inputElem => {
inputElem.checked = allChecked
})
const handleCheckboxChanged = (e?: SyntheticEvent) => {
if (!formRef.current) return
const targetInput = e?.target as HTMLInputElement
if (targetInput && targetInput.classList.contains('select-all')) {
setItemsChckedFromAll(targetInput)
} else {
// 개별아이템 선택시
const allChecked = selectedCount === items.length
formRef.current.querySelector<HTMLInputElement>('.select-all')!.checked = allChecked
setAllCheckedFromItems()
}
const data = new FormData(formRef.current)
setFormData(data)
}

const handleSubmit = () => {
if (checkedCartData.length) {
navigate('/payment')
} else {
alert('결제할 대상이 없어요')
}
}

useEffect(() => {
checkedCartData.forEach(item => {
const itemRef = checkboxRefs.find(ref => ref.current!.dataset.id === item.id)
if (itemRef) itemRef.current!.checked = true
})
setAllCheckedFromItems()
}, [])

useEffect(() => {
const checkedItems = checkboxRefs.reduce<CartType[]>((res, ref, i) => {
if (ref.current!.checked) res.push(items[i])
return res
}, [])
setCheckedCartData(checkedItems)
}, [items, formData])

return (
<div>
<form ref={formRef} onChange={handleCheckboxChanged}>
Expand All @@ -34,10 +74,11 @@ const CartList = ({ items }: { items: CartType[] }) => {
</label>
<ul className="cart">
{items.map((item, i) => (
<CartItem {...item} key={item.id} />
<CartItem {...item} key={item.id} ref={checkboxRefs[i]} />
))}
</ul>
</form>
<WillPay submitTitle="결제창으로" handleSubmit={handleSubmit} />
</div>
)
}
Expand Down
22 changes: 15 additions & 7 deletions src/components/cart/item.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { SyntheticEvent } from 'react'
import { ForwardedRef, forwardRef, SyntheticEvent } from 'react'
import { useMutation } from 'react-query'
import { CartType, DELETE_CART, UPDATE_CART } from '../../graphql/cart'
import { getClient, graphqlFetcher, QueryKeys } from '../../queryClient'
import ItemData from './itemData'

const CartItem = ({ id, imageUrl, price, title, amount }: CartType) => {
const CartItem = (
{ id, imageUrl, price, title, amount }: CartType,
ref: ForwardedRef<HTMLInputElement>,
) => {
const queryClient = getClient()
const { mutate: updateCart } = useMutation(
({ id, amount }: { id: string; amount: number }) => graphqlFetcher(UPDATE_CART, { id, amount }),
Expand Down Expand Up @@ -52,10 +56,14 @@ const CartItem = ({ id, imageUrl, price, title, amount }: CartType) => {

return (
<li className="cart-item">
<input className="cart-item__checkbox" type="checkbox" name="select-item" />
<img className="cart-item__image" src={imageUrl} />
<p className="cart-item__price">{price}</p>
<p className="cart-item__title">{title}</p>
<input
className="cart-item__checkbox"
type="checkbox"
name="select-item"
ref={ref}
data-id={id}
/>
<ItemData imageUrl={imageUrl} price={price} title={title} />
<input
className="cart-item__amount"
type="number"
Expand All @@ -70,4 +78,4 @@ const CartItem = ({ id, imageUrl, price, title, amount }: CartType) => {
)
}

export default CartItem
export default forwardRef(CartItem)
11 changes: 11 additions & 0 deletions src/components/cart/itemData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CartType } from '../../graphql/cart'

const ItemData = ({ imageUrl, price, title }: Pick<CartType, 'imageUrl' | 'price' | 'title'>) => (
<>
<img className="cart-item__image" src={imageUrl} />
<p className="cart-item__price">{price}</p>
<p className="cart-item__title">{title}</p>
</>
)

export default ItemData
49 changes: 49 additions & 0 deletions src/components/payment/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useState } from 'react'
import { useMutation } from 'react-query'
import { useNavigate } from 'react-router-dom'
import { useRecoilState, useSetRecoilState } from 'recoil'
import { EXECUTE_PAY } from '../../graphql/payment'
import { graphqlFetcher } from '../../queryClient'
import { checkedCartState } from '../../recoils/cart'
import WillPay from '../willPay'
import PaymentModal from './modal'

type PayInfo = {
id: string
amount: number
}
type PaymentInfos = PayInfo[]

const Payment = () => {
const navigate = useNavigate()
const [checkedCartData, setCheckedCartData] = useRecoilState(checkedCartState)
const [modalShown, toggleModal] = useState(false)
const { mutate: executePay } = useMutation((payInfos: PaymentInfos) =>
graphqlFetcher(EXECUTE_PAY, payInfos),
)

const showModal = () => {
toggleModal(true)
}

const proceed = () => {
const payInfos = checkedCartData.map(({ id, amount }) => ({ id, amount }))
executePay(payInfos)
setCheckedCartData([])

// navigate('/products', { replace: true })
}

const cancel = () => {
toggleModal(false)
}

return (
<div>
<WillPay submitTitle="결제하기" handleSubmit={showModal} />
<PaymentModal show={modalShown} proceed={proceed} cancel={cancel} />
</div>
)
}

export default Payment
32 changes: 32 additions & 0 deletions src/components/payment/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactChild } from 'react'
import { createPortal } from 'react-dom'

const ModalPortal = ({ children }: { children: ReactChild }) => {
return createPortal(children, document.getElementById('modal')!)
}

const PaymentModal = ({
show,
proceed,
cancel,
}: {
show: boolean
proceed: () => void
cancel: () => void
}) => {
return show ? (
<ModalPortal>
<div className={`modal ${show ? 'show' : ''}`}>
<div className="modal__inner">
<p>정말 결제할까요?</p>
<div>
<button onClick={proceed}></button>
<button onClick={cancel}>아니오</button>
</div>
</div>
</div>
</ModalPortal>
) : null
}

export default PaymentModal
2 changes: 1 addition & 1 deletion src/components/product/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ADD_CART } from '../../graphql/cart'
import { Product } from '../../graphql/products'
import { graphqlFetcher } from '../../queryClient'

const ProductItem = ({ id, imageUrl, price, title, description, createdAt }: Product) => {
const ProductItem = ({ id, imageUrl, price, title }: Product) => {
const { mutate: addCart } = useMutation((id: string) => graphqlFetcher(ADD_CART, { id }))

return (
Expand Down
12 changes: 12 additions & 0 deletions src/components/product/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Product } from '../../graphql/products'
import ProductItem from './item'

const ProductList = ({ list }: { list: Product[] }) => (
<ul className="products">
{list.map(product => (
<ProductItem {...product} key={product.id} />
))}
</ul>
)

export default ProductList
37 changes: 37 additions & 0 deletions src/components/willPay/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { SyntheticEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { checkedCartState } from '../../recoils/cart'
import ItemData from '../cart/itemData'

const WillPay = ({
submitTitle,
handleSubmit,
}: {
submitTitle: string
handleSubmit: (e: SyntheticEvent) => void
}) => {
const checkedItems = useRecoilValue(checkedCartState)
const totalPrice = checkedItems.reduce((res, { price, amount }) => {
res += price * amount
return res
}, 0)

return (
<div className="cart-willpay">
<ul>
{checkedItems.map(({ imageUrl, price, title, amount, id }) => (
<li key={id}>
<ItemData imageUrl={imageUrl} price={price} title={title} />
<p>수량: {amount}</p>
<p>금액: {price * amount}</p>
</li>
))}
</ul>
<p>총예상결제액: {totalPrice}</p>
<button onClick={handleSubmit}>{submitTitle}</button>
</div>
)
}

export default WillPay
12 changes: 12 additions & 0 deletions src/graphql/payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { gql } from 'graphql-request'

export const EXECUTE_PAY = gql`
type PayInfo {
id: String!
amount: Int!
}
mutation EXECUTE_PAY($info: [PayInfo]) {
payInfo(info: $info)
}
`
5 changes: 5 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { graphql } from 'msw'
import { GET_CART, ADD_CART, CartType, UPDATE_CART, DELETE_CART } from '../graphql/cart'
import { EXECUTE_PAY } from '../graphql/payment'
import GET_PRODUCTS, { GET_PRODUCT } from '../graphql/products'

const mockProducts = (() =>
Expand Down Expand Up @@ -68,4 +69,8 @@ export const handlers = [
cartData = newData
return res(ctx.data(id))
}),
graphql.mutation(EXECUTE_PAY, ({ variables }, res, ctx) => {
console.log(variables)
return res()
}),
]
6 changes: 4 additions & 2 deletions src/pages/payment/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const Payment = () => <div />
import Payment from '../../components/payment'

export default Payment
const PaymentPage = () => <Payment />

export default PaymentPage
12 changes: 4 additions & 8 deletions src/pages/products/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { useQuery } from 'react-query'
import ProductItem from '../../components/product/item'
import ProductList from '../../components/product/list'
import GET_PRODUCTS, { Products } from '../../graphql/products'
import { graphqlFetcher, QueryKeys } from '../../queryClient'

const ProductList = () => {
const ProductListPage = () => {
const { data } = useQuery<Products>(QueryKeys.PRODUCTS, () => graphqlFetcher(GET_PRODUCTS))

return (
<div>
<h2>상품목록</h2>
<ul className="products">
{data?.products?.map(product => (
<ProductItem {...product} key={product.id} />
))}
</ul>
<ProductList list={data?.products || []} />
</div>
)
}

export default ProductList
export default ProductListPage
8 changes: 5 additions & 3 deletions src/recoils/cart.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { atom, selector, selectorFamily, useRecoilValue } from 'recoil'
import { CartType } from '../graphql/cart'

const cartState = atom<Map<string, number>>({
export const checkedCartState = atom<CartType[]>({
key: 'cartState',
default: new Map(),
default: [],
})

export const cartItemSelector = selectorFamily<number | undefined, string>({
/* export const cartItemSelector = selectorFamily<number | undefined, string>({
key: 'cartItem',
get:
(id: string) =>
Expand All @@ -24,3 +25,4 @@ export const cartItemSelector = selectorFamily<number | undefined, string>({
}
},
})
*/

0 comments on commit 85d62d9

Please sign in to comment.