Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,133 changes: 1,133 additions & 0 deletions keyword/chapter09/keyword09.md

Large diffs are not rendered by default.

Empty file.
24 changes: 24 additions & 0 deletions mission/chapter09/mission_1/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Navbar from './components/Navbar';
import CartContainer from './components/CartContainer';
import { calculateTotals } from './store/cartSlice';
import type { RootState } from './store/store';

function App() {
const { cartItems } = useSelector((state: RootState) => state.cart);
const dispatch = useDispatch();

useEffect(() => {
dispatch(calculateTotals());
}, [cartItems, dispatch]);

return (
<>
<Navbar />
<CartContainer />
</>
);
}

export default App;
44 changes: 44 additions & 0 deletions mission/chapter09/mission_1/src/components/CartContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useDispatch, useSelector } from 'react-redux';
import CartItem from './CartItem';
import { clearCart } from '../store/cartSlice';
import type { RootState } from '../store/store';

const CartContainer = () => {
const { cartItems, total } = useSelector((state: RootState) => state.cart);
const dispatch = useDispatch();

if (cartItems.length === 0) {
return (
<main className="mx-auto w-full max-w-[900px] py-20 text-center">
<h2 className="text-3xl font-bold">장바구니가 비어 있습니다.</h2>
<p className="mt-3 text-gray-500">담긴 음반이 없습니다.</p>
</main>
);
}

return (
<main className="mx-auto w-full max-w-[900px]">
{cartItems.map((item) => (
<CartItem key={item.id} {...item} />
))}

<footer className="py-10">
<div className="mb-8 flex items-center justify-between border-t border-gray-300 pt-6 text-2xl font-bold">
<span>총 금액</span>
<span>${total.toLocaleString()}</span>
</div>

<div className="flex justify-center">
<button
onClick={() => dispatch(clearCart())}
className="rounded border border-black px-6 py-4 text-base hover:bg-black hover:text-white"
>
전체 삭제
</button>
</div>
</footer>
</main>
);
};

export default CartContainer;
54 changes: 54 additions & 0 deletions mission/chapter09/mission_1/src/components/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useDispatch } from 'react-redux';
import { decrease, increase, removeItem } from '../store/cartSlice';
import type { CartItemType } from '../constants/cartItems';

const CartItem = ({ id, title, singer, price, img, amount }: CartItemType) => {
const dispatch = useDispatch();

return (
<article className="flex items-center justify-between border-b border-gray-200 py-5">
<div className="flex items-center gap-5">
<img
src={img}
alt={title}
className="h-[100px] w-[100px] rounded object-cover"
/>

<div>
<h2 className="text-2xl font-bold text-black">{title}</h2>
<p className="text-lg text-slate-500">{singer}</p>
<p className="text-xl font-bold text-slate-700">${price}</p>

<button
onClick={() => dispatch(removeItem(id))}
className="mt-2 text-sm text-red-500 hover:underline"
>
삭제
</button>
</div>
</div>

<div className="flex items-center">
<button
onClick={() => dispatch(decrease(id))}
className="h-10 w-10 rounded-l bg-slate-300 text-xl font-medium"
>
-
</button>

<span className="flex h-10 w-12 items-center justify-center border border-gray-300 text-xl">
{amount}
</span>

<button
onClick={() => dispatch(increase(id))}
className="h-10 w-10 rounded-r bg-slate-300 text-xl font-medium"
>
+
</button>
</div>
</article>
);
};

export default CartItem;
21 changes: 21 additions & 0 deletions mission/chapter09/mission_1/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../store/store';

const Navbar = () => {
const { amount } = useSelector((state: RootState) => state.cart);

return (
<nav className="h-[84px] bg-slate-800 text-white">
<div className="mx-auto flex h-full max-w-[1280px] items-center justify-between px-4">
<h1 className="text-4xl font-bold">Chae Mart</h1>

<div className="flex items-center gap-2 text-3xl font-bold">
<span>🛒</span>
<span>{amount}</span>
</div>
</div>
</nav>
);
};

export default Navbar;
109 changes: 109 additions & 0 deletions mission/chapter09/mission_1/src/constants/cartItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
export type CartItemType = {
id: string;
title: string;
singer: string;
price: string;
img: string;
amount: number;
};

