feat(projects): 메인 언어 Gitea 자동 판별 + 수동 선택 폴백
- src/gitea.js: repo_url 파싱 + 언어 통계 API(fetchMainLang) 신규 - 등록 폼에 '메인 언어' 드롭다운 추가(기본 '자동 감지') - 결정 순서: 드롭다운 → Gitea 자동감지 → 태그 추론 → 미표시 - langFromTags 기본값 TypeScript 제거(안 맞으면 미표시) - GITEA_TOKEN(선택): 비공개 레포 자동감지용 스위치 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
src/gitea.js
Normal file
50
src/gitea.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user