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

78
src/db.js Normal file
View File

@@ -0,0 +1,78 @@
// Postgres 풀 (OpenEverest appdb, search_path=portal). DATABASE_URL 로 연결.
import pg from "pg";
import { config } from "./config.js";
export const pool = new pg.Pool({ connectionString: config.databaseUrl });
export const q = (text, params) => pool.query(text, params);
// 로그인 시 사용자 upsert
export async function upsertUser({ sabun, name, email }) {
await q(
`INSERT INTO users (sabun, name, email, avatar_seed, last_login)
VALUES ($1,$2,$3,$1, now())
ON CONFLICT (sabun) DO UPDATE
SET name=EXCLUDED.name, email=EXCLUDED.email, last_login=now()`,
[sabun, name, email || null]
);
}
// 프로젝트 목록 (스타 수 + 코멘트 수 + 소유자 이름 포함)
export async function listProjects() {
const { rows } = await q(
`SELECT p.*, u.name AS owner_name,
(SELECT count(*) FROM stars s WHERE s.project_id=p.id) AS star_count,
(SELECT count(*) FROM comments c WHERE c.project_id=p.id) AS comment_count
FROM projects p JOIN users u ON u.sabun=p.owner_sabun
ORDER BY p.created_at DESC`
);
return rows;
}
export async function getProject(id) {
const { rows } = await q(
`SELECT p.*, u.name AS owner_name,
(SELECT count(*) FROM stars s WHERE s.project_id=p.id) AS star_count
FROM projects p JOIN users u ON u.sabun=p.owner_sabun WHERE p.id=$1`,
[id]
);
return rows[0];
}
export async function listComments(projectId) {
const { rows } = await q(
`SELECT c.*, u.name AS author_name
FROM comments c JOIN users u ON u.sabun=c.author_sabun
WHERE c.project_id=$1 ORDER BY c.created_at ASC`,
[projectId]
);
return rows;
}
export async function addProject({ owner, title, description, repoUrl, appUrl, tags }) {
const { rows } = await q(
`INSERT INTO projects (owner_sabun, title, description, repo_url, app_url, tags)
VALUES ($1,$2,$3,$4,$5,$6) RETURNING id`,
[owner, title, description || "", repoUrl || "", appUrl || "", tags || ""]
);
return rows[0].id;
}
export async function addComment({ projectId, author, body }) {
await q(`INSERT INTO comments (project_id, author_sabun, body) VALUES ($1,$2,$3)`,
[projectId, author, body]);
}
export async function toggleStar({ projectId, sabun }) {
const { rowCount } = await q(`DELETE FROM stars WHERE project_id=$1 AND sabun=$2`, [projectId, sabun]);
if (rowCount === 0) {
await q(`INSERT INTO stars (project_id, sabun) VALUES ($1,$2)`, [projectId, sabun]);
return true;
}
return false;
}
export async function isStarred({ projectId, sabun }) {
const { rowCount } = await q(`SELECT 1 FROM stars WHERE project_id=$1 AND sabun=$2`, [projectId, sabun]);
return rowCount > 0;
}