로그인 UI 추가

This commit is contained in:
koreacomp5
2025-10-10 11:22:43 +09:00
parent c28698cd5c
commit 7ba8091ab9
5 changed files with 95 additions and 2 deletions

View File

@@ -620,7 +620,7 @@ model CouponRedemption {
// 제휴업체(위치 기반) // 제휴업체(위치 기반)
model Partner { model Partner {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String @unique
category String category String
latitude Float latitude Float
longitude Float longitude Float

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 781 KiB

After

Width:  |  Height:  |  Size: 781 KiB

View File

@@ -4,6 +4,23 @@ import prisma from "@/lib/prisma";
import { verifyPassword } from "@/lib/password"; import { verifyPassword } from "@/lib/password";
import { getClientKey, isRateLimited } from "@/lib/ratelimit"; import { getClientKey, isRateLimited } from "@/lib/ratelimit";
export async function GET(req: Request) {
try {
const cookieHeader = req.headers.get("cookie") || "";
const uid = cookieHeader
.split(";")
.map((s) => s.trim())
.find((pair) => pair.startsWith("uid="))
?.split("=")[1];
if (!uid) return NextResponse.json({ ok: false }, { status: 200 });
const user = await prisma.user.findUnique({ where: { userId: decodeURIComponent(uid) } });
if (!user) return NextResponse.json({ ok: false }, { status: 200 });
return NextResponse.json({ ok: true, user: { userId: user.userId, nickname: user.nickname } });
} catch {
return NextResponse.json({ ok: false }, { status: 200 });
}
}
export async function POST(req: Request) { export async function POST(req: Request) {
const key = getClientKey(req, "login"); const key = getClientKey(req, "login");
if (isRateLimited(key, 5, 60_000)) { if (isRateLimited(key, 5, 60_000)) {

View File

@@ -1,7 +1,22 @@
"use client";
import { ThemeToggle } from "@/app/components/ThemeToggle"; import { ThemeToggle } from "@/app/components/ThemeToggle";
import { SearchBar } from "@/app/components/SearchBar"; import { SearchBar } from "@/app/components/SearchBar";
import { Button } from "@/app/components/ui/Button";
import React from "react";
export function AppHeader() { export function AppHeader() {
const [user, setUser] = React.useState<{ nickname: string } | null>(null);
React.useEffect(() => {
fetch("/api/auth/session")
.then((r) => r.json())
.then((d) => setUser(d?.ok ? d.user : null))
.catch(() => setUser(null));
}, []);
const onLogout = async () => {
await fetch("/api/auth/session", { method: "DELETE" });
setUser(null);
location.reload();
};
return ( return (
<header style={{ display: "flex", justifyContent: "space-between", padding: 12 }}> <header style={{ display: "flex", justifyContent: "space-between", padding: 12 }}>
<div>msg App</div> <div>msg App</div>
@@ -10,6 +25,14 @@ export function AppHeader() {
<a href="/boards"></a> <a href="/boards"></a>
<SearchBar /> <SearchBar />
<ThemeToggle /> <ThemeToggle />
{user ? (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span>{user.nickname}</span>
<Button variant="ghost" onClick={onLogout}></Button>
</div>
) : (
<a href="/login"></a>
)}
</nav> </nav>
</header> </header>
); );

53
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,53 @@
"use client";
import React from "react";
import { Button } from "@/app/components/ui/Button";
import { useToast } from "@/app/components/ui/ToastProvider";
export default function LoginPage() {
const { show } = useToast();
const [nickname, setNickname] = React.useState("");
const [password, setPassword] = React.useState("");
const [loading, setLoading] = React.useState(false);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch("/api/auth/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nickname, password }),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "로그인 실패");
show("로그인되었습니다");
location.href = "/";
} catch (err: any) {
show(err.message || "로그인 실패");
} finally {
setLoading(false);
}
};
return (
<div style={{ maxWidth: 420, margin: "40px auto" }}>
<h1></h1>
<form onSubmit={onSubmit} style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<input
placeholder="닉네임"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
style={{ padding: 8, border: "1px solid #ddd", borderRadius: 6 }}
/>
<input
placeholder="비밀번호"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: 8, border: "1px solid #ddd", borderRadius: 6 }}
/>
<Button type="submit" disabled={loading}>{loading ? "로그인 중..." : "로그인"}</Button>
</form>
</div>
);
}