check
All checks were successful
deploy-on-main / deploy (push) Successful in 26s

This commit is contained in:
koreacomp5
2025-11-10 21:58:36 +09:00
parent 14c80baeec
commit c5bc8f5b49
6 changed files with 60 additions and 10 deletions

View File

@@ -61,15 +61,27 @@ export async function POST(req: Request) {
});
isAdmin = !!hasAdmin;
}
// 추가 안전장치: 사용자 레코드의 authLevel이 ADMIN이면 관리자 취급
if (!isAdmin && user.authLevel === "ADMIN") {
isAdmin = true;
}
const res = NextResponse.json({ ok: true, user: { userId: user.userId, nickname: user.nickname } });
// HTTPS 요청에서만 Secure 속성 부여 (HTTP 환경에서는 생략하여 로컬 start에서도 동작)
let secureAttr = "";
try {
const isHttps = new URL(req.url).protocol === "https:";
secureAttr = isHttps ? "; Secure" : "";
} catch {
secureAttr = "";
}
res.headers.append(
"Set-Cookie",
`uid=${encodeURIComponent(user.userId)}; Path=/; HttpOnly; SameSite=Lax`
`uid=${encodeURIComponent(user.userId)}; Path=/; HttpOnly; SameSite=Lax${secureAttr}`
);
res.headers.append(
"Set-Cookie",
`isAdmin=${isAdmin ? "1" : "0"}; Path=/; HttpOnly; SameSite=Lax`
`isAdmin=${isAdmin ? "1" : "0"}; Path=/; HttpOnly; SameSite=Lax${secureAttr}`
);
return res;
}

View File

@@ -4,7 +4,7 @@ import prisma from "@/lib/prisma";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const category = searchParams.get("category"); // slug or id
const where: any = {};
const where: any = { status: "active" };
if (category) {
if (category.length === 25 || category.length === 24) {
where.categoryId = category;

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { BoardToolbar } from "@/app/components/BoardToolbar";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import { UserAvatar } from "@/app/components/UserAvatar";
import { RankIcon1st } from "@/app/components/RankIcon1st";
import { RankIcon2nd } from "@/app/components/RankIcon2nd";
@@ -33,7 +34,10 @@ export default async function BoardDetail({ params, searchParams }: { params: an
const res = await fetch(new URL("/api/boards", base).toString(), { cache: "no-store" });
const { boards } = await res.json();
const board = (boards || []).find((b: any) => b.slug === idOrSlug || b.id === idOrSlug);
const id = board?.id as string;
if (!board) {
return notFound();
}
const id = board.id as string;
const siblingBoards = (boards || []).filter((b: any) => b.category?.id && b.category.id === board?.category?.id);
const categoryName = board?.category?.name ?? "";
// 메인배너 표시 설정

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import { RankIcon1st } from "./RankIcon1st";
import { RankIcon2nd } from "./RankIcon2nd";
@@ -52,6 +52,23 @@ export function BoardPanelClient({
}) {
const [selectedBoardId, setSelectedBoardId] = useState(initialBoardId);
// 데이터가 비어있을 때 안전 처리
if (!boardsData || boardsData.length === 0) {
return (
<div className="h-full min-h-0 flex items-center justify-center rounded-xl bg-white text-sm text-neutral-500">
.
</div>
);
}
// 선택된 보드가 목록에 없으면 첫 번째 보드로 동기화
useEffect(() => {
const exists = boardsData.some((bd) => bd.board.id === selectedBoardId);
if (!exists) {
setSelectedBoardId(boardsData[0].board.id);
}
}, [boardsData, selectedBoardId]);
// 선택된 게시판 데이터 찾기
const selectedBoardData = boardsData.find(bd => bd.board.id === selectedBoardId) || boardsData[0];
const { board, categoryName, siblingBoards } = selectedBoardData;

View File

@@ -77,7 +77,7 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
// 보드 메타데이터 (메인뷰 타입 포함)
const boardsMeta = visibleBoardIds.length
? await prisma.board.findMany({
where: { id: { in: visibleBoardIds } },
where: { id: { in: visibleBoardIds }, status: "active" },
select: {
id: true,
name: true,
@@ -116,14 +116,26 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
// 게시판 패널 데이터 수집 함수 (모든 sibling boards 포함)
const prepareBoardPanelData = async (board: { id: string; name: string; slug: string; categoryId: string | null; categoryName: string; mainTypeKey?: string }) => {
// 같은 카테고리의 소분류(게시판) 탭 목록
const siblingBoards = board.categoryId
let siblingBoards: any[] = board.categoryId
? await prisma.board.findMany({
where: { categoryId: board.categoryId },
where: { categoryId: board.categoryId, status: "active" },
select: { id: true, name: true, slug: true, mainPageViewType: { select: { key: true } } },
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
})
: [];
// 카테고리가 없거나 동료 게시판이 없을 때, 선택된 보드 단독 구성으로 대체
if (siblingBoards.length === 0) {
siblingBoards = [
{
id: board.id,
name: board.name,
slug: board.slug,
mainPageViewType: { key: board.mainTypeKey },
},
];
}
const boardsData = [];
for (const sb of siblingBoards) {

View File

@@ -17,7 +17,8 @@ export async function middleware(req: NextRequest) {
}
// uid 쿠키가 있어도 실제 유저가 존재하지 않으면 비로그인으로 간주하고 쿠키 정리
let authenticated = false;
// 기본값은 쿠키 존재 여부 기준(네트워크 오류 시 과도한 로그아웃 방지)
let authenticated = !!uid;
if (uid) {
try {
const verifyUrl = new URL("/api/auth/session", req.url);
@@ -30,9 +31,13 @@ export async function middleware(req: NextRequest) {
if (verifyRes.ok) {
const data = await verifyRes.json().catch(() => ({ ok: false }));
authenticated = !!data?.ok;
} else {
// 검증 API가 200이 아니면 기존 판단 유지(일시적 오류 대비)
authenticated = !!uid;
}
} catch {
authenticated = false;
// 네트워크/런타임 오류 시 기존 판단 유지(쿠키 있으면 로그인 간주)
authenticated = !!uid;
}
// 유효하지 않은 uid 쿠키는 즉시 제거
if (!authenticated) {