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:
@@ -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