- 개발 단계별 문서 추가 (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 업데이트
21 KiB
MNG 기술 표준 문서
목적: 모든 Phase에서 일관되게 적용할 기술 표준 및 개발 규칙 정의 (SAM API Rules 기반)
📋 목차
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)
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)
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)
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)
// 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 (선택적)
사용 시기: 복잡한 쿼리, 여러 모델 조인, 재사용성 높은 쿼리
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 네이밍 규칙
// 클래스: 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
원칙: 한글 직접 사용 금지, 언어 키 사용 필수
// ❌ 잘못된 예
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)
return [
// 공통 메시지
'fetched' => '조회되었습니다.',
'created' => '생성되었습니다.',
'updated' => '수정되었습니다.',
'deleted' => '삭제되었습니다.',
'bulk_upsert' => '일괄 저장되었습니다.',
'reordered' => '순서가 변경되었습니다.',
// 도메인별 메시지 (선택적)
'user' => [
'created' => '사용자가 생성되었습니다.',
'updated' => '사용자 정보가 수정되었습니다.',
],
];
lang/ko/error.php 예시:
return [
'not_found' => '데이터를 찾을 수 없습니다.',
'unauthorized' => '권한이 없습니다.',
'validation_failed' => '입력값 검증에 실패했습니다.',
];
2.3 코드 스타일 (SAM API Rule #7)
Laravel Pint 사용 (자동 포맷팅):
./vendor/bin/pint
주석 규칙:
/**
* 사용자를 생성합니다.
*
* @param array $data 사용자 데이터
* @return User 생성된 사용자
* @throws \Exception 생성 실패 시
*/
public function create(array $data): User
{
// 복잡한 로직 설명이 필요한 경우에만 인라인 주석
// 단순 코드는 주석 없이 자명하게 작성
}
3. 데이터베이스 설계 원칙
3.1 Multi-tenant 필수 컬럼 (SAM API Rule #2)
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)
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:
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 적용
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 테이블:
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 패턴 사용:
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에 등록:
public function boot()
{
User::observe(UserObserver::class);
Client::observe(ClientObserver::class);
// 기타 모델...
}
13개월 보관 정책 (Scheduler):
// 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 인덱스 전략
-- 필수 인덱스
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 사용:
// 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 사용:
// 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 자동 이스케이프:
{{-- 자동 이스케이프 (안전) --}}
{{ $user->name }}
{{-- 이스케이프 없음 (주의) --}}
{!! $htmlContent !!}
4.4 CSRF 보호
모든 POST/PUT/DELETE 요청에 @csrf:
<form method="POST" action="{{ route('users.store') }}">
@csrf
<!-- 폼 필드 -->
</form>
4.5 SQL Injection 방지
Eloquent 또는 Query Builder 사용 (Raw 쿼리 금지):
// ✅ 올바른 예
User::where('email', $email)->first();
// ❌ 잘못된 예
DB::select("SELECT * FROM users WHERE email = '$email'");
4.6 민감 정보 암호화
// 비밀번호
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 예시
// 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 예시
// 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 테스트 실행
# 전체 테스트
php artisan test
# 특정 테스트
php artisan test --filter UserServiceTest
# 커버리지 (Xdebug 필요)
php artisan test --coverage
6. 성능 최적화
6.1 Eager Loading
N+1 쿼리 방지:
// ❌ 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 쿼리 최적화
// ✅ 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 캐싱
// 캐시 저장 (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)
무거운 작업은 큐로 처리:
// 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 배포 체크리스트
# 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 롤백 프로세스
# 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 기반)