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..489cc59 100644 --- a/package.json +++ b/package.json @@ -20,18 +20,28 @@ "@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", @@ -39,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/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/cookie.ts b/package/pyconkr-common/utils/cookie.ts new file mode 100644 index 0000000..5e30cd0 --- /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('=', 2) + if (key.trim() === name) cookieValue = decodeURIComponent(value) as string + }) + return cookieValue +} 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 +} diff --git a/package/pyconkr-shop/apis/client.ts b/package/pyconkr-shop/apis/client.ts new file mode 100644 index 0000000..e4e568f --- /dev/null +++ b/package/pyconkr-shop/apis/client.ts @@ -0,0 +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 }], +}; + +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, + 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 + ) => { + 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)< + 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)< + 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)< + 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)< + T, + AxiosResponse, + D + >(url, data, 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 +); diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts new file mode 100644 index 0000000..1458645 --- /dev/null +++ b/package/pyconkr-shop/apis/index.ts @@ -0,0 +1,173 @@ +import { shopAPIClient } from "./client"; + +import ShopAPISchema from "@pyconkr-shop/schemas"; + +namespace ShopAPIRoute { + /** + * 로그인합니다. + * @param data - 로그인 정보 + * @param data.email - 사용자 이름 + * @param data.password - 비밀번호 + * @returns 로그인 정보 + * @throws 401 - 로그인 정보가 없습니다. + */ + 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", requestPayload); + }; + + /** + * 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(); + setTimeout(() => document.body.removeChild(form), 100); + }; + + /** + * 로그아웃합니다. + * @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 data.orderId - 환불할 주문 내역의 UUID + */ + 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 new file mode 100644 index 0000000..7811186 --- /dev/null +++ b/package/pyconkr-shop/hooks/index.ts @@ -0,0 +1,145 @@ +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_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"], + 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 useUserStatus = () => + useSuspenseQuery({ + queryKey: QUERY_KEYS.USER, + queryFn: async () => { + try { + const userInfo = await ShopAPIRoute.retrieveUserInfo(); + return userInfo.meta.is_authenticated === true ? userInfo : null; + } catch (e) { + return null; + } + }, + }); + + export const useSignInWithEmailMutation = () => + 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) => + 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, + 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 usePrepareCartOrderMutation = () => + useMutation({ + mutationKey: MUTATION_KEYS.CART_ORDER_START, + mutationFn: ShopAPIRoute.prepareCartOrder, + meta: { invalidates: [QUERY_KEYS.CART_INFO, QUERY_KEYS.ORDER_LIST] }, + }); + + 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, + 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] }, + }); +} + +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..32e5350 --- /dev/null +++ b/package/pyconkr-shop/schemas/index.ts @@ -0,0 +1,198 @@ +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 UserSignedInStatus = { + 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; + }[]; + }; + } + + export type UserNotSignedInStatus = { + 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((error) => { + return ( + R.isPlainObject(error) && + R.isString(error.code) && + R.isString(error.detail) && + (error.attr === null || R.isString(error.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..655ebfd --- /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_PYCONKR_SHOP_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) + } + ) +} 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: diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..4b3ccaa 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,14 @@ /// +interface ViteTypeOptions { + strictImportEnv: unknown; +} + +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 { + 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'), + }, + }, });