- 등록 모달: 저장소 URL 필드를 제목 아래(설명 위)로 이동 — README 가져오기 흐름 자연화 - URL 미입력 상태로 README 가져오기 시 입력란에 빨간 인라인 안내 표시(토스트 대신) - 피드 카드: 설명 미리보기 2줄 클램프 + flex:1 로 태그·푸터를 카드 하단에 정렬 - fetchReadme: CRLF→LF·줄끝공백·연속빈줄 정리로 불필요한 개행 유입 방지 - mdPlain: 개행을 살려(빈 줄만 제거) 카드 미리보기가 README 줄 구조로 보이게 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
3.6 KiB
JavaScript
93 lines
3.6 KiB
JavaScript
// 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);
|
|
}
|
|
}
|
|
|
|
// raw README 의 개행을 정리한다. Gitea 화면(렌더링)에는 없지만 raw 원문에 남아 있는
|
|
// 군더더기 개행이 설명란에 "없던 엔터"처럼 들어오는 문제를 막는다.
|
|
// - CRLF/CR → LF 로 통일(Windows 개행의 \r 제거)
|
|
// - 줄 끝 공백 제거
|
|
// - 3줄 이상 연속 빈 줄 → 빈 줄 1개(2개 개행)로 축소 (마크다운상 동일)
|
|
// - 문서 앞뒤 공백/개행 제거
|
|
function normalizeReadme(text) {
|
|
return String(text)
|
|
.replace(/\r\n?/g, "\n")
|
|
.replace(/[ \t]+$/gm, "")
|
|
.replace(/\n{3,}/g, "\n\n")
|
|
.trim();
|
|
}
|
|
|
|
// 레포의 README 원문(마크다운)을 반환. 실패/없음이면 "".
|
|
// Gitea raw 엔드포인트로 기본 브랜치의 README 파일을 후보 순서대로 시도한다.
|
|
export async function fetchReadme(repoUrl) {
|
|
const r = parseRepo(repoUrl);
|
|
if (!r) return "";
|
|
const headers = {};
|
|
if (config.giteaToken) headers.Authorization = `token ${config.giteaToken}`;
|
|
const base = `${r.base}/api/v1/repos/${encodeURIComponent(r.owner)}/${encodeURIComponent(r.repo)}/raw`;
|
|
const candidates = ["README.md", "README.MD", "readme.md", "README", "README.markdown"];
|
|
|
|
const ctrl = new AbortController();
|
|
const timer = setTimeout(() => ctrl.abort(), 4000);
|
|
try {
|
|
for (const name of candidates) {
|
|
const res = await fetch(`${base}/${name}`, { headers, signal: ctrl.signal });
|
|
if (!res.ok) continue;
|
|
const text = normalizeReadme(await res.text());
|
|
// 너무 큰 README 는 앞부분만(설명란이 감당할 범위). 20KB 컷.
|
|
return text.length > 20000 ? text.slice(0, 20000) : text;
|
|
}
|
|
return "";
|
|
} catch {
|
|
return "";
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|