기능구현중
This commit is contained in:
299
frontend/project/[id]/commits/index.vue
Normal file
299
frontend/project/[id]/commits/index.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader />
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<!-- 헤더 -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">
|
||||
<NuxtLink :to="`/project/${projectId}`" class="text-decoration-none text-muted me-2">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</NuxtLink>
|
||||
<i class="bi bi-git me-2"></i>커밋 내역
|
||||
</h4>
|
||||
<button class="btn btn-outline-primary" @click="refreshCommits" :disabled="isRefreshing">
|
||||
<span v-if="isRefreshing" class="spinner-border spinner-border-sm me-1"></span>
|
||||
<i v-else class="bi bi-arrow-clockwise me-1"></i>새로고침
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 필터 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">시작일</label>
|
||||
<input type="date" class="form-control form-control-sm" v-model="filters.startDate" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">종료일</label>
|
||||
<input type="date" class="form-control form-control-sm" v-model="filters.endDate" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">저장소</label>
|
||||
<select class="form-select form-select-sm" v-model="filters.repoId">
|
||||
<option value="">전체</option>
|
||||
<option v-for="r in repositories" :key="r.repoId" :value="r.repoId">
|
||||
[{{ r.serverType }}] {{ r.repoName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">작성자</label>
|
||||
<select class="form-select form-select-sm" v-model="filters.authorId">
|
||||
<option value="">전체</option>
|
||||
<option v-for="a in authors" :key="a.employeeId" :value="a.employeeId">
|
||||
{{ a.employeeName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary btn-sm w-100" @click="loadCommits">
|
||||
<i class="bi bi-search me-1"></i>검색
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 -->
|
||||
<div class="row mb-4" v-if="stats">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body py-3">
|
||||
<h3 class="mb-1 text-primary">{{ stats.commitCount }}</h3>
|
||||
<small class="text-muted">총 커밋</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body py-3">
|
||||
<h3 class="mb-1 text-success">+{{ formatNumber(stats.totalInsertions) }}</h3>
|
||||
<small class="text-muted">추가된 라인</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body py-3">
|
||||
<h3 class="mb-1 text-danger">-{{ formatNumber(stats.totalDeletions) }}</h3>
|
||||
<small class="text-muted">삭제된 라인</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body py-3">
|
||||
<h3 class="mb-1 text-info">{{ stats.authorCount }}</h3>
|
||||
<small class="text-muted">참여자</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 커밋 목록 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<strong>커밋 목록</strong>
|
||||
<span class="text-muted ms-2" v-if="pagination">({{ pagination.total }}건)</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div v-if="isLoading" class="text-center py-5">
|
||||
<div class="spinner-border text-primary"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="commits.length === 0" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||
커밋 내역이 없습니다.
|
||||
</div>
|
||||
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:140px">날짜</th>
|
||||
<th style="width:150px">저장소</th>
|
||||
<th style="width:120px">작성자</th>
|
||||
<th>커밋 메시지</th>
|
||||
<th style="width:100px" class="text-end">변경</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="c in commits" :key="c.commitId">
|
||||
<td class="small">{{ formatDateTime(c.commitDate) }}</td>
|
||||
<td>
|
||||
<span :class="getServerBadgeClass(c.serverType)" class="me-1">{{ c.serverType }}</span>
|
||||
<span class="small">{{ c.repoName }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="c.employeeName">{{ c.employeeName }}</span>
|
||||
<span v-else class="text-muted small">{{ c.commitAuthor }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="me-2 text-muted">{{ c.commitHash }}</code>
|
||||
{{ c.commitMessage }}
|
||||
</td>
|
||||
<td class="text-end small">
|
||||
<span class="text-success" v-if="c.insertions">+{{ c.insertions }}</span>
|
||||
<span class="text-danger ms-1" v-if="c.deletions">-{{ c.deletions }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
<div class="card-footer" v-if="pagination && pagination.totalPages > 1">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
<li class="page-item" :class="{ disabled: pagination.page <= 1 }">
|
||||
<a class="page-link" href="#" @click.prevent="goToPage(pagination.page - 1)">이전</a>
|
||||
</li>
|
||||
<li class="page-item" v-for="p in visiblePages" :key="p" :class="{ active: p === pagination.page }">
|
||||
<a class="page-link" href="#" @click.prevent="goToPage(p)">{{ p }}</a>
|
||||
</li>
|
||||
<li class="page-item" :class="{ disabled: pagination.page >= pagination.totalPages }">
|
||||
<a class="page-link" href="#" @click.prevent="goToPage(pagination.page + 1)">다음</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => parseInt(route.params.id as string))
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const commits = ref<any[]>([])
|
||||
const pagination = ref<any>(null)
|
||||
const stats = ref<any>(null)
|
||||
const repositories = ref<any[]>([])
|
||||
const authors = ref<any[]>([])
|
||||
|
||||
// 기본 필터: 최근 2주
|
||||
const today = new Date()
|
||||
const twoWeeksAgo = new Date(today)
|
||||
twoWeeksAgo.setDate(today.getDate() - 14)
|
||||
|
||||
const filters = ref({
|
||||
startDate: twoWeeksAgo.toISOString().split('T')[0],
|
||||
endDate: today.toISOString().split('T')[0],
|
||||
repoId: '',
|
||||
authorId: ''
|
||||
})
|
||||
|
||||
// 저장소 목록 로드
|
||||
async function loadRepositories() {
|
||||
try {
|
||||
const data = await $fetch(`/api/repository/list?projectId=${projectId.value}`)
|
||||
repositories.value = data.repositories || []
|
||||
} catch (e) {
|
||||
console.error('저장소 목록 로드 실패:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 작성자 목록 (프로젝트 멤버)
|
||||
async function loadAuthors() {
|
||||
try {
|
||||
const data = await $fetch(`/api/project/${projectId.value}/members`)
|
||||
authors.value = data.members || []
|
||||
} catch (e) {
|
||||
// 멤버 API가 없을 수 있음
|
||||
authors.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 커밋 목록 로드
|
||||
async function loadCommits(page = 1) {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
if (filters.value.startDate) params.append('startDate', filters.value.startDate)
|
||||
if (filters.value.endDate) params.append('endDate', filters.value.endDate)
|
||||
if (filters.value.repoId) params.append('repoId', filters.value.repoId)
|
||||
if (filters.value.authorId) params.append('authorId', filters.value.authorId)
|
||||
params.append('page', String(page))
|
||||
params.append('limit', '50')
|
||||
|
||||
const data = await $fetch(`/api/project/${projectId.value}/commits?${params}`)
|
||||
commits.value = data.commits || []
|
||||
pagination.value = data.pagination
|
||||
stats.value = data.stats
|
||||
} catch (e: any) {
|
||||
console.error('커밋 목록 로드 실패:', e)
|
||||
alert(e.data?.message || '커밋 목록을 불러오는데 실패했습니다.')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 새로고침 (동기화)
|
||||
async function refreshCommits() {
|
||||
if (!confirm('저장소를 동기화하시겠습니까? 잠시 시간이 걸릴 수 있습니다.')) return
|
||||
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
const result = await $fetch(`/api/project/${projectId.value}/commits/refresh`, { method: 'POST' })
|
||||
alert(result.message || '동기화 완료')
|
||||
await loadCommits()
|
||||
} catch (e: any) {
|
||||
alert(e.data?.message || '동기화 실패')
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 이동
|
||||
function goToPage(page: number) {
|
||||
if (page < 1 || (pagination.value && page > pagination.value.totalPages)) return
|
||||
loadCommits(page)
|
||||
}
|
||||
|
||||
// 보이는 페이지 번호
|
||||
const visiblePages = computed(() => {
|
||||
if (!pagination.value) return []
|
||||
const total = pagination.value.totalPages
|
||||
const current = pagination.value.page
|
||||
const pages: number[] = []
|
||||
|
||||
let start = Math.max(1, current - 2)
|
||||
let end = Math.min(total, current + 2)
|
||||
|
||||
if (end - start < 4) {
|
||||
if (start === 1) end = Math.min(total, 5)
|
||||
else start = Math.max(1, total - 4)
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) pages.push(i)
|
||||
return pages
|
||||
})
|
||||
|
||||
// 포맷터
|
||||
function formatDateTime(date: string) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function formatNumber(num: number) {
|
||||
if (!num) return '0'
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
function getServerBadgeClass(type: string) {
|
||||
return type === 'GIT' ? 'badge bg-success' : 'badge bg-warning text-dark'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRepositories()
|
||||
loadAuthors()
|
||||
loadCommits()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user