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 && (
+
+ )
}
-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 (
- <>
+
+
+
{
+ 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
+ />
+ )}
+
-
요기로 배달
+
+ {isPending || isUpdatePending ? (
+
+ ) : (
+ 등록하기
+ )}
+
)
}
-const AddressConfirmModal = ({ onAddress, isAddress }) => {
- const { hideModal } = modalStore()
-
- return (
-
-
주소를 등록하시겠습니까?
-
-
- 아니요
-
-
- {isAddress ? : 등록}
-
-
-
- )
-}
+// const AddressConfirmModal = ({ onAddress, isAddress }) => {
+// const { hideModal } = modalStore()
+
+// return (
+//
+//
주소를 등록하시겠습니까?
+//
+//
+// 아니요
+//
+//
+// {isAddress ? : 등록}
+//
+//
+//
+// )
+// }
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 (
-
+
-
-
-
-
{member?.roadAddress}
-
(으)로 배달
+
+
+
+
+
+ {`${address?.defaultAddress?.roadAddress} ${address?.defaultAddress?.detailAddress}`}
+
+
(으)로 배달
+
+
+
+
+
+ [지번] {address?.defaultAddress?.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 (
+
+
onSelectPaymentType(type)}
+ >
+
+
+
+
+ {type === OrderPayType.TOSS ? '토스페이' : 'PAY200'}
+
+
+ )
+}
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 (
-
-
-
이 가게를 추천하시겠어요?
-
{orderSummary}
+ <>
+ {(isPosting || isPatching) &&
}
+
+
-
setValue('totalScore', value)}
- size={40}
- />
-
- setValue('tasteScore', value)}
- />
- setValue('quantityScore', value)}
- />
-
-
-
-
{content.length}/1000
-
-
-
사진 등록하기 (선택)
-
- {!imagePreview ? (
-
- ) : (
-
-
-
-
-
+
+
이 가게를 추천하시겠어요?
+
{orderSummary}
+
+
setValue('totalScore', value)}
+ size={40}
+ />
+
+ setValue('tasteScore', value)}
+ />
+ setValue('quantityScore', value)}
+ />
+
+
+
+
+ {!isContentValid && (
+ 최소 5자 이상 작성해주세요.
+ )}
+ {content.length}/1000
- )}
-
-
-
-
배달은 어떠셨어요?
-
-
setValue('deliveryQuality', 'GOOD')}
- >
- 좋아요
-
setValue('deliveryQuality', 'BAD')}
- >
- 아쉬워요
+
+
사진 등록하기 (선택)
+
+ {!imagePreview ? (
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+
+
배달은 어떠셨어요?
+
+
setValue('deliveryQuality', 'GOOD')}
+ >
+ 좋아요
+
+
setValue('deliveryQuality', 'BAD')}
+ >
+ 아쉬워요
+
+
+
+
+
+ 리뷰 등록하기
+
+
-
- 리뷰 등록하기
-
-
+ >
)
}
diff --git a/src/app/reviews/_components/ReviewTab.tsx b/src/app/reviews/_components/ReviewTab.tsx
index 40128373..81b58e81 100644
--- a/src/app/reviews/_components/ReviewTab.tsx
+++ b/src/app/reviews/_components/ReviewTab.tsx
@@ -41,7 +41,7 @@ const ReviewTab = ({
}}
transition={{
type: 'spring',
- stiffness: 200,
+ stiffness: 400,
damping: 40,
}}
/>
diff --git a/src/app/store/detail/[id]/_components/StoreOrderDetail.tsx b/src/app/store/detail/[id]/_components/StoreOrderDetail.tsx
index 0df94892..f3bffd31 100644
--- a/src/app/store/detail/[id]/_components/StoreOrderDetail.tsx
+++ b/src/app/store/detail/[id]/_components/StoreOrderDetail.tsx
@@ -235,7 +235,7 @@ const StoreOrderDetail = ({ minimumOrderAmount }: { minimumOrderAmount: number }
if (!orderDetail) return null
return createPortal(
-
+
{
const [isMounted, setIsMounted] = useState(false)
- const [isLoaded, setIsLoaded] = useState(false)
- const [coordinates, setCoordinatesState] = useState<{
- latitude: number
- longitude: number
- } | null>(null)
- const { address, error, setCoordinates, setAddress, setError, setIsLoading } =
- useGeoLocationStore()
+
const pathname = usePathname()
const HIDDEN_BOTTOM_NAV_PATHS = [
@@ -41,18 +31,29 @@ const CommonLayout = ({ children }: CommonLayoutProps) => {
ROUTE_PATHS.MYPAGE_EDIT_PROFILE,
ROUTE_PATHS.PAY,
ROUTE_PATHS.ORDERS_DETAIL,
+ ROUTE_PATHS.ADDRESS,
+ ROUTE_PATHS.ADDRESS_DETAIL,
]
const { storedValue: accessToken } = useLocalStorage('accessToken')
- const { data: memberData, isFetching, refetch } = useGetMember()
+
+ const { refetch } = useGetMember()
const { mutate: logout } = usePostLogout()
- const { setMember } = memberStore()
- const { setIsGlobalLoading } = globalLoaderStore()
+ const { address } = useGetAddress()
+ const { mutate: getGeolocationToAddress } = useGetGeolocationToAddress()
+
+ const { member, setMember } = memberStore()
+ const {
+ setCoordinates,
+ setAddress,
+ address: addressStoreAddress,
+ setError,
+ setIsLoading,
+ } = useGeoLocationStore()
+ const { isGlobalLoading, setIsGlobalLoading } = globalLoaderStore()
useEffect(() => {
if (accessToken) {
- setIsGlobalLoading(true)
-
refetch()
.then((res) => {
if (res.error) {
@@ -67,13 +68,13 @@ const CommonLayout = ({ children }: CommonLayoutProps) => {
.catch((error) => {
logout()
})
- .finally(() => {
- setIsGlobalLoading(false)
- })
}
}, [accessToken])
useEffect(() => {
+ setIsLoading(true)
+ setIsGlobalLoading(true)
+
// 메인 페이지 스크롤 위치 제거
sessionStorage.removeItem('homeScrollPosition')
setIsMounted(true)
@@ -100,79 +101,59 @@ const CommonLayout = ({ children }: CommonLayoutProps) => {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
}
- setCoordinatesState(coords)
- setCoordinates(coords)
- setIsLoading(false)
+
+ if (!addressStoreAddress || !member) {
+ getGeolocationToAddress(
+ {
+ latitude: coords.latitude.toString(),
+ longitude: coords.longitude.toString(),
+ },
+ {
+ onSuccess: (data) => {
+ const address = data.documents[0]
+
+ setAddress({
+ addressName: address.address.address_name,
+ sido: address.address.region_1depth_name,
+ sigungu: address.address.region_2depth_name,
+ roadAddress: address.road_address?.address_name || '',
+ jibunAddress: address.address.address_name,
+ })
+ },
+ onError: (error) => {
+ console.log(error)
+ },
+ onSettled: () => {
+ setCoordinates(coords)
+ setIsLoading(false)
+ setIsGlobalLoading(false)
+ },
+ }
+ )
+ } else {
+ setCoordinates(coords)
+ setIsLoading(false)
+ setIsGlobalLoading(false)
+ }
},
(error) => {
- console.error('위치 정보 에러:', error)
+ console.log('위치 정보 에러:', error)
setError('위치 정보를 가져오는데 실패했습니다.')
}
)
}
} catch (error) {
- console.error('위치 정보 권한 확인 에러:', error)
+ console.log('위치 정보 권한 확인 에러:', error)
setError('위치 정보 권한을 확인하는데 실패했습니다.')
+ setIsGlobalLoading(false)
}
}
requestGeolocation()
}, [isMounted])
- useEffect(() => {
- if (!isMounted) return
-
- // 카카오맵 스크립트 로드
- 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(() => {
- setIsLoaded(true)
- })
- })
- document.head.appendChild(script)
- return () => {
- document.head.removeChild(script)
- }
- }, [isMounted])
-
- useEffect(() => {
- if (!isLoaded || !coordinates) return
-
- const geocoder = new window.kakao.maps.services.Geocoder()
- geocoder.coord2Address(
- coordinates.longitude,
- coordinates.latitude,
- (result: any, status: any) => {
- if (status === window.kakao.maps.services.Status.OK) {
- const roadAddr = result[0].road_address
- const jibunAddr = result[0].address
-
- const address = roadAddr
- ? {
- roadAddress: roadAddr.address_name,
- jibunAddress: jibunAddr.address_name,
- sido: roadAddr.region_1depth_name,
- sigungu: roadAddr.region_2depth_name,
- addressName: roadAddr.address_name,
- }
- : {
- roadAddress: jibunAddr.address_name,
- jibunAddress: jibunAddr.address_name,
- sido: jibunAddr.region_1depth_name,
- sigungu: jibunAddr.region_2depth_name,
- addressName: jibunAddr.address_name,
- }
- setAddress(address)
- }
- }
- )
- }, [isLoaded, coordinates])
-
// 클라이언트 사이드 렌더링 전에는 로딩 상태 표시
- if (!isMounted) return
+ if (!isMounted || isGlobalLoading) return
// if (error) return
// if (!address) return
diff --git a/src/components/Confirm.tsx b/src/components/Confirm.tsx
index e8fdbbf3..616c2dab 100644
--- a/src/components/Confirm.tsx
+++ b/src/components/Confirm.tsx
@@ -22,7 +22,6 @@ const Confirm = ({
const handleConfirmClick = () => {
hideModal()
- console.log({onConfirmClick})
onConfirmClick()
}
diff --git a/src/components/FullpageLoader.tsx b/src/components/FullpageLoader.tsx
new file mode 100644
index 00000000..823a6100
--- /dev/null
+++ b/src/components/FullpageLoader.tsx
@@ -0,0 +1,31 @@
+import Icon from '@/components/Icon'
+import { cn } from '@/lib/utils'
+
+interface FullpageLoaderProps {
+ useNavigation?: boolean
+ useBottomNavigation?: boolean
+ className?: string
+}
+
+const FullpageLoader = ({
+ useNavigation = false,
+ useBottomNavigation = false,
+ className,
+}: FullpageLoaderProps) => {
+ return (
+
+ )
+}
+
+export default FullpageLoader
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
index 7bfbe948..ceed3e92 100644
--- a/src/components/Loading.tsx
+++ b/src/components/Loading.tsx
@@ -3,7 +3,7 @@ import LoadingImage from '@/assets/images/loading.gif'
const Loading = () => {
return (
-
+
diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx
index 6f7c8f50..123b7c80 100644
--- a/src/components/Navigation.tsx
+++ b/src/components/Navigation.tsx
@@ -1,8 +1,14 @@
'use client'
+import { cn } from '@/lib/utils'
+import addressStore from '@/store/addressStore'
import { useGeoLocationStore } from '@/store/geoLocation'
+import { modalStore } from '@/store/modal'
+import memberStore from '@/store/user'
+import { ROUTE_PATHS } from '@/utils/routes'
import { useRouter } from 'next/navigation'
import Icon from './Icon'
+import LoginModal from './shared/LoginModal'
export interface NavigationProps {
hasBackButton?: boolean
@@ -25,6 +31,9 @@ const Navigation = ({
}: NavigationProps) => {
const router = useRouter()
const { address } = useGeoLocationStore()
+ const { member } = memberStore()
+ const { address: addressStoreAddress } = addressStore()
+ const { showModal } = modalStore()
return (
diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx
index c1833030..dfaa1862 100644
--- a/src/components/Toast.tsx
+++ b/src/components/Toast.tsx
@@ -10,7 +10,7 @@ import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const toastVariants = cva(
- 'absolute group pointer-events-auto flex size-fit items-center justify-between space-x-4 overflow-hidden rounded-2xl border py-3 px-4 shadow-lg data-[state=closed]:fade-out-30 data-[state=closed]:animate-fade-out',
+ 'absolute group pointer-events-auto flex size-fit items-center justify-between space-x-4 overflow-hidden rounded-2xl border py-3 px-4 shadow-lg data-[state=closed]:fade-out-30 data-[state=closed]:animate-fade-out max-w-[480px] m-auto',
{
variants: {
variant: {
@@ -40,7 +40,7 @@ const ToastViewport = React.forwardRef<
= {
[ROUTE_PATHS.HOME]: {
hasBackButton: false,
- useAddress: false,
+ useAddress: true,
title: '홈',
rightElement: (
diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts
index 11219341..d9ee3ebd 100644
--- a/src/hooks/useInfiniteScroll.ts
+++ b/src/hooks/useInfiniteScroll.ts
@@ -1,7 +1,6 @@
'use client'
import { api } from '@/lib/api'
-import { isMockingMode, useMockReady } from '@/providers/MockProvider'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useRef } from 'react'
@@ -20,6 +19,7 @@ interface InfiniteScrollOptions {
root?: Element | null
rootMargin?: string
location?: { lat: number; lng: number }
+ enabled?: boolean
}
export const useInfiniteScroll = ({
@@ -27,15 +27,16 @@ export const useInfiniteScroll = ({
endpoint,
filter,
size = 10,
- threshold = 0.1,
+ threshold = 0,
root = null,
rootMargin = '0px',
location = { lat: 37.5177, lng: 127.0473 },
+ enabled = true,
}: InfiniteScrollOptions) => {
const observerRef = useRef(null)
const targetRef = useRef(null)
const timeoutRef = useRef(null)
- const isMockReady = useMockReady()
+ // const isMockReady = useMockReady()
const {
data,
@@ -77,7 +78,8 @@ export const useInfiniteScroll = ({
},
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: 1,
- enabled: isMockingMode ? isMockReady : true,
+ enabled: enabled,
+ // placeholderData: keepPreviousData,
})
const handleObserver = useCallback(
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 34882fb0..b0bcbaaf 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -48,6 +48,19 @@ export const api = {
return handleError(error)
}
},
+ put: async (endpoint: string, body: unknown, options?: Options): Promise => {
+ try {
+ const response = await kyClient
+ .put(endpoint, {
+ ...(body instanceof FormData ? { body } : { json: body }),
+ ...options,
+ })
+ .json>()
+ return response.data
+ } catch (error) {
+ return handleError(error)
+ }
+ },
delete: async (endpoint: string, options?: Options): Promise => {
try {
const response = await kyClient.delete(endpoint, options).json>()
diff --git a/src/mocks/handlers/index.ts b/src/mocks/handlers/index.ts
index 1d41837c..de897b82 100644
--- a/src/mocks/handlers/index.ts
+++ b/src/mocks/handlers/index.ts
@@ -1,6 +1,8 @@
import BANNER_MOCK_DATA from '@/constants/banners'
import STORE_MOCK_DATA from '@/constants/stores'
import { delay, http, HttpResponse, passthrough } from 'msw'
+import { DUMMY_ORDER_LIST } from './orders'
+import { ORDER_STATUS, OrderStatus } from '@/api/useGetOrdersDetail'
// API 엔드포인트 예시
export const handlers = [
@@ -171,6 +173,65 @@ export const handlers = [
return passthrough()
}),
+ http.get('*/api/v1/orders', () => {
+ return HttpResponse.json({
+ status: 200,
+ message: 'OK',
+ data: {
+ totalCount: null,
+ nextCursor: null,
+ content: [
+ {
+ ...DUMMY_ORDER_LIST[0],
+ storeId: '1',
+ orderSummary: '새우 로제 파스타 외 2개',
+ imageThumbnail: null,
+ },
+ ],
+ },
+ })
+ }),
+
+ http.get('*/api/v1/orders/:orderId', ({ request, params }) => {
+ const orderId = decodeURIComponent(params.orderId as string)
+
+ const orderList = DUMMY_ORDER_LIST.filter((order) => order.orderId === orderId)
+ return HttpResponse.json({
+ status: 200,
+ message: 'success',
+ data: orderList.length === 1 ? orderList[0] : DUMMY_ORDER_LIST[0],
+ })
+ }),
+
+ http.get('*/api/v1/orders/:orderId/status', ({ request, params }) => {
+ const orderId = params.orderId as string
+
+ const getNextStatus = (currentStatus: ORDER_STATUS): OrderStatus => {
+ switch (currentStatus.desc) {
+ case '주문대기': // S1
+ return 'NEW'
+ case '주문접수': // S2
+ return 'ONGOING'
+ case '주문수락': //S3
+ return 'DONE'
+ case '주문거절': // S4
+ return 'REFUSE'
+ case '주문완료': // S5
+ return 'DONE'
+ case '주문취소': // S6
+ return 'CANCELED'
+ default:
+ return 'DONE'
+ }
+ }
+ const currentOrder =
+ DUMMY_ORDER_LIST.filter((order) => order.orderId === orderId)[0] ?? DUMMY_ORDER_LIST[0]
+ return HttpResponse.json({
+ status: 200,
+ message: 'success',
+ data: getNextStatus(currentOrder.status),
+ })
+ }),
// Get Menu Options
http.get('*/api/v1/stores/:id/menus/:menuId/options', async ({ request }) => {
return passthrough()
diff --git a/src/mocks/handlers/orders.ts b/src/mocks/handlers/orders.ts
new file mode 100644
index 00000000..346ab274
--- /dev/null
+++ b/src/mocks/handlers/orders.ts
@@ -0,0 +1,124 @@
+import { OrdersDetail } from '@/api/useGetOrdersDetail'
+
+export const DUMMY_ORDER_LIST: OrdersDetail[] = [
+ {
+ orderId: '1',
+ status: { code: 'S1', desc: '주문대기' },
+ orderTime: '2025-02-25T12:30:00Z',
+ storeName: '맛있는 식당',
+ tel: '010-1234-5678',
+ roadAddress: '서울특별시 강남구 테헤란로 123',
+ jibunAddress: '서울특별시 강남구 역삼동 456-7',
+ detailAddress: '3층 305호',
+ excludingSpoonAndFork: false,
+ requestToRider: null,
+ orderPrice: 15000,
+ deliveryPrice: 3000,
+ deliveryCompleteTime: null,
+ paymentPrice: 18000,
+ paymentId: 987654,
+ paymentType: { code: 'CARD', desc: '신용카드' },
+ type: { code: 'DELIVERY', desc: '배달' },
+ orderMenus: [],
+ },
+ {
+ orderId: '2',
+ status: { code: 'S2', desc: '주문접수' },
+ orderTime: '2025-02-25T12:40:00Z',
+ storeName: '중국집',
+ tel: '010-1111-2222',
+ roadAddress: '서울특별시 송파구 올림픽로 55',
+ jibunAddress: '서울특별시 송파구 잠실동 33-2',
+ detailAddress: '101동 202호',
+ excludingSpoonAndFork: false,
+ requestToRider: '도착하면 전화 주세요.',
+ orderPrice: 18000,
+ deliveryPrice: 2000,
+ deliveryCompleteTime: null,
+ paymentPrice: 20000,
+ paymentId: 765432,
+ paymentType: { code: 'CARD', desc: '신용카드' },
+ type: { code: 'DELIVERY', desc: '배달' },
+ orderMenus: [],
+ },
+ {
+ orderId: '3',
+ status: { code: 'S3', desc: '주문수락' },
+ orderTime: '2025-02-25T12:50:00Z',
+ storeName: '분식집',
+ tel: '010-5555-6666',
+ roadAddress: '서울특별시 성동구 왕십리로 100',
+ jibunAddress: '서울특별시 성동구 왕십리동 12-1',
+ detailAddress: '405호',
+ excludingSpoonAndFork: true,
+ requestToRider: null,
+ orderPrice: 12000,
+ deliveryPrice: 2000,
+ deliveryCompleteTime: '2025-02-25T13:10:00Z',
+ paymentPrice: 14000,
+ paymentId: 543210,
+ paymentType: { code: 'CARD', desc: '신용카드' },
+ type: { code: 'DELIVERY', desc: '배달' },
+ orderMenus: [],
+ },
+ {
+ orderId: '4',
+ status: { code: 'S4', desc: '주문거절' },
+ orderTime: '2025-02-25T12:55:00Z',
+ storeName: '피자집',
+ tel: '010-7777-8888',
+ roadAddress: '서울특별시 강동구 천호대로 200',
+ jibunAddress: '서울특별시 강동구 천호동 45-6',
+ detailAddress: '102호',
+ excludingSpoonAndFork: false,
+ requestToRider: null,
+ orderPrice: 30000,
+ deliveryPrice: 2500,
+ deliveryCompleteTime: null,
+ paymentPrice: 32500,
+ paymentId: 432109,
+ paymentType: { code: 'CARD', desc: '신용카드' },
+ type: { code: 'DELIVERY', desc: '배달' },
+ orderMenus: [],
+ },
+ {
+ orderId: '5',
+ status: { code: 'S5', desc: '주문완료' },
+ orderTime: '2025-02-25T12:55:00Z',
+ storeName: '피자집',
+ tel: '010-7777-8888',
+ roadAddress: '서울특별시 강동구 천호대로 200',
+ jibunAddress: '서울특별시 강동구 천호동 45-6',
+ detailAddress: '102호',
+ excludingSpoonAndFork: false,
+ requestToRider: null,
+ orderPrice: 30000,
+ deliveryPrice: 2500,
+ deliveryCompleteTime: null,
+ paymentPrice: 32500,
+ paymentId: 432109,
+ paymentType: { code: 'CARD', desc: '신용카드' },
+ type: { code: 'DELIVERY', desc: '배달' },
+ orderMenus: [],
+ },
+ {
+ orderId: '6',
+ status: { code: 'S6', desc: '주문취소' },
+ orderTime: '2025-02-25T12:55:00Z',
+ storeName: '피자집',
+ tel: '010-7777-8888',
+ roadAddress: '서울특별시 강동구 천호대로 200',
+ jibunAddress: '서울특별시 강동구 천호동 45-6',
+ detailAddress: '102호',
+ excludingSpoonAndFork: false,
+ requestToRider: null,
+ orderPrice: 30000,
+ deliveryPrice: 2500,
+ deliveryCompleteTime: null,
+ paymentPrice: 32500,
+ paymentId: 432109,
+ paymentType: { code: 'CARD', desc: '신용카드' },
+ type: { code: 'DELIVERY', desc: '배달' },
+ orderMenus: [],
+ },
+]
diff --git a/src/mocks/index.ts b/src/mocks/index.ts
index 5851fdce..8c361659 100644
--- a/src/mocks/index.ts
+++ b/src/mocks/index.ts
@@ -4,6 +4,6 @@ export async function initMsw() {
server.listen()
} else {
const { worker } = await import('./browser')
- await worker.start()
+ await worker.start({ onUnhandledRequest: 'bypass' })
}
}
diff --git a/src/models/auth.ts b/src/models/auth.ts
index 876e8428..968602c5 100644
--- a/src/models/auth.ts
+++ b/src/models/auth.ts
@@ -38,8 +38,4 @@ export interface Member {
id: number
signname: string
nickname: string
- profileImage: string
- roadAddress?: string
- jibunAddress?: string
- detailAddress?: string
}
diff --git a/src/store/addressStore.ts b/src/store/addressStore.ts
new file mode 100644
index 00000000..1216d0fa
--- /dev/null
+++ b/src/store/addressStore.ts
@@ -0,0 +1,16 @@
+import { AddressResponseData } from '@/api/useGetAddress'
+import { create } from 'zustand'
+
+interface AddressStore {
+ address: AddressResponseData | null
+ setAddress: (address: AddressResponseData) => void
+ resetAddress: () => void
+}
+
+const addressStore = create((set) => ({
+ address: null,
+ setAddress: (address) => set({ address }),
+ resetAddress: () => set({ address: null }),
+}))
+
+export default addressStore
diff --git a/src/store/geoLocation.ts b/src/store/geoLocation.ts
index e403abcb..f3e92c09 100644
--- a/src/store/geoLocation.ts
+++ b/src/store/geoLocation.ts
@@ -1,6 +1,6 @@
import { create } from 'zustand'
-interface Coordinates {
+export interface Coordinates {
latitude: number
longitude: number
}
diff --git a/src/store/user.ts b/src/store/user.ts
index b263a3f7..c9dd85fb 100644
--- a/src/store/user.ts
+++ b/src/store/user.ts
@@ -13,9 +13,6 @@ const memberStore = create((set) => ({
set({
member: {
...member,
- roadAddress: '서울특별시 강남구 테헤란로 123',
- jibunAddress: '서울특별시 강남구 역삼동 123-45',
- detailAddress: '101호',
},
}),
resetMember: () => set({ member: null }),
diff --git a/yarn.lock b/yarn.lock
index 6e7cb426..4f04d4ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1877,6 +1877,15 @@ __metadata:
languageName: node
linkType: hard
+"@pay200/sdk@npm:^0.0.5":
+ version: 0.0.5
+ resolution: "@pay200/sdk@npm:0.0.5"
+ dependencies:
+ event-source-polyfill: "npm:^1.0.31"
+ checksum: 10c0/d8772a94e167a16e4af17ba434ec3cc0969c1dda91c048fee1d3d24a6021eb0733ef21defb3ba5fd5bbf9304a3cc521a14e144d73ee6deb49e7442d42172650a
+ languageName: node
+ linkType: hard
+
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
@@ -2720,6 +2729,13 @@ __metadata:
languageName: node
linkType: hard
+"@tosspayments/tosspayments-sdk@npm:^2.3.4":
+ version: 2.3.4
+ resolution: "@tosspayments/tosspayments-sdk@npm:2.3.4"
+ checksum: 10c0/73b5534dc6c826509ab022cf93a36c44cd459c30455a7a058a418fb10538429750511840f5cd2da1348624e9d8fddb059c6038fbf0ea35ff62e5fe16d8cd5f09
+ languageName: node
+ linkType: hard
+
"@trysound/sax@npm:0.2.0":
version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0"
@@ -4398,14 +4414,14 @@ __metadata:
languageName: node
linkType: hard
-"eslint-config-prettier@npm:^9.1.0":
- version: 9.1.0
- resolution: "eslint-config-prettier@npm:9.1.0"
+"eslint-config-prettier@npm:^10.0.2":
+ version: 10.0.2
+ resolution: "eslint-config-prettier@npm:10.0.2"
peerDependencies:
eslint: ">=7.0.0"
bin:
- eslint-config-prettier: bin/cli.js
- checksum: 10c0/6d332694b36bc9ac6fdb18d3ca2f6ac42afa2ad61f0493e89226950a7091e38981b66bac2b47ba39d15b73fff2cd32c78b850a9cf9eed9ca9a96bfb2f3a2f10d
+ eslint-config-prettier: build/bin/cli.js
+ checksum: 10c0/e0ef3c442661a26fc6e82acec5bb9a418c4a8f65ec8adf0983d3aaba7716d2ed448358b063cce6e3c272c847d14cb856ddf30031770c6571e2b2c3e2a439afd4
languageName: node
linkType: hard
@@ -4705,6 +4721,13 @@ __metadata:
languageName: node
linkType: hard
+"event-source-polyfill@npm:^1.0.31":
+ version: 1.0.31
+ resolution: "event-source-polyfill@npm:1.0.31"
+ checksum: 10c0/79966f5084796e14f9a9dec315a2ccc220dedc51ff5f2b198dc80e3cb2ae01428d39d9bf66ed679f1944be086b9f6e84ea3dc933b81b0411c07f99672135679b
+ languageName: node
+ linkType: hard
+
"exponential-backoff@npm:^3.1.1":
version: 3.1.2
resolution: "exponential-backoff@npm:3.1.2"
@@ -5742,7 +5765,7 @@ __metadata:
languageName: node
linkType: hard
-"kakao.maps.d.ts@npm:^0.1.39":
+"kakao.maps.d.ts@npm:^0.1.39, kakao.maps.d.ts@npm:^0.1.40":
version: 0.1.40
resolution: "kakao.maps.d.ts@npm:0.1.40"
checksum: 10c0/16eef0d9846311921ae3c5c020a8769ab8d1f894b2a6f1aef869a83648462982c2891bc8ea174665345290483bf73feb6ae264273cee38a40a1adbb17aa6862f
@@ -6353,6 +6376,7 @@ __metadata:
"@eslint/js": "npm:^9.17.0"
"@hookform/resolvers": "npm:^3.10.0"
"@next/eslint-plugin-next": "npm:^15.1.3"
+ "@pay200/sdk": "npm:^0.0.5"
"@radix-ui/react-accordion": "npm:^1.2.2"
"@radix-ui/react-checkbox": "npm:^1.1.3"
"@radix-ui/react-dialog": "npm:^1.1.4"
@@ -6365,6 +6389,7 @@ __metadata:
"@tanstack/react-query": "npm:^5.64.1"
"@tanstack/react-query-devtools": "npm:^5.66.3"
"@tanstack/react-virtual": "npm:^3.13.0"
+ "@tosspayments/tosspayments-sdk": "npm:^2.3.4"
"@types/eslint-plugin-tailwindcss": "npm:^3"
"@types/node": "npm:^20"
"@types/react": "npm:^19"
@@ -6376,19 +6401,20 @@ __metadata:
clsx: "npm:^2.1.1"
eslint: "npm:^9"
eslint-config-next: "npm:15.1.3"
- eslint-config-prettier: "npm:^9.1.0"
+ eslint-config-prettier: "npm:^10.0.2"
eslint-plugin-prettier: "npm:^5.2.3"
eslint-plugin-react-hooks: "npm:^5.1.0"
eslint-plugin-tailwindcss: "npm:^3.17.5"
husky: "npm:^9.1.7"
+ kakao.maps.d.ts: "npm:^0.1.40"
ky: "npm:^1.7.4"
lucide-react: "npm:^0.469.0"
motion: "npm:^11.15.0"
msw: "npm:^2.7.0"
next: "npm:15.1.3"
postcss: "npm:^8"
- prettier: "npm:^3.4.2"
- prettier-plugin-tailwindcss: "npm:^0.6.9"
+ prettier: "npm:^3.5.2"
+ prettier-plugin-tailwindcss: "npm:^0.6.11"
react: "npm:^19.0.0"
react-daum-postcode: "npm:^3.2.0"
react-dom: "npm:^19.0.0"
@@ -6813,7 +6839,7 @@ __metadata:
languageName: node
linkType: hard
-"prettier-plugin-tailwindcss@npm:^0.6.9":
+"prettier-plugin-tailwindcss@npm:^0.6.11":
version: 0.6.11
resolution: "prettier-plugin-tailwindcss@npm:0.6.11"
peerDependencies:
@@ -6871,7 +6897,7 @@ __metadata:
languageName: node
linkType: hard
-"prettier@npm:^3.4.2":
+"prettier@npm:^3.5.2":
version: 3.5.2
resolution: "prettier@npm:3.5.2"
bin: