From f0c77388bf94bd04d8dbc88dc27c4706f1278841 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:09:49 +0900 Subject: [PATCH 01/12] =?UTF-8?q?fix:=20Shop=20API=EC=9D=98=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20=EB=B0=8F=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index 1458645..aebd71f 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 ); From 97be60a92e136adde042a1f3f0d55782b802ceab Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:12:05 +0900 Subject: [PATCH 02/12] =?UTF-8?q?add:=20=EA=B2=B0=EC=A0=9C=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=EB=90=9C=20=EC=A3=BC=EB=AC=B8=EC=9D=98=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95=20API=20=EB=B0=8F=20hook=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/apis/index.ts | 14 ++++++++++++++ package/pyconkr-shop/hooks/index.ts | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/package/pyconkr-shop/apis/index.ts b/package/pyconkr-shop/apis/index.ts index aebd71f..eb4e0c8 100644 --- a/package/pyconkr-shop/apis/index.ts +++ b/package/pyconkr-shop/apis/index.ts @@ -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/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; From 857eaf81efbefe9d86b90b1cfa67acdc24dad97c Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:12:17 +0900 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20Shop=20API=20Schema=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/schemas/index.ts | 68 ++++++++++++++++++++------- 1 file changed, 50 insertions(+), 18 deletions(-) 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 = ( From 0e02f9fb297e96c3aeb4403c4b14104228bb94c7 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:12:34 +0900 Subject: [PATCH 04/12] =?UTF-8?q?chore:=20ShopAPIUtil=20namespace=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/utils/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 package/pyconkr-shop/utils/index.ts 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; From 5652173d0d57b2791db14826826bd8cbda6d9e45 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:12:54 +0900 Subject: [PATCH 05/12] =?UTF-8?q?chore:=20PriceDisplay=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{pyconkr-common => pyconkr-shop}/components/price_display.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename package/{pyconkr-common => pyconkr-shop}/components/price_display.tsx (100%) 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 From fefaa6e7c31f02973a1f08018d2992be6780982c Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:13:19 +0900 Subject: [PATCH 06/12] =?UTF-8?q?add:=20SignInGuard=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EC=98=B5=EC=85=98=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/option_group_input.tsx | 121 ++++++++++++++++++ .../pyconkr-shop/components/signin_guard.tsx | 11 ++ 2 files changed, 132 insertions(+) create mode 100644 package/pyconkr-shop/components/option_group_input.tsx create mode 100644 package/pyconkr-shop/components/signin_guard.tsx 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 + {option.name} + {option.additional_price > 0 && (<> [ + ])} + {isOptionOutOfStock && <> (품절)} + + } + ) + + 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-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; +}; From 4ccdcae0c8c4cef6fd99ef79f7ae8d7b874bf829 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:13:40 +0900 Subject: [PATCH 07/12] =?UTF-8?q?chore:=20ShopComponent=20namespace=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pyconkr-shop/components/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 package/pyconkr-shop/components/index.ts 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; From ea314927e82240fa89f8a558a72fc9d5dd9666cb Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:14:56 +0900 Subject: [PATCH 08/12] =?UTF-8?q?add:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=95=84=EC=88=98=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC(mdx-icon)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +++- pnpm-lock.yaml | 56 +++++++++++++++++++++++++++++++++++++++++---- pnpm-workspace.yaml | 2 ++ 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index 489cc59..3d51ae2 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", @@ -54,7 +55,8 @@ "typescript": "~5.7.2", "typescript-eslint": "^8.24.1", "vite": "^6.2.0", - "vite-plugin-mdx": "^3.6.1" + "vite-plugin-mdx": "^3.6.1", + "vite-plugin-mkcert": "^1.17.8" }, "packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1abe734..bbfa3ce 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,7 +46,7 @@ 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) @@ -126,6 +129,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.14.1)) + vite-plugin-mkcert: + specifier: ^1.17.8 + version: 1.17.8(vite@6.2.6(@types/node@22.14.1)) packages: @@ -208,6 +214,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'} @@ -521,6 +531,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'} @@ -2514,6 +2535,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@6.2.6: resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2693,6 +2720,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 @@ -3001,6 +3030,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 @@ -3431,9 +3468,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: @@ -4086,7 +4123,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: @@ -5460,6 +5499,15 @@ snapshots: unified: 9.2.2 vite: 6.2.6(@types/node@22.14.1) + vite-plugin-mkcert@1.17.8(vite@6.2.6(@types/node@22.14.1)): + dependencies: + axios: 1.8.4(debug@4.4.0) + debug: 4.4.0 + picocolors: 1.1.1 + vite: 6.2.6(@types/node@22.14.1) + transitivePeerDependencies: + - supports-color + vite@6.2.6(@types/node@22.14.1): dependencies: esbuild: 0.25.2 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 From 7d60b331d0991e198f152b337a3ae9b07130df3d Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:16:02 +0900 Subject: [PATCH 09/12] =?UTF-8?q?chore:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 44 +++++++++++++-- public/favicon-180.png | Bin 0 -> 3871 bytes public/favicon-192.png | Bin 0 -> 4371 bytes public/favicon-512.png | Bin 0 -> 15538 bytes public/favicon.ico | Bin 0 -> 1380 bytes public/favicon.svg | 1 + public/site.webmanifest | 25 +++++++++ public/vite.svg | 1 - src/App.css | 42 --------------- src/App.tsx | 21 ++++++-- .../Test.tsx => debug/page/mdi_test.tsx} | 15 +++--- src/index.css | 0 src/main.tsx | 51 +++++++++++++++--- 13 files changed, 135 insertions(+), 65 deletions(-) create mode 100755 public/favicon-180.png create mode 100755 public/favicon-192.png create mode 100755 public/favicon-512.png create mode 100755 public/favicon.ico create mode 100755 public/favicon.svg create mode 100755 public/site.webmanifest delete mode 100644 public/vite.svg delete mode 100644 src/App.css rename src/{components/Test.tsx => debug/page/mdi_test.tsx} (96%) delete mode 100644 src/index.css 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/public/favicon-180.png b/public/favicon-180.png new file mode 100755 index 0000000000000000000000000000000000000000..232c395e2fba3f13fa964e4248ec53d75a3a3558 GIT binary patch literal 3871 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 RXQL14t`%u)G_&P$(_BY2n8{-H^ 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 \ 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 b7ec56c..b77982b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,18 @@ -import Test from "./components/Test"; +import React from "react"; -function App() { - return ; -} +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 default App; +export const App: 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/index.css b/src/index.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.tsx b/src/main.tsx index e6a6703..0f06805 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,22 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ThemeProvider, CssBaseline } from "@mui/material"; + +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 { theme } from "./theme"; -import "./index.css"; -import App from "./App.tsx"; + +declare module '@tanstack/react-query' { + interface Register { + mutationMeta: { + invalidates?: string[][]; + } + } +} const queryClient = new QueryClient({ defaultOptions: { @@ -14,14 +26,41 @@ 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( + - - + + + + From d292ddf9035d929a25207146df08c7941c5b742c Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:16:29 +0900 Subject: [PATCH 10/12] =?UTF-8?q?add:=20ShopAPI=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=95=EC=9D=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dotenv/.env.development | 1 + dotenv/.env.production | 1 + vite.config.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) 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/vite.config.ts b/vite.config.ts index 609fe35..5a6c183 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,12 +2,13 @@ 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'; // https://vite.dev/config/ export default defineConfig({ base: "/frontend/", envDir: "./dotenv", - plugins: [react(), mdx()], + plugins: [react(), mdx(), mkcert({ hosts: ["local.dev.pycon.kr"] })], resolve: { alias: { '@pyconkr-common': path.resolve(__dirname, './package/pyconkr-common'), From 856a3ee52e89757d368ade86aed501a4384f7bc3 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 12 May 2025 09:16:56 +0900 Subject: [PATCH 11/12] =?UTF-8?q?add:=20Shop=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/debug/page/shop_component/cart.tsx | 102 ++++++++++++ src/debug/page/shop_component/order.tsx | 137 +++++++++++++++ src/debug/page/shop_component/product.tsx | 156 ++++++++++++++++++ src/debug/page/shop_component/user_status.tsx | 58 +++++++ src/debug/page/shop_test.tsx | 21 +++ 5 files changed, 474 insertions(+) create mode 100644 src/debug/page/shop_component/cart.tsx create mode 100644 src/debug/page/shop_component/order.tsx create mode 100644 src/debug/page/shop_component/product.tsx create mode 100644 src/debug/page/shop_component/user_status.tsx create mode 100644 src/debug/page/shop_test.tsx 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} + +
{ e.preventDefault(); patchOneItemOptions(); }}> + + { + prodRels.options.map( + (optionRel) => + ) + } + +
+
+ + + + +
+ } + ) + } +
+ +
+ + + + +
+} + +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) + ? <> +
+
+ + {product.option_groups.map((group) => )} + +
+
+ +
+ 결제 금액: + + : {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 + + + + + + + + + From f278ee7aef8f331bc6cd744756d35c867fb1e4c1 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Tue, 13 May 2025 08:04:17 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20base=20path=EB=A5=BC=20root?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index d39e53a..303a9e2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ - base: "/frontend/", + base: "/", envDir: "./dotenv", plugins: [react(), mdx(), mkcert({ hosts: ["local.dev.pycon.kr"] }), svgr()], resolve: {