diff --git a/src/advanced/__tests__/advanced.test.tsx b/src/advanced/__tests__/advanced.test.tsx index 85941d10..eb23706d 100644 --- a/src/advanced/__tests__/advanced.test.tsx +++ b/src/advanced/__tests__/advanced.test.tsx @@ -1,10 +1,14 @@ -import { fireEvent, render, screen, within } from '@testing-library/react'; -import { useState } from 'react'; -import { describe, expect, test } from 'vitest'; +import { fireEvent, render, renderHook, screen, within } from '@testing-library/react'; +import { act, useState } from 'react'; +import { beforeEach, describe, expect, test } from 'vitest'; +import { useForm, useLocalStorage } from '@/refactoring/hooks'; import { AdminPage } from '@/refactoring/pages/Admin/AdminPage'; +import { useCouponForm } from '@/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/hooks/useCouponForm'; +import { useProductForm } from '@/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/hooks/useProductForm'; import { CartPage } from '@/refactoring/pages/Cart/CartPage'; -import type { Coupon, Product } from '@/types'; +import { useCartLocalStorage } from '@/refactoring/pages/Cart/hooks/useCartLocalStorage'; +import type { Coupon, Discount, Product } from '@/types'; const mockProducts: Product[] = [ { @@ -72,7 +76,7 @@ const TestAdminPage = () => { }; describe('advanced > ', () => { - describe.only('시나리오 테스트 > ', () => { + describe('시나리오 테스트 > ', () => { test('장바구니 페이지 테스트 > ', async () => { render(); const product1 = screen.getByTestId('product-p1'); @@ -136,7 +140,7 @@ describe('advanced > ', () => { // 10. 쿠폰 적용하기 const couponSelect = screen.getByRole('combobox'); - fireEvent.change(couponSelect, { target: { value: '1' } }); // 10% 할인 쿠폰 선택 + fireEvent.change(couponSelect, { target: { value: mockCoupons[1].code } }); // 10% 할인 쿠폰 선택 // 11. 할인율 계산 expect(screen.getByText('상품 금액: 700,000원')).toBeInTheDocument(); @@ -144,7 +148,7 @@ describe('advanced > ', () => { expect(screen.getByText('최종 결제 금액: 531,000원')).toBeInTheDocument(); // 12. 다른 할인 쿠폰 적용하기 - fireEvent.change(couponSelect, { target: { value: '0' } }); // 5000원 할인 쿠폰 + fireEvent.change(couponSelect, { target: { value: mockCoupons[0].code } }); // 5000원 할인 쿠폰 expect(screen.getByText('상품 금액: 700,000원')).toBeInTheDocument(); expect(screen.getByText('할인 금액: 115,000원')).toBeInTheDocument(); expect(screen.getByText('최종 결제 금액: 585,000원')).toBeInTheDocument(); @@ -219,13 +223,276 @@ describe('advanced > ', () => { }); }); - describe('자유롭게 작성해보세요.', () => { - test('새로운 유틸 함수를 만든 후에 테스트 코드를 작성해서 실행해보세요', () => { - expect(true).toBe(false); + describe('useForm > ', () => { + test('초기 상태로 주어진 값으로 초기화되어야 합니다', () => { + const initialState = { name: 'John', age: 30 }; + const { result } = renderHook(() => useForm(initialState)); + + expect(result.current.value).toEqual(initialState); + }); + + test('값을 올바르게 업데이트해야 합니다', () => { + const initialState = { name: 'John', age: 30 }; + const { result } = renderHook(() => useForm(initialState)); + + act(() => { + result.current.updateValue('name', 'Doe'); + }); + + expect(result.current.value).toEqual({ name: 'Doe', age: 30 }); + }); + + test('init이 호출되면 초기 상태로 리셋되어야 합니다', () => { + const initialState = { name: 'John', age: 30 }; + const { result } = renderHook(() => useForm(initialState)); + + act(() => { + result.current.updateValue('name', 'Doe'); + result.current.init(); + }); + + expect(result.current.value).toEqual(initialState); + }); + }); + + describe('useCouponForm > ', () => { + test('기본 값으로 초기화되어야 한다', () => { + const { result } = renderHook(() => useCouponForm()); + + expect(result.current.editingCoupon).toEqual({ + name: '', + code: '', + discountType: 'amount', + discountValue: 0 + }); + }); + + test('이름이 올바르게 업데이트되어야 한다', () => { + const { result } = renderHook(() => useCouponForm()); + + act(() => { + result.current.updateName('New Coupon Name'); + }); + + expect(result.current.editingCoupon.name).toBe('New Coupon Name'); + }); + + test('코드가 올바르게 업데이트되어야 한다', () => { + const { result } = renderHook(() => useCouponForm()); + + act(() => { + result.current.updateCode('NEWCODE123'); + }); + + expect(result.current.editingCoupon.code).toBe('NEWCODE123'); + }); + + test('할인 유형이 올바르게 업데이트되어야 한다', () => { + const { result } = renderHook(() => useCouponForm()); + + act(() => { + result.current.updateDiscountType('percentage'); + }); + + expect(result.current.editingCoupon.discountType).toBe('percentage'); + }); + + test('할인 값이 올바르게 업데이트되어야 한다', () => { + const { result } = renderHook(() => useCouponForm()); + + act(() => { + result.current.updateDiscountValue(50); + }); + + expect(result.current.editingCoupon.discountValue).toBe(50); + }); + }); + + describe('useProductForm > ', () => { + const initialProduct: Product = { + id: '1', + name: 'Test Product', + price: 100, + stock: 10, + discounts: [] + }; + + test('초기 제품 데이터로 올바르게 초기화되어야 한다', () => { + const { result } = renderHook(() => useProductForm({ initProduct: initialProduct })); + expect(result.current.editingProduct).toEqual(initialProduct); + }); + + test('제품 이름을 업데이트할 수 있어야 한다', () => { + const { result } = renderHook(() => useProductForm({ initProduct: initialProduct })); + act(() => { + result.current.updateName('Updated Product'); + }); + expect(result.current.editingProduct.name).toBe('Updated Product'); + }); + + test('제품 가격을 업데이트할 수 있어야 한다', () => { + const { result } = renderHook(() => useProductForm({ initProduct: initialProduct })); + act(() => { + result.current.updatePrice(200); + }); + expect(result.current.editingProduct.price).toBe(200); + }); + + test('제품 재고를 업데이트할 수 있어야 한다', () => { + const { result } = renderHook(() => useProductForm({ initProduct: initialProduct })); + act(() => { + result.current.updateStock(20); + }); + expect(result.current.editingProduct.stock).toBe(20); + }); + + test('제품 할인 정보를 업데이트할 수 있어야 한다', () => { + const newDiscounts: Discount[] = [{ quantity: 10, rate: 0.1 }]; + const { result } = renderHook(() => useProductForm({ initProduct: initialProduct })); + act(() => { + result.current.updateDiscounts(newDiscounts); + }); + expect(result.current.editingProduct.discounts).toEqual(newDiscounts); + }); + }); + + describe('useLocalStorage > ', () => { + const key = 'testKey'; + const initialValue = 'initialValue'; + + beforeEach(() => { + window.localStorage.clear(); + }); + + test('값이 저장되어 있지 않으면 초기 값을 반환해야 합니다.', () => { + const { result } = renderHook(() => useLocalStorage({ key, initialValue })); + + expect(result.current[0]).toBe(initialValue); + }); + + test('값을 저장하고 검색할 수 있어야 합니다.', () => { + const { result } = renderHook(() => useLocalStorage({ key, initialValue })); + + act(() => { + result.current[1]('newValue'); + }); + + expect(result.current[0]).toBe('newValue'); + expect(localStorage.getItem(key)).toBe(JSON.stringify('newValue')); + }); + + test('localStorage에서 저장되어 있다면, 저장된 값을 가지고 와야 합니다.', () => { + localStorage.setItem(key, JSON.stringify('storedValue')); + + const { result } = renderHook(() => useLocalStorage({ key, initialValue })); + + expect(result.current[0]).toBe('storedValue'); + }); + + test('JSON 파싱 오류를 잘 처리해야 합니다.', () => { + localStorage.setItem(key, '{'); + + const { result } = renderHook(() => useLocalStorage({ key, initialValue })); + + expect(result.current[0]).toBe(initialValue); + }); + }); +}); + +describe('useCartLocalStorage > ', () => { + const sampleProduct: Product = { id: '1', name: 'Sample Product', price: 100, stock: 10, discounts: [] }; + const sampleCoupon: Coupon = { + name: '10% 할인 쿠폰', + code: 'DISCOUNT10', + discountType: 'percentage', + discountValue: 10 + }; + + beforeEach(() => { + window.localStorage.clear(); + }); + + test('상품을 장바구니에 추가할 수 있어야 합니다.', () => { + const { result } = renderHook(() => useCartLocalStorage()); + + act(() => { + result.current.addToCart(sampleProduct); + }); + + expect(result.current.cart).toHaveLength(1); + expect(result.current.cart[0].product).toEqual(sampleProduct); + }); + + test('이미 존재하는 상품의 수량이 증가해야 합니다.', () => { + const { result } = renderHook(() => useCartLocalStorage()); + + act(() => { + result.current.addToCart(sampleProduct); + }); + + act(() => { + result.current.addToCart(sampleProduct); + }); + + expect(result.current.cart[0].quantity).toBe(2); + }); + + test('장바구니에서 상품을 제거할 수 있어야 합니다.', () => { + const { result } = renderHook(() => useCartLocalStorage()); + + act(() => { + result.current.addToCart(sampleProduct); + }); + + expect(result.current.cart).toHaveLength(1); + + act(() => { + result.current.removeFromCart(sampleProduct.id); + }); + + expect(result.current.cart).toHaveLength(0); + }); + + test('상품의 수량을 업데이트할 수 있어야 합니다.', () => { + const { result } = renderHook(() => useCartLocalStorage()); + + act(() => { + result.current.addToCart(sampleProduct); + }); + + act(() => { + result.current.updateQuantity(sampleProduct.id, 5); + }); + + expect(result.current.cart[0].quantity).toBe(5); + }); + + test('쿠폰을 적용할 수 있어야 합니다.', () => { + const { result } = renderHook(() => useCartLocalStorage()); + + act(() => { + result.current.applyCoupon(sampleCoupon); + }); + + expect(result.current.selectedCoupon).toEqual(sampleCoupon); + }); + + test('총액이 올바르게 계산되어야 합니다.', () => { + const { result } = renderHook(() => useCartLocalStorage()); + + act(() => { + result.current.addToCart(sampleProduct); + }); + + act(() => { + result.current.applyCoupon(sampleCoupon); }); - test('새로운 hook 함수르 만든 후에 테스트 코드를 작성해서 실행해보세요', () => { - expect(true).toBe(false); + const total = result.current.calculateTotal(); + expect(total).toEqual({ + totalAfterDiscount: 90, + totalBeforeDiscount: 100, + totalDiscount: 10 }); }); }); diff --git a/src/basic/__tests__/basic.test.tsx b/src/basic/__tests__/basic.test.tsx index 9e62dd69..e74a4235 100644 --- a/src/basic/__tests__/basic.test.tsx +++ b/src/basic/__tests__/basic.test.tsx @@ -139,7 +139,7 @@ describe('basic > ', () => { // 10. 쿠폰 적용하기 const couponSelect = screen.getByRole('combobox'); - fireEvent.change(couponSelect, { target: { value: '1' } }); // 10% 할인 쿠폰 선택 + fireEvent.change(couponSelect, { target: { value: mockCoupons[1].code } }); // 10% 할인 쿠폰 선택 // 11. 할인율 계산 expect(screen.getByText('상품 금액: 700,000원')).toBeInTheDocument(); @@ -147,7 +147,7 @@ describe('basic > ', () => { expect(screen.getByText('최종 결제 금액: 531,000원')).toBeInTheDocument(); // 12. 다른 할인 쿠폰 적용하기 - fireEvent.change(couponSelect, { target: { value: '0' } }); // 5000원 할인 쿠폰 + fireEvent.change(couponSelect, { target: { value: mockCoupons[0].code } }); // 5000원 할인 쿠폰 expect(screen.getByText('상품 금액: 700,000원')).toBeInTheDocument(); expect(screen.getByText('할인 금액: 115,000원')).toBeInTheDocument(); expect(screen.getByText('최종 결제 금액: 585,000원')).toBeInTheDocument(); @@ -453,11 +453,14 @@ describe('basic > ', () => { expect(result.current.cart).toHaveLength(0); }); - test('제품 수량을 업데이트해야 합니다', () => { + test('제품 수량을 업데이트해야 합니다', async () => { const { result } = renderHook(() => useCart()); act(() => { result.current.addToCart(testProduct); + }); + + act(() => { result.current.updateQuantity(testProduct.id, 5); }); @@ -479,7 +482,13 @@ describe('basic > ', () => { act(() => { result.current.addToCart(testProduct); + }); + + act(() => { result.current.updateQuantity(testProduct.id, 2); + }); + + act(() => { result.current.applyCoupon(testCoupon); }); diff --git a/src/pages/Admin/CouponManagement/index.tsx b/src/pages/Admin/CouponManagement/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/Admin/ProductManagement/components/ProductEditor/DiscountDetails.tsx b/src/pages/Admin/ProductManagement/components/ProductEditor/DiscountDetails.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/index.tsx b/src/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/Admin/ProductManagement/index.tsx b/src/pages/Admin/ProductManagement/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/refactoring/hooks/index.ts b/src/refactoring/hooks/index.ts index f449bca1..789e86f2 100644 --- a/src/refactoring/hooks/index.ts +++ b/src/refactoring/hooks/index.ts @@ -1,2 +1,4 @@ export * from './useCoupon'; export * from './useProduct'; +export * from './useForm'; +export * from './useLocalstorage'; diff --git a/src/refactoring/hooks/useForm.ts b/src/refactoring/hooks/useForm.ts new file mode 100644 index 00000000..8c383707 --- /dev/null +++ b/src/refactoring/hooks/useForm.ts @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +export const useForm = (initialState: T) => { + const [value, setValue] = useState(initialState); + + const init = () => { + setValue(initialState); + }; + + const updateValue = (key: keyof T, value: T[keyof T]) => { + setValue(prev => ({ ...prev, [key]: value })); + }; + + return { value, updateValue, init }; +}; diff --git a/src/refactoring/hooks/useLocalstorage.ts b/src/refactoring/hooks/useLocalstorage.ts new file mode 100644 index 00000000..f3c2509d --- /dev/null +++ b/src/refactoring/hooks/useLocalstorage.ts @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react'; + +interface UseLocalStorageProps { + key: string; + initialValue: T; +} + +export const useLocalStorage = ({ key, initialValue }: UseLocalStorageProps): [T, (value: T) => void] => { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.error(`Error reading localStorage key: ${key}, error: ${error}`); + alert(`로컬스토리지를 읽는 중 문제가 발생했습니다. 문제가 지속될 경우 관리자에게 문의해주세요.`); + return initialValue; + } + }); + + useEffect(() => { + try { + window.localStorage.setItem(key, JSON.stringify(storedValue)); + } catch (error) { + console.error(`Error setting localStorage key: ${key}, error: ${error}`); + alert(`로컬스토리지를 사용하는 중 문제가 발생했습니다. 문제가 지속될 경우 관리자에게 문의해주세요.`); + } + }, [key, storedValue]); + + const setValue = (value: T) => { + setStoredValue(value); + }; + + return [storedValue, setValue]; +}; diff --git a/src/refactoring/pages/Admin/AdminPage.tsx b/src/refactoring/pages/Admin/AdminPage.tsx index bd1ed0e9..0a13f51d 100644 --- a/src/refactoring/pages/Admin/AdminPage.tsx +++ b/src/refactoring/pages/Admin/AdminPage.tsx @@ -1,5 +1,5 @@ -import { CouponManagement } from '@/refactoring/pages/Admin/features/CouponManagement/CouponManagement'; -import { ProductManagement } from '@/refactoring/pages/Admin/features/ProductManagement/ProductManagement'; +import { CouponManagement } from '@/refactoring/pages/Admin/CouponManagement'; +import { ProductManagement } from '@/refactoring/pages/Admin/ProductManagement'; import type { Coupon, Product } from '@/types'; interface Props { diff --git a/src/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/hooks/useCouponForm.ts b/src/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/hooks/useCouponForm.ts new file mode 100644 index 00000000..b3ed9676 --- /dev/null +++ b/src/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/hooks/useCouponForm.ts @@ -0,0 +1,33 @@ +import { useForm } from '@/refactoring/hooks'; +import type { Coupon } from '@/types'; + +export const useCouponForm = () => { + const { + value: editingCoupon, + updateValue, + init + } = useForm({ + name: '', + code: '', + discountType: 'amount', + discountValue: 0 + }); + + const updateName = (name: string) => { + updateValue('name', name); + }; + + const updateCode = (code: string) => { + updateValue('code', code); + }; + + const updateDiscountType = (discountType: 'amount' | 'percentage') => { + updateValue('discountType', discountType); + }; + + const updateDiscountValue = (discountValue: number) => { + updateValue('discountValue', discountValue); + }; + + return { editingCoupon, updateName, updateCode, updateDiscountType, updateDiscountValue, init }; +}; diff --git a/src/refactoring/pages/Admin/features/CouponManagement/components/CouponAddForm.tsx b/src/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/index.tsx similarity index 56% rename from src/refactoring/pages/Admin/features/CouponManagement/components/CouponAddForm.tsx rename to src/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/index.tsx index 5e44c1e4..f51977f1 100644 --- a/src/refactoring/pages/Admin/features/CouponManagement/components/CouponAddForm.tsx +++ b/src/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/index.tsx @@ -1,5 +1,4 @@ -import { useState } from 'react'; - +import { useCouponForm } from '@/refactoring/pages/Admin/CouponManagement/components/CouponAddForm/hooks/useCouponForm'; import type { Coupon } from '@/types'; interface CouponAddFormProps { @@ -7,21 +6,11 @@ interface CouponAddFormProps { } export const CouponAddForm = ({ onCouponAdd }: CouponAddFormProps) => { - const [newCoupon, setNewCoupon] = useState({ - name: '', - code: '', - discountType: 'percentage', - discountValue: 0 - }); + const { editingCoupon, updateName, updateCode, updateDiscountType, updateDiscountValue, init } = useCouponForm(); const handleAddCoupon = () => { - onCouponAdd(newCoupon); - setNewCoupon({ - name: '', - code: '', - discountType: 'percentage', - discountValue: 0 - }); + onCouponAdd(editingCoupon); + init(); }; return ( @@ -29,22 +18,22 @@ export const CouponAddForm = ({ onCouponAdd }: CouponAddFormProps) => { setNewCoupon({ ...newCoupon, name: e.target.value })} + value={editingCoupon.name} + onChange={e => updateName(e.target.value)} className="w-full rounded border p-2" /> setNewCoupon({ ...newCoupon, code: e.target.value })} + value={editingCoupon.code} + onChange={e => updateCode(e.target.value)} className="w-full rounded border p-2" />
setNewCoupon({ ...newCoupon, discountValue: parseInt(e.target.value) })} + value={editingCoupon.discountValue} + onChange={e => updateDiscountValue(parseInt(e.target.value))} className="w-full rounded border p-2" />
diff --git a/src/refactoring/pages/Admin/features/CouponManagement/components/CouponInfo.tsx b/src/refactoring/pages/Admin/CouponManagement/components/CouponInfo.tsx similarity index 100% rename from src/refactoring/pages/Admin/features/CouponManagement/components/CouponInfo.tsx rename to src/refactoring/pages/Admin/CouponManagement/components/CouponInfo.tsx diff --git a/src/refactoring/pages/Admin/features/CouponManagement/CouponManagement.tsx b/src/refactoring/pages/Admin/CouponManagement/index.tsx similarity index 78% rename from src/refactoring/pages/Admin/features/CouponManagement/CouponManagement.tsx rename to src/refactoring/pages/Admin/CouponManagement/index.tsx index 1e2b309e..7eeeb229 100644 --- a/src/refactoring/pages/Admin/features/CouponManagement/CouponManagement.tsx +++ b/src/refactoring/pages/Admin/CouponManagement/index.tsx @@ -1,5 +1,5 @@ -import { CouponAddForm } from '@/refactoring/pages/Admin/features/CouponManagement/components/CouponAddForm'; -import { CouponInfo } from '@/refactoring/pages/Admin/features/CouponManagement/components/CouponInfo'; +import { CouponAddForm } from '@/refactoring/pages/Admin/CouponManagement/components/CouponAddForm'; +import { CouponInfo } from '@/refactoring/pages/Admin/CouponManagement/components/CouponInfo'; import type { Coupon } from '@/types'; interface CouponManagementProps { diff --git a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductAddForm.tsx b/src/refactoring/pages/Admin/ProductManagement/components/ProductAddForm.tsx similarity index 91% rename from src/refactoring/pages/Admin/features/ProductManagement/components/ProductAddForm.tsx rename to src/refactoring/pages/Admin/ProductManagement/components/ProductAddForm.tsx index 163a0a95..916ef01b 100644 --- a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductAddForm.tsx +++ b/src/refactoring/pages/Admin/ProductManagement/components/ProductAddForm.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; -import { Collapsible } from '@/refactoring/pages/Admin/features/ProductManagement/ui/Collapsible'; -import { InputFieldWithLabel } from '@/refactoring/pages/Admin/features/ProductManagement/ui/InputFieldWithLabel'; +import { Collapsible } from '@/refactoring/pages/Admin/ProductManagement/ui/Collapsible'; +import { InputFieldWithLabel } from '@/refactoring/pages/Admin/ProductManagement/ui/InputFieldWithLabel'; import type { Product } from '@/types'; interface ProductAddFormProps { diff --git a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductDetails.tsx b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductDetails.tsx similarity index 100% rename from src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductDetails.tsx rename to src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductDetails.tsx diff --git a/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/DiscountDetails.tsx b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/DiscountDetails.tsx new file mode 100644 index 00000000..5a666b20 --- /dev/null +++ b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/DiscountDetails.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; + +import type { Discount } from '@/types'; + +interface DiscountDetailsProps { + discounts: Discount[]; + onDiscountsUpdate: (newDiscounts: Discount[]) => void; +} + +export const DiscountDetails = ({ discounts, onDiscountsUpdate }: DiscountDetailsProps) => { + const [newDiscount, setNewDiscount] = useState({ quantity: 0, rate: 0 }); + + const handleAddDiscount = () => { + onDiscountsUpdate([...discounts, newDiscount]); + setNewDiscount({ quantity: 0, rate: 0 }); + }; + + const handleRemoveDiscount = (index: number) => { + onDiscountsUpdate(discounts.filter((_, i) => i !== index)); + }; + + return ( +
+

할인 정보

+ {discounts.map((discount, index) => ( +
+ + {discount.quantity}개 이상 구매 시 {discount.rate * 100}% 할인 + + +
+ ))} +
+ setNewDiscount({ ...newDiscount, quantity: parseInt(e.target.value) })} + className="w-1/3 rounded border p-2" + /> + setNewDiscount({ ...newDiscount, rate: parseInt(e.target.value) / 100 })} + className="w-1/3 rounded border p-2" + /> + +
+
+ ); +}; diff --git a/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/hooks/useProductForm.ts b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/hooks/useProductForm.ts new file mode 100644 index 00000000..3885c940 --- /dev/null +++ b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/hooks/useProductForm.ts @@ -0,0 +1,34 @@ +import { useForm } from '@/refactoring/hooks'; +import type { Discount, Product } from '@/types'; + +interface UseProductFormProps { + initProduct: Product; +} + +export const useProductForm = ({ initProduct }: UseProductFormProps) => { + const { value: editingProduct, updateValue } = useForm(initProduct); + + const updateName = (name: string) => { + updateValue('name', name); + }; + + const updatePrice = (price: number) => { + updateValue('price', price); + }; + + const updateStock = (stock: number) => { + updateValue('stock', stock); + }; + + const updateDiscounts = (newDiscounts: Discount[]) => { + updateValue('discounts', newDiscounts); + }; + + return { + editingProduct, + updateName, + updatePrice, + updateStock, + updateDiscounts + }; +}; diff --git a/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/index.tsx b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/index.tsx new file mode 100644 index 00000000..0c4298c0 --- /dev/null +++ b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/index.tsx @@ -0,0 +1,59 @@ +import { DiscountDetails } from '@/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/DiscountDetails'; +import { useProductForm } from '@/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm/hooks/useProductForm'; +import type { Product } from '@/types'; + +interface ProductUpdateFormProps { + initProduct: Product; + onProductUpdate: (updatedProduct: Product) => void; + onEditComplete: () => void; +} + +export const ProductUpdateForm = ({ initProduct, onProductUpdate, onEditComplete }: ProductUpdateFormProps) => { + const { editingProduct, updateName, updatePrice, updateStock, updateDiscounts } = useProductForm({ initProduct }); + + const handleEditComplete = () => { + onProductUpdate(editingProduct); + onEditComplete(); + }; + + return ( +
+
+ + updateName(e.target.value)} + className="w-full rounded border p-2" + /> +
+
+ + updatePrice(parseInt(e.target.value))} + className="w-full rounded border p-2" + /> +
+
+ + updateStock(parseInt(e.target.value))} + className="w-full rounded border p-2" + /> +
+ + + + +
+ ); +}; diff --git a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/ProductEditor.tsx b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/index.tsx similarity index 76% rename from src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/ProductEditor.tsx rename to src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/index.tsx index 81e47811..d898244d 100644 --- a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/ProductEditor.tsx +++ b/src/refactoring/pages/Admin/ProductManagement/components/ProductEditor/index.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import { ProductDetails } from '@/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductDetails'; -import { ProductUpdateForm } from '@/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductUpdateForm'; -import { Collapsible } from '@/refactoring/pages/Admin/features/ProductManagement/ui/Collapsible'; +import { ProductDetails } from '@/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductDetails'; +import { ProductUpdateForm } from '@/refactoring/pages/Admin/ProductManagement/components/ProductEditor/ProductUpdateForm'; +import { Collapsible } from '@/refactoring/pages/Admin/ProductManagement/ui/Collapsible'; import type { Product } from '@/types'; interface ProductEditorProps { diff --git a/src/refactoring/pages/Admin/features/ProductManagement/ProductManagement.tsx b/src/refactoring/pages/Admin/ProductManagement/index.tsx similarity index 74% rename from src/refactoring/pages/Admin/features/ProductManagement/ProductManagement.tsx rename to src/refactoring/pages/Admin/ProductManagement/index.tsx index 0ce2de9a..5c0452d5 100644 --- a/src/refactoring/pages/Admin/features/ProductManagement/ProductManagement.tsx +++ b/src/refactoring/pages/Admin/ProductManagement/index.tsx @@ -1,5 +1,5 @@ -import { ProductAddForm } from '@/refactoring/pages/Admin/features/ProductManagement/components/ProductAddForm'; -import { ProductEditor } from '@/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/ProductEditor'; +import { ProductAddForm } from '@/refactoring/pages/Admin/ProductManagement/components/ProductAddForm'; +import { ProductEditor } from '@/refactoring/pages/Admin/ProductManagement/components/ProductEditor'; import type { Product } from '@/types'; interface ProductManagementProps { diff --git a/src/refactoring/pages/Admin/features/ProductManagement/ui/Collapsible.tsx b/src/refactoring/pages/Admin/ProductManagement/ui/Collapsible.tsx similarity index 100% rename from src/refactoring/pages/Admin/features/ProductManagement/ui/Collapsible.tsx rename to src/refactoring/pages/Admin/ProductManagement/ui/Collapsible.tsx diff --git a/src/refactoring/pages/Admin/features/ProductManagement/ui/InputFieldWithLabel.tsx b/src/refactoring/pages/Admin/ProductManagement/ui/InputFieldWithLabel.tsx similarity index 100% rename from src/refactoring/pages/Admin/features/ProductManagement/ui/InputFieldWithLabel.tsx rename to src/refactoring/pages/Admin/ProductManagement/ui/InputFieldWithLabel.tsx diff --git a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductUpdateForm.tsx b/src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductUpdateForm.tsx deleted file mode 100644 index 404c6dd3..00000000 --- a/src/refactoring/pages/Admin/features/ProductManagement/components/ProductEditor/components/ProductUpdateForm.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useState } from 'react'; - -import type { Discount, Product } from '@/types'; - -interface ProductUpdateFormProps { - initProduct: Product; - onProductUpdate: (updatedProduct: Product) => void; - onEditComplete: () => void; -} - -export const ProductUpdateForm = ({ initProduct, onProductUpdate, onEditComplete }: ProductUpdateFormProps) => { - const [editingProduct, setEditingProduct] = useState(initProduct); - const [newDiscount, setNewDiscount] = useState({ quantity: 0, rate: 0 }); - - const handleProductNameUpdate = (e: React.ChangeEvent) => { - const newName = e.target.value; - const updatedProduct = { ...editingProduct, name: newName }; - setEditingProduct(updatedProduct); - }; - - const handlePriceUpdate = (e: React.ChangeEvent) => { - const newPrice = parseInt(e.target.value); - const updatedProduct = { ...editingProduct, price: newPrice }; - setEditingProduct(updatedProduct); - }; - - const handleStockUpdate = (e: React.ChangeEvent) => { - const newStock = parseInt(e.target.value); - const updatedProduct = { ...editingProduct, stock: newStock }; - setEditingProduct(updatedProduct); - }; - - const handleAddDiscount = () => { - const newProduct = { - ...editingProduct, - discounts: [...editingProduct.discounts, newDiscount] - }; - setEditingProduct(newProduct); - setNewDiscount({ quantity: 0, rate: 0 }); - }; - - const handleRemoveDiscount = (index: number) => { - const newProduct = { - ...editingProduct, - discounts: editingProduct.discounts.filter((_, i) => i !== index) - }; - setEditingProduct(newProduct); - }; - - const handleEditComplete = () => { - onProductUpdate(editingProduct); - onEditComplete(); - }; - - return ( -
-
- - -
-
- - -
-
- - -
- {/* 할인 정보 수정 부분 */} -
-

할인 정보

- {editingProduct.discounts.map((discount, index) => ( -
- - {discount.quantity}개 이상 구매 시 {discount.rate * 100}% 할인 - - -
- ))} -
- setNewDiscount({ ...newDiscount, quantity: parseInt(e.target.value) })} - className="w-1/3 rounded border p-2" - /> - setNewDiscount({ ...newDiscount, rate: parseInt(e.target.value) / 100 })} - className="w-1/3 rounded border p-2" - /> - -
-
- - -
- ); -}; diff --git a/src/refactoring/pages/Cart/CartPage.tsx b/src/refactoring/pages/Cart/CartPage.tsx index 5420a81d..1dec17c0 100644 --- a/src/refactoring/pages/Cart/CartPage.tsx +++ b/src/refactoring/pages/Cart/CartPage.tsx @@ -1,6 +1,6 @@ -import { ProductList } from '@/refactoring/pages/Cart/features/ProductList/ProductList'; -import { ShoppingCart } from '@/refactoring/pages/Cart/features/ShoppingCart/ShoppingCart'; -import { useCart } from '@/refactoring/pages/Cart/hooks/useCart'; +import { useCartLocalStorage } from '@/refactoring/pages/Cart/hooks/useCartLocalStorage'; +import { ProductList } from '@/refactoring/pages/Cart/ProductList/\bindex'; +import { ShoppingCart } from '@/refactoring/pages/Cart/ShoppingCart'; import type { Coupon, Product } from '@/types'; interface Props { @@ -9,7 +9,8 @@ interface Props { } export const CartPage = ({ products, coupons }: Props) => { - const { cart, addToCart, removeFromCart, updateQuantity, applyCoupon, calculateTotal, selectedCoupon } = useCart(); + const { cart, addToCart, removeFromCart, updateQuantity, applyCoupon, calculateTotal, selectedCoupon } = + useCartLocalStorage(); return (
diff --git a/src/refactoring/pages/Cart/features/ProductList/ProductList.tsx "b/src/refactoring/pages/Cart/ProductList/\bindex.tsx" similarity index 84% rename from src/refactoring/pages/Cart/features/ProductList/ProductList.tsx rename to "src/refactoring/pages/Cart/ProductList/\bindex.tsx" index 8fe619cc..dc38f535 100644 --- a/src/refactoring/pages/Cart/features/ProductList/ProductList.tsx +++ "b/src/refactoring/pages/Cart/ProductList/\bindex.tsx" @@ -1,4 +1,4 @@ -import { ProductItem } from '@/refactoring/pages/Cart/features/ProductList/components/ProductItem'; +import { ProductItem } from '@/refactoring/pages/Cart/ProductList/components/ProductItem'; import type { CartItem, Product } from '@/types'; interface ProductListProps { diff --git a/src/refactoring/pages/Cart/features/ProductList/components/ProductItem.tsx b/src/refactoring/pages/Cart/ProductList/components/ProductItem.tsx similarity index 100% rename from src/refactoring/pages/Cart/features/ProductList/components/ProductItem.tsx rename to src/refactoring/pages/Cart/ProductList/components/ProductItem.tsx diff --git a/src/refactoring/pages/Cart/features/ShoppingCart/components/CartItem.tsx b/src/refactoring/pages/Cart/ShoppingCart/components/CartItem.tsx similarity index 100% rename from src/refactoring/pages/Cart/features/ShoppingCart/components/CartItem.tsx rename to src/refactoring/pages/Cart/ShoppingCart/components/CartItem.tsx diff --git a/src/refactoring/pages/Cart/features/ShoppingCart/components/AppliedCoupon.tsx b/src/refactoring/pages/Cart/ShoppingCart/components/CouponSelect/AppliedCoupon.tsx similarity index 100% rename from src/refactoring/pages/Cart/features/ShoppingCart/components/AppliedCoupon.tsx rename to src/refactoring/pages/Cart/ShoppingCart/components/CouponSelect/AppliedCoupon.tsx diff --git a/src/refactoring/pages/Cart/ShoppingCart/components/CouponSelect/index.tsx b/src/refactoring/pages/Cart/ShoppingCart/components/CouponSelect/index.tsx new file mode 100644 index 00000000..c1f3f3a7 --- /dev/null +++ b/src/refactoring/pages/Cart/ShoppingCart/components/CouponSelect/index.tsx @@ -0,0 +1,30 @@ +import { AppliedCoupon } from '@/refactoring/pages/Cart/ShoppingCart/components/CouponSelect/AppliedCoupon'; +import type { Coupon } from '@/types'; + +type CouponSelectProps = { + selectedCoupon: Coupon | null; + coupons: Coupon[]; + onCouponApply: (coupon: Coupon) => void; +}; + +export const CouponSelect = ({ selectedCoupon, coupons, onCouponApply }: CouponSelectProps) => { + return ( + <> + + + {selectedCoupon && } + + ); +}; diff --git a/src/refactoring/pages/Cart/features/ShoppingCart/components/OrderSummary.tsx b/src/refactoring/pages/Cart/ShoppingCart/components/OrderSummary.tsx similarity index 100% rename from src/refactoring/pages/Cart/features/ShoppingCart/components/OrderSummary.tsx rename to src/refactoring/pages/Cart/ShoppingCart/components/OrderSummary.tsx diff --git a/src/refactoring/pages/Cart/features/ShoppingCart/ShoppingCart.tsx b/src/refactoring/pages/Cart/ShoppingCart/index.tsx similarity index 74% rename from src/refactoring/pages/Cart/features/ShoppingCart/ShoppingCart.tsx rename to src/refactoring/pages/Cart/ShoppingCart/index.tsx index 3a013b1e..08a12d2a 100644 --- a/src/refactoring/pages/Cart/features/ShoppingCart/ShoppingCart.tsx +++ b/src/refactoring/pages/Cart/ShoppingCart/index.tsx @@ -1,7 +1,6 @@ -import { AppliedCoupon } from '@/refactoring/pages/Cart/features/ShoppingCart/components/AppliedCoupon'; -import { CartItem } from '@/refactoring/pages/Cart/features/ShoppingCart/components/CartItem'; -import { CouponSelect } from '@/refactoring/pages/Cart/features/ShoppingCart/components/CouponSelect'; -import { OrderSummary } from '@/refactoring/pages/Cart/features/ShoppingCart/components/OrderSummary'; +import { CartItem } from '@/refactoring/pages/Cart/ShoppingCart/components/CartItem'; +import { CouponSelect } from '@/refactoring/pages/Cart/ShoppingCart/components/CouponSelect'; +import { OrderSummary } from '@/refactoring/pages/Cart/ShoppingCart/components/OrderSummary'; import type { CartItem as CartItemType, Coupon } from '@/types'; interface ShoppingCartProps { @@ -47,8 +46,7 @@ export const ShoppingCart = ({

쿠폰 적용

- - {selectedCoupon && } +
diff --git a/src/refactoring/pages/Cart/features/ShoppingCart/components/CouponSelect.tsx b/src/refactoring/pages/Cart/features/ShoppingCart/components/CouponSelect.tsx deleted file mode 100644 index 6196fc92..00000000 --- a/src/refactoring/pages/Cart/features/ShoppingCart/components/CouponSelect.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Coupon } from '@/types'; - -type CouponSelectProps = { - coupons: Coupon[]; - onCouponApply: (coupon: Coupon) => void; -}; - -export const CouponSelect = ({ coupons, onCouponApply }: CouponSelectProps) => { - return ( - - ); -}; diff --git a/src/refactoring/pages/Cart/hooks/useCart.ts b/src/refactoring/pages/Cart/hooks/useCart.ts index 9176d9e4..61ef51d8 100644 --- a/src/refactoring/pages/Cart/hooks/useCart.ts +++ b/src/refactoring/pages/Cart/hooks/useCart.ts @@ -8,25 +8,22 @@ export const useCart = () => { const [cart, setCart] = useState([]); const [selectedCoupon, setSelectedCoupon] = useState(null); - // Q. updateCartItemQuantity는 함수로 분리 되어 있는데, addToCart는 분리 안되어 있는 이유는 뭘까..? const addToCart = (product: Product) => { - setCart(prevCart => { - const existingItem = prevCart.find(item => item.product.id === product.id); - if (existingItem) { - return prevCart.map(item => - item.product.id === product.id ? { ...item, quantity: Math.min(item.quantity + 1, product.stock) } : item - ); - } - return [...prevCart, { product, quantity: 1 }]; - }); + const existingItem = cart.find(item => item.product.id === product.id); + if (existingItem) { + setCart(updateCartItemQuantity(cart, product.id, existingItem.quantity + 1)); + return; + } + + setCart([...cart, { product, quantity: 1 }]); }; const removeFromCart = (productId: string) => { - setCart(prevCart => prevCart.filter(item => item.product.id !== productId)); + setCart(cart.filter(item => item.product.id !== productId)); }; const updateQuantity = (productId: string, newQuantity: number) => { - setCart(prevCart => updateCartItemQuantity(prevCart, productId, newQuantity)); + setCart(updateCartItemQuantity(cart, productId, newQuantity)); }; const applyCoupon = (coupon: Coupon) => { diff --git a/src/refactoring/pages/Cart/hooks/useCartLocalStorage.ts b/src/refactoring/pages/Cart/hooks/useCartLocalStorage.ts new file mode 100644 index 00000000..3f39e8e4 --- /dev/null +++ b/src/refactoring/pages/Cart/hooks/useCartLocalStorage.ts @@ -0,0 +1,47 @@ +import { useLocalStorage } from '@/refactoring/hooks'; +import { calculateCartTotal, updateCartItemQuantity } from '@/refactoring/pages/Cart/model/cart'; +import type { CartItem, Coupon, Product } from '@/types'; + +export const useCartLocalStorage = () => { + const [cart, setCart] = useLocalStorage({ key: 'cart', initialValue: [] }); + const [selectedCoupon, setSelectedCoupon] = useLocalStorage({ + key: 'selectedCoupon', + initialValue: null + }); + + const addToCart = (product: Product) => { + const existingItem = cart.find(item => item.product.id === product.id); + if (existingItem) { + setCart(updateCartItemQuantity(cart, product.id, existingItem.quantity + 1)); + return; + } + + setCart([...cart, { product, quantity: 1 }]); + }; + + const removeFromCart = (productId: string) => { + setCart(cart.filter(item => item.product.id !== productId)); + }; + + const updateQuantity = (productId: string, newQuantity: number) => { + setCart(updateCartItemQuantity(cart, productId, newQuantity)); + }; + + const applyCoupon = (coupon: Coupon) => { + setSelectedCoupon(coupon); + }; + + const calculateTotal = () => { + return calculateCartTotal(cart, selectedCoupon); + }; + + return { + cart, + addToCart, + removeFromCart, + updateQuantity, + applyCoupon, + calculateTotal, + selectedCoupon + }; +};