Files
sam-manage/docs/99_TECHNICAL_STANDARDS.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

21 KiB

MNG 기술 표준 문서

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

📋 목차

  1. 아키텍처 패턴 (SAM API Rules)
  2. 코딩 컨벤션
  3. 데이터베이스 설계 원칙
  4. 보안 정책
  5. 테스트 전략
  6. 성능 최적화
  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)

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 (각 저장소별)

외부 문서


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