Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/customer/src/app/login/_components/KakaoLoginButton.tsx
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>
);
}
64 changes: 64 additions & 0 deletions apps/customer/src/app/login/_components/LoginForm.tsx
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}
Comment on lines +49 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# LoginForm 사용처에서 form submit 연결 여부 확인
rg -n -C3 'LoginForm' apps/customer/src/app/login
rg -n -C3 '<form|onSubmit=|type="submit"' apps/customer/src/app/login

Repository: CUK-Compasser/FE

Length of output: 3382


🏁 Script executed:

cat -n apps/customer/src/app/login/_components/LoginForm.tsx

Repository: CUK-Compasser/FE

Length of output: 2103


🏁 Script executed:

cat -n apps/customer/src/app/login/page.tsx | head -100

Repository: CUK-Compasser/FE

Length of output: 3148


🏁 Script executed:

rg -n 'form|Form' apps/customer/src/app/login/_components/LoginForm.tsx

Repository: CUK-Compasser/FE

Length of output: 151


LoginForm이 Enter 키 제출을 지원하지 않습니다

현재 구현에서 LoginForm은 <form> 요소로 감싸져 있지 않고, Button의 type="button"onClick={onSubmit} 조합으로만 제출을 처리합니다. 따라서 비밀번호 입력 필드에서 Enter 키를 눌러도 폼이 제출되지 않으며, 오직 버튼 클릭으로만 로그인이 가능합니다.

표준 HTML 폼 동작에 대한 사용자 기대치를 벗어나므로 다음 중 하나로 개선이 필요합니다:

  • <form> 요소로 inputs을 감싸고 Button의 type="submit"으로 변경
  • 또는 Input 요소들에 Enter 키 이벤트 핸들러(onKeyDown) 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/_components/LoginForm.tsx` around lines 49 - 55,
The LoginForm currently handles submission via Button type="button" and
onClick={onSubmit}, so pressing Enter in inputs won't submit; modify the
LoginForm component to wrap the username/password inputs in a <form> and move
the submission handler to the form (e.g., onSubmit={onSubmit}), then change the
Button in this diff from type="button" to type="submit" (keep
disabled={isPending}); alternatively you may add an onKeyDown Enter handler on
the input elements, but prefer the standard form approach so the existing
onSubmit handler is used by the form submission.

>
{isPending
? LOGIN_TEXT.loginPendingButton
: LOGIN_TEXT.loginButton}
</Button>
</div>
</>
);
}
23 changes: 23 additions & 0 deletions apps/customer/src/app/login/_components/SignupLinkButton.tsx
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>
);
}
11 changes: 11 additions & 0 deletions apps/customer/src/app/login/_constants/login.constants.ts
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;
113 changes: 64 additions & 49 deletions apps/customer/src/app/login/page.tsx
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

사용자에게 유효성 검사 실패 피드백 필요

console.log는 개발 중에만 확인 가능하며, 실제 사용자는 이메일/비밀번호가 비어있을 때 아무런 피드백을 받지 못합니다. Toast, alert, 또는 inline error message를 통해 사용자에게 알려야 합니다.

🛠️ 사용자 피드백 추가 제안
    if (!email || !password) {
-     console.log(LOGIN_TEXT.emptyFieldMessage);
+     // TODO: Toast 또는 alert로 사용자에게 피드백 제공
+     alert(LOGIN_TEXT.emptyFieldMessage);
      return;
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/page.tsx` around lines 43 - 46, Replace the
console.log in the empty-field check with a user-facing notification so users
get immediate feedback: where the code checks if (!email || !password) and
currently calls console.log(LOGIN_TEXT.emptyFieldMessage) (likely inside your
submit/handleLogin flow in page.tsx), call the app's UI feedback mechanism
instead (e.g., show a Toast, alert, or set an inline error state/variable bound
to the form) using LOGIN_TEXT.emptyFieldMessage so the message is visible to
users and prevent submission by returning as before.


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

