From 1d05e3e38e2b22c31bfaa0f518c69bae22a3730c Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 16:10:11 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20util=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20Cookie=20=EA=B4=80=EB=A0=A8=20=EB=8F=84=EA=B5=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-common/utils/cookie.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 package/pyconkr-common/utils/cookie.ts diff --git a/package/pyconkr-common/utils/cookie.ts b/package/pyconkr-common/utils/cookie.ts new file mode 100644 index 0000000..fbf3dc7 --- /dev/null +++ b/package/pyconkr-common/utils/cookie.ts @@ -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 +} From 18ccb6edc9e40cc7f7bac278195cf6f1a749e459 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 16:10:32 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20shop=20api=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EB=B0=8F=20hook=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/client.ts | 108 ++++++++++++++ package/pyconkr-shop/apis/index.ts | 189 +++++++++++++++++++++++++ package/pyconkr-shop/hooks/index.ts | 103 ++++++++++++++ package/pyconkr-shop/schemas/index.ts | 196 ++++++++++++++++++++++++++ package/pyconkr-shop/utils/portone.ts | 33 +++++ 5 files changed, 629 insertions(+) create mode 100644 package/pyconkr-shop/apis/client.ts create mode 100644 package/pyconkr-shop/apis/index.ts create mode 100644 package/pyconkr-shop/hooks/index.ts create mode 100644 package/pyconkr-shop/schemas/index.ts create mode 100644 package/pyconkr-shop/utils/portone.ts diff --git a/package/pyconkr-shop/apis/client.ts b/package/pyconkr-shop/apis/client.ts new file mode 100644 index 0000000..148bab1 --- /dev/null +++ b/package/pyconkr-shop/apis/client.ts @@ -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 = , D = any>(url: string, config?: AxiosRequestConfig) => Promise +type AxiosRequestWithPayload = , D = any>(url: string, data?: D, config?: AxiosRequestConfig) => Promise + +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 , D = any>(url: string, config?: AxiosRequestConfig) => { + try { + return await requestFunc(url, config) + } catch (error) { + throw new ShopAPIClientError(error) + } + } + } + + _safe_request_with_payload(requestFunc: AxiosRequestWithPayload): AxiosRequestWithPayload { + return async , D = any>(url: string, data: D, config?: AxiosRequestConfig) => { + try { + return await requestFunc(url, data, config) + } catch (error) { + throw new ShopAPIClientError(error) + } + } + } + + getCSRFToken(): string | undefined { return getCookie(this.csrfCookieName) } + + async get(url: string, config?: AxiosRequestConfig): Promise { + return (await this._safe_request_without_payload(this.shopAPI.get), D>(url, config)).data + } + async post(url: string, data: D, config?: AxiosRequestConfig): Promise { + return (await this._safe_request_with_payload(this.shopAPI.post), D>(url, data, config)).data + } + async put(url: string, data: D, config?: AxiosRequestConfig): Promise { + return (await this._safe_request_with_payload(this.shopAPI.put), D>(url, data, config)).data + } + async patch(url: string, data: D, config?: AxiosRequestConfig): Promise { + return (await this._safe_request_with_payload(this.shopAPI.patch), D>(url, data, config)).data + } + async delete(url: string, config?: AxiosRequestConfig): Promise { + return (await this._safe_request_without_payload(this.shopAPI.delete), D>(url, config)).data + } +} + +export const shopAPIClient = new ShopAPIClient(import.meta.env.VITE_SHOP_URL) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts new file mode 100644 index 0000000..39fc0d7 --- /dev/null +++ b/package/pyconkr-shop/apis/index.ts @@ -0,0 +1,189 @@ +import { shopAPIClient } from "./client"; + +import ShopAPISchema from "@pyconkr-shop/schemas"; + +export const redirectToProvider = async ( + socialSignInInfo: ShopAPISchema.SocialSignInRequest +) => { + const f = document.createElement("form"); + f.method = "POST"; + f.action = `${process.env.REACT_APP_PYCONKR_API}/authn/social/browser/v1/auth/provider/redirect`; + + Object.entries({ + ...socialSignInInfo, + csrfmiddlewaretoken: shopAPIClient.getCSRFToken() ?? "", + }).forEach(([key, value]) => { + const d = document.createElement("input"); + d.type = "hidden"; + d.name = key; + d.value = value; + f.appendChild(d); + }); + document.body.appendChild(f); + f.submit(); + document.body.removeChild(f); +}; + +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 f = document.createElement("form"); + f.method = "POST"; + f.action = `${shopAPIClient.baseURL}/authn/social/browser/v1/auth/provider/redirect`; + + Object.entries({ + ...socialSignInInfo, + csrfmiddlewaretoken: shopAPIClient.getCSRFToken() ?? "", + process: "login", + }).forEach(([key, value]) => { + const d = document.createElement("input"); + d.type = "hidden"; + d.name = key; + d.value = value; + f.appendChild(d); + }); + document.body.appendChild(f); + f.submit(); + document.body.removeChild(f); + }; + + /** + * 로그아웃합니다. + * @throws 401 - 로그아웃이 성공할 시에도 항상 401 에러가 발생합니다. + */ + export const signOut = () => + shopAPIClient.delete( + "authn/social/browser/v1/auth/session" + ); + + /** + * 로그인 정보를 조회합니다. + * @returns 로그인 정보 + * @throws 401 - 로그인 정보가 없습니다. + */ + export const retrieveUserInfo = () => + shopAPIClient.get( + "authn/social/browser/v1/auth/session" + ); + + /** + * 노출 중인 모든 상품의 목록을 가져옵니다. + * @param qs - 상품 목록을 가져올 쿼리 파라미터 + * @param qs.category_group - 상품 목록을 가져올 카테고리 그룹의 이름 + * @param qs.category - 상품 목록을 가져올 카테고리의 이름 + * @returns 노출 중인 모든 상품의 목록 + */ + export const listProducts = (qs?: ShopAPISchema.ProductListQueryParams) => + shopAPIClient.get("v1/products/", { params: qs }); + + /** + * 현재 사용자의 장바구니에 담긴 상품의 목록을 가져옵니다. + * @returns 현재 장바구니 상태 + */ + export const retrieveCart = () => + shopAPIClient.get("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( + `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( + "v1/orders/single/", + data + ); + + /** + * 고객의 장바구니에 담긴 전체 상품 결제를 PortOne에 등록합니다. + * @returns PortOne에 등록된 주문 정보 + */ + export const prepareCartOrder = () => + shopAPIClient.post( + "v1/orders/cart/", + undefined + ); + + /** + * 고객의 모든 결제 내역을 가져옵니다. + * @returns 고객의 모든 결제 내역 + */ + export const listOrders = () => + shopAPIClient.get("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( + `v1/orders/${data.order_id}/products/${data.order_product_relation_id}/` + ); + + /** + * 결제 완료된 주문 내역을 환불 시도합니다. + * @param orderId - 환불할 주문 내역의 UUID + */ + export const refundAllItemsInOrder = (orderId: string) => + shopAPIClient.delete(`v1/orders/${orderId}/`); +} + +export default ShopAPIRoute; diff --git a/package/pyconkr-shop/hooks/index.ts b/package/pyconkr-shop/hooks/index.ts new file mode 100644 index 0000000..5172c7c --- /dev/null +++ b/package/pyconkr-shop/hooks/index.ts @@ -0,0 +1,103 @@ +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; + +import ShopAPIRoute from "@pyconkr-shop/apis"; +import ShopAPISchema from "@pyconkr-shop/schemas"; + +const QUERY_KEYS = { + USER: ["query", "user"], + PRODUCT_LIST: ["query", "products"], + CART_INFO: ["query", "cart"], + ORDER_LIST: ["query", "orders"], +}; + +const MUTATION_KEYS = { + USER_SIGN_IN: ["mutation", "user", "sign_in"], + CART_ITEM_APPEND: ["mutation", "cart", "item", "append"], + CART_ITEM_REMOVE: ["mutation", "cart", "item", "remove"], + CART_ORDER_START: ["mutation", "cart_order", "start"], + ONE_ITEM_ORDER_START: ["mutation", "one_item_order", "start"], + ALL_ORDER_REFUND: ["mutation", "all_order_refund"], + ONE_ITEM_REFUND: ["mutation", "one_item_refund"], +}; + +namespace ShopAPIHook { + export const useIsSignedIn = () => + useSuspenseQuery({ + queryKey: QUERY_KEYS.USER, + queryFn: async () => { + try { + return (await ShopAPIRoute.retrieveUserInfo()).meta.is_authenticated; + } catch (e) { + return false; + } + }, + }); + + export const useSignInWithEmailMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.USER_SIGN_IN, + mutationFn: ShopAPIRoute.signInWithEmail, + }); + + export const useSignInWithSNSMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.USER_SIGN_IN, + mutationFn: ShopAPIRoute.signInWithSNS, + }); + + export const useProducts = (qs?: ShopAPISchema.ProductListQueryParams) => + useSuspenseQuery({ + queryKey: QUERY_KEYS.PRODUCT_LIST, + queryFn: () => ShopAPIRoute.listProducts(qs), + }); + + export const useCart = () => + useSuspenseQuery({ + queryKey: QUERY_KEYS.CART_INFO, + queryFn: ShopAPIRoute.retrieveCart, + }); + + export const useAddItemToCartMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.CART_ITEM_APPEND, + mutationFn: ShopAPIRoute.appendItemToCart, + }); + + export const useRemoveItemFromCartMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.CART_ITEM_REMOVE, + mutationFn: ShopAPIRoute.removeItemFromCart, + }); + + export const usePrepareOneItemOrderMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.ONE_ITEM_ORDER_START, + mutationFn: ShopAPIRoute.prepareOneItemOrder, + }); + + export const usePrepareOrderMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.CART_ORDER_START, + mutationFn: ShopAPIRoute.prepareCartOrder, + }); + + export const useOrders = () => + useSuspenseQuery({ + queryKey: QUERY_KEYS.ORDER_LIST, + queryFn: ShopAPIRoute.listOrders, + }); + + export const useOneItemRefundMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.ONE_ITEM_REFUND, + mutationFn: ShopAPIRoute.refundOneItemFromOrder, + }); + + export const useOrderRefundMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.ALL_ORDER_REFUND, + mutationFn: ShopAPIRoute.refundAllItemsInOrder, + }); +} + +export default ShopAPIHook; diff --git a/package/pyconkr-shop/schemas/index.ts b/package/pyconkr-shop/schemas/index.ts new file mode 100644 index 0000000..3c78283 --- /dev/null +++ b/package/pyconkr-shop/schemas/index.ts @@ -0,0 +1,196 @@ +import * as R from "remeda"; + +namespace ShopAPISchema { + export type DetailedErrorSchema = { + code: string; + detail: string; + attr: string | null; + }; + + export type ErrorResponseSchema = { + type: string; + errors: DetailedErrorSchema[]; + }; + + export type SocialSignInProvider = "google" | "naver" | "kakao"; + + export type SocialSignInRequest = { + provider: SocialSignInProvider; + callback_url: string; + }; + + export type SocialSessionStatusType = { + meta: { is_authenticated: boolean }; + data: { user?: { email: string } }; + }; + + export type EmailSignInRequest = { + email: string; + password: string; + }; + + export type UserStatus = { + status: number; + meta: { + is_authenticated: true; + }; + data: { + user: { + id: number; + display: string; + has_usable_password: boolean; + email: string; + username: string; + }; + methods: { + method: string; + at: number; + email: string; + }[]; + }; + } | { + status: number; + meta: { + is_authenticated: false; + }; + data: { + flows: { + id: "login" + } | { + id: "provider_redirect"; + providers: SocialSignInProvider[]; + } | { + id: "provider_token"; + providers: SocialSignInProvider[]; + } + } + }; + + export type Option = { + id: string; + name: string; + additional_price: number; + max_quantity_per_user: number; + leftover_stock: number; + }; + + export type OptionGroup = { + id: string; + name: string; + min_quantity_per_product: number; + max_quantity_per_product: number; + is_custom_response: boolean; + custom_response_pattern: string | null; + options: Option[]; + }; + + export type ProductListQueryParams = { + category_group?: string; + category?: string; + }; + + export type Product = { + id: string; + name: string; + description: string | null; + image: string | null; + price: number; + orderable_starts_at: string; + orderable_ends_at: string; + refundable_ends_at: string; + + category_group: string; + category: string; + + option_groups: OptionGroup[]; + leftover_stock: number; + tag_names: string[]; + }; + + export type PaymentHistoryStatus = + | "pending" + | "completed" + | "partial_refunded" + | "refunded"; + + export type PaymentHistory = { + price: number; + status: PaymentHistoryStatus; + }; + + export type OrderProductItemStatus = "pending" | "paid" | "used" | "refunded"; + + export type OrderProductItem = { + id: string; + status: PaymentHistoryStatus; + price: number; + additional_price: number; + product: { + id: string; + name: string; + price: number; + image: string | null; + }; + options: { + product_option_group: { + id: string; + name: string; + is_custom_response: boolean; + }; + product_option: { + id: string; + name: string; + additional_price: number; + } | null; + custom_response: string | null; + }[]; + }; + + export type Order = { + id: string; + name: string; + first_paid_price: number; + current_paid_price: number; + current_status: PaymentHistoryStatus; + created_at: string; + + payment_histories: PaymentHistory[]; + products: OrderProductItem[]; + }; + export type Cart = Order; + + export type CartItemAppendRequest = { + product: string; + options: { + product_option_group: string; + product_option: string | null; + custom_response: string | null; + }[]; + }; + export type OneItemOrderRequest = CartItemAppendRequest; + + export type OneItemRefundRequest = { + order_id: string; + order_product_relation_id: string; + }; +} + +export const isObjectErrorResponseSchema = ( + obj?: unknown +): obj is ShopAPISchema.ErrorResponseSchema => { + return ( + R.isPlainObject(obj) && + R.isString(obj.type) && + R.isArray(obj.errors) && + obj.errors.every(() => { + return ( + R.isPlainObject(obj) && + R.isString(obj.code) && + R.isString(obj.detail) && + (obj.attr === null || R.isString(obj.attr)) + ); + }) + ); +}; + +export default ShopAPISchema; diff --git a/package/pyconkr-shop/utils/portone.ts b/package/pyconkr-shop/utils/portone.ts new file mode 100644 index 0000000..31b63e5 --- /dev/null +++ b/package/pyconkr-shop/utils/portone.ts @@ -0,0 +1,33 @@ +import { RequestPayResponse } from "iamport-typings/src" + +import ShopAPISchema from "@pyconkr-shop/schemas" + +export const startPortOnePurchase = ( + order: ShopAPISchema.Order, + onSuccess?: (response: RequestPayResponse) => void, + onFailure?: (response: RequestPayResponse) => void, + onCleanUp?: (response: RequestPayResponse) => void, +) => { + const { IMP } = window + if (!IMP) { + alert('PortOne 라이브러리가 로드되지 않았습니다.') + return + } + + IMP.init(import.meta.env.VITE_IMP_ACCOUNT_ID) + IMP.request_pay( + { + pg: "kcp", + pay_method: "card", + merchant_uid: order.id, + name: "상품 구매", + amount: order.first_paid_price, + buyer_tel: "", + }, + async (response: RequestPayResponse) => { + if (response.success) onSuccess?.(response) + else onFailure?.(response) + onCleanUp?.(response) + } + ) +} From abfcdba58ee0ba26aa227b20aba23181bece5ace Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 16:12:12 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=ED=95=9C=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=EB=A5=BC=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dotenv/.env.development | 2 ++ dotenv/.env.production | 2 ++ package.json | 3 +++ src/vite-env.d.ts | 12 ++++++++++++ tsconfig.app.json | 2 +- vite.config.ts | 13 +++++++++++-- 6 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 dotenv/.env.development create mode 100644 dotenv/.env.production diff --git a/dotenv/.env.development b/dotenv/.env.development new file mode 100644 index 0000000..f1e20bf --- /dev/null +++ b/dotenv/.env.development @@ -0,0 +1,2 @@ +VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.dev.pycon.kr +VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=DEBUG_csrftoken diff --git a/dotenv/.env.production b/dotenv/.env.production new file mode 100644 index 0000000..aee9515 --- /dev/null +++ b/dotenv/.env.production @@ -0,0 +1,2 @@ +VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.pycon.kr +VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=csrftoken diff --git a/package.json b/package.json index 1e11a03..1caef19 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "@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", "@tanstack/react-query": "^5.72.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.2", diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..4878f10 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,13 @@ /// +interface ViteTypeOptions { + strictImportEnv: unknown; +} + +interface ImportMetaEnv { + readonly VITE_PYCONKR_SHOP_API_DOMAIN: string; + readonly VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tsconfig.app.json b/tsconfig.app.json index adadc51..5accaa5 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -23,5 +23,5 @@ "noUncheckedSideEffectImports": true, "forceConsistentCasingInFileNames": false }, - "include": ["src"] + "include": ["src", "package"] } diff --git a/vite.config.ts b/vite.config.ts index 81e7aad..609fe35 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,18 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; import mdx from "@mdx-js/rollup"; +import react from "@vitejs/plugin-react"; +import path from 'path'; +import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ base: "/frontend/", + envDir: "./dotenv", plugins: [react(), mdx()], + resolve: { + alias: { + '@pyconkr-common': path.resolve(__dirname, './package/pyconkr-common'), + '@pyconkr-shop': path.resolve(__dirname, './package/pyconkr-shop'), + '@src': path.resolve(__dirname, './src'), + }, + }, }); From d2e5ebca08db9b59dc15ff02b73dc30d8e2336ed Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 16:13:47 +0900 Subject: [PATCH 04/16] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=95=84=EC=88=98=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EB=AA=87=EA=B0=80=EC=A7=80=20helper=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 13 +++- pnpm-lock.yaml | 208 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 211 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 1caef19..489cc59 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,25 @@ "@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", @@ -42,10 +49,12 @@ "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": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51e9b12..1abe734 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,25 +26,52 @@ importers: '@mui/material': specifier: ^7.0.2 version: 7.0.2(@emotion/react@11.14.0(@types/react@19.1.1)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.1)(react@19.1.0))(@types/react@19.1.1)(react@19.1.0))(@types/react@19.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@pyconkr-common': + specifier: link:package/pyconkr-common + version: link:package/pyconkr-common + '@pyconkr-shop': + specifier: link:package/pyconkr-shop + version: link:package/pyconkr-shop + '@src': + specifier: link:src + version: link:src + '@suspensive/react': + specifier: ^2.18.12 + version: 2.18.12(react@19.1.0) '@tanstack/react-query': specifier: ^5.72.2 version: 5.72.2(react@19.1.0) + axios: + specifier: ^1.8.4 + version: 1.8.4 eslint-plugin-import: specifier: ^2.31.0 version: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0)(typescript@5.7.3))(eslint@9.24.0) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.24.0) + notistack: + specifier: ^3.0.2 + version: 3.0.2(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + remeda: + specifier: ^2.21.3 + version: 2.21.3 devDependencies: '@eslint/js': specifier: ^9.21.0 version: 9.24.0 + '@tanstack/react-query-devtools': + specifier: ^5.74.4 + version: 5.74.4(@tanstack/react-query@5.72.2(react@19.1.0))(react@19.1.0) + '@types/node': + specifier: ^22.14.1 + version: 22.14.1 '@types/react': specifier: ^19.0.10 version: 19.1.1 @@ -56,7 +83,10 @@ importers: version: 8.29.1(eslint@9.24.0)(typescript@5.7.3) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.2.6) + version: 4.3.4(vite@6.2.6(@types/node@22.14.1)) + csstype: + specifier: ^3.1.3 + version: 3.1.3 eslint: specifier: ^9.21.0 version: 9.24.0 @@ -78,6 +108,9 @@ importers: globals: specifier: ^15.15.0 version: 15.15.0 + iamport-typings: + specifier: ^1.4.0 + version: 1.4.0 prettier: specifier: ^3.5.3 version: 3.5.3 @@ -89,10 +122,10 @@ importers: version: 8.29.1(eslint@9.24.0)(typescript@5.7.3) vite: specifier: ^6.2.0 - version: 6.2.6 + version: 6.2.6(@types/node@22.14.1) vite-plugin-mdx: specifier: ^3.6.1 - version: 3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6) + version: 3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6(@types/node@22.14.1)) packages: @@ -696,9 +729,23 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@suspensive/react@2.18.12': + resolution: {integrity: sha512-De3sVLxLnMpTSOfW3t3D8uh8+/bK8+L/mV8YRAwjW2PyR8BBe9+nctFRVO+ZCIFKUs7VPtnIXnb+5bKfBQ1vog==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/query-core@5.72.2': resolution: {integrity: sha512-fxl9/0yk3mD/FwTmVEf1/H6N5B975H0luT+icKyX566w6uJG0x6o+Yl+I38wJRCaogiMkstByt+seXfDbWDAcA==} + '@tanstack/query-devtools@5.73.3': + resolution: {integrity: sha512-hBQyYwsOuO7QOprK75NzfrWs/EQYjgFA0yykmcvsV62q0t6Ua97CU3sYgjHx0ZvxkXSOMkY24VRJ5uv9f5Ik4w==} + + '@tanstack/react-query-devtools@5.74.4': + resolution: {integrity: sha512-PGCAcytQMmeagoeGG45ccBhrC1x0/5OlNjsM1FAb9OfsQZIhPzjwjhGcwmMu6TbT4RIHgvjxLwC5NHgkUwJQzw==} + peerDependencies: + '@tanstack/react-query': ^5.74.4 + react: ^18 || ^19 + '@tanstack/react-query@5.72.2': resolution: {integrity: sha512-SVNHzyBUYiis+XiCl+8yiPZmMYei2AKYY94wM/zpvB5l1jxqOo82FQTziSJ4pBi96jtYqvYrTMxWynmbQh3XKw==} peerDependencies: @@ -743,6 +790,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.14.1': + resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -890,6 +940,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -898,6 +951,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -968,6 +1024,10 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -982,6 +1042,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1058,6 +1122,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1429,10 +1497,23 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + fs-extra@11.3.0: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} @@ -1501,6 +1582,11 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + goober@2.1.16: + resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} + peerDependencies: + csstype: ^3.0.10 + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1550,6 +1636,9 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + iamport-typings@1.4.0: + resolution: {integrity: sha512-7V+/qtyPb+JS7fxcZpWaLHGXqJA5rrmAqAjx+CaoveYntU4VIG4yW5X3Cmw69uByDahOQv6jzv2qYbi6gJKlwg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1907,6 +1996,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1931,6 +2028,13 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + notistack@3.0.2: + resolution: {integrity: sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==} + engines: {node: '>=12.0.0', npm: '>=6.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2055,6 +2159,9 @@ packages: property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2122,6 +2229,9 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remeda@2.21.3: + resolution: {integrity: sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2307,6 +2417,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@4.40.0: + resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==} + engines: {node: '>=16'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2339,6 +2453,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3047,8 +3164,20 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@suspensive/react@2.18.12(react@19.1.0)': + dependencies: + react: 19.1.0 + '@tanstack/query-core@5.72.2': {} + '@tanstack/query-devtools@5.73.3': {} + + '@tanstack/react-query-devtools@5.74.4(@tanstack/react-query@5.72.2(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/query-devtools': 5.73.3 + '@tanstack/react-query': 5.72.2(react@19.1.0) + react: 19.1.0 + '@tanstack/react-query@5.72.2(react@19.1.0)': dependencies: '@tanstack/query-core': 5.72.2 @@ -3101,6 +3230,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@22.14.1': + dependencies: + undici-types: 6.21.0 + '@types/parse-json@4.0.2': {} '@types/prop-types@15.7.14': {} @@ -3200,14 +3333,14 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.3.4(vite@6.2.6)': + '@vitejs/plugin-react@4.3.4(vite@6.2.6(@types/node@22.14.1))': dependencies: '@babel/core': 7.26.10 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.2.6 + vite: 6.2.6(@types/node@22.14.1) transitivePeerDependencies: - supports-color @@ -3290,12 +3423,22 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.10.3: {} + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} babel-plugin-macros@3.1.0: @@ -3366,6 +3509,8 @@ snapshots: character-reference-invalid@2.0.1: {} + clsx@1.2.1: {} + clsx@2.1.1: {} collapse-white-space@2.1.0: {} @@ -3376,6 +3521,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@13.1.0: {} @@ -3450,6 +3599,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + dequal@2.0.3: {} devlop@1.1.0: @@ -3935,10 +4086,19 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 @@ -4025,6 +4185,10 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + goober@2.1.16(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4102,6 +4266,8 @@ snapshots: dependencies: react-is: 16.13.1 + iamport-typings@1.4.0: {} + ignore@5.3.2: {} import-fresh@3.3.1: @@ -4639,6 +4805,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4657,6 +4829,15 @@ snapshots: node-releases@2.0.19: {} + notistack@3.0.2(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + clsx: 1.2.1 + goober: 2.1.16(csstype@3.1.3) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - csstype + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -4788,6 +4969,8 @@ snapshots: property-information@7.0.0: {} + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -4898,6 +5081,10 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remeda@2.21.3: + dependencies: + type-fest: 4.40.0 + resolve-from@4.0.0: {} resolve@1.22.10: @@ -5123,6 +5310,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.40.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -5175,6 +5364,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@6.21.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -5260,21 +5451,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-mdx@3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6): + vite-plugin-mdx@3.6.1(@mdx-js/mdx@3.1.0(acorn@8.14.1))(vite@6.2.6(@types/node@22.14.1)): dependencies: '@alloc/quick-lru': 5.2.0 '@mdx-js/mdx': 3.1.0(acorn@8.14.1) esbuild: 0.13.8 resolve: 1.22.10 unified: 9.2.2 - vite: 6.2.6 + vite: 6.2.6(@types/node@22.14.1) - vite@6.2.6: + vite@6.2.6(@types/node@22.14.1): dependencies: esbuild: 0.25.2 postcss: 8.5.3 rollup: 4.39.0 optionalDependencies: + '@types/node': 22.14.1 fsevents: 2.3.3 which-boxed-primitive@1.1.1: From d834e3b6ff92a987ba455b774bd054ee146b2fc6 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 16:16:14 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20Port?= =?UTF-8?q?One=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/utils/portone.ts | 2 +- src/vite-env.d.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package/pyconkr-shop/utils/portone.ts b/package/pyconkr-shop/utils/portone.ts index 31b63e5..655ebfd 100644 --- a/package/pyconkr-shop/utils/portone.ts +++ b/package/pyconkr-shop/utils/portone.ts @@ -14,7 +14,7 @@ export const startPortOnePurchase = ( return } - IMP.init(import.meta.env.VITE_IMP_ACCOUNT_ID) + IMP.init(import.meta.env.VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID) IMP.request_pay( { pg: "kcp", diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 4878f10..4b3ccaa 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -6,6 +6,7 @@ interface ViteTypeOptions { interface ImportMetaEnv { readonly VITE_PYCONKR_SHOP_API_DOMAIN: string; readonly VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME: string; + readonly VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID: string; } interface ImportMeta { From b89a8a1629e6f3864641cee8fc45045ab080c03b Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 19:06:20 +0900 Subject: [PATCH 06/16] =?UTF-8?q?fix:=20ErrorResponseSchema=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/schemas/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package/pyconkr-shop/schemas/index.ts b/package/pyconkr-shop/schemas/index.ts index 3c78283..5597060 100644 --- a/package/pyconkr-shop/schemas/index.ts +++ b/package/pyconkr-shop/schemas/index.ts @@ -182,12 +182,12 @@ export const isObjectErrorResponseSchema = ( R.isPlainObject(obj) && R.isString(obj.type) && R.isArray(obj.errors) && - obj.errors.every(() => { + obj.errors.every((error) => { return ( - R.isPlainObject(obj) && - R.isString(obj.code) && - R.isString(obj.detail) && - (obj.attr === null || R.isString(obj.attr)) + R.isPlainObject(error) && + R.isString(error.code) && + R.isString(error.detail) && + (error.attr === null || R.isString(error.attr)) ); }) ); From f2efdbedb500ab83a5a100260e12c0a27be88a15 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 19:08:43 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=EC=A2=80=20=EB=8D=94=20=EB=AA=85=ED=99=95=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index 39fc0d7..617a001 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -5,23 +5,23 @@ import ShopAPISchema from "@pyconkr-shop/schemas"; export const redirectToProvider = async ( socialSignInInfo: ShopAPISchema.SocialSignInRequest ) => { - const f = document.createElement("form"); - f.method = "POST"; - f.action = `${process.env.REACT_APP_PYCONKR_API}/authn/social/browser/v1/auth/provider/redirect`; + const form = document.createElement("form"); + form.method = "POST"; + form.action = `${process.env.REACT_APP_PYCONKR_API}/authn/social/browser/v1/auth/provider/redirect`; Object.entries({ ...socialSignInInfo, csrfmiddlewaretoken: shopAPIClient.getCSRFToken() ?? "", }).forEach(([key, value]) => { - const d = document.createElement("input"); - d.type = "hidden"; - d.name = key; - d.value = value; - f.appendChild(d); + const inputElement = document.createElement("input"); + inputElement.type = "hidden"; + inputElement.name = key; + inputElement.value = value; + form.appendChild(inputElement); }); - document.body.appendChild(f); - f.submit(); - document.body.removeChild(f); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); }; namespace ShopAPIRoute { From c384573818766088a569b2018799d3451dbcd91a Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 19:10:21 +0900 Subject: [PATCH 08/16] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8D=98=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/pyconkr-shop/apis/client.ts b/package/pyconkr-shop/apis/client.ts index 148bab1..d68d67b 100644 --- a/package/pyconkr-shop/apis/client.ts +++ b/package/pyconkr-shop/apis/client.ts @@ -105,4 +105,4 @@ class ShopAPIClient { } } -export const shopAPIClient = new ShopAPIClient(import.meta.env.VITE_SHOP_URL) +export const shopAPIClient = new ShopAPIClient(import.meta.env.VITE_PYCONKR_SHOP_API_DOMAIN) From 042210ba78aab0ff9fefbc5b95e3d3b06dde4f28 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 19:10:54 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20=EC=9E=98=EB=AA=BB=20?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=84=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index 617a001..3a7a024 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -2,28 +2,6 @@ import { shopAPIClient } from "./client"; import ShopAPISchema from "@pyconkr-shop/schemas"; -export const redirectToProvider = async ( - socialSignInInfo: ShopAPISchema.SocialSignInRequest -) => { - const form = document.createElement("form"); - form.method = "POST"; - form.action = `${process.env.REACT_APP_PYCONKR_API}/authn/social/browser/v1/auth/provider/redirect`; - - Object.entries({ - ...socialSignInInfo, - csrfmiddlewaretoken: shopAPIClient.getCSRFToken() ?? "", - }).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); -}; - namespace ShopAPIRoute { /** * 로그인합니다. From fd203335774f77999c8b0941847e55b1886ace53 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 20 Apr 2025 19:13:50 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=ED=95=9C=EB=B2=88=20=EB=8D=94=20=EB=AA=85=ED=99=95?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index 3a7a024..799b45a 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -26,24 +26,24 @@ namespace ShopAPIRoute { export const signInWithSNS = async ( socialSignInInfo: ShopAPISchema.SocialSignInRequest ) => { - const f = document.createElement("form"); - f.method = "POST"; - f.action = `${shopAPIClient.baseURL}/authn/social/browser/v1/auth/provider/redirect`; + 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 d = document.createElement("input"); - d.type = "hidden"; - d.name = key; - d.value = value; - f.appendChild(d); + const inputElement = document.createElement("input"); + inputElement.type = "hidden"; + inputElement.name = key; + inputElement.value = value; + form.appendChild(inputElement); }); - document.body.appendChild(f); - f.submit(); - document.body.removeChild(f); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); }; /** From a688efaba6cfc551bde8155be9b3bb47794a0766 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Tue, 22 Apr 2025 07:44:38 +0900 Subject: [PATCH 11/16] =?UTF-8?q?fix:=20=EC=BF=A0=ED=82=A4=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EC=8B=9C=20=EB=A7=A8=20=EC=B2=98=EC=9D=8C=20?= =?UTF-8?q?=EB=B0=9C=EA=B2=AC=EB=90=9C=20=3D=EB=A7=8C=20split=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-common/utils/cookie.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/pyconkr-common/utils/cookie.ts b/package/pyconkr-common/utils/cookie.ts index fbf3dc7..5e30cd0 100644 --- a/package/pyconkr-common/utils/cookie.ts +++ b/package/pyconkr-common/utils/cookie.ts @@ -7,7 +7,7 @@ export const getCookie = (name: string) => { let cookieValue: string | undefined document.cookie.split(';').forEach((cookie) => { if (R.isEmpty(cookie) || !cookie.includes('=')) return - const [key, value] = cookie.split('=') + const [key, value] = cookie.split('=', 2) if (key.trim() === name) cookieValue = decodeURIComponent(value) as string }) return cookieValue From 7d941268c19872f217b2ecf1b0c752f78e55cca0 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Tue, 22 Apr 2025 07:46:15 +0900 Subject: [PATCH 12/16] =?UTF-8?q?chore:=20=EC=86=8C=EC=85=9C=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=95=BD=EA=B0=84=EC=9D=98=20?= =?UTF-8?q?delay=EB=A5=BC=20=EB=91=90=EA=B3=A0=20form=EC=9D=84=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index 799b45a..8d70f68 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -43,7 +43,7 @@ namespace ShopAPIRoute { }); document.body.appendChild(form); form.submit(); - document.body.removeChild(form); + setTimeout(() => document.body.removeChild(form), 100); }; /** From 178a0181ffac5900d94b661bebb292a74362c5a1 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Tue, 22 Apr 2025 07:49:01 +0900 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EB=B3=84=EB=A1=9C=20mutationKey=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/hooks/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package/pyconkr-shop/hooks/index.ts b/package/pyconkr-shop/hooks/index.ts index 5172c7c..3f88efe 100644 --- a/package/pyconkr-shop/hooks/index.ts +++ b/package/pyconkr-shop/hooks/index.ts @@ -11,7 +11,8 @@ const QUERY_KEYS = { }; const MUTATION_KEYS = { - USER_SIGN_IN: ["mutation", "user", "sign_in"], + USER_SIGN_IN_EMAIL: ["mutation", "user", "sign_in", "email"], + USER_SIGN_IN_SNS: ["mutation", "user", "sign_in", "sns"], CART_ITEM_APPEND: ["mutation", "cart", "item", "append"], CART_ITEM_REMOVE: ["mutation", "cart", "item", "remove"], CART_ORDER_START: ["mutation", "cart_order", "start"], @@ -35,13 +36,13 @@ namespace ShopAPIHook { export const useSignInWithEmailMutation = () => useMutation({ - mutationKey: MUTATION_KEYS.USER_SIGN_IN, + mutationKey: MUTATION_KEYS.USER_SIGN_IN_EMAIL, mutationFn: ShopAPIRoute.signInWithEmail, }); export const useSignInWithSNSMutation = () => useMutation({ - mutationKey: MUTATION_KEYS.USER_SIGN_IN, + mutationKey: MUTATION_KEYS.USER_SIGN_IN_SNS, mutationFn: ShopAPIRoute.signInWithSNS, }); From fdb7f1da20d878a6562102988af296aadcde27f1 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Wed, 23 Apr 2025 23:23:32 +0900 Subject: [PATCH 14/16] =?UTF-8?q?fix:=20hook=EC=9D=84=20=EC=8B=A4=EC=A0=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20=EC=9A=A9=EB=A1=80=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/client.ts | 222 +++++++++++++++++++------- package/pyconkr-shop/apis/index.ts | 30 ++-- package/pyconkr-shop/hooks/index.ts | 49 +++++- package/pyconkr-shop/schemas/index.ts | 6 +- 4 files changed, 227 insertions(+), 80 deletions(-) diff --git a/package/pyconkr-shop/apis/client.ts b/package/pyconkr-shop/apis/client.ts index d68d67b..e4e568f 100644 --- a/package/pyconkr-shop/apis/client.ts +++ b/package/pyconkr-shop/apis/client.ts @@ -1,108 +1,206 @@ -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 }] } +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 + 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 + let message: string = DEFAULT_ERROR_MESSAGE; + let detail: ShopAPISchema.ErrorResponseSchema = DEFAULT_ERROR_RESPONSE; + let status = -1; if (axios.isAxiosError(error)) { - const response = error.response + 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 }] - } + 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 + message = error.message; detail = { - type: error.name || typeof error || 'unknown', - errors: [{ code: 'unknown', detail: error.message, attr: null }], - } + 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 + super(message); + this.originalError = error || null; + this.status = status; + this.detail = detail; } isRequiredAuth(): boolean { - return this.status === 401 || this.status === 403 + return this.status === 401 || this.status === 403; } } -type AxiosRequestWithoutPayload = , D = any>(url: string, config?: AxiosRequestConfig) => Promise -type AxiosRequestWithPayload = , D = any>(url: string, data?: D, config?: AxiosRequestConfig) => Promise +type AxiosRequestWithoutPayload = , D = any>( + url: string, + config?: AxiosRequestConfig +) => Promise; +type AxiosRequestWithPayload = , D = any>( + url: string, + data?: D, + config?: AxiosRequestConfig +) => Promise; class ShopAPIClient { - readonly baseURL: string - protected readonly csrfCookieName: string - private readonly shopAPI: AxiosInstance + 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, + timeout: number = DEFAULT_TIMEOUT ) { - this.baseURL = baseURL - this.csrfCookieName = csrfCookieName - this.shopAPI = axios.create({ baseURL, timeout, headers: { 'Content-Type': 'application/json' } }) + this.baseURL = baseURL; + this.csrfCookieName = csrfCookieName; + this.shopAPI = axios.create({ + baseURL, + timeout, + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }); + this.shopAPI.interceptors.request.use( + (config) => { + config.headers["x-csrftoken"] = this.getCSRFToken(); + return config; + }, + (error) => Promise.reject(error) + ); } - _safe_request_without_payload(requestFunc: AxiosRequestWithoutPayload): AxiosRequestWithoutPayload { - return async , D = any>(url: string, config?: AxiosRequestConfig) => { + _safe_request_without_payload( + requestFunc: AxiosRequestWithoutPayload + ): AxiosRequestWithoutPayload { + return async , D = any>( + url: string, + config?: AxiosRequestConfig + ) => { try { - return await requestFunc(url, config) + return await requestFunc(url, config); } catch (error) { - throw new ShopAPIClientError(error) + throw new ShopAPIClientError(error); } - } + }; } - _safe_request_with_payload(requestFunc: AxiosRequestWithPayload): AxiosRequestWithPayload { - return async , D = any>(url: string, data: D, config?: AxiosRequestConfig) => { + _safe_request_with_payload( + requestFunc: AxiosRequestWithPayload + ): AxiosRequestWithPayload { + return async , D = any>( + url: string, + data: D, + config?: AxiosRequestConfig + ) => { try { - return await requestFunc(url, data, config) + return await requestFunc(url, data, config); } catch (error) { - throw new ShopAPIClientError(error) + throw new ShopAPIClientError(error); } - } + }; } - getCSRFToken(): string | undefined { return getCookie(this.csrfCookieName) } + getCSRFToken(): string | undefined { + return getCookie(this.csrfCookieName); + } - async get(url: string, config?: AxiosRequestConfig): Promise { - return (await this._safe_request_without_payload(this.shopAPI.get), D>(url, config)).data + async get( + url: string, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_without_payload(this.shopAPI.get)< + T, + AxiosResponse, + D + >(url, config) + ).data; } - async post(url: string, data: D, config?: AxiosRequestConfig): Promise { - return (await this._safe_request_with_payload(this.shopAPI.post), D>(url, data, config)).data + async post( + url: string, + data: D, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_with_payload(this.shopAPI.post)< + T, + AxiosResponse, + D + >(url, data, config) + ).data; } - async put(url: string, data: D, config?: AxiosRequestConfig): Promise { - return (await this._safe_request_with_payload(this.shopAPI.put), D>(url, data, config)).data + async put( + url: string, + data: D, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_with_payload(this.shopAPI.put)< + T, + AxiosResponse, + D + >(url, data, config) + ).data; } - async patch(url: string, data: D, config?: AxiosRequestConfig): Promise { - return (await this._safe_request_with_payload(this.shopAPI.patch), D>(url, data, config)).data + async patch( + url: string, + data: D, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_with_payload(this.shopAPI.patch)< + T, + AxiosResponse, + D + >(url, data, config) + ).data; } - async delete(url: string, config?: AxiosRequestConfig): Promise { - return (await this._safe_request_without_payload(this.shopAPI.delete), D>(url, config)).data + async delete( + url: string, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_without_payload(this.shopAPI.delete)< + T, + AxiosResponse, + D + >(url, config) + ).data; } } -export const shopAPIClient = new ShopAPIClient(import.meta.env.VITE_PYCONKR_SHOP_API_DOMAIN) +export const shopAPIClient = new ShopAPIClient( + import.meta.env.VITE_PYCONKR_SHOP_API_DOMAIN +); diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index 8d70f68..a678e97 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -5,16 +5,22 @@ import ShopAPISchema from "@pyconkr-shop/schemas"; namespace ShopAPIRoute { /** * 로그인합니다. - * @param username - 사용자 이름 - * @param password - 비밀번호 + * @param data - 로그인 정보 + * @param data.username - 사용자 이름 + * @param data.password - 비밀번호 * @returns 로그인 정보 * @throws 401 - 로그인 정보가 없습니다. */ - export const signInWithEmail = (data: ShopAPISchema.EmailSignInRequest) => - shopAPIClient.post< - ShopAPISchema.UserStatus, + export const signInWithEmail = (data: ShopAPISchema.EmailSignInRequest) => { + const requestPayload = { + ...data, + csrfmiddlewaretoken: shopAPIClient.getCSRFToken() ?? "", + }; + return shopAPIClient.post< + ShopAPISchema.UserSignedInStatus, ShopAPISchema.EmailSignInRequest - >("authn/social/browser/v1/auth/login", data); + >("authn/social/browser/v1/auth/login", requestPayload); + }; /** * SNS로 로그인합니다. @@ -51,7 +57,7 @@ namespace ShopAPIRoute { * @throws 401 - 로그아웃이 성공할 시에도 항상 401 에러가 발생합니다. */ export const signOut = () => - shopAPIClient.delete( + shopAPIClient.delete( "authn/social/browser/v1/auth/session" ); @@ -61,7 +67,7 @@ namespace ShopAPIRoute { * @throws 401 - 로그인 정보가 없습니다. */ export const retrieveUserInfo = () => - shopAPIClient.get( + shopAPIClient.get( "authn/social/browser/v1/auth/session" ); @@ -80,7 +86,7 @@ namespace ShopAPIRoute { * @returns 현재 장바구니 상태 */ export const retrieveCart = () => - shopAPIClient.get("v1/orders/cart/"); + shopAPIClient.get("v1/orders/cart/"); /** * 장바구니에 상품을 추가합니다. @@ -158,10 +164,10 @@ namespace ShopAPIRoute { /** * 결제 완료된 주문 내역을 환불 시도합니다. - * @param orderId - 환불할 주문 내역의 UUID + * @param data.orderId - 환불할 주문 내역의 UUID */ - export const refundAllItemsInOrder = (orderId: string) => - shopAPIClient.delete(`v1/orders/${orderId}/`); + export const refundAllItemsInOrder = (data: { order_id: string }) => + shopAPIClient.delete(`v1/orders/${data.order_id}/`); } export default ShopAPIRoute; diff --git a/package/pyconkr-shop/hooks/index.ts b/package/pyconkr-shop/hooks/index.ts index 3f88efe..7811186 100644 --- a/package/pyconkr-shop/hooks/index.ts +++ b/package/pyconkr-shop/hooks/index.ts @@ -13,6 +13,7 @@ const QUERY_KEYS = { const MUTATION_KEYS = { USER_SIGN_IN_EMAIL: ["mutation", "user", "sign_in", "email"], USER_SIGN_IN_SNS: ["mutation", "user", "sign_in", "sns"], + USER_SIGN_OUT: ["mutation", "user", "sign_out"], CART_ITEM_APPEND: ["mutation", "cart", "item", "append"], CART_ITEM_REMOVE: ["mutation", "cart", "item", "remove"], CART_ORDER_START: ["mutation", "cart_order", "start"], @@ -22,14 +23,15 @@ const MUTATION_KEYS = { }; namespace ShopAPIHook { - export const useIsSignedIn = () => + export const useUserStatus = () => useSuspenseQuery({ queryKey: QUERY_KEYS.USER, queryFn: async () => { try { - return (await ShopAPIRoute.retrieveUserInfo()).meta.is_authenticated; + const userInfo = await ShopAPIRoute.retrieveUserInfo(); + return userInfo.meta.is_authenticated === true ? userInfo : null; } catch (e) { - return false; + return null; } }, }); @@ -38,12 +40,45 @@ namespace ShopAPIHook { useMutation({ mutationKey: MUTATION_KEYS.USER_SIGN_IN_EMAIL, mutationFn: ShopAPIRoute.signInWithEmail, + meta: { + invalidates: [ + QUERY_KEYS.USER, + QUERY_KEYS.CART_INFO, + QUERY_KEYS.ORDER_LIST, + ], + }, }); export const useSignInWithSNSMutation = () => useMutation({ mutationKey: MUTATION_KEYS.USER_SIGN_IN_SNS, mutationFn: ShopAPIRoute.signInWithSNS, + meta: { + invalidates: [ + QUERY_KEYS.USER, + QUERY_KEYS.CART_INFO, + QUERY_KEYS.ORDER_LIST, + ], + }, + }); + + export const useSignOutMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.USER_SIGN_OUT, + mutationFn: async () => { + try { + return await ShopAPIRoute.signOut(); + } catch (e) { + return null; + } + }, + meta: { + invalidates: [ + QUERY_KEYS.USER, + QUERY_KEYS.CART_INFO, + QUERY_KEYS.ORDER_LIST, + ], + }, }); export const useProducts = (qs?: ShopAPISchema.ProductListQueryParams) => @@ -62,24 +97,28 @@ namespace ShopAPIHook { useMutation({ mutationKey: MUTATION_KEYS.CART_ITEM_APPEND, mutationFn: ShopAPIRoute.appendItemToCart, + meta: { invalidates: [QUERY_KEYS.CART_INFO] }, }); export const useRemoveItemFromCartMutation = () => useMutation({ mutationKey: MUTATION_KEYS.CART_ITEM_REMOVE, mutationFn: ShopAPIRoute.removeItemFromCart, + meta: { invalidates: [QUERY_KEYS.CART_INFO] }, }); export const usePrepareOneItemOrderMutation = () => useMutation({ mutationKey: MUTATION_KEYS.ONE_ITEM_ORDER_START, mutationFn: ShopAPIRoute.prepareOneItemOrder, + meta: { invalidates: [QUERY_KEYS.CART_INFO, QUERY_KEYS.ORDER_LIST] }, }); - export const usePrepareOrderMutation = () => + export const usePrepareCartOrderMutation = () => useMutation({ mutationKey: MUTATION_KEYS.CART_ORDER_START, mutationFn: ShopAPIRoute.prepareCartOrder, + meta: { invalidates: [QUERY_KEYS.CART_INFO, QUERY_KEYS.ORDER_LIST] }, }); export const useOrders = () => @@ -92,12 +131,14 @@ namespace ShopAPIHook { useMutation({ mutationKey: MUTATION_KEYS.ONE_ITEM_REFUND, mutationFn: ShopAPIRoute.refundOneItemFromOrder, + meta: { invalidates: [QUERY_KEYS.ORDER_LIST] }, }); export const useOrderRefundMutation = () => useMutation({ mutationKey: MUTATION_KEYS.ALL_ORDER_REFUND, mutationFn: ShopAPIRoute.refundAllItemsInOrder, + meta: { invalidates: [QUERY_KEYS.ORDER_LIST] }, }); } diff --git a/package/pyconkr-shop/schemas/index.ts b/package/pyconkr-shop/schemas/index.ts index 5597060..32e5350 100644 --- a/package/pyconkr-shop/schemas/index.ts +++ b/package/pyconkr-shop/schemas/index.ts @@ -29,7 +29,7 @@ namespace ShopAPISchema { password: string; }; - export type UserStatus = { + export type UserSignedInStatus = { status: number; meta: { is_authenticated: true; @@ -48,7 +48,9 @@ namespace ShopAPISchema { email: string; }[]; }; - } | { + } + + export type UserNotSignedInStatus = { status: number; meta: { is_authenticated: false; From 0d3ab6d762053117cbd49ae407814470833241f8 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Wed, 23 Apr 2025 23:24:04 +0900 Subject: [PATCH 15/16] =?UTF-8?q?feat:=20=EC=9E=90=EC=A3=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20utils=20=EB=B0=8F=20components=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-common/components/mdx.tsx | 26 ++++++++++++ .../components/price_display.tsx | 5 +++ package/pyconkr-common/utils/form.ts | 40 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 package/pyconkr-common/components/mdx.tsx create mode 100644 package/pyconkr-common/components/price_display.tsx create mode 100644 package/pyconkr-common/utils/form.ts diff --git a/package/pyconkr-common/components/mdx.tsx b/package/pyconkr-common/components/mdx.tsx new file mode 100644 index 0000000..f6afdce --- /dev/null +++ b/package/pyconkr-common/components/mdx.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import * as runtime from "react/jsx-runtime"; + +import { evaluate } from "@mdx-js/mdx"; +import { MDXProvider } from "@mdx-js/react"; +import { CircularProgress, Typography } from '@mui/material'; +import { wrap } from '@suspensive/react'; +import { useSuspenseQuery } from "@tanstack/react-query"; + +const useMDX = (text: string) => useSuspenseQuery({ + queryKey: ['mdx', text], + queryFn: async () => { + const { default: RenderResult } = await evaluate(text, { ...runtime, baseUrl: import.meta.url }); + return + } +}) + +export const MDXRenderer: React.FC<{ text: string }> = wrap + .ErrorBoundary({ + fallback: ({ error }) => { + console.error('MDX 변환 오류:', error); + return MDX 변환 오류: {error.message} + } + }) + .Suspense({ fallback: }) + .on(({ text }) => useMDX(text).data); diff --git a/package/pyconkr-common/components/price_display.tsx b/package/pyconkr-common/components/price_display.tsx new file mode 100644 index 0000000..f15a70f --- /dev/null +++ b/package/pyconkr-common/components/price_display.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const PriceDisplay: React.FC<{ price: number, label?: string }> = ({ price, label }) => { + return <>{(label ? `${label} : ` : '') + price.toLocaleString()}원 +}; diff --git a/package/pyconkr-common/utils/form.ts b/package/pyconkr-common/utils/form.ts new file mode 100644 index 0000000..4a98305 --- /dev/null +++ b/package/pyconkr-common/utils/form.ts @@ -0,0 +1,40 @@ +import * as R from 'remeda' + +export type PossibleFormInputType = HTMLFormElement | undefined | null +export type FormResultObject = { [k: string]: FormDataEntryValue | boolean | null } + +export const isFormValid = (form: HTMLFormElement | null | undefined): form is HTMLFormElement => { + if (!(R.isObjectType(form) && form instanceof HTMLFormElement)) return false + + if (!form.checkValidity()) { + form.reportValidity() + return false + } + + return true +} + +export function getFormValue(_: { form: HTMLFormElement; fieldToExcludeWhenFalse?: string[]; fieldToNullWhenFalse?: string[] }): T { + const formData: { + [k: string]: FormDataEntryValue | boolean | null + } = Object.fromEntries(new FormData(_.form)) + Object.keys(formData) + .filter((key) => (_.fieldToExcludeWhenFalse ?? []).includes(key) || (_.fieldToNullWhenFalse ?? []).includes(key)) + .filter((key) => R.isEmpty(formData[key] as string)) + .forEach((key) => { + if ((_.fieldToExcludeWhenFalse ?? []).includes(key)) { + delete formData[key] + } else if ((_.fieldToNullWhenFalse ?? []).includes(key)) { + formData[key] = null + } + }) + Array.from(_.form.children).forEach((child) => { + const targetElement: Element | null = child + if (targetElement && !(targetElement instanceof HTMLInputElement)) { + const targetElements = targetElement.querySelectorAll('input') + for (const target of targetElements) + if (target instanceof HTMLInputElement && target.type === 'checkbox') formData[target.name] = target.checked ? true : false + } + }) + return formData as T +} From d940057a90f53b87cdf1e6a4d39ad851ab8e0469 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Wed, 23 Apr 2025 23:26:10 +0900 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20=EC=A3=BC=EC=84=9D=EC=9D=98=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=EB=90=9C=20parameter=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index a678e97..1458645 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -6,7 +6,7 @@ namespace ShopAPIRoute { /** * 로그인합니다. * @param data - 로그인 정보 - * @param data.username - 사용자 이름 + * @param data.email - 사용자 이름 * @param data.password - 비밀번호 * @returns 로그인 정보 * @throws 401 - 로그인 정보가 없습니다.