From 9026a471196e95a885bdd71553ac5713c40c8ffe Mon Sep 17 00:00:00 2001 From: 2620227 Date: Wed, 1 Jul 2026 14:04:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=A9=EB=AA=85=EB=A1=9D(guestbook)?= =?UTF-8?q?=20=EC=9B=B9=EC=82=AC=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/db.js: initGuestbook/listMessages/addMessage (본인 schema에 테이블 생성) - src/server.js: express.json + public 정적 서빙, GET/POST /api/messages, 시작 시 테이블 자동 생성 - public/index.html: 작성 폼 + 목록 UI (XSS 이스케이프, 길이 검증) Co-Authored-By: Claude Opus 4.8 --- public/index.html | 122 ++++++++++++++++++++++++++++++++++++++++++++++ src/db.js | 29 +++++++++++ src/server.js | 49 ++++++++++++++++--- 3 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 public/index.html diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3257912 --- /dev/null +++ b/public/index.html @@ -0,0 +1,122 @@ + + + + + + 방명록 + + + +
+

📖 방명록

+

한 줄 남기고 가세요. (Postgres 에 저장됩니다)

+
+ + +
+ +
+
불러오는 중…
+
+ + + + diff --git a/src/db.js b/src/db.js index 0c5aec4..4db9186 100644 --- a/src/db.js +++ b/src/db.js @@ -13,3 +13,32 @@ export async function pingDb() { ); return rows[0]; } + +// --- 방명록(guestbook) --- +// 테이블은 search_path에 고정된 본인 schema에 생성된다(schema 접두어 불필요). +export async function initGuestbook() { + await pool.query(` + create table if not exists guestbook ( + id bigint generated always as identity primary key, + name text not null, + message text not null, + created_at timestamptz not null default now() + ) + `); +} + +export async function listMessages(limit = 100) { + const { rows } = await pool.query( + "select id, name, message, created_at from guestbook order by id desc limit $1", + [limit] + ); + return rows; +} + +export async function addMessage(name, message) { + const { rows } = await pool.query( + "insert into guestbook (name, message) values ($1, $2) returning id, name, message, created_at", + [name, message] + ); + return rows[0]; +} diff --git a/src/server.js b/src/server.js index e94230a..46a7508 100644 --- a/src/server.js +++ b/src/server.js @@ -1,10 +1,16 @@ -// 최소 Express 서버 — /healthz, /db, /s3 로 연결 확인. +// Express 서버 — 방명록(guestbook) 웹사이트 + 연결 확인용 /healthz, /db, /s3. import express from "express"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; import { config } from "./config.js"; -import { pingDb } from "./db.js"; +import { pingDb, initGuestbook, listMessages, addMessage } from "./db.js"; import { pingS3 } from "./s3.js"; +const __dirname = dirname(fileURLToPath(import.meta.url)); + const app = express(); +app.use(express.json()); +app.use(express.static(join(__dirname, "..", "public"))); app.get("/healthz", (_req, res) => res.json({ ok: true })); @@ -24,10 +30,37 @@ app.get("/s3", async (_req, res) => { } }); -app.get("/", (_req, res) => - res.json({ name: "sample", endpoints: ["/healthz", "/db", "/s3"] }) -); - -app.listen(config.port, "0.0.0.0", () => { - console.log(`sample listening on :${config.port}`); +// --- 방명록 API --- +app.get("/api/messages", async (_req, res) => { + try { + res.json({ ok: true, messages: await listMessages() }); + } catch (e) { + res.status(500).json({ ok: false, error: String(e.message || e) }); + } +}); + +app.post("/api/messages", async (req, res) => { + try { + const name = String(req.body?.name ?? "").trim(); + const message = String(req.body?.message ?? "").trim(); + if (!name || !message) { + return res + .status(400) + .json({ ok: false, error: "name 과 message 는 필수입니다." }); + } + if (name.length > 40 || message.length > 500) { + return res + .status(400) + .json({ ok: false, error: "이름은 40자, 메시지는 500자 이내." }); + } + res.json({ ok: true, message: await addMessage(name, message) }); + } catch (e) { + res.status(500).json({ ok: false, error: String(e.message || e) }); + } +}); + +// 앱 시작 전에 테이블 보장 후 리슨. +await initGuestbook(); +app.listen(config.port, "0.0.0.0", () => { + console.log(`guestbook listening on :${config.port}`); });