- 개발 단계별 문서 추가 (00_OVERVIEW ~ 06_PHASE) - 기술 표준 문서 추가 (99_TECHNICAL_STANDARDS) - 개발 프로세스 및 패턴 문서 추가 - API_FLOW_TESTER_DESIGN, DEV_PROCESS - HTMX_API_PATTERN, LAYOUT_PATTERN - SETUP_GUIDE, MNG_PROJECT_PLAN - 프로젝트 관리 문서 추가 (project-management/) - INDEX.md, MNG_CRITICAL_RULES.md 업데이트
27 KiB
27 KiB
MNG 프로젝트 개발 계획서
📋 프로젝트 개요
목적
- 문제점: 기존 admin/ (Filament v4)은 AI 없이 수정이 어려움
- 목표: 수정 용이한 Plain Laravel 기반 관리자 패널 구축
- 도메인: mng.sam.kr
- 철학: 단순함 > 복잡함, AI 없이도 수정 가능한 직관적 코드
핵심 전략
┌─────────────────────────────────────────────┐
│ MNG (mng.sam.kr) │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Web Routes │────▶│ Blade + HTMX │ │ ← DaisyUI (심플)
│ │ (세션 인증) │ │ (수정 용이) │ │
│ └──────────────┘ └─────────────────┘ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ API Routes │────▶│ Admin API │ │ ← 처음부터 분리
│ │ (토큰 인증) │ │ (관리자 전용) │ │
│ └──────────────┘ └─────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ Service Layer (비즈니스 로직) │ │ ← admin/ 복사
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ Models (admin/ 복사, Filament 제거) │ │
│ └──────────────────────────────────────┘ │
└─────────────────┬───────────────────────────┘
↓
┌─────────────────────────────┐
│ MySQL 8.0 (공유 DB) │
│ - admin/ (점차 deprecated) │
│ - api/ (외부 API) │
│ - mng/ (새 관리자) ← 최종 │
└─────────────────────────────┘
설계 원칙
- 단순성: 복잡한 추상화 금지, 인라인 코드 허용
- 수정 용이성: AI 없이도 Blade 템플릿 수정 가능
- 코드 재사용: admin/ 모델/서비스 복사 후 간소화
- DB 공유: 기존 테이블 최대한 활용
🏗️ 아키텍처 설계
1. 디렉토리 구조
SAM/
├── admin/ # Filament (점차 deprecated)
├── api/ # 외부 클라이언트 API
├── mng/ # ⭐ 운영 관리자 패널 (NEW)
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Controllers/
│ │ │ │ ├── Web/ # Blade 컨트롤러 (단순)
│ │ │ │ │ ├── Auth/
│ │ │ │ │ ├── Dashboard/
│ │ │ │ │ ├── User/
│ │ │ │ │ └── Product/
│ │ │ │ └── Api/ # Admin API (향후)
│ │ │ │ └── Admin/
│ │ │ ├── Requests/ # FormRequest (필수)
│ │ │ └── Middleware/
│ │ ├── Services/ # admin/ 복사 후 간소화
│ │ ├── Models/ # admin/ 복사, Filament 코드 제거
│ │ └── Traits/
│ │ ├── BelongsToTenant.php # admin/에서 복사
│ │ └── HasAuditLog.php # admin/에서 복사
│ ├── routes/
│ │ ├── web.php # Blade 라우트
│ │ └── api.php # Admin API (/api/admin/*)
│ ├── resources/
│ │ └── views/
│ │ ├── layouts/
│ │ │ ├── app.blade.php # 단순 레이아웃
│ │ │ └── guest.blade.php
│ │ ├── auth/ # 로그인 화면
│ │ ├── dashboard/ # 대시보드
│ │ ├── users/ # 사용자 관리
│ │ └── products/ # 제품 관리
│ ├── database/
│ │ └── migrations/
│ │ └── # 관리자 전용: admin_*
│ │ └── # 통계 전용: stat_*
│ ├── tests/
│ │ └── Feature/
│ └── .env
├── docker/
│ └── nginx/
│ └── mng.sam.kr.conf
└── claudedocs/
└── mng/
├── MNG_PROJECT_PLAN.md # 이 문서
├── API_SPEC.md # API 명세
└── PROGRESS.md # 진행 상황
2. 기술 스택 (확정)
| 레이어 | 기술 | 버전 | 비고 |
|---|---|---|---|
| 백엔드 | Laravel | 12.x | PHP 8.4+ |
| 인증 | Sanctum | 4.x | 세션 + 토큰 |
| DB | MySQL | 8.0 | admin, api와 공유 |
| 프론트엔드 | Blade + HTMX | 1.x | 단순, 수정 용이 |
| CSS | Tailwind CSS | 3.x | 기존과 통일 |
| UI 컴포넌트 | DaisyUI | 4.x | 심플, 클래스 기반 |
| 아이콘 | Heroicons | - | Tailwind 친화적 |
| 문서화 | L5-Swagger | - | Admin API 전용 |
| 테스트 | PHPUnit | - | Feature Test |
3. DB 테이블 명명 규칙 (변경)
기존 테이블 재사용 (마이그레이션 없음)
✅ users, roles, departments
✅ products, materials, bom_items
✅ menus, menu_role
✅ audit_logs, categories, files
✅ tenants
관리자 전용 테이블 (admin_* 접두사)
// database/migrations/2025_01_20_create_admin_settings_table.php
Schema::create('admin_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->string('type')->default('string'); // string, json, boolean
$table->timestamps();
});
// 예시 테이블
- admin_settings # 관리자 설정
- admin_logs # 관리자 작업 로그
- admin_preferences # 관리자 개인 설정
통계 테이블 (stat_* 접두사)
// database/migrations/2025_01_20_create_stat_daily_sales_table.php
Schema::create('stat_daily_sales', function (Blueprint $table) {
$table->id();
$table->date('date');
$table->decimal('total_amount', 15, 2);
$table->integer('order_count');
$table->timestamps();
$table->unique('date');
});
// 예시 테이블
- stat_daily_sales # 일별 매출 통계
- stat_inventory # 재고 통계
- stat_user_activity # 사용자 활동 통계
4. 모델/서비스 복사 전략
admin/ → mng/ 복사 프로세스
# 1. 모델 복사 (Filament 의존성 제거)
cp -r admin/app/Models/* mng/app/Models/
# Filament 관련 코드 제거 (getNavigationLabel, form, table 등)
# 2. Traits 복사 (그대로 사용)
cp admin/app/Traits/BelongsToTenant.php mng/app/Traits/
cp admin/app/Traits/HasAuditLog.php mng/app/Traits/
# 3. Services 복사 (있다면)
cp -r admin/app/Services/* mng/app/Services/
# 또는 신규 작성 (Service-First 원칙)
모델 예시 (Filament 제거)
// admin/app/Models/User.php (Before)
class User extends Authenticatable implements FilamentUser
{
use BelongsToTenant, HasAuditLog;
public static function form(Form $form): Form { ... } // ❌ 제거
public static function table(Table $table): Table { ... } // ❌ 제거
public function canAccessPanel(Panel $panel): bool { ... } // ❌ 제거
}
// mng/app/Models/User.php (After)
class User extends Authenticatable
{
use BelongsToTenant, HasAuditLog;
protected $fillable = [
'tenant_id', 'email', 'password', 'name',
'role_id', 'department_id', 'is_active',
];
// 순수 Eloquent 관계만 유지
public function role() { return $this->belongsTo(Role::class); }
public function department() { return $this->belongsTo(Department::class); }
}
🎨 UI 설계 원칙 (수정 용이성 최우선)
DaisyUI 사용 철학
{{-- ✅ GOOD: 단순하고 직관적 --}}
<button class="btn btn-primary">저장</button>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">제목</h2>
<p>내용</p>
</div>
</div>
{{-- ❌ BAD: 과도한 추상화 --}}
<x-custom-button variant="primary" size="large" />
<x-card-wrapper :config="$complexConfig" />
Blade 템플릿 구조 (2레벨 최대)
{{-- layouts/app.blade.php (레이아웃) --}}
<!DOCTYPE html>
<html data-theme="light">
<head>
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<div class="drawer lg:drawer-open">
{{-- 사이드바 --}}
<input id="drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-side">
<label for="drawer" class="drawer-overlay"></label>
<ul class="menu p-4 w-64 bg-base-200">
@foreach($menus as $menu)
<li><a href="{{ $menu->url }}">{{ __($menu->name) }}</a></li>
@endforeach
</ul>
</div>
{{-- 메인 컨텐츠 --}}
<div class="drawer-content">
<div class="navbar bg-base-100">
<div class="flex-1">
<a class="btn btn-ghost normal-case text-xl">MNG</a>
</div>
<div class="flex-none">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost">
{{ auth()->user()->name }}
</label>
<ul class="menu dropdown-content">
<li><a href="/logout">로그아웃</a></li>
</ul>
</div>
</div>
</div>
<main class="p-6">
@yield('content')
</main>
</div>
</div>
</body>
</html>
{{-- users/index.blade.php (페이지) --}}
@extends('layouts.app')
@section('content')
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">사용자 목록</h2>
{{-- Alpine.js 최소 사용 --}}
<div x-data="{ search: '' }">
<input x-model="search" type="text"
placeholder="검색..."
class="input input-bordered w-full" />
</div>
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
<th>이름</th>
<th>이메일</th>
<th>역할</th>
<th>작업</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->role->name }}</td>
<td>
<a href="/users/{{ $user->id }}/edit" class="btn btn-sm">수정</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{ $users->links() }} {{-- Pagination --}}
</div>
</div>
@endsection
Alpine.js 사용 원칙 (최소화)
{{-- ✅ GOOD: 단순 인터랙션 --}}
<div x-data="{ open: false }">
<button @click="open = !open" class="btn">메뉴 열기</button>
<div x-show="open" class="dropdown-content">메뉴 내용</div>
</div>
{{-- ❌ BAD: 복잡한 로직 (서버에서 처리) --}}
<div x-data="complexDataFetching()">
<div x-init="loadData()">...</div>
</div>
🚀 개발 로드맵
Phase 1: 인프라 구축 (1일)
체크리스트
- Laravel 12 프로젝트 생성 (
mng/)cd SAM composer create-project laravel/laravel mng cd mng .env환경 변수 설정APP_NAME=MNG APP_URL=http://mng.sam.kr DB_CONNECTION=pgsql DB_HOST=postgres DB_PORT=5432 DB_DATABASE=sam_db DB_USERNAME=sam_user DB_PASSWORD=sam_password- Composer 패키지 설치
composer require laravel/sanctum composer require darkaonline/l5-swagger composer require --dev laravel/pint - Tailwind + DaisyUI + HTMX 설정
npm install -D tailwindcss daisyui @tailwindcss/forms npm install htmx.org// tailwind.config.js module.exports = { plugins: [require('daisyui')], daisyui: { themes: ['light', 'dark'], }, } - Docker Nginx 설정 (mng.sam.kr)
server { listen 80; server_name mng.sam.kr; root /var/www/mng/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass mng:9000; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } } - admin/ 모델 복사
cp -r admin/app/Models/* mng/app/Models/ cp -r admin/app/Traits/* mng/app/Traits/ # Filament 관련 코드 제거 후 커밋
산출물
mng/디렉토리 (Git 독립 저장소)- DaisyUI + Alpine.js 환경
- 복사된 모델 (Filament 제거)
Phase 2: 인증 시스템 (2일)
로그인 화면 (DaisyUI)
{{-- resources/views/auth/login.blade.php --}}
<!DOCTYPE html>
<html data-theme="light">
<head>
<title>로그인 - MNG</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-base-200">
<div class="hero min-h-screen">
<div class="hero-content flex-col">
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title justify-center mb-4">MNG 로그인</h2>
<form method="POST" action="/login">
@csrf
<div class="form-control">
<label class="label">
<span class="label-text">이메일</span>
</label>
<input type="email" name="email"
placeholder="email@example.com"
class="input input-bordered"
required autofocus />
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">비밀번호</span>
</label>
<input type="password" name="password"
class="input input-bordered"
required />
</div>
@if ($errors->any())
<div class="alert alert-error mt-4">
{{ $errors->first() }}
</div>
@endif
<div class="form-control mt-6">
<button class="btn btn-primary">로그인</button>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
AuthService (admin/ 참고)
// app/Services/AuthService.php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthService
{
public function login(array $credentials): bool
{
return Auth::attempt($credentials);
}
public function logout(): void
{
Auth::logout();
}
public function createToken(array $credentials): ?string
{
$user = User::where('email', $credentials['email'])->first();
if (!$user || !Hash::check($credentials['password'], $user->password)) {
return null;
}
return $user->createToken('mng-token')->plainTextToken;
}
}
체크리스트
- LoginRequest (FormRequest)
- AuthService 작성
- Web 로그인 구현 (세션)
- API 로그인 구현 (토큰)
- BelongsToTenant 적용 확인
- Feature Test 작성
Phase 3: 대시보드 (1-2일)
DaisyUI Drawer 레이아웃
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html data-theme="light">
<head>
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<div class="drawer lg:drawer-open">
<input id="drawer" type="checkbox" class="drawer-toggle" />
{{-- 메인 컨텐츠 --}}
<div class="drawer-content flex flex-col">
{{-- 네비게이션 바 --}}
<div class="w-full navbar bg-base-300">
<div class="flex-none lg:hidden">
<label for="drawer" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-6 h-6 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</label>
</div>
<div class="flex-1 px-2 mx-2">MNG</div>
<div class="flex-none">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost">
{{ auth()->user()->name }}
</label>
<ul tabindex="0" class="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="/profile">프로필</a></li>
<li>
<form method="POST" action="/logout">
@csrf
<button type="submit">로그아웃</button>
</form>
</li>
</ul>
</div>
</div>
</div>
{{-- 페이지 컨텐츠 --}}
<main class="p-6 flex-1">
@yield('content')
</main>
</div>
{{-- 사이드바 --}}
<div class="drawer-side">
<label for="drawer" class="drawer-overlay"></label>
<ul class="menu p-4 w-64 bg-base-200 text-base-content">
@foreach($menus as $menu)
@if($menu->children->isEmpty())
<li>
<a href="{{ $menu->url }}"
class="{{ request()->is($menu->url) ? 'active' : '' }}">
{{ __($menu->name) }}
</a>
</li>
@else
<li>
<details>
<summary>{{ __($menu->name) }}</summary>
<ul>
@foreach($menu->children as $child)
<li><a href="{{ $child->url }}">{{ __($child->name) }}</a></li>
@endforeach
</ul>
</details>
</li>
@endif
@endforeach
</ul>
</div>
</div>
</body>
</html>
대시보드 컨트롤러
// app/Http/Controllers/Web/DashboardController.php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Services\MenuService;
class DashboardController extends Controller
{
public function __construct(
private MenuService $menuService
) {}
public function index()
{
$menus = $this->menuService->getMenusForUser(auth()->user());
return view('dashboard.index', compact('menus'));
}
}
체크리스트
- 레이아웃 템플릿 (DaisyUI Drawer)
- 메뉴 서비스 (MenuService)
- 역할별 메뉴 필터링
- 대시보드 메인 페이지
Phase 4: 핵심 기능 (주 단위)
4.1 사용자 관리 (3-5일)
{{-- resources/views/users/index.blade.php --}}
@extends('layouts.app')
@section('content')
<div class="space-y-4">
{{-- 헤더 --}}
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold">사용자 관리</h1>
<a href="/users/create" class="btn btn-primary">사용자 추가</a>
</div>
{{-- 검색/필터 --}}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<form method="GET" action="/users">
<div class="grid grid-cols-3 gap-4">
<div class="form-control">
<input type="text" name="search"
placeholder="이름 또는 이메일"
class="input input-bordered"
value="{{ request('search') }}" />
</div>
<div class="form-control">
<select name="role_id" class="select select-bordered">
<option value="">전체 역할</option>
@foreach($roles as $role)
<option value="{{ $role->id }}"
{{ request('role_id') == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</div>
<div class="form-control">
<button type="submit" class="btn btn-primary">검색</button>
</div>
</div>
</form>
</div>
</div>
{{-- 테이블 --}}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr>
<th>ID</th>
<th>이름</th>
<th>이메일</th>
<th>역할</th>
<th>부서</th>
<th>상태</th>
<th>작업</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr>
<td>{{ $user->id }}</td>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->role->name }}</td>
<td>{{ $user->department->name }}</td>
<td>
<span class="badge {{ $user->is_active ? 'badge-success' : 'badge-error' }}">
{{ $user->is_active ? '활성' : '비활성' }}
</span>
</td>
<td>
<div class="btn-group">
<a href="/users/{{ $user->id }}/edit" class="btn btn-sm">수정</a>
<button class="btn btn-sm btn-error"
onclick="confirmDelete({{ $user->id }})">삭제</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{ $users->links() }}
</div>
</div>
</div>
<script>
function confirmDelete(userId) {
if (confirm('정말 삭제하시겠습니까?')) {
document.getElementById('delete-form-' + userId).submit();
}
}
</script>
@endsection
체크리스트
- 사용자 목록 (검색, 필터, 페이징)
- 사용자 생성 (FormRequest)
- 사용자 수정
- 사용자 삭제 (Soft Delete)
- Feature Test
📊 데이터베이스 전략
DB 테이블 전략 (최종)
✅ 기존 테이블 재사용 (마이그레이션 없음)
- users, roles, departments
- products, materials
- menus, audit_logs
🆕 관리자 전용 (admin_*)
- admin_settings
- admin_logs
- admin_preferences
📊 통계 (stat_*)
- stat_daily_sales
- stat_inventory
- stat_user_activity
모델 관리 전략
초기 복사: admin/app/Models → mng/app/Models
Filament 제거: form(), table(), canAccessPanel() 등
이후 운영: mng/ 독립 (admin 점차 deprecated)
🛡️ 품질 관리
코드 품질 체크리스트
□ Service-First (비즈니스 로직 → Service)
□ FormRequest (컨트롤러 검증 금지)
□ BelongsToTenant (multi-tenant 스코프)
□ i18n 키 (하드코딩 금지)
□ Soft Delete (deleted_at)
□ 감사 로그 (HasAuditLog trait)
□ Feature Test
□ Pint (코드 스타일)
UI 수정 용이성 체크리스트
□ DaisyUI 클래스 직접 사용 (추상화 최소)
□ Alpine.js 단순 인터랙션만
□ Blade 템플릿 2레벨 이하
□ 인라인 Tailwind 허용
□ AI 없이 수정 가능
🎯 예상 타임라인
MVP (최소 기능 제품) - 2주
Day 1-2: Phase 1 (인프라) + admin/ 모델 복사
Day 3-4: Phase 2 (인증)
Day 5-6: Phase 3 (대시보드)
Day 7-14: Phase 4 (사용자, 역할, 제품 관리)
전체 기능 이식 - 4-6주
Week 3-4: 제품, 자재 관리
Week 5: 게시판, 통계
Week 6: 테스트, 최적화
📚 참고 문서
✅ 다음 단계
즉시 시작 가능
mng/Laravel 프로젝트 생성- DaisyUI + Alpine.js 설치
- admin/ 모델 복사 및 Filament 제거
- 로그인 화면 구현
작성일: 2025-01-20 버전: 2.0 상태: 정책 반영 완료 ✅ 변경사항:
- 폴더명:
adm2/→mng/ - UI: DaisyUI + Blade + Alpine.js 확정
- DB: 기존 테이블 재사용,
admin_*,stat_*접두사 - 모델: admin/ 복사 후 Filament 제거
- 철학: 단순함, 수정 용이성 최우선