AI DEV portal 초기 커밋

Express+EJS+Postgres 사내 개발 포털.
- Keycloak OIDC(사번 로그인), GitHub 감성 UI
- 사이트 모음 + 프로젝트 공유/코멘트/스타
- DB: OpenEverest appdb portal schema (portal_app role)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0310700
2026-06-22 16:16:40 +09:00
commit 9eb3fd1b4c
18 changed files with 1919 additions and 0 deletions

116
src/server.js Normal file
View File

@@ -0,0 +1,116 @@
import express from "express";
import session from "express-session";
import cookieParser from "cookie-parser";
import { fileURLToPath } from "url";
import path from "path";
import { config } from "./config.js";
import { siteGroups } from "./sites.js";
import { initOidc, loginRedirect, handleCallback, requireAuth } from "./auth.js";
import * as db from "./db.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "..", "views"));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser());
app.use("/public", express.static(path.join(__dirname, "..", "public")));
app.use(
session({
secret: config.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: "lax", maxAge: 8 * 3600 * 1000 },
})
);
// 모든 뷰에 공통 노출
app.use((req, res, next) => {
res.locals.brand = config.brand;
res.locals.user = req.session.user || null;
next();
});
app.get("/healthz", (_req, res) => res.json({ ok: true }));
// --- 인증 ---
app.get("/login", (req, res) => {
if (req.session.user) return res.redirect("/");
res.render("login");
});
app.get("/auth/login", (req, res) => loginRedirect(req, res));
app.get("/auth/callback", async (req, res) => {
try {
const u = await handleCallback(req);
await db.upsertUser(u);
req.session.user = u;
res.redirect("/");
} catch (e) {
console.error("oidc callback error", e);
res.status(500).render("login", { error: "로그인 처리 중 오류가 발생했습니다." });
}
});
app.get("/logout", (req, res) => {
req.session.destroy(() => res.redirect("/login"));
});
// --- 대시보드 (로그인 필요) ---
app.get("/", requireAuth, async (req, res) => {
const projects = await db.listProjects();
res.render("dashboard", { siteGroups, projects });
});
// --- 프로젝트 ---
app.post("/projects", requireAuth, async (req, res) => {
const { title, description, repo_url, app_url, tags } = req.body;
if (!title || !title.trim()) return res.redirect("/");
await db.addProject({
owner: req.session.user.sabun,
title: title.trim(),
description,
repoUrl: repo_url,
appUrl: app_url,
tags,
});
res.redirect("/");
});
app.get("/projects/:id", requireAuth, async (req, res) => {
const project = await db.getProject(req.params.id);
if (!project) return res.status(404).send("not found");
const comments = await db.listComments(project.id);
const starred = await db.isStarred({ projectId: project.id, sabun: req.session.user.sabun });
res.render("project", { project, comments, starred });
});
app.post("/projects/:id/comments", requireAuth, async (req, res) => {
if (req.body.body && req.body.body.trim()) {
await db.addComment({
projectId: req.params.id,
author: req.session.user.sabun,
body: req.body.body.trim(),
});
}
res.redirect(`/projects/${req.params.id}`);
});
app.post("/projects/:id/star", requireAuth, async (req, res) => {
await db.toggleStar({ projectId: req.params.id, sabun: req.session.user.sabun });
res.redirect(`/projects/${req.params.id}`);
});
// --- 부팅 ---
const start = async () => {
try {
await initOidc();
console.log("OIDC discovery 완료:", config.oidc.issuer);
} catch (e) {
console.error("OIDC 초기화 실패(로그인 비활성):", e.message);
}
app.listen(config.port, "0.0.0.0", () =>
console.log(`${config.brand} portal listening on :${config.port}`)
);
};
start();