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
-5
View File
@@ -1,5 +0,0 @@
# ==========================================
# 프론트엔드 고유 설정 예시 — 복사해 .env.development 생성
# VITE_API_BASE_URL 은 docker-compose 가 루트 .env 의 BACKEND_API_URL 을
# build arg 로 주입하므로 별도로 작성하지 않는다.
# ==========================================
-5
View File
@@ -1,5 +0,0 @@
# ==========================================
# 프론트엔드 고유 설정 예시 — 복사해 .env.production 생성
# VITE_API_BASE_URL 은 docker-compose 가 루트 .env 의 BACKEND_API_URL 을
# build arg 로 주입하므로 별도로 작성하지 않는다.
# ==========================================
+191
View File
@@ -0,0 +1,191 @@
<script setup lang="ts">
// Gitea 저장소 바 — 클론 주소 복사 + Gitea 웹에서 열기 (미연동 시 렌더 안 함)
import { computed, ref } from 'vue'
interface Props {
cloneUrl?: string | null
htmlUrl?: string | null
}
const props = defineProps<Props>()
// 둘 다 없으면 Gitea 미연동 — 표시하지 않음
const connected = computed(() => !!(props.cloneUrl || props.htmlUrl))
const copied = ref(false)
let copyTimer: ReturnType<typeof setTimeout> | null = null
// 클론 주소 클립보드 복사 — 성공 시 잠시 "복사됨" 표시
async function copyClone() {
if (!props.cloneUrl) return
try {
await navigator.clipboard.writeText(props.cloneUrl)
copied.value = true
if (copyTimer) clearTimeout(copyTimer)
copyTimer = setTimeout(() => (copied.value = false), 1500)
} catch {
// 클립보드 권한 없음 등 — 무시(주소는 화면에 노출되어 있음)
}
}
</script>
<template>
<div
v-if="connected"
class="gitea-bar"
>
<span class="gb-ico">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 17H7A5 5 0 0 1 7 7h2" />
<path d="M15 7h2a5 5 0 0 1 0 10h-2" />
<path d="M8 12h8" />
</svg>
</span>
<code
v-if="cloneUrl"
class="gb-url"
:title="cloneUrl"
>{{ cloneUrl }}</code>
<span
v-else
class="gb-url muted"
>클론 주소 없음</span>
<button
v-if="cloneUrl"
class="gb-btn"
type="button"
:aria-label="copied ? '복사됨' : '클론 주소 복사'"
@click="copyClone"
>
<svg
v-if="!copied"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
<svg
v-else
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 6 9 17l-5-5" />
</svg>
{{ copied ? '복사됨' : '복사' }}
</button>
<a
v-if="htmlUrl"
class="gb-btn open"
:href="htmlUrl"
target="_blank"
rel="noopener noreferrer"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<path d="M15 3h6v6M10 14 21 3" />
</svg>
Gitea에서 열기
</a>
</div>
</template>
<style scoped>
.gitea-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.gb-ico {
color: #1b7a3d;
display: grid;
place-items: center;
flex-shrink: 0;
}
.gb-ico svg {
width: 1rem;
height: 1rem;
}
.gb-url {
flex: 1;
min-width: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.781rem;
color: var(--text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gb-url.muted {
font-family: inherit;
color: var(--text-3);
}
.gb-btn {
display: inline-flex;
align-items: center;
gap: 0.3125rem;
height: 2rem;
padding: 0 0.6875rem;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
background: #fff;
color: var(--text-2);
font-family: inherit;
font-size: 0.781rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
text-decoration: none;
}
.gb-btn:hover {
background: #f7f8fa;
color: var(--text);
}
.gb-btn svg {
width: 0.875rem;
height: 0.875rem;
}
.gb-btn.open {
border-color: var(--accent-border);
color: var(--accent);
background: var(--accent-weak);
}
.gb-btn.open:hover {
background: var(--accent-weak);
color: var(--accent);
}
</style>
@@ -5,6 +5,7 @@ import { useRoute, useRouter, RouterLink } from 'vue-router'
import AppShell from '@/layouts/AppShell.vue'
import RepoHeader from '@/components/RepoHeader.vue'
import RepoTabs from '@/components/RepoTabs.vue'
import GiteaRepoBar from '@/components/GiteaRepoBar.vue'
import { useRepoStore } from '@/stores/repo.store'
import { type Repo, type Visibility } from '@/mock/relay.mock'
@@ -181,6 +182,11 @@ function renameRepo() {
/><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>
</span>
</div>
<GiteaRepoBar
:clone-url="repo.cloneUrl"
:html-url="repo.htmlUrl"
class="gitea-bar-spacing"
/>
</div>
<div class="fblock">
@@ -449,6 +455,10 @@ textarea.tarea {
height: auto;
}
.gitea-bar-spacing {
margin-top: 0.6875rem;
}
.readonly {
display: flex;
align-items: center;
+3
View File
@@ -39,6 +39,9 @@ function toRepoView(api: ApiRepo): RepoView {
progressPct: prog.pct,
progressLevel: prog.level,
progressLabel: prog.label,
cloneUrl: api.cloneUrl,
htmlUrl: api.htmlUrl,
giteaMissing: api.giteaMissing,
}
}