Skip to content
Open
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
31 changes: 25 additions & 6 deletions app/_components/BubbleDiv.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import Image from "next/image";
import React from "react";

const BubbleDiv = () => {
const BubbleDiv = ({
children,
w = 226,
h = 41.77,
typo = "typo-16-600",
top = 1,
}: {
children?: React.ReactNode;
w?: number;
h?: number;
typo?: string;
top?: number;
}) => {
return (
<div className="relative h-[41.77px] w-[226px]">
<span className="typo-16-600 absolute top-1 left-0 z-20 w-full text-center text-black">
현재 <span className="text-bubble-text-highight">775명 </span>
참여중이에요!
<div className="relative" style={{ width: `${w}px`, height: `${h}px` }}>
<span
className={`${typo} absolute left-0 z-20 w-full text-center text-black`}
style={{ top: `${top}px` }}
>
{children || (
<>
현재 <span className="text-bubble-text-highight">775명 </span>
참여중이에요!
</>
)}
</span>
<Image src="/bubble/bubble.svg" alt="bubble" width={226} height={41.77} />
<Image src="/bubble/bubble.svg" alt="말풍선" width={w} height={h} />
</div>
);
};
Expand Down
8 changes: 4 additions & 4 deletions app/_components/LoginActionSection.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"use client";
import BubbleDiv from "@/app/_components/BubbleDiv";
import Button from "@/components/ui/Button";
import { KakaoLoginButton, GoogleLoginButton } from "./SocialButtonList";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import Link from "next/link";

export default function ScreenLoginActionSection() {
const handleKakaoLogin = () => {
Expand Down Expand Up @@ -65,12 +65,12 @@ export default function ScreenLoginActionSection() {
</GoogleLoginButton>
<div className="typo-14-500 text-bottomsheet-text-caption mt-6 flex flex-col items-center">
<span>혹은</span>
<button
type="button"
<Link
href="/login"
className="all-[unset] cursor-pointer underline"
>
이메일로 로그인
</button>
</Link>
</div>
</DrawerContent>
</Drawer>
Expand Down
17 changes: 17 additions & 0 deletions app/login/_components/LocalLoginIntro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";

const LocalLoginIntro = () => {
return (
<section className="mt-6 flex flex-col items-start gap-2">
<span className="typo-26-700 leading-[1.4] text-black">
이메일로
<br /> 코매칭 시작하기
</span>
<span className="typo-16-500 text-color-text-caption2">
로그인하거나 새로운 계정을 만들어보세요.
</span>
</section>
);
};

export default LocalLoginIntro;
90 changes: 90 additions & 0 deletions app/login/_components/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";
import BubbleDiv from "@/app/_components/BubbleDiv";
import Button from "@/components/ui/Button";
import { User } from "lucide-react";
import React, { useActionState } from "react";
import Link from "next/link";
import { loginAction } from "@/lib/actions/loginAction";

const INPUT_STYLE = {
background:
"linear-gradient(180deg, rgba(248, 248, 248, 0.03) 0%, rgba(248, 248, 248, 0.24) 100%)",
};
const INPUT_CLASSNAME =
"all:unset box-border w-full border-b border-gray-300 px-2 py-[14.5px] leading-[19px] placeholder:text-[#B3B3B3]";

export const LoginForm = () => {
// React 19: useActionState로 폼 상태 및 팬딩 처리 관리
const [state, formAction, isPending] = useActionState(loginAction, {
success: false,
message: "",
});

return (
<section className="mt-10 flex w-full flex-1 flex-col items-start gap-6">
<form className="flex w-full flex-col gap-4" action={formAction}>
<div className="flex w-full flex-col gap-2">
<label htmlFor="email" className="typo-14-500 text-gray-700">
아이디(이메일)
</label>
<input
id="email"
type="email"
name="email"
placeholder="이메일 입력"
required
autoComplete="email"
className={INPUT_CLASSNAME}
style={INPUT_STYLE}
/>
</div>

<div className="relative mb-6 flex w-full flex-col gap-2">
<label htmlFor="password" className="typo-14-500 text-gray-700">
비밀번호
</label>
<input
id="password"
type="password"
name="password"
placeholder="비밀번호 입력"
required
autoComplete="current-password"
className={INPUT_CLASSNAME}
style={INPUT_STYLE}
/>
{!state.success && (
<span className="typo-12-400 text-color-flame-700 absolute bottom-[-25px] left-0">
* 이메일 혹은 비밀번호가 틀립니다
</span>
)}
Comment on lines +56 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

high

useActionState에서 반환된 state 객체를 사용하여 오류 메시지를 동적으로 표시해야 합니다. 현재는 메시지가 하드코딩되어 있고, 초기 렌더링 시에도 불필요하게 표시되는 문제가 있습니다. state.message가 있을 때만 오류 메시지를 표시하고, 그 내용을 동적으로 채우도록 수정해야 합니다.

Suggested change
{!state.success && (
<span className="typo-12-400 text-color-flame-700 absolute bottom-[-25px] left-0">
* 이메일 혹은 비밀번호가 틀립니다
</span>
)}
{state.message && (
<span className="typo-12-400 text-color-flame-700 absolute bottom-[-25px] left-0">
* {state.message}
</span>
)}
References
  1. useActionState(구 useFormState)와 useFormStatus를 활용하여 폼 상태와 로딩 UI를 선언적으로 관리하는지 확인하세요. (link)

</div>
<Button shadow={true} type="submit" disabled={isPending}>
{isPending ? "로그인 중..." : "로그인"}
</Button>
</form>
<div className="typo-14-500 text-color-text-caption2 flex w-full justify-center">
<Link href="/find-email" className="cursor-pointer">
이메일 찾기
</Link>
<span className="mx-4">|</span>
<Link href="/reset-password" className="cursor-pointer">
비밀번호 변경
</Link>
</div>

<div className="mt-auto flex w-full flex-col items-center gap-4">
<BubbleDiv w={162} h={26} typo="typo-12-600" top={3}>
아직 계정이 없으신가요?!
</BubbleDiv>
<button
type="button"
className="typo-14-500 flex items-center gap-1 border-b-2 border-gray-500 text-gray-500"
>
<User />
이메일로 회원가입
</button>
</div>
</section>
);
};
16 changes: 16 additions & 0 deletions app/login/_components/ScreenLocalLoginPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import { BackButton } from "@/components/ui/BackButton";
import LocalLoginIntro from "./LocalLoginIntro";
import { LoginForm } from "./LoginForm";

const ScreenLocalLoginPage = () => {
return (
<main className="flex h-dvh flex-col items-start px-4 pt-2 pb-[6.2vh]">
<BackButton />
<LocalLoginIntro />
<LoginForm />
</main>
);
};

export default ScreenLocalLoginPage;
14 changes: 14 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import { Metadata } from "next";
import ScreenLocalLoginPage from "./_components/ScreenLocalLoginPage";

export const metadata: Metadata = {
title: "로그인 | COMAtching",
description: "COMAtching 로그인 페이지",
};

const LoginPage = () => {
return <ScreenLocalLoginPage />;
};

export default LoginPage;
36 changes: 36 additions & 0 deletions components/ui/BackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import { ChevronLeft } from "lucide-react";
import { cn } from "@/lib/utils";

type BackButtonProps = {
variant?: "absolute" | "static";
className?: string;
onClick?: () => void;
};

export const BackButton = ({
variant = "static",
className = "",
onClick,
}: BackButtonProps) => {
const router = useRouter();
const positionClass = variant === "absolute" ? "absolute left-4 top-2" : "";
const handleClick = onClick ?? (() => router.back());

return (
<button
type="button"
onClick={handleClick}
aria-label="뒤로 가기"
className={cn(
"flex h-12 w-12 cursor-pointer items-center justify-center rounded-full border border-[#FFFFFF]/30 bg-[#FFFFFF]/50 [box-shadow:0px_4px_8px_rgba(0,0,0,0.08),0px_0px_16px_rgba(0,0,0,0.1)] backdrop-blur-[15px]",
positionClass,
className,
)}
>
<ChevronLeft className="text-[#282828]" size={20} />
</button>
);
};
2 changes: 1 addition & 1 deletion components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function Button({
disabled={disabled}
className={cn(
// 기본 스타일 (기본 높이 h-12, 너비 w-full, 폰트 등 복구)
"typo-20-600 text-button-primary-text-default flex h-12 w-full items-center justify-center rounded-[12px] transition-colors duration-100",
"typo-20-600 text-button-primary-text-default bg-button-primary flex h-12 w-full items-center justify-center rounded-[16px] transition-colors duration-100",
fixed && "fixed z-50 mx-auto",
disabled ? "cursor-not-allowed" : "cursor-pointer",
// 1. 사용자 className 먼저 적용 (여기서 h-10 등을 넣으면 위 h-12가 덮어씌워짐)
Expand Down
31 changes: 31 additions & 0 deletions lib/actions/loginAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use server";

type LoginState = {
success: boolean;
message: string;
};

export async function loginAction(
prevState: LoginState,
formData: FormData,
): Promise<LoginState> {
const email = formData.get("email");
const password = formData.get("password");

// Mock delay
await new Promise((resolve) => setTimeout(resolve, 1000));

// TODO: 실제 백엔드 API 호출로 교체 필요
// const response = await fetch("https://api.comatching.com/login", {
// method: "POST",
// body: JSON.stringify({ email, password }),
// headers: { "Content-Type": "application/json" },
// });

// Mock logic
if (email === "test@test.com" && password === "1234") {
return { success: true, message: "" };
}
Comment on lines +26 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The loginAction function contains hardcoded credentials (test@test.com / 1234). This creates a functional backdoor that allows anyone with these credentials to bypass authentication if the code is deployed to production. Mock logic should be removed before merging.

References
  1. Security warnings should be reserved for truly sensitive values like tokens, passwords, or private keys.


return { success: false, message: "이메일 혹은 비밀번호가 틀립니다" };
}
21 changes: 21 additions & 0 deletions lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "AIzaSyDAE4IwNPU33dniaBSnHA7hjw33ByoQ-14",
Copy link
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The Firebase API key and configuration are hardcoded in the source code, which is a security vulnerability. It is a security best practice to use environment variables (e.g., via .env file with NEXT_PUBLIC_ prefix) for configuration to prevent project details from being committed to the repository and to allow for environment-specific settings, improving both security and maintainability.

Suggested change
apiKey: "AIzaSyDAE4IwNPU33dniaBSnHA7hjw33ByoQ-14",
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
References
  1. Security warnings should be reserved for truly sensitive values like tokens, passwords, or private keys.

authDomain: "comatching-59f3e.firebaseapp.com",
projectId: "comatching-59f3e",
storageBucket: "comatching-59f3e.firebasestorage.app",
messagingSenderId: "106298040488",
appId: "1:106298040488:web:e4e7b9c54d55dcaab4229d",
measurementId: "G-188J563VND",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

Check warning on line 21 in lib/firebase.ts

View workflow job for this annotation

GitHub Actions / lint

'analytics' is assigned a value but never used
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"axios": "^1.13.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"firebase": "^12.8.0",
"lucide-react": "^0.562.0",
"next": "16.1.3",
"react": "19.2.3",
Expand Down
Loading
Loading