diff --git a/.github/workflows/develop-deploy-v1.yml b/.github/workflows/develop-deploy-v1.yml index 11d587a8..37d66971 100644 --- a/.github/workflows/develop-deploy-v1.yml +++ b/.github/workflows/develop-deploy-v1.yml @@ -15,7 +15,7 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - + # Docker 이미지 빌드 및 태깅 - name: Build and push uses: docker/build-push-action@v3 @@ -27,7 +27,9 @@ jobs: NEXT_PUBLIC_API_MOCKING=${{ secrets.NEXT_PUBLIC_API_MOCKING }} NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_KAKAO_APP_KEY=${{ secrets.NEXT_PUBLIC_KAKAO_APP_KEY }} - + NEXT_PUBLIC_PAY200_KEY=${{ secrets.NEXT_PUBLIC_PAY200_KEY }} + NEXT_PUBLIC_TOSS_PAY_KEY=${{ secrets.NEXT_PUBLIC_TOSS_PAY_KEY }} + NEXT_PUBLIC_KAKAO_API_KEY=${{ secrets.NEXT_PUBLIC_KAKAO_API_KEY }} # EC2 도커 컴포즈 실행 - name: SSH and Deploy uses: appleboy/ssh-action@v0.1.8 diff --git a/.github/workflows/main-deploy-v1.yml b/.github/workflows/main-deploy-v1.yml index 04ff1628..d0f9cab1 100644 --- a/.github/workflows/main-deploy-v1.yml +++ b/.github/workflows/main-deploy-v1.yml @@ -13,7 +13,7 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - + - name: Build and push uses: docker/build-push-action@v3 with: @@ -24,6 +24,8 @@ jobs: NEXT_PUBLIC_API_MOCKING=${{ secrets.NEXT_PUBLIC_API_MOCKING }} NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_PROD_API_URL }} NEXT_PUBLIC_KAKAO_APP_KEY=${{ secrets.NEXT_PUBLIC_KAKAO_APP_KEY }} + NEXT_PUBLIC_PAY200_KEY=${{ secrets.NEXT_PUBLIC_PAY200_KEY }} + NEXT_PUBLIC_TOSS_PAY_KEY=${{ secrets.NEXT_PUBLIC_TOSS_PAY_KEY }} - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 diff --git a/.vscode/settings.json b/.vscode/settings.json index b6c597f5..d11680b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,11 +12,11 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "prettier.requireConfig": true, - "prettier.useEditorConfig": false, + "prettier.requireConfig": false, + "prettier.useEditorConfig": true, "javascript.format.enable": false, "typescript.format.enable": false, - "editor.formatOnSaveTimeout": 1500, + "editor.formatOnSaveTimeout": 5000, "[typescript]": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" @@ -27,9 +27,9 @@ }, "[typescriptreact]": { "editor.formatOnSave": true, - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "editor.formatOnSaveMode": "modifications", + "editor.formatOnSaveMode": "file", "tailwindCSS.validate": false, "files.watcherExclude": { "**/node_modules/**": true, diff --git a/Dockerfile b/Dockerfile index 64fa7e4c..c1c076ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,16 @@ RUN apk add --no-cache libc6-compat ARG NEXT_PUBLIC_API_MOCKING ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_KAKAO_APP_KEY - +ARG NEXT_PUBLIC_PAY200_KEY +ARG NEXT_PUBLIC_TOSS_PAY_KEY +ARG NEXT_PUBLIC_KAKAO_API_KEY # .env 파일 생성 RUN echo "NEXT_PUBLIC_API_MOCKING=${NEXT_PUBLIC_API_MOCKING}" > .env && \ echo "NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}" >> .env && \ - echo "NEXT_PUBLIC_KAKAO_APP_KEY=${NEXT_PUBLIC_KAKAO_APP_KEY}" >> .env + echo "NEXT_PUBLIC_KAKAO_APP_KEY=${NEXT_PUBLIC_KAKAO_APP_KEY}" >> .env && \ + echo "NEXT_PUBLIC_PAY200_KEY=${NEXT_PUBLIC_PAY200_KEY}" >> .env && \ + echo "NEXT_PUBLIC_TOSS_PAY_KEY=${NEXT_PUBLIC_TOSS_PAY_KEY}" >> .env && \ + echo "NEXT_PUBLIC_KAKAO_API_KEY=${NEXT_PUBLIC_KAKAO_API_KEY}" >> .env # .env 파일 확인 RUN echo "=== .env file contents ===" && cat .env diff --git a/docker-compose.yml b/docker-compose.yml index 4abe9db8..379f76d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.8" +version: '3.8' services: o2o-fe: @@ -10,6 +10,9 @@ services: - NEXT_PUBLIC_API_MOCKING=${NEXT_PUBLIC_API_MOCKING} - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_KAKAO_APP_KEY=${NEXT_PUBLIC_KAKAO_APP_KEY} + - NEXT_PUBLIC_PAY200_KEY=${NEXT_PUBLIC_PAY200_KEY} + - NEXT_PUBLIC_TOSS_PAY_KEY=${NEXT_PUBLIC_TOSS_PAY_KEY} + - NEXT_PUBLIC_KAKAO_API_KEY=${NEXT_PUBLIC_KAKAO_API_KEY} image: yong7317/o2o-fe:latest ports: - '3000:3000' @@ -17,9 +20,12 @@ services: - NEXT_PUBLIC_API_MOCKING=${NEXT_PUBLIC_API_MOCKING} - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_KAKAO_APP_KEY=${NEXT_PUBLIC_KAKAO_APP_KEY} + - NEXT_PUBLIC_PAY200_KEY=${NEXT_PUBLIC_PAY200_KEY} + - NEXT_PUBLIC_TOSS_PAY_KEY=${NEXT_PUBLIC_TOSS_PAY_KEY} + - NEXT_PUBLIC_KAKAO_API_KEY=${NEXT_PUBLIC_KAKAO_API_KEY} networks: - o2o-network - + networks: o2o-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/eslint.config.mjs b/eslint.config.mjs index d9132c8b..6d2a1061 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,5 @@ import js from '@eslint/js' import nextPlugin from '@next/eslint-plugin-next' -import pluginPrettier from 'eslint-plugin-prettier' import tailwind from 'eslint-plugin-tailwindcss' import ts from 'typescript-eslint' @@ -16,24 +15,11 @@ export default [ plugins: { '@typescript-eslint': ts.plugin, tailwindcss: tailwind, - prettier: pluginPrettier, '@next/next': nextPlugin, }, rules: { '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-explicit-any': 'warn', - 'prettier/prettier': [ - 'error', - { - tabWidth: 2, - useTabs: false, - singleQuote: true, - trailingComma: 'es5', - semi: false, - endOfLine: 'lf', - printWidth: 100, - }, - ], indent: 'off', '@typescript-eslint/indent': 'off', 'tailwindcss/no-custom-classname': 'off', diff --git a/next.config.ts b/next.config.ts index 2be6d025..423000af 100644 --- a/next.config.ts +++ b/next.config.ts @@ -18,6 +18,7 @@ const nextConfig: NextConfig = { return config }, + reactStrictMode: false, output: 'standalone', devIndicators: { buildActivity: false, diff --git a/package.json b/package.json index d539ea0d..7b8e0776 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.10.0", + "@pay200/sdk": "^0.0.5", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.4", @@ -49,6 +50,7 @@ "@next/eslint-plugin-next": "^15.1.3", "@svgr/webpack": "^8.1.0", "@tanstack/react-query-devtools": "^5.66.3", + "@tosspayments/tosspayments-sdk": "^2.3.4", "@types/eslint-plugin-tailwindcss": "^3", "@types/node": "^20", "@types/react": "^19", @@ -58,15 +60,16 @@ "@yarnpkg/sdks": "^3.2.0", "eslint": "^9", "eslint-config-next": "15.1.3", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^10.0.2", "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-tailwindcss": "^3.17.5", "husky": "^9.1.7", + "kakao.maps.d.ts": "^0.1.40", "msw": "^2.7.0", "postcss": "^8", - "prettier": "^3.4.2", - "prettier-plugin-tailwindcss": "^0.6.9", + "prettier": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^3.4.1", "typescript": "^5", "typescript-eslint": "^8.19.0" diff --git a/src/api/useDeleteAddress.ts b/src/api/useDeleteAddress.ts index 4b4f7d06..a76e8dad 100644 --- a/src/api/useDeleteAddress.ts +++ b/src/api/useDeleteAddress.ts @@ -5,11 +5,11 @@ const useDeleteAddress = () => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (addressId: string) => { + mutationFn: async (addressId: number) => { return await api.delete(`members/address/${addressId}`) }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['completed-addresses'] }) + // queryClient.invalidateQueries({ queryKey: ['completed-addresses'] }) }, }) } diff --git a/src/api/useGetAddress.ts b/src/api/useGetAddress.ts index a9e30046..e0fae927 100644 --- a/src/api/useGetAddress.ts +++ b/src/api/useGetAddress.ts @@ -1,30 +1,40 @@ import { api } from '@/lib/api' +import addressStore from '@/store/addressStore' +import memberStore from '@/store/user' import { useQuery, useQueryClient } from '@tanstack/react-query' interface Address { id: number + isDefault: boolean roadAddress: string jibunAddress: string detailAddress: string latitude: number longitude: number + alias?: string } -interface AddressData { - defaultAddress: Address - house: Address - company: Address - others: Address[] +export interface AddressResponseData { + defaultAddress?: Address + house?: Address + company?: Address + others?: Address[] } const useGetAddress = () => { const qc = useQueryClient() + const { member } = memberStore() + const { setAddress } = addressStore() const { data: address, isSuccess } = useQuery({ queryKey: ['address'], queryFn: async () => { - return await api.get(`members/address`) + const response = await api.get(`members/address`) + setAddress(response) + + return response }, + enabled: !!member, }) const resetGetAddress = () => { diff --git a/src/api/useGetAddressToGeolocation.ts b/src/api/useGetAddressToGeolocation.ts new file mode 100644 index 00000000..ac798e86 --- /dev/null +++ b/src/api/useGetAddressToGeolocation.ts @@ -0,0 +1,27 @@ +import { useMutation } from '@tanstack/react-query' +import ky from 'ky' + +const useGetAddressToGeolocation = () => { + return useMutation({ + mutationFn: (address: string) => + ky + .get<{ + documents: { + jibunAddress: string + roadAddress: string + x: string + y: string + }[] + }>('https://dapi.kakao.com/v2/local/search/address.json', { + headers: { + Authorization: `KakaoAK ${process.env.NEXT_PUBLIC_KAKAO_API_KEY}`, + }, + searchParams: { + query: address, + }, + }) + .json(), + }) +} + +export default useGetAddressToGeolocation diff --git a/src/api/useGetGeolocationToAddress.ts b/src/api/useGetGeolocationToAddress.ts new file mode 100644 index 00000000..44d1466a --- /dev/null +++ b/src/api/useGetGeolocationToAddress.ts @@ -0,0 +1,37 @@ +import { useMutation } from '@tanstack/react-query' +import ky from 'ky' + +interface GeolocationToAddressResponse { + address_name: string + main_address_no: string + mountain_yn: string + region_1depth_name: string + region_2depth_name: string + region_3depth_name: string + sub_address_no: string + zip_code: string +} + +const useGetGeolocationToAddress = () => { + return useMutation({ + mutationFn: ({ longitude, latitude }: { longitude: string; latitude: string }) => + ky + .get<{ + documents: { + address: GeolocationToAddressResponse + road_address: GeolocationToAddressResponse + }[] + }>('https://dapi.kakao.com/v2/local/geo/coord2address.json', { + headers: { + Authorization: `KakaoAK ${process.env.NEXT_PUBLIC_KAKAO_API_KEY}`, + }, + searchParams: { + x: longitude, + y: latitude, + }, + }) + .json(), + }) +} + +export default useGetGeolocationToAddress diff --git a/src/api/useGetOrderStatus.ts b/src/api/useGetOrderStatus.ts new file mode 100644 index 00000000..1f0aac6b --- /dev/null +++ b/src/api/useGetOrderStatus.ts @@ -0,0 +1,20 @@ +import { api } from '@/lib/api' +import { useQuery } from '@tanstack/react-query' +import { OrderStatus } from './useGetOrdersDetail' + +const useGetOrderStatus = (orderId?: string) => { + const { data: status, isSuccess } = useQuery({ + queryKey: ['orderStatus', orderId], + queryFn: async () => { + return await api.get<{ status: OrderStatus }>(`orders/${orderId}/status`) + }, + refetchInterval: (data) => { + const shouldRefetch = ['NEW', 'ONGOING'].includes(data.state.data?.status as OrderStatus) + return shouldRefetch ? 5000 : false + }, + }) + + return { status, isSuccess } +} + +export default useGetOrderStatus diff --git a/src/api/useGetOrders.ts b/src/api/useGetOrders.ts deleted file mode 100644 index 89573797..00000000 --- a/src/api/useGetOrders.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useLocalStorage } from '@/hooks/useLocalStorage' -import { api } from '@/lib/api' -import { useQuery, useQueryClient } from '@tanstack/react-query' - -export interface Orders { - content: OrdersList[] -} - -export interface OrdersList { - storeId: string - storeName: string - orderId: string - status: { - code: string - desc: string - } - orderTime: string - orderSummary: string - deliveryCompleteTime: string | null - imageThumbnail: string - paymentPrice: number -} - -const useGetOrders = (searchParams?: string) => { - const qc = useQueryClient() - const { storedValue: accessToken } = useLocalStorage('accessToken') - - const { data: orders, isSuccess } = useQuery({ - queryKey: ['orders', searchParams], - queryFn: async () => { - const params: Record = {} - - params.page = '' - params.size = '' - params.keyword = searchParams ?? '' - - return await api.get(`orders`, { - searchParams: params, - }) - }, - enabled: !!accessToken, - }) - - const resetGetOrders = () => { - qc.removeQueries({ queryKey: ['orders'] }) - } - - return { orders, isSuccess, resetGetOrders } -} - -export default useGetOrders diff --git a/src/api/useGetOrdersDetail.ts b/src/api/useGetOrdersDetail.ts index 14ad070a..35fe7aad 100644 --- a/src/api/useGetOrdersDetail.ts +++ b/src/api/useGetOrdersDetail.ts @@ -1,14 +1,17 @@ import { api } from '@/lib/api' import { useQuery, useQueryClient } from '@tanstack/react-query' -export type ORDER_STATUS_CODE = 'S1' | 'S2' | 'S3' | 'S4' | 'S5' | 'S6' // S1: 주문대기, S2: 주문접수, S3: 주문수락, S4: 주문거절, S5: 주문완료, S6: 주문취소 - +export type OrderStatus = 'NEW' | 'ONGOING' | 'DONE' | 'REFUSE' | 'CANCEL' +export type ORDER_STATUS = + | { code: 'S1'; desc: '주문대기' } + | { code: 'S2'; desc: '주문접수' } + | { code: 'S3'; desc: '주문수락' } + | { code: 'S4'; desc: '주문거절' } + | { code: 'S5'; desc: '주문완료' } + | { code: 'S6'; desc: '주문취소' } export interface OrdersDetail { orderId: string - status: { - code: ORDER_STATUS_CODE - desc: string - } + status: ORDER_STATUS orderTime: string storeName: string tel: string @@ -62,7 +65,7 @@ const useGetOrdersDetail = (orderId?: string) => { }) const resetGetOrdersDetail = () => { - qc.removeQueries({ queryKey: ['orders'] }) + qc.invalidateQueries({ queryKey: ['ordersDetail', orderId] }) } return { ordersDetail, isSuccess, resetGetOrdersDetail } diff --git a/src/api/useGetRecentStores.tsx b/src/api/useGetRecentStores.tsx index 9f4408f2..60b01008 100644 --- a/src/api/useGetRecentStores.tsx +++ b/src/api/useGetRecentStores.tsx @@ -14,7 +14,7 @@ const useGetRecentStores = () => { searchParams: { storeIds: recentStoreIds ? recentStoreIds.join(',') : '' }, }) }, - enabled: !!recentStoreIds && recentStoreIds.length > 0, + enabled: !!recentStoreIds, placeholderData: keepPreviousData, }) } diff --git a/src/api/usePostAddress.ts b/src/api/usePostAddress.ts index f43d233f..6974e15a 100644 --- a/src/api/usePostAddress.ts +++ b/src/api/usePostAddress.ts @@ -1,12 +1,18 @@ -import { useMutation } from '@tanstack/react-query' import { api } from '@/lib/api' +import { useMutation } from '@tanstack/react-query' + +export enum AddressType { + HOME = 'HOME', + COMPANY = 'COMPANY', + OTHERS = 'OTHER', +} export interface Address { - memberAddressType: string | undefined + memberAddressType: AddressType | undefined roadAddress: string jibunAddress: string detailAddress: string - alias: string + alias?: string latitude: number longitude: number } @@ -17,6 +23,9 @@ const usePostAddress = () => { mutationFn: async (data: Address) => { return await api.post(`members/address`, data) }, + onSuccess: () => { + // queryClient.invalidateQueries({ queryKey: ['addressKey'] }) + }, }) } diff --git a/src/api/usePostDefaultAddress.ts b/src/api/usePostDefaultAddress.ts new file mode 100644 index 00000000..b9f914d7 --- /dev/null +++ b/src/api/usePostDefaultAddress.ts @@ -0,0 +1,15 @@ +'use client' + +import { api } from '@/lib/api' +import { useMutation } from '@tanstack/react-query' + +const usePostDefaultAddress = () => { + return useMutation({ + mutationKey: ['addressKey'], + mutationFn: async (id: number) => { + return await api.put(`members/address/${id}/default`, {}) + }, + }) +} + +export default usePostDefaultAddress diff --git a/src/api/usePostLogout.ts b/src/api/usePostLogout.ts index 4e61236c..83731642 100644 --- a/src/api/usePostLogout.ts +++ b/src/api/usePostLogout.ts @@ -1,5 +1,6 @@ import { useLocalStorage } from '@/hooks/useLocalStorage' import { api } from '@/lib/api' +import addressStore from '@/store/addressStore' import memberStore from '@/store/user' import { useMutation, useQueryClient } from '@tanstack/react-query' @@ -8,6 +9,7 @@ const usePostLogout = () => { const accessToken = useLocalStorage('accessToken') const refreshToken = useLocalStorage('refreshToken') const { resetMember } = memberStore() + const { resetAddress } = addressStore() return useMutation({ mutationFn: async () => @@ -19,6 +21,7 @@ const usePostLogout = () => { accessToken.resetValue() refreshToken.resetValue() resetMember() + resetAddress() queryClient.removeQueries({ queryKey: ['member'] }) queryClient.removeQueries({ queryKey: ['favorites'] }) queryClient.removeQueries({ queryKey: ['carts'] }) diff --git a/src/api/usePostOrderPay.ts b/src/api/usePostOrderPay.ts index 3bd70351..7274d296 100644 --- a/src/api/usePostOrderPay.ts +++ b/src/api/usePostOrderPay.ts @@ -1,6 +1,11 @@ import { api } from '@/lib/api' import { useMutation, useQueryClient } from '@tanstack/react-query' +export enum OrderPayType { + TOSS = 'TOSS_PAY', + PAY200 = 'PAY200', +} + export interface OrderPay { storeId: string // 가게ID roadAddress: string // 주문시점의 도로명주소 @@ -8,7 +13,7 @@ export interface OrderPay { detailAddress: string // 주문시점의 상세주소 excludingSpoonAndFork: boolean // 스푼과 포크 제외 여부 orderType: 'DELIVERY' | 'PACKING' // 주문타입 - paymentType: 'TOSS_PAY' | 'KAKAO_PAY' // 결제타입 + paymentType: OrderPayType // 결제타입 orderMenus: { id: string // 주문할 메뉴ID quantity: number // 주문할 메뉴 구매수량 diff --git a/src/api/usePostPayment.ts b/src/api/usePostPayment.ts index 988013c8..376e82ed 100644 --- a/src/api/usePostPayment.ts +++ b/src/api/usePostPayment.ts @@ -11,7 +11,7 @@ const usePostPayment = () => { return useMutation({ mutationKey: ['payment'], mutationFn: async (data: Payment) => { - return await api.post<{}>(`payments/approve`, data) + return await api.post(`payments/approve`, data) }, }) } diff --git a/src/api/usePostSearch.ts b/src/api/usePostSearch.ts index 2012b957..7b2614ec 100644 --- a/src/api/usePostSearch.ts +++ b/src/api/usePostSearch.ts @@ -6,7 +6,7 @@ const usePostSearch = () => { mutationFn: async (keyword: string) => await api.post<{ keyword: string }>(`stores/search`, { keyword }), onSuccess: (data) => { - console.log(data) + // console.log(data) }, }) } diff --git a/src/api/usePutAddress.ts b/src/api/usePutAddress.ts new file mode 100644 index 00000000..1c5b9745 --- /dev/null +++ b/src/api/usePutAddress.ts @@ -0,0 +1,18 @@ +import { api } from '@/lib/api' +import { useMutation } from '@tanstack/react-query' +import { Address } from './usePostAddress' + +interface UpdateAddress extends Address { + id: number +} + +const usePutAddress = () => { + return useMutation({ + mutationKey: ['updateAddress'], + mutationFn: async (data: UpdateAddress) => { + return await api.put(`members/address/${data.id}`, data) + }, + }) +} + +export default usePutAddress diff --git a/src/app/favorites/_components/FavoritesStoreList.tsx b/src/app/favorites/_components/FavoritesStoreList.tsx index 02ee2e08..26d7c050 100644 --- a/src/app/favorites/_components/FavoritesStoreList.tsx +++ b/src/app/favorites/_components/FavoritesStoreList.tsx @@ -39,7 +39,7 @@ const EmptyFavorites = () => {

