diff --git a/src/components/domain/coupon/coupon-status-tag/index.tsx b/src/components/domain/coupon/coupon-status-tag/index.tsx deleted file mode 100644 index 3667c1a6..00000000 --- a/src/components/domain/coupon/coupon-status-tag/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import styled from 'styled-components'; -import { CouponTagProps, StyledCouponTagProps } from './type'; -import { colors } from '@/constants/colors'; -import { TextBox } from '@components/atom/text-box'; -import { - COUPON_STATUS_DISABLE, - COUPON_STATUS_ENABLE, - COUPON_STATUS_SOLD_OUT, -} from '@/constants/coupon'; - -export const CouponStatusTag = ({ status }: CouponTagProps) => { - let borderColor = ''; - let backgroundColor = colors.primary; - let color = colors.white; - let text: string = COUPON_STATUS_ENABLE.label; - - if (status === COUPON_STATUS_SOLD_OUT.value) { - borderColor = colors.orange; - backgroundColor = colors.white; - color = colors.orange; - text = COUPON_STATUS_SOLD_OUT.label; - } - - if (status === COUPON_STATUS_DISABLE.value) { - backgroundColor = colors.black600; - text = COUPON_STATUS_DISABLE.label; - } - return ( - - - {text} - - - ); -}; - -const StyledLayout = styled.div` - width: 75px; - height: 28px; - - background-color: ${(props) => props.backgroundColor}; - color: ${(props) => props.color}; - border: 1px solid ${(props) => props.borderColor || ''}; - border-radius: 2px; - - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/src/components/domain/coupon/coupon-status-tag/type.ts b/src/components/domain/coupon/coupon-status-tag/type.ts deleted file mode 100644 index 7ec53047..00000000 --- a/src/components/domain/coupon/coupon-status-tag/type.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type StyledCouponTagProps = { - backgroundColor: string; - borderColor: string; - color: string; -}; -export type CouponTagProps = { - status: string; -}; diff --git a/src/components/domain/coupon/table-cell/index.tsx b/src/components/domain/coupon/table-cell/index.tsx index 328746f0..670e52b5 100644 --- a/src/components/domain/coupon/table-cell/index.tsx +++ b/src/components/domain/coupon/table-cell/index.tsx @@ -2,11 +2,19 @@ import { TextBox } from '@components/atom/text-box'; import { Input, Tooltip } from 'antd'; import styled from 'styled-components'; import { + CouponTagProps, + StyledCouponTagProps, couponNameContainerProps, dayLimitInputProps, roomContainerProps, } from './type'; import { InfoCircleOutlined } from '@ant-design/icons'; +import { colors } from '@/constants/colors'; +import { + COUPON_STATUS_DISABLE, + COUPON_STATUS_ENABLE, + COUPON_STATUS_SOLD_OUT, +} from '@/constants/coupon'; export const RoomContainer = ({ room }: roomContainerProps) => { return ( @@ -115,3 +123,47 @@ const StyledDayLimitTitle = styled.div` display: flex; gap: 4px; `; + +export const CouponStatusTag = ({ status }: CouponTagProps) => { + let borderColor = ''; + let backgroundColor = colors.primary; + let color = colors.white; + let text: string = COUPON_STATUS_ENABLE.label; + + if (status === COUPON_STATUS_SOLD_OUT.value) { + borderColor = colors.orange; + backgroundColor = colors.white; + color = colors.orange; + text = COUPON_STATUS_SOLD_OUT.label; + } + + if (status === COUPON_STATUS_DISABLE.value) { + backgroundColor = colors.black600; + text = COUPON_STATUS_DISABLE.label; + } + return ( + + + {text} + + + ); +}; + +const StyledCouponStatusTag = styled.div` + width: 75px; + height: 28px; + + background-color: ${(props) => props.backgroundColor}; + color: ${(props) => props.color}; + border: 1px solid ${(props) => props.borderColor || ''}; + border-radius: 2px; + + display: flex; + align-items: center; + justify-content: center; +`; diff --git a/src/components/domain/coupon/table-cell/type.ts b/src/components/domain/coupon/table-cell/type.ts index c4195802..bb9a31c6 100644 --- a/src/components/domain/coupon/table-cell/type.ts +++ b/src/components/domain/coupon/table-cell/type.ts @@ -22,3 +22,11 @@ export type dayLimitInputProps = { key: number, ) => void; }; +export type StyledCouponTagProps = { + backgroundColor: string; + borderColor: string; + color: string; +}; +export type CouponTagProps = { + status: string; +}; diff --git a/src/components/domain/coupon/table/index.tsx b/src/components/domain/coupon/table/index.tsx index cf5339d3..3de0f0cd 100644 --- a/src/components/domain/coupon/table/index.tsx +++ b/src/components/domain/coupon/table/index.tsx @@ -3,11 +3,11 @@ import Table, { ColumnsType } from 'antd/lib/table'; import styled from 'styled-components'; import { CouponNameContainer, + CouponStatusTag, DayLimitInput, DayLimitTitle, RoomContainer, } from '../table-cell'; -import { CouponStatusTag } from '../coupon-status-tag'; import { Select } from 'antd'; import { TextBox } from '@components/atom/text-box'; import { TableProps, couponTableProps, tableData } from './type'; diff --git a/src/hooks/coupon/useCoupon.ts b/src/hooks/coupon/useCoupon.ts index 862c046d..d788283d 100644 --- a/src/hooks/coupon/useCoupon.ts +++ b/src/hooks/coupon/useCoupon.ts @@ -25,17 +25,29 @@ import { isCouponModifiedState } from '@stores/coupon/atom'; * @description 쿠폰 관리 페이지 로직을 다루는 hook * * @returns - * data, - isGetCouponError, - deleteCoupon, + deleteCoupon, couponData, handleSelectStatus, handleSelectRecord, handleSelectCouponType, handleChangeDayLimit, handleDeleteButton, - isModified, handleChangeDate, + handleEditButton, + handleModalOpen, + handleModalClose, + isModalOpen, + handleBatchEditCheckbox, + purchaseData, + handleChangeBatchValue, + handleChangeBuyQuantity, + handlePurchaseButton, + isPointModalOpen, + setIsPointModalOpen, + isGetCouponLoading, + handleAgreeCheckbox, + isAgreed, + error, */ export const useCoupon = () => { @@ -111,9 +123,9 @@ export const useCoupon = () => { className: 'confirm-modal', onOk: () => setIsPointModalOpen(true), }); - } else { - message.error('요청에 실패했습니다. 잠시 후 다시 시도해 주세요.'); + return; } + message.error('요청에 실패했습니다. 잠시 후 다시 시도해 주세요.'); }, }); @@ -164,37 +176,29 @@ export const useCoupon = () => { }; }, []); + /** + * 서버로부터 받은 쿠폰 데이터를 테이블에 할당할 수 있는 데이터로 가공 + * @param {Coupons} data 서버로 부터 받은 쿠폰 데이터 + */ const processCouponTableData = (data: Coupons) => { - const couponTableData = []; - const originData = []; + const couponTableData = createData(data); + const originData = createData(data); + setCouponData({ expiry: data.expiry, coupons: [...couponTableData] }); + originCouponTableData.current = { + expiry: data.expiry, + coupons: [...originData], + }; + }; + + const createData = (data: Coupons) => { + const resultData = []; let key = -1; for (const room of data.rooms) { for (let index = 0; index < room.coupons.length; index++) { key++; const coupon = room.coupons[index]; const length = index === 0 ? room.coupons.length : 0; - couponTableData.push({ - room: { - name: room.roomName, - price: room.roomPrice, - id: room.roomId, - length, - }, - key, - couponId: coupon.couponId, - status: coupon.status, - info: { - name: coupon.couponName, - appliedPrice: coupon.appliedPrice, - }, - dayLimit: coupon.dayLimit, - quantity: coupon.quantity, - couponType: coupon.couponType, - discount: coupon.discount, - discountType: coupon.discountType, - isSoldOut: coupon.status === 'SOLD_OUT', - }); - originData.push({ + resultData.push({ room: { name: room.roomName, price: room.roomPrice, @@ -217,13 +221,11 @@ export const useCoupon = () => { }); } } - setCouponData({ expiry: data.expiry, coupons: [...couponTableData] }); - originCouponTableData.current = { - expiry: data.expiry, - coupons: [...originData], - }; + return resultData; }; - + /** + * 서버로부터 받은 쿠폰 데이터를 추가 구매 모달에 출력될 수 있는 데이터로 가공 + */ const processPurchaseData = () => { const data: PurchaseData = { batchValue: 0, @@ -266,6 +268,11 @@ export const useCoupon = () => { setPurchaseData(data); }; + /** + * 쿠폰 상태 변경시 check 된 아이템의 쿠폰 상태를 변경 + * @param {string} value 변경된 쿠폰 상태 + */ + const handleSelectStatus = (value: string) => { setSelectedStatus(value); const { expiry, coupons: data } = { ...couponData }; @@ -275,6 +282,12 @@ export const useCoupon = () => { setCouponData({ expiry, coupons: data }); }; + /** + * checkbox를 통해 쿠폰 아이템 선택 시 selectedRowKey에 해당 쿠폰 아이템 key를 추가하고 + * 쿠폰 아이템의 상태를 select box에 있는 상태로 변경시켜주는 함수 + * @param {number} selectedRowKeys 선택된 record keys + */ + const handleSelectRecord = (selectedRowKeys: number[]) => { const { expiry, coupons: data } = { ...couponData }; selectedRowKeys.map((key) => { @@ -286,12 +299,24 @@ export const useCoupon = () => { setSelectedRowKeys(selectedRowKeys); }; + /** + * 노출 기준 선택 시 couponData state를 업데이트 시켜주는 함수 + * @param {string} value 선택된 노출 기준 값 + * @param {number} key 선택된 쿠폰 아이템 + */ + const handleSelectCouponType = (value: string, key: number) => { const { expiry, coupons: data } = { ...couponData }; data[key].couponType = value; setCouponData({ expiry, coupons: data }); }; + /** + * 일일 제한 수량 input 값 변경 시 couponData state를 업데이트 시켜주는 함수 + * @param {React.ChangeEvent} event 발생한 이벤트 + * @param {number} key 선택된 쿠폰 아이템 + */ + const handleChangeDayLimit = ( event: React.ChangeEvent, key: number, @@ -304,15 +329,29 @@ export const useCoupon = () => { setCouponData({ expiry, coupons: data }); }; + /** + * 쿠폰 적용 기간 변경 시 couponData state를 업데이트 시켜주는 함수 + * @param {string} date 날짜 + */ + const handleChangeDate = (date: string) => { const { coupons } = { ...couponData }; setCouponData({ expiry: date, coupons }); }; + /** + * checkbox로 선택된 row가 존재하는지 확인하는 함수 + * @returns {boolean} 존재 여부 + */ const isSelectedRow = () => { return selectedRowKeys.length !== 0; }; + /** + * 선택된 row 중 쿠폰 상태가 소진인 아이템이 존재하는지 확인하는 함수 + * @param {number[]} selectedRowKeys + * @returns {boolean} 존재 여부 + */ const findNotSoldOutData = (selectedRowKeys: number[]) => { for (let index = 0; index < selectedRowKeys.length; index++) { const key = selectedRowKeys[index]; @@ -321,35 +360,56 @@ export const useCoupon = () => { return false; }; + /** + * 삭제할 데이터를 서버에게 request 하기 위해 가공하는 함수 + * @param {number[]} selectedRowKeys 선택된 rows의 key + */ const processDeleteData = (selectedRowKeys: number[]) => { - const rooms: { couponId: number }[][] = []; - for (let index = 0; index < selectedRowKeys.length; index++) { - const key = selectedRowKeys[index]; + const roomsMap = createDeleteRoomsMap(selectedRowKeys); + const data = createDeleteParams(roomsMap); + return data; + }; + + const createDeleteRoomsMap = (selectedRowKeys: number[]) => { + const roomsMap = new Map(); + for (const key of selectedRowKeys) { const { room, couponId } = couponData.coupons[key]; - if (!rooms[room.id]) { - rooms[room.id] = []; - } - rooms[room.id].push({ couponId }); + const roomCoupons = roomsMap.get(room.id) || []; + roomCoupons.push({ couponId }); + roomsMap.set(room.id, roomCoupons); } + return roomsMap; + }; + + const createDeleteParams = ( + roomsMap: Map, + ) => { const data: CouponDeleteParams = { accommodationId: Number(accommodationId as string), rooms: [], }; - for (let index = 0; index < rooms.length; index++) { - if (rooms[index]) { - const roomsData = { - roomId: index, - coupons: rooms[index], - }; - data.rooms.push(roomsData); - } - } + roomsMap.forEach((roomCoupons, roomId) => { + const roomsData = { + roomId, + coupons: roomCoupons, + }; + data.rooms.push(roomsData); + }); return data; }; + /** + * 수정할 쿠폰 데이터를 서버에게 request 하기 위해 가공하는 함수 + */ const processEditData = () => { - const rooms: EditCoupon[][] = []; - for (let index = 0; index < couponData.coupons.length; index++) { + const roomsMap = createEditRoomsMap(); + const data = createEditParams(roomsMap); + return data; + }; + + const createEditRoomsMap = () => { + const roomsMap = new Map(); + for (const coupon of couponData.coupons) { const { room, couponId, @@ -358,11 +418,9 @@ export const useCoupon = () => { discountType, dayLimit, couponType, - } = couponData.coupons[index]; - if (!rooms[room.id]) { - rooms[room.id] = []; - } - rooms[room.id].push({ + } = coupon; + const roomCoupons = roomsMap.get(room.id) || []; + roomCoupons.push({ couponId, status, discount, @@ -370,23 +428,29 @@ export const useCoupon = () => { dayLimit, couponType, }); + roomsMap.set(room.id, roomCoupons); } + return roomsMap; + }; + + const createEditParams = (roomsMap: Map) => { const data: CouponEditParams = { accommodationId: Number(accommodationId as string), expiry: couponData.expiry, rooms: [], }; - for (let index = 0; index < rooms.length; index++) { - if (rooms[index]) { - data.rooms.push({ - roomId: index, - coupons: rooms[index], - }); - } - } + roomsMap.forEach((roomCoupons, roomId) => { + data.rooms.push({ + roomId, + coupons: roomCoupons, + }); + }); return data; }; + /** + * 삭제 버튼을 클릭했을 때 실행할 함수 + */ const handleDeleteButton = () => { if (isCouponModified) { message.warning('수정 중인 내용을 먼저 저장하세요'); @@ -421,6 +485,9 @@ export const useCoupon = () => { }); }; + /** + * 저장 버튼을 클릭했을 때 실행할 함수 + */ const handleEditButton = () => { Modal.confirm({ title: @@ -435,6 +502,10 @@ export const useCoupon = () => { }); }; + /** + * 추가 구매 버튼을 클릭했을 때 실행할 함수 + */ + const handleModalOpen = () => { if (isCouponModified) { message.warning('수정 중인 내용을 먼저 저장하세요'); @@ -451,18 +522,32 @@ export const useCoupon = () => { setIsModalOpen(false); }; + /** + * 개별 구매 수량 input 값의 유효성 검사를 하는 함수 + * @param {number} value + * @param {PurchaseCoupons} coupon + */ const validateBuyQuantity = (value: number, coupon: PurchaseCoupons) => { if (value > 999 || value < 0) return; if (Number.isNaN(value)) coupon.buyQuantity = 0; else coupon.buyQuantity = value; }; + /** + * 일괄 적용 input 값의 유효성 검사를 하는 함수 + * @param {number} value + * @param {PurchaseData} data + */ const validateBatchValue = (value: number, data: PurchaseData) => { if (value > 999 || value < 0) return; if (Number.isNaN(value)) data.batchValue = 0; else data.batchValue = value; }; + /** + * 일괄 적용 input 값이 업데이트 되었을 때 실행할 함수 + * @param {PurchaseData} data + */ const handleBatchUpdate = (data: PurchaseData) => { for (const room of data.rooms) { if (!room) continue; @@ -475,6 +560,9 @@ export const useCoupon = () => { setPurchaseData(data); }; + /** + * 일괄 적용 checkbox의 값을 변경했을 때 실행할 함수 + */ const handleBatchEditCheckbox = () => { if (!purchaseData) return; const data = { ...purchaseData }; @@ -484,6 +572,10 @@ export const useCoupon = () => { handleBatchUpdate(data); }; + /** + *일괄 적용 input 값이 업데이트 되었을 때 실행할 함수 + * @param {React.ChangeEvent} event + */ const handleChangeBatchValue = ( event: React.ChangeEvent, ) => { @@ -494,6 +586,12 @@ export const useCoupon = () => { handleBatchUpdate(data); }; + /** + * 개별 구매 수량 input 값이 업데이트 되었을 때 실행할 함수 + * @param {React.ChangeEvent} event + * @param {number} couponId + * @param {number} roomId + */ const handleChangeBuyQuantity = ( event: React.ChangeEvent, couponId: number, @@ -515,6 +613,10 @@ export const useCoupon = () => { setPurchaseData(data); }; + /** + * 추가 구매 시 서버에게 request 보낼 데이터를 가공하는 함수 + */ + const processPurchasePostData = () => { const data: PurchaseCouponParams = { accommodationId: Number(accommodationId as string), @@ -553,6 +655,10 @@ export const useCoupon = () => { data.rooms = roomData; return data; }; + + /** + * 구매하기 버튼 클릭 시 실행할 함수 + */ const handlePurchaseButton = () => { Modal.confirm({ content: '쿠폰을 구매하시겠습니까?', diff --git a/src/test/coupon/Coupon.test.tsx b/src/test/coupon/Coupon.test.tsx index bf5d105f..a6d46404 100644 --- a/src/test/coupon/Coupon.test.tsx +++ b/src/test/coupon/Coupon.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Coupon } from '@pages/coupon'; +import { RecoilRoot } from 'recoil'; jest.mock('antd/es/locale/ko_KR', () => ({ locale: () => null, @@ -13,21 +14,25 @@ describe('쿠폰 데이터', () => { /* 기본 동작 */ test('쿠폰 페이지가 렌더링된다', async () => { render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('coupon-header')); }); test('쿠폰 아이템 체크박스가 작동한다', () => { const { container } = render( - - - - - , + + + + + + + , ); const checkbox = container.querySelectorAll( "input[type='checkbox']", @@ -40,11 +45,13 @@ describe('쿠폰 데이터', () => { /* 삭제 */ test('아무것도 선택되지 않았을 때, 선택 삭제를 클릭하면 삭제할 쿠폰을 먼저 선택하세요 라는 메세지가 출력된다', async () => { render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { timeout: 5000, @@ -60,11 +67,13 @@ describe('쿠폰 데이터', () => { test('수정 중 선택 삭제 버튼을 클릭하면 수정 중인 내용을 먼저 저장하세요 라는 메시지가 출력된다', async () => { render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { @@ -86,11 +95,13 @@ describe('쿠폰 데이터', () => { test('수량이 남아있는 쿠폰 삭제 시 수량이 남아있는 쿠폰이 있습니다 라는 메시지가 출력된다', async () => { const { container } = render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { @@ -120,11 +131,13 @@ describe('쿠폰 데이터', () => { /* 수정 */ test('쿠폰 아이템 일일 제한 수량 수정 후 저장버튼 활성화된다.', () => { const { container } = render( - - - - - , + + + + + + + , ); const inputBox = container.querySelectorAll( @@ -140,11 +153,13 @@ describe('쿠폰 데이터', () => { }); test('쿠폰 아이템 일일 제한 수량에 문자 입력 시 block 된다', async () => { render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { @@ -160,11 +175,13 @@ describe('쿠폰 데이터', () => { test('저장 버튼을 클릭하면 저장하시겠습니까? 라는 문구가 출력된다', async () => { render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { @@ -192,11 +209,13 @@ describe('쿠폰 데이터', () => { test('아무것도 선택되지 않았을 때, 추가 구매를 클릭하면 삭제할 쿠폰을 먼저 선택하세요 라는 메세지가 출력된다', async () => { render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { timeout: 5000, @@ -212,11 +231,13 @@ describe('쿠폰 데이터', () => { test('쿠폰 아이템 선택 후 추가 구매 버튼 클릭 시 추가 구매 모달이 출력된다', async () => { const { container } = render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { @@ -236,11 +257,13 @@ describe('쿠폰 데이터', () => { }); test('수량 일괄 적용 클릭 시 수량 일괄 적용 input 값이 구매 쿠폰 input에 적용된다', async () => { const { container } = render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), { @@ -269,11 +292,13 @@ describe('쿠폰 데이터', () => { }); test('추가 구매 input에 문자 입력 시 block 된다', async () => { const { container } = render( - - - - - , + + + + + + + , ); await waitFor(() => screen.findByTestId('table-container'), {