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:
@@ -1,6 +1,7 @@
|
||||
# ==========================================
|
||||
# 백엔드 고유 설정 예시 — 복사해 .env.development 생성
|
||||
# 루트 .env 에서 내려주지 않는 값만 작성한다 (JWT 등)
|
||||
# 백엔드 고유 설정 — 루트 .env 에서 내려주지 않는 값만 작성 (JWT/Gitea 등)
|
||||
# DB/Redis/CORS/포트 등은 docker-compose 가 루트 .env.development 에서 주입한다.
|
||||
# .example 을 복사해 .env.development 를 만들고 실제 값으로 채운다.
|
||||
# ==========================================
|
||||
|
||||
# JWT 시크릿 (access/refresh 분리) — 임의의 강력한 랜덤 문자열로 교체
|
||||
@@ -13,7 +14,7 @@ JWT_ACCESS_TTL=15m
|
||||
JWT_REFRESH_TTL=14d
|
||||
|
||||
# --- Gitea 연동 (외부 Gitea 인스턴스) ---
|
||||
# 세 값이 모두 채워져야 연동 활성화. 비우면 저장소는 Gitea 동기화 없이 동작한다.
|
||||
# 세 값(BASE_URL/TOKEN/ORG)이 모두 채워져야 연동 활성화. 비우면 Gitea 동기화 없이 동작한다.
|
||||
# 저장소 생성/수정/삭제 시 GITEA_ORG 조직 아래 repo 가 자동 동기화된다.
|
||||
# GITEA_BASE_URL: 끝 슬래시 없이 (/api/v1 는 코드가 붙임)
|
||||
GITEA_BASE_URL=https://gitea.example.com
|
||||
@@ -22,5 +23,9 @@ GITEA_TOKEN=change-me-gitea-token
|
||||
# GITEA_ORG: 저장소가 생성될 조직 이름 (Relay 의 owner 로도 사용)
|
||||
GITEA_ORG=marketing-team
|
||||
# 템플릿 저장소(생성 시 복제) — 미설정 시 기본 TtiPo/project-base 사용. Gitea 에서 template 설정 필요
|
||||
GITEA_TEMPLATE_OWNER=ttiPo
|
||||
GITEA_TEMPLATE_REPO=project-base
|
||||
GITEA_TEMPLATE_OWNER=ttipo
|
||||
GITEA_TEMPLATE_REPO=project-base
|
||||
# import(역방향 동기화로 가져온, 멤버 0명) 저장소의 기본 owner-admin 으로 지정할 가입 사용자 이메일.
|
||||
# 동기화는 시스템 작업이라 실행 주체가 없으므로 여기서 지정한다.
|
||||
# 비우거나 미가입 이메일이면 import 저장소는 멤버 0명으로 남고(초대 불가), 가입 후 다음 동기화에서 지정된다.
|
||||
REPO_IMPORT_ADMIN_EMAIL=admin@example.com
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# ==========================================
|
||||
# 백엔드 고유 설정 예시 — 복사해 .env.development 생성
|
||||
# 루트 .env 에서 내려주지 않는 값만 작성한다 (JWT 등)
|
||||
# 백엔드 고유 설정 — 루트 .env 에서 내려주지 않는 값만 작성 (JWT/Gitea 등)
|
||||
# DB/Redis/CORS/포트 등은 docker-compose 가 루트 .env.production 에서 주입한다.
|
||||
# .example 을 복사해 .env.production 를 만들고 실제 값으로 채운다.
|
||||
# ==========================================
|
||||
|
||||
# JWT 시크릿 (access/refresh 분리) — 임의의 강력한 랜덤 문자열로 교체
|
||||
@@ -13,7 +14,7 @@ JWT_ACCESS_TTL=15m
|
||||
JWT_REFRESH_TTL=14d
|
||||
|
||||
# --- Gitea 연동 (외부 Gitea 인스턴스) ---
|
||||
# 세 값이 모두 채워져야 연동 활성화. 비우면 저장소는 Gitea 동기화 없이 동작한다.
|
||||
# 세 값(BASE_URL/TOKEN/ORG)이 모두 채워져야 연동 활성화. 비우면 Gitea 동기화 없이 동작한다.
|
||||
# 저장소 생성/수정/삭제 시 GITEA_ORG 조직 아래 repo 가 자동 동기화된다.
|
||||
# GITEA_BASE_URL: 끝 슬래시 없이 (/api/v1 는 코드가 붙임)
|
||||
GITEA_BASE_URL=https://gitea.example.com
|
||||
@@ -22,5 +23,9 @@ GITEA_TOKEN=change-me-gitea-token
|
||||
# GITEA_ORG: 저장소가 생성될 조직 이름 (Relay 의 owner 로도 사용)
|
||||
GITEA_ORG=marketing-team
|
||||
# 템플릿 저장소(생성 시 복제) — 미설정 시 기본 TtiPo/project-base 사용. Gitea 에서 template 설정 필요
|
||||
GITEA_TEMPLATE_OWNER=ttiPo
|
||||
GITEA_TEMPLATE_REPO=project-base
|
||||
GITEA_TEMPLATE_OWNER=ttipo
|
||||
GITEA_TEMPLATE_REPO=project-base
|
||||
# import(역방향 동기화로 가져온, 멤버 0명) 저장소의 기본 owner-admin 으로 지정할 가입 사용자 이메일.
|
||||
# 동기화는 시스템 작업이라 실행 주체가 없으므로 여기서 지정한다.
|
||||
# 비우거나 미가입 이메일이면 import 저장소는 멤버 0명으로 남고(초대 불가), 가입 후 다음 동기화에서 지정된다.
|
||||
REPO_IMPORT_ADMIN_EMAIL=admin@example.com
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user