const cartItems = [
{
id: 'recB6qcHPxb62YJ75',
title: 'Vancouver',
singer: 'BIG Naughty (서동현)',
price: '25000',
img: 'https://image.bugsm.co.kr/album/images/500/40752/4075248.jpg',
amount: 1,
},
{
id: 'recdRxBsE14Rr2VuJ',
title: 'Empty Island',
singer: 'greenblue',
price: '18000',
img: 'https://f4.bcbits.com/img/a1472100223_10.jpg',
amount: 1,
},
{
id: 'recwTo120XST3PIoW',
title: 'golden hour',
singer: 'JVKE',
price: '28000',
img: 'https://image.bugsm.co.kr/album/images/200/193874/19387484.jpg?version=20230503022513.0',
amount: 1,
},
{
id: 'rec1JZlfCIBOPdcT2',
title: 'Home Sweet Home(From "어쩌면 우린 헤어졌는지 모른다")',
singer: 'Gogang (고갱)',
price: '20000',
img: 'https://is1-ssl.mzstatic.com/image/thumb/Music116/v4/8d/d7/0f/8dd70fba-0a8f-b7ce-a2d2-f0d32dad2837/8809912894132.jpg/1200x1200bf-60.jpg',
amount: 1,
},
{
id: 'recwTo160XST3PIoW',
title: 'Lemon',
singer: 'Kenshi Yonezu(켄시 요네즈/米津 玄師)',
price: '30000',
img: 'https://image.bugsm.co.kr/album/images/200/7222/722272.jpg?version=20220514022202.0',
amount: 1,
},
{
id: 'recaBo120XST3PIoW',
title: '돌멩이',
singer: 'MASYTA (마시따)',
price: '12000',
img: 'https://image.bugsm.co.kr/album/images/200/3271/327113.jpg?version=20230606014806.0',
amount: 1,
},
{
id: 'recqBo123XST3PIoK',
title: 'L’Amour, Les Baguettes, Paris',
singer: '스텔라 장(Stella Jang)',
price: '32000',
img: 'https://image.bugsm.co.kr/album/images/200/40660/4066056.jpg?version=20211020003912.0',
amount: 1,
},
{
id: 'recqBo133XST3PIoK',
title: 'NO PAIN',
singer: '실리카겔',
price: '22000',
img: 'https://image.bugsm.co.kr/album/images/200/40790/4079061.jpg?version=20220826063340.0',
amount: 1,
},
{
id: 'recqBo145XST3PIoK',
title: '너에게 (feat. HYUN SEO)',
singer: 'Halsoon',
price: '20000',
img: 'https://image.bugsm.co.kr/album/images/200/204634/20463445.jpg?version=20230110013144.0',
amount: 1,
},
{
id: 'recqBo129XST3PIoK',
title: '널 떠올리는 중이야(Think About You)',
singer: 'PATEKO (파테코) , Jayci yucca(제이씨 유카)',
price: '25000',
img: 'https://image.bugsm.co.kr/album/images/200/40581/4058181.jpg?version=20210726063528.0',
amount: 1,
},
{
id: 'rdaqBo129XST3PIoK',
title: '끝나지 않은 얘기(feat. 다이나믹 듀오)',
singer: '릴러말즈 & TOIL',
price: '23000',
img: 'https://image.bugsm.co.kr/album/images/200/204692/20469237.jpg?version=20220827004220.0',
amount: 1,
},
{
id: 'rdaqBo149XQT3PIoK',
title: '각자의 밤',
singer: '나상현씨 밴드',
price: '21000',
img: 'https://image.bugsm.co.kr/album/images/200/202235/20223594.jpg?version=20230904194021.0',
amount: 1,
},
];

export default cartItems;
1 change: 1 addition & 0 deletions mission/chapter09/mission_1/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'tailwindcss';
14 changes: 14 additions & 0 deletions mission/chapter09/mission_1/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './store/store';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
);
78 changes: 78 additions & 0 deletions mission/chapter09/mission_1/src/store/cartSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { createSlice } from '@reduxjs/toolkit';
import cartItems from '../constants/cartItems';
import type { CartItemType } from '../constants/cartItems';

type CartState = {
cartItems: CartItemType[];
amount: number;
total: number;
};

const initialState: CartState = {
cartItems,
amount: 0,
total: 0,
};

const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
increase: (state, action) => {
const item = state.cartItems.find((item) => item.id === action.payload);

if (item) {
item.amount += 1;
}
},

decrease: (state, action) => {
const item = state.cartItems.find((item) => item.id === action.payload);

if (item) {
item.amount -= 1;

if (item.amount < 1) {
state.cartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload
);
}
}
},

removeItem: (state, action) => {
state.cartItems = state.cartItems.filter(
(item) => item.id !== action.payload
);
},

clearCart: (state) => {
state.cartItems = [];
state.amount = 0;
state.total = 0;
},

calculateTotals: (state) => {
let amount = 0;
let total = 0;

state.cartItems.forEach((item) => {
amount += item.amount;
total += Number(item.price) * item.amount;
});

state.amount = amount;
state.total = total;
},
},
});

export const {
increase,
decrease,
removeItem,
clearCart,
calculateTotals,
} = cartSlice.actions;

export default cartSlice.reducer;
11 changes: 11 additions & 0 deletions mission/chapter09/mission_1/src/store/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';

export const store = configureStore({
reducer: {
cart: cartReducer,
},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Empty file.
24 changes: 24 additions & 0 deletions mission/chapter09/mission_2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Navbar from './components/Navbar';
import CartContainer from './components/CartContainer';
import { calculateTotals } from './features/cart/cartSlice';
import type { RootState } from './store/store';

function App() {
const { cartItems } = useSelector((state: RootState) => state.cart);
const dispatch = useDispatch();

useEffect(() => {
dispatch(calculateTotals());
}, [cartItems, dispatch]);

return (
<>
<Navbar />
<CartContainer />
</>
);
}

export default App;
Loading