Files
sam-manage/docs/MNG_PROJECT_PLAN.md
hskwon 76c8a94e4f docs: MNG 프로젝트 문서 정비
- 개발 단계별 문서 추가 (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 업데이트
2025-11-30 21:04:19 +09:00

838 lines
27 KiB
Markdown

# 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/ (새 관리자) ← 최종 │
└─────────────────────────────┘
```
### 설계 원칙
1. **단순성**: 복잡한 추상화 금지, 인라인 코드 허용
2. **수정 용이성**: AI 없이도 Blade 템플릿 수정 가능
3. **코드 재사용**: admin/ 모델/서비스 복사 후 간소화
4. **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_* 접두사)
```php
// 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_* 접두사)
```php
// 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/ 복사 프로세스
```bash
# 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 제거)
```php
// 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 사용 철학
```blade
{{-- ✅ 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레벨 최대)
```blade
{{-- 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 사용 원칙 (최소화)
```blade
{{-- ✅ 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/`)
```bash
cd SAM
composer create-project laravel/laravel mng
cd mng
```
- [ ] `.env` 환경 변수 설정
```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 패키지 설치
```bash
composer require laravel/sanctum
composer require darkaonline/l5-swagger
composer require --dev laravel/pint
```
- [ ] Tailwind + DaisyUI + HTMX 설정
```bash
npm install -D tailwindcss daisyui @tailwindcss/forms
npm install htmx.org
```
```js
// tailwind.config.js
module.exports = {
plugins: [require('daisyui')],
daisyui: {
themes: ['light', 'dark'],
},
}
```
- [ ] Docker Nginx 설정 (mng.sam.kr)
```nginx
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/ 모델 복사
```bash
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)
```blade
{{-- 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/ 참고)
```php
// 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 레이아웃
```blade
{{-- 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>
```
#### 대시보드 컨트롤러
```php
// 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일)
```blade
{{-- 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: 테스트, 최적화
```
---
## 📚 참고 문서
- [DaisyUI Components](https://daisyui.com/components/)
- [Alpine.js Documentation](https://alpinejs.dev/)
- [Laravel 12 Blade](https://laravel.com/docs/12.x/blade)
---
## ✅ 다음 단계
### 즉시 시작 가능
- [ ] `mng/` Laravel 프로젝트 생성
- [ ] DaisyUI + Alpine.js 설치
- [ ] admin/ 모델 복사 및 Filament 제거
- [ ] 로그인 화면 구현
---
**작성일**: 2025-01-20
**버전**: 2.0
**상태**: 정책 반영 완료 ✅
**변경사항**:
- 폴더명: `adm2/` → `mng/`
- UI: DaisyUI + Blade + Alpine.js 확정
- DB: 기존 테이블 재사용, `admin_*`, `stat_*` 접두사
- 모델: admin/ 복사 후 Filament 제거
- 철학: 단순함, 수정 용이성 최우선