프로젝트 기본 구성: Next.js + PostgreSQL + Gitea Actions 배포
- Next.js 15 (App Router, TypeScript, Tailwind), pg 커넥션 풀 - /api/health DB 헬스체크, standalone Dockerfile - Gitea Actions: 빌드 → 컨테이너 교체 → Traefik(jskim.vexplor.com) 라우팅 - 로컬 개발용 SSH DB 터널 스크립트 (scripts/db-tunnel.sh) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { rows } = await db.query("SELECT now() AS now, version() AS version");
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
db: {
|
||||
connected: true,
|
||||
now: rows[0].now,
|
||||
version: rows[0].version,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "error",
|
||||
db: { connected: false },
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
}
|
||||
바이너리 파일은 표시되지 않음.
|
이후 너비: | 높이: | 크기: 25 KiB |
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center gap-4 p-8">
|
||||
<h1 className="text-4xl font-bold">jskim.vexplor.com</h1>
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400">
|
||||
Next.js + PostgreSQL 프로젝트가 준비되었습니다.
|
||||
</p>
|
||||
<a
|
||||
href="/api/health"
|
||||
className="mt-4 rounded-lg border border-gray-300 px-4 py-2 text-sm hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-800"
|
||||
>
|
||||
DB 연결 상태 확인 → /api/health
|
||||
</a>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Pool } from "pg";
|
||||
|
||||
// Next.js 개발 모드의 핫 리로드에서 Pool이 중복 생성되지 않도록 전역에 보관
|
||||
const globalForDb = globalThis as unknown as { pgPool?: Pool };
|
||||
|
||||
export const db =
|
||||
globalForDb.pgPool ??
|
||||
new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 10,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForDb.pgPool = db;
|
||||
}
|
||||
|
||||
export async function query<T extends Record<string, unknown>>(
|
||||
text: string,
|
||||
params?: unknown[],
|
||||
) {
|
||||
const result = await db.query(text, params);
|
||||
return result.rows as T[];
|
||||
}
|
||||
새 이슈에서 참조
사용자 차단