diff --git a/dotenv/.env.development b/dotenv/.env.development
index f1e20bf..1f5fdf5 100644
--- a/dotenv/.env.development
+++ b/dotenv/.env.development
@@ -1,2 +1,3 @@
VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.dev.pycon.kr
VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=DEBUG_csrftoken
+VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID=imp80859147
diff --git a/dotenv/.env.production b/dotenv/.env.production
index aee9515..a4b0532 100644
--- a/dotenv/.env.production
+++ b/dotenv/.env.production
@@ -1,2 +1,3 @@
VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.pycon.kr
VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=csrftoken
+VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID=imp96676915
diff --git a/index.html b/index.html
index e4b78ea..759e7ee 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,46 @@
-
-
-
- Vite + React + TS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PyCon Korea 2025
diff --git a/package.json b/package.json
index 5933f99..07661b6 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@mdx-js/mdx": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@mdx-js/rollup": "^3.1.0",
+ "@mui/icons-material": "^7.1.0",
"@mui/material": "^7.0.2",
"@pyconkr-common": "link:package/pyconkr-common",
"@pyconkr-shop": "link:package/pyconkr-shop",
@@ -28,10 +29,12 @@
"axios": "^1.8.4",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
+ "globals": "^15.15.0",
"notistack": "^3.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-router-dom": "^7.5.3"
+ "react-router-dom": "^7.5.3",
+ "remeda": "^2.21.3"
},
"devDependencies": {
@@ -49,14 +52,13 @@
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"gh-pages": "^6.3.0",
- "globals": "^15.15.0",
"iamport-typings": "^1.4.0",
"prettier": "^3.5.3",
- "remeda": "^2.21.3",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0",
"vite-plugin-mdx": "^3.6.1",
+ "vite-plugin-mkcert": "^1.17.8",
"vite-plugin-svgr": "^4.3.0"
},
diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts
index 1458645..eb4e0c8 100644
--- a/package/pyconkr-shop/apis/index.ts
+++ b/package/pyconkr-shop/apis/index.ts
@@ -86,7 +86,7 @@ namespace ShopAPIRoute {
* @returns 현재 장바구니 상태
*/
export const retrieveCart = () =>
- shopAPIClient.get("v1/orders/cart/");
+ shopAPIClient.get("v1/orders/cart/");
/**
* 장바구니에 상품을 추가합니다.
@@ -138,7 +138,7 @@ namespace ShopAPIRoute {
*/
export const prepareCartOrder = () =>
shopAPIClient.post(
- "v1/orders/cart/",
+ "v1/orders/",
undefined
);
@@ -168,6 +168,20 @@ namespace ShopAPIRoute {
*/
export const refundAllItemsInOrder = (data: { order_id: string }) =>
shopAPIClient.delete(`v1/orders/${data.order_id}/`);
+
+ /**
+ * 결제 완료된 주문의 사용자 정의 응답용 상품 옵션을 수정합니다.
+ * @param data.order_id - 수정할 주문 내역의 UUID
+ * @param data.order_product_relation_id - 수정할 주문 내역 내 상품의 UUID
+ * @param data.options - 수정할 상품 옵션 정보
+ * @param data.options[].order_product_option_relation - 수정할 상품 옵션의 UUID
+ * @param data.options[].custom_response - 수정할 상품 옵션에 대한 사용자 정의 응답
+ */
+ export const patchOrderOptions = async (data: ShopAPISchema.OrderOptionsPatchRequest) =>
+ shopAPIClient.patch(
+ `v1/orders/${data.order_id}/products/${data.order_product_relation_id}/options/`,
+ data.options
+ );
}
export default ShopAPIRoute;
diff --git a/package/pyconkr-shop/components/index.ts b/package/pyconkr-shop/components/index.ts
new file mode 100644
index 0000000..2c9b089
--- /dev/null
+++ b/package/pyconkr-shop/components/index.ts
@@ -0,0 +1,15 @@
+import {
+ OptionGroupInput as OptionGroupInputComponent,
+ OrderProductRelationOptionInput as OrderProductRelationOptionInputComponent,
+} from "./option_group_input";
+import { PriceDisplay as PriceDisplayComponent } from "./price_display";
+import { ShopSignInGuard as ShopSignInGuardComponent } from "./signin_guard";
+
+namespace ShopComponent {
+ export const OptionGroupInput = OptionGroupInputComponent;
+ export const OrderProductRelationOptionInput = OrderProductRelationOptionInputComponent;
+ export const PriceDisplay = PriceDisplayComponent;
+ export const ShopSignInGuard = ShopSignInGuardComponent;
+}
+
+export default ShopComponent;
diff --git a/package/pyconkr-shop/components/option_group_input.tsx b/package/pyconkr-shop/components/option_group_input.tsx
new file mode 100644
index 0000000..bdbf15d
--- /dev/null
+++ b/package/pyconkr-shop/components/option_group_input.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import * as R from 'remeda';
+
+import { FormControl, InputLabel, MenuItem, Select, TextField, Tooltip } from '@mui/material';
+
+import ShopAPISchema from '@pyconkr-shop/schemas';
+import ShopAPIUtil from '@pyconkr-shop/utils';
+import { PriceDisplay } from './price_display';
+
+type CommonOptionGroupType = {
+ id: string;
+ name: string;
+}
+type SelectableOptionGroupType = CommonOptionGroupType & {
+ is_custom_response: false;
+ custom_response_pattern: null;
+}
+type CustomResponseOptionGroupType = CommonOptionGroupType & {
+ is_custom_response: true;
+ custom_response_pattern: string;
+}
+type OptionGroupType = SelectableOptionGroupType | CustomResponseOptionGroupType;
+
+type SimplifiedOption = Pick;
+
+const isFilledString = (str: unknown): str is string => R.isString(str) && !R.isEmpty(str);
+
+const SelectableOptionGroupInput: React.FC<{
+ optionGroup: SelectableOptionGroupType;
+ options: SimplifiedOption[];
+ defaultValue?: string;
+ disabled?: boolean;
+ disabledReason?: string;
+}> = ({ optionGroup, options, defaultValue, disabled, disabledReason }) => {
+ const optionElements = options.map(
+ (option) => {
+ const isOptionOutOfStock = R.isNumber(option.leftover_stock) && option.leftover_stock <= 0;
+
+ return
+ }
+ )
+
+ const selectElement =
+ {optionGroup.name}
+
+
+
+ return isFilledString(disabledReason) ? {selectElement} : selectElement
+}
+
+const CustomResponseOptionGroupInput: React.FC<{
+ optionGroup: CustomResponseOptionGroupType;
+ defaultValue?: string;
+ disabled?: boolean;
+ disabledReason?: string;
+}> = ({ optionGroup, defaultValue, disabled, disabledReason }) => {
+ const pattern = ShopAPIUtil.getCustomResponsePattern(optionGroup)?.source;
+
+ const textFieldElement =
+
+ return isFilledString(disabledReason) ? {textFieldElement} : textFieldElement
+}
+
+export const OptionGroupInput: React.FC<{
+ optionGroup: OptionGroupType;
+ options: SimplifiedOption[];
+
+ defaultValue?: string;
+ disabled?: boolean;
+ disabledReason?: string;
+}> = ({ optionGroup, options, defaultValue, disabled, disabledReason }) => optionGroup.is_custom_response
+ ?
+ :
+
+
+export const OrderProductRelationOptionInput: React.FC<{
+ optionRel: ShopAPISchema.OrderProductItem["options"][number];
+ disabled?: boolean;
+ disabledReason?: string;
+}> = ({ optionRel, disabled, disabledReason }) => {
+ let defaultValue: string | null = null;
+ let guessedDisabledReason: string | undefined = undefined;
+ let dummyOptions: { id: string; name: string; additional_price: number; leftover_stock: number | null }[] = [];
+
+ // type hinting을 위해 if문을 사용함
+ if (optionRel.product_option_group.is_custom_response === false && R.isNonNull(optionRel.product_option)) {
+ defaultValue = optionRel.product_option.id;
+ guessedDisabledReason = '추가 비용이 발생하는 옵션은 수정할 수 없어요.';
+ dummyOptions = [
+ {
+ id: optionRel.product_option.id,
+ name: optionRel.product_option.name,
+ additional_price: optionRel.product_option.additional_price || 0,
+ leftover_stock: null,
+ }
+ ]
+ } else {
+ defaultValue = optionRel.custom_response;
+ }
+
+ return
+}
diff --git a/package/pyconkr-common/components/price_display.tsx b/package/pyconkr-shop/components/price_display.tsx
similarity index 100%
rename from package/pyconkr-common/components/price_display.tsx
rename to package/pyconkr-shop/components/price_display.tsx
diff --git a/package/pyconkr-shop/components/signin_guard.tsx b/package/pyconkr-shop/components/signin_guard.tsx
new file mode 100644
index 0000000..9e0c753
--- /dev/null
+++ b/package/pyconkr-shop/components/signin_guard.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import { Typography } from '@mui/material';
+
+import ShopAPIHook from '@pyconkr-shop/hooks';
+
+export const ShopSignInGuard: React.FC<{ children: React.ReactNode, fallback?: React.ReactNode }> = ({ children, fallback }) => {
+ const { data } = ShopAPIHook.useUserStatus();
+ const renderedFallback = fallback || 로그인 후 이용해주세요.;
+ return (data && data.meta.is_authenticated === true) ? children : renderedFallback;
+};
diff --git a/package/pyconkr-shop/hooks/index.ts b/package/pyconkr-shop/hooks/index.ts
index 7811186..82a5cc5 100644
--- a/package/pyconkr-shop/hooks/index.ts
+++ b/package/pyconkr-shop/hooks/index.ts
@@ -140,6 +140,13 @@ namespace ShopAPIHook {
mutationFn: ShopAPIRoute.refundAllItemsInOrder,
meta: { invalidates: [QUERY_KEYS.ORDER_LIST] },
});
+
+ export const useOptionsOfOneItemInOrderPatchMutation = () =>
+ useMutation({
+ mutationKey: MUTATION_KEYS.CART_ITEM_APPEND,
+ mutationFn: ShopAPIRoute.patchOrderOptions,
+ 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
index 32e5350..13409b0 100644
--- a/package/pyconkr-shop/schemas/index.ts
+++ b/package/pyconkr-shop/schemas/index.ts
@@ -73,18 +73,25 @@ namespace ShopAPISchema {
name: string;
additional_price: number;
max_quantity_per_user: number;
- leftover_stock: number;
+ leftover_stock: number | null;
};
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[];
- };
+ } & (
+ {
+ is_custom_response: false;
+ custom_response_pattern: null;
+ } | {
+ is_custom_response: true;
+ custom_response_pattern: string;
+ }
+ );
export type ProductListQueryParams = {
category_group?: string;
@@ -124,28 +131,43 @@ namespace ShopAPISchema {
export type OrderProductItem = {
id: string;
- status: PaymentHistoryStatus;
+ status: OrderProductItemStatus;
price: number;
additional_price: number;
+ not_refundable_reason: string | null;
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;
- }[];
+ options: (
+ {
+ product_option_group: {
+ id: string;
+ name: string;
+ is_custom_response: false;
+ custom_response_pattern: null;
+ response_modifiable_ends_at: string | null;
+ }
+ product_option: {
+ id: string;
+ name: string;
+ additional_price: number;
+ };
+ custom_response: null;
+ } | {
+ product_option_group: {
+ id: string;
+ name: string;
+ is_custom_response: true;
+ custom_response_pattern: string;
+ response_modifiable_ends_at: string | null;
+ }
+ product_option: null;
+ custom_response: string;
+ }
+ )[];
};
export type Order = {
@@ -154,6 +176,7 @@ namespace ShopAPISchema {
first_paid_price: number;
current_paid_price: number;
current_status: PaymentHistoryStatus;
+ not_fully_refundable_reason: string | null;
created_at: string;
payment_histories: PaymentHistory[];
@@ -175,6 +198,15 @@ namespace ShopAPISchema {
order_id: string;
order_product_relation_id: string;
};
+
+ export type OrderOptionsPatchRequest = {
+ order_id: string
+ order_product_relation_id: string;
+ options: {
+ order_product_option_relation: string
+ custom_response: string
+ }[]
+ };
}
export const isObjectErrorResponseSchema = (
diff --git a/package/pyconkr-shop/utils/index.ts b/package/pyconkr-shop/utils/index.ts
new file mode 100644
index 0000000..73aa413
--- /dev/null
+++ b/package/pyconkr-shop/utils/index.ts
@@ -0,0 +1,26 @@
+import * as R from "remeda";
+
+import ShopAPISchema from "@pyconkr-shop/schemas";
+import { startPortOnePurchase as _startPortOnePurchase } from './portone';
+
+namespace ShopAPIUtil {
+ export const getCustomResponsePattern = (optionGroup: Pick) => {
+ const pattern = optionGroup.custom_response_pattern?.trim() ?? "";
+ return R.isString(pattern) && !R.isEmpty(pattern) ? new RegExp(pattern, "g") : undefined;
+ };
+
+ export const isOrderProductOptionModifiable = (optionRel: ShopAPISchema.OrderProductItem["options"][number]): boolean => {
+ if (!optionRel.product_option_group.is_custom_response) return false;
+
+ if (R.isNullish(optionRel.product_option_group.response_modifiable_ends_at))
+ return true;
+ else if (new Date() <= new Date(optionRel.product_option_group.response_modifiable_ends_at))
+ return true;
+
+ return false;
+ }
+
+ export const startPortOnePurchase = _startPortOnePurchase;
+};
+
+export default ShopAPIUtil;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae2a479..1bc3f88 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
'@mdx-js/rollup':
specifier: ^3.1.0
version: 3.1.0(acorn@8.14.1)(rollup@4.39.0)
+ '@mui/icons-material':
+ specifier: ^7.1.0
+ version: 7.1.0(@mui/material@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))(@types/react@19.1.1)(react@19.1.0)
'@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)
@@ -43,13 +46,16 @@ importers:
version: 5.72.2(react@19.1.0)
axios:
specifier: ^1.8.4
- version: 1.8.4
+ version: 1.8.4(debug@4.4.0)
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)
+ globals:
+ specifier: ^15.15.0
+ version: 15.15.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)
@@ -69,12 +75,12 @@ importers:
'@eslint/js':
specifier: ^9.21.0
version: 9.24.0
- '@types/node':
- specifier: ^22.15.3
- version: 22.15.3
'@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.15.3
+ version: 22.15.3
'@types/react':
specifier: ^19.0.10
version: 19.1.1
@@ -87,6 +93,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.3.4(vite@6.2.6(@types/node@22.15.3))
+ csstype:
+ specifier: ^3.1.3
+ version: 3.1.3
eslint:
specifier: ^9.21.0
version: 9.24.0
@@ -105,9 +114,6 @@ importers:
gh-pages:
specifier: ^6.3.0
version: 6.3.0
- globals:
- specifier: ^15.15.0
- version: 15.15.0
iamport-typings:
specifier: ^1.4.0
version: 1.4.0
@@ -126,6 +132,9 @@ importers:
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(@types/node@22.15.3))
+ vite-plugin-mkcert:
+ specifier: ^1.17.8
+ version: 1.17.8(vite@6.2.6(@types/node@22.15.3))
vite-plugin-svgr:
specifier: ^4.3.0
version: 4.3.0(rollup@4.39.0)(typescript@5.7.3)(vite@6.2.6(@types/node@22.15.3))
@@ -211,6 +220,10 @@ packages:
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.27.1':
+ resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.27.0':
resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==}
engines: {node: '>=6.9.0'}
@@ -524,6 +537,17 @@ packages:
'@mui/core-downloads-tracker@7.0.2':
resolution: {integrity: sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==}
+ '@mui/icons-material@7.1.0':
+ resolution: {integrity: sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@mui/material': ^7.1.0
+ '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@mui/material@7.0.2':
resolution: {integrity: sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==}
engines: {node: '>=14.0.0'}
@@ -732,6 +756,11 @@ 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
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
engines: {node: '>=14'}
@@ -799,10 +828,6 @@ packages:
engines: {node: '>=14'}
peerDependencies:
'@svgr/core': '*'
- '@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==}
@@ -2643,6 +2668,12 @@ packages:
'@mdx-js/mdx': <2
vite: <3
+ vite-plugin-mkcert@1.17.8:
+ resolution: {integrity: sha512-S+4tNEyGqdZQ3RLAG54ETeO2qyURHWrVjUWKYikLAbmhh/iJ+36gDEja4OWwFyXNuvyXcZwNt5TZZR9itPeG5Q==}
+ engines: {node: '>=v16.7.0'}
+ peerDependencies:
+ vite: '>=3'
+
vite-plugin-svgr@4.3.0:
resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==}
peerDependencies:
@@ -2827,6 +2858,8 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
+ '@babel/runtime@7.27.1': {}
+
'@babel/template@7.27.0':
dependencies:
'@babel/code-frame': 7.26.2
@@ -3135,6 +3168,14 @@ snapshots:
'@mui/core-downloads-tracker@7.0.2': {}
+ '@mui/icons-material@7.1.0(@mui/material@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))(@types/react@19.1.1)(react@19.1.0)':
+ dependencies:
+ '@babel/runtime': 7.27.1
+ '@mui/material': 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)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.1
+
'@mui/material@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)':
dependencies:
'@babel/runtime': 7.27.0
@@ -3298,6 +3339,10 @@ snapshots:
'@rtsao/scc@1.1.0': {}
+ '@suspensive/react@2.18.12(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.10)':
dependencies:
'@babel/core': 7.26.10
@@ -3367,9 +3412,6 @@ snapshots:
svg-parser: 2.0.4
transitivePeerDependencies:
- supports-color
- '@suspensive/react@2.18.12(react@19.1.0)':
- dependencies:
- react: 19.1.0
'@tanstack/query-core@5.72.2': {}
@@ -3634,9 +3676,9 @@ snapshots:
axe-core@4.10.3: {}
- axios@1.8.4:
+ axios@1.8.4(debug@4.4.0):
dependencies:
- follow-redirects: 1.15.9
+ follow-redirects: 1.15.9(debug@4.4.0)
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -4309,7 +4351,9 @@ snapshots:
flatted@3.3.3: {}
- follow-redirects@1.15.9: {}
+ follow-redirects@1.15.9(debug@4.4.0):
+ optionalDependencies:
+ debug: 4.4.0
for-each@0.3.5:
dependencies:
@@ -5718,6 +5762,15 @@ snapshots:
unified: 9.2.2
vite: 6.2.6(@types/node@22.15.3)
+ vite-plugin-mkcert@1.17.8(vite@6.2.6(@types/node@22.15.3)):
+ dependencies:
+ axios: 1.8.4(debug@4.4.0)
+ debug: 4.4.0
+ picocolors: 1.1.1
+ vite: 6.2.6(@types/node@22.15.3)
+ transitivePeerDependencies:
+ - supports-color
+
vite-plugin-svgr@4.3.0(rollup@4.39.0)(typescript@5.7.3)(vite@6.2.6(@types/node@22.15.3)):
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.39.0)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..efc037a
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+onlyBuiltDependencies:
+ - esbuild
diff --git a/public/favicon-180.png b/public/favicon-180.png
new file mode 100755
index 0000000..232c395
Binary files /dev/null and b/public/favicon-180.png differ
diff --git a/public/favicon-192.png b/public/favicon-192.png
new file mode 100755
index 0000000..a48d5be
Binary files /dev/null and b/public/favicon-192.png differ
diff --git a/public/favicon-512.png b/public/favicon-512.png
new file mode 100755
index 0000000..785bbdc
Binary files /dev/null and b/public/favicon-512.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100755
index 0000000..9b77659
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100755
index 0000000..53e26f3
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100755
index 0000000..13d3b71
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,25 @@
+{
+ "name": "PyCon Korea 2025",
+ "icons": [
+ {
+ "src": "favicon-192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "favicon-512.png",
+ "type": "image/png",
+ "sizes": "512x512",
+ "purpose": "maskable"
+ },
+ {
+ "src": "favicon-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "id": "/",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone"
+}
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/src/App.tsx b/src/App.tsx
index a4ae52e..39a4523 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,10 @@
-import { BrowserRouter, Routes, Route } from "react-router-dom";
+import React from "react";
+
+import { BrowserRouter, Route, Routes } from "react-router-dom";
import MainLayout from "./components/layout";
-import Test from "./components/Test";
+import { Test } from "./components/pages/test";
-function App() {
+export const App: React.FC = () => {
return (
@@ -13,5 +15,3 @@ function App() {
);
}
-
-export default App;
diff --git a/src/components/pages/test.tsx b/src/components/pages/test.tsx
new file mode 100644
index 0000000..bf3c553
--- /dev/null
+++ b/src/components/pages/test.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { Box, Button } from '@mui/material';
+import { MdiTestPage } from "@src/debug/page/mdi_test";
+import { ShopTestPage } from '@src/debug/page/shop_test';
+
+type SelectedTabType = "shop" | "mdi";
+
+export const Test: React.FC = () => {
+ const [selectedTab, setSelectedTab] = React.useState("shop");
+
+ return
+
+
+ {selectedTab === "shop" && }
+ {selectedTab === "mdi" && }
+
+}
diff --git a/src/components/Test.tsx b/src/debug/page/mdi_test.tsx
similarity index 96%
rename from src/components/Test.tsx
rename to src/debug/page/mdi_test.tsx
index 9a5b6ae..8bf2ffc 100644
--- a/src/components/Test.tsx
+++ b/src/debug/page/mdi_test.tsx
@@ -1,17 +1,18 @@
-import { useState } from "react";
-import { MDXProvider } from "@mdx-js/react";
-import * as runtime from "react/jsx-runtime";
+
+import styled from "@emotion/styled";
import { evaluate } from "@mdx-js/mdx";
+import { MDXProvider } from "@mdx-js/react";
import {
Box,
Button,
- TextField,
- Typography,
Card,
CardContent,
Stack,
+ TextField,
+ Typography,
} from "@mui/material";
-import styled from "@emotion/styled";
+import React, { useState } from "react";
+import * as runtime from "react/jsx-runtime";
// styled 컴포넌트 방식
const StyledCard = styled(Card)`
@@ -33,7 +34,7 @@ const StyledCardContent = styled(CardContent)`
}
`;
-export default function Test() {
+export const MdiTestPage: React.FC = () => {
const [mdxInput, setMdxInput] = useState("");
const [Content, setContent] = useState(() => () => null);
const [count, setCount] = useState(0);
diff --git a/src/debug/page/shop_component/cart.tsx b/src/debug/page/shop_component/cart.tsx
new file mode 100644
index 0000000..76a903c
--- /dev/null
+++ b/src/debug/page/shop_component/cart.tsx
@@ -0,0 +1,102 @@
+import React from "react";
+
+import { ExpandMore } from '@mui/icons-material';
+import { Accordion, AccordionActions, AccordionDetails, AccordionSummary, Button, CircularProgress, Divider, Stack, Typography } from "@mui/material";
+import { wrap } from "@suspensive/react";
+import { useQueryClient } from '@tanstack/react-query';
+
+import ShopComponent from "@pyconkr-shop/components";
+import ShopAPIHook from "@pyconkr-shop/hooks";
+import ShopAPISchema from '@pyconkr-shop/schemas';
+import ShopAPIUtil from "@pyconkr-shop/utils";
+
+const ShopCartItem: React.FC<{
+ cartProdRel: ShopAPISchema.OrderProductItem,
+ removeItemFromCartFunc: (cartProductId: string) => void,
+ disabled?: boolean
+}> = ({ cartProdRel, disabled, removeItemFromCartFunc }) =>
+ }>
+
+ {cartProdRel.product.name}
+
+
+
+
+ {
+ cartProdRel.options.map(
+ (optionRel) =>
+ )
+ }
+
+
+
+
+
+ 상품 가격:
+
+
+
+
+
+
+
+
+export const ShopCartList: React.FC<{ onPaymentCompleted?: () => void; }> = ({ onPaymentCompleted }) => {
+ const queryClient = useQueryClient();
+ const cartOrderStartMutation = ShopAPIHook.usePrepareCartOrderMutation();
+ const removeItemFromCartMutation = ShopAPIHook.useRemoveItemFromCartMutation();
+
+ const removeItemFromCart = (cartProductId: string) => removeItemFromCartMutation.mutate({ cartProductId });
+ const startCartOrder = () => cartOrderStartMutation.mutate(
+ undefined,
+ {
+ onSuccess: (order: ShopAPISchema.Order) => {
+ ShopAPIUtil.startPortOnePurchase(
+ order,
+ () => {
+ queryClient.invalidateQueries();
+ queryClient.resetQueries();
+ onPaymentCompleted?.();
+ },
+ (response) => alert("결제를 실패했습니다!\n" + response.error_msg),
+ () => { }
+ );
+ },
+ onError: (error) => alert(error.message || '결제 준비 중 문제가 발생했습니다,\n잠시 후 다시 시도해주세요.'),
+ }
+ );
+
+ const disabled = removeItemFromCartMutation.isPending || cartOrderStartMutation.isPending;
+
+ const WrappedShopCartList = wrap
+ .ErrorBoundary({ fallback: 장바구니 정보를 불러오는 중 문제가 발생했습니다.
})
+ .Suspense({ fallback: })
+ .on(() => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { data } = ShopAPIHook.useCart();
+
+ return data.products.length === 0
+ ? 장바구니가 비어있어요!
+ : <>
+ {data.products.map((prodRel) => )}
+
+
+
+ 결제 금액:
+
+
+ >
+ });
+
+ return <>
+ Cart List
+
+
+
+ >
+}
diff --git a/src/debug/page/shop_component/order.tsx b/src/debug/page/shop_component/order.tsx
new file mode 100644
index 0000000..ddb77b8
--- /dev/null
+++ b/src/debug/page/shop_component/order.tsx
@@ -0,0 +1,137 @@
+import React from "react";
+import * as R from "remeda";
+
+import { ExpandMore } from '@mui/icons-material';
+import {
+ Accordion,
+ AccordionActions,
+ AccordionDetails,
+ AccordionSummary,
+ Button,
+ CircularProgress,
+ Divider,
+ List,
+ Stack,
+ Typography
+} from "@mui/material";
+import { wrap } from "@suspensive/react";
+
+import { getFormValue, isFormValid } from '@pyconkr-common/utils/form';
+import ShopComponent from "@pyconkr-shop/components";
+import ShopAPIHook from "@pyconkr-shop/hooks";
+import ShopAPISchema from "@pyconkr-shop/schemas";
+import ShopAPIUtil from '@pyconkr-shop/utils';
+
+const PaymentHistoryStatusTranslated: { [k in ShopAPISchema.PaymentHistoryStatus]: string } = {
+ "pending": "결제 대기중",
+ "completed": "결제 완료",
+ "partial_refunded": "부분 환불됨",
+ "refunded": "환불됨",
+}
+
+const ShopOrderItem: React.FC<{ order: ShopAPISchema.Order, disabled?: boolean }> = ({ order, disabled }) => {
+ const orderRefundMutation = ShopAPIHook.useOrderRefundMutation();
+ const oneItemRefundMutation = ShopAPIHook.useOneItemRefundMutation();
+ const optionsOfOneItemInOrderPatchMutation = ShopAPIHook.useOptionsOfOneItemInOrderPatchMutation();
+
+ const receiptUrl = `${import.meta.env.VITE_PYCONKR_SHOP_API_DOMAIN}/v1/orders/${order.id}/receipt/`
+
+ const refundOrder = () => orderRefundMutation.mutate({ order_id: order.id });
+ const openReceipt = () => window.open(receiptUrl, '_blank');
+
+ const isPending = disabled || orderRefundMutation.isPending || oneItemRefundMutation.isPending || optionsOfOneItemInOrderPatchMutation.isPending;
+ const btnDisabled = isPending || !R.isNullish(order.not_fully_refundable_reason);
+ const btnText = R.isNullish(order.not_fully_refundable_reason) ? '주문 전체 환불' : order.current_status === 'refunded' ? '주문 전체 환불됨' : order.not_fully_refundable_reason;
+
+ return
+ }>
+ {order.name}
+
+
+
+
+ 주문 결제 금액 :
+ 상태: {PaymentHistoryStatusTranslated[order.current_status]}
+
+
+
+ 주문 상품 목록
+
+ {
+ order.products.map(
+ (prodRels) => {
+ const formRef = React.useRef(null);
+ const currentCustomOptionValues: { [k: string]: string } = prodRels.options
+ .filter((optionRel) => ShopAPIUtil.isOrderProductOptionModifiable(optionRel))
+ .reduce((acc, optionRel) => ({ ...acc, [optionRel.product_option_group.id]: optionRel.custom_response }), {});
+
+ const hasPatchableOption = Object.entries(currentCustomOptionValues).length > 0;
+ const patchOptionBtnDisabled = isPending || !hasPatchableOption;
+
+ const refundBtnDisabled = isPending || !R.isNullish(prodRels.not_refundable_reason);
+ const refundBtnText = R.isNullish(prodRels.not_refundable_reason) ? '단일 상품 환불' : prodRels.status === 'refunded' ? '환불됨' : prodRels.not_refundable_reason;
+
+ const refundOneItem = () => oneItemRefundMutation.mutate({ order_id: order.id, order_product_relation_id: prodRels.id });
+ const patchOneItemOptions = () => {
+ if (!isFormValid(formRef.current))
+ throw new Error('Form is not valid');
+
+ const modifiedCustomOptionValues: ShopAPISchema.OrderOptionsPatchRequest['options'] = Object.entries(getFormValue<{ [key: string]: string }>({ form: formRef.current }))
+ .filter(([key, value]) => currentCustomOptionValues[key] !== value)
+ .map(([key, value]) => ({ order_product_option_relation: key, custom_response: value }));
+
+ optionsOfOneItemInOrderPatchMutation.mutate({ order_id: order.id, order_product_relation_id: prodRels.id, options: modifiedCustomOptionValues });
+ }
+
+ return
+ }>{prodRels.product.name}
+
+
+
+
+
+
+
+
+ }
+ )
+ }
+
+
+
+
+
+
+
+
+}
+
+export const ShopOrderList: React.FC = () => {
+ const WrappedShopOrderList = wrap
+ .ErrorBoundary({ fallback: 주문 내역을 불러오는 중 문제가 발생했습니다.
})
+ .Suspense({ fallback: })
+ .on(() => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { data } = ShopAPIHook.useOrders();
+ return {data.map((item) => )}
+ })
+
+ return <>
+ Order List
+
+
+
+ >
+}
diff --git a/src/debug/page/shop_component/product.tsx b/src/debug/page/shop_component/product.tsx
new file mode 100644
index 0000000..cf29de8
--- /dev/null
+++ b/src/debug/page/shop_component/product.tsx
@@ -0,0 +1,156 @@
+import React from "react";
+import * as R from "remeda";
+
+import { ExpandMore } from "@mui/icons-material";
+import {
+ Accordion,
+ AccordionActions,
+ AccordionDetails,
+ AccordionSummary,
+ Button,
+ ButtonProps,
+ CircularProgress,
+ Divider,
+ List,
+ Stack,
+ Typography
+} from "@mui/material";
+import { wrap } from "@suspensive/react";
+import { useQueryClient } from '@tanstack/react-query';
+
+import { MDXRenderer } from "@pyconkr-common/components/mdx";
+import { getFormValue, isFormValid } from '@pyconkr-common/utils/form';
+import ShopComponent from '@pyconkr-shop/components';
+import ShopAPIHook from "@pyconkr-shop/hooks";
+import ShopAPISchema from "@pyconkr-shop/schemas";
+import ShopAPIUtil from "@pyconkr-shop/utils";
+
+const getCartAppendRequestPayload = (product: ShopAPISchema.Product, formRef: React.RefObject): ShopAPISchema.CartItemAppendRequest => {
+ if (!isFormValid(formRef.current))
+ throw new Error('Form is not valid');
+
+ const options = Object.entries(getFormValue<{ [key: string]: string }>({ form: formRef.current })).map(([product_option_group, value]) => {
+ const optionGroup = product.option_groups.find((group) => group.id === product_option_group);
+ if (!optionGroup) throw new Error(`Option group ${product_option_group} not found`);
+
+ const product_option = optionGroup.is_custom_response ? null : value;
+ const custom_response = optionGroup.is_custom_response ? value : null;
+ return { product_option_group, product_option, custom_response }
+ });
+ return { product: product.id, options };
+}
+
+const getProductNotPurchasableReason = (product: ShopAPISchema.Product): string | null => {
+ // 상품이 구매 가능 기간 내에 있고, 상품이 매진되지 않았으며, 매진된 상품 옵션 재고가 없으면 true
+ const now = new Date();
+ const orderableStartsAt = new Date(product.orderable_starts_at);
+ const orderableEndsAt = new Date(product.orderable_ends_at);
+ if (orderableStartsAt > now) return `아직 구매할 수 없어요!\n(${orderableStartsAt.toLocaleString()}부터 구매하실 수 있어요.)`;
+ if (orderableEndsAt < now) return '판매가 종료됐어요!';
+
+ if (R.isNumber(product.leftover_stock) && product.leftover_stock <= 0) return '상품이 품절되었어요!';
+ if (product.option_groups.some((og) => !R.isEmpty(og.options) && og.options.every((o) => R.isNumber(o.leftover_stock) && o.leftover_stock <= 0)))
+ return '선택 가능한 상품 옵션이 모두 품절되어 구매할 수 없어요!';
+
+ return null;
+}
+
+const NotPurchasable: React.FC = ({ children }) => {
+ return
+ {children}
+
+}
+
+const ShopProductItem: React.FC<{ product: ShopAPISchema.Product; onPaymentCompleted?: () => void; }> = ({ product, onPaymentCompleted }) => {
+ const optionFormRef = React.useRef(null);
+
+ const queryClient = useQueryClient();
+ const oneItemOrderStartMutation = ShopAPIHook.usePrepareOneItemOrderMutation();
+ const addItemToCartMutation = ShopAPIHook.useAddItemToCartMutation();
+
+ const addItemToCart = () => addItemToCartMutation.mutate(getCartAppendRequestPayload(product, optionFormRef));
+ const oneItemOrderStart = () => oneItemOrderStartMutation.mutate(
+ getCartAppendRequestPayload(product, optionFormRef),
+ {
+ onSuccess: (order: ShopAPISchema.Order) => {
+ ShopAPIUtil.startPortOnePurchase(
+ order,
+ () => {
+ queryClient.invalidateQueries();
+ queryClient.resetQueries();
+ onPaymentCompleted?.();
+ },
+ (response) => alert("결제를 실패했습니다!\n" + response.error_msg),
+ () => { }
+ );
+ },
+ onError: (error) => alert(error.message || '결제 준비 중 문제가 발생했습니다,\n잠시 후 다시 시도해주세요.'),
+ }
+ );
+
+ const formOnSubmit: React.FormEventHandler = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ const disabled = oneItemOrderStartMutation.isPending || addItemToCartMutation.isPending;
+
+ const notPurchasableReason = getProductNotPurchasableReason(product);
+ const actionButtonProps: ButtonProps = {
+ variant: "contained",
+ color: "secondary",
+ disabled,
+ }
+
+ return
+ } sx={{ m: '0' }}>
+
+ {product.name}
+
+
+
+
+
+
+ 로그인 후 장바구니에 담거나 구매할 수 있어요.}>
+ {
+ R.isNullish(notPurchasableReason)
+ ? <>
+
+
+
+
+
+ 결제 금액:
+ >
+ : {notPurchasableReason}
+ }
+
+
+ {
+ R.isNullish(notPurchasableReason) &&
+
+
+
+ }
+
+}
+
+export const ShopProductList: React.FC = () => {
+ const WrappedShopProductList = wrap
+ .ErrorBoundary({ fallback: 상품 목록을 불러오는 중 문제가 발생했습니다.
})
+ .Suspense({ fallback: })
+ .on(() => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { data } = ShopAPIHook.useProducts();
+ return {data.map((product) => )}
+ })
+
+ return
+ Product List
+
+
+}
diff --git a/src/debug/page/shop_component/user_status.tsx b/src/debug/page/shop_component/user_status.tsx
new file mode 100644
index 0000000..759864e
--- /dev/null
+++ b/src/debug/page/shop_component/user_status.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+
+import { Button, CircularProgress, Stack, TextField, Typography } from "@mui/material";
+import { wrap } from "@suspensive/react";
+
+import { getFormValue, isFormValid } from "@pyconkr-common/utils/form";
+import ShopAPIHook from "@pyconkr-shop/hooks";
+
+export const ShopUserStatus: React.FC = () => {
+ const formRef = React.useRef(null);
+ const signInWithEmailMutation = ShopAPIHook.useSignInWithEmailMutation();
+ const SignInWithSNSMutation = ShopAPIHook.useSignInWithSNSMutation();
+ const signOutMutation = ShopAPIHook.useSignOutMutation();
+
+ const signInWithGoogle = () => SignInWithSNSMutation.mutate({ provider: 'google', callback_url: window.location.href });
+ const signInWithEmail = (e: React.FormEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (!isFormValid(formRef.current)) return;
+ signInWithEmailMutation.mutate(getFormValue<{ email: string; password: string }>({ form: formRef.current }));
+ }
+
+ const disabled = SignInWithSNSMutation.isPending || signInWithEmailMutation.isPending || signOutMutation.isPending;
+
+ const WrappedShopUserStatus = wrap
+ .ErrorBoundary({ fallback: 로그인 정보를 불러오는 중 문제가 발생했습니다.
})
+ .Suspense({ fallback: })
+ .on(() => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { data } = ShopAPIHook.useUserStatus();
+
+ return (data && data.meta.is_authenticated === true) ? (
+
+ User: {data.data.user.username}
+
+
+ ) : (
+
+
+
+
+ )
+ })
+
+ return
+ User Status
+
+
+};
diff --git a/src/debug/page/shop_test.tsx b/src/debug/page/shop_test.tsx
new file mode 100644
index 0000000..efe38f2
--- /dev/null
+++ b/src/debug/page/shop_test.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+import { Divider, Stack, Typography } from "@mui/material";
+
+import { ShopCartList } from '@src/debug/page/shop_component/cart';
+import { ShopOrderList } from "@src/debug/page/shop_component/order";
+import { ShopProductList } from "@src/debug/page/shop_component/product";
+import { ShopUserStatus } from "@src/debug/page/shop_component/user_status";
+
+export const ShopTestPage: React.FC = () =>
+
+ Shop Test Page
+
+
+
+
+
+
+
+
+
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main.tsx b/src/main.tsx
index edcb36c..e229113 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,11 +1,23 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { ThemeProvider, CssBaseline } from "@mui/material";
+
import { Global } from "@emotion/react";
-import { muiTheme, globalStyles } from "./styles/globalStyles";
-import "./index.css";
-import App from "./App.tsx";
+import { CircularProgress, CssBaseline, ThemeProvider } from "@mui/material";
+import { wrap } from "@suspensive/react";
+import { matchQuery, MutationCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { SnackbarProvider } from "notistack";
+
+import { App } from "./App.tsx";
+import { globalStyles, muiTheme } from "./styles/globalStyles";
+
+declare module '@tanstack/react-query' {
+ interface Register {
+ mutationMeta: {
+ invalidates?: string[][];
+ }
+ }
+}
const queryClient = new QueryClient({
defaultOptions: {
@@ -15,15 +27,42 @@ const queryClient = new QueryClient({
retry: 1,
},
},
+ mutationCache: new MutationCache({
+ onSuccess: (_data, _variables, _context, mutation) => {
+ queryClient.invalidateQueries({ predicate: (query) => mutation.meta?.invalidates?.some((queryKey) => matchQuery({ queryKey }, query)) ?? true })
+ },
+ }),
});
+const CenteredPage: React.FC = ({ children }) => (
+
+);
+
+const ErrorBoundariedApp: React.FC = wrap
+ .ErrorBoundary({ fallback: 문제가 발생했습니다, 새로고침을 해주세요. })
+ .Suspense({ fallback: })
+ .on(() => );
+
createRoot(document.getElementById("root")!).render(
+
-
-
-
+
+
+
+
+
diff --git a/vite.config.ts b/vite.config.ts
index 1fe7815..303a9e2 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,13 +2,14 @@ import mdx from "@mdx-js/rollup";
import react from "@vitejs/plugin-react";
import path from "path";
import { defineConfig } from "vite";
+import mkcert from "vite-plugin-mkcert";
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
- base: "/frontend/",
+ base: "/",
envDir: "./dotenv",
- plugins: [react(), mdx(), svgr()],
+ plugins: [react(), mdx(), mkcert({ hosts: ["local.dev.pycon.kr"] }), svgr()],
resolve: {
alias: {
"@pyconkr-common": path.resolve(__dirname, "./package/pyconkr-common"),