feat: Gitea 프론트 연동(클론바·원격없음 배지)·import-admin 인프라 마무리

- GiteaRepoBar 컴포넌트(클론 주소 복사 + Gitea 열기)를 RepoSettingsPage 에 적용
- repo.store 뷰에 cloneUrl/htmlUrl/giteaMissing 매핑
- repo-sync: import 저장소 owner-admin 자동 지정(REPO_IMPORT_ADMIN_EMAIL)
- env example·docker-compose.dev 에 Gitea/import-admin 키 정비

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 18:11:16 +09:00
parent b626bbdf6c
commit e2fe089321
9 changed files with 294 additions and 22 deletions
+62 -2
View File
@@ -1,9 +1,13 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService } from '../user/user.service';
import type { User } from '../user/entities/user.entity';
import { GiteaService } from '../gitea/gitea.service';
import { Repo } from './entities/repo.entity';
import { RepoMember } from './entities/repo-member.entity';
// Gitea description 에서 분리한 표시명/설명
interface ParsedDescription {
@@ -21,12 +25,23 @@ export class RepoSyncService implements OnApplicationBootstrap {
private readonly logger = new Logger(RepoSyncService.name);
// 동기화 중복 실행 방지(부트스트랩과 스케줄이 겹치는 경우)
private running = false;
// import(멤버 0명) 저장소에 기본 owner-admin 으로 지정할 사용자 이메일.
// 동기화는 시스템 작업이라 실행 주체가 없으므로 env 로 지정한다. 비어 있으면 멤버 0명으로 둔다.
private readonly importAdminEmail: string;
constructor(
@InjectRepository(Repo)
private readonly repoRepo: Repository<Repo>,
@InjectRepository(RepoMember)
private readonly memberRepo: Repository<RepoMember>,
private readonly userService: UserService,
private readonly gitea: GiteaService,
) {}
config: ConfigService,
) {
this.importAdminEmail = (
config.get<string>('REPO_IMPORT_ADMIN_EMAIL') ?? ''
).trim();
}
// 서버 기동 직후 1회 동기화 — 부팅을 막지 않도록 비차단(백그라운드)으로 실행
onApplicationBootstrap(): void {
@@ -111,8 +126,12 @@ export class RepoSyncService implements OnApplicationBootstrap {
}
}
// import(멤버 0명) 저장소에 기본 owner-admin 지정 — 멤버 할당 데드락 방지.
// (env REPO_IMPORT_ADMIN_EMAIL 미설정/미가입 시 건너뜀)
const adminAssigned = await this.ensureImportAdmin();
this.logger.log(
`Gitea 동기화 완료 (${trigger}) — 원격 ${remote.length}개 / 신규 ${imported} · 갱신 ${relinked} · 누락표시 ${missing}`,
`Gitea 동기화 완료 (${trigger}) — 원격 ${remote.length}개 / 신규 ${imported} · 갱신 ${relinked} · 누락표시 ${missing} · admin지정 ${adminAssigned}`,
);
} catch (e) {
this.logger.error(
@@ -124,6 +143,47 @@ export class RepoSyncService implements OnApplicationBootstrap {
}
}
// import(멤버 0명) Gitea 연동 저장소에 기본 owner-admin 을 지정한다.
// env REPO_IMPORT_ADMIN_EMAIL 의 가입 사용자를 owner-admin 으로 등록(신규 import + 기존 멤버 0명 모두 대상).
// 반환: 이번 실행에서 admin 을 새로 붙인 저장소 수.
private async ensureImportAdmin(): Promise<number> {
if (!this.importAdminEmail) return 0;
const admin = await this.userService.findByEmail(this.importAdminEmail);
if (!admin) {
this.logger.warn(
`REPO_IMPORT_ADMIN_EMAIL(${this.importAdminEmail}) 에 해당하는 가입 사용자가 없어 import 저장소 admin 지정을 건너뜁니다. (가입 후 다음 동기화에서 지정됨)`,
);
return 0;
}
// Gitea 연동(import) 저장소 중 멤버가 0명인 것만 대상.
// Relay 생성 저장소는 생성자(owner)가 항상 멤버이므로 제외된다.
const linked = await this.repoRepo.find({ relations: ['members'] });
let assigned = 0;
for (const repo of linked) {
if (repo.giteaRepoId === null) continue;
if ((repo.members?.length ?? 0) > 0) continue;
await this.assignOwnerAdmin(repo, admin);
assigned++;
this.logger.log(
`import 저장소 owner-admin 지정: ${repo.slugName}${admin.email}`,
);
}
return assigned;
}
// 저장소에 사용자를 owner-admin 멤버로 등록(Relay 생성 시 owner 등록과 동일 규약)
private async assignOwnerAdmin(repo: Repo, user: User): Promise<void> {
const member = this.memberRepo.create({
repo,
user,
roleType: 'admin',
isOwner: true,
});
await this.memberRepo.save(member);
}
// 기존 저장소에 Gitea 연동정보를 채우고 누락표시를 해제 — 변경이 있었으면 true
private backfillLinkage(
repo: Repo,