Files
sam-manage/docs/99_TECHNICAL_STANDARDS.md

895 lines
21 KiB
Markdown
Raw Normal View History

# 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 기반)