아직 댓글이 없습니다.
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.
-
-
{c.authorName} · {new Date(c.createdAt).toLocaleString('ko-KR')}
+
))}
+ {user ? (
+
+ ) : (
+
댓글은 로그인 후 작성하실 수 있습니다.
+ )}
);
+
return (
);
}
diff --git a/next-app/apps/web/src/app/[boardSlug]/search/page.tsx b/next-app/apps/web/src/app/[boardSlug]/search/page.tsx
new file mode 100644
index 0000000..f2723e4
--- /dev/null
+++ b/next-app/apps/web/src/app/[boardSlug]/search/page.tsx
@@ -0,0 +1,8 @@
+import { StubPage } from '@/lib/page-shells';
+export default async function Page({ params, searchParams }: { params: Promise<{ boardSlug: string }>; searchParams: Promise<{ stx?: string; sfl?: string }> }) {
+ const { boardSlug } = await params;
+ const sp = await searchParams;
+ return
+ 검색 인덱스는 PostgreSQL FTS 또는 Meilisearch 연동 후 활성화됩니다.
+ ;
+}
diff --git a/next-app/apps/web/src/app/[boardSlug]/write/page.tsx b/next-app/apps/web/src/app/[boardSlug]/write/page.tsx
new file mode 100644
index 0000000..f30aad6
--- /dev/null
+++ b/next-app/apps/web/src/app/[boardSlug]/write/page.tsx
@@ -0,0 +1,28 @@
+import { StubPage } from '@/lib/page-shells';
+import { getBoardMeta } from '@/lib/legacy-board';
+import { getCurrentSiteUser } from '@/lib/page-data';
+import { redirect, notFound } from 'next/navigation';
+export const dynamic = 'force-dynamic';
+export default async function Page({ params }: { params: Promise<{ boardSlug: string }> }) {
+ const { boardSlug } = await params;
+ const user = await getCurrentSiteUser();
+ if (!user) redirect(`/login?next=/${boardSlug}/write`);
+ const meta = await getBoardMeta(boardSlug);
+ if (!meta) return notFound();
+ return
+
+ ;
+}
diff --git a/next-app/apps/web/src/app/admin/betting/page.tsx b/next-app/apps/web/src/app/admin/betting/page.tsx
new file mode 100644
index 0000000..5cdc24e
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/betting/page.tsx
@@ -0,0 +1,46 @@
+import { legacySql } from '@slot/db/legacy';
+
+export const dynamic = 'force-dynamic';
+
+export default async function BettingAdmin() {
+ const [bacara, swiun, gamePoints] = await Promise.all([
+ legacySql<{ c: string }[]>`SELECT COUNT(*)::text AS c FROM inspection2.bacara_betting`.catch(() => [{ c: '0' }] as any),
+ legacySql<{ c: string }[]>`SELECT COUNT(*)::text AS c FROM inspection2.swiun_betting`.catch(() => [{ c: '0' }] as any),
+ legacySql<{ c: string }[]>`SELECT COUNT(*)::text AS c FROM inspection2.game_point`.catch(() => [{ c: '0' }] as any),
+ ]);
+ const recent = await legacySql<{ mb_id: string; bg_amount: number; bg_result: string; bg_datetime: Date }[]>`
+ SELECT mb_id, bg_amount, bg_result, bg_datetime
+ FROM inspection2.bacara_betting ORDER BY bg_no DESC LIMIT 30
+ `.catch(() => []);
+ return (
+
+
베팅 관리
+
+
+
+
+
+
최근 바카라 베팅 30건
+
+
+ | 회원 | 금액 | 결과 | 시각 |
+
+
+ {recent.map((r, i) => (
+
+ | {r.mb_id} |
+ {r.bg_amount?.toLocaleString()} |
+ {r.bg_result} |
+ {new Date(r.bg_datetime).toLocaleString('ko-KR')} |
+
+ ))}
+
+
+
+ );
+}
+function Tile({ label, value }: { label: string; value: string }) {
+ return
;
+}
+const th: React.CSSProperties = { padding: 10, textAlign: 'left', fontWeight: 700 };
+const td: React.CSSProperties = { padding: 10 };
diff --git a/next-app/apps/web/src/app/admin/boards/page.tsx b/next-app/apps/web/src/app/admin/boards/page.tsx
new file mode 100644
index 0000000..8f61270
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/boards/page.tsx
@@ -0,0 +1,36 @@
+import { legacySql } from '@slot/db/legacy';
+
+export const dynamic = 'force-dynamic';
+
+export default async function BoardsAdmin() {
+ const rows = await legacySql<{ bo_table: string; bo_subject: string; gr_id: string; bo_count_write: number; bo_count_comment: number; bo_use_secret: number }[]>`
+ SELECT bo_table, bo_subject, gr_id, bo_count_write, bo_count_comment, bo_use_secret
+ FROM inspection2.g5_board ORDER BY bo_count_write DESC NULLS LAST LIMIT 100
+ `.catch(() => []);
+ return (
+
+
게시판 관리 ({rows.length})
+
+
+ | 그룹 | 슬러그 | 제목 | 글 | 댓글 | 비밀글 | 관리 |
+
+
+ {rows.map((r) => (
+
+ | {r.gr_id} |
+ {r.bo_table} |
+ {r.bo_subject} |
+ {r.bo_count_write?.toLocaleString() ?? 0} |
+ {r.bo_count_comment?.toLocaleString() ?? 0} |
+ {r.bo_use_secret ? '✓' : '-'} |
+ 설정 |
+
+ ))}
+
+
+
+ );
+}
+const th: React.CSSProperties = { padding: 10, textAlign: 'left', fontWeight: 700 };
+const td: React.CSSProperties = { padding: 10 };
+const btn = (): React.CSSProperties => ({ display: 'inline-block', padding: '4px 10px', background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', textDecoration: 'none', color: 'var(--color-text)', borderRadius: 3, fontSize: 12 });
diff --git a/next-app/apps/web/src/app/admin/games/page.tsx b/next-app/apps/web/src/app/admin/games/page.tsx
new file mode 100644
index 0000000..dc3e028
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/games/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage } from '@/lib/page-shells';
+export default function Page() {
+ return
+ 조회만 지원하며, 편집·승인·삭제 등 변이 동작은 M5 단계에서 활성화됩니다.
+ ;
+}
diff --git a/next-app/apps/web/src/app/admin/layout.tsx b/next-app/apps/web/src/app/admin/layout.tsx
new file mode 100644
index 0000000..a456b3a
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/layout.tsx
@@ -0,0 +1,36 @@
+import { redirect } from 'next/navigation';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export const dynamic = 'force-dynamic';
+
+const TABS = [
+ { label: '대시보드', href: '/admin' },
+ { label: '회원 관리', href: '/admin/members' },
+ { label: '게시판 관리', href: '/admin/boards' },
+ { label: '베팅 관리', href: '/admin/betting' },
+ { label: '룰렛/복권', href: '/admin/games' },
+ { label: '포인트 정책', href: '/admin/points' },
+ { label: '메뉴 관리', href: '/admin/menu' },
+ { label: '권한', href: '/admin/permissions' },
+ { label: '통계', href: '/admin/stats' },
+ { label: '테마', href: '/admin/themes' },
+];
+
+export default async function AdminLayout({ children }: { children: React.ReactNode }) {
+ const user = await getCurrentSiteUser();
+ if (!user) redirect('/login?next=/admin');
+ if ((user.level ?? 0) < 10) redirect('/?error=permission');
+ return (
+
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/admin/members/page.tsx b/next-app/apps/web/src/app/admin/members/page.tsx
new file mode 100644
index 0000000..8be91a9
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/members/page.tsx
@@ -0,0 +1,56 @@
+import { legacySql } from '@slot/db/legacy';
+
+export const dynamic = 'force-dynamic';
+
+export default async function MembersAdmin({ searchParams }: { searchParams: Promise<{ q?: string; page?: string }> }) {
+ const sp = await searchParams;
+ const q = (sp.q ?? '').trim();
+ const page = Math.max(1, parseInt(sp.page ?? '1', 10));
+ const offset = (page - 1) * 30;
+ const rows = await legacySql<{ mb_id: string; mb_nick: string; mb_level: number; mb_point: number; mb_datetime: Date; mb_email: string; mb_today_login: Date | null; mb_intercept_date: string }[]>`
+ SELECT mb_id, mb_nick, mb_level, mb_point, mb_datetime, mb_email, mb_today_login, mb_intercept_date
+ FROM inspection2.g5_member
+ WHERE (${q === ''} OR mb_id ILIKE ${'%' + q + '%'} OR mb_nick ILIKE ${'%' + q + '%'} OR mb_email ILIKE ${'%' + q + '%'})
+ ORDER BY mb_datetime DESC
+ LIMIT 30 OFFSET ${offset}
+ `.catch(() => []);
+ return (
+
+
회원 관리
+
+
+
+ | 아이디 | 닉네임 | 레벨 | 포인트 | 이메일 | 가입일 | 최근접속 | 상태 | 관리 |
+
+
+ {rows.map((r) => (
+
+ | {r.mb_id} |
+ {r.mb_nick} |
+ {r.mb_level} |
+ {(r.mb_point ?? 0).toLocaleString()}p |
+ {r.mb_email} |
+ {r.mb_datetime ? new Date(r.mb_datetime).toISOString().slice(0,10) : '-'} |
+ {r.mb_today_login ? new Date(r.mb_today_login).toISOString().slice(0,10) : '-'} |
+ {r.mb_intercept_date ? 차단 : 정상} |
+
+ 상세
+ 수정
+ |
+
+ ))}
+
+
+
{rows.length}명 표시 (페이지 {page})
+
+
+ );
+}
+const th: React.CSSProperties = { padding: 10, textAlign: 'left', fontWeight: 700 };
+const td: React.CSSProperties = { padding: 10 };
+const btn = (): React.CSSProperties => ({ display: 'inline-block', padding: '4px 10px', background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', textDecoration: 'none', color: 'var(--color-text)', borderRadius: 3, fontSize: 12 });
diff --git a/next-app/apps/web/src/app/admin/menu/page.tsx b/next-app/apps/web/src/app/admin/menu/page.tsx
new file mode 100644
index 0000000..96fda9c
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/menu/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage } from '@/lib/page-shells';
+export default function Page() {
+ return
+ 조회만 지원하며, 편집·승인·삭제 등 변이 동작은 M5 단계에서 활성화됩니다.
+ ;
+}
diff --git a/next-app/apps/web/src/app/admin/page.tsx b/next-app/apps/web/src/app/admin/page.tsx
new file mode 100644
index 0000000..bd940ca
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/page.tsx
@@ -0,0 +1,45 @@
+import { legacySql } from '@slot/db/legacy';
+
+export const dynamic = 'force-dynamic';
+
+async function safeCount(query: () => Promise<{ c: string }[]>): Promise
{
+ const r = await query().catch(() => []);
+ return Number(r[0]?.c ?? 0);
+}
+
+export default async function AdminDashboard() {
+ const [members, posts, comments, points, today] = await Promise.all([
+ safeCount(() => legacySql`SELECT COUNT(*)::text AS c FROM inspection2.g5_member`),
+ safeCount(() => legacySql`SELECT COUNT(*)::text AS c FROM inspection2.g5_write_free WHERE wr_is_comment = 0`),
+ safeCount(() => legacySql`SELECT COUNT(*)::text AS c FROM inspection2.g5_write_free WHERE wr_is_comment = 1`),
+ safeCount(() => legacySql`SELECT COALESCE(SUM(po_point),0)::text AS c FROM inspection2.g5_point`),
+ safeCount(() => legacySql`SELECT COUNT(*)::text AS c FROM inspection2.g5_visit WHERE vi_date >= CURRENT_DATE`),
+ ]);
+ return (
+
+
관리자 대시보드
+
+
+
+
+
+
+
+
최근 작업 빠른 액세스
+
+ {['새 회원 검토', '신고된 글 처리', '환전 신청 처리', '복권 추첨 트리거', '랭킹 업데이트', 'KICK 방송 상태'].map((l) => (
+
+ ))}
+
+
+ );
+}
+
+function Card({ label, value, sub, color }: { label: string; value: string; sub: string; color: string }) {
+ return (
+
+
{label}
+
{value} {sub}
+
+ );
+}
diff --git a/next-app/apps/web/src/app/admin/permissions/page.tsx b/next-app/apps/web/src/app/admin/permissions/page.tsx
new file mode 100644
index 0000000..875d45f
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/permissions/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage } from '@/lib/page-shells';
+export default function Page() {
+ return
+ 조회만 지원하며, 편집·승인·삭제 등 변이 동작은 M5 단계에서 활성화됩니다.
+ ;
+}
diff --git a/next-app/apps/web/src/app/admin/points/page.tsx b/next-app/apps/web/src/app/admin/points/page.tsx
new file mode 100644
index 0000000..fcd4597
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/points/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage } from '@/lib/page-shells';
+export default function Page() {
+ return
+ 조회만 지원하며, 편집·승인·삭제 등 변이 동작은 M5 단계에서 활성화됩니다.
+ ;
+}
diff --git a/next-app/apps/web/src/app/admin/stats/page.tsx b/next-app/apps/web/src/app/admin/stats/page.tsx
new file mode 100644
index 0000000..a9a2701
--- /dev/null
+++ b/next-app/apps/web/src/app/admin/stats/page.tsx
@@ -0,0 +1,49 @@
+import { legacySql } from '@slot/db/legacy';
+
+export const dynamic = 'force-dynamic';
+
+export default async function StatsPage() {
+ const [byDay, byBoard, levelHist] = await Promise.all([
+ legacySql<{ d: string; c: string }[]>`
+ SELECT to_char(vi_date, 'YYYY-MM-DD') AS d, COUNT(*)::text AS c
+ FROM inspection2.g5_visit
+ WHERE vi_date >= CURRENT_DATE - INTERVAL '14 days'
+ GROUP BY 1 ORDER BY 1
+ `.catch(() => []),
+ legacySql<{ bo_table: string; bo_subject: string; bo_count_write: number; bo_count_comment: number }[]>`
+ SELECT bo_table, bo_subject, bo_count_write, bo_count_comment
+ FROM inspection2.g5_board ORDER BY bo_count_write DESC NULLS LAST LIMIT 10
+ `.catch(() => []),
+ legacySql<{ mb_level: number; c: string }[]>`
+ SELECT mb_level, COUNT(*)::text AS c FROM inspection2.g5_member GROUP BY 1 ORDER BY 1 DESC
+ `.catch(() => []),
+ ]);
+ const maxVisit = Math.max(1, ...byDay.map((r) => Number(r.c)));
+ return (
+
+
통계
+
최근 14일 방문자 수
+
+
+ {byDay.map((r) => (
+
+ ))}
+
+
+ {byDay.map((r) => {r.d.slice(5)})}
+
+
+
인기 게시판 Top 10
+
+ | 게시판 | 글 | 댓글 |
+ {byBoard.map((b) => {b.bo_subject} /{b.bo_table} | {b.bo_count_write?.toLocaleString()} | {b.bo_count_comment?.toLocaleString()} |
)}
+
+
회원 레벨 분포
+
+ {levelHist.map((l) => Lv.{l.mb_level} {Number(l.c).toLocaleString()})}
+
+
+ );
+}
+const th: React.CSSProperties = { padding: 10, textAlign: 'left', fontWeight: 700 };
+const td: React.CSSProperties = { padding: 10 };
diff --git a/next-app/apps/web/src/app/api/auth/register/route.ts b/next-app/apps/web/src/app/api/auth/register/route.ts
new file mode 100644
index 0000000..59439dc
--- /dev/null
+++ b/next-app/apps/web/src/app/api/auth/register/route.ts
@@ -0,0 +1,47 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { db, members } from '@slot/db';
+import { eq } from 'drizzle-orm';
+import { hashPassword } from '@slot/auth/password';
+import { createSession, SESSION_COOKIE } from '@slot/auth';
+
+export async function POST(req: NextRequest) {
+ const form = await req.formData();
+ const loginId = String(form.get('loginId') ?? '').trim();
+ const nick = String(form.get('nick') ?? '').trim();
+ const password = String(form.get('password') ?? '');
+ const password2 = String(form.get('password2') ?? '');
+ const email = String(form.get('email') ?? '').trim() || null;
+
+ if (!/^[a-zA-Z0-9_]{3,20}$/.test(loginId)) return back(req, '아이디는 영문/숫자 3-20자입니다.');
+ if (nick.length < 2 || nick.length > 20) return back(req, '닉네임은 2-20자입니다.');
+ if (password.length < 6) return back(req, '비밀번호는 6자 이상입니다.');
+ if (password !== password2) return back(req, '비밀번호가 일치하지 않습니다.');
+
+ const exists = await db.select().from(members).where(eq(members.loginId, loginId)).limit(1);
+ if (exists[0]) return back(req, '이미 사용 중인 아이디입니다.');
+
+ const hash = await hashPassword(password);
+ const [created] = await db.insert(members).values({
+ legacyMbId: loginId,
+ loginId,
+ passwordHash: hash,
+ passwordAlgo: 'scrypt',
+ nick,
+ email: email ?? undefined,
+ level: 2,
+ }).returning();
+
+ if (!created) return back(req, '가입 중 오류가 발생했습니다.');
+
+ const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? undefined;
+ const session = await createSession(created.id, { ip, userAgent: req.headers.get('user-agent') ?? undefined });
+ const res = NextResponse.redirect(new URL('/', req.url), { status: 303 });
+ res.cookies.set(SESSION_COOKIE, session.id, { httpOnly: true, sameSite: 'lax', path: '/', expires: session.expiresAt });
+ return res;
+}
+
+function back(req: NextRequest, msg: string) {
+ const url = new URL('/register', req.url);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/[postId]/bad/route.ts b/next-app/apps/web/src/app/api/posts/[postId]/bad/route.ts
new file mode 100644
index 0000000..b74d5e6
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/[postId]/bad/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { recommendPost, scrapPost, reportPost, softDeletePost, findPostBoard } from '@/lib/post-actions';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest, ctx: { params: Promise<{ postId: string }> }) {
+ const { postId } = await ctx.params;
+ const user = await getCurrentSiteUser();
+ if (!user) return back(req, '로그인이 필요합니다');
+ const url = new URL(req.url);
+ let boardSlug = url.searchParams.get('board');
+ if (!boardSlug) {
+ const found = await findPostBoard(postId);
+ boardSlug = found?.slug ?? null;
+ }
+ if (!boardSlug) return back(req, '게시글을 찾을 수 없습니다');
+ let result: { ok: boolean; error?: string };
+ switch ('bad') {
+ case 'good': result = await recommendPost(boardSlug, postId, user, 'G'); break;
+ case 'bad': result = await recommendPost(boardSlug, postId, user, 'N'); break;
+ case 'scrap': result = await scrapPost(boardSlug, postId, user); break;
+ case 'report': result = await reportPost(boardSlug, postId, user); break;
+ case 'delete': result = await softDeletePost(boardSlug, postId, user); break;
+ default: result = { ok: false, error: 'unknown_action' };
+ }
+ if (!result.ok && result.error) return back(req, result.error);
+ return NextResponse.redirect(new URL(`/${boardSlug}/${postId}?action=bad-ok`, req.url), { status: 303 });
+}
+
+function back(req: NextRequest, msg: string) {
+ const ref = req.headers.get('referer') ?? '/';
+ const url = new URL(ref);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/[postId]/comment/route.ts b/next-app/apps/web/src/app/api/posts/[postId]/comment/route.ts
new file mode 100644
index 0000000..728048e
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/[postId]/comment/route.ts
@@ -0,0 +1,28 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { addComment, findPostBoard } from '@/lib/post-actions';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest, ctx: { params: Promise<{ postId: string }> }) {
+ const { postId } = await ctx.params;
+ const user = await getCurrentSiteUser();
+ if (!user) return NextResponse.redirect(new URL('/login', req.url), { status: 303 });
+ const form = await req.formData();
+ const content = String(form.get('content') ?? '').trim();
+ const slugParam = String(form.get('boardSlug') ?? '').trim() || null;
+ if (!content) return back(req, '내용을 입력하세요');
+
+ const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? '127.0.0.1';
+ const slug = slugParam ?? (await findPostBoard(postId))?.slug;
+ if (!slug) return back(req, '게시글을 찾을 수 없습니다');
+
+ const result = await addComment(slug, postId, user, content, ip);
+ if (!result.ok) return back(req, result.error ?? '실패');
+ return NextResponse.redirect(new URL(`/${slug}/${postId}#comment-${result.commentId}`, req.url), { status: 303 });
+}
+
+function back(req: NextRequest, msg: string) {
+ const ref = req.headers.get('referer') ?? '/';
+ const url = new URL(ref);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/[postId]/delete/route.ts b/next-app/apps/web/src/app/api/posts/[postId]/delete/route.ts
new file mode 100644
index 0000000..fd8d57d
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/[postId]/delete/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { recommendPost, scrapPost, reportPost, softDeletePost, findPostBoard } from '@/lib/post-actions';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest, ctx: { params: Promise<{ postId: string }> }) {
+ const { postId } = await ctx.params;
+ const user = await getCurrentSiteUser();
+ if (!user) return back(req, '로그인이 필요합니다');
+ const url = new URL(req.url);
+ let boardSlug = url.searchParams.get('board');
+ if (!boardSlug) {
+ const found = await findPostBoard(postId);
+ boardSlug = found?.slug ?? null;
+ }
+ if (!boardSlug) return back(req, '게시글을 찾을 수 없습니다');
+ let result: { ok: boolean; error?: string };
+ switch ('delete') {
+ case 'good': result = await recommendPost(boardSlug, postId, user, 'G'); break;
+ case 'bad': result = await recommendPost(boardSlug, postId, user, 'N'); break;
+ case 'scrap': result = await scrapPost(boardSlug, postId, user); break;
+ case 'report': result = await reportPost(boardSlug, postId, user); break;
+ case 'delete': result = await softDeletePost(boardSlug, postId, user); break;
+ default: result = { ok: false, error: 'unknown_action' };
+ }
+ if (!result.ok && result.error) return back(req, result.error);
+ return NextResponse.redirect(new URL(`/${boardSlug}/${postId}?action=delete-ok`, req.url), { status: 303 });
+}
+
+function back(req: NextRequest, msg: string) {
+ const ref = req.headers.get('referer') ?? '/';
+ const url = new URL(ref);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/[postId]/good/route.ts b/next-app/apps/web/src/app/api/posts/[postId]/good/route.ts
new file mode 100644
index 0000000..77a3974
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/[postId]/good/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { recommendPost, scrapPost, reportPost, softDeletePost, findPostBoard } from '@/lib/post-actions';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest, ctx: { params: Promise<{ postId: string }> }) {
+ const { postId } = await ctx.params;
+ const user = await getCurrentSiteUser();
+ if (!user) return back(req, '로그인이 필요합니다');
+ const url = new URL(req.url);
+ let boardSlug = url.searchParams.get('board');
+ if (!boardSlug) {
+ const found = await findPostBoard(postId);
+ boardSlug = found?.slug ?? null;
+ }
+ if (!boardSlug) return back(req, '게시글을 찾을 수 없습니다');
+ let result: { ok: boolean; error?: string };
+ switch ('good') {
+ case 'good': result = await recommendPost(boardSlug, postId, user, 'G'); break;
+ case 'bad': result = await recommendPost(boardSlug, postId, user, 'N'); break;
+ case 'scrap': result = await scrapPost(boardSlug, postId, user); break;
+ case 'report': result = await reportPost(boardSlug, postId, user); break;
+ case 'delete': result = await softDeletePost(boardSlug, postId, user); break;
+ default: result = { ok: false, error: 'unknown_action' };
+ }
+ if (!result.ok && result.error) return back(req, result.error);
+ return NextResponse.redirect(new URL(`/${boardSlug}/${postId}?action=good-ok`, req.url), { status: 303 });
+}
+
+function back(req: NextRequest, msg: string) {
+ const ref = req.headers.get('referer') ?? '/';
+ const url = new URL(ref);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/[postId]/report/route.ts b/next-app/apps/web/src/app/api/posts/[postId]/report/route.ts
new file mode 100644
index 0000000..576491a
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/[postId]/report/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { recommendPost, scrapPost, reportPost, softDeletePost, findPostBoard } from '@/lib/post-actions';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest, ctx: { params: Promise<{ postId: string }> }) {
+ const { postId } = await ctx.params;
+ const user = await getCurrentSiteUser();
+ if (!user) return back(req, '로그인이 필요합니다');
+ const url = new URL(req.url);
+ let boardSlug = url.searchParams.get('board');
+ if (!boardSlug) {
+ const found = await findPostBoard(postId);
+ boardSlug = found?.slug ?? null;
+ }
+ if (!boardSlug) return back(req, '게시글을 찾을 수 없습니다');
+ let result: { ok: boolean; error?: string };
+ switch ('report') {
+ case 'good': result = await recommendPost(boardSlug, postId, user, 'G'); break;
+ case 'bad': result = await recommendPost(boardSlug, postId, user, 'N'); break;
+ case 'scrap': result = await scrapPost(boardSlug, postId, user); break;
+ case 'report': result = await reportPost(boardSlug, postId, user); break;
+ case 'delete': result = await softDeletePost(boardSlug, postId, user); break;
+ default: result = { ok: false, error: 'unknown_action' };
+ }
+ if (!result.ok && result.error) return back(req, result.error);
+ return NextResponse.redirect(new URL(`/${boardSlug}/${postId}?action=report-ok`, req.url), { status: 303 });
+}
+
+function back(req: NextRequest, msg: string) {
+ const ref = req.headers.get('referer') ?? '/';
+ const url = new URL(ref);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/[postId]/scrap/route.ts b/next-app/apps/web/src/app/api/posts/[postId]/scrap/route.ts
new file mode 100644
index 0000000..6a3c3e1
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/[postId]/scrap/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { recommendPost, scrapPost, reportPost, softDeletePost, findPostBoard } from '@/lib/post-actions';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest, ctx: { params: Promise<{ postId: string }> }) {
+ const { postId } = await ctx.params;
+ const user = await getCurrentSiteUser();
+ if (!user) return back(req, '로그인이 필요합니다');
+ const url = new URL(req.url);
+ let boardSlug = url.searchParams.get('board');
+ if (!boardSlug) {
+ const found = await findPostBoard(postId);
+ boardSlug = found?.slug ?? null;
+ }
+ if (!boardSlug) return back(req, '게시글을 찾을 수 없습니다');
+ let result: { ok: boolean; error?: string };
+ switch ('scrap') {
+ case 'good': result = await recommendPost(boardSlug, postId, user, 'G'); break;
+ case 'bad': result = await recommendPost(boardSlug, postId, user, 'N'); break;
+ case 'scrap': result = await scrapPost(boardSlug, postId, user); break;
+ case 'report': result = await reportPost(boardSlug, postId, user); break;
+ case 'delete': result = await softDeletePost(boardSlug, postId, user); break;
+ default: result = { ok: false, error: 'unknown_action' };
+ }
+ if (!result.ok && result.error) return back(req, result.error);
+ return NextResponse.redirect(new URL(`/${boardSlug}/${postId}?action=scrap-ok`, req.url), { status: 303 });
+}
+
+function back(req: NextRequest, msg: string) {
+ const ref = req.headers.get('referer') ?? '/';
+ const url = new URL(ref);
+ url.searchParams.set('error', msg);
+ return NextResponse.redirect(url, { status: 303 });
+}
diff --git a/next-app/apps/web/src/app/api/posts/create/route.ts b/next-app/apps/web/src/app/api/posts/create/route.ts
new file mode 100644
index 0000000..abb3d1f
--- /dev/null
+++ b/next-app/apps/web/src/app/api/posts/create/route.ts
@@ -0,0 +1,37 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { legacySql } from '@slot/db/legacy';
+import { getCurrentSiteUser } from '@/lib/page-data';
+
+export async function POST(req: NextRequest) {
+ const user = await getCurrentSiteUser();
+ if (!user) return NextResponse.redirect(new URL('/login', req.url), { status: 303 });
+ const form = await req.formData();
+ const boardSlug = String(form.get('boardSlug') ?? '').replace(/[^a-z0-9_]/gi, '');
+ const subject = String(form.get('subject') ?? '').slice(0, 200);
+ const content = String(form.get('content') ?? '');
+ const isSecret = form.get('isSecret') === 'on';
+ if (!boardSlug || !subject || !content) {
+ return NextResponse.redirect(new URL(`/${boardSlug}/write?error=invalid`, req.url), { status: 303 });
+ }
+ const tbl = `inspection2.g5_write_${boardSlug}`;
+ const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? '127.0.0.1';
+ try {
+ const ins = await legacySql<{ wr_id: number }[]>`
+ INSERT INTO ${legacySql(tbl)}
+ (wr_num, wr_reply, wr_parent, wr_is_comment, wr_comment, wr_subject, wr_content, wr_link1, wr_link2, wr_hit, wr_good, wr_nogood, mb_id, wr_password, wr_name, wr_email, wr_homepage, wr_datetime, wr_last, wr_ip, wr_facebook_user, wr_twitter_user, wr_option)
+ VALUES (
+ (-1 * COALESCE((SELECT MIN(wr_num) FROM ${legacySql(tbl)})::int, 0) - 1),
+ '', 0, 0, 0,
+ ${subject}, ${content}, '', '', 0, 0, 0,
+ ${user.loginId}, '', ${user.nick}, '', '',
+ NOW(), NOW(), ${ip}, '', '', ${isSecret ? 'secret' : ''}
+ )
+ RETURNING wr_id
+ `;
+ const wrId = ins[0]?.wr_id;
+ return NextResponse.redirect(new URL(`/${boardSlug}/${wrId}`, req.url), { status: 303 });
+ } catch (e: any) {
+ console.error('create post failed', e);
+ return NextResponse.redirect(new URL(`/${boardSlug}/write?error=${encodeURIComponent(e.message ?? 'fail')}`, req.url), { status: 303 });
+ }
+}
diff --git a/next-app/apps/web/src/app/api/ui/dark-mode/route.ts b/next-app/apps/web/src/app/api/ui/dark-mode/route.ts
new file mode 100644
index 0000000..bc2a605
--- /dev/null
+++ b/next-app/apps/web/src/app/api/ui/dark-mode/route.ts
@@ -0,0 +1,9 @@
+import { NextRequest, NextResponse } from 'next/server';
+export async function POST(req: NextRequest) {
+ const c = req.cookies.get('slot_dark')?.value;
+ const next = c === '1' ? '0' : '1';
+ const back = req.headers.get('referer') ?? '/';
+ const res = NextResponse.redirect(back, { status: 303 });
+ res.cookies.set('slot_dark', next, { path: '/', maxAge: 60 * 60 * 24 * 365 });
+ return res;
+}
diff --git a/next-app/apps/web/src/app/auth/recover/page.tsx b/next-app/apps/web/src/app/auth/recover/page.tsx
new file mode 100644
index 0000000..c4f8918
--- /dev/null
+++ b/next-app/apps/web/src/app/auth/recover/page.tsx
@@ -0,0 +1,31 @@
+export default function RecoverPage() {
+ return (
+
+
아이디 / 비밀번호 찾기
+
+
+
+
+
+ ※ 본 기능은 아직 미연동 상태입니다 (M2 단계). 이메일 발송은 운영 SMTP 또는 Resend 연동 후 활성화됩니다.
+
+
+ );
+}
+const card = (): React.CSSProperties => ({ background: 'var(--color-bgSurface)', border: '1px solid var(--color-border)', borderRadius: 8, padding: 16 });
+const ch = (): React.CSSProperties => ({ fontSize: 16, margin: '0 0 12px', borderBottom: '2px solid var(--color-primary)', paddingBottom: 6 });
+const inp = (): React.CSSProperties => ({ padding: '8px 10px', border: '1px solid var(--color-border)', borderRadius: 4, fontSize: 14 });
+const btn = (): React.CSSProperties => ({ background: 'var(--color-primary)', color: '#fff', border: 'none', padding: '10px', borderRadius: 4, fontWeight: 700, cursor: 'pointer' });
diff --git a/next-app/apps/web/src/app/bookmarks/page.tsx b/next-app/apps/web/src/app/bookmarks/page.tsx
new file mode 100644
index 0000000..0c442c8
--- /dev/null
+++ b/next-app/apps/web/src/app/bookmarks/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage } from '@/lib/page-shells';
+export default function Page() {
+ return
+ 북마크 기능은 회원 로그인 후 사용 가능합니다.
+ ;
+}
diff --git a/next-app/apps/web/src/app/complaint/page.tsx b/next-app/apps/web/src/app/complaint/page.tsx
new file mode 100644
index 0000000..bc953ea
--- /dev/null
+++ b/next-app/apps/web/src/app/complaint/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/event/page.tsx b/next-app/apps/web/src/app/event/page.tsx
new file mode 100644
index 0000000..bc42c19
--- /dev/null
+++ b/next-app/apps/web/src/app/event/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/fakesite/page.tsx b/next-app/apps/web/src/app/fakesite/page.tsx
new file mode 100644
index 0000000..b4f5d73
--- /dev/null
+++ b/next-app/apps/web/src/app/fakesite/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/games/bacara/page.tsx b/next-app/apps/web/src/app/games/bacara/page.tsx
new file mode 100644
index 0000000..06813ac
--- /dev/null
+++ b/next-app/apps/web/src/app/games/bacara/page.tsx
@@ -0,0 +1,16 @@
+import { StubPage, FeatureGrid } from '@/lib/page-shells';
+export default function Page() {
+ return (
+
+
+
🃏
+
실시간 게임 클라이언트는 M3 단계에서 React + WebSocket 으로 구현됩니다.
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/games/fivetreasures/page.tsx b/next-app/apps/web/src/app/games/fivetreasures/page.tsx
new file mode 100644
index 0000000..deffcab
--- /dev/null
+++ b/next-app/apps/web/src/app/games/fivetreasures/page.tsx
@@ -0,0 +1,16 @@
+import { StubPage, FeatureGrid } from '@/lib/page-shells';
+export default function Page() {
+ return (
+
+
+
💎
+
실시간 게임 클라이언트는 M3 단계에서 React + WebSocket 으로 구현됩니다.
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/games/fortunes/page.tsx b/next-app/apps/web/src/app/games/fortunes/page.tsx
new file mode 100644
index 0000000..bbebf02
--- /dev/null
+++ b/next-app/apps/web/src/app/games/fortunes/page.tsx
@@ -0,0 +1,16 @@
+import { StubPage, FeatureGrid } from '@/lib/page-shells';
+export default function Page() {
+ return (
+
+
+
🎰
+
실시간 게임 클라이언트는 M3 단계에서 React + WebSocket 으로 구현됩니다.
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/games/ranking/page.tsx b/next-app/apps/web/src/app/games/ranking/page.tsx
new file mode 100644
index 0000000..f244e65
--- /dev/null
+++ b/next-app/apps/web/src/app/games/ranking/page.tsx
@@ -0,0 +1,25 @@
+import { StubPage } from '@/lib/page-shells';
+import { getMemberRankings } from '@/lib/page-data';
+export const dynamic = 'force-dynamic';
+export default async function Page() {
+ const ranks = await getMemberRankings();
+ return (
+
+
+
+ | 순위 | 닉네임 | 레벨 | 포인트 |
+
+
+ {ranks.map((r) => (
+
+ | {r.rank <= 3 ? ['🥇','🥈','🥉'][r.rank-1] : r.rank} |
+ {r.nick} |
+ Lv.{r.level} |
+ {r.point.toLocaleString()}p |
+
+ ))}
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/games/roulette/page.tsx b/next-app/apps/web/src/app/games/roulette/page.tsx
new file mode 100644
index 0000000..2518f94
--- /dev/null
+++ b/next-app/apps/web/src/app/games/roulette/page.tsx
@@ -0,0 +1,16 @@
+import { StubPage, FeatureGrid } from '@/lib/page-shells';
+export default function Page() {
+ return (
+
+
+
🎡
+
실시간 게임 클라이언트는 M3 단계에서 React + WebSocket 으로 구현됩니다.
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/games/slot/page.tsx b/next-app/apps/web/src/app/games/slot/page.tsx
new file mode 100644
index 0000000..d1f3038
--- /dev/null
+++ b/next-app/apps/web/src/app/games/slot/page.tsx
@@ -0,0 +1,16 @@
+import { StubPage, FeatureGrid } from '@/lib/page-shells';
+export default function Page() {
+ return (
+
+
+
🎲
+
실시간 게임 클라이언트는 M3 단계에서 React + WebSocket 으로 구현됩니다.
+
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/gift_coupons/page.tsx b/next-app/apps/web/src/app/gift_coupons/page.tsx
new file mode 100644
index 0000000..aa30c56
--- /dev/null
+++ b/next-app/apps/web/src/app/gift_coupons/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/gift_exchanges/page.tsx b/next-app/apps/web/src/app/gift_exchanges/page.tsx
new file mode 100644
index 0000000..76b05cd
--- /dev/null
+++ b/next-app/apps/web/src/app/gift_exchanges/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/guarantee/apply/page.tsx b/next-app/apps/web/src/app/guarantee/apply/page.tsx
new file mode 100644
index 0000000..498414a
--- /dev/null
+++ b/next-app/apps/web/src/app/guarantee/apply/page.tsx
@@ -0,0 +1,8 @@
+import { StubPage } from '@/lib/page-shells';
+export default function Page() {
+ return (
+
+ ✈️ @slotlifeCS 텔레그램 열기
+
+ );
+}
diff --git a/next-app/apps/web/src/app/guarantee/page.tsx b/next-app/apps/web/src/app/guarantee/page.tsx
new file mode 100644
index 0000000..18561ee
--- /dev/null
+++ b/next-app/apps/web/src/app/guarantee/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/guide/community/page.tsx b/next-app/apps/web/src/app/guide/community/page.tsx
new file mode 100644
index 0000000..a4a8439
--- /dev/null
+++ b/next-app/apps/web/src/app/guide/community/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage, StaticContent } from '@/lib/page-shells';
+export default function Page() {
+ return
+
+ ;
+}
diff --git a/next-app/apps/web/src/app/guide/mukti/page.tsx b/next-app/apps/web/src/app/guide/mukti/page.tsx
new file mode 100644
index 0000000..9f93a84
--- /dev/null
+++ b/next-app/apps/web/src/app/guide/mukti/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage, StaticContent } from '@/lib/page-shells';
+export default function Page() {
+ return
+
+ ;
+}
diff --git a/next-app/apps/web/src/app/guide/page.tsx b/next-app/apps/web/src/app/guide/page.tsx
new file mode 100644
index 0000000..0bb8dd2
--- /dev/null
+++ b/next-app/apps/web/src/app/guide/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage, StaticContent } from '@/lib/page-shells';
+export default function Page() {
+ return
+
+ ;
+}
diff --git a/next-app/apps/web/src/app/guide/pointgame/page.tsx b/next-app/apps/web/src/app/guide/pointgame/page.tsx
new file mode 100644
index 0000000..809d9d6
--- /dev/null
+++ b/next-app/apps/web/src/app/guide/pointgame/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage, StaticContent } from '@/lib/page-shells';
+export default function Page() {
+ return
+
+ ;
+}
diff --git a/next-app/apps/web/src/app/guide/tv/page.tsx b/next-app/apps/web/src/app/guide/tv/page.tsx
new file mode 100644
index 0000000..1e08ded
--- /dev/null
+++ b/next-app/apps/web/src/app/guide/tv/page.tsx
@@ -0,0 +1,6 @@
+import { StubPage, StaticContent } from '@/lib/page-shells';
+export default function Page() {
+ return
+
+ ;
+}
diff --git a/next-app/apps/web/src/app/help/faq/page.tsx b/next-app/apps/web/src/app/help/faq/page.tsx
new file mode 100644
index 0000000..112a23c
--- /dev/null
+++ b/next-app/apps/web/src/app/help/faq/page.tsx
@@ -0,0 +1,22 @@
+import { StubPage } from '@/lib/page-shells';
+const FAQ = [
+ { q: '회원가입은 어떻게 하나요?', a: '오른쪽 상단의 [회원가입] 버튼을 누르고 안내에 따라 가입하시면 됩니다.' },
+ { q: '포인트는 어떻게 적립되나요?', a: '글쓰기, 댓글, 출석체크, 게임 참여 등 활동 시 자동 적립됩니다.' },
+ { q: '먹튀신고는 어디서 하나요?', a: '[먹튀사이트] → [먹튀신고] 메뉴에서 신고하실 수 있습니다.' },
+ { q: '슬생복권 추첨 시간은 언제인가요?', a: '매일 오후 9시에 자동 추첨됩니다.' },
+ { q: '포인트 환전은 얼마부터 가능한가요?', a: '최소 50,000P 부터 환전 신청 가능합니다.' },
+];
+export default function FaqPage() {
+ return (
+
+
+ {FAQ.map((f, i) => (
+
+ Q. {f.q}
+ A. {f.a}
+
+ ))}
+
+
+ );
+}
diff --git a/next-app/apps/web/src/app/help/qa/page.tsx b/next-app/apps/web/src/app/help/qa/page.tsx
new file mode 100644
index 0000000..2785069
--- /dev/null
+++ b/next-app/apps/web/src/app/help/qa/page.tsx
@@ -0,0 +1,24 @@
+import { StubPage, FeatureGrid } from '@/lib/page-shells';
+import { getCurrentSiteUser } from '@/lib/page-data';
+export const dynamic = 'force-dynamic';
+export default async function QaPage() {
+ const user = await getCurrentSiteUser();
+ return (
+
+ {!user && 문의는 로그인 후 작성하실 수 있습니다.
}
+
+ {user && }
+
+ );
+}
+const th: React.CSSProperties = { padding: '10px', fontWeight: 700, textAlign: 'center' };
+const tr: React.CSSProperties = { borderBottom: '1px solid var(--color-border)' };
+const td: React.CSSProperties = { padding: '10px', textAlign: 'center' };
diff --git a/next-app/apps/web/src/app/inspection/page.tsx b/next-app/apps/web/src/app/inspection/page.tsx
new file mode 100644
index 0000000..a68518c
--- /dev/null
+++ b/next-app/apps/web/src/app/inspection/page.tsx
@@ -0,0 +1,5 @@
+import BoardPage from '@/app/[boardSlug]/page';
+export const dynamic = 'force-dynamic';
+export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
+ return ;
+}
diff --git a/next-app/apps/web/src/app/layout.tsx b/next-app/apps/web/src/app/layout.tsx
index 9d5ff9c..2667bec 100644
--- a/next-app/apps/web/src/app/layout.tsx
+++ b/next-app/apps/web/src/app/layout.tsx
@@ -1,30 +1,47 @@
import type { Metadata } from 'next';
-import { headers } from 'next/headers';
+import { cookies } from 'next/headers';
import { getThemeForPath } from '@/lib/theme';
-import { tokensToCssVars } from '@slot/themes';
-import { getCurrentMember } from '@/lib/session';
+import { tokensToCssVars, type ThemeId } from '@slot/themes';
+import { getCurrentSiteUser, getCurrentPathname, getPopularTags, getMemberRankings, MEGA_MENUS } from '@/lib/page-data';
export const metadata: Metadata = {
- title: 'Slot - 슬롯 커뮤니티',
- description: '안전 슬롯사이트 추천 및 슬롯커뮤니티',
+ title: '슬생닷컴 | 안전 슬롯사이트 추천 및 슬롯커뮤니티',
+ description: '실시간 먹튀검증과 슬롯사이트 추천을 제공하는 안전 슬롯 커뮤니티',
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
- const h = await headers();
- const pathname = h.get('x-pathname') ?? '/';
- const member = await getCurrentMember();
- const theme = await getThemeForPath(pathname, member?.themePref as any);
+ const pathname = await getCurrentPathname();
+ const c = await cookies();
+ const cookieTheme = c.get('slot_theme_pref')?.value as ThemeId | undefined;
+ const user = await getCurrentSiteUser();
+ const theme = await getThemeForPath(pathname, cookieTheme ?? (user?.themePref as ThemeId | undefined));
+ const [popularTags, rankings] = await Promise.all([getPopularTags(), getMemberRankings()]);
const Root = theme.layouts.root;
+ const cssVars = tokensToCssVars(theme);
+ const isDark = c.get('slot_dark')?.value === '1';
+
+ const hideEverything = pathname === '/login';
+ const hideSidebar = pathname.startsWith('/admin') || pathname.startsWith('/mypage') || pathname === '/login' || pathname === '/register';
+ const showHeader = !hideEverything;
+ const showFooter = !hideEverything;
+
return (
-
+
-
+