프로젝트 기본 구성: 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>
이 커밋은 다음에 포함됨:
kjs
2026-08-12 17:01:55 +09:00
co-authored by Claude Fable 5
커밋 f695b583e4
24개의 변경된 파일과 7365개의 추가작업 그리고 0개의 파일을 삭제
+27
파일 보기
@@ -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

+26
파일 보기
@@ -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;
}
+29
파일 보기
@@ -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>
);
}
+16
파일 보기
@@ -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[];
}