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; isAdmin = !!hasAdmin;
} }
// 추가 안전장치: 사용자 레코드의 authLevel이 ADMIN이면 관리자 취급
if (!isAdmin && user.authLevel === "ADMIN") {
isAdmin = true;
}
const res = NextResponse.json({ ok: true, user: { userId: user.userId, nickname: user.nickname } }); 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( res.headers.append(
"Set-Cookie", "Set-Cookie",
`uid=${encodeURIComponent(user.userId)}; Path=/; HttpOnly; SameSite=Lax` `uid=${encodeURIComponent(user.userId)}; Path=/; HttpOnly; SameSite=Lax${secureAttr}`
); );
res.headers.append( res.headers.append(
"Set-Cookie", "Set-Cookie",
`isAdmin=${isAdmin ? "1" : "0"}; Path=/; HttpOnly; SameSite=Lax` `isAdmin=${isAdmin ? "1" : "0"}; Path=/; HttpOnly; SameSite=Lax${secureAttr}`
); );
return res; return res;
} }

View File

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

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { BoardToolbar } from "@/app/components/BoardToolbar"; import { BoardToolbar } from "@/app/components/BoardToolbar";
import { headers } from "next/headers"; import { headers } from "next/headers";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";
import { UserAvatar } from "@/app/components/UserAvatar"; import { UserAvatar } from "@/app/components/UserAvatar";
import { RankIcon1st } from "@/app/components/RankIcon1st"; import { RankIcon1st } from "@/app/components/RankIcon1st";
import { RankIcon2nd } from "@/app/components/RankIcon2nd"; 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 res = await fetch(new URL("/api/boards", base).toString(), { cache: "no-store" });
const { boards } = await res.json(); const { boards } = await res.json();
const board = (boards || []).find((b: any) => b.slug === idOrSlug || b.id === idOrSlug); 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 siblingBoards = (boards || []).filter((b: any) => b.category?.id && b.category.id === board?.category?.id);
const categoryName = board?.category?.name ?? ""; const categoryName = board?.category?.name ?? "";
// 메인배너 표시 설정 // 메인배너 표시 설정

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { RankIcon1st } from "./RankIcon1st"; import { RankIcon1st } from "./RankIcon1st";
import { RankIcon2nd } from "./RankIcon2nd"; import { RankIcon2nd } from "./RankIcon2nd";
@@ -52,6 +52,23 @@ export function BoardPanelClient({
}) { }) {
const [selectedBoardId, setSelectedBoardId] = useState(initialBoardId); 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 selectedBoardData = boardsData.find(bd => bd.board.id === selectedBoardId) || boardsData[0];
const { board, categoryName, siblingBoards } = selectedBoardData; const { board, categoryName, siblingBoards } = selectedBoardData;

View File

@@ -77,7 +77,7 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
// 보드 메타데이터 (메인뷰 타입 포함) // 보드 메타데이터 (메인뷰 타입 포함)
const boardsMeta = visibleBoardIds.length const boardsMeta = visibleBoardIds.length
? await prisma.board.findMany({ ? await prisma.board.findMany({
where: { id: { in: visibleBoardIds } }, where: { id: { in: visibleBoardIds }, status: "active" },
select: { select: {
id: true, id: true,
name: true, name: true,
@@ -116,14 +116,26 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
// 게시판 패널 데이터 수집 함수 (모든 sibling boards 포함) // 게시판 패널 데이터 수집 함수 (모든 sibling boards 포함)
const prepareBoardPanelData = async (board: { id: string; name: string; slug: string; categoryId: string | null; categoryName: string; mainTypeKey?: string }) => { 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({ ? 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 } } }, select: { id: true, name: true, slug: true, mainPageViewType: { select: { key: true } } },
orderBy: [{ sortOrder: "asc" }, { name: "asc" }], 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 = []; const boardsData = [];
for (const sb of siblingBoards) { for (const sb of siblingBoards) {

View File

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