diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java index da7bf0ff3..c89b42d4a 100644 --- a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java @@ -55,6 +55,11 @@ public List findCouponsByCafe(Long ownerId, Long cafe for (CustomerCoupons customerCoupon : customerCoupons) { Coupons coupons = new Coupons(customerCoupon.coupons); VisitHistories visitHistories = findVisitHistories(cafe, customerCoupon); + + if (visitHistories.getVisitCount() == 0) { + continue; + } + // TODO: CustomerCouponStatistics 없애도 될 것 같다. CustomerCouponStatistics customerCouponStatistics = coupons.calculateStatistics(); cafeCustomerFindResultDtos.add( diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java index 49e9bb6f3..0bb93cf6a 100644 --- a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java @@ -8,7 +8,7 @@ import com.stampcrush.backend.entity.visithistory.VisitHistory; import com.stampcrush.backend.exception.CafeNotFoundException; import com.stampcrush.backend.exception.OwnerNotFoundException; -import com.stampcrush.backend.exception.OwnerUnAuthorizationException; +import com.stampcrush.backend.exception.VisitHistoryNotFoundException; import com.stampcrush.backend.repository.cafe.CafeRepository; import com.stampcrush.backend.repository.reward.RewardRepository; import com.stampcrush.backend.repository.user.OwnerRepository; @@ -33,7 +33,7 @@ public List findRewards(Long ownerId, RewardFindDto rewardF List visitHistories = visitHistoryRepository.findByCafeIdAndCustomerId(rewardFindDto.getCafeId(), rewardFindDto.getCustomerId()); if (visitHistories.isEmpty()) { - throw new OwnerUnAuthorizationException("카페의 고객이 아닙니다"); + throw new VisitHistoryNotFoundException("카페의 고객이 아닙니다"); } Cafe cafe = cafeRepository.findById(rewardFindDto.getCafeId()) diff --git a/backend/src/main/java/com/stampcrush/backend/config/RequestLoggingFilter.java b/backend/src/main/java/com/stampcrush/backend/config/RequestLoggingFilter.java index 641bacd92..e97818f0f 100644 --- a/backend/src/main/java/com/stampcrush/backend/config/RequestLoggingFilter.java +++ b/backend/src/main/java/com/stampcrush/backend/config/RequestLoggingFilter.java @@ -4,8 +4,10 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -13,10 +15,16 @@ import java.util.UUID; @Slf4j +@RequiredArgsConstructor @Component public class RequestLoggingFilter extends OncePerRequestFilter { + + private final ThreadPoolTaskExecutor executor; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + System.out.println("????"); + log.info("thread-id : {}, thread-name: {}, pool-size: {}", Thread.currentThread().getId(), Thread.currentThread().getName(), executor.getPoolSize()); MDC.put("requestId", UUID.randomUUID().toString()); MDC.put("requestMethod", request.getMethod()); MDC.put("requestUrl", request.getRequestURI()); diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 4d982d916..607a49bb1 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -3,7 +3,7 @@ + value="%d{HH:mm:ss.SSS} %clr([request-id: %X{requestId:-startup}]) %clr(%X{requestUrl} %X{requestMethod}){green} | %clr(authorization : %X{authorization}){faint} | [%t] %-5level %logger%n %msg}"/> @@ -22,6 +22,17 @@ + + + INFO + ACCEPT + DENY + + + ${CONSOLE_LOG_PATTERN} + + + TRACE @@ -93,11 +104,24 @@ ./logs/info.log - - ${FILE_LOG_PATTERN} + + INFO + ACCEPT + DENY + + + + + + + + + + + yyyy-MM-dd' 'HH:mm:ss.SSS - ./was-logs/%d{yyyy-MM-dd}.%i.log.gz + ./was-info/warn.%d{yyyy-MM-dd}.%i.log.gz 100MB @@ -115,9 +139,10 @@ - + + @@ -125,5 +150,8 @@ + + + diff --git a/backend/src/main/resources/security b/backend/src/main/resources/security index a69bd4c4c..37aa58888 160000 --- a/backend/src/main/resources/security +++ b/backend/src/main/resources/security @@ -1 +1 @@ -Subproject commit a69bd4c4ce1784d9b2e482a2764253bae6b81a11 +Subproject commit 37aa5888896f47fed11272d851692b85fb5f69f6 diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardFindAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardFindAcceptanceTest.java index 2c9fe2e2c..20622e00c 100644 --- a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardFindAcceptanceTest.java +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardFindAcceptanceTest.java @@ -12,6 +12,7 @@ import com.stampcrush.backend.repository.user.OwnerRepository; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import org.apache.http.HttpStatus; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -72,7 +73,7 @@ public class ManagerRewardFindAcceptanceTest extends AcceptanceTest { ExtractableResponse response = 리워드_목록_조회(notOwner, cafeId, customer.getId()); // then - assertThat(response.statusCode()).isEqualTo(401); + assertThat(response.statusCode()).isEqualTo(HttpStatus.SC_UNAUTHORIZED); } @Test @@ -97,7 +98,7 @@ public class ManagerRewardFindAcceptanceTest extends AcceptanceTest { ExtractableResponse response = 리워드_목록_조회(owner, cafeId, notMyCustomer.getId()); // then - assertThat(response.statusCode()).isEqualTo(401); + assertThat(response.statusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND); } // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 diff --git a/frontend/package.json b/frontend/package.json index ab1ec4355..b652b42e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,12 +4,12 @@ "main": "index.js", "license": "MIT", "dependencies": { + "@react-icons/all-files": "^4.1.0", "@tanstack/react-query": "^4.29.25", "@tanstack/react-query-devtools": "^4.29.25", "react": "^18.2.0", "react-daum-postcode": "^3.1.3", "react-dom": "^18.2.0", - "react-icons": "^4.10.1", "react-router-dom": "^6.14.1", "styled-components": "^6.0.2" }, @@ -32,6 +32,7 @@ "@storybook/react": "^7.0.27", "@storybook/react-webpack5": "^7.0.27", "@storybook/testing-library": "^0.0.14-next.2", + "@svgr/webpack": "^8.1.0", "@tanstack/eslint-plugin-query": "^4.29.25", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index fdba52dec..f8e93658f 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,6 +1,5 @@ import { RouterProvider, createBrowserRouter, Outlet } from 'react-router-dom'; import { ROUTER_PATH } from './constants'; - import CustomerList from './pages/Admin/CustomerList'; import ManageCafe from './pages/Admin/ManageCafe'; import CouponList from './pages/Customer/CouponList'; @@ -23,7 +22,7 @@ import Auth from './pages/Auth'; import AdminLogin from './pages/Admin/AdminLogin'; import AdminAuth from './pages/Admin/AdminAuth'; import TemplateCouponDesign from './pages/Admin/CouponDesign/TemplateCouponDesign'; -import InputPhoneNumber from './pages/Admin/InputPhoneNumber'; +import InputPhoneNumber from './pages/Customer/InputPhoneNumber'; import CustomerNotFound from './pages/NotFound/CustomerNotFound'; import PrivateProvider from './provider/PrivateProvider'; diff --git a/frontend/src/api/get.ts b/frontend/src/api/get.ts index d8e61d37b..7ca9f2fdb 100644 --- a/frontend/src/api/get.ts +++ b/frontend/src/api/get.ts @@ -23,9 +23,34 @@ import { OAuthJWTRes, CustomerProfileRes, RewardRes, + CustomerRegisterTypeRes, } from '../types/api/response'; import { CouponDesign } from '../types/domain/coupon'; +// 인증 header가 필요없는 api +export const getAdminOAuthToken = async ( + { params }: QueryReq, + init: RequestInit = {}, +) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/login/${params.resourceServer}/token?code=${params.code}`, + init, + ); +}; + +export const getOAuthToken = async ( + { params }: QueryReq, + init: RequestInit = {}, +) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/login/${params.resourceServer}/token?code=${params.code}`, + init, + ); +}; + +// 사장모드 api export const getCafe = async () => { return await api.get('/admin/cafes', ownerHeader()); }; @@ -67,6 +92,26 @@ export const getCouponSamples = async ({ params }: QueryReq ); }; +export const getCouponDesign = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/coupon-setting?cafe-id=${params.cafeId}`, + ownerHeader(), + ); +}; + +export const getCurrentCouponDesign = async ({ + params, +}: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/coupon-setting/${params.couponId}?cafe-id=${params.cafeId}`, + ownerHeader(), + ); +}; + +// 고객모드 api + export const getCoupons = async () => { return await api.get('/coupons', customerHeader()); }; @@ -85,46 +130,14 @@ export const getStampHistories = async () => { return await api.get('/stamp-histories', customerHeader()); }; -export const getAdminOAuthToken = async ( - { params }: QueryReq, - init: RequestInit = {}, -) => { - if (!params) throw new Error(PARAMS_ERROR_MESSAGE); - return await api.get( - `/admin/login/${params.resourceServer}/token?code=${params.code}`, - init, - ); -}; - -export const getOAuthToken = async ( - { params }: QueryReq, - init: RequestInit = {}, -) => { - if (!params) throw new Error(PARAMS_ERROR_MESSAGE); - return await api.get( - `/login/${params.resourceServer}/token?code=${params.code}`, - init, - ); -}; - export const getCustomerProfile = async () => { return await api.get('/profiles', customerHeader()); }; -export const getCouponDesign = async ({ params }: QueryReq) => { +export const getCustomerRegisterType = async ({ params }: QueryReq) => { if (!params) throw new Error(PARAMS_ERROR_MESSAGE); - return await api.get( - `/admin/coupon-setting?cafe-id=${params.cafeId}`, - ownerHeader(), - ); -}; - -export const getCurrentCouponDesign = async ({ - params, -}: QueryReq) => { - if (!params) throw new Error(PARAMS_ERROR_MESSAGE); - return await api.get( - `/admin/coupon-setting/${params.couponId}?cafe-id=${params.cafeId}`, - ownerHeader(), + return await api.get( + `/profiles/search?phone-number=${params.phoneNumber}`, + customerHeader(), ); }; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index d5b027e64..0418cf2c3 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -9,7 +9,19 @@ const request = async (path: string, init?: RequestInit) => { }, }); - if (!response.ok) throw new Error(response.status.toString()); + if (!response.ok) { + if (response.status === 401) { + const isAdminRequest = path.startsWith('/admin'); + const expiredTokenKey = isAdminRequest ? 'admin-login-token' : 'login-token'; + localStorage.setItem(expiredTokenKey, ''); + const redirectUrl = `${location.origin}${isAdminRequest ? '/admin' : ''}`; + if (!location.pathname.endsWith('/login')) { + location.href = `${redirectUrl}/login`; + } + } + + throw new Error(response.status.toString()); + } return response; }; diff --git a/frontend/src/api/post.ts b/frontend/src/api/post.ts index c5c90f1f2..b14a71263 100644 --- a/frontend/src/api/post.ts +++ b/frontend/src/api/post.ts @@ -11,6 +11,7 @@ import { CafeIdParams, CafeRegisterReqBody, IsFavoritesReqBody, + CustomerLinkDataReqBody, } from '../types/api/request'; export const postEarnStamp = async ({ @@ -81,3 +82,7 @@ export const postUploadImage = async (file: File) => { export const postCustomerPhoneNumber = async ({ body }: MutateReq) => { return await api.post('/profiles/phone-number', customerHeader(), body); }; + +export const postCustomerLinkData = async ({ body }: MutateReq) => { + return await api.post('/profiles/link-data', customerHeader(), body); +}; diff --git a/frontend/src/assets/check.svg b/frontend/src/assets/check.svg index 9c2aab7f8..e34f78dcc 100644 --- a/frontend/src/assets/check.svg +++ b/frontend/src/assets/check.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/src/assets/icons/book_open_text.svg b/frontend/src/assets/icons/book_open_text.svg new file mode 100644 index 000000000..1162774b1 --- /dev/null +++ b/frontend/src/assets/icons/book_open_text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/buildings.svg b/frontend/src/assets/icons/buildings.svg new file mode 100644 index 000000000..51dd28a9b --- /dev/null +++ b/frontend/src/assets/icons/buildings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/ci_circle_alert.svg b/frontend/src/assets/icons/ci_circle_alert.svg new file mode 100644 index 000000000..85759cc88 --- /dev/null +++ b/frontend/src/assets/icons/ci_circle_alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/fa_location_dot.svg b/frontend/src/assets/icons/fa_location_dot.svg new file mode 100644 index 000000000..5c0ab7564 --- /dev/null +++ b/frontend/src/assets/icons/fa_location_dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/gift.svg b/frontend/src/assets/icons/gift.svg new file mode 100644 index 000000000..7710e60de --- /dev/null +++ b/frontend/src/assets/icons/gift.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/md_outline_photo_size_select_large.svg b/frontend/src/assets/icons/md_outline_photo_size_select_large.svg new file mode 100644 index 000000000..8d81e7020 --- /dev/null +++ b/frontend/src/assets/icons/md_outline_photo_size_select_large.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/stamp.svg b/frontend/src/assets/icons/stamp.svg new file mode 100644 index 000000000..8a7175ad0 --- /dev/null +++ b/frontend/src/assets/icons/stamp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/user_list.svg b/frontend/src/assets/icons/user_list.svg new file mode 100644 index 000000000..a79e6ec0a --- /dev/null +++ b/frontend/src/assets/icons/user_list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/index.ts b/frontend/src/assets/index.ts index 36cb92845..3ea1c4c31 100644 --- a/frontend/src/assets/index.ts +++ b/frontend/src/assets/index.ts @@ -5,3 +5,11 @@ export { default as KakaoLoginButton } from './kakao_login.svg'; export { default as AdminKakaoLoginButton } from './kakao_login_large_wide.png'; export { default as CustomerKakaoLoginButton } from './kakao_login_large_narrow.png'; export { default as LoginLogo } from './login_logo.svg'; +export { default as PiBookOpenTextLight } from './icons/book_open_text.svg'; +export { default as PiBuildingsLight } from './icons/buildings.svg'; +export { default as PiUserListLight } from './icons/user_list.svg'; +export { default as PiGiftLight } from './icons/gift.svg'; +export { default as PiStampLight } from './icons/stamp.svg'; +export { default as MdOutlinePhotoSizeSelectLarge } from './icons/md_outline_photo_size_select_large.svg'; +export { default as CiCircleAlert } from './icons/ci_circle_alert.svg'; +export { default as FaLocationDot } from './icons/fa_location_dot.svg'; diff --git a/frontend/src/assets/login_logo.svg b/frontend/src/assets/login_logo.svg index 792596b0b..5c9f10f03 100644 --- a/frontend/src/assets/login_logo.svg +++ b/frontend/src/assets/login_logo.svg @@ -1,23 +1,23 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/Alert/index.tsx b/frontend/src/components/Alert/index.tsx index 18fee9477..8ada55dcb 100644 --- a/frontend/src/components/Alert/index.tsx +++ b/frontend/src/components/Alert/index.tsx @@ -1,6 +1,6 @@ import ReactDOM from 'react-dom'; import { AlertContainer, BackDrop, OptionContainer, OptionWrapper, TextContainer } from './style'; -import { CiCircleAlert } from 'react-icons/ci'; +import { CiCircleAlert } from '../../assets'; export interface AlertProps { text: string; @@ -16,7 +16,7 @@ const Alert = ({ text, rightOption, leftOption, onClickLeft, onClickRight }: Ale - + {text} diff --git a/frontend/src/components/Header/SubHeader/index.tsx b/frontend/src/components/Header/SubHeader/index.tsx index 3f0716ec6..21b3c83e7 100644 --- a/frontend/src/components/Header/SubHeader/index.tsx +++ b/frontend/src/components/Header/SubHeader/index.tsx @@ -1,24 +1,19 @@ import { ArrowIconWrapper, HeaderContainer } from './style'; -import { useNavigate } from 'react-router-dom'; -import { ROUTER_PATH } from '../../../constants'; -import { BiArrowBack } from 'react-icons/bi'; +import { BiArrowBack } from '@react-icons/all-files/bi/BiArrowBack'; interface SubHeaderProps { title: string; + onClickBack?: () => void; } -const SubHeader = ({ title }: SubHeaderProps) => { - const navigate = useNavigate(); - - const navigateMyPage = () => { - navigate(ROUTER_PATH.myPage); - }; - +const SubHeader = ({ title, onClickBack }: SubHeaderProps) => { return ( - - - + {onClickBack && ( + + + + )} {title} ); diff --git a/frontend/src/components/LoadingSpinner/CustomerLoading/index.tsx b/frontend/src/components/LoadingSpinner/CustomerLoading/index.tsx index 6a54f6a99..faea50a5c 100644 --- a/frontend/src/components/LoadingSpinner/CustomerLoading/index.tsx +++ b/frontend/src/components/LoadingSpinner/CustomerLoading/index.tsx @@ -1,4 +1,4 @@ -import { FaStamp } from 'react-icons/fa'; +import { FaStamp } from '@react-icons/all-files/fa/FaStamp'; import { LoadingContainer } from './style'; const CustomerLoading = () => { diff --git a/frontend/src/components/LoadingSpinner/CustomerLoadingSpinner/index.tsx b/frontend/src/components/LoadingSpinner/CustomerLoadingSpinner/index.tsx index c3c57fb96..91f1c3d81 100644 --- a/frontend/src/components/LoadingSpinner/CustomerLoadingSpinner/index.tsx +++ b/frontend/src/components/LoadingSpinner/CustomerLoadingSpinner/index.tsx @@ -1,10 +1,10 @@ -import loadingSpinner from '../../../assets/c_loading.svg'; +import LoadingSpinner from '../../../assets/c_loading.svg'; import { CustomerLoadingContainer } from '../style'; const CustomerLoadingSpinner = () => { return ( - + ); }; diff --git a/frontend/src/components/LoadingSpinner/index.tsx b/frontend/src/components/LoadingSpinner/index.tsx index 95eed9236..3fa3e8859 100644 --- a/frontend/src/components/LoadingSpinner/index.tsx +++ b/frontend/src/components/LoadingSpinner/index.tsx @@ -1,10 +1,10 @@ -import loadingSpinner from '../../assets/loading_spinner.svg'; +import LoadingSpinnerSVG from '../../assets/loading_spinner.svg'; import { LoadingContainer } from './style'; const LoadingSpinner = () => { return ( - 로딩 중입니다. + ); }; diff --git a/frontend/src/components/SearchBar/index.tsx b/frontend/src/components/SearchBar/index.tsx index ccffd3fb5..1c0be5005 100644 --- a/frontend/src/components/SearchBar/index.tsx +++ b/frontend/src/components/SearchBar/index.tsx @@ -1,5 +1,5 @@ import { BaseInput, Container, SearchButton } from './style'; -import { BsSearch } from 'react-icons/bs'; +import { BsSearch } from '@react-icons/all-files/bs/BsSearch'; import { ChangeEvent, FormEvent, InputHTMLAttributes } from 'react'; export interface SearchBarProps extends InputHTMLAttributes { diff --git a/frontend/src/components/SideBar/index.tsx b/frontend/src/components/SideBar/index.tsx index 8400aca13..094ed3663 100644 --- a/frontend/src/components/SideBar/index.tsx +++ b/frontend/src/components/SideBar/index.tsx @@ -14,13 +14,13 @@ import { } from './style'; import { useEffect, useState } from 'react'; import { - PiUserListLight, - PiBuildingsLight, - PiStampLight, PiBookOpenTextLight, + PiBuildingsLight, PiGiftLight, -} from 'react-icons/pi'; -import { IoIosLogOut } from 'react-icons/io'; + PiStampLight, + PiUserListLight, +} from '../../assets'; +import { IoIosLogOut } from '@react-icons/all-files/io/IoIosLogOut'; import { ROUTER_PATH } from '../../constants'; import { Option } from '../../types/utils'; @@ -36,11 +36,11 @@ const SIDE_BAR_OPTIONS: Option[] = [ const SIDEBAR_ICONS = [ <>, - , - , - , - , - , + , + , + , + , + , <>, ]; @@ -106,11 +106,15 @@ const SideBar = () => { navigate(SIDE_BAR_OPTIONS[index].value); }; + const navigateCustomerList = () => { + navigate(ROUTER_PATH.customerList); + }; + return ( - + diff --git a/frontend/src/components/SideBar/style.tsx b/frontend/src/components/SideBar/style.tsx index 1ab3db043..6b5ac09ae 100644 --- a/frontend/src/components/SideBar/style.tsx +++ b/frontend/src/components/SideBar/style.tsx @@ -25,7 +25,6 @@ export const LogoImgWrapper = styled.button` display: flex; align-self: flex-start; background: transparent; - width: 150px; padding: 0 0 40px 40px; cursor: pointer; diff --git a/frontend/src/components/Template/CustomerTemplate/BottomTabBar/index.tsx b/frontend/src/components/Template/CustomerTemplate/BottomTabBar/index.tsx index 7c0174b75..d9b1085dd 100644 --- a/frontend/src/components/Template/CustomerTemplate/BottomTabBar/index.tsx +++ b/frontend/src/components/Template/CustomerTemplate/BottomTabBar/index.tsx @@ -1,6 +1,8 @@ import { Link, useLocation } from 'react-router-dom'; import { TabBarContainer, TapBarItem } from './style'; -import { AiOutlineHome, AiOutlineGift, AiOutlineUser } from 'react-icons/ai'; +import { AiOutlineHome } from '@react-icons/all-files/ai/AiOutlineHome'; +import { AiOutlineGift } from '@react-icons/all-files/ai/AiOutlineGift'; +import { AiOutlineUser } from '@react-icons/all-files/ai/AiOutlineUser'; import { ROUTER_PATH } from '../../../../constants'; import { RouterPath } from '../../../../types/utils'; diff --git a/frontend/src/components/Template/index.tsx b/frontend/src/components/Template/index.tsx index 3e899ac6f..541dc6895 100644 --- a/frontend/src/components/Template/index.tsx +++ b/frontend/src/components/Template/index.tsx @@ -2,7 +2,7 @@ import { PropsWithChildren } from 'react'; import SideBar from '../SideBar'; import { BaseTemplate, Footer, PageContainer, SideBarWrapper } from './style'; -import { AiOutlineMail } from 'react-icons/ai'; +import { AiOutlineMail } from '@react-icons/all-files/ai/AiOutlineMail'; const Template = ({ children }: PropsWithChildren) => { return ( diff --git a/frontend/src/custom.d.ts b/frontend/src/custom.d.ts index 9e0f7bcc7..cc0cf38ab 100644 --- a/frontend/src/custom.d.ts +++ b/frontend/src/custom.d.ts @@ -1,5 +1,10 @@ declare module '*.png'; -declare module '*.svg'; declare module '*.ttf'; declare module '*.gif'; declare module '*.jpg'; + +declare module '*.svg' { + import React = require('react'); + const SVG: React.VFC>; + export default SVG; +} diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 105561069..8db1c67f8 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -11,6 +11,7 @@ import { usedCustomerRewards, customerRewards, stampHistorys, + customerRegisterType, } from './mockData'; const customerList = [...customers]; @@ -347,4 +348,16 @@ export const handlers = [ ctx.json({ profile: { id: 10, nickname: '강영민', phoneNumber: null, email: null } }), ); }), + + rest.get('/customers/profiles/search', (req, res, ctx) => { + const phoneNumber = req.url.searchParams.get('phone-number'); + + if (phoneNumber === '01011111111') + return res(ctx.status(200), ctx.json({ customers: [customerRegisterType[0]] })); + + if (phoneNumber === '01022222222') + return res(ctx.status(200), ctx.json({ customers: [customerRegisterType[1]] })); + + return res(ctx.status(200), ctx.json({ customers: [] })); + }), ]; diff --git a/frontend/src/mocks/mockData.ts b/frontend/src/mocks/mockData.ts index 18778ea31..18bf3bca1 100644 --- a/frontend/src/mocks/mockData.ts +++ b/frontend/src/mocks/mockData.ts @@ -569,3 +569,8 @@ export const stampHistorys = { }, ], }; + +export const customerRegisterType = [ + { id: 1, nickname: '가입유저', phoneNumber: '01011111111', registerType: 'register' }, + { id: 2, nickname: '임시유저', phoneNumber: '01022222222', registerType: 'temporary' }, +]; diff --git a/frontend/src/pages/Admin/AdminLogin/CheckItem.tsx b/frontend/src/pages/Admin/AdminLogin/CheckItem.tsx index 711457018..395a326ea 100644 --- a/frontend/src/pages/Admin/AdminLogin/CheckItem.tsx +++ b/frontend/src/pages/Admin/AdminLogin/CheckItem.tsx @@ -1,5 +1,5 @@ -import CheckIconSrc from '../../../assets/check.svg'; -import { CheckIcon, CheckItemContainer } from './style'; +import CheckIconSVG from '../../../assets/check.svg'; +import { CheckItemContainer } from './style'; interface CheckItemProps { text: string; @@ -8,7 +8,7 @@ interface CheckItemProps { const CheckItem = ({ text }: CheckItemProps) => { return ( - + {text} ); diff --git a/frontend/src/pages/Admin/AdminLogin/index.tsx b/frontend/src/pages/Admin/AdminLogin/index.tsx index 7fa807893..451857934 100644 --- a/frontend/src/pages/Admin/AdminLogin/index.tsx +++ b/frontend/src/pages/Admin/AdminLogin/index.tsx @@ -3,7 +3,6 @@ import { BASE_URL, ROUTER_PATH } from '../../../constants'; import { Container, KakaoLoginImg, - LogoImg, LoginContent, BackgroundImg, Title, @@ -21,8 +20,8 @@ const AdminLogin = () => { return ( - - 안녕하세요 사장님!:) + + 안녕하세요 사장님! :) 스탬프크러쉬입니다. 쿠폰 관리, 이제는 온라인으로 만나보세요. diff --git a/frontend/src/pages/Admin/AdminLogin/style.tsx b/frontend/src/pages/Admin/AdminLogin/style.tsx index 86e68f103..13eb194de 100644 --- a/frontend/src/pages/Admin/AdminLogin/style.tsx +++ b/frontend/src/pages/Admin/AdminLogin/style.tsx @@ -65,6 +65,10 @@ export const CheckItemContainer = styled.div` text-align: center; color: #2a1d1f; font-weight: 300; + + svg { + margin-right: 10px; + } `; export const CheckIcon = styled.img` diff --git a/frontend/src/pages/Admin/CustomerList/Customers/index.tsx b/frontend/src/pages/Admin/CustomerList/Customers/index.tsx index c8c1805c5..99b6b3333 100644 --- a/frontend/src/pages/Admin/CustomerList/Customers/index.tsx +++ b/frontend/src/pages/Admin/CustomerList/Customers/index.tsx @@ -12,13 +12,13 @@ import { } from './style'; interface CustomersProps { - customersData: CustomersRes; + customers: Customer[]; } -const Customers = ({ customersData }: CustomersProps) => { +const Customers = ({ customers }: CustomersProps) => { return ( - {customersData.customers.map( + {customers.map( ({ id, nickname, diff --git a/frontend/src/pages/Admin/CustomerList/hooks/useGetCustomers.ts b/frontend/src/pages/Admin/CustomerList/hooks/useGetCustomers.ts new file mode 100644 index 000000000..c9b6c87e1 --- /dev/null +++ b/frontend/src/pages/Admin/CustomerList/hooks/useGetCustomers.ts @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; +import { getCustomers } from '../../../../api/get'; +import { INVALID_CAFE_ID } from '../../../../constants'; +import { CustomersRes } from '../../../../types/api/response'; +import { Customer } from '../../../../types/domain/customer'; +import { Option } from '../../../../types/utils'; + +export interface CustomerOrderOption extends Omit { + key: keyof Customer; +} + +const useGetCustomers = (cafeId: number, orderOption: CustomerOrderOption) => { + return useQuery({ + queryKey: ['customers'], + queryFn: () => + getCustomers({ + params: { + cafeId, + }, + }), + select: (data) => + data.customers.sort((a, b) => { + if (a[orderOption.key] === b[orderOption.key]) + return a['nickname'] > b['nickname'] ? 1 : -1; + return a[orderOption.key] < b[orderOption.key] ? 1 : -1; + }), + enabled: cafeId !== INVALID_CAFE_ID, + }); +}; + +export default useGetCustomers; diff --git a/frontend/src/pages/Admin/CustomerList/index.tsx b/frontend/src/pages/Admin/CustomerList/index.tsx index 2c306afc0..0714bb5d4 100644 --- a/frontend/src/pages/Admin/CustomerList/index.tsx +++ b/frontend/src/pages/Admin/CustomerList/index.tsx @@ -1,61 +1,25 @@ import { CustomerContainer, Container, EmptyCustomers } from './style'; import Text from '../../../components/Text'; -import { useEffect, useState } from 'react'; -import SearchBar from '../../../components/SearchBar'; -import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; import SelectBox from '../../../components/SelectBox'; -import { getCustomers } from '../../../api/get'; -import { CUSTOMERS_ORDER_OPTIONS, INVALID_CAFE_ID, ROUTER_PATH } from '../../../constants'; +import { CUSTOMERS_ORDER_OPTIONS } from '../../../constants'; import LoadingSpinner from '../../../components/LoadingSpinner'; import Customers from './Customers'; import { useRedirectRegisterPage } from '../../../hooks/useRedirectRegisterPage'; -import { useNavigate } from 'react-router-dom'; -import { Customer } from '../../../types/domain/customer'; -import { CustomersRes } from '../../../types/api/response'; +import useGetCustomers, { CustomerOrderOption } from './hooks/useGetCustomers'; const CustomerList = () => { - const navigate = useNavigate(); const cafeId = useRedirectRegisterPage(); - const [searchWord, setSearchWord] = useState(''); - const [orderOption, setOrderOption] = useState({ key: 'stampCount', value: '스탬프순' }); - const orderCustomer = (customers: Customer[]) => { - customers.sort((a: Customer, b: Customer) => { - if (a[orderOption.key as keyof Customer] === b[orderOption.key as keyof Customer]) { - return a['nickname'] > b['nickname'] ? 1 : -1; - } - return a[orderOption.key as keyof Customer] < b[orderOption.key as keyof Customer] ? 1 : -1; - }); - }; - - const { data, status } = useQuery({ - queryKey: ['customers'], - queryFn: () => - getCustomers({ - params: { - cafeId, - }, - }), - onSuccess: (data) => { - orderCustomer(data.customers); - }, - enabled: cafeId !== INVALID_CAFE_ID, + const [orderOption, setOrderOption] = useState({ + key: 'stampCount', + value: '스탬프순', }); - - useEffect(() => { - if ( - localStorage.getItem('admin-login-token') === '' || - !localStorage.getItem('admin-login-token') - ) - navigate(ROUTER_PATH.adminLogin); - if (status === 'success' && data.customers.length !== 0) { - orderCustomer(data.customers); - } - }, [orderOption]); + const { data: customers, status } = useGetCustomers(cafeId, orderOption as CustomerOrderOption); if (status === 'loading') return ; if (status === 'error') return Error; - if (data.customers.length === 0) + if (customers.length === 0) return ( 내 고객 목록 @@ -66,24 +30,17 @@ const CustomerList = () => { ); - const searchCustomer = () => { - if (searchWord === '') return; - - // TODO: 추후에 백엔드와 검색 기능 토의 후 수정 예정 - }; - return ( 내 고객 목록 - - + ); }; diff --git a/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx b/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx index ed75ec9b8..cb28eca84 100644 --- a/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx +++ b/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx @@ -1,6 +1,16 @@ import Dialpad from './Dialpad'; -import { Container, IconWrapper, PageContainer, PrivacyBox, Title } from './style'; -import { IoIosArrowBack } from 'react-icons/io'; +import { IoIosArrowBack } from '@react-icons/all-files/io/IoIosArrowBack'; +import { + Container, + IconWrapper, + PageContainer, + PrivacyBox, + RowContainer, + TableItem, + TableTitleItem, + Title, +} from './style'; + import { useNavigate } from 'react-router-dom'; import { ROUTER_PATH } from '../../../constants'; import { useRedirectRegisterPage } from '../../../hooks/useRedirectRegisterPage'; @@ -25,8 +35,24 @@ const EnterPhoneNumber = () => { -

개인정보 제공동의

-

전화번호를 입력하시면 개인정보 제공에 동의하시는 것으로 간주됩니다.

+

개인정보 수집 및 이용동의

+

+ 스탬프크러쉬 서비스 회원가입, 고지사항 전달 등을 위해 아래와 같이 개인정보를 수집 및 + 이용합니다.
전화번호를 입력하시면 개인정보 수집 및 이용에 동의하시는 것으로 + 간주됩니다. +

+ + 수집 목적 + 수집 항목 + 수집 근거 + + + + 회원 식별 및 회원제 서비스 제공과
서비스 변경사항 및 고지사항 전달 +
+ 전화번호 + 개인정보 보호법 제 15조 1항 +
diff --git a/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx b/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx index fa2a2748b..e487836a1 100644 --- a/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx +++ b/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx @@ -31,18 +31,19 @@ export const Container = styled.div` `; export const PrivacyBox = styled.section` - padding: 50px 10%; + padding: 50px 5%; width: 53%; & > p { font-size: 16px; line-height: 24px; + margin-bottom: 20px; } & > h1 { font-size: 24px; font-weight: 700; - margin-bottom: 15px; + margin-bottom: 30px; } @media screen and (max-width: 700px) { @@ -50,6 +51,40 @@ export const PrivacyBox = styled.section` } `; +export const RowContainer = styled.div` + display: grid; + grid-template-columns: 2.8fr 1.2fr 2fr; + grid-template-rows: 1; + border: 1px solid #888; + width: 75%; + + &:last-child { + border-top: none; + } +`; + +export const TableTitleItem = styled.div` + display: flex; + justify-content: center; + align-items: center; + background: ${({ theme }) => theme.colors.gray200}; + font-size: 12px; + width: 100%; + height: 30px; +`; + +export const TableItem = styled.div` + display: flex; + justify-content: center; + align-items: center; + + color: black; + width: 100%; + padding: 10px 10px; + font-size: 12px; + line-height: 24px; +`; + export const PageContainer = styled.div` width: 100vw; height: 87vh; diff --git a/frontend/src/pages/Admin/InputPhoneNumber/index.tsx b/frontend/src/pages/Admin/InputPhoneNumber/index.tsx deleted file mode 100644 index 7466eb6ac..000000000 --- a/frontend/src/pages/Admin/InputPhoneNumber/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Input } from '../../../components/Input'; -import { Container } from './style'; -import { LoginLogo } from '../../../assets'; -import Button from '../../../components/Button'; -import { ChangeEvent, useEffect, useState } from 'react'; -import { parsePhoneNumber } from '../../../utils'; -import { useMutation } from '@tanstack/react-query'; -import { postCustomerPhoneNumber } from '../../../api/post'; -import { useNavigate } from 'react-router-dom'; -import { ROUTER_PATH } from '../../../constants'; -import { useCustomerProfile } from '../../../hooks/useCustomerProfile'; - -const InputPhoneNumber = () => { - const { customerProfile } = useCustomerProfile(); - const navigate = useNavigate(); - - useEffect(() => { - if (customerProfile?.profile.phoneNumber) navigate(ROUTER_PATH.couponList); - }, [customerProfile]); - - const [phoneNumber, setPhoneNumber] = useState(''); - const { mutate } = useMutation({ - mutationFn: () => postCustomerPhoneNumber({ body: { phoneNumber } }), - onSuccess: () => { - navigate(ROUTER_PATH.couponList); - }, - }); - - const inputPhoneNumber = (e: ChangeEvent) => { - const { value } = e.target; - const removeHyphenPhoneNumber = value.replace(/-/g, ''); - - setPhoneNumber(removeHyphenPhoneNumber); - }; - - const submitPhoneNumber = () => { - if ( - (phoneNumber.startsWith('02') && phoneNumber.length !== (11 || 12)) || - (!phoneNumber.startsWith('02') && phoneNumber.length !== 11) - ) { - alert('전화번호를 정확하게 입력해주세요.'); - return; - } - - mutate(); - }; - - return ( - - 스탬프크러쉬 로고 - - - - ); -}; - -export default InputPhoneNumber; diff --git a/frontend/src/pages/Admin/InputPhoneNumber/style.tsx b/frontend/src/pages/Admin/InputPhoneNumber/style.tsx deleted file mode 100644 index d12150ab2..000000000 --- a/frontend/src/pages/Admin/InputPhoneNumber/style.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import styled from 'styled-components'; - -export const Container = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100%; - margin: 0 auto; - - div > div { - margin-top: 100px; - } - - div > input { - width: 300px; - margin-bottom: 40px; - } -`; diff --git a/frontend/src/pages/Admin/ManageCafe/components/CafeImageUpload/index.tsx b/frontend/src/pages/Admin/ManageCafe/components/CafeImageUpload/index.tsx index d7c953655..b9411884b 100644 --- a/frontend/src/pages/Admin/ManageCafe/components/CafeImageUpload/index.tsx +++ b/frontend/src/pages/Admin/ManageCafe/components/CafeImageUpload/index.tsx @@ -1,4 +1,4 @@ -import { AiOutlineUpload } from 'react-icons/ai'; +import { AiOutlineUpload } from '@react-icons/all-files/ai/AiOutlineUpload'; import { ImageUpLoadInput, ImageUpLoadInputLabel, diff --git a/frontend/src/pages/Admin/ManageCafe/components/PreviewContent/index.tsx b/frontend/src/pages/Admin/ManageCafe/components/PreviewContent/index.tsx index 60929ae8e..445352d1d 100644 --- a/frontend/src/pages/Admin/ManageCafe/components/PreviewContent/index.tsx +++ b/frontend/src/pages/Admin/ManageCafe/components/PreviewContent/index.tsx @@ -1,10 +1,11 @@ import Text from '../../../../../components/Text'; -import { FaRegClock, FaPhoneAlt } from 'react-icons/fa'; -import { FaLocationDot } from 'react-icons/fa6'; +import { FaRegClock } from '@react-icons/all-files/fa/FaRegClock'; +import { FaPhoneAlt } from '@react-icons/all-files/fa/FaPhoneAlt'; import { parsePhoneNumber, parseTime } from '../../../../../utils'; import { Cafe } from '../../../../../types/domain/cafe'; import { Time } from '../../../../../types/utils'; import { PreviewContentContainer } from './style'; +import { FaLocationDot } from '../../../../../assets'; interface PreviewContentProps { openTime: Time; @@ -25,7 +26,7 @@ const PreviewContent = ({ openTime, closeTime, phoneNumber, cafeInfo }: PreviewC {parsePhoneNumber(phoneNumber)} - + {`${cafeInfo.roadAddress} ${cafeInfo.detailAddress}`} diff --git a/frontend/src/pages/Admin/ModifyCouponPolicy/CreatedType/index.tsx b/frontend/src/pages/Admin/ModifyCouponPolicy/CreatedType/index.tsx index 6d433936a..4ccb6202d 100644 --- a/frontend/src/pages/Admin/ModifyCouponPolicy/CreatedType/index.tsx +++ b/frontend/src/pages/Admin/ModifyCouponPolicy/CreatedType/index.tsx @@ -9,9 +9,9 @@ import { } from './style'; import { Spacing } from '../../../../style/layout/common'; import Text from '../../../../components/Text'; -import { GrSelect } from 'react-icons/gr'; -import { MdOutlinePhotoSizeSelectLarge } from 'react-icons/md'; +import { GrSelect } from '@react-icons/all-files/gr/GrSelect'; import { CouponCreated } from '../../../../types/domain/coupon'; +import { MdOutlinePhotoSizeSelectLarge } from '../../../../assets'; interface CreatedTypeProps { value: CouponCreated; @@ -58,7 +58,7 @@ const CreatedType = ({ value, setValue }: CreatedTypeProps) => { 커스텀할게요. - + diff --git a/frontend/src/pages/Admin/ModifyCouponPolicy/ExpirePeriod/index.tsx b/frontend/src/pages/Admin/ModifyCouponPolicy/ExpirePeriod/index.tsx index 9eea5c929..31b18f3b5 100644 --- a/frontend/src/pages/Admin/ModifyCouponPolicy/ExpirePeriod/index.tsx +++ b/frontend/src/pages/Admin/ModifyCouponPolicy/ExpirePeriod/index.tsx @@ -4,7 +4,7 @@ import SelectBox from '../../../../components/SelectBox'; import { EXPIRE_DATE_OPTIONS } from '../../../../constants'; import { Dispatch, SetStateAction } from 'react'; import { WarningText } from './style'; -import { AiFillWarning } from 'react-icons/ai'; +import { AiFillWarning } from '@react-icons/all-files/ai/AiFillWarning'; import { Option } from '../../../../types/utils'; interface ExpirePeriodProps { diff --git a/frontend/src/pages/Customer/CouponList/CafeInfo/index.tsx b/frontend/src/pages/Customer/CouponList/CafeInfo/index.tsx index ac088e1af..9b85936ec 100644 --- a/frontend/src/pages/Customer/CouponList/CafeInfo/index.tsx +++ b/frontend/src/pages/Customer/CouponList/CafeInfo/index.tsx @@ -1,5 +1,6 @@ import Color from 'color-thief-react'; -import { AiFillStar, AiOutlineStar } from 'react-icons/ai'; +import { AiFillStar } from '@react-icons/all-files/ai/AiFillStar'; +import { AiOutlineStar } from '@react-icons/all-files/ai/AiOutlineStar'; import ProgressBar from '../../../../components/ProgressBar'; import { addGoogleProxyUrl } from '../../../../utils'; import { diff --git a/frontend/src/pages/Customer/CouponList/CouponDetail/index.tsx b/frontend/src/pages/Customer/CouponList/CouponDetail/index.tsx index fbc793c5a..a9762527a 100644 --- a/frontend/src/pages/Customer/CouponList/CouponDetail/index.tsx +++ b/frontend/src/pages/Customer/CouponList/CouponDetail/index.tsx @@ -6,11 +6,14 @@ import { ContentContainer, CouponDetailContainer, DeleteButton, + DetailItem, OverviewContainer, } from './style'; -import { BiArrowBack } from 'react-icons/bi'; -import { FaRegClock, FaPhoneAlt, FaRegBell, FaRegTrashAlt } from 'react-icons/fa'; -import { FaLocationDot } from 'react-icons/fa6'; +import { BiArrowBack } from '@react-icons/all-files/bi/BiArrowBack'; +import { FaRegClock } from '@react-icons/all-files/fa/FaRegClock'; +import { FaPhoneAlt } from '@react-icons/all-files/fa/FaPhoneAlt'; +import { FaRegBell } from '@react-icons/all-files/fa/FaRegBell'; +import { FaRegTrashAlt } from '@react-icons/all-files/fa/FaRegTrashAlt'; import { parsePhoneNumber } from '../../../../utils'; import { QueryObserverResult, @@ -26,6 +29,7 @@ import Alert from '../../../../components/Alert'; import CustomerLoadingSpinner from '../../../../components/LoadingSpinner/CustomerLoadingSpinner'; import { CouponRes } from '../../../../types/api/response'; import { Coupon } from '../../../../types/domain/coupon'; +import { FaLocationDot } from '../../../../assets'; interface CouponDetailProps { isDetail: boolean; @@ -112,7 +116,7 @@ const CouponDetail = ({ )} {cafeData.cafe.roadAddress && ( - + {cafeData.cafe.roadAddress + ' ' + cafeData.cafe.detailAddress} )} diff --git a/frontend/src/pages/Customer/CouponList/CouponDetail/style.tsx b/frontend/src/pages/Customer/CouponList/CouponDetail/style.tsx index f40df3841..00b857454 100644 --- a/frontend/src/pages/Customer/CouponList/CouponDetail/style.tsx +++ b/frontend/src/pages/Customer/CouponList/CouponDetail/style.tsx @@ -11,6 +11,7 @@ export const CouponDetailContainer = styled.section<{ $isDetail: boolean }>` background: white; max-width: 450px; transform: translateY(100%); + z-index: 1; ${({ $isDetail }) => $isDetail && @@ -74,15 +75,31 @@ export const CafeImage = styled.img` object-fit: cover; `; +export const DetailItem = styled.div` + display: flex; + flex-direction: row; + gap: 10px; + + & > :first-child { + min-width: 22px; + min-height: 22px; + } +`; + export const ContentContainer = styled.div` display: flex; flex-direction: column; position: absolute; bottom: 15%; left: 50px; - width: 345px; + width: 310px; gap: 10px; + /** iPhone SE */ + @media only screen and (min-device-width: 320px) and (max-device-width: 375px) { + width: 290px; + } + :nth-child(n) { display: flex; align-items: center; diff --git a/frontend/src/pages/Customer/CouponList/Header/index.tsx b/frontend/src/pages/Customer/CouponList/Header/index.tsx index 8acae35da..98e10fa03 100644 --- a/frontend/src/pages/Customer/CouponList/Header/index.tsx +++ b/frontend/src/pages/Customer/CouponList/Header/index.tsx @@ -1,13 +1,13 @@ import { Link } from 'react-router-dom'; import { ROUTER_PATH } from '../../../../constants'; -import { HeaderContainer, LogoImg } from '../style'; +import { HeaderContainer } from '../style'; import { StampcrushLogo } from '../../../../assets'; const Header = () => { return ( - + ); diff --git a/frontend/src/pages/Customer/HistoryPage/HistoryPage.tsx b/frontend/src/pages/Customer/HistoryPage/HistoryPage.tsx index ee8f5ee72..b39a3dbfc 100644 --- a/frontend/src/pages/Customer/HistoryPage/HistoryPage.tsx +++ b/frontend/src/pages/Customer/HistoryPage/HistoryPage.tsx @@ -1,14 +1,22 @@ import { PropsWithChildren } from 'react'; +import { useNavigate } from 'react-router-dom'; import SubHeader from '../../../components/Header/SubHeader'; +import { ROUTER_PATH } from '../../../constants'; interface HistoryPageProps { title: string; } const HistoryPage = ({ title, children }: HistoryPageProps & PropsWithChildren) => { + const navigate = useNavigate(); + + const navigateMyPage = () => { + navigate(ROUTER_PATH.myPage); + }; + return ( <> - + {children} ); diff --git a/frontend/src/pages/Customer/HistoryPage/style.tsx b/frontend/src/pages/Customer/HistoryPage/style.tsx index 748e0a795..93e06ad53 100644 --- a/frontend/src/pages/Customer/HistoryPage/style.tsx +++ b/frontend/src/pages/Customer/HistoryPage/style.tsx @@ -2,7 +2,7 @@ import { styled } from 'styled-components'; export const DateTitle = styled.div` background-color: #eee; - color: #871e90; + color: ${({ theme }) => theme.colors.main}; padding: 5px 0px 5px 20px; `; diff --git a/frontend/src/pages/Customer/InputPhoneNumber/hooks/useGetCustomerRegisterType.ts b/frontend/src/pages/Customer/InputPhoneNumber/hooks/useGetCustomerRegisterType.ts new file mode 100644 index 000000000..5909ec51c --- /dev/null +++ b/frontend/src/pages/Customer/InputPhoneNumber/hooks/useGetCustomerRegisterType.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; +import { getCustomerRegisterType } from '../../../../api/get'; +import { PHONE_NUMBER_LENGTH } from '../../../../constants'; +import { removeHyphen } from '../../../../utils'; + +const useGetCustomerRegisterType = (phoneNumber: string) => { + return useQuery(['customerRegisterType'], { + queryFn: () => getCustomerRegisterType({ params: { phoneNumber: removeHyphen(phoneNumber) } }), + select: (data) => data.customers, + enabled: phoneNumber.length === PHONE_NUMBER_LENGTH, + }); +}; + +export default useGetCustomerRegisterType; diff --git a/frontend/src/pages/Customer/InputPhoneNumber/hooks/useInputPhoneNumber.ts b/frontend/src/pages/Customer/InputPhoneNumber/hooks/useInputPhoneNumber.ts new file mode 100644 index 000000000..d9dfd80b3 --- /dev/null +++ b/frontend/src/pages/Customer/InputPhoneNumber/hooks/useInputPhoneNumber.ts @@ -0,0 +1,54 @@ +import { useState, ChangeEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { PHONE_NUMBER_LENGTH, ROUTER_PATH } from '../../../../constants'; +import useGetCustomerRegisterType from './useGetCustomerRegisterType'; +import usePostCustomerLinkData from './usePostCustomerLinkData'; +import usePostCustomerPhoneNumber from './usePostCustomerPhoneNumber'; + +const useInputPhoneNumber = () => { + const navigate = useNavigate(); + const [phoneNumber, setPhoneNumber] = useState(''); + const { data: customerRegisterType } = useGetCustomerRegisterType(phoneNumber); + const { mutateAsync: mutatePhoneNumber } = usePostCustomerPhoneNumber(); + const { mutateAsync: mutateLinkData } = usePostCustomerLinkData(); + + const changePhoneNumber = (e: ChangeEvent) => { + setPhoneNumber(e.target.value); + }; + + const submitPhoneNumber = () => { + if (phoneNumber.length < PHONE_NUMBER_LENGTH) { + alert('전화번호를 모두 입력해주세요.'); + return; + } + registerPhoneNumber(); + }; + + const registerPhoneNumber = async () => { + if (!customerRegisterType) return; + + /** 순서가 바뀌면 안되는 로직입니다. */ + // 임시회원이 아닌 유저가 정상적인 전화번호를 등록하려고 할때 + if (customerRegisterType.length === 0) { + await mutatePhoneNumber(phoneNumber); + navigate(ROUTER_PATH.couponList); + return; + } + + // 임시회원이 자신의 전화번호를 연동하려고 할 때 + if (customerRegisterType[0].registerType === 'temporary') { + await mutateLinkData(customerRegisterType[0].id); + alert('전화번호 등록이 완료되었습니다! 다시 한번 로그인해주세요 😄.'); + localStorage.setItem('login-token', ''); + navigate(ROUTER_PATH.login); + } + + // 이미 등록되어 있는 전화번호로 등록하려고 할 때 + if (customerRegisterType[0].registerType === 'register') + alert('이미 등록되어 있는 전화번호입니다. 다시 입력해주세요.'); + }; + + return { phoneNumber, changePhoneNumber, submitPhoneNumber }; +}; + +export default useInputPhoneNumber; diff --git a/frontend/src/pages/Customer/InputPhoneNumber/hooks/usePostCustomerLinkData.ts b/frontend/src/pages/Customer/InputPhoneNumber/hooks/usePostCustomerLinkData.ts new file mode 100644 index 000000000..9c0ead8a6 --- /dev/null +++ b/frontend/src/pages/Customer/InputPhoneNumber/hooks/usePostCustomerLinkData.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; +import { postCustomerLinkData } from '../../../../api/post'; + +const usePostCustomerLinkData = () => { + return useMutation({ + mutationFn: (customerId: number) => postCustomerLinkData({ body: { id: customerId } }), + }); +}; + +export default usePostCustomerLinkData; diff --git a/frontend/src/pages/Customer/InputPhoneNumber/hooks/usePostCustomerPhoneNumber.tsx b/frontend/src/pages/Customer/InputPhoneNumber/hooks/usePostCustomerPhoneNumber.tsx new file mode 100644 index 000000000..4e1fc906f --- /dev/null +++ b/frontend/src/pages/Customer/InputPhoneNumber/hooks/usePostCustomerPhoneNumber.tsx @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; +import { postCustomerPhoneNumber } from '../../../../api/post'; + +const usePostCustomerPhoneNumber = () => { + return useMutation({ + mutationFn: (phoneNumber: string) => postCustomerPhoneNumber({ body: { phoneNumber } }), + }); +}; + +export default usePostCustomerPhoneNumber; diff --git a/frontend/src/pages/Customer/InputPhoneNumber/index.tsx b/frontend/src/pages/Customer/InputPhoneNumber/index.tsx new file mode 100644 index 000000000..b39e41560 --- /dev/null +++ b/frontend/src/pages/Customer/InputPhoneNumber/index.tsx @@ -0,0 +1,42 @@ +import { Input } from '../../../components/Input'; +import { LoginLogo } from '../../../assets'; +import Button from '../../../components/Button'; +import { useEffect } from 'react'; +import { parsePhoneNumber } from '../../../utils'; +import { useNavigate } from 'react-router-dom'; +import { ROUTER_PATH } from '../../../constants'; +import { useCustomerProfile } from '../../../hooks/useCustomerProfile'; +import useInputPhoneNumber from './hooks/useInputPhoneNumber'; +import { Form } from './style'; + +const InputPhoneNumber = () => { + const navigate = useNavigate(); + const { customerProfile } = useCustomerProfile(); + const { phoneNumber, changePhoneNumber, submitPhoneNumber } = useInputPhoneNumber(); + + useEffect(() => { + if (customerProfile?.profile.phoneNumber) navigate(ROUTER_PATH.couponList); + }, [customerProfile]); + + return ( +
+ + + + + ); +}; + +export default InputPhoneNumber; diff --git a/frontend/src/pages/Customer/InputPhoneNumber/style.tsx b/frontend/src/pages/Customer/InputPhoneNumber/style.tsx new file mode 100644 index 000000000..51f098659 --- /dev/null +++ b/frontend/src/pages/Customer/InputPhoneNumber/style.tsx @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const Form = styled.form` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 50% 10%; + gap: 20px; + + & > div { + margin-top: 50px; + width: 100%; + } +`; diff --git a/frontend/src/pages/Customer/Login/index.tsx b/frontend/src/pages/Customer/Login/index.tsx index c1e9969fe..ad141ae12 100644 --- a/frontend/src/pages/Customer/Login/index.tsx +++ b/frontend/src/pages/Customer/Login/index.tsx @@ -18,7 +18,7 @@ const Login = () => { return (