# 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') 새 사용자 @endcan ``` ### 4.3 XSS 방지 **Blade 자동 이스케이프:** ```blade {{-- 자동 이스케이프 (안전) --}} {{ $user->name }} {{-- 이스케이프 없음 (주의) --}} {!! $htmlContent !!} ``` ### 4.4 CSRF 보호 **모든 POST/PUT/DELETE 요청에 @csrf:** ```blade
``` ### 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 기반)