-
Notifications
You must be signed in to change notification settings - Fork 0
feat: pyconkr-shop API 호출을 위한 hook 추가 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
1d05e3e
feat: 공통 util 관련 디렉토리 및 Cookie 관련 도구 추가
MU-Software 18ccb6e
feat: shop api 관련 디렉토리 및 hook 추가
MU-Software abfcdba
feat: 추가한 디렉토리를 패키지로 추가
MU-Software d2e5ebc
fix: 누락된 필수 라이브러리 및 몇가지 helper 라이브러리 추가
MU-Software d834e3b
fix: 누락된 PortOne 환경변수 정의 추가
MU-Software b89a8a1
fix: ErrorResponseSchema를 잘못 검증하던 문제 수정
MU-Software f2efdbe
refactor: 변수명을 좀 더 명확하게 수정
MU-Software c384573
fix: 잘못된 환경 변수를 사용하던 이슈 수정
MU-Software 042210b
refactor: 잘못 들어간 중복된 함수 제거
MU-Software fd20333
refactor: 변수명을 한번 더 명확하게 수정
MU-Software a688efa
fix: 쿠키 파싱 시 맨 처음 발견된 =만 split하도록 수정
MU-Software 7d94126
chore: 소셜로그인 시 약간의 delay를 두고 form을 삭제하도록 수정
MU-Software 178a018
chore: 로그인 방식별로 mutationKey 분리
MU-Software fdb7f1d
fix: hook을 실제 사용 용례에 맞게 수정
MU-Software 0d3ab6d
feat: 자주 사용되는 utils 및 components 추가
MU-Software d940057
fix: 주석의 잘못된 parameter 수정
MU-Software File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.dev.pycon.kr | ||
VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=DEBUG_csrftoken |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.pycon.kr | ||
VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=csrftoken |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,29 +20,41 @@ | |
"@mdx-js/react": "^3.1.0", | ||
"@mdx-js/rollup": "^3.1.0", | ||
"@mui/material": "^7.0.2", | ||
"@pyconkr-common": "link:package/pyconkr-common", | ||
"@pyconkr-shop": "link:package/pyconkr-shop", | ||
"@src": "link:src", | ||
"@suspensive/react": "^2.18.12", | ||
"@tanstack/react-query": "^5.72.2", | ||
"axios": "^1.8.4", | ||
"eslint-plugin-import": "^2.31.0", | ||
"eslint-plugin-jsx-a11y": "^6.10.2", | ||
"notistack": "^3.0.2", | ||
"react": "^19.0.0", | ||
"react-dom": "^19.0.0" | ||
"react-dom": "^19.0.0", | ||
"remeda": "^2.21.3" | ||
}, | ||
"devDependencies": { | ||
"@eslint/js": "^9.21.0", | ||
"@tanstack/react-query-devtools": "^5.74.4", | ||
"@types/node": "^22.14.1", | ||
"@types/react": "^19.0.10", | ||
"@types/react-dom": "^19.0.4", | ||
"@typescript-eslint/parser": "^8.29.1", | ||
"@vitejs/plugin-react": "^4.3.4", | ||
"csstype": "^3.1.3", | ||
"eslint": "^9.21.0", | ||
"eslint-config-prettier": "^10.1.2", | ||
"eslint-plugin-prettier": "^5.2.6", | ||
"eslint-plugin-react-hooks": "^5.1.0", | ||
"eslint-plugin-react-refresh": "^0.4.19", | ||
"gh-pages": "^6.3.0", | ||
"globals": "^15.15.0", | ||
"iamport-typings": "^1.4.0", | ||
"prettier": "^3.5.3", | ||
"typescript": "~5.7.2", | ||
"typescript-eslint": "^8.24.1", | ||
"vite": "^6.2.0", | ||
"vite-plugin-mdx": "^3.6.1" | ||
} | ||
}, | ||
"packageManager": "[email protected]+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import * as R from 'remeda' | ||
|
||
export const getCookie = (name: string) => { | ||
if (!R.isString(document.cookie) || R.isEmpty(document.cookie)) | ||
return undefined | ||
|
||
let cookieValue: string | undefined | ||
document.cookie.split(';').forEach((cookie) => { | ||
if (R.isEmpty(cookie) || !cookie.includes('=')) return | ||
const [key, value] = cookie.split('=') | ||
if (key.trim() === name) cookieValue = decodeURIComponent(value) as string | ||
}) | ||
return cookieValue | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; | ||
import * as R from 'remeda'; | ||
|
||
import { getCookie } from '@pyconkr-common/utils/cookie'; | ||
import ShopAPISchema, { isObjectErrorResponseSchema } from "@pyconkr-shop/schemas"; | ||
|
||
const DEFAULT_TIMEOUT = 10000 | ||
const DEFAULT_ERROR_MESSAGE = '알 수 없는 문제가 발생했습니다, 잠시 후 다시 시도해주세요.' | ||
const DEFAULT_ERROR_RESPONSE = { type: 'unknown', errors: [{ code: 'unknown', detail: DEFAULT_ERROR_MESSAGE, attr: null }] } | ||
|
||
export class ShopAPIClientError extends Error { | ||
readonly name = 'ShopAPIError' | ||
readonly status: number | ||
readonly detail: ShopAPISchema.ErrorResponseSchema | ||
readonly originalError: unknown | ||
|
||
constructor(error?: unknown) { | ||
let message: string = DEFAULT_ERROR_MESSAGE | ||
let detail: ShopAPISchema.ErrorResponseSchema = DEFAULT_ERROR_RESPONSE | ||
let status = -1 | ||
|
||
if (axios.isAxiosError(error)) { | ||
const response = error.response | ||
|
||
if (response) { | ||
status = response.status | ||
detail = isObjectErrorResponseSchema(response.data) ? response.data : { | ||
type: 'axios_error', | ||
errors: [{ code: 'unknown', detail: R.isString(response.data) ? response.data : DEFAULT_ERROR_MESSAGE, attr: null }] | ||
} | ||
} | ||
} else if (error instanceof Error) { | ||
message = error.message | ||
detail = { | ||
type: error.name || typeof error || 'unknown', | ||
errors: [{ code: 'unknown', detail: error.message, attr: null }], | ||
} | ||
} | ||
|
||
super(message) | ||
this.originalError = error || null | ||
this.status = status | ||
this.detail = detail | ||
} | ||
|
||
isRequiredAuth(): boolean { | ||
return this.status === 401 || this.status === 403 | ||
} | ||
} | ||
|
||
type AxiosRequestWithoutPayload = <T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>) => Promise<R> | ||
type AxiosRequestWithPayload = <T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<R> | ||
|
||
class ShopAPIClient { | ||
readonly baseURL: string | ||
protected readonly csrfCookieName: string | ||
private readonly shopAPI: AxiosInstance | ||
|
||
constructor( | ||
baseURL: string = import.meta.env.VITE_PYCONKR_SHOP_API_DOMAIN, | ||
csrfCookieName: string = import.meta.env.VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME, | ||
timeout: number = DEFAULT_TIMEOUT, | ||
) { | ||
this.baseURL = baseURL | ||
this.csrfCookieName = csrfCookieName | ||
this.shopAPI = axios.create({ baseURL, timeout, headers: { 'Content-Type': 'application/json' } }) | ||
} | ||
|
||
_safe_request_without_payload(requestFunc: AxiosRequestWithoutPayload): AxiosRequestWithoutPayload { | ||
return async <T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>) => { | ||
try { | ||
return await requestFunc<T, R, D>(url, config) | ||
} catch (error) { | ||
throw new ShopAPIClientError(error) | ||
} | ||
} | ||
} | ||
|
||
_safe_request_with_payload(requestFunc: AxiosRequestWithPayload): AxiosRequestWithPayload { | ||
return async <T = any, R = AxiosResponse<T>, D = any>(url: string, data: D, config?: AxiosRequestConfig<D>) => { | ||
try { | ||
return await requestFunc<T, R, D>(url, data, config) | ||
} catch (error) { | ||
throw new ShopAPIClientError(error) | ||
} | ||
} | ||
} | ||
|
||
getCSRFToken(): string | undefined { return getCookie(this.csrfCookieName) } | ||
|
||
async get<T, D=any>(url: string, config?: AxiosRequestConfig<D>): Promise<T> { | ||
return (await this._safe_request_without_payload(this.shopAPI.get)<T, AxiosResponse<T>, D>(url, config)).data | ||
} | ||
async post<T, D>(url: string, data: D, config?: AxiosRequestConfig<D>): Promise<T> { | ||
return (await this._safe_request_with_payload(this.shopAPI.post)<T, AxiosResponse<T>, D>(url, data, config)).data | ||
} | ||
async put<T, D>(url: string, data: D, config?: AxiosRequestConfig<D>): Promise<T> { | ||
return (await this._safe_request_with_payload(this.shopAPI.put)<T, AxiosResponse<T>, D>(url, data, config)).data | ||
} | ||
async patch<T, D>(url: string, data: D, config?: AxiosRequestConfig<D>): Promise<T> { | ||
return (await this._safe_request_with_payload(this.shopAPI.patch)<T, AxiosResponse<T>, D>(url, data, config)).data | ||
} | ||
async delete<T, D=any>(url: string, config?: AxiosRequestConfig<D>): Promise<T> { | ||
return (await this._safe_request_without_payload(this.shopAPI.delete)<T, AxiosResponse<T>, D>(url, config)).data | ||
} | ||
} | ||
|
||
export const shopAPIClient = new ShopAPIClient(import.meta.env.VITE_PYCONKR_SHOP_API_DOMAIN) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { shopAPIClient } from "./client"; | ||
|
||
import ShopAPISchema from "@pyconkr-shop/schemas"; | ||
|
||
namespace ShopAPIRoute { | ||
/** | ||
* 로그인합니다. | ||
* @param username - 사용자 이름 | ||
* @param password - 비밀번호 | ||
* @returns 로그인 정보 | ||
* @throws 401 - 로그인 정보가 없습니다. | ||
*/ | ||
export const signInWithEmail = (data: ShopAPISchema.EmailSignInRequest) => | ||
shopAPIClient.post< | ||
ShopAPISchema.UserStatus, | ||
ShopAPISchema.EmailSignInRequest | ||
>("authn/social/browser/v1/auth/login", data); | ||
|
||
/** | ||
* SNS로 로그인합니다. | ||
* @param socialSignInInfo - SNS 로그인 정보 | ||
* @param socialSignInInfo.provider - SNS 제공자 | ||
* @param socialSignInInfo.callback_url - SNS 로그인 후 리다이렉트할 URL | ||
* @returns 로그인 정보 | ||
*/ | ||
export const signInWithSNS = async ( | ||
socialSignInInfo: ShopAPISchema.SocialSignInRequest | ||
) => { | ||
const form = document.createElement("form"); | ||
form.method = "POST"; | ||
form.action = `${shopAPIClient.baseURL}/authn/social/browser/v1/auth/provider/redirect`; | ||
|
||
Object.entries({ | ||
...socialSignInInfo, | ||
csrfmiddlewaretoken: shopAPIClient.getCSRFToken() ?? "", | ||
process: "login", | ||
}).forEach(([key, value]) => { | ||
const inputElement = document.createElement("input"); | ||
inputElement.type = "hidden"; | ||
inputElement.name = key; | ||
inputElement.value = value; | ||
form.appendChild(inputElement); | ||
}); | ||
document.body.appendChild(form); | ||
form.submit(); | ||
document.body.removeChild(form); | ||
MU-Software marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
/** | ||
* 로그아웃합니다. | ||
* @throws 401 - 로그아웃이 성공할 시에도 항상 401 에러가 발생합니다. | ||
*/ | ||
export const signOut = () => | ||
shopAPIClient.delete<ShopAPISchema.UserStatus>( | ||
"authn/social/browser/v1/auth/session" | ||
); | ||
|
||
/** | ||
* 로그인 정보를 조회합니다. | ||
* @returns 로그인 정보 | ||
* @throws 401 - 로그인 정보가 없습니다. | ||
*/ | ||
export const retrieveUserInfo = () => | ||
shopAPIClient.get<ShopAPISchema.UserStatus>( | ||
"authn/social/browser/v1/auth/session" | ||
); | ||
|
||
/** | ||
* 노출 중인 모든 상품의 목록을 가져옵니다. | ||
* @param qs - 상품 목록을 가져올 쿼리 파라미터 | ||
* @param qs.category_group - 상품 목록을 가져올 카테고리 그룹의 이름 | ||
* @param qs.category - 상품 목록을 가져올 카테고리의 이름 | ||
* @returns 노출 중인 모든 상품의 목록 | ||
*/ | ||
export const listProducts = (qs?: ShopAPISchema.ProductListQueryParams) => | ||
shopAPIClient.get<ShopAPISchema.Product[]>("v1/products/", { params: qs }); | ||
|
||
/** | ||
* 현재 사용자의 장바구니에 담긴 상품의 목록을 가져옵니다. | ||
* @returns 현재 장바구니 상태 | ||
*/ | ||
export const retrieveCart = () => | ||
shopAPIClient.get<ShopAPISchema.Order>("v1/orders/cart/"); | ||
|
||
/** | ||
* 장바구니에 상품을 추가합니다. | ||
* @param data 장바구니에 추가할 상품과 상품의 옵션 정보 | ||
* @param data.product 장바구니에 추가할 상품의 UUID | ||
* @param data.options 장바구니에 추가할 상품의 옵션 정보 | ||
* @param data.options[].product_option_group 장바구니에 추가할 상품 옵션 그룹의 UUID | ||
* @param data.options[].product_option 장바구니에 추가할 상품 옵션의 UUID (`null`일 경우 사용자 정의 응답) | ||
* @param data.options[].custom_response 장바구니에 추가할 상품 옵션에 대한 사용자 정의 응답 (옵션 그룹이 사용자 정의 응답일 경우 필수) | ||
* @returns 현재 장바구니 상태 | ||
*/ | ||
export const appendItemToCart = (data: ShopAPISchema.CartItemAppendRequest) => | ||
shopAPIClient.post< | ||
ShopAPISchema.Order, | ||
ShopAPISchema.CartItemAppendRequest | ||
>("v1/orders/cart/products/", data); | ||
|
||
/** | ||
* 장바구니에서 특정 상품을 제거합니다. | ||
* @param data 제거할 장바구니 내 상품의 UUID | ||
* @returns 현재 장바구니 상태 | ||
*/ | ||
export const removeItemFromCart = (data: { cartProductId: string }) => | ||
shopAPIClient.delete<ShopAPISchema.Order>( | ||
`v1/orders/cart/products/${data.cartProductId}/` | ||
); | ||
|
||
/** | ||
* 단일 상품 즉시 결제를 PortOne에 등록합니다. | ||
* @param data 결제할 상품과 상품의 옵션 정보 | ||
* @param data.product 결제할 상품의 UUID | ||
* @param data.options 결제할 상품의 옵션 정보 | ||
* @param data.options[].product_option_group 결제할 상품 옵션 그룹의 UUID | ||
* @param data.options[].product_option 결제할 상품 옵션의 UUID (`null`일 경우 사용자 정의 응답) | ||
* @param data.options[].custom_response 결제할 상품 옵션에 대한 사용자 정의 응답 (옵션 그룹이 사용자 정의 응답일 경우 필수) | ||
* @returns PortOne에 등록된 주문 정보 | ||
*/ | ||
export const prepareOneItemOrder = ( | ||
data: ShopAPISchema.OneItemOrderRequest | ||
) => | ||
shopAPIClient.post<ShopAPISchema.Order, ShopAPISchema.OneItemOrderRequest>( | ||
"v1/orders/single/", | ||
data | ||
); | ||
|
||
/** | ||
* 고객의 장바구니에 담긴 전체 상품 결제를 PortOne에 등록합니다. | ||
* @returns PortOne에 등록된 주문 정보 | ||
*/ | ||
export const prepareCartOrder = () => | ||
shopAPIClient.post<ShopAPISchema.Order, undefined>( | ||
"v1/orders/cart/", | ||
undefined | ||
); | ||
|
||
/** | ||
* 고객의 모든 결제 내역을 가져옵니다. | ||
* @returns 고객의 모든 결제 내역 | ||
*/ | ||
export const listOrders = () => | ||
shopAPIClient.get<ShopAPISchema.Order[]>("v1/orders/"); | ||
|
||
/** | ||
* 결제 완료된 주문 내역에서 특정 상품을 환불 시도합니다. | ||
* @param data - 주문 내역 UUID와 주문 내역 내 환불할 상품 UUID | ||
* @param data.order_id - 환불할 상품이 포함된 주문 내역의 UUID | ||
* @param data.order_product_relation_id - 주문 내역 내 환불할 상품의 UUID | ||
*/ | ||
export const refundOneItemFromOrder = ( | ||
data: ShopAPISchema.OneItemRefundRequest | ||
) => | ||
shopAPIClient.delete<void>( | ||
`v1/orders/${data.order_id}/products/${data.order_product_relation_id}/` | ||
); | ||
|
||
/** | ||
* 결제 완료된 주문 내역을 환불 시도합니다. | ||
* @param orderId - 환불할 주문 내역의 UUID | ||
*/ | ||
export const refundAllItemsInOrder = (orderId: string) => | ||
shopAPIClient.delete<void>(`v1/orders/${orderId}/`); | ||
} | ||
|
||
export default ShopAPIRoute; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.