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:
2026-06-17 00:20:21 +09:00
parent 6652e79768
commit 7f432a0013
13 changed files with 173 additions and 51 deletions
+24 -15
View File
@@ -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">
+1 -1
View File
@@ -17,7 +17,7 @@ const { repos, loading, owner } = storeToRefs(repoStore)
onMounted(() => {
void repoStore.fetchList()
void repoStore.fetchOwner()
void repoStore.fetchCreateMeta()
})
// 검색어 + 공개여부 필터 적용