diff --git a/.project-env.example b/.project-env.example index 13b42b4..1871dfa 100644 --- a/.project-env.example +++ b/.project-env.example @@ -25,6 +25,11 @@ OIDC_CLIENT_SECRET=REPLACE_WITH_PORTAL_CLIENT_SECRET # 이 Keycloak 역할(realm role)을 가진 사용자에게만 인프라 도구(OpenEverest/MinIO/Keycloak/LiteLLM) 노출. ADMIN_ROLE=admin +# --- Gitea 언어 자동 판별 (선택) --- +# 프로젝트 등록 시 repo_url 이 Gitea 레포면 언어 통계 API 로 메인 언어를 자동으로 채운다. +# 공개 레포는 토큰 없이 동작. 비공개 레포까지 판별하려면 read:repository 권한 토큰 필요. +# GITEA_TOKEN= + # --- 도구 URL (기본값 있음 · 도메인/폐쇄망 대응으로 덮어쓰기) --- # CODER_URL=https://coder.bokdev.in/api/v2/users/oidc/callback # GITEA_URL=https://gitea.bokdev.in/user/oauth2/keycloak diff --git a/src/config.js b/src/config.js index 4462d78..a3ad670 100644 --- a/src/config.js +++ b/src/config.js @@ -16,6 +16,10 @@ export const config = { // 관리자 판별용 Keycloak 역할 이름. 이 role 을 가진 사용자에게만 인프라 도구 노출. adminRole: process.env.ADMIN_ROLE || "admin", + // 프로젝트 등록 시 Gitea 언어 통계 API 로 메인 언어 자동 판별용 토큰(선택). + // 공개 레포는 없어도 되고, 비공개 레포 조회에만 필요. + giteaToken: process.env.GITEA_TOKEN || "", + // 도구(사이트) URL — 폐쇄망/도메인 변경 대비 환경변수로 덮어쓰기 가능. tools: { coder: process.env.CODER_URL || "https://coder.bokdev.in/api/v2/users/oidc/callback", diff --git a/src/gitea.js b/src/gitea.js new file mode 100644 index 0000000..06a2560 --- /dev/null +++ b/src/gitea.js @@ -0,0 +1,50 @@ +// Gitea 언어 통계 조회 — 프로젝트의 repo_url 로 메인 언어를 자동 판별. +// 공개 레포는 토큰 없이도 조회되고, 비공개 레포는 GITEA_TOKEN 이 있어야 한다. +// 네트워크 실패/타임아웃/비-Gitea URL 이면 빈 문자열을 반환해 호출부가 폴백하도록 한다. +import { config } from "./config.js"; + +// repo_url → { base, owner, repo } 파싱. URL 형태가 아니거나 owner/repo 가 없으면 null. +// 예) https://gitea.bokdev.in/hong/my-app(.git) → { base, owner: hong, repo: my-app } +export function parseRepo(repoUrl) { + if (!repoUrl) return null; + let u; + try { + u = new URL(repoUrl); + } catch { + return null; + } + const parts = u.pathname.replace(/\.git$/, "").split("/").filter(Boolean); + if (parts.length < 2) return null; + return { base: `${u.protocol}//${u.host}`, owner: parts[0], repo: parts[1] }; +} + +// 레포의 메인 언어(바이트 최다) 1개를 반환. 실패/없음이면 "". +export async function fetchMainLang(repoUrl) { + const r = parseRepo(repoUrl); + if (!r) return ""; + const url = `${r.base}/api/v1/repos/${encodeURIComponent(r.owner)}/${encodeURIComponent(r.repo)}/languages`; + const headers = { Accept: "application/json" }; + if (config.giteaToken) headers.Authorization = `token ${config.giteaToken}`; + + // 등록 응답이 Gitea 응답을 기다리다 멈추지 않도록 3초 타임아웃. + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 3000); + try { + const res = await fetch(url, { headers, signal: ctrl.signal }); + if (!res.ok) return ""; + const data = await res.json(); // { "JavaScript": 12345, "CSS": 678, ... } + let top = ""; + let max = -1; + for (const [lang, bytes] of Object.entries(data || {})) { + if (typeof bytes === "number" && bytes > max) { + max = bytes; + top = lang; + } + } + return top; + } catch { + return ""; + } finally { + clearTimeout(timer); + } +} diff --git a/src/helpers.js b/src/helpers.js index 2d53661..31782f3 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -56,7 +56,7 @@ export function langFromTags(tags) { if (t.includes("go") || t.includes("golang")) return "Go"; if (t.includes("rust")) return "Rust"; if (t.includes("node") || t.includes("js")) return "JavaScript"; - return "TypeScript"; + return ""; // 매칭되는 언어 태그가 없으면 언어 미표시 } // 문자열 → 결정적 해시(아바타/색 시드). diff --git a/src/server.js b/src/server.js index 93a0106..ff1a673 100644 --- a/src/server.js +++ b/src/server.js @@ -7,6 +7,7 @@ import { config } from "./config.js"; import { toolsForUser } from "./sites.js"; import { initOidc, loginRedirect, handleCallback, requireAuth, oidcReady, logoutUrl } from "./auth.js"; import { icon, identicon, langColor, langFromTags } from "./helpers.js"; +import { fetchMainLang } from "./gitea.js"; import * as db from "./db.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -110,15 +111,18 @@ app.post("/projects", requireAuth, async (req, res) => { .map((t) => t.trim().replace(/^#/, "")) .filter(Boolean) .slice(0, 5); + const repoUrl = (repo_url || "").trim(); + // 메인 언어: 사용자가 직접 고르면 그 값, '자동 감지'면 Gitea 언어 통계 → 태그 추론 순으로 폴백. + const lang = (req.body.lang || "").trim() || (await fetchMainLang(repoUrl)) || langFromTags(tagArr); await db.addProject({ owner: req.session.user.sabun, title: title.trim(), description: (description || "").trim(), - repoUrl: (repo_url || "").trim(), + repoUrl, appUrl: (app_url || "").trim(), tags: tagArr.join(", "), isPublic: req.body.is_public !== "0", - lang: langFromTags(tagArr), + lang, }); const msg = req.body.is_public === "0" ? "비공개로 등록됐습니다" : "등록 완료 · 피드에 공유됐습니다"; res.redirect("/?toast=" + encodeURIComponent(msg)); diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 02090a6..fe2019e 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -219,6 +219,19 @@ 저장소 URL 선택 +