Skip to content

Commit 5736c9e

Browse files
committed
feat: 로그인 버튼 구현
1 parent a4cb73b commit 5736c9e

File tree

7 files changed

+173
-16
lines changed

7 files changed

+173
-16
lines changed

apps/pyconkr/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as R from "remeda";
55

66
import MainLayout from "./components/layout/index.tsx";
77
import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx";
8+
import { ShopSignInPage } from "./components/pages/sign_in.tsx";
89
import { Test } from "./components/pages/test.tsx";
910
import { IS_DEBUG_ENV } from "./consts";
1011
import { useAppContext } from "./contexts/app_context";
@@ -40,6 +41,7 @@ export const App: React.FC = () => {
4041
<Routes>
4142
<Route element={<MainLayout />}>
4243
{IS_DEBUG_ENV && <Route path="/debug" element={<Test />} />}
44+
<Route path="/account/sign-in" element={<ShopSignInPage />} />
4345
<Route path="/pages/:id" element={<PageIdParamRenderer />} />
4446
<Route path="*" element={<RouteRenderer />} />
4547
</Route>

apps/pyconkr/src/components/layout/Header/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as R from "remeda";
99
import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI";
1010
import { useAppContext } from "../../../contexts/app_context";
1111
import LanguageSelector from "../LanguageSelector";
12-
import LoginButton from "../LoginButton";
12+
import { SignInButton } from "../SignInButton";
1313

1414
type MenuType = BackendAPISchemas.NestedSiteMapSchema;
1515
type MenuOrUndefinedType = MenuType | undefined;
@@ -129,7 +129,7 @@ const Header: React.FC = () => {
129129
<NavSideElementContainer>
130130
<Stack direction="row" alignItems="center" gap={1} sx={{ marginLeft: "auto" }}>
131131
<LanguageSelector />
132-
<LoginButton />
132+
<SignInButton />
133133
</Stack>
134134
</NavSideElementContainer>
135135
</HeaderContainer>

apps/pyconkr/src/components/layout/LoginButton/index.tsx

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as Shop from "@frontend/shop";
2+
import { Button } from "@mui/material";
3+
import { ErrorBoundary, Suspense } from "@suspensive/react";
4+
import { useNavigate } from "react-router-dom";
5+
6+
import { useAppContext } from "../../../contexts/app_context";
7+
8+
type InnerSignInButtonImplPropType = {
9+
loading?: boolean;
10+
signedIn?: boolean;
11+
onSignOut?: () => void;
12+
};
13+
14+
const InnerSignInButtonImpl: React.FC<InnerSignInButtonImplPropType> = ({ loading, signedIn, onSignOut }) => {
15+
const navigate = useNavigate();
16+
const { language } = useAppContext();
17+
18+
const signInBtnStr = language === "ko" ? "로그인" : "Sign In";
19+
const signOutBtnStr = language === "ko" ? "로그아웃" : "Sign Out";
20+
21+
return (
22+
<Button
23+
variant="text"
24+
sx={({ palette }) => ({ color: palette.primary.dark })}
25+
loading={loading}
26+
onClick={() => (signedIn ? onSignOut?.() : navigate("/account/sign-in"))}
27+
children={signedIn ? signOutBtnStr : signInBtnStr}
28+
/>
29+
);
30+
};
31+
32+
export const SignInButton: React.FC = ErrorBoundary.with(
33+
{ fallback: <InnerSignInButtonImpl /> },
34+
Suspense.with({ fallback: <InnerSignInButtonImpl loading /> }, () => {
35+
const shopAPIClient = Shop.Hooks.useShopClient();
36+
const signOutMutation = Shop.Hooks.useSignOutMutation(shopAPIClient);
37+
const { data } = Shop.Hooks.useUserStatus(shopAPIClient);
38+
39+
return <InnerSignInButtonImpl signedIn={data !== null} onSignOut={signOutMutation.mutate} />;
40+
})
41+
);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as Shop from "@frontend/shop";
2+
import { AccountCircleOutlined, Google } from "@mui/icons-material";
3+
import { Backdrop, Button, ButtonProps, CircularProgress, Stack, styled, Typography } from "@mui/material";
4+
import { Suspense } from "@suspensive/react";
5+
import { enqueueSnackbar, OptionsObject } from "notistack";
6+
import * as React from "react";
7+
import { useNavigate } from "react-router-dom";
8+
9+
import { useAppContext } from "../../contexts/app_context";
10+
11+
const SignInPageContainer = styled(Stack)(({ theme }) => ({
12+
height: "75%",
13+
width: "100%",
14+
maxWidth: "1200px",
15+
16+
justifyContent: "flex-start",
17+
alignItems: "center",
18+
19+
paddingTop: theme.spacing(8),
20+
paddingBottom: theme.spacing(8),
21+
22+
paddingRight: theme.spacing(16),
23+
paddingLeft: theme.spacing(16),
24+
25+
[theme.breakpoints.down("lg")]: {
26+
padding: theme.spacing(4),
27+
},
28+
[theme.breakpoints.down("sm")]: {
29+
padding: theme.spacing(2),
30+
},
31+
}));
32+
33+
type PageeStateType = {
34+
openBackdrop: boolean;
35+
};
36+
37+
export const ShopSignInPage: React.FC = Suspense.with({ fallback: <CircularProgress /> }, () => {
38+
const { setAppContext, language } = useAppContext();
39+
const [state, setState] = React.useState<PageeStateType>({ openBackdrop: false });
40+
const navigate = useNavigate();
41+
const shopAPIClient = Shop.Hooks.useShopClient();
42+
const SignInMutation = Shop.Hooks.useSignInWithSNSMutation(shopAPIClient);
43+
const { data } = Shop.Hooks.useUserStatus(shopAPIClient);
44+
45+
const shouldOpenBackdrop = SignInMutation.isPending || state.openBackdrop;
46+
47+
const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) =>
48+
enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } });
49+
50+
const triggerSignIn = (provider: "google" | "kakao" | "naver") => {
51+
setState((ps) => ({ ...ps, openBackdrop: true }));
52+
SignInMutation.mutate({ provider, callback_url: window.location.origin });
53+
};
54+
const signInWithGoogle = () => triggerSignIn("google");
55+
const signInWithKakao = () => triggerSignIn("kakao");
56+
const signInWithNaver = () => triggerSignIn("naver");
57+
58+
const signInTitleStr = language === "ko" ? "로그인" : "Sign In";
59+
const signInWithGoogleStr = language === "ko" ? "구글로 로그인" : "Sign In with Google";
60+
const signInWithKakaoStr = language === "ko" ? "카카오로 로그인" : "Sign In with Kakao";
61+
const signInWithNaverStr = language === "ko" ? "네이버로 로그인" : "Sign In with Naver";
62+
63+
React.useEffect(() => {
64+
if (data && data.meta.is_authenticated) {
65+
addSnackbar(
66+
language === "ko"
67+
? `이미 ${data.data.user.username}님으로 로그인되어 있습니다!`
68+
: `You are already signed in as ${data.data.user.username}!`,
69+
"success"
70+
);
71+
navigate("/");
72+
return;
73+
}
74+
75+
setAppContext((prev) => ({
76+
...prev,
77+
title: signInTitleStr,
78+
shouldShowTitleBanner: true,
79+
shouldShowSponsorBanner: false,
80+
}));
81+
}, [signInTitleStr]);
82+
83+
const commonBtnProps: ButtonProps = {
84+
variant: "contained",
85+
fullWidth: true,
86+
size: "large",
87+
disabled: SignInMutation.isPending,
88+
};
89+
const commonBtnSxProps: ButtonProps["sx"] = {
90+
textTransform: "none",
91+
};
92+
const btnProps: ButtonProps[] = [
93+
{
94+
children: signInWithGoogleStr,
95+
onClick: signInWithGoogle,
96+
startIcon: <Google />,
97+
sx: { ...commonBtnSxProps, backgroundColor: "#4285F4", color: "#fff" },
98+
},
99+
{
100+
children: signInWithNaverStr,
101+
onClick: signInWithNaver,
102+
startIcon: <AccountCircleOutlined />,
103+
sx: { ...commonBtnSxProps, backgroundColor: "#03C75A", color: "#fff" },
104+
},
105+
{
106+
children: signInWithKakaoStr,
107+
onClick: signInWithKakao,
108+
startIcon: <AccountCircleOutlined />,
109+
sx: { ...commonBtnSxProps, backgroundColor: "#FEE500", color: "#000" },
110+
},
111+
];
112+
113+
return (
114+
<>
115+
<SignInPageContainer spacing={6}>
116+
<Typography variant="h4" sx={{ textAlign: "center", fontWeight: "bolder" }} children={signInTitleStr} />
117+
<Stack spacing={1} sx={{ width: "100%", maxWidth: "400px" }}>
118+
{btnProps.map((props, index) => (
119+
<Button key={index} {...commonBtnProps} {...props} />
120+
))}
121+
</Stack>
122+
</SignInPageContainer>
123+
<Backdrop sx={({ zIndex }) => ({ zIndex: zIndex.drawer + 1 })} open={shouldOpenBackdrop} onClick={() => {}} />
124+
</>
125+
);
126+
});

packages/shop/src/apis/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ namespace ShopAPIs {
4545
*/
4646
export const signOut = (client: ShopAPIClient) => async () => {
4747
try {
48-
await client.delete<ShopSchemas.UserSignedInStatus>("authn/social/browser/v1/auth/session");
48+
await client.delete<void>("authn/social/browser/v1/auth/session");
4949
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5050
} catch (_) {
5151
return Promise.resolve({});

packages/shop/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ namespace ShopHooks {
6565
useMutation({
6666
mutationKey: MUTATION_KEYS.USER_SIGN_OUT,
6767
mutationFn: ShopAPIs.signOut(client),
68+
retry: 0,
6869
meta: { invalidates: [QUERY_KEYS.BASE] },
6970
});
7071

0 commit comments

Comments
 (0)