This commit is contained in:
@@ -225,7 +225,7 @@ model Post {
|
|||||||
|
|
||||||
// 사용자
|
// 사용자
|
||||||
model User {
|
model User {
|
||||||
userId String @id @default(cuid())
|
userId String @id
|
||||||
nickname String @unique
|
nickname String @unique
|
||||||
passwordHash String?
|
passwordHash String?
|
||||||
name String
|
name String
|
||||||
|
|||||||
@@ -121,16 +121,29 @@ async function createRandomUsers(count = 100) {
|
|||||||
|
|
||||||
const createdUsers = [];
|
const createdUsers = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
// 닉네임을 결정론적으로 생성해 재실행 시 중복 생성 방지
|
// 고정 ID: user001, user002, ...
|
||||||
const nickname = `user${String(i + 1).padStart(3, "0")}`;
|
const userId = `user${String(i + 1).padStart(3, "0")}`;
|
||||||
const existing = await prisma.user.findUnique({ where: { nickname } });
|
const existing = await prisma.user.findUnique({ where: { userId } });
|
||||||
let user = existing;
|
let user = existing;
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
const name = generateRandomKoreanName();
|
const name = generateRandomKoreanName();
|
||||||
const birth = randomDate(1975, 2005);
|
const birth = randomDate(1975, 2005);
|
||||||
const phone = await findAvailablePhone(i + 2); // admin이 0001 사용하므로 겹치지 않도록 오프셋
|
const phone = await findAvailablePhone(i + 2); // admin이 0001 사용하므로 겹치지 않도록 오프셋
|
||||||
|
// 닉네임: 중복 없는 랜덤 한글 생성
|
||||||
|
let nickname = generateRandomKoreanName();
|
||||||
|
for (let tries = 0; tries < 10; tries++) {
|
||||||
|
const dup = await prisma.user.findUnique({ where: { nickname } });
|
||||||
|
if (!dup) break;
|
||||||
|
nickname = generateRandomKoreanName();
|
||||||
|
}
|
||||||
|
// 그래도 중복이면 희귀 조합 한 번 더 시도
|
||||||
|
const finalDup = await prisma.user.findUnique({ where: { nickname } });
|
||||||
|
if (finalDup) {
|
||||||
|
nickname = generateRandomKoreanName();
|
||||||
|
}
|
||||||
user = await prisma.user.create({
|
user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
userId,
|
||||||
nickname,
|
nickname,
|
||||||
name,
|
name,
|
||||||
birth,
|
birth,
|
||||||
@@ -247,6 +260,7 @@ async function upsertAdmin() {
|
|||||||
level: 200,
|
level: 200,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
|
userId: "admin",
|
||||||
nickname: "admin",
|
nickname: "admin",
|
||||||
name: "Administrator",
|
name: "Administrator",
|
||||||
birth: new Date("1990-01-01"),
|
birth: new Date("1990-01-01"),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function AdminUsersPage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-[#f6f4f4] border-b border-[#e6e6e6]">
|
<thead className="bg-[#f6f4f4] border-b border-[#e6e6e6]">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">ID</th>
|
||||||
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">닉네임</th>
|
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">닉네임</th>
|
||||||
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">이름</th>
|
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">이름</th>
|
||||||
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">전화</th>
|
<th className="px-4 py-2 text-left text-[12px] text-[#8c8c8c]">전화</th>
|
||||||
@@ -90,6 +91,7 @@ function Row({ u, onChanged }: { u: any; onChanged: () => void }) {
|
|||||||
const allRoles = ["admin", "editor", "user"] as const;
|
const allRoles = ["admin", "editor", "user"] as const;
|
||||||
return (
|
return (
|
||||||
<tr className="hover:bg-neutral-50">
|
<tr className="hover:bg-neutral-50">
|
||||||
|
<td className="px-4 py-2 text-left tabular-nums">{u.userId}</td>
|
||||||
<td className="px-4 py-2 text-left">{u.nickname}</td>
|
<td className="px-4 py-2 text-left">{u.nickname}</td>
|
||||||
<td className="px-4 py-2 text-left">{u.name}</td>
|
<td className="px-4 py-2 text-left">{u.name}</td>
|
||||||
<td className="px-4 py-2 text-left">{u.phone}</td>
|
<td className="px-4 py-2 text-left">{u.phone}</td>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pointer-events-none absolute bottom-[-5px] left-1/2 -translate-x-1/2 z-20 flex items-center gap-6">
|
<div className="pointer-events-none absolute bottom-[-5px] left-1/2 -translate-x-1/2 z-20 flex items-center gap-6 w-full">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="이전"
|
aria-label="이전"
|
||||||
@@ -159,7 +159,7 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
|
|||||||
<path d="M9.5 5.5L15 12l-5.5 6.5" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M9.5 5.5L15 12l-5.5 6.5" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div ref={trackRef} className="pointer-events-auto relative h-1 w-[480px] rounded bg-[#EDEDED]">
|
<div ref={trackRef} className="pointer-events-auto relative h-1 flex-1 min-w-0 max-w-[480px] rounded bg-[#EDEDED]">
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 h-1 rounded bg-[var(--red-50,#F94B37)]"
|
className="absolute top-0 h-1 rounded bg-[var(--red-50,#F94B37)]"
|
||||||
style={{ width: `${thumbWidth}px`, left: `${thumbLeft}px` }}
|
style={{ width: `${thumbWidth}px`, left: `${thumbLeft}px` }}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function SendMessageForm({ receiverId, receiverNickname, onSent }: { rece
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col gap-2">
|
<form onSubmit={onSubmit} className="flex flex-col gap-2">
|
||||||
<div className="text-sm text-neutral-700">
|
<div className="text-sm text-neutral-700">
|
||||||
받는 사람: <span className="font-semibold">{receiverNickname || receiverId}</span>
|
받는 사람: <span className="font-semibold">{receiverNickname || "알 수 없음"}</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={body}
|
value={body}
|
||||||
|
|||||||
@@ -11,13 +11,51 @@ export async function middleware(req: NextRequest) {
|
|||||||
const uid = req.cookies.get("uid")?.value;
|
const uid = req.cookies.get("uid")?.value;
|
||||||
const isAdmin = req.cookies.get("isAdmin")?.value;
|
const isAdmin = req.cookies.get("isAdmin")?.value;
|
||||||
|
|
||||||
|
// 세션 검증 엔드포인트 자체에 대해선 미들웨어 검증을 생략 (무한 루프 방지)
|
||||||
|
if (pathname === "/api/auth/session") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// uid 쿠키가 있어도 실제 유저가 존재하지 않으면 비로그인으로 간주하고 쿠키 정리
|
||||||
|
let authenticated = false;
|
||||||
|
if (uid) {
|
||||||
|
try {
|
||||||
|
const verifyUrl = new URL("/api/auth/session", req.url);
|
||||||
|
const verifyRes = await fetch(verifyUrl.toString(), {
|
||||||
|
headers: {
|
||||||
|
// 현재 요청의 쿠키를 전달하여 세션을 검증
|
||||||
|
cookie: req.headers.get("cookie") ?? "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (verifyRes.ok) {
|
||||||
|
const data = await verifyRes.json().catch(() => ({ ok: false }));
|
||||||
|
authenticated = !!data?.ok;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
authenticated = false;
|
||||||
|
}
|
||||||
|
// 유효하지 않은 uid 쿠키는 즉시 제거
|
||||||
|
if (!authenticated) {
|
||||||
|
response.cookies.set("uid", "", { path: "/", maxAge: 0 });
|
||||||
|
response.cookies.set("isAdmin", "", { path: "/", maxAge: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Admin 페이지 보호
|
// Admin 페이지 보호
|
||||||
if (pathname.startsWith("/admin")) {
|
if (pathname.startsWith("/admin")) {
|
||||||
|
console.log("Admin 페이지 접근");
|
||||||
|
console.log("uid", uid);
|
||||||
// 비로그인 사용자는 로그인 페이지로
|
// 비로그인 사용자는 로그인 페이지로
|
||||||
if (!uid) {
|
if (!authenticated) {
|
||||||
const loginUrl = req.nextUrl.clone();
|
const loginUrl = req.nextUrl.clone();
|
||||||
loginUrl.pathname = "/login";
|
loginUrl.pathname = "/login";
|
||||||
return NextResponse.redirect(loginUrl);
|
const res = NextResponse.redirect(loginUrl);
|
||||||
|
// 리다이렉트 응답에도 쿠키 정리 반영
|
||||||
|
if (uid && !authenticated) {
|
||||||
|
res.cookies.set("uid", "", { path: "/", maxAge: 0 });
|
||||||
|
res.cookies.set("isAdmin", "", { path: "/", maxAge: 0 });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
// 로그인했지만 관리자가 아니면 홈으로
|
// 로그인했지만 관리자가 아니면 홈으로
|
||||||
if (isAdmin !== "1") {
|
if (isAdmin !== "1") {
|
||||||
@@ -29,7 +67,7 @@ export async function middleware(req: NextRequest) {
|
|||||||
|
|
||||||
// 로그인된 상태에서 로그인 페이지 접근 시 홈으로 리다이렉트
|
// 로그인된 상태에서 로그인 페이지 접근 시 홈으로 리다이렉트
|
||||||
if (pathname === "/login" || pathname.startsWith("/login/")) {
|
if (pathname === "/login" || pathname.startsWith("/login/")) {
|
||||||
if (uid) {
|
if (authenticated) {
|
||||||
const homeUrl = req.nextUrl.clone();
|
const homeUrl = req.nextUrl.clone();
|
||||||
homeUrl.pathname = "/";
|
homeUrl.pathname = "/";
|
||||||
return NextResponse.redirect(homeUrl);
|
return NextResponse.redirect(homeUrl);
|
||||||
@@ -37,7 +75,7 @@ export async function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const needAuth = protectedApi.some((re) => re.test(pathname));
|
const needAuth = protectedApi.some((re) => re.test(pathname));
|
||||||
if (needAuth && !uid) {
|
if (needAuth && !authenticated) {
|
||||||
return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user