Files
sam-manage/resources/views/roadmap/index.blade.php
김보곤 f3f1416004 feat: [roadmap] 중장기 계획 메뉴 및 전용 페이지 개발
- 모델: AdminRoadmapPlan, AdminRoadmapMilestone
- 서비스: RoadmapPlanService, RoadmapMilestoneService
- FormRequest: Store/Update Plan/Milestone 4개
- 컨트롤러: Blade(RoadmapController), API(Plan/Milestone) 3개
- 라우트: web.php, api.php에 roadmap 라우트 추가
- Blade 뷰: 대시보드, 목록, 생성, 수정, 상세, 파셜 테이블 6개
- HTMX 기반 필터링/페이지네이션, 마일스톤 인라인 추가/토글
2026-03-02 15:50:20 +09:00

175 lines
8.9 KiB
PHP

@extends('layouts.app')
@section('title', '중장기 계획 대시보드')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
중장기 계획 대시보드
</h1>
<div class="flex gap-2">
<a href="{{ route('roadmap.plans.index') }}" class="bg-white hover:bg-gray-100 text-gray-700 px-4 py-2 rounded-lg border transition">
목록 보기
</a>
<a href="{{ route('roadmap.plans.create') }}" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition">
+ 계획
</a>
</div>
</div>
<!-- 통계 카드 -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<!-- 전체 -->
<div class="bg-white rounded-lg shadow-sm p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 mb-1">전체 계획</p>
<p class="text-3xl font-bold text-gray-800">{{ $summary['stats']['total'] }}</p>
</div>
<div class="w-11 h-11 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
</div>
</div>
<!-- 진행중 -->
<div class="bg-white rounded-lg shadow-sm p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 mb-1">진행중</p>
<p class="text-3xl font-bold text-blue-600">{{ $summary['stats']['in_progress'] }}</p>
</div>
<div class="w-11 h-11 bg-blue-100 rounded-full flex items-center justify-center text-blue-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</div>
<!-- 완료 -->
<div class="bg-white rounded-lg shadow-sm p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 mb-1">완료</p>
<p class="text-3xl font-bold text-green-600">{{ $summary['stats']['completed'] }}</p>
</div>
<div class="w-11 h-11 bg-green-100 rounded-full flex items-center justify-center text-green-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
<!-- 지연 -->
<div class="bg-white rounded-lg shadow-sm p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 mb-1">지연</p>
<p class="text-3xl font-bold text-red-600">{{ $summary['stats']['delayed'] }}</p>
</div>
<div class="w-11 h-11 bg-red-100 rounded-full flex items-center justify-center text-red-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
</div>
<!-- 로드맵 타임라인 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-bold text-gray-800">로드맵 타임라인</h2>
<div class="flex gap-1" id="phaseTabs">
<button onclick="filterPhase(null)" class="phase-tab px-3 py-1.5 text-sm rounded-lg bg-indigo-600 text-white transition" data-phase="all">전체</button>
@foreach($phases as $key => $label)
<button onclick="filterPhase('{{ $key }}')" class="phase-tab px-3 py-1.5 text-sm rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700 transition" data-phase="{{ $key }}">{{ Str::before($label, ' —') }}</button>
@endforeach
</div>
</div>
<div id="timelineContainer">
@foreach($summary['timeline'] as $phaseKey => $phaseData)
@if($phaseData['plans']->count() > 0)
<div class="phase-group mb-6" data-phase="{{ $phaseKey }}">
<h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-3">{{ $phaseData['label'] }}</h3>
<div class="space-y-3">
@foreach($phaseData['plans'] as $plan)
<a href="{{ route('roadmap.plans.show', $plan->id) }}" class="block border border-gray-200 rounded-lg p-4 hover:border-indigo-300 hover:shadow-sm transition">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full" style="background-color: {{ $plan->color }}"></span>
<span class="font-medium text-gray-900">{{ $plan->title }}</span>
<span class="px-2 py-0.5 text-xs rounded-full {{ $plan->status_color }}">{{ $plan->status_label }}</span>
<span class="px-2 py-0.5 text-xs rounded-full {{ $plan->priority_color }}">{{ $plan->priority_label }}</span>
</div>
<span class="text-sm text-gray-500">{{ $plan->period }}</span>
</div>
@if($plan->description)
<p class="text-sm text-gray-500 mb-2">{{ Str::limit($plan->description, 80) }}</p>
@endif
<div class="flex items-center gap-3">
<div class="flex-1 bg-gray-200 rounded-full h-2.5 overflow-hidden">
<div class="h-2.5 rounded-full transition-all" style="width: {{ $plan->progress }}%; background-color: {{ $plan->color }}"></div>
</div>
<span class="text-sm font-medium text-gray-700 whitespace-nowrap" style="min-width: 40px; text-align: right;">{{ $plan->progress }}%</span>
</div>
</a>
@endforeach
</div>
</div>
@endif
@endforeach
@if(collect($summary['timeline'])->every(fn($d) => $d['plans']->count() === 0))
<div class="text-center py-12 text-gray-400">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<p>등록된 계획이 없습니다.</p>
<a href="{{ route('roadmap.plans.create') }}" class="inline-block mt-4 text-indigo-600 hover:text-indigo-700 font-medium">
+ 번째 계획 등록하기
</a>
</div>
@endif
</div>
</div>
@endsection
@push('scripts')
<script>
function filterPhase(phase) {
// 탭 활성 상태
document.querySelectorAll('.phase-tab').forEach(tab => {
tab.classList.remove('bg-indigo-600', 'text-white');
tab.classList.add('bg-gray-100', 'text-gray-700');
});
const activeTab = phase
? document.querySelector(`.phase-tab[data-phase="${phase}"]`)
: document.querySelector('.phase-tab[data-phase="all"]');
if (activeTab) {
activeTab.classList.remove('bg-gray-100', 'text-gray-700');
activeTab.classList.add('bg-indigo-600', 'text-white');
}
// Phase 그룹 필터링
document.querySelectorAll('.phase-group').forEach(group => {
if (!phase || group.dataset.phase === phase) {
group.style.display = '';
} else {
group.style.display = 'none';
}
});
}
</script>
@endpush