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'),
+ },
+ },
});