로그인 UI 추가
This commit is contained in:
@@ -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 |
@@ -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)) {
|
||||||
|
|||||||
@@ -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
53
src/app/login/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user