Skip to content

Commit 1ee8b48

Browse files
committed
feat: 결제 시 고객 정보를 받는 dialog 추가
1 parent 8d1dcf0 commit 1ee8b48

File tree

7 files changed

+242
-70
lines changed

7 files changed

+242
-70
lines changed

packages/shop/src/apis/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ namespace ShopAPIs {
109109
* 고객의 장바구니에 담긴 전체 상품 결제를 PortOne에 등록합니다.
110110
* @returns PortOne에 등록된 주문 정보
111111
*/
112-
export const prepareCartOrder = (client: ShopAPIClient) => () =>
113-
client.post<ShopSchemas.Order, undefined>("v1/orders/", undefined);
112+
export const prepareCartOrder = (client: ShopAPIClient) => (data: ShopSchemas.CustomerInfo) =>
113+
client.post<ShopSchemas.Order, ShopSchemas.CustomerInfo>("v1/orders/", data);
114114

115115
/**
116116
* 고객의 모든 결제 내역을 가져옵니다.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as Common from "@frontend/common";
2+
import {
3+
Button,
4+
CircularProgress,
5+
Dialog,
6+
DialogActions,
7+
DialogContent,
8+
DialogTitle,
9+
Stack,
10+
TextField,
11+
} from "@mui/material";
12+
import { Suspense } from "@suspensive/react";
13+
import * as React from "react";
14+
15+
import ShopHooks from "../../hooks";
16+
import ShopSchemas from "../../schemas";
17+
18+
const PHONE_REGEX = new RegExp(/^(010-\d{4}-\d{4}|(\+82|0)10\d{3,4}\d{4})$/, "g").source;
19+
20+
type CustomerInfoFormDialogPropsType = {
21+
open: boolean;
22+
closeFunc: () => void;
23+
onSubmit?: (formData: ShopSchemas.CustomerInfo) => void;
24+
defaultValue?: ShopSchemas.CustomerInfo | null;
25+
};
26+
27+
export const CustomerInfoFormDialog: React.FC<CustomerInfoFormDialogPropsType> = Suspense.with(
28+
{ fallback: <CircularProgress /> },
29+
({ open, closeFunc, onSubmit, defaultValue }) => {
30+
const formRef = React.useRef<HTMLFormElement | null>(null);
31+
32+
const { language } = ShopHooks.useShopContext();
33+
const shopAPIClient = ShopHooks.useShopClient();
34+
const { data: userInfo } = ShopHooks.useUserStatus(shopAPIClient);
35+
36+
if (!userInfo) {
37+
closeFunc();
38+
return;
39+
}
40+
41+
const onSubmitFunc: React.MouseEventHandler<HTMLButtonElement> = (e) => {
42+
e.preventDefault();
43+
e.stopPropagation();
44+
if (Common.Utils.isFormValid(formRef?.current))
45+
onSubmit?.(Common.Utils.getFormValue<ShopSchemas.CustomerInfo>({ form: formRef.current }));
46+
};
47+
48+
const titleStr = language === "ko" ? "고객 정보 입력" : "Customer Information";
49+
const cancelButtonText = language === "ko" ? "취소" : "Cancel";
50+
const submitButtonText = language === "ko" ? "결제" : "Proceed to Payment";
51+
const nameLabelStr = language === "ko" ? "성명" : "Name";
52+
const organizationLabelStr = language === "ko" ? "소속" : "Organization";
53+
const emailLabelStr = language === "ko" ? "이메일 주소" : "Email Address";
54+
const phoneLabelStr =
55+
language === "ko"
56+
? "전화번호 (예: 010-1234-5678 또는 +821012345678)"
57+
: "Phone Number (e.g., 010-1234-5678 or +821012345678)";
58+
const phoneValidationFailedStr =
59+
language === "ko"
60+
? "전화번호 형식이 올바르지 않습니다. 예: 010-1234-5678 또는 +821012345678"
61+
: "Invalid phone number format. e.g., 010-1234-5678 or +821012345678";
62+
63+
return (
64+
<Dialog open={open} onClose={closeFunc} fullWidth maxWidth="sm">
65+
<DialogTitle>{titleStr}</DialogTitle>
66+
<DialogContent>
67+
<form ref={formRef}>
68+
<Stack spacing={2}>
69+
<TextField name="name" label={nameLabelStr} defaultValue={defaultValue?.name} required fullWidth />
70+
<TextField
71+
name="organization"
72+
label={organizationLabelStr}
73+
defaultValue={defaultValue?.organization}
74+
fullWidth
75+
/>
76+
<TextField
77+
name="email"
78+
label={emailLabelStr}
79+
defaultValue={defaultValue?.email || userInfo.data.user.email}
80+
type="email"
81+
required
82+
fullWidth
83+
/>
84+
<TextField
85+
name="phone"
86+
label={phoneLabelStr}
87+
defaultValue={defaultValue?.phone}
88+
slotProps={{ htmlInput: { pattern: PHONE_REGEX, title: phoneValidationFailedStr } }}
89+
fullWidth
90+
required
91+
/>
92+
</Stack>
93+
</form>
94+
</DialogContent>
95+
<DialogActions>
96+
<Button onClick={closeFunc} color="error" children={cancelButtonText} />
97+
<Button onClick={onSubmitFunc} children={submitButtonText} />
98+
</DialogActions>
99+
</Dialog>
100+
);
101+
}
102+
);

packages/shop/src/components/common/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CustomerInfoFormDialog as CustomerInfoFormDialog_ } from "./customer_info_form_dialog";
12
import {
23
OptionGroupInput as OptionGroupInput_,
34
OrderProductRelationOptionInput as OrderProductRelationOptionInput_,
@@ -12,6 +13,7 @@ namespace CommonComponents {
1213
export const OrderProductRelationOptionInput = OrderProductRelationOptionInput_;
1314
export const PriceDisplay = PriceDisplay_;
1415
export const SignInGuard = SignInGuard_;
16+
export const CustomerInfoFormDialog = CustomerInfoFormDialog_;
1517
}
1618

1719
export default CommonComponents;

packages/shop/src/components/features/cart.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
AccordionActions,
55
AccordionDetails,
66
AccordionSummary,
7+
Backdrop,
78
Button,
89
CircularProgress,
910
Divider,
@@ -71,14 +72,23 @@ const CartItem: React.FC<{
7172
);
7273
};
7374

75+
type CartStatusStateType = {
76+
openDialog: boolean;
77+
openBackdrop: boolean;
78+
};
79+
7480
export const CartStatus: React.FC<{ onPaymentCompleted?: () => void }> = Suspense.with(
7581
{ fallback: <CircularProgress /> },
7682
({ onPaymentCompleted }) => {
7783
const queryClient = useQueryClient();
78-
const { language } = ShopHooks.useShopContext();
84+
const { language, shopImpAccountId } = ShopHooks.useShopContext();
7985
const shopAPIClient = ShopHooks.useShopClient();
8086
const cartOrderStartMutation = ShopHooks.usePrepareCartOrderMutation(shopAPIClient);
8187
const removeItemFromCartMutation = ShopHooks.useRemoveItemFromCartMutation(shopAPIClient);
88+
const [state, setState] = React.useState<CartStatusStateType>({
89+
openDialog: false,
90+
openBackdrop: false,
91+
});
8292

8393
const cartIsEmptyStr = language === "ko" ? "장바구니가 비어있어요!" : "Your cart is empty!";
8494
const totalPriceStr = language === "ko" ? "총 결제 금액" : "Total Payment Amount";
@@ -97,22 +107,32 @@ export const CartStatus: React.FC<{ onPaymentCompleted?: () => void }> = Suspens
97107
: "Failed to complete the cart order.\nPlease try again later.\n";
98108

99109
const removeItemFromCart = (cartProductId: string) => removeItemFromCartMutation.mutate({ cartProductId });
100-
const startCartOrder = () =>
101-
cartOrderStartMutation.mutate(undefined, {
110+
111+
const openDialog = () => setState((ps) => ({ ...ps, openDialog: true }));
112+
const closeDialog = () => setState((ps) => ({ ...ps, openDialog: false }));
113+
const openBackdrop = () => setState((ps) => ({ ...ps, openBackdrop: true }));
114+
const closeBackdrop = () => setState((ps) => ({ ...ps, openBackdrop: false }));
115+
116+
const onFormSubmit = (formData: ShopSchemas.CustomerInfo) => {
117+
closeDialog();
118+
openBackdrop();
119+
cartOrderStartMutation.mutate(formData, {
102120
onSuccess: (order: ShopSchemas.Order) => {
103121
ShopUtils.startPortOnePurchase(
122+
shopImpAccountId,
104123
order,
105124
() => {
106125
queryClient.invalidateQueries();
107126
queryClient.resetQueries();
108127
onPaymentCompleted?.();
109128
},
110129
(response) => alert(failedToOrderStr + response.error_msg),
111-
() => {}
130+
closeBackdrop
112131
);
113132
},
114133
onError: (error) => alert(error.message || errorWhilePreparingOrderStr),
115134
});
135+
};
116136

117137
const disabled = removeItemFromCartMutation.isPending || cartOrderStartMutation.isPending;
118138

@@ -125,6 +145,17 @@ export const CartStatus: React.FC<{ onPaymentCompleted?: () => void }> = Suspens
125145
</Typography>
126146
) : (
127147
<>
148+
<Backdrop
149+
sx={(theme) => ({ zIndex: theme.zIndex.drawer + 1 })}
150+
open={state.openBackdrop}
151+
onClick={() => {}}
152+
/>
153+
<CommonComponents.CustomerInfoFormDialog
154+
open={state.openDialog}
155+
closeFunc={closeDialog}
156+
onSubmit={onFormSubmit}
157+
defaultValue={data.customer_info}
158+
/>
128159
{data.products.map((prodRel) => (
129160
<CartItem
130161
language={language}
@@ -139,7 +170,7 @@ export const CartStatus: React.FC<{ onPaymentCompleted?: () => void }> = Suspens
139170
<Typography variant="h6" sx={{ textAlign: "end" }}>
140171
{totalPriceStr}: <CommonComponents.PriceDisplay price={data.first_paid_price} />
141172
</Typography>
142-
<Button variant="contained" color="secondary" onClick={startCartOrder} disabled={disabled}>
173+
<Button variant="contained" color="secondary" onClick={openDialog} disabled={disabled}>
143174
{orderCartStr}
144175
</Button>
145176
</>

0 commit comments

Comments
 (0)