diff --git a/src/app/globals.css b/src/app/globals.css index 3b1cf3d4..a628e56a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -215,7 +215,7 @@ button { /*************** utilities (start) ***************/ @layer utilities { .ignore-mobile-safe { - width: calc(100% + (2 * theme(spacing[mobile_safe]))); + width: calc(100% + (theme(spacing[mobile_safe]))); margin-left: calc(-1 * theme(spacing[mobile_safe])); } } diff --git a/src/app/home/_components/Home.tsx b/src/app/home/_components/Home.tsx index 2322ca46..f2480c36 100644 --- a/src/app/home/_components/Home.tsx +++ b/src/app/home/_components/Home.tsx @@ -35,7 +35,7 @@ const Home = () => {
diff --git a/src/app/mypage/_components/MeunList.tsx b/src/app/mypage/_components/MeunList.tsx index 177f2701..2c1ec621 100644 --- a/src/app/mypage/_components/MeunList.tsx +++ b/src/app/mypage/_components/MeunList.tsx @@ -1,8 +1,8 @@ import Icon from '@/components/Icon' import RippleeEffect from '@/components/RippleeEffect' +import memberStore from '@/store/user' import { ROUTE_PATHS } from '@/utils/routes' import Link from 'next/link' -import memberStore from '@/store/user' const menuItems = [ // { @@ -22,7 +22,7 @@ const menuItems = [ }, { icon: , - label: '자주 문는 질문', + label: '자주 묻는 질문', href: '', }, { diff --git a/src/app/mypage/address/_components/AddressOption.tsx b/src/app/mypage/address/_components/AddressOption.tsx index 893317cc..d17b2eab 100644 --- a/src/app/mypage/address/_components/AddressOption.tsx +++ b/src/app/mypage/address/_components/AddressOption.tsx @@ -1,55 +1,115 @@ 'use client' -import Input from '@/components/Input' -import { useState } from 'react' +import useDeleteAddress from '@/api/useDeleteAddress' +import useGetAddress, { AddressResponseData } from '@/api/useGetAddress' +import useGetAddressToGeolocation from '@/api/useGetAddressToGeolocation' +import { AddressType } from '@/api/usePostAddress' +import usePostDefaultAddress from '@/api/usePostDefaultAddress' +import AddressSearchModal from '@/app/mypage/address/_components/AddressSearchModal' +import Badge from '@/components/Badge' import Icon from '@/components/Icon' +import Input from '@/components/Input' import Separator from '@/components/Separator' +import LoginButtonSection from '@/components/shared/LoginButtonSection' +import { useToast } from '@/hooks/useToast' +import { cn } from '@/lib/utils' +import { modalStore } from '@/store/modal' +import memberStore from '@/store/user' import { ROUTE_PATHS } from '@/utils/routes' -import Link from 'next/link' -import Badge from '@/components/Badge' +import { useQueryClient } from '@tanstack/react-query' +import { useRouter } from 'next/navigation' +import { useState } from 'react' import DaumPostcode from 'react-daum-postcode' -import AddressSearchModal from '@/app/mypage/address/_components/AddressSearchModal' -import { useRouter, useSearchParams } from 'next/navigation' -import useGetAddress from '@/api/useGetAddress' -import useDeleteAddress from '@/api/useDeleteAddress' -import { modalStore } from '@/store/modal' -import { useToast } from '@/hooks/useToast' -import { Button } from '@/components/button' -import { SignupData } from '@/models/auth' -import Address from '@/app/mypage/address/page' -import AddressDetail from '@/app/mypage/address/detail/page' +import AddressDetail, { AddressData } from '../detail/_components/AddressDetail' -const AddressOption = ({ signup }) => { +const AddressOption = () => { const [word, setWord] = useState('') const [popup, setPopup] = useState(false) - const router = useRouter() - const { address } = signup ? { address: null } : useGetAddress() + + const { member } = memberStore() const { showModal, hideModal } = modalStore() + + const { mutate: deleteAddress, isPending: isPendingDeleting } = useDeleteAddress() + const { mutate: setDefaultAddress, isPending: isPendingSettingDefaultAddress } = + usePostDefaultAddress() + const { address } = useGetAddress() + const { mutate: addressToGeolocation } = useGetAddressToGeolocation() + const { toast } = useToast() - const { mutate: deleteAddress, isPending: isDeleting } = useDeleteAddress() + const router = useRouter() + + const queryClient = useQueryClient() - const handleComplete = (data) => { - setPopup(!popup) - hideModal() - handleClickDetail(data.roadAddress, signup) + const handleComplete = async (data: { address: string }) => { + addressToGeolocation(data.address, { + onSuccess: (data) => { + showModal({ + content: ( + + ), + useAnimation: true, + useDimmedClickClose: true, + }) + }, + onError: (error) => { + console.log({ error }) + toast({ + title: '주소 검색에 실패했습니다.', + description: '다시 시도해주세요.', + variant: 'destructive', + position: 'center', + }) + + hideModal() + }, + onSettled: () => { + setPopup(false) + }, + }) } - const handleClickDetail = (address, signup) => { + const handleClickDetail = (type?: AddressType) => { showModal({ - content: , + content: , useAnimation: true, useDimmedClickClose: true, }) } - const handleClickDeleteButton = (id) => { + const handleClickSetDefaultAddress = (id: number | undefined) => { + if (!id || isPendingSettingDefaultAddress) return + if (id === address?.defaultAddress?.id) { + router.push(ROUTE_PATHS.HOME) + return + } + + setDefaultAddress(id, { + onSuccess: () => { + router.push(ROUTE_PATHS.HOME) + queryClient.invalidateQueries({ queryKey: ['address'] }) + }, + }) + } + + const handleClickDeleteButton = (id: number | undefined) => { + if (!id || isPendingDeleting) return + deleteAddress(id, { onSuccess: () => { toast({ title: '주소가 삭제되었습니다.', position: 'center', }) - hideModal() + queryClient.invalidateQueries({ queryKey: ['address'] }) }, onError: () => { toast({ @@ -62,154 +122,208 @@ const AddressOption = ({ signup }) => { }) } - // todo: input를 readonly 할 수는 없는지 - return ( -
-
- setWord(e.target.value)} - onReset={() => setWord('')} - icon={} - offOutline - onClick={() => setPopup(true)} - /> - setPopup(false)}> - - + if (!member) + return ( +
+
-
- -
handleClickDetail}> - 현재 위치로 주소 찾기 + ) + + if (!address) return <> + else + return ( +
+
+ setWord(e.target.value)} + onReset={() => setWord('')} + icon={} + offOutline + onClick={() => setPopup(true)} + readOnly + /> + setPopup(false)}> + +
-
+
+ +
handleClickDetail()}> + 현재 위치로 주소 찾기 +
+
+ + - {signup ? ( - <> - - - ) : ( - <> - {address?.defaultAddress ? ( - <> - -
- -
-
-
{address?.defaultAddress.roadAddress}
- 현재 + {address.defaultAddress && ( + <> +
+ +
+
+
+ {address.defaultAddress.roadAddress || address.defaultAddress.jibunAddress} + {', '} + {address.defaultAddress.detailAddress}
+ 현재 +
+ {address.defaultAddress.roadAddress && (
[지번] {address?.defaultAddress.jibunAddress} + {', '} + {address?.defaultAddress.detailAddress}
-
+ )}
- - ) : ( - <> - - - - )} - {address?.house ? ( - <> - - -
-
-
- -
-
- -
+
+ + + )} -
- {address.house.roadAddress} {address.house.detailAddress} -
+
+
+ +
{ + if (!address.house) { + handleClickDetail(AddressType.HOME) + } else { + handleClickSetDefaultAddress(address.house.id) + } + }} + > +
+ {address.house ? '집' : '집 추가'} +
+ {address.house && ( +
+ {address.house?.roadAddress || address.house?.jibunAddress} + {', '} + {address.house?.detailAddress}
- - - ) : ( - <> - - + {address.house && address.defaultAddress?.id !== address.house.id && ( + -
+ + + )} +
-
- {address.company.roadAddress} {address.company.detailAddress} -
+ + +
+ +
{ + if (!address.company) { + handleClickDetail(AddressType.COMPANY) + } else { + handleClickSetDefaultAddress(address.company.id) + } + }} + > +
+ {address.company ? '회사' : '회사 추가'} +
+ {address.company && ( +
+ {address.company?.roadAddress || address.company?.jibunAddress} + {', '} + {address.company?.detailAddress}
- - - ) : ( - <> - - + {address.company && address.defaultAddress?.id !== address.company.id && ( + + )} +
+
+ + + + {address.others?.map((other, index) => ( +
+
{ + handleClickSetDefaultAddress(other.id) + }} + > + +
+
{other.alias || '별명 보내주세요'}
+
+ {other.roadAddress || other.jibunAddress} + {', '} + {other.detailAddress}
- - - )} - - )} -
- ) +
+ {address.defaultAddress?.id !== other.id && ( + + )} +
+
+ ))} + {(address.others?.length || 0) > 0 && } +
+ ) } -const AddressDetailModal = ({ address, signup }) => { +const AddressDetailModal = ({ + type, + userAddress, + addressData, +}: { + type?: AddressType + userAddress?: AddressResponseData + addressData?: AddressData +}) => { const { hideModal } = modalStore() return ( -
+
주소 등록
- +
+ +
) } diff --git a/src/app/mypage/address/detail/_components/AddressDetail.tsx b/src/app/mypage/address/detail/_components/AddressDetail.tsx new file mode 100644 index 00000000..09c53125 --- /dev/null +++ b/src/app/mypage/address/detail/_components/AddressDetail.tsx @@ -0,0 +1,74 @@ +'use client' + +import { AddressResponseData } from '@/api/useGetAddress' +import { AddressType } from '@/api/usePostAddress' +import KakaoMap from '@/app/mypage/address/detail/_components/KakaoMap' +import { useGeoLocationStore } from '@/store/geoLocation' +import { useState } from 'react' +import { useKakaoLoader } from 'react-kakao-maps-sdk' +import MapInfo from './MapInfo' + +export interface AddressData { + type: AddressType | undefined + address: string + roadAddr: string + detail: string + coords: { + lat: number + lng: number + } + alias?: string +} + +const AddressDetail = ({ + type = AddressType.HOME, + userAddress, + defaultAddressData, +}: { + type?: AddressType + userAddress?: AddressResponseData + defaultAddressData?: AddressData +}) => { + const [loading] = useKakaoLoader({ + appkey: process.env.NEXT_PUBLIC_KAKAO_APP_KEY!, + libraries: ['services'], + }) + const { coordinates } = useGeoLocationStore() + const [addressData, setAddressData] = useState( + defaultAddressData || { + type, + address: '', + roadAddr: '', + detail: '', + alias: '', + coords: coordinates + ? { + lat: coordinates.latitude, + lng: coordinates.longitude, + } + : { + lat: 0, + lng: 0, + }, + } + ) + + const handleAddressChange = (data: AddressData) => { + setAddressData(data) + } + + if (!coordinates || loading) return <> + + return ( +
+ + +
+ ) +} + +export default AddressDetail diff --git a/src/app/mypage/address/detail/_components/KakaoMap.tsx b/src/app/mypage/address/detail/_components/KakaoMap.tsx index 080a2539..d109e808 100644 --- a/src/app/mypage/address/detail/_components/KakaoMap.tsx +++ b/src/app/mypage/address/detail/_components/KakaoMap.tsx @@ -1,9 +1,13 @@ 'use client' -import { useEffect, useState, useRef } from 'react' -import { Map, MapMarker, useKakaoLoader } from 'react-kakao-maps-sdk' -import useGeolocation from '@/app/mypage/address/detail/_components/useGeolocation' -import { useSearchParams } from 'next/navigation' +import Pin from '@/assets/images/pin.png' +import Icon from '@/components/Icon' +import { cn } from '@/lib/utils' +import { useGeoLocationStore } from '@/store/geoLocation' +import Image from 'next/image' +import { useEffect, useRef, useState } from 'react' +import { Map } from 'react-kakao-maps-sdk' +import { AddressData } from './AddressDetail' declare global { interface Window { @@ -11,89 +15,141 @@ declare global { } } -const KakaoMap = ({ onAddressChange, data }) => { - const apiKey: string | undefined = process.env.NEXT_PUBLIC_KAKAO_APP_KEY - const [position, setPosition] = useState<{ lat: number; lng: number }>() - const [address, setAddress] = useState('') - const [roadAddr, setRoadAddr] = useState('') - const [lng, setLng] = useState(0) - const [lat, setLat] = useState(0) - const { coordinates, currentAddr, error, isLoading } = useGeolocation() - const searchParams = useSearchParams() - const [isMapLoading] = useKakaoLoader({ - appkey: apiKey, - libraries: ['services'], - }) +const KakaoMap = ({ + addressData, + onAddressChange, +}: { + addressData: AddressData + onAddressChange: (data: AddressData) => void +}) => { + const [geocoder, setGeocoder] = useState(null) + const mapRef = useRef(null) - useEffect(() => { - if (isMapLoading || isLoading || !coordinates) return - - if (data != '') { - const geocoder = new window.kakao.maps.services.Geocoder() - geocoder.addressSearch(data, (result, status) => { - setLng(result[0].x) - setLat(result[0].y) - setAddress(result[0].address.address_name) - setRoadAddr(result[0].road_address.address_name) - onAddressChange( - result[0].address.address_name, - result[0].road_address.address_name, - result[0].x, - result[0].y - ) + const [isDragging, setIsDragging] = useState(false) + + const { coordinates } = useGeoLocationStore() + + const getCenter = () => { + if (!mapRef.current || !geocoder) return + + const latlng = mapRef.current.getCenter() + + geocoder.coord2Address(latlng.getLng(), latlng.getLat(), (result, status) => { + const addr = result[0] + + onAddressChange({ + ...addressData, + address: addr.address.address_name, + roadAddr: addr.road_address?.address_name || '', + coords: { + lat: latlng.getLat(), + lng: latlng.getLng(), + }, }) - } else { - const geocoder = new window.kakao.maps.services.Geocoder() - geocoder.coord2Address(coordinates?.longitude, coordinates?.latitude, (result, status) => { - const addr = result[0] - setAddress(addr.address.address_name) - setRoadAddr(addr.road_address.address_name) - setLng(coordinates?.longitude) - setLat(coordinates?.latitude) - onAddressChange( - addr.address.address_name, - addr.road_address.address_name, - coordinates?.longitude, - coordinates?.latitude - ) + }) + } + + useEffect(() => { + // 카카오맵 스크립트 로드 + const script = document.createElement('script') + script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_APP_KEY}&libraries=services&autoload=false` + script.async = true + + script.addEventListener('load', () => { + window.kakao.maps.load(() => { + const geocoder = new window.kakao.maps.services.Geocoder() + setGeocoder(geocoder) + + if (addressData.coords) { + geocoder.coord2Address( + addressData.coords.lng, + addressData.coords.lat, + ( + result: Array<{ + address: kakao.maps.services.Address + road_address: kakao.maps.services.RoadAaddress | null + }>, + status: kakao.maps.services.Status + ) => { + const addr = result[0] + + onAddressChange({ + ...addressData, + address: addr.address.address_name, + roadAddr: addr.road_address?.address_name || '', + }) + } + ) + } }) + }) + + document.head.appendChild(script) + + return () => { + document.head.removeChild(script) } - }, [isMapLoading, isLoading, coordinates]) + }, []) + + // if (loading) return <> return ( - <> +
{ - const latlng = mouseEvent.latLng - setPosition({ - lat: latlng.getLat(), - lng: latlng.getLng(), - }) + onDragStart={() => { + setIsDragging(true) + }} + onDragEnd={(_, mouseEvent) => { + setIsDragging(false) + getCenter() + }} + onZoomChanged={(map) => { + getCenter() + }} + > +
+
+ pin +
+
+
+
{ + if (!mapRef.current || !window.kakao || !coordinates) return - const geocoder = new window.kakao.maps.services.Geocoder() - geocoder.coord2Address(latlng.getLng(), latlng.getLat(), (result, status) => { - const addr = result[0] - setAddress(addr.address.address_name) - setRoadAddr(addr.road_address.address_name) - onAddressChange( - addr.address.address_name, - addr.road_address.address_name, - latlng.getLng(), - latlng.getLat() - ) + onAddressChange({ + ...addressData, + coords: { + lat: coordinates.latitude, + lng: coordinates.longitude, + }, }) + mapRef.current.setCenter( + new window.kakao.maps.LatLng(coordinates.latitude, coordinates.longitude) + ) + + setTimeout(() => { + getCenter() + }, 100) }} > - - - + +
+
) } diff --git a/src/app/mypage/address/detail/_components/MapInfo.tsx b/src/app/mypage/address/detail/_components/MapInfo.tsx index 21c07851..fc434922 100644 --- a/src/app/mypage/address/detail/_components/MapInfo.tsx +++ b/src/app/mypage/address/detail/_components/MapInfo.tsx @@ -1,159 +1,223 @@ 'use client' -import Input from '@/components/Input' -import { useCallback, useEffect, useState } from 'react' +import { AddressResponseData } from '@/api/useGetAddress' +import usePostAddress, { Address, AddressType } from '@/api/usePostAddress' +import usePutAddress from '@/api/usePutAddress' import Icon from '@/components/Icon' +import Input from '@/components/Input' import { Button } from '@/components/button' -import { useSearchParams } from 'next/navigation' -import usePostAddress, { Address } from '@/api/usePostAddress' +import { useToast } from '@/hooks/useToast' +import { cn } from '@/lib/utils' import { modalStore } from '@/store/modal' -import { toast } from '@/hooks/useToast' - -const MapInfo = ({ address, roadAddr, lng, lat, signup }) => { - const [word, setWord] = useState('') - const searchParams = useSearchParams() - const { mutate: addressApi, data: addressResponse, isPending: isAddress } = usePostAddress() - const [flag, setFlag] = useState(true) - const { showModal, hideModal, setAddressData } = modalStore() - const [isClickedHome, setIsClickedHome] = useState(false) - const [isClickedCompany, setIsClickedCompany] = useState(false) - const [isClickedEtc, setIsClickedEtc] = useState(false) +import { useQueryClient } from '@tanstack/react-query' +import { useEffect, useState } from 'react' +import { AddressData } from './AddressDetail' + +const MapInfo = ({ + addressData, + onAddressChange, + userAddress, +}: { + addressData: AddressData + onAddressChange: (data: AddressData) => void + userAddress?: AddressResponseData +}) => { + const queryClient = useQueryClient() + const { toast } = useToast() + + const { hideModal } = modalStore() + + const [addressDetail, setAddressDetail] = useState('') + const [alias, setAlias] = useState('') + const [isAddressValid, setIsAddressValid] = useState(false) + const { mutate: registerAddress, isPending } = usePostAddress() + const { mutate: updateAddress, isPending: isUpdatePending } = usePutAddress() const handleAddress = () => { - let addressType = '' - - if (isClickedHome) { - addressType = 'HOME' - } else if (isClickedCompany) { - addressType = 'COMPANY' - } else if (isClickedEtc) { - addressType = 'OTHERS' + if (!addressData.roadAddr) { + toast({ + description: '배달이 불가능한 주소입니다.', + position: 'center', + }) + return } - const addressData: Address = { - memberAddressType: signup ? 'HOME' : addressType, - roadAddress: roadAddr, - jibunAddress: address, - detailAddress: word, - alias: '대표주소', - latitude: lat, - longitude: lng, + if (!addressDetail) { + toast({ + description: '상세주소를 입력해주세요.', + position: 'center', + }) + return } - if (signup) { - setAddressData(addressData) - hideModal() - } else { - // showModal({ - // content: , - // useAnimation: true, - // useDimmedClickClose: true, - // }) - - addressApi(addressData) + if (addressData.type === AddressType.OTHERS) { + if (!alias) { + toast({ + description: '별명을 입력해주세요.', + position: 'center', + }) + return + } + + if (userAddress && userAddress.others && userAddress.others.length >= 5) { + toast({ + description: '최대 5개의 주소만 등록할 수 있습니다.', + position: 'center', + }) + return + } + } - console.log('data', addressResponse) + const _address: Address = { + memberAddressType: addressData.type, + roadAddress: addressData.roadAddr || addressData.address, + jibunAddress: addressData.address, + detailAddress: addressDetail, + alias: addressData.type === AddressType.OTHERS ? alias : undefined, + latitude: addressData.coords.lat, + longitude: addressData.coords.lng, + } - toast({ - description: '주소 등록이 완료되었습니다.', - position: 'center', - }) + const _options = { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['address'] }) + hideModal() + }, + onError: () => { + toast({ + description: '주소 등록에 실패했습니다.', + position: 'center', + }) + }, } - } - const handleClickHome = () => { - setIsClickedHome((prevState) => !prevState) - setIsClickedCompany(false) - setIsClickedEtc(false) + if ( + userAddress && + ((addressData.type === AddressType.HOME && userAddress.house) || + (addressData.type === AddressType.COMPANY && userAddress.company)) + ) { + updateAddress( + { + id: + addressData.type === AddressType.HOME ? userAddress.house!.id : userAddress.company!.id, + ..._address, + }, + _options + ) + } else { + registerAddress(_address, _options) + } } - const handleClickCompany = () => { - setIsClickedCompany((prevState) => !prevState) - setIsClickedHome(false) - setIsClickedEtc(false) - } + useEffect(() => { + setAlias('') + }, [addressData.type]) - const handleClickEtc = () => { - setIsClickedEtc((prevState) => !prevState) - setIsClickedHome(false) - setIsClickedCompany(false) - } + useEffect(() => { + setIsAddressValid( + Boolean(addressData.roadAddr && addressData.address && addressDetail && addressData.type) + ) + }, [addressData, addressDetail]) return ( -
-
-
{roadAddr}
-
[지번] {address}
+
+
+
+ {addressData.roadAddr || '배달이 불가능한 주소입니다.'} +
+
+ {addressData.roadAddr ? `[지번] ${addressData.address}` : '위치를 이동해주세요!'} +
setWord(e.target.value)} - onReset={() => setWord('')} + onChange={(e) => setAddressDetail(e.target.value)} + onReset={() => setAddressDetail('')} offOutline /> - {!signup && ( -
-
-
- -
-
-
- -
회사
-
-
- -
기타
-
+
+
+
onAddressChange({ ...addressData, type: AddressType.HOME })} + > + +
+
+
onAddressChange({ ...addressData, type: AddressType.COMPANY })} + > + +
회사
+
+
onAddressChange({ ...addressData, type: AddressType.OTHERS })} + > + +
기타
- {isClickedEtc ? ( - setWord(e.target.value)} - onReset={() => setWord('')} - offOutline - /> - ) : ( - <> - )}
- )} + {addressData.type === AddressType.OTHERS && ( + setAlias(e.target.value)} + onReset={() => setAlias('')} + offOutline + /> + )} +
- +
) } -const AddressConfirmModal = ({ onAddress, isAddress }) => { - const { hideModal } = modalStore() - - return ( -
-
주소를 등록하시겠습니까?
-
- - -
-
- ) -} +// const AddressConfirmModal = ({ onAddress, isAddress }) => { +// const { hideModal } = modalStore() + +// return ( +//
+//
주소를 등록하시겠습니까?
+//
+// +// +//
+//
+// ) +// } export default MapInfo diff --git a/src/app/mypage/address/detail/_components/useGeolocation.tsx b/src/app/mypage/address/detail/_components/useGeolocation.tsx index 71ea6de6..ef3bf508 100644 --- a/src/app/mypage/address/detail/_components/useGeolocation.tsx +++ b/src/app/mypage/address/detail/_components/useGeolocation.tsx @@ -21,7 +21,7 @@ const useGeolocation = () => { const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isMapLoading] = useKakaoLoader({ - appkey: apiKey, + appkey: apiKey!, libraries: ['services'], }) diff --git a/src/app/mypage/address/detail/page.tsx b/src/app/mypage/address/detail/page.tsx index 97a93415..116df6a8 100644 --- a/src/app/mypage/address/detail/page.tsx +++ b/src/app/mypage/address/detail/page.tsx @@ -1,33 +1,7 @@ -'use client' +import AddressDetail from './_components/AddressDetail' -import KakaoMap from '@/app/mypage/address/detail/_components/KakaoMap' -import MapInfo from '@/app/mypage/address/detail/_components/MapInfo' -import { useState } from 'react' - -const AddressDetail = ({ data, signup }) => { - const [address, setAddress] = useState('') - const [roadAddr, setRoadAddr] = useState('') - const [lng, setLng] = useState(0) - const [lat, setLat] = useState(0) - const [signupChk] = useState(signup) - - const handleAddressChange = (address: string, roadAddr: string, lng: number, lat: number) => { - setAddress(address) - setRoadAddr(roadAddr) - setLng(lng) - setLat(lat) - } - - if (typeof data !== 'string') { - data = '' - } - - return ( -
- - -
- ) +const AddressDetailPage = () => { + return } -export default AddressDetail +export default AddressDetailPage diff --git a/src/app/mypage/address/page.tsx b/src/app/mypage/address/page.tsx index a38c9ca5..8b869e68 100644 --- a/src/app/mypage/address/page.tsx +++ b/src/app/mypage/address/page.tsx @@ -1,12 +1,7 @@ -'use client' - import AddressOption from './_components/AddressOption' -const Address = (signup) => { - if (signup.signup !== true) { - signup = false - } - return +const Address = () => { + return } export default Address diff --git a/src/app/mypage/edit-profile/_components/EditProfile.tsx b/src/app/mypage/edit-profile/_components/EditProfile.tsx index 6b7bc1d4..cf5680c9 100644 --- a/src/app/mypage/edit-profile/_components/EditProfile.tsx +++ b/src/app/mypage/edit-profile/_components/EditProfile.tsx @@ -1,9 +1,9 @@ 'use client' import usePostLogout from '@/api/usePostLogout' import Icon from '@/components/Icon' +import { useToast } from '@/hooks/useToast' import memberStore from '@/store/user' import { useQueryClient } from '@tanstack/react-query' -import Link from 'next/link' import { useRouter } from 'next/navigation' import { useEffect } from 'react' @@ -51,10 +51,19 @@ const EditProfile = () => { export default EditProfile const EditProfileItem = ({ title, value }: { title: string; value: string }) => { + const { toast } = useToast() + + const handleEditProfile = () => { + toast({ + description: '준비중입니다.', + position: 'center', + }) + } + return ( -
{title}
@@ -63,6 +72,6 @@ const EditProfileItem = ({ title, value }: { title: string; value: string }) =>
- +
) } diff --git a/src/app/orders/_components/Order.tsx b/src/app/orders/_components/Order.tsx index 9f62ccaf..351f001c 100644 --- a/src/app/orders/_components/Order.tsx +++ b/src/app/orders/_components/Order.tsx @@ -1,24 +1,97 @@ 'use client' -import useGetOrders from '@/api/useGetOrders' +import { ORDER_STATUS } from '@/api/useGetOrdersDetail' import CartButton from '@/components/CartButton' +import ScrollToTopButton from '@/components/ScrollToTopButton' import Separator from '@/components/Separator' import LoginButtonSection from '@/components/shared/LoginButtonSection' +import { useInfiniteScroll } from '@/hooks/useInfiniteScroll' import memberStore from '@/store/user' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import OrderItem from './OrderItem' +import OrderItemSkeleton from './OrderItemSkeleton' import OrderSearch from './OrderSearch' + +export interface Orders { + content: OrdersList[] +} + +export interface OrdersList { + storeId: string + storeName: string + orderId: string + status: ORDER_STATUS + orderTime: string + orderSummary: string + deliveryCompleteTime: string | null + imageThumbnail: string + paymentPrice: number +} + const Order = () => { + const [isSearch, setIsSearch] = useState(false) const [searchValue, setSearchValue] = useState('') - const { orders } = useGetOrders(searchValue) + const [showScrollButton, setShowScrollButton] = useState(false) + const { member } = memberStore() + const { data, targetRef, isFetching } = useInfiniteScroll({ + queryKey: 'orders', + endpoint: 'orders', + filter: { keyword: searchValue }, + size: 10, + enabled: !!member, + rootMargin: '0px 0px 1000px 0px', + }) + + const scrollToTop = () => { + const ordersList = document.getElementById('orders_list') + + if (ordersList) { + ordersList.scrollTo({ top: 0, behavior: 'smooth' }) + } + } + + const saveScrollPosition = () => { + const ordersList = document.getElementById('orders_list') + + if (ordersList) { + sessionStorage.setItem('ordersListScrollPosition', ordersList.scrollTop.toString()) + } + } const handelSearch = useCallback((value: string) => { + setIsSearch(true) setSearchValue(value) }, []) + const handleScroll = useCallback((e: Event) => { + const target = e.target as HTMLElement + const scrollTop = target.scrollTop + + setShowScrollButton(scrollTop > 100) + }, []) + + useEffect(() => { + if (!data || data.length === 0) return + + const ordersList = document.getElementById('orders_list') + const savedScrollPosition = sessionStorage.getItem('ordersListScrollPosition') + + if (ordersList) { + ordersList.addEventListener('scroll', handleScroll) + + if (isSearch) { + ordersList.scrollTo(0, 0) + setIsSearch(false) + } else if (savedScrollPosition) { + ordersList.scrollTop = parseInt(savedScrollPosition) + sessionStorage.removeItem('ordersListScrollPosition') + } + } + }, [data]) + return ( - <> +
{!member ? (
{
) : ( <> -
+
-
- {orders?.content.map((order, index) => ( +
+ {data?.map((order, index) => ( - - {index !== orders?.content.length - 1 && ( - - )} + + {index !== data?.length - 1 && } ))} +
+ {isFetching && + new Array(5) + .fill(0) + .map((_, index) => )} + {showScrollButton && }
{member && } )} - +
) } diff --git a/src/app/orders/_components/OrderItem.tsx b/src/app/orders/_components/OrderItem.tsx index 5c582453..ed2fa5eb 100644 --- a/src/app/orders/_components/OrderItem.tsx +++ b/src/app/orders/_components/OrderItem.tsx @@ -1,14 +1,40 @@ -import { OrdersList } from '@/api/useGetOrders' -import Badge from '@/components/Badge' +import Badge, { badgeVariants } from '@/components/Badge' import { Button } from '@/components/button' import { Skeleton } from '@/components/shadcn/skeleton' import { ROUTE_PATHS } from '@/utils/routes' import Image from 'next/image' import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { OrdersList } from './Order' + +const OrderItem = ({ + order, + onBeforeNavigate, +}: { + order: OrdersList + onBeforeNavigate?: () => void +}) => { + const router = useRouter() + + const handleNavigate = () => { + if (onBeforeNavigate) { + onBeforeNavigate() + } + + router.push(`${ROUTE_PATHS.ORDERS_DETAIL}/${order.orderId}`) + } + + const variant = { + S1: 'waiting', + S2: 'received', + S3: 'accepted', + S4: 'rejected', + S5: 'completed', + S6: 'canceled', + } -const OrderItem = ({ order }: { order: OrdersList }) => { return ( -
+
{order.imageThumbnail ? ( { )}
- {order.status.desc} + + {order.status.code === 'S5' ? '배달완료' : order.status.desc} +
{new Date(order.orderTime).toLocaleString()}
@@ -36,16 +64,18 @@ const OrderItem = ({ order }: { order: OrdersList }) => {
- - - - - - +
+ {order.status.code === 'S5' && ( + + + + )}
) diff --git a/src/app/orders/_components/OrderItemSkeleton.tsx b/src/app/orders/_components/OrderItemSkeleton.tsx new file mode 100644 index 00000000..e6e82480 --- /dev/null +++ b/src/app/orders/_components/OrderItemSkeleton.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from '@/components/shadcn/skeleton' + +const OrderItemSkeleton = () => { + return ( +
+
+ +
+ + +
+
+ +
+ ) +} + +export default OrderItemSkeleton diff --git a/src/app/orders/detail/[id]/_components/OrderList.tsx b/src/app/orders/detail/[id]/_components/OrderList.tsx index fbc67bfa..35f09c91 100644 --- a/src/app/orders/detail/[id]/_components/OrderList.tsx +++ b/src/app/orders/detail/[id]/_components/OrderList.tsx @@ -30,7 +30,9 @@ const OrderList = ({ ordersData }: OrderListProps) => {
{ordersData.storeName}
- {ordersData.status.code === 'S2' && } + {(ordersData.status.code === 'S1' || ordersData.status.code === 'S2') && ( + + )}
주문정보
diff --git a/src/app/orders/detail/[id]/_components/OrderStatus.tsx b/src/app/orders/detail/[id]/_components/OrderStatus.tsx index 1659f403..2fd96253 100644 --- a/src/app/orders/detail/[id]/_components/OrderStatus.tsx +++ b/src/app/orders/detail/[id]/_components/OrderStatus.tsx @@ -1,14 +1,14 @@ 'use client' +import { OrderStatus as TOrderStatus } from '@/api/useGetOrdersDetail' import { Progress } from '@/components/shadcn/progress' import { useEffect, useState } from 'react' type OrderStatusProps = { - orderStatus?: string | null + orderStatus: TOrderStatus } const OrderStatus: React.FC = ({ orderStatus }) => { - const [status, setStatus] = useState(orderStatus ?? '') const [title, setTitle] = useState('') const [subTitle, setSubTitle] = useState('') const [value, setValue] = useState(0) @@ -16,35 +16,31 @@ const OrderStatus: React.FC = ({ orderStatus }) => { useEffect(() => { switch (orderStatus) { - case '주문거절': - setTitle('주문이 거절되었습니다.') - setSubTitle('다음에 다시 이용해 주세요') - setIsDisabledProgress(true) - setValue(0) - break - case '주문접수': + case 'NEW': setTitle('주문이 접수되었습니다.') setSubTitle('확인 중입니다. 잠시만 기다려 주세요!') - setValue(0) - break - case '주문수락': - setTitle('주문을 수락했어요') - setSubTitle('잠시 후 도착 예정 시간을 알려드릴게요.') setValue(20) break - case '배달진행중': - setTitle('음식이 배달 중입니다') - setSubTitle('라이더가 빠르게 가는 중입니다. 기다려 주세요!') + case 'ONGOING': + setTitle('음식을 준비하고있어요.') + setSubTitle('주문하신 메뉴를 준비하고 있어요') setValue(50) break - case '배달완료': + case 'DONE': setTitle('배달을 완료했어요') setSubTitle('맛있게 드시고 리뷰를 남겨주세요!') setValue(100) break - case '주문취소': + + case 'REFUSE': + setTitle('주문이 거절되었습니다.') + setSubTitle('다음에 다시 이용해 주세요') + setIsDisabledProgress(true) + setValue(0) + break + case 'CANCEL': setTitle('주문을 취소했습니다') - setSubTitle('') + setSubTitle('다음에 다시 이용해 주세요') setValue(0) setIsDisabledProgress(true) break @@ -68,13 +64,13 @@ const OrderStatus: React.FC = ({ orderStatus }) => {
-
+
주문 수락
-
- 배달 진행중 +
+ 조리중
-
+
배달 완료
diff --git a/src/app/orders/detail/[id]/page.tsx b/src/app/orders/detail/[id]/page.tsx index a7df8d95..8c3bcc4d 100644 --- a/src/app/orders/detail/[id]/page.tsx +++ b/src/app/orders/detail/[id]/page.tsx @@ -1,33 +1,39 @@ 'use client' import useGetOrdersDetail from '@/api/useGetOrdersDetail' +import useGetOrderStatus from '@/api/useGetOrderStatus' import OrderList from '@/app/orders/detail/[id]/_components/OrderList' -import OrderStatus from '@/app/orders/detail/[id]/_components/OrderStatus' +import FullpageLoader from '@/components/FullpageLoader' import Separator from '@/components/Separator' +import { useQueryClient } from '@tanstack/react-query' import { usePathname } from 'next/navigation' -import { useEffect, useState } from 'react' +import { useEffect, useMemo } from 'react' +import OrderStatus from './_components/OrderStatus' const OrderDetailPage = () => { + const queryClient = useQueryClient() const path = usePathname() - const { ordersDetail, resetGetOrdersDetail, isSuccess } = useGetOrdersDetail( - path.split('/').pop() - ) - - const [status, setStatus] = useState(null) + const orderId = useMemo(() => { + return path.split('/').pop() + }, [path]) + const { ordersDetail, resetGetOrdersDetail } = useGetOrdersDetail(orderId) + const { status } = useGetOrderStatus(orderId) useEffect(() => { - if (ordersDetail) { - console.log(ordersDetail.status.desc) - setStatus(ordersDetail.status.desc) - } - }, [ordersDetail]) + if (!status) return + + queryClient.invalidateQueries({ + queryKey: ['orders'], + }) + resetGetOrdersDetail() + }, [status]) - if (!ordersDetail) return
주문 상세 정보를 불러오는 중입니다.
+ if (!ordersDetail) return return (
- {status && } + {status && }
{ordersDetail && }
diff --git a/src/app/pay/_components/MenuItem.tsx b/src/app/pay/_components/MenuItem.tsx index da379e19..928308c8 100644 --- a/src/app/pay/_components/MenuItem.tsx +++ b/src/app/pay/_components/MenuItem.tsx @@ -23,7 +23,7 @@ const MenuItem = ({ menu, onIncrease, onDecrease, onRemove }: MenuItemProps) => } return ( -
+
{menu.imageUrl ? ( diff --git a/src/app/pay/_components/OrderInfo.tsx b/src/app/pay/_components/OrderInfo.tsx index 9a84d7cf..2f92907a 100644 --- a/src/app/pay/_components/OrderInfo.tsx +++ b/src/app/pay/_components/OrderInfo.tsx @@ -4,42 +4,48 @@ import useDeleteCart from '@/api/useDeleteCarts' import useGetCarts from '@/api/useGetCarts' import useGetStoreDetail from '@/api/useGetStoreDetail' import usePatchCarts from '@/api/usePatchCarts' -import usePostOrderPay, { OrderPay } from '@/api/usePostOrderPay' -import usePostPayment from '@/api/usePostPayment' +import usePostOrderPay, { OrderPay, OrderPayResponse, OrderPayType } from '@/api/usePostOrderPay' import MenuItem from '@/app/pay/_components/MenuItem' import Alert from '@/components/Alert' import { Button } from '@/components/button' -import Confirm from '@/components/Confirm' import Icon from '@/components/Icon' import Separator from '@/components/Separator' import { Checkbox } from '@/components/shadcn/checkbox' import { Label } from '@/components/shadcn/label' +import useBottomSheet from '@/hooks/useBottomSheet' +import { useToast } from '@/hooks/useToast' import { cn } from '@/lib/utils' +import addressStore from '@/store/addressStore' import { modalStore } from '@/store/modal' import memberStore from '@/store/user' import { ROUTE_PATHS } from '@/utils/routes' -import { useQueryClient } from '@tanstack/react-query' +import { pay200SDK } from '@pay200/sdk' +import { ANONYMOUS, loadTossPayments } from '@tosspayments/tosspayments-sdk' import { useRouter } from 'next/navigation' import { useEffect, useMemo, useState } from 'react' +import OrderPayBottomSheet from './OrderPayBottomSheet' const OrderInfo = () => { - const queryClient = useQueryClient() const router = useRouter() const { carts, resetCarts } = useGetCarts() - const [cartsState, setCartsState] = useState(carts) const { mutate: deleteCarts } = useDeleteCart() const { mutate: updateCarts } = usePatchCarts() + const { storeDetail } = useGetStoreDetail(carts?.storeId || null) + const { mutate: orderPay, data: orderResponse } = usePostOrderPay() - const { storeDetail } = useGetStoreDetail(Number(carts?.storeId) || null) - const { member } = memberStore() - const { showModal, hideModal, modals } = modalStore() - + const [cartsState, setCartsState] = useState(carts) const [isExcludingSpoon, setIsExcludingSpoon] = useState(false) - const [deliveryPrice, setDeliveryPrice] = useState(0) + const [paymentType, setPaymentType] = useState(null) + const [deliveryPrice] = useState(0) - const { mutate: orderPay, isSuccess, data: orderResponse } = usePostOrderPay() - const { mutate: payment, isSuccess: paymentSuccess } = usePostPayment() + const { member } = memberStore() + const { showModal } = modalStore() + const { address } = addressStore() + // const { payments, setPayments } = successPaymentStore() + + const { BottomSheet, hide } = useBottomSheet() + const { toast } = useToast() const handleEmptyCart = () => { if (!cartsState) return @@ -52,27 +58,6 @@ const OrderInfo = () => { ) } const handleIncreaseQuantity = (cartId: number) => { - // TODO: 테스트용 -> 배포 시 아래걸로 바꾸기 - // const updateCartsState = (newQuantity: number) => { - // setCartsState((prev) => { - // if (!prev) return - // return { - // storeId: prev.storeId, - // orderMenus: prev.orderMenus.map(menu => { - // if (menu.menuId !== menuId) return menu - - // const unitPrice = menu.totalPrice / menu.quantity - // console.log(unitPrice, menu.quantity) - // return { - // ...menu, - // quantity: menu.quantity + 1, - // totalPrice: Math.round(unitPrice * (menu.quantity + 1)) - // } - // }) - // } - // }) - // } - const updateCartsState = (newQuantity: number) => { setCartsState((prev) => { if (!prev) return @@ -107,26 +92,6 @@ const OrderInfo = () => { } const handleDecreaseQuantity = (cartId: number) => { - // TODO: 테스트용 -> 배포 시 아래걸로 바꾸기 - // const updateCartsState = (newQuantity: number) => { - // setCartsState((prev) => { - // if (!prev) return - // return { - // storeId: prev.storeId, - // orderMenus: prev.orderMenus.map(menu => { - // if (menu.menuId !== menuId) return menu - - // const unitPrice = menu.totalPrice / menu.quantity - // console.log(unitPrice, menu.quantity) - // return { - // ...menu, - // quantity: menu.quantity - 1, - // totalPrice: Math.round(unitPrice * (menu.quantity - 1)) - // } - // }) - // } - // }) - // } const updateCartsState = (newQuantity: number) => { setCartsState((prev) => { if (!prev) return @@ -172,8 +137,8 @@ const OrderInfo = () => { deleteCarts({ cartIds: [cartId] }, { onSuccess: updateCartsState }) } - const handleOrderPay = () => { - if (!cartsState || !member) { + const handleOrderPay = async () => { + if (!cartsState || !member || !address) { showModal({ content: ( { return } + if (!paymentType) { + toast({ + description: '결제수단을 선택해주세요.', + position: 'center', + }) + return + } + const orderData: OrderPay = { storeId: cartsState.storeId, - roadAddress: member.roadAddress || '', - jibunAddress: member.jibunAddress || '', - detailAddress: member.detailAddress || '', + roadAddress: address?.defaultAddress?.roadAddress || '', + jibunAddress: address?.defaultAddress?.jibunAddress || '', + detailAddress: address?.defaultAddress?.detailAddress || '', excludingSpoonAndFork: isExcludingSpoon, orderType: 'DELIVERY', - paymentType: 'TOSS_PAY', + // paymentType, + paymentType: OrderPayType.TOSS, orderMenus: cartsState.orderMenus.map((item) => { return { id: item.menuId, - quantity: item.quantity, orderMenuOptionGroups: item.orderMenuOptionGroups.map((group) => ({ id: group.id, @@ -210,7 +183,6 @@ const OrderInfo = () => { orderPay(orderData) } - const totalMenuPrice = useMemo(() => { if (!cartsState) return 0 return Object.values(cartsState.orderMenus).reduce((acc, menu) => { @@ -218,50 +190,109 @@ const OrderInfo = () => { }, 0) }, [cartsState]) + const isUnderMinOrder = useMemo(() => { + if (!storeDetail) return true + return totalMenuPrice < storeDetail.minimumOrderAmount + }, [storeDetail, cartsState]) + + const handleSelectPaymentType = (type: OrderPayType) => { + setPaymentType(type) + hide() + } + + const handleSelectOrderPay = () => { + BottomSheet({ + title: '결제 수단', + content: ( + + ), + }) + } + + const handleSelectRiderRequest = () => { + toast({ + description: '준비중입니다.', + position: 'center', + }) + } + + async function requestPayment(orderResponse: OrderPayResponse) { + if (!process.env.NEXT_PUBLIC_TOSS_PAY_KEY || !process.env.NEXT_PUBLIC_PAY200_KEY) { + toast({ + description: '결제 수단 설정에 문제가 있습니다.', + position: 'center', + }) + return + } + + if (paymentType === OrderPayType.PAY200) { + // SDK 초기화 + const requestPayment = pay200SDK({ + apiKey: process.env.NEXT_PUBLIC_PAY200_KEY, + }) + + try { + await requestPayment({ + orderId: orderResponse.orderId, + amount: orderResponse.totalPrice, + orderName: '개발의 민족 주문', + successUrl: `${window.location.origin}/pay/success`, + }) + } catch (error) { + console.error('결제 중 오류가 발생했습니다:', error) + } + } else if (paymentType === OrderPayType.TOSS) { + try { + const tossPayments = await loadTossPayments(process.env.NEXT_PUBLIC_TOSS_PAY_KEY) + + const payment = tossPayments.payment({ customerKey: ANONYMOUS }) + + await payment.requestPayment({ + method: 'CARD', // 카드 및 간편결제 + amount: { + currency: 'KRW', + value: orderResponse.totalPrice, + }, + orderId: orderResponse.orderId, // 고유 주문번호 + orderName: '개발의 민족 주문', + successUrl: window.location.origin + '/pay/success', // 결제 요청이 성공하면 리다이렉트되는 URL + failUrl: window.location.origin + '/pay/fail', // 결제 요청이 실패하면 리다이렉트되는 URL + // 가상계좌 안내, 퀵계좌이체 휴대폰 번호 자동 완성에 사용되는 값입니다. 필요하다면 주석을 해제해 주세요. + // customerMobilePhone: "01012341234", + card: { + useEscrow: false, + // flowMode: 'DIRECT', + flowMode: 'DEFAULT', + // cardCompany: 'TOSSBANK', + useCardPoint: false, + useAppCardOnly: false, + }, + }) + } catch (error) { + console.log(error) + } + } + } + useEffect(() => { return () => { resetCarts() } }, []) + useEffect(() => { setCartsState(carts) }, [carts]) useEffect(() => { if (orderResponse) { - payment({ - orderId: orderResponse.orderId, - paymentKey: '', - amount: orderResponse.totalPrice, - }) + requestPayment(orderResponse) } }, [orderResponse]) - useEffect(() => { - if (paymentSuccess) { - queryClient.invalidateQueries({ queryKey: ['orders'] }) - - showModal({ - content: ( - { - handleEmptyCart() - router.push(`${ROUTE_PATHS.ORDERS_DETAIL}/${orderResponse?.orderId}`) - }} - cancelText="홈으로" - onCancelClick={() => { - handleEmptyCart() - router.push(ROUTE_PATHS.HOME) - }} - /> - ), - }) - } - }, [paymentSuccess]) - useEffect(() => { if (cartsState && cartsState.orderMenus.length === 0) { setCartsState(undefined) @@ -269,11 +300,6 @@ const OrderInfo = () => { } }, [cartsState]) - const isUnderMinOrder = useMemo(() => { - if (!storeDetail) return true - return totalMenuPrice < storeDetail.minimumOrderAmount - }, [storeDetail, cartsState]) - if (!cartsState || !storeDetail) { return (
@@ -290,31 +316,37 @@ const OrderInfo = () => { } return ( -
+
가게배달
49~64분 후 도착
-
+ {/*
-
+
*/}
-
-
- -
{member?.roadAddress}
-
(으)로 배달
+
+
+
+ +
+ {`${address?.defaultAddress?.roadAddress} ${address?.defaultAddress?.detailAddress}`} +
+
(으)로 배달
+
+ +
+
+
+ [지번] {address?.defaultAddress?.jibunAddress} +
- -
-
-
{member?.jibunAddress}
-
+
router.push(`${ROUTE_PATHS.STORE_DETAIL}/${storeDetail.id}`)} @@ -329,7 +361,7 @@ const OrderInfo = () => {
-
+
{cartsState.orderMenus.map((menu, index) => ( { ))}
-
+
{
-
+
가게 요청사항
{/* */}
@@ -371,17 +403,28 @@ const OrderInfo = () => {
라이더 요청사항
-
+
요청사항 없음
-
+
결제수단
-
-
결제수단을 선택해주세요
+
+
+ {!paymentType ? ( + '결제수단을 선택해주세요' + ) : ( + + {paymentType === OrderPayType.TOSS ? '토스페이' : 'PAY200'} + + )} +
diff --git a/src/app/pay/_components/OrderPayBottomSheet.tsx b/src/app/pay/_components/OrderPayBottomSheet.tsx new file mode 100644 index 00000000..41df7265 --- /dev/null +++ b/src/app/pay/_components/OrderPayBottomSheet.tsx @@ -0,0 +1,86 @@ +'use client' + +import { OrderPayType } from '@/api/usePostOrderPay' +import PayLogo from '@/assets/images/pay200_log_img.png' +import Toss from '@/assets/images/toss_logo_img.png' +import Separator from '@/components/Separator' +import { cn } from '@/lib/utils' +import Image from 'next/image' +import { useState } from 'react' +interface OrderPayBottomSheetProps { + currentPaymentType: OrderPayType | null + onSelectPaymentType: (type: OrderPayType) => void +} + +const OrderPayBottomSheet = ({ + currentPaymentType, + onSelectPaymentType, +}: OrderPayBottomSheetProps) => { + const [paymentType, setPaymentType] = useState(currentPaymentType) + const handleSelectPaymentType = (type: OrderPayType) => { + setPaymentType(type) + onSelectPaymentType(type) + } + return ( +
+
+ + +
+ + + +
+
+

결제 안내

+

+ • 실제로 결제가 이루어지지 않는 테스트 페이지입니다. +

+

• Pay200로 결제 시 할인 혜택을 받을 수 있습니다.

+
+
+
+ ) +} + +export default OrderPayBottomSheet + +const PayButton = ({ + type, + paymentType, + onSelectPaymentType, +}: { + type: OrderPayType + paymentType: OrderPayType | null + onSelectPaymentType: (type: OrderPayType) => void +}) => { + return ( +
+ +
+ ) +} diff --git a/src/app/pay/success/_components/PaySuccess.tsx b/src/app/pay/success/_components/PaySuccess.tsx new file mode 100644 index 00000000..ae3ad583 --- /dev/null +++ b/src/app/pay/success/_components/PaySuccess.tsx @@ -0,0 +1,90 @@ +'use client' + +import usePostPayment from '@/api/usePostPayment' +import Alert from '@/components/Alert' +import Confirm from '@/components/Confirm' +import Icon from '@/components/Icon' +import { ApiErrorResponse } from '@/lib/api' +import { modalStore } from '@/store/modal' +import { ROUTE_PATHS } from '@/utils/routes' +import { useRouter } from 'next/navigation' +import { useEffect } from 'react' + +const PaySuccess = () => { + const params = new URLSearchParams(location.search) + const orderId = params.get('orderId') + const paymentKey = params.get('paymentKey') + const amount = params.get('amount') + + const { showModal } = modalStore() + const router = useRouter() + const { mutate: payment } = usePostPayment() + + useEffect(() => { + if (orderId && paymentKey && amount) { + payment( + { + orderId: orderId, + paymentKey: paymentKey, + amount: Number(amount), + }, + { + onSuccess: () => { + showModal({ + content: ( + { + router.replace(ROUTE_PATHS.HOME) + }} + confirmText="주문 상세" + onConfirmClick={() => { + router.replace(`${ROUTE_PATHS.ORDERS_DETAIL}/${orderId}`) + }} + /> + ), + }) + }, + onError: (error) => { + const errorData = error as unknown as ApiErrorResponse + + showModal({ + content: ( + { + router.replace(ROUTE_PATHS.HOME) + }} + /> + ), + }) + }, + } + ) + } else { + showModal({ + content: ( + { + router.push(ROUTE_PATHS.PAY) + }} + /> + ), + }) + } + }, []) + + return ( +
+ +

결제가 진행중입니다...

+
+ ) +} + +export default PaySuccess diff --git a/src/app/pay/success/page.tsx b/src/app/pay/success/page.tsx new file mode 100644 index 00000000..a87f9ca4 --- /dev/null +++ b/src/app/pay/success/page.tsx @@ -0,0 +1,7 @@ +import PaySuccess from './_components/PaySuccess' + +const PaySuccessTossPage = () => { + return +} + +export default PaySuccessTossPage diff --git a/src/app/reviews/_components/CompletedReview.tsx b/src/app/reviews/_components/CompletedReview.tsx index c2ebc5b9..57b2f6d2 100644 --- a/src/app/reviews/_components/CompletedReview.tsx +++ b/src/app/reviews/_components/CompletedReview.tsx @@ -53,8 +53,6 @@ const CompletedReview = ({ review, offSeparator }: CompletedReviewProps) => { const handleClickDeleteButton = () => { showModal({ content: , - useAnimation: true, - useDimmedClickClose: true, }) } @@ -145,7 +143,7 @@ const CompletedReview = ({ review, offSeparator }: CompletedReviewProps) => { {!representativeImageErrors[review.reviewId] && review.representativeImageUri && ( 리뷰 사진 { const { hideModal } = modalStore() - const { mutate: postReview } = usePostReview() - const { mutate: patchReview } = usePatchReview() + const { mutate: postReview, isPending: isPosting } = usePostReview() + const { mutate: patchReview, isPending: isPatching } = usePatchReview() const { toast } = useToast() const { register, handleSubmit, watch, setValue } = useForm({ @@ -48,7 +51,7 @@ const ReviewEditorModal = ({ content: prevData?.clientReviewContent || '', deliveryQuality: prevData?.deliveryQuality || '', image: null, - imagePreview: prevData?.representativeImageUri || null, + imagePreview: prevData?.representativeImageUri + `?v=${Date.now()}` || null, isImageChanged: false, }, }) @@ -58,7 +61,6 @@ const ReviewEditorModal = ({ const quantityScore = watch('quantityScore') const content = watch('content') const deliveryQuality = watch('deliveryQuality') - const imagePreview = watch('imagePreview') const isFormValid = @@ -67,7 +69,18 @@ const ReviewEditorModal = ({ quantityScore > 0 && content.length >= 5 && (deliveryQuality === 'GOOD' || deliveryQuality === 'BAD') + const [isContentValid, setIsContentValid] = useState(true) + const handleBlurContent = () => { + if (content.length < 5) { + setIsContentValid(false) + } else { + setIsContentValid(true) + } + } + const handleFocusContent = () => { + setIsContentValid(true) + } const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return @@ -144,97 +157,122 @@ const ReviewEditorModal = ({ }, } ) - console.log('🚀 data.image:', data.image) } } return ( -
-
- -
{storeName}
-
-
이 가게를 추천하시겠어요?
-
{orderSummary}
+ <> + {(isPosting || isPatching) && } +
+
+ +
{storeName}
+
- setValue('totalScore', value)} - size={40} - /> -
- setValue('tasteScore', value)} - /> - setValue('quantityScore', value)} - /> -
-
-