feat: 저장소 생성 시 설명 필수화 + 템플릿 복제 생성
- 프로젝트 설명을 생성 필수 항목으로 변경(CreateRepoDto.description required + 프론트 검증)
- 템플릿 복제: useTemplate=true 면 Gitea generate API 로 GITEA_TEMPLATE_OWNER/REPO
(기본 TtiPo/project-base) 저장소를 복제 생성(브랜치는 템플릿을 따름)
- GET /repos/owner → /repos/meta 로 확장({ owner, template }) — 생성폼 메타 일괄 제공
- 프론트 생성폼: 가짜 템플릿 목록 제거 → 실제 템플릿 옵션, 설명 필수 UI
- env.example/ api-contract/ 진행현황 문서 갱신
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,9 @@ GITEA_BASE_URL=https://gitea.example.com
|
||||
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
|
||||
|
||||
# --- 단독 실행(non-Docker) 시 주석 해제 ---
|
||||
# NODE_ENV=development
|
||||
|
||||
@@ -21,6 +21,9 @@ GITEA_BASE_URL=https://gitea.example.com
|
||||
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
|
||||
|
||||
# --- 단독 실행(non-Docker) 시 주석 해제 ---
|
||||
# NODE_ENV=development
|
||||
|
||||
@@ -40,6 +40,9 @@ export class GiteaService {
|
||||
private readonly baseUrl: string; // 끝 슬래시 제거된 형태
|
||||
private readonly token: string;
|
||||
readonly org: string;
|
||||
// 템플릿 저장소(이 저장소를 복제해 새 repo 생성). 기본값: TtiPo/project-base
|
||||
readonly templateOwner: string;
|
||||
readonly templateRepo: string;
|
||||
|
||||
constructor(config: ConfigService) {
|
||||
this.baseUrl = (config.get<string>('GITEA_BASE_URL') ?? '').replace(
|
||||
@@ -48,6 +51,9 @@ export class GiteaService {
|
||||
);
|
||||
this.token = config.get<string>('GITEA_TOKEN') ?? '';
|
||||
this.org = config.get<string>('GITEA_ORG') ?? '';
|
||||
this.templateOwner = config.get<string>('GITEA_TEMPLATE_OWNER') ?? 'TtiPo';
|
||||
this.templateRepo =
|
||||
config.get<string>('GITEA_TEMPLATE_REPO') ?? 'project-base';
|
||||
|
||||
if (this.enabled) {
|
||||
this.logger.log(`Gitea 연동 활성화 — ${this.baseUrl} (org: ${this.org})`);
|
||||
@@ -63,6 +69,16 @@ export class GiteaService {
|
||||
return Boolean(this.baseUrl && this.token && this.org);
|
||||
}
|
||||
|
||||
// 템플릿 식별자 (owner/repo) — 프론트 표시·로깅용
|
||||
get templateSlug(): string {
|
||||
return `${this.templateOwner}/${this.templateRepo}`;
|
||||
}
|
||||
|
||||
// 템플릿 복제 사용 가능 여부 — 연동 활성 + 템플릿 owner/repo 설정
|
||||
get templateEnabled(): boolean {
|
||||
return this.enabled && Boolean(this.templateOwner && this.templateRepo);
|
||||
}
|
||||
|
||||
// 조직 아래 저장소 생성 (초기 커밋/기본 브랜치 포함)
|
||||
async createRepo(input: {
|
||||
name: string; // = Relay slugName
|
||||
@@ -84,6 +100,29 @@ export class GiteaService {
|
||||
return this.toResult(data);
|
||||
}
|
||||
|
||||
// 템플릿 저장소를 복제해 조직 아래 새 저장소 생성 (git 콘텐츠 포함).
|
||||
// 생성된 저장소의 기본 브랜치는 템플릿을 따른다.
|
||||
async generateFromTemplate(input: {
|
||||
name: string; // = Relay slugName
|
||||
description: string;
|
||||
private: boolean;
|
||||
}): Promise<GiteaRepoResult> {
|
||||
const data = await this.request<GiteaRepoApi>(
|
||||
'POST',
|
||||
`/repos/${encodeURIComponent(this.templateOwner)}/${encodeURIComponent(
|
||||
this.templateRepo,
|
||||
)}/generate`,
|
||||
{
|
||||
owner: this.org, // 대상 조직
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
private: input.private,
|
||||
git_content: true, // 템플릿의 파일/폴더 구조 복제
|
||||
},
|
||||
);
|
||||
return this.toResult(data);
|
||||
}
|
||||
|
||||
// 저장소 메타데이터(설명/공개범위) 수정 — slug(name) 는 변경하지 않음
|
||||
async updateRepo(
|
||||
name: string,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
@@ -41,13 +42,27 @@ export class CreateRepoDto {
|
||||
@IsIn(['private', 'public'], { message: '공개 범위를 선택해 주세요.' })
|
||||
visibility!: RepoVisibility;
|
||||
|
||||
@ApiProperty({ description: '프로젝트 설명(선택)', required: false })
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: '프로젝트 설명(필수)',
|
||||
example: '3분기 신제품 런칭을 위한 마케팅 캠페인 저장소',
|
||||
})
|
||||
@IsString()
|
||||
description?: string;
|
||||
@IsNotEmpty({ message: '프로젝트 설명은 필수 입력 항목입니다.' })
|
||||
@MaxLength(300)
|
||||
description!: string;
|
||||
|
||||
@ApiProperty({ description: '메인 브랜치', example: 'main', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
branch?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'템플릿 사용 여부 — true 면 설정된 템플릿 저장소(GITEA_TEMPLATE)를 복제해 생성',
|
||||
example: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
useTemplate?: boolean;
|
||||
}
|
||||
|
||||
@@ -47,12 +47,15 @@ export class RepoController {
|
||||
return this.repoService.create(dto, user.id);
|
||||
}
|
||||
|
||||
// ':repoId' 보다 먼저 선언해야 'owner' 가 파라미터로 매칭되지 않음
|
||||
@Get('owner')
|
||||
@ApiOperation({ summary: '현재 조직(소유자) 조회 — 생성 폼 표시용' })
|
||||
// ':repoId' 보다 먼저 선언해야 'meta' 가 파라미터로 매칭되지 않음
|
||||
@Get('meta')
|
||||
@ApiOperation({ summary: '저장소 생성 폼 메타(소유자/템플릿) 조회' })
|
||||
@ApiResponse({ status: 200, description: '조회 성공' })
|
||||
getOwner(): { owner: string } {
|
||||
return { owner: this.repoService.getOwner() };
|
||||
getCreateMeta(): {
|
||||
owner: string;
|
||||
template: { available: boolean; slug: string };
|
||||
} {
|
||||
return this.repoService.getCreateMeta();
|
||||
}
|
||||
|
||||
@Get(':repoId')
|
||||
|
||||
@@ -57,10 +57,20 @@ export class RepoService {
|
||||
private readonly gitea: GiteaService,
|
||||
) {}
|
||||
|
||||
// 현재 저장소 소유자(조직) — Gitea 연동 시 GITEA_ORG, 아니면 기본 상수.
|
||||
// 프론트 생성 폼의 "소유자 고정" 표시에 사용(저장소 생성 전 미리 노출).
|
||||
getOwner(): string {
|
||||
return this.gitea.enabled ? this.gitea.org : DEFAULT_OWNER;
|
||||
// 저장소 생성 폼 메타데이터 — 소유자(조직) 고정 표시 + 템플릿 가용 정보.
|
||||
// 저장소 생성 전 프론트가 미리 조회하여 표시한다.
|
||||
getCreateMeta(): {
|
||||
owner: string;
|
||||
template: { available: boolean; slug: string };
|
||||
} {
|
||||
return {
|
||||
// 연동 시 owner = GITEA_ORG, 아니면 기본 상수
|
||||
owner: this.gitea.enabled ? this.gitea.org : DEFAULT_OWNER,
|
||||
template: {
|
||||
available: this.gitea.templateEnabled,
|
||||
slug: this.gitea.templateSlug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 내가 멤버인 저장소 목록
|
||||
@@ -110,22 +120,27 @@ export class RepoService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
const description = dto.description ?? null;
|
||||
const description = dto.description;
|
||||
const visibility = dto.visibility;
|
||||
const branch = dto.branch?.trim() || 'main';
|
||||
const requestedBranch = dto.branch?.trim() || 'main';
|
||||
// 연동 시 소유자는 Gitea 조직명 — slug `owner/slugName` 이 Gitea full_name 과 일치
|
||||
const owner = this.gitea.enabled ? this.gitea.org : DEFAULT_OWNER;
|
||||
|
||||
// 1) Gitea 조직에 실제 저장소 생성 (연동 활성 시)
|
||||
// - useTemplate: 템플릿 저장소를 복제해 생성(브랜치는 템플릿을 따름)
|
||||
// - 아니면 빈 저장소(auto_init, 요청 브랜치)
|
||||
let giteaRepo: GiteaRepoResult | null = null;
|
||||
if (this.gitea.enabled) {
|
||||
giteaRepo = await this.createGiteaRepo(dto.slug, {
|
||||
name: dto.name,
|
||||
description,
|
||||
visibility,
|
||||
branch,
|
||||
branch: requestedBranch,
|
||||
useTemplate: dto.useTemplate ?? false,
|
||||
});
|
||||
}
|
||||
// 템플릿 복제 시 실제 기본 브랜치는 Gitea 응답값을 우선 사용
|
||||
const branch = giteaRepo?.defaultBranch || requestedBranch;
|
||||
|
||||
// 2) Relay 메타데이터 저장 (Gitea 연동 정보 포함)
|
||||
try {
|
||||
@@ -236,7 +251,8 @@ export class RepoService {
|
||||
|
||||
// --- 내부 헬퍼 ---
|
||||
|
||||
// Gitea 조직에 저장소 생성 — 실패 시 사용자 노출 가능한 BusinessException 으로 변환
|
||||
// Gitea 조직에 저장소 생성 — 실패 시 사용자 노출 가능한 BusinessException 으로 변환.
|
||||
// useTemplate 이면 템플릿 저장소를 복제(generate), 아니면 빈 저장소(auto_init).
|
||||
private async createGiteaRepo(
|
||||
slug: string,
|
||||
meta: {
|
||||
@@ -244,13 +260,23 @@ export class RepoService {
|
||||
description: string | null;
|
||||
visibility: RepoVisibility;
|
||||
branch: string;
|
||||
useTemplate: boolean;
|
||||
},
|
||||
): Promise<GiteaRepoResult> {
|
||||
const description = this.buildGiteaDescription(meta.name, meta.description);
|
||||
const isPrivate = meta.visibility === 'private';
|
||||
try {
|
||||
if (meta.useTemplate) {
|
||||
return await this.gitea.generateFromTemplate({
|
||||
name: slug,
|
||||
description,
|
||||
private: isPrivate,
|
||||
});
|
||||
}
|
||||
return await this.gitea.createRepo({
|
||||
name: slug,
|
||||
description: this.buildGiteaDescription(meta.name, meta.description),
|
||||
private: meta.visibility === 'private',
|
||||
description,
|
||||
private: isPrivate,
|
||||
defaultBranch: meta.branch,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -80,12 +80,17 @@ status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프
|
||||
| 메서드 | 경로 | 요청 | 응답 |
|
||||
|---|---|---|---|
|
||||
| GET | `/repos` | `?page&size` | `Repo[]`(요약) — 내가 멤버인 저장소 |
|
||||
| GET | `/repos/meta` | — | `{ owner, template }` — 생성 폼 메타(`:repoId` 보다 먼저 매칭) |
|
||||
| POST | `/repos` | `CreateRepoDto` | `Repo` |
|
||||
| GET | `/repos/:repoId` | — | `Repo`(상세) |
|
||||
| PATCH | `/repos/:repoId` | `UpdateRepoDto` | `Repo` |
|
||||
| DELETE | `/repos/:repoId` | — | `null` |
|
||||
|
||||
`CreateRepoDto`: `{ slug, name, visibility('private'|'public'), description?, branch? }`
|
||||
`GET /repos/meta` 응답: `{ owner: string, template: { available: boolean, slug: string } }`
|
||||
— 생성 전 소유자(조직) 고정 표시 + 템플릿 옵션 노출용.
|
||||
|
||||
`CreateRepoDto`: `{ slug, name, visibility('private'|'public'), description, branch?, useTemplate? }`
|
||||
(**description 필수**, `useTemplate=true` 면 템플릿 저장소를 복제해 생성)
|
||||
`Repo` 응답(원시값 중심):
|
||||
|
||||
```jsonc
|
||||
@@ -116,10 +121,12 @@ status 매핑보다 우선해 그대로 표준 응답으로 내려보내고, 프
|
||||
|
||||
| Relay 동작 | Gitea API | 매핑 |
|
||||
|---|---|---|
|
||||
| `POST /repos` | `POST /api/v1/orgs/{org}/repos` (auto_init) | `slug`→repo name, `visibility`→private, `branch`→default_branch, `name`+`desc`→description |
|
||||
| `POST /repos` (빈 저장소) | `POST /api/v1/orgs/{org}/repos` (auto_init) | `slug`→repo name, `visibility`→private, `branch`→default_branch, `name`+`desc`→description |
|
||||
| `POST /repos` (`useTemplate`) | `POST /api/v1/repos/{tplOwner}/{tplRepo}/generate` (git_content) | `owner`→org, `slug`→name, `name`+`desc`→description, `visibility`→private. 기본 브랜치는 템플릿을 따름 |
|
||||
| `PATCH /repos/:id` | `PATCH /api/v1/repos/{org}/{slug}` | `name`/`desc` 변경→description, `visibility` 변경→private (slug 불변) |
|
||||
| `DELETE /repos/:id` | `DELETE /api/v1/repos/{org}/{slug}` | — |
|
||||
|
||||
- 템플릿 출처: `GITEA_TEMPLATE_OWNER`/`GITEA_TEMPLATE_REPO`(기본 `TtiPo`/`project-base`). 해당 저장소는 Gitea 에서 *template* 로 설정돼 있어야 한다.
|
||||
- 인증: 조직 권한 토큰 단일 사용(`Authorization: token <TOKEN>`). 사용자별 Gitea 계정 매핑은 추후.
|
||||
- 생성은 Gitea 우선 → 성공 시 Relay 저장(연동 정보 `giteaRepoId`/`cloneUrl`/`htmlUrl` 보관). Relay 저장 실패 시 Gitea repo 롤백 삭제.
|
||||
- Gitea 이름 충돌(409) → `BIZ_001` "이미 사용 중인 저장소 이름입니다.", 그 외 실패 → `BIZ_001`(502).
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
- **ID 전략**: User/Repo PK=UUID, Repo 라우팅 식별자=`slugName`, Task=저장소 내 순번(#9, 4단계).
|
||||
- **Gitea 연동 방식**: HTTP 클라이언트는 새 의존성(@nestjs/axios) 없이 Node 20 내장 `fetch` 사용(베이스 이미지 alpine 호환). 인증은 조직 토큰 1개(사용자별 Gitea 계정 매핑은 8단계 후보). `slug`↔Gitea repo name 1:1, 한글 표시제목은 Gitea description 에 합쳐 저장(Gitea 에 표시명 필드 없음).
|
||||
- **연동 비활성 폴백**: `GITEA_*` 미설정 시 `GiteaService.enabled=false` → 저장소 CRUD 가 Gitea 없이 동작. 로컬 개발/테스트에서 Gitea 없이도 기존 흐름 유지.
|
||||
- **저장소 생성 폼 보강**: ① **프로젝트 설명 필수화**(`CreateRepoDto.description` required, 프론트 검증 추가). ② **템플릿 복제** — `useTemplate=true` 면 Gitea generate API 로 `GITEA_TEMPLATE_OWNER/REPO`(기본 `TtiPo/project-base`, *template* 설정 필요) 저장소를 복제 생성(브랜치는 템플릿을 따름). 프론트는 `GET /repos/meta`(owner+template) 로 owner 고정표시·템플릿 옵션을 받아 하드코딩 제거.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useApi } from './useApi'
|
||||
import type { ApiRepo, CreateRepoPayload, UpdateRepoPayload } from '@/types/repo'
|
||||
import type {
|
||||
ApiRepo,
|
||||
CreateRepoPayload,
|
||||
RepoCreateMeta,
|
||||
UpdateRepoPayload,
|
||||
} from '@/types/repo'
|
||||
|
||||
// 저장소 도메인 API Composable — 인터셉터가 success/data 를 언래핑
|
||||
export function useRepo() {
|
||||
@@ -9,10 +14,9 @@ export function useRepo() {
|
||||
return (await api.get('/repos')) as unknown as ApiRepo[]
|
||||
}
|
||||
|
||||
// 현재 조직(소유자) — 생성 폼의 "소유자 고정" 표시용
|
||||
async function owner(): Promise<string> {
|
||||
const res = (await api.get('/repos/owner')) as unknown as { owner: string }
|
||||
return res.owner
|
||||
// 저장소 생성 폼 메타(소유자/템플릿) — 생성 전 미리 조회
|
||||
async function meta(): Promise<RepoCreateMeta> {
|
||||
return (await api.get('/repos/meta')) as unknown as RepoCreateMeta
|
||||
}
|
||||
|
||||
async function get(id: string): Promise<ApiRepo> {
|
||||
@@ -31,5 +35,5 @@ export function useRepo() {
|
||||
await api.delete(`/repos/${id}`)
|
||||
}
|
||||
|
||||
return { list, owner, get, create, update, remove }
|
||||
return { list, meta, get, create, update, remove }
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import type { Visibility } from '@/mock/relay.mock'
|
||||
const router = useRouter()
|
||||
const repoStore = useRepoStore()
|
||||
|
||||
// 현재 조직(소유자) — 백엔드 설정값(GITEA_ORG)을 받아 고정 표시
|
||||
const { owner } = storeToRefs(repoStore)
|
||||
// 생성 폼 메타 — 소유자(GITEA_ORG) 고정 표시 + 템플릿 가용 정보
|
||||
const { owner, templateAvailable, templateSlug } = storeToRefs(repoStore)
|
||||
const ownerInitial = computed(() => initialOf(owner.value || '?').toUpperCase())
|
||||
onMounted(() => {
|
||||
void repoStore.fetchOwner()
|
||||
void repoStore.fetchCreateMeta()
|
||||
})
|
||||
|
||||
// 폼 상태
|
||||
@@ -23,12 +23,11 @@ const repoName = ref('')
|
||||
const displayTitle = ref('')
|
||||
const visibility = ref<Visibility>('private')
|
||||
const description = ref('')
|
||||
const template = ref('없음 (빈 저장소)')
|
||||
// 템플릿 사용 여부 — 사용 시 설정된 템플릿 저장소(project-base)를 복제해 생성
|
||||
const useTemplate = ref(false)
|
||||
const branch = ref('main')
|
||||
const submitting = ref(false)
|
||||
|
||||
const templates = ['없음 (빈 저장소)', '기본 업무 템플릿', '디자인 프로젝트 템플릿', '개발 프로젝트 템플릿']
|
||||
|
||||
// 생성 — 성공 시 새 저장소 상세로 이동
|
||||
async function createRepo() {
|
||||
if (submitting.value) return
|
||||
@@ -46,6 +45,10 @@ async function createRepo() {
|
||||
window.alert('표시 제목을 입력해 주세요.')
|
||||
return
|
||||
}
|
||||
if (!description.value.trim()) {
|
||||
window.alert('프로젝트 설명을 입력해 주세요.')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
@@ -53,8 +56,9 @@ async function createRepo() {
|
||||
slug: repoName.value.trim(),
|
||||
name: displayTitle.value.trim(),
|
||||
visibility: visibility.value,
|
||||
description: description.value.trim() || undefined,
|
||||
description: description.value.trim(),
|
||||
branch: branch.value.trim() || undefined,
|
||||
useTemplate: useTemplate.value,
|
||||
})
|
||||
await router.push(`/repos/${repo.id}`)
|
||||
} catch {
|
||||
@@ -251,15 +255,16 @@ async function createRepo() {
|
||||
<!-- 프로젝트 설명 -->
|
||||
<div class="fblock">
|
||||
<div class="flabel">
|
||||
프로젝트 설명
|
||||
<span class="req">*</span> 프로젝트 설명
|
||||
</div>
|
||||
<div class="fhint">
|
||||
저장소 목록과 상단에 표시됩니다. (선택)
|
||||
저장소 목록과 상단에 표시됩니다. 프로젝트의 목적과 범위를 간단히 적어주세요.
|
||||
</div>
|
||||
<textarea
|
||||
v-model="description"
|
||||
class="tinput tarea"
|
||||
placeholder="이 프로젝트의 목적과 범위를 간단히 적어주세요."
|
||||
maxlength="300"
|
||||
placeholder="예: 3분기 신제품 런칭을 위한 콘텐츠·광고 제작 저장소"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -269,15 +274,19 @@ async function createRepo() {
|
||||
템플릿
|
||||
</div>
|
||||
<div class="fhint">
|
||||
선택한 템플릿의 폴더 구조와 기본 업무가 새 저장소에 복제됩니다.
|
||||
템플릿을 사용하면 해당 저장소의 폴더 구조·파일이 새 저장소에 그대로 복제됩니다.
|
||||
(기본 브랜치는 템플릿을 따릅니다)
|
||||
</div>
|
||||
<div class="select-wrap">
|
||||
<select v-model="template">
|
||||
<select v-model="useTemplate">
|
||||
<option :value="false">
|
||||
없음 (빈 저장소)
|
||||
</option>
|
||||
<option
|
||||
v-for="t in templates"
|
||||
:key="t"
|
||||
v-if="templateAvailable"
|
||||
:value="true"
|
||||
>
|
||||
{{ t }}
|
||||
{{ templateSlug }} 템플릿
|
||||
</option>
|
||||
</select>
|
||||
<span class="caret">
|
||||
|
||||
@@ -17,7 +17,7 @@ const { repos, loading, owner } = storeToRefs(repoStore)
|
||||
|
||||
onMounted(() => {
|
||||
void repoStore.fetchList()
|
||||
void repoStore.fetchOwner()
|
||||
void repoStore.fetchCreateMeta()
|
||||
})
|
||||
|
||||
// 검색어 + 공개여부 필터 적용
|
||||
|
||||
@@ -47,17 +47,20 @@ export const useRepoStore = defineStore('repo', () => {
|
||||
const repos = ref<RepoView[]>([])
|
||||
const current = ref<RepoView | null>(null)
|
||||
const loading = ref(false)
|
||||
// 현재 조직(소유자) — 백엔드 설정값(GITEA_ORG). 최초 1회 조회 후 캐시
|
||||
// 생성 폼 메타 — 소유자(GITEA_ORG)·템플릿 정보. 최초 1회 조회 후 캐시
|
||||
const owner = ref('')
|
||||
const templateAvailable = ref(false)
|
||||
const templateSlug = ref('')
|
||||
|
||||
const repoApi = useRepo()
|
||||
|
||||
// 조직(소유자) 조회 — 이미 있으면 재사용
|
||||
async function fetchOwner(): Promise<string> {
|
||||
if (!owner.value) {
|
||||
owner.value = await repoApi.owner()
|
||||
}
|
||||
return owner.value
|
||||
// 생성 폼 메타(소유자/템플릿) 조회 — 이미 있으면 재사용
|
||||
async function fetchCreateMeta(): Promise<void> {
|
||||
if (owner.value) return
|
||||
const meta = await repoApi.meta()
|
||||
owner.value = meta.owner
|
||||
templateAvailable.value = meta.template.available
|
||||
templateSlug.value = meta.template.slug
|
||||
}
|
||||
|
||||
// 목록 조회
|
||||
@@ -106,7 +109,9 @@ export const useRepoStore = defineStore('repo', () => {
|
||||
current,
|
||||
loading,
|
||||
owner,
|
||||
fetchOwner,
|
||||
templateAvailable,
|
||||
templateSlug,
|
||||
fetchCreateMeta,
|
||||
fetchList,
|
||||
fetchOne,
|
||||
create,
|
||||
|
||||
@@ -29,8 +29,15 @@ export interface CreateRepoPayload {
|
||||
slug: string // 영문 저장소 이름
|
||||
name: string // 표시 제목(한글)
|
||||
visibility: Visibility
|
||||
description?: string
|
||||
description: string // 프로젝트 설명(필수)
|
||||
branch?: string
|
||||
useTemplate?: boolean // 템플릿 저장소 복제 여부
|
||||
}
|
||||
|
||||
// 저장소 생성 폼 메타데이터 (소유자/템플릿)
|
||||
export interface RepoCreateMeta {
|
||||
owner: string
|
||||
template: { available: boolean; slug: string }
|
||||
}
|
||||
|
||||
// 저장소 수정 요청
|
||||
|
||||
Reference in New Issue
Block a user