feat: [roadmap] 로드맵 문서 페이지 추가
- sam/docs 중장기 계획 문서를 렌더링하는 전용 페이지
- 비전&전략, 프로젝트 런칭, 제품 설계, 시스템 개요 4개 카테고리
- Markdown → HTML 변환 (Str::markdown)
- /roadmap/documents 목록 + /roadmap/documents/{slug} 상세
This commit is contained in:
@@ -4,10 +4,112 @@
|
||||
|
||||
use App\Models\Admin\AdminRoadmapPlan;
|
||||
use App\Services\Roadmap\RoadmapPlanService;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RoadmapController extends Controller
|
||||
{
|
||||
private const DOCS_BASE = __DIR__.'/../../../../docs';
|
||||
|
||||
private const DOCUMENT_REGISTRY = [
|
||||
[
|
||||
'category' => 'vision',
|
||||
'category_label' => '비전 & 전략',
|
||||
'icon' => 'ri-lightbulb-line',
|
||||
'color' => 'indigo',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'ai-automation-vision',
|
||||
'title' => 'SAM AI 자동화 비전',
|
||||
'description' => 'SAM의 장기 비전과 AI 자동화 전략. 영업에서 출고까지 End-to-End 자동화 로드맵.',
|
||||
'path' => 'system/ai-automation-vision.md',
|
||||
'date' => '2026-03-02',
|
||||
'badge' => '설계 확정',
|
||||
],
|
||||
[
|
||||
'slug' => 'scaling-roadmap',
|
||||
'title' => '10,000 테넌트 스케일링 로드맵',
|
||||
'description' => '현재 아키텍처 진단부터 5단계 스케일링 계획까지. 세계 수준 엔지니어링 시나리오.',
|
||||
'path' => 'system/scaling-roadmap.md',
|
||||
'date' => '2026-02-22',
|
||||
'badge' => '가상 시나리오',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'launch',
|
||||
'category_label' => '프로젝트 런칭',
|
||||
'icon' => 'ri-rocket-line',
|
||||
'color' => 'blue',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'project-launch-roadmap',
|
||||
'title' => 'SAM 프로젝트 런칭 로드맵',
|
||||
'description' => '전체 시스템 구성, MVP 범위, 마일스톤(MS1~MS3), 개발 완료율 현황.',
|
||||
'path' => 'guides/project-launch-roadmap.md',
|
||||
'date' => '2025-12-02',
|
||||
'badge' => '진행중',
|
||||
],
|
||||
[
|
||||
'slug' => 'production-deployment-plan',
|
||||
'title' => '운영 환경 배포 계획서',
|
||||
'description' => 'MS3 정식 런칭 배포 계획. 무중단 전환, 롤백, Jenkins CI/CD 자동화.',
|
||||
'path' => 'plans/production-deployment-plan.md',
|
||||
'date' => '2026-02-22',
|
||||
'badge' => '계획 수립',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'product',
|
||||
'category_label' => '제품 설계',
|
||||
'icon' => 'ri-draft-line',
|
||||
'color' => 'green',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'erp-storyboard',
|
||||
'title' => 'SAM ERP 스토리보드 D1.4',
|
||||
'description' => '전체 ERP 메뉴 구조와 화면 설계. 대시보드, MES, HR, 전자결재, 회계, 구독 관리.',
|
||||
'path' => 'plans/SAM_ERP_Storyboard_D1.4.md',
|
||||
'date' => '2026-01-16',
|
||||
'badge' => 'D1.4',
|
||||
],
|
||||
[
|
||||
'slug' => 'erp-accounting-storyboard',
|
||||
'title' => 'SAM ERP 회계관리 스토리보드 D1.6',
|
||||
'description' => '세금계산서, 계좌 입출금, OCR, 일일 보고서, 건설/생산 대시보드.',
|
||||
'path' => 'plans/SAM_ERP_회계관리_Storyboard_D1.6.md',
|
||||
'date' => '2026-02-20',
|
||||
'badge' => 'D1.6',
|
||||
],
|
||||
[
|
||||
'slug' => 'integrated-master-plan',
|
||||
'title' => '통합 개선 마스터 플랜',
|
||||
'description' => '제품코드 추적성 + 검사 단위 구조 통합 개선. 7단계 Phase 로드맵.',
|
||||
'path' => 'plans/integrated-master-plan.md',
|
||||
'date' => '2026-02-27',
|
||||
'badge' => 'Phase 0~3 완료',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'category' => 'system',
|
||||
'category_label' => '시스템 개요',
|
||||
'icon' => 'ri-server-line',
|
||||
'color' => 'gray',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'system-overview',
|
||||
'title' => 'SAM 시스템 개요',
|
||||
'description' => '프로젝트 아키텍처, 기술 스택, 멀티테넌시, 레거시 마이그레이션 현황.',
|
||||
'path' => 'system/overview.md',
|
||||
'date' => '2026-02-27',
|
||||
'badge' => '최신',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly RoadmapPlanService $planService
|
||||
) {}
|
||||
@@ -76,4 +178,49 @@ public function editPlan(int $id): View
|
||||
'plan', 'statuses', 'categories', 'priorities', 'phases'
|
||||
));
|
||||
}
|
||||
|
||||
public function documents(): View
|
||||
{
|
||||
$registry = self::DOCUMENT_REGISTRY;
|
||||
|
||||
// 각 문서의 파일 존재 여부 확인
|
||||
$docsBase = realpath(self::DOCS_BASE) ?: self::DOCS_BASE;
|
||||
foreach ($registry as &$group) {
|
||||
foreach ($group['items'] as &$item) {
|
||||
$item['exists'] = file_exists($docsBase.'/'.$item['path']);
|
||||
}
|
||||
}
|
||||
|
||||
return view('roadmap.documents.index', compact('registry'));
|
||||
}
|
||||
|
||||
public function showDocument(string $slug): View
|
||||
{
|
||||
$document = null;
|
||||
foreach (self::DOCUMENT_REGISTRY as $group) {
|
||||
foreach ($group['items'] as $item) {
|
||||
if ($item['slug'] === $slug) {
|
||||
$document = $item;
|
||||
$document['category_label'] = $group['category_label'];
|
||||
$document['color'] = $group['color'];
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $document) {
|
||||
abort(404, '문서를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
$docsBase = realpath(self::DOCS_BASE) ?: self::DOCS_BASE;
|
||||
$filePath = $docsBase.'/'.$document['path'];
|
||||
$content = null;
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
$markdown = file_get_contents($filePath);
|
||||
$content = Str::markdown($markdown);
|
||||
}
|
||||
|
||||
return view('roadmap.documents.show', compact('document', 'content'));
|
||||
}
|
||||
}
|
||||
|
||||
67
resources/views/roadmap/documents/index.blade.php
Normal file
67
resources/views/roadmap/documents/index.blade.php
Normal file
@@ -0,0 +1,67 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '로드맵 문서')
|
||||
|
||||
@section('content')
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 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 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
로드맵 문서
|
||||
</h1>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ route('roadmap.index') }}" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm bg-white hover:bg-gray-100 text-gray-700 rounded-lg border transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
대시보드
|
||||
</a>
|
||||
<a href="{{ route('roadmap.plans.index') }}" class="inline-flex items-center gap-1 px-3 py-1.5 text-sm bg-white hover:bg-gray-100 text-gray-700 rounded-lg border transition">
|
||||
계획 목록
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 소개 -->
|
||||
<div class="bg-gradient-to-r from-indigo-50 to-blue-50 rounded-lg p-6 mb-8 border border-indigo-100">
|
||||
<h2 class="text-lg font-bold text-indigo-900 mb-2">SAM 중장기 계획 문서</h2>
|
||||
<p class="text-sm text-indigo-700">SAM 프로젝트의 비전, 로드맵, 제품 설계 문서를 한 곳에서 확인할 수 있습니다. 각 문서를 클릭하면 상세 내용을 볼 수 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 문서 카테고리별 목록 -->
|
||||
@foreach($registry as $group)
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-bold text-gray-800 flex items-center gap-2 mb-4">
|
||||
<i class="{{ $group['icon'] }} text-{{ $group['color'] }}-600"></i>
|
||||
{{ $group['category_label'] }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
@foreach($group['items'] as $item)
|
||||
<a href="{{ $item['exists'] ? route('roadmap.documents.show', $item['slug']) : '#' }}"
|
||||
class="block bg-white rounded-lg shadow-sm border border-gray-200 p-5 hover:shadow-md hover:border-{{ $group['color'] }}-300 transition group {{ !$item['exists'] ? 'opacity-50 cursor-not-allowed' : '' }}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<h3 class="text-sm font-bold text-gray-900 group-hover:text-{{ $group['color'] }}-700 transition leading-snug">{{ $item['title'] }}</h3>
|
||||
<span class="shrink-0 ml-2 px-2 py-0.5 text-xs rounded-full bg-{{ $group['color'] }}-100 text-{{ $group['color'] }}-700">{{ $item['badge'] }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mb-3 leading-relaxed">{{ $item['description'] }}</p>
|
||||
<div class="flex items-center justify-between text-xs text-gray-400">
|
||||
<span>{{ $item['date'] }}</span>
|
||||
@if($item['exists'])
|
||||
<span class="flex items-center gap-1 text-{{ $group['color'] }}-500">
|
||||
문서 보기
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-300">파일 없음</span>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endsection
|
||||
56
resources/views/roadmap/documents/show.blade.php
Normal file
56
resources/views/roadmap/documents/show.blade.php
Normal file
@@ -0,0 +1,56 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $document['title'])
|
||||
|
||||
@section('content')
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('roadmap.documents.index') }}" class="text-gray-500 hover:text-gray-700">
|
||||
← 로드맵 문서
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-gray-800">{{ $document['title'] }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-3 py-1 text-sm rounded-full bg-{{ $document['color'] }}-100 text-{{ $document['color'] }}-700">{{ $document['category_label'] }}</span>
|
||||
<span class="px-3 py-1 text-sm rounded-full bg-gray-100 text-gray-600">{{ $document['badge'] }}</span>
|
||||
<span class="text-sm text-gray-400">{{ $document['date'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($document['description'])
|
||||
<p class="text-gray-500 mb-6">{{ $document['description'] }}</p>
|
||||
@endif
|
||||
|
||||
<!-- 문서 본문 -->
|
||||
@if($content)
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
||||
<article class="prose prose-sm max-w-none
|
||||
prose-headings:text-gray-900 prose-headings:font-bold
|
||||
prose-h1:text-2xl prose-h1:border-b prose-h1:border-gray-200 prose-h1:pb-3 prose-h1:mb-6
|
||||
prose-h2:text-xl prose-h2:mt-8 prose-h2:mb-4
|
||||
prose-h3:text-lg prose-h3:mt-6 prose-h3:mb-3
|
||||
prose-p:text-gray-700 prose-p:leading-relaxed
|
||||
prose-a:text-indigo-600 prose-a:no-underline hover:prose-a:underline
|
||||
prose-code:text-indigo-700 prose-code:bg-indigo-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:before:content-none prose-code:after:content-none
|
||||
prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:rounded-lg prose-pre:overflow-x-auto
|
||||
prose-table:text-sm
|
||||
prose-th:bg-gray-50 prose-th:px-4 prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:text-gray-700
|
||||
prose-td:px-4 prose-td:py-2 prose-td:border-t prose-td:border-gray-200
|
||||
prose-blockquote:border-indigo-300 prose-blockquote:bg-indigo-50 prose-blockquote:rounded-r-lg prose-blockquote:py-1 prose-blockquote:px-4
|
||||
prose-strong:text-gray-900
|
||||
prose-li:text-gray-700
|
||||
prose-hr:border-gray-200 prose-hr:my-8">
|
||||
{!! $content !!}
|
||||
</article>
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
|
||||
<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 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p class="text-gray-500 mb-2">문서 파일을 찾을 수 없습니다.</p>
|
||||
<p class="text-sm text-gray-400">경로: <code class="bg-gray-100 px-2 py-0.5 rounded">docs/{{ $document['path'] }}</code></p>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
@@ -361,6 +361,8 @@
|
||||
// 중장기 계획 (Blade 화면만)
|
||||
Route::prefix('roadmap')->name('roadmap.')->group(function () {
|
||||
Route::get('/', [RoadmapController::class, 'index'])->name('index');
|
||||
Route::get('/documents', [RoadmapController::class, 'documents'])->name('documents.index');
|
||||
Route::get('/documents/{slug}', [RoadmapController::class, 'showDocument'])->name('documents.show');
|
||||
Route::get('/plans', [RoadmapController::class, 'plans'])->name('plans.index');
|
||||
Route::get('/plans/create', [RoadmapController::class, 'createPlan'])->name('plans.create');
|
||||
Route::get('/plans/{id}', [RoadmapController::class, 'showPlan'])->name('plans.show');
|
||||
|
||||
Reference in New Issue
Block a user