admin 권한 세팅
Some checks failed
deploy-on-main / deploy (push) Failing after 21s

This commit is contained in:
koreacomp5
2025-11-10 11:26:00 +09:00
parent 97c8e1c9fb
commit cb2d1f34d3
6 changed files with 65 additions and 11 deletions

View File

@@ -225,7 +225,7 @@ model Post {
// 사용자
model User {
userId String @id @default(cuid())
userId String @id
nickname String @unique
passwordHash String?
name String

View File

@@ -121,16 +121,29 @@ async function createRandomUsers(count = 100) {
const createdUsers = [];
for (let i = 0; i < count; i++) {
// 닉네임을 결정론적으로 생성해 재실행 시 중복 생성 방지
const nickname = `user${String(i + 1).padStart(3, "0")}`;
const existing = await prisma.user.findUnique({ where: { nickname } });
// 고정 ID: user001, user002, ...
const userId = `user${String(i + 1).padStart(3, "0")}`;
const existing = await prisma.user.findUnique({ where: { userId } });
let user = existing;
if (!existing) {
const name = generateRandomKoreanName();
const birth = randomDate(1975, 2005);
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({
data: {
userId,
nickname,
name,
birth,
@@ -247,6 +260,7 @@ async function upsertAdmin() {
level: 200,
},
create: {
userId: "admin",
nickname: "admin",
name: "Administrator",
birth: new Date("1990-01-01"),

View File

@@ -47,6 +47,7 @@ export default function AdminUsersPage() {
<table className="w-full text-sm">
<thead className="bg-[#f6f4f4] border-b border-[#e6e6e6]">
<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>
@@ -90,6 +91,7 @@ function Row({ u, onChanged }: { u: any; onChanged: () => void }) {
const allRoles = ["admin", "editor", "user"] as const;
return (
<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.name}</td>
<td className="px-4 py-2 text-left">{u.phone}</td>

View File

@@ -148,7 +148,7 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
</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
type="button"
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" />
</svg>
</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
className="absolute top-0 h-1 rounded bg-[var(--red-50,#F94B37)]"
style={{ width: `${thumbWidth}px`, left: `${thumbLeft}px` }}

View File

@@ -43,7 +43,7 @@ export function SendMessageForm({ receiverId, receiverNickname, onSent }: { rece
return (
<form onSubmit={onSubmit} className="flex flex-col gap-2">
<div className="text-sm text-neutral-700">
: <span className="font-semibold">{receiverNickname || receiverId}</span>
: <span className="font-semibold">{receiverNickname || "알 수 없음"}</span>
</div>
<textarea
value={body}

View File

@@ -11,13 +11,51 @@ export async function middleware(req: NextRequest) {
const uid = req.cookies.get("uid")?.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 페이지 보호
if (pathname.startsWith("/admin")) {
console.log("Admin 페이지 접근");
console.log("uid", uid);
// 비로그인 사용자는 로그인 페이지로
if (!uid) {
if (!authenticated) {
const loginUrl = req.nextUrl.clone();
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") {
@@ -29,7 +67,7 @@ export async function middleware(req: NextRequest) {
// 로그인된 상태에서 로그인 페이지 접근 시 홈으로 리다이렉트
if (pathname === "/login" || pathname.startsWith("/login/")) {
if (uid) {
if (authenticated) {
const homeUrl = req.nextUrl.clone();
homeUrl.pathname = "/";
return NextResponse.redirect(homeUrl);
@@ -37,7 +75,7 @@ export async function middleware(req: NextRequest) {
}
const needAuth = protectedApi.some((re) => re.test(pathname));
if (needAuth && !uid) {
if (needAuth && !authenticated) {
return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
}