로그인 실패 시 사용자 피드백 필요

로그인 실패 시 console.log만 호출되어 사용자는 실패 원인을 알 수 없습니다. 잘못된 자격 증명, 네트워크 오류 등 상황에 맞는 에러 메시지를 사용자에게 표시해야 합니다.

🛠️ 에러 피드백 추가 제안
        onError: (error) => {
-         console.log("일반 로그인 실패", error);
+         console.error("일반 로그인 실패", error);
+         // TODO: 사용자에게 에러 메시지 표시
+         alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.");
        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onError: (error) => {
console.log("일반 로그인 실패", error);
},
onError: (error) => {
console.error("일반 로그인 실패", error);
// TODO: 사용자에게 에러 메시지 표시
alert("로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.");
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/login/page.tsx` around lines 63 - 65, The onError
handler in apps/customer/src/app/login/page.tsx currently only console.logs the
error; replace that with user-facing feedback by updating the onError: (error)
=> { ... } handler to surface a clear message (e.g., show a toast or set a local
error state used by the UI) and map common error cases (invalid credentials vs
network/timeout vs unknown) to friendly messages; use the existing
toast/toastHook or a setState like loginError and ensure the message includes
error.message when appropriate and is cleared on retry or success.

},
);
};

const handleKakaoLogin = () => {
Expand All @@ -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>
Expand Down
55 changes: 42 additions & 13 deletions apps/customer/src/app/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

검증은 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await signUp({
memberName: name,
nickname,
email,
password,
passwordConfirm,
});
await signUp({
memberName: name.trim(),
nickname: nickname.trim(),
email: email.trim(),
password,
passwordConfirm,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/signup/page.tsx` around lines 44 - 50, Validation trims
the inputs but the signUp call sends original values, causing possible data
mismatch; update the signUp invocation (the call to signUp in page.tsx) to send
the normalized/trimmed versions of the fields (e.g., use trimmedName,
trimmedNickname, trimmedEmail or call .trim() on name, nickname, email before
passing) so the transmitted payload matches the earlier trim-based validation.
Ensure the same normalization is applied to all fields validated
(memberName/name, nickname, email, etc.) and reuse the normalized variables
rather than the raw inputs.


router.push("/main");
router.push("/login");
} catch (error) {
console.error("회원가입 실패", error);

setSubmitError(
error instanceof Error
? error.message
: "회원가입 중 오류가 발생했습니다.",
);
Comment on lines +56 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

원본 에러 메시지 직접 노출은 피하는 것이 좋습니다

Line 56-60에서 error.message를 그대로 보여주면 내부 메시지가 사용자에게 노출될 수 있습니다. 사용자 메시지는 고정/매핑 기반으로 제한하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer/src/app/signup/page.tsx` around lines 56 - 60, setSubmitError
currently assigns error.message directly which exposes internal errors; instead
update the signup error handling in page.tsx (where setSubmitError is called) to
map known Error types/messages to fixed user-facing strings and fall back to a
single generic message like "회원가입 중 오류가 발생했습니다." Do this by inspecting the
thrown value (e.g., error instanceof Error ? error.message : ...) and
translating it via a small switch/map before calling setSubmitError, ensuring no
raw error.message is passed through.

}
};

return (
Expand Down Expand Up @@ -96,6 +120,10 @@ export default function SignupPage() {
errorMessage="비밀번호가 일치하지 않습니다."
/>
</div>

{submitError ? (
<p className="body3-m text-red-500">{submitError}</p>
) : null}
</div>
</section>

Expand All @@ -106,8 +134,9 @@ export default function SignupPage() {
kind="default"
variant="primary"
onClick={handleSignup}
disabled={isFormInvalid || isPending}
>
회원가입
{isPending ? "회원가입 중..." : "회원가입"}
</Button>
</div>
</main>
Expand Down
Loading
Loading