-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#81] 클라이언트 Auth Apis 연동 #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa09d2a
17e4aee
5e949c4
08935fc
52b07f8
e69383b
b5a3c92
dba4df5
94924b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| "use client"; | ||
|
|
||
| import { Icon } from "@compasser/design-system"; | ||
| import { LOGIN_TEXT } from "../_constants/login.constants"; | ||
|
|
||
| interface KakaoLoginButtonProps { | ||
| onClick: () => void; | ||
| } | ||
|
|
||
| export default function KakaoLoginButton({ | ||
| onClick, | ||
| }: KakaoLoginButtonProps) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={onClick} | ||
| aria-label={LOGIN_TEXT.kakaoLoginButton} | ||
| className="relative flex w-full items-center justify-center rounded-[8px] bg-[#FEE500] px-[1.2rem] py-[1rem]" | ||
| > | ||
| <div className="absolute left-[1.4rem] top-1/2 -translate-y-1/2"> | ||
| <Icon name="KakaoLogo" width={23} height={23} ariaHidden={true} /> | ||
| </div> | ||
|
|
||
| <span className="head3-m text-[#191919]"> | ||
| {LOGIN_TEXT.kakaoLoginButton} | ||
| </span> | ||
| </button> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| "use client"; | ||
|
|
||
| import { Input, Button } from "@compasser/design-system"; | ||
| import type { LoginReqDTO } from "@compasser/api"; | ||
| import { LOGIN_TEXT } from "../_constants/login.constants"; | ||
|
|
||
| interface LoginFormProps { | ||
| values: LoginReqDTO; | ||
| isPending: boolean; | ||
| onChangeEmail: (value: string) => void; | ||
| onChangePassword: (value: string) => void; | ||
| onSubmit: () => void; | ||
| } | ||
|
|
||
| export default function LoginForm({ | ||
| values, | ||
| isPending, | ||
| onChangeEmail, | ||
| onChangePassword, | ||
| onSubmit, | ||
| }: LoginFormProps) { | ||
| return ( | ||
| <> | ||
| <div className="w-full"> | ||
| <p className="body2-m pb-[0.2rem] text-default"> | ||
| {LOGIN_TEXT.emailLabel} | ||
| </p> | ||
| <Input | ||
| type="email" | ||
| placeholder={LOGIN_TEXT.emailPlaceholder} | ||
| value={values.email} | ||
| onChange={(e) => onChangeEmail(e.target.value)} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="mt-[2rem] mb-[5.2rem] w-full"> | ||
| <p className="body2-m pb-[0.2rem] text-default"> | ||
| {LOGIN_TEXT.passwordLabel} | ||
| </p> | ||
| <Input | ||
| type="password" | ||
| placeholder={LOGIN_TEXT.passwordPlaceholder} | ||
| value={values.password} | ||
| onChange={(e) => onChangePassword(e.target.value)} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="w-full"> | ||
| <Button | ||
| type="button" | ||
| size="lg" | ||
| kind="default" | ||
| variant="primary" | ||
| onClick={onSubmit} | ||
| disabled={isPending} | ||
| > | ||
| {isPending | ||
| ? LOGIN_TEXT.loginPendingButton | ||
| : LOGIN_TEXT.loginButton} | ||
| </Button> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| "use client"; | ||
|
|
||
| import { LOGIN_TEXT } from "../_constants/login.constants"; | ||
|
|
||
| interface SignupLinkButtonProps { | ||
| onClick: () => void; | ||
| } | ||
|
|
||
| export default function SignupLinkButton({ | ||
| onClick, | ||
| }: SignupLinkButtonProps) { | ||
| return ( | ||
| <div className="mt-[1.6rem] flex justify-center"> | ||
| <button | ||
| type="button" | ||
| onClick={onClick} | ||
| className="body1-m text-gray-700 underline underline-offset-[0.2rem]" | ||
| > | ||
| {LOGIN_TEXT.signupButton} | ||
| </button> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export const LOGIN_TEXT = { | ||
| emailLabel: "이메일", | ||
| emailPlaceholder: "이메일을 입력해주세요", | ||
| passwordLabel: "비밀번호", | ||
| passwordPlaceholder: "비밀번호를 입력해주세요", | ||
| loginButton: "일반 로그인", | ||
| loginPendingButton: "로그인 중...", | ||
| kakaoLoginButton: "카카오 로그인", | ||
| signupButton: "회원가입", | ||
| emptyFieldMessage: "이메일과 비밀번호를 입력해주세요.", | ||
| } as const; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,17 +1,70 @@ | ||||||||||||||||||
| "use client"; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||
| import { useRouter } from "next/navigation"; | ||||||||||||||||||
| import { Input, Button, Icon } from "@compasser/design-system"; | ||||||||||||||||||
| import type { LoginReqDTO } from "@compasser/api"; | ||||||||||||||||||
| import LoginForm from "./_components/LoginForm"; | ||||||||||||||||||
| import KakaoLoginButton from "./_components/KakaoLoginButton"; | ||||||||||||||||||
| import SignupLinkButton from "./_components/SignupLinkButton"; | ||||||||||||||||||
| import { LOGIN_TEXT } from "./_constants/login.constants"; | ||||||||||||||||||
| import { useLoginMutation } from "@/shared/queries/mutation/auth/useLoginMutation"; | ||||||||||||||||||
|
|
||||||||||||||||||
| export default function LoginPage() { | ||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||
| const loginMutation = useLoginMutation(); | ||||||||||||||||||
|
|
||||||||||||||||||
| const [formValues, setFormValues] = useState<LoginReqDTO>({ | ||||||||||||||||||
| email: "", | ||||||||||||||||||
| password: "", | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleMoveRoleSelect = () => { | ||||||||||||||||||
| router.push("/role-select"); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleChangeEmail = (value: string) => { | ||||||||||||||||||
| setFormValues((prev) => ({ | ||||||||||||||||||
| ...prev, | ||||||||||||||||||
| email: value, | ||||||||||||||||||
| })); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleChangePassword = (value: string) => { | ||||||||||||||||||
| setFormValues((prev) => ({ | ||||||||||||||||||
| ...prev, | ||||||||||||||||||
| password: value, | ||||||||||||||||||
| })); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleLogin = () => { | ||||||||||||||||||
| console.log("일반 로그인"); | ||||||||||||||||||
| const email = formValues.email.trim(); | ||||||||||||||||||
| const password = formValues.password.trim(); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!email || !password) { | ||||||||||||||||||
| console.log(LOGIN_TEXT.emptyFieldMessage); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자에게 유효성 검사 실패 피드백 필요
🛠️ 사용자 피드백 추가 제안 if (!email || !password) {
- console.log(LOGIN_TEXT.emptyFieldMessage);
+ // TODO: Toast 또는 alert로 사용자에게 피드백 제공
+ alert(LOGIN_TEXT.emptyFieldMessage);
return;
}🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| loginMutation.mutate( | ||||||||||||||||||
| { | ||||||||||||||||||
| email, | ||||||||||||||||||
| password, | ||||||||||||||||||
| }, | ||||||||||||||||||
| { | ||||||||||||||||||
| onSuccess: (response) => { | ||||||||||||||||||
| const accessToken = response.data.accessToken; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (typeof window !== "undefined") { | ||||||||||||||||||
| localStorage.setItem("accessToken", accessToken); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| router.push("/main"); | ||||||||||||||||||
| }, | ||||||||||||||||||
| onError: (error) => { | ||||||||||||||||||
| console.log("일반 로그인 실패", error); | ||||||||||||||||||
| }, | ||||||||||||||||||
|
Comment on lines
+63
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그인 실패 시 사용자 피드백 필요 로그인 실패 시 🛠️ 에러 피드백 추가 제안 onError: (error) => {
- console.log("일반 로그인 실패", error);
+ console.error("일반 로그인 실패", error);
+ // TODO: 사용자에게 에러 메시지 표시
+ alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.");
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| }, | ||||||||||||||||||
| ); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleKakaoLogin = () => { | ||||||||||||||||||
|
|
@@ -22,57 +75,19 @@ export default function LoginPage() { | |||||||||||||||||
| <main className="flex min-h-screen w-full items-center justify-center px-[1.6rem]"> | ||||||||||||||||||
| <section className="w-full"> | ||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||
| <div className="w-full"> | ||||||||||||||||||
| <p className="body2-m pb-[0.2rem] text-default">이메일</p> | ||||||||||||||||||
| <Input type="email" placeholder="이메일을 입력해주세요" /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| <div className="mt-[2rem] mb-[5.2rem] w-full"> | ||||||||||||||||||
| <p className="body2-m pb-[0.2rem] text-default">비밀번호</p> | ||||||||||||||||||
| <Input type="password" placeholder="비밀번호를 입력해주세요" /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| <div className="w-full"> | ||||||||||||||||||
| <Button | ||||||||||||||||||
| type="button" | ||||||||||||||||||
| size="lg" | ||||||||||||||||||
| kind="default" | ||||||||||||||||||
| variant="primary" | ||||||||||||||||||
| onClick={handleLogin} | ||||||||||||||||||
| > | ||||||||||||||||||
| 일반 로그인 | ||||||||||||||||||
| </Button> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| <LoginForm | ||||||||||||||||||
| values={formValues} | ||||||||||||||||||
| isPending={loginMutation.isPending} | ||||||||||||||||||
| onChangeEmail={handleChangeEmail} | ||||||||||||||||||
| onChangePassword={handleChangePassword} | ||||||||||||||||||
| onSubmit={handleLogin} | ||||||||||||||||||
| /> | ||||||||||||||||||
|
|
||||||||||||||||||
| <div className="mt-[1.2rem] w-full"> | ||||||||||||||||||
| <button | ||||||||||||||||||
| type="button" | ||||||||||||||||||
| onClick={handleKakaoLogin} | ||||||||||||||||||
| aria-label="카카오 로그인" | ||||||||||||||||||
| className="relative flex w-full items-center justify-center rounded-[8px] bg-[#FEE500] px-[1.2rem] py-[1rem]" | ||||||||||||||||||
| > | ||||||||||||||||||
| <div className="absolute left-[1.4rem] top-1/2 -translate-y-1/2"> | ||||||||||||||||||
| <Icon | ||||||||||||||||||
| name="KakaoLogo" | ||||||||||||||||||
| width={23} | ||||||||||||||||||
| height={23} | ||||||||||||||||||
| ariaHidden={true} | ||||||||||||||||||
| /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| <span className="head3-m text-[#191919]">카카오 로그인</span> | ||||||||||||||||||
| </button> | ||||||||||||||||||
| <KakaoLoginButton onClick={handleKakaoLogin} /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| <div className="mt-[1.6rem] flex justify-center"> | ||||||||||||||||||
| <button | ||||||||||||||||||
| type="button" | ||||||||||||||||||
| onClick={handleMoveRoleSelect} | ||||||||||||||||||
| className="body1-m text-gray-700 underline underline-offset-[0.2rem]" | ||||||||||||||||||
| > | ||||||||||||||||||
| 회원가입 | ||||||||||||||||||
| </button> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| <SignupLinkButton onClick={handleMoveRoleSelect} /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </section> | ||||||||||||||||||
| </main> | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||||||
| import { useRouter } from "next/navigation"; | ||||||||||||||||||||||||||||||
| import { Input, Button } from "@compasser/design-system"; | ||||||||||||||||||||||||||||||
| import { useSignupMutation } from "@/shared/queries/mutation/auth/useSignupMutation"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export default function SignupPage() { | ||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||
|
|
@@ -12,29 +13,52 @@ export default function SignupPage() { | |||||||||||||||||||||||||||||
| const [email, setEmail] = useState(""); | ||||||||||||||||||||||||||||||
| const [password, setPassword] = useState(""); | ||||||||||||||||||||||||||||||
| const [passwordConfirm, setPasswordConfirm] = useState(""); | ||||||||||||||||||||||||||||||
| const [submitError, setSubmitError] = useState(""); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { mutateAsync: signUp, isPending } = useSignupMutation(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const isEmailInvalid = | ||||||||||||||||||||||||||||||
| email.length > 0 && !emailRegex.test(email); | ||||||||||||||||||||||||||||||
| const isEmailInvalid = email.length > 0 && !emailRegex.test(email); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const isPasswordMismatch = | ||||||||||||||||||||||||||||||
| password.length > 0 && | ||||||||||||||||||||||||||||||
| passwordConfirm.length > 0 && | ||||||||||||||||||||||||||||||
| password !== passwordConfirm; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const handleSignup = () => { | ||||||||||||||||||||||||||||||
| if (isEmailInvalid || isPasswordMismatch) return; | ||||||||||||||||||||||||||||||
| const isFormInvalid = | ||||||||||||||||||||||||||||||
| !name.trim() || | ||||||||||||||||||||||||||||||
| !nickname.trim() || | ||||||||||||||||||||||||||||||
| !email.trim() || | ||||||||||||||||||||||||||||||
| !password.trim() || | ||||||||||||||||||||||||||||||
| !passwordConfirm.trim() || | ||||||||||||||||||||||||||||||
| isEmailInvalid || | ||||||||||||||||||||||||||||||
| isPasswordMismatch; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const handleSignup = async () => { | ||||||||||||||||||||||||||||||
| if (isFormInvalid || isPending) return; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| setSubmitError(""); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| console.log("회원가입", { | ||||||||||||||||||||||||||||||
| name, | ||||||||||||||||||||||||||||||
| nickname, | ||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||
| password, | ||||||||||||||||||||||||||||||
| passwordConfirm, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| await signUp({ | ||||||||||||||||||||||||||||||
| memberName: name, | ||||||||||||||||||||||||||||||
| nickname, | ||||||||||||||||||||||||||||||
| email, | ||||||||||||||||||||||||||||||
| password, | ||||||||||||||||||||||||||||||
| passwordConfirm, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 검증은 trim인데 전송은 원본 값이라 데이터 불일치가 발생할 수 있습니다 Line 29-36에서 trim 검증을 하므로, Line 44-49 전송값도 동일하게 정규화해 보내는 편이 안전합니다. 수정 제안 await signUp({
- memberName: name,
- nickname,
- email,
+ memberName: name.trim(),
+ nickname: nickname.trim(),
+ email: email.trim(),
password,
passwordConfirm,
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| router.push("/main"); | ||||||||||||||||||||||||||||||
| router.push("/login"); | ||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||
| console.error("회원가입 실패", error); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| setSubmitError( | ||||||||||||||||||||||||||||||
| error instanceof Error | ||||||||||||||||||||||||||||||
| ? error.message | ||||||||||||||||||||||||||||||
| : "회원가입 중 오류가 발생했습니다.", | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 원본 에러 메시지 직접 노출은 피하는 것이 좋습니다 Line 56-60에서 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
|
|
@@ -96,6 +120,10 @@ export default function SignupPage() { | |||||||||||||||||||||||||||||
| errorMessage="비밀번호가 일치하지 않습니다." | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {submitError ? ( | ||||||||||||||||||||||||||||||
| <p className="body3-m text-red-500">{submitError}</p> | ||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -106,8 +134,9 @@ export default function SignupPage() { | |||||||||||||||||||||||||||||
| kind="default" | ||||||||||||||||||||||||||||||
| variant="primary" | ||||||||||||||||||||||||||||||
| onClick={handleSignup} | ||||||||||||||||||||||||||||||
| disabled={isFormInvalid || isPending} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| 회원가입 | ||||||||||||||||||||||||||||||
| {isPending ? "회원가입 중..." : "회원가입"} | ||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: CUK-Compasser/FE
Length of output: 3382
🏁 Script executed:
Repository: CUK-Compasser/FE
Length of output: 2103
🏁 Script executed:
cat -n apps/customer/src/app/login/page.tsx | head -100Repository: CUK-Compasser/FE
Length of output: 3148
🏁 Script executed:
rg -n 'form|Form' apps/customer/src/app/login/_components/LoginForm.tsxRepository: CUK-Compasser/FE
Length of output: 151
LoginForm이 Enter 키 제출을 지원하지 않습니다
현재 구현에서 LoginForm은
<form>요소로 감싸져 있지 않고, Button의type="button"과onClick={onSubmit}조합으로만 제출을 처리합니다. 따라서 비밀번호 입력 필드에서 Enter 키를 눌러도 폼이 제출되지 않으며, 오직 버튼 클릭으로만 로그인이 가능합니다.표준 HTML 폼 동작에 대한 사용자 기대치를 벗어나므로 다음 중 하나로 개선이 필요합니다:
<form>요소로 inputs을 감싸고 Button의type="submit"으로 변경🤖 Prompt for AI Agents