# MNG 기술 표준 문서

**목적:** 모든 Phase에서 일관되게 적용할 기술 표준 및 개발 규칙 정의 (SAM API Rules 기반)

## 📋 목차

1. [아키텍처 패턴 (SAM API Rules)](#1-아키텍처-패턴-sam-api-rules)
2. [코딩 컨벤션](#2-코딩-컨벤션)
3. [데이터베이스 설계 원칙](#3-데이터베이스-설계-원칙)
4. [보안 정책](#4-보안-정책)
5. [테스트 전략](#5-테스트-전략)
6. [성능 최적화](#6-성능-최적화)
7. [배포 프로세스](#7-배포-프로세스)

---

## 1. 아키텍처 패턴 (SAM API Rules)

### 1.1 Service-First Pattern (필수)

**원칙:** 모든 비즈니스 로직은 Service 클래스에 위치 (SAM API Rule #1)

```
[Request]
    ↓
[Route]
    ↓
[Controller] (DI 주입, Service 호출만)
    ↓
[FormRequest] (유효성 검증 - SAM API Rule #8)
    ↓
[Service] (비즈니스 로직, tenantId()/apiUserId() 필수)
    ↓
[Model] (Eloquent ORM, BelongsToTenant)
    ↓
[Database]
```

**Controller 예시:** (SAM API Rule #5)
```php
class UserController extends Controller
{
    public function __construct(private UserService $userService) {}

    public function index(Request $request)
    {
        // Controller는 Service 호출만
        $users = $this->userService->list($request->all());
        return view('users.index', compact('users'));
    }

    public function store(StoreUserRequest $request)
    {
        // FormRequest 검증 완료 데이터만 Service로 전달
        $user = $this->userService->create($request->validated());
        return redirect()->route('users.index')
            ->with('success', __('message.user.created')); // i18n 키 필수
    }
}
```

**Service 예시:** (SAM API Rule #5)
```php
namespace App\Services;

use App\Services\Service as BaseService; // Base Service 상속 필수

class UserService extends BaseService
{
    // tenantId(), apiUserId() 필수 설정 (SAM API Rule #1)
    public function list(array $filters): LengthAwarePaginator
    {
        $query = User::query();

        // Multi-tenant 필터링 (BelongsToTenant global scope 자동 적용)
        if (isset($filters['search'])) {
            $query->where(function($q) use ($filters) {
                $q->where('name', 'like', "%{$filters['search']}%")
                  ->orWhere('email', 'like', "%{$filters['search']}%");
            });
        }

        return $query->paginate(20);
    }

    public function create(array $data): User
    {
        DB::beginTransaction();
        try {
            $user = User::create([
                'tenant_id' => $this->tenantId(), // Base Service에서 제공
                'name' => $data['name'],
                'email' => $data['email'],
                'password' => Hash::make($data['password']),
                'created_by' => $this->apiUserId(), // Base Service에서 제공
            ]);

            if (isset($data['roles'])) {
                $user->assignRole($data['roles']);
            }

            DB::commit();
            return $user;
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}
```

### 1.2 FormRequest Pattern (필수)

**원칙:** Controller에서 검증 금지, FormRequest 사용 (SAM API Rule #8)

```php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        // 권한 체크 (RBAC)
        return $this->user()->can('users.create');
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|min:8|confirmed',
            'roles' => 'array',
            'roles.*' => 'exists:roles,id',
        ];
    }

    public function messages(): array
    {
        // i18n 키 사용 (SAM API Rule #6)
        return [
            'name.required' => __('validation.required', ['attribute' => __('users.name')]),
            'email.unique' => __('validation.unique', ['attribute' => __('users.email')]),
        ];
    }
}
```

**공통 Request 재사용:** (SAM API Rule #8)
```php
// app/Http/Requests/PaginateRequest.php
class PaginateRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'page' => 'integer|min:1',
            'per_page' => 'integer|min:1|max:100',
            'sort' => 'string',
            'order' => 'in:asc,desc',
        ];
    }
}

// 사용 예시
public function index(PaginateRequest $request)
{
    $users = $this->userService->list($request->validated());
}
```

### 1.3 Repository Pattern (선택적)

**사용 시기:** 복잡한 쿼리, 여러 모델 조인, 재사용성 높은 쿼리

```php
namespace App\Repositories;

class UserRepository
{
    public function findWithRolesAndDepartment(int $id): ?User
    {
        return User::with(['roles', 'department'])
            ->find($id);
    }

    public function getActiveUsersWithRecentActivity(int $days = 30): Collection
    {
        return User::where('is_active', true)
            ->where('last_login_at', '>=', now()->subDays($days))
            ->orderBy('last_login_at', 'desc')
            ->get();
    }
}
```

---

## 2. 코딩 컨벤션

### 2.1 네이밍 규칙

```php
// 클래스: PascalCase
class UserService {}
class StoreUserRequest {}

// 메서드: camelCase
public function createUser() {}
public function getUserById() {}

// 변수: camelCase
$userName = 'John';
$isActive = true;

// 상수: UPPER_SNAKE_CASE
const MAX_USERS = 100;
const DEFAULT_ROLE = 'user';

// 데이터베이스: snake_case
// 테이블: 복수형
users, sales_opportunities, quotation_items

// 컬럼: snake_case
user_id, created_at, is_active, tenant_id
```

### 2.2 i18n (국제화) - SAM API Rule #6

**원칙:** 한글 직접 사용 금지, 언어 키 사용 필수

```php
// ❌ 잘못된 예
return redirect()->back()->with('success', '사용자가 생성되었습니다.');

// ✅ 올바른 예 (SAM API Rule #6)
return redirect()->back()->with('success', __('message.user.created'));
```

**언어 파일 구조:**
```
lang/
├── ko/
│   ├── message.php    // 성공 메시지
│   ├── error.php      // 에러 메시지
│   ├── validation.php // 검증 메시지
│   └── users.php      // 도메인별 메시지
└── en/
    ├── message.php
    ├── error.php
    ├── validation.php
    └── users.php
```

**`lang/ko/message.php` 예시:** (SAM API Rule #6)
```php
return [
    // 공통 메시지
    'fetched' => '조회되었습니다.',
    'created' => '생성되었습니다.',
    'updated' => '수정되었습니다.',
    'deleted' => '삭제되었습니다.',
    'bulk_upsert' => '일괄 저장되었습니다.',
    'reordered' => '순서가 변경되었습니다.',

    // 도메인별 메시지 (선택적)
    'user' => [
        'created' => '사용자가 생성되었습니다.',
        'updated' => '사용자 정보가 수정되었습니다.',
    ],
];
```

**`lang/ko/error.php` 예시:**
```php
return [
    'not_found' => '데이터를 찾을 수 없습니다.',
    'unauthorized' => '권한이 없습니다.',
    'validation_failed' => '입력값 검증에 실패했습니다.',
];
```

### 2.3 코드 스타일 (SAM API Rule #7)

**Laravel Pint 사용 (자동 포맷팅):**
```bash
./vendor/bin/pint
```

**주석 규칙:**
```php
/**
 * 사용자를 생성합니다.
 *
 * @param array $data 사용자 데이터
 * @return User 생성된 사용자
 * @throws \Exception 생성 실패 시
 */
public function create(array $data): User
{
    // 복잡한 로직 설명이 필요한 경우에만 인라인 주석
    // 단순 코드는 주석 없이 자명하게 작성
}
```

---

## 3. 데이터베이스 설계 원칙

### 3.1 Multi-tenant 필수 컬럼 (SAM API Rule #2)

```sql
CREATE TABLE example_table (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    tenant_id BIGINT UNSIGNED NOT NULL COMMENT '소속 테넌트 (필수)',
    -- 기타 컬럼...
    created_by BIGINT UNSIGNED NULL COMMENT '생성자',
    updated_by BIGINT UNSIGNED NULL COMMENT '수정자',
    deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL COMMENT 'Soft Delete',

    INDEX idx_tenant_id (tenant_id),
    INDEX idx_deleted_at (deleted_at),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

**Model에 BelongsToTenant trait 적용:** (SAM API Rule #2)
```php
namespace App\Models;

use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\SoftDeletes;

class ExampleModel extends Model
{
    use BelongsToTenant, SoftDeletes;

    protected $fillable = ['tenant_id', 'name', ...];
}
```

**BelongsToTenant trait:**
```php
namespace App\Traits;

use App\Scopes\TenantScope;

trait BelongsToTenant
{
    protected static function bootBelongsToTenant()
    {
        // Global Scope 적용 (자동 tenant_id 필터링)
        static::addGlobalScope(new TenantScope);

        // Model 생성 시 자동으로 tenant_id 설정
        static::creating(function ($model) {
            if (!$model->tenant_id && auth()->check()) {
                $model->tenant_id = auth()->user()->tenant_id;
            }
        });
    }

    public function tenant()
    {
        return $this->belongsTo(Tenant::class);
    }
}
```

### 3.2 Soft Delete 기본 정책 (SAM API Rule #2)

**원칙:** 모든 테넌트 데이터는 Soft Delete 적용

```php
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model
{
    use SoftDeletes;

    protected $dates = ['deleted_at'];
}
```

### 3.3 Audit Log (감사 로그) - SAM API Rule #9

**원칙:** 모든 CUD 작업 자동 기록 (13개월 보관)

**audit_logs 테이블:**
```sql
CREATE TABLE audit_logs (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    tenant_id BIGINT UNSIGNED NULL COMMENT '소속 테넌트',
    target_type VARCHAR(255) NOT NULL COMMENT '대상 모델',
    target_id BIGINT UNSIGNED NOT NULL COMMENT '대상 ID',
    action VARCHAR(50) NOT NULL COMMENT 'created, updated, deleted 등',
    before_values JSON NULL COMMENT '변경 전 데이터',
    after_values JSON NULL COMMENT '변경 후 데이터',
    actor_id BIGINT UNSIGNED NULL COMMENT '작업자 ID',
    ip_address VARCHAR(45) NULL,
    user_agent TEXT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_tenant_id (tenant_id),
    INDEX idx_target (target_type, target_id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

**Observer 패턴 사용:**
```php
namespace App\Observers;

use App\Models\AuditLog;

class UserObserver
{
    public function created(User $user)
    {
        AuditLog::create([
            'tenant_id' => $user->tenant_id,
            'target_type' => User::class,
            'target_id' => $user->id,
            'action' => 'created',
            'before_values' => null,
            'after_values' => $user->toArray(),
            'actor_id' => auth()->id(),
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);
    }

    public function updated(User $user)
    {
        AuditLog::create([
            'tenant_id' => $user->tenant_id,
            'target_type' => User::class,
            'target_id' => $user->id,
            'action' => 'updated',
            'before_values' => $user->getOriginal(),
            'after_values' => $user->getChanges(),
            'actor_id' => auth()->id(),
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);
    }

    public function deleted(User $user)
    {
        AuditLog::create([
            'tenant_id' => $user->tenant_id,
            'target_type' => User::class,
            'target_id' => $user->id,
            'action' => 'deleted',
            'before_values' => $user->toArray(),
            'after_values' => null,
            'actor_id' => auth()->id(),
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
        ]);
    }
}
```

**AppServiceProvider에 등록:**
```php
public function boot()
{
    User::observe(UserObserver::class);
    Client::observe(ClientObserver::class);
    // 기타 모델...
}
```

**13개월 보관 정책 (Scheduler):**
```php
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // 13개월 이전 감사 로그 삭제
    $schedule->command('audit:prune')->monthlyOn(1, '02:00');
}

// app/Console/Commands/PruneAuditLogs.php
public function handle()
{
    $retentionMonths = 13;
    $cutoffDate = now()->subMonths($retentionMonths);

    $deleted = AuditLog::where('created_at', '<', $cutoffDate)->delete();

    $this->info("Pruned {$deleted} audit log records older than {$retentionMonths} months.");
}
```

### 3.4 인덱스 전략

```sql
-- 필수 인덱스
INDEX idx_tenant_id (tenant_id)  -- Multi-tenant 필터링
INDEX idx_created_at (created_at) -- 날짜 정렬
INDEX idx_deleted_at (deleted_at) -- Soft Delete 필터링

-- 검색 인덱스
INDEX idx_email (email)           -- 이메일 검색
FULLTEXT idx_search (title, content) -- 전문 검색

-- 복합 인덱스
INDEX idx_tenant_status (tenant_id, status) -- 테넌트 + 상태 필터
```

---

## 4. 보안 정책

### 4.1 인증 (Authentication)

**Laravel Sanctum 사용:**
```php
// config/sanctum.php
'expiration' => 60 * 24, // 24시간

// 로그인 (MNG는 세션 기반, API는 토큰 기반)
public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (!Auth::attempt($credentials)) {
        throw ValidationException::withMessages([
            'email' => [__('error.auth.failed')],
        ]);
    }

    $user = Auth::user();

    // 세션 기반 로그인 (MNG)
    return redirect()->route('dashboard');

    // 또는 API 토큰 발급 (필요시)
    // $token = $user->createToken('mng-token')->plainTextToken;
    // return response()->json(['token' => $token, 'user' => $user]);
}
```

### 4.2 권한 (Authorization) - RBAC

**Spatie Laravel Permission 사용:**
```php
// Permission 체크
if ($user->can('users.create')) {
    // 권한 있음
}

// Middleware
Route::group(['middleware' => ['auth', 'can:users.index']], function () {
    Route::get('/users', [UserController::class, 'index']);
});

// Blade
@can('users.create')
    <a href="{{ route('users.create') }}">새 사용자</a>
@endcan
```

### 4.3 XSS 방지

**Blade 자동 이스케이프:**
```blade
{{-- 자동 이스케이프 (안전) --}}
{{ $user->name }}

{{-- 이스케이프 없음 (주의) --}}
{!! $htmlContent !!}
```

### 4.4 CSRF 보호

**모든 POST/PUT/DELETE 요청에 @csrf:**
```blade
<form method="POST" action="{{ route('users.store') }}">
    @csrf
    <!-- 폼 필드 -->
</form>
```

### 4.5 SQL Injection 방지

**Eloquent 또는 Query Builder 사용 (Raw 쿼리 금지):**
```php
// ✅ 올바른 예
User::where('email', $email)->first();

// ❌ 잘못된 예
DB::select("SELECT * FROM users WHERE email = '$email'");
```

### 4.6 민감 정보 암호화

```php
// 비밀번호
use Illuminate\Support\Facades\Hash;
Hash::make($password);

// API 키, 토큰
use Illuminate\Support\Facades\Crypt;
$encrypted = Crypt::encryptString($apiKey);
$decrypted = Crypt::decryptString($encrypted);
```

---

## 5. 테스트 전략

### 5.1 테스트 레벨

```
┌─────────────────────────────────┐
│ Feature Tests (통합 테스트)     │  ← 주력
├─────────────────────────────────┤
│ Unit Tests (단위 테스트)        │  ← Service 계층
├─────────────────────────────────┤
│ Browser Tests (E2E)             │  ← 선택적 (Playwright MCP)
└─────────────────────────────────┘
```

### 5.2 Unit Test 예시

```php
// tests/Unit/Services/UserServiceTest.php
namespace Tests\Unit\Services;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserServiceTest extends TestCase
{
    use RefreshDatabase;

    public function test_create_user()
    {
        $service = new UserService();
        $service->setTenantId(1); // Base Service tenantId 설정
        $service->setApiUserId(1); // Base Service apiUserId 설정

        $data = [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password123',
        ];

        $user = $service->create($data);

        $this->assertDatabaseHas('users', [
            'email' => 'test@example.com',
        ]);
        $this->assertTrue(Hash::check('password123', $user->password));
    }
}
```

### 5.3 Feature Test 예시

```php
// tests/Feature/UserControllerTest.php
namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_view_users_list()
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $response = $this->get(route('users.index'));

        $response->assertStatus(200);
        $response->assertViewIs('users.index');
    }

    public function test_user_can_create_new_user()
    {
        $admin = User::factory()->create();
        $admin->givePermissionTo('users.create');
        $this->actingAs($admin);

        $data = [
            'name' => 'New User',
            'email' => 'new@example.com',
            'password' => 'password123',
            'password_confirmation' => 'password123',
        ];

        $response = $this->post(route('users.store'), $data);

        $response->assertRedirect(route('users.index'));
        $this->assertDatabaseHas('users', ['email' => 'new@example.com']);
    }
}
```

### 5.4 테스트 실행

```bash
# 전체 테스트
php artisan test

# 특정 테스트
php artisan test --filter UserServiceTest

# 커버리지 (Xdebug 필요)
php artisan test --coverage
```

---

## 6. 성능 최적화

### 6.1 Eager Loading

**N+1 쿼리 방지:**
```php
// ❌ N+1 문제
$users = User::all();
foreach ($users as $user) {
    echo $user->department->name; // N번 쿼리
}

// ✅ Eager Loading
$users = User::with('department')->get();
foreach ($users as $user) {
    echo $user->department->name; // 1번 쿼리
}
```

### 6.2 쿼리 최적화

```php
// ✅ select로 필요한 컬럼만
User::select('id', 'name', 'email')->get();

// ✅ chunk로 대량 데이터 처리
User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // 처리
    }
});

// ✅ 카운트 최적화
$count = User::count(); // SELECT COUNT(*) (빠름)
// ❌ $count = User::all()->count(); (느림)
```

### 6.3 캐싱

```php
// 캐시 저장 (60분)
Cache::put('users.all', User::all(), now()->addMinutes(60));

// 캐시 조회 (없으면 생성)
$users = Cache::remember('users.all', 60 * 60, function () {
    return User::all();
});

// 캐시 삭제
Cache::forget('users.all');
```

### 6.4 큐 (Queue)

**무거운 작업은 큐로 처리:**
```php
// Job 생성
php artisan make:job SendWelcomeEmail

// Job 클래스
namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;

class SendWelcomeEmail implements ShouldQueue
{
    public function __construct(public User $user) {}

    public function handle()
    {
        Mail::to($this->user->email)->send(new WelcomeEmail($this->user));
    }
}

// Job 디스패치
SendWelcomeEmail::dispatch($user);

// 큐 워커 실행
php artisan queue:work
```

---

## 7. 배포 프로세스

### 7.1 환경 설정

```
로컬 (sam.kr) → 개발 서버 (codebridge-x.com) → 운영 서버 (TBD)
```

### 7.2 배포 체크리스트

```bash
# 1. Git 푸시
git add .
git commit -m "feat: 사용자 관리 구현"
git push origin main

# 2. 서버 접속 (개발 서버)
ssh user@codebridge-x.com

# 3. 코드 Pull
cd /var/www/mng
git pull origin main

# 4. 의존성 업데이트
composer install --no-dev --optimize-autoloader

# 5. 마이그레이션
php artisan migrate --force

# 6. 캐시 클리어
php artisan config:cache
php artisan route:cache
php artisan view:cache

# 7. 권한 설정
chown -R www-data:www-data storage bootstrap/cache

# 8. 큐 재시작
php artisan queue:restart

# 9. Supervisor 재시작 (큐 워커)
sudo supervisorctl restart mng-worker:*
```

### 7.3 롤백 프로세스

```bash
# 1. Git 롤백
git revert HEAD
git push origin main

# 2. 서버에서 Pull
git pull origin main

# 3. 마이그레이션 롤백 (필요 시)
php artisan migrate:rollback --step=1

# 4. 캐시 클리어
php artisan cache:clear
php artisan config:clear
```

---

## 📚 참고 자료

### SAM 프로젝트 문서
- **SAM CLAUDE.md:** `/SAM/CLAUDE.md` - 전체 프로젝트 구조
- **API CLAUDE.md:** `/SAM/api/CLAUDE.md` - SAM API Development Rules 상세
- **MNG CLAUDE.md:** `/SAM/mng/CLAUDE.md` - MNG 프로젝트 특화 가이드
- **현재 작업:** `CURRENT_WORKS.md` (각 저장소별)

### 외부 문서
- **Laravel 공식 문서:** https://laravel.com/docs
- **Tailwind CSS:** https://tailwindcss.com/docs
- **DaisyUI:** https://daisyui.com/
- **HTMX:** https://htmx.org/
- **Spatie Permission:** https://spatie.be/docs/laravel-permission

---

**최종 업데이트:** 2025-11-21
**작성자:** Claude Code
**버전:** 2.0.0 (SAM API Rules 기반)