Files
sam-api/CURRENT_WORKS.md
hskwon 48e76432ee feat: 회원가입 + 테넌트 생성 통합 API 추가 (/api/v1/register)
- 사용자 등록 + 테넌트 생성 + 시스템 관리자 권한 자동 부여
- 사업자번호 조건부 검증 (active 테넌트만 unique)
- 글로벌 메뉴 자동 복제 (parent_id 매핑 알고리즘)
- DB 트랜잭션으로 전체 프로세스 원자성 보장

추가:
- RegisterRequest: FormRequest 검증 (conditional unique)
- RegisterService: 9-step 통합 비즈니스 로직
- RegisterController: ApiResponse::handle() 패턴
- RegisterApi: 완전한 Swagger 문서

수정:
- MenusStep: 글로벌 메뉴 복제 로직 구현
- message.php: 'registered' 키 추가
- error.php: 4개 에러 메시지 추가
- routes/api.php: POST /api/v1/register 라우트

SAM API Rules 준수:
- Service-First, FormRequest, i18n, Swagger, DB Transaction
2025-11-06 17:24:42 +09:00

10 KiB

SAM API 저장소 작업 현황

2025-11-06 (수) - Register API 개발 (/api/v1/register)

주요 작업

  • Register API 전체 구현: 회원가입 + 테넌트 생성 + 시스템 관리자 권한 자동 부여
  • 글로벌 메뉴 복제 로직: 새 테넌트 생성 시 글로벌 메뉴 자동 복사 (parent_id 매핑)
  • 사업자번호 조건부 유효성 검사: 정식 서비스(active) 업체만 unique 제약
  • 완전한 Swagger 문서: 상세한 요청/응답 스키마 및 에러 케이스

추가된 파일:

  • app/Http/Requests/RegisterRequest.php - 회원가입 요청 검증 (FormRequest)
  • app/Services/RegisterService.php - 통합 비즈니스 로직 (DB 트랜잭션)
  • app/Http/Controllers/Api/V1/RegisterController.php - 컨트롤러 (ApiResponse::handle)
  • app/Swagger/v1/RegisterApi.php - Swagger 문서

수정된 파일:

  • app/Services/TenantBootstrap/Steps/MenusStep.php - 글로벌 메뉴 복제 로직 구현
  • lang/ko/message.php - registered 키 추가
  • lang/ko/error.php - 4개 에러 메시지 추가 (business_num_format, business_num_duplicate_active, user_id_format, phone_format)
  • routes/api.php - POST /api/v1/register 라우트 추가

작업 내용:

1. RegisterRequest 검증 규칙

사용자 필드:

'user_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9_-]+$/|unique:users,user_id',
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email',
'phone' => 'nullable|string|max:20|regex:/^[0-9-]+$/',
'password' => 'required|string|min:8|confirmed',
'position' => 'nullable|string|max:100',  // options JSON에 저장

테넌트 필드:

'company_name' => 'required|string|max:255',
'business_num' => [
    'required',
    'string',
    'regex:/^\d{3}-\d{2}-\d{5}$/',
    Rule::unique('tenants', 'business_num')->where(function ($query) {
        return $query->where('tenant_st_code', 'active');  // ⚠️ active만 unique
    }),
],
'company_scale' => 'nullable|string|max:50',   // options JSON에 저장
'industry' => 'nullable|string|max:100',       // options JSON에 저장

핵심 특징:

  • 사업자번호: tenant_st_code='active'인 경우만 unique (trial/none은 중복 허용)
  • 비밀번호: confirmed 규칙 (password_confirmation 필요)
  • 커스텀 에러 메시지: i18n 키 사용

2. RegisterService 비즈니스 로직

전체 프로세스 (DB::transaction 래핑):

1. Tenant 생성
   - company_name, business_num
   - tenant_st_code = 'trial' (데모 버전)
   - options = {company_scale, industry}

2. TenantBootstrap 실행 (STANDARD 레시피)
   - MenusStep: 글로벌 메뉴 복제 (parent_id 매핑)
   - CategoriesStep, SettingsStep 

3. User 생성
   - user_id, name, email, phone
   - password = Hash::make()
   - options = {position}

4. TenantUserProfile 생성
   - is_default = 1, is_active = 1

5. Tenant Context 설정
   - app()->bind('tenant_id', $tenant->id)
   - PermissionRegistrar::setPermissionsTeamId($tenant->id)

6. system_manager Role 생성
   - guard_name = 'api'
   - description = '시스템 관리자'

7. 모든 테넌트 메뉴 권한 생성  할당
   - Menu::where('tenant_id', $tenant->id)->pluck('id')
   - Permission::firstOrCreate(['name' => "menu.{menu_id}"])
   - $role->syncPermissions($permissions)

8. User에게 system_manager Role 할당
   - $user->assignRole($role)

9. 결과 반환
   - user: {id, user_id, name, email, phone, options}
   - tenant: {id, company_name, business_num, tenant_st_code, options}

주의 사항 (자동 적용됨):

  • ⚠️ 트랜잭션 필수: 실패 시 전체 롤백
  • ⚠️ 멀티테넌시: Tenant context 명시적 설정
  • ⚠️ 보안: Hash::make() 사용, 입력 검증
  • ⚠️ 글로벌 메뉴 복제: parent_id 매핑으로 계층 구조 유지
  • ⚠️ 사업자번호 검증: 조건부 unique (active만)

3. MenusStep 글로벌 메뉴 복제 로직

기존 문제:

  • ROOT 메뉴만 생성하는 stub 구현
  • 글로벌 메뉴가 복사되지 않음

개선 내용:

public function run(int $tenantId): void
{
    // 1. 중복 실행 방지
    if (Menu::where('tenant_id', $tenantId)->exists()) {
        return;
    }

    // 2. 글로벌 메뉴 조회 (계층 순서로 정렬)
    $globalMenus = DB::table('menus')
        ->whereNull('tenant_id')
        ->orderByRaw('parent_id IS NULL DESC, parent_id ASC, sort_order ASC')
        ->get();

    // 3. parent_id 매핑 (old_id => new_id)
    $parentIdMap = [];

    foreach ($globalMenus as $menu) {
        // 4. 부모 ID 매핑 확인
        $newParentId = null;
        if ($menu->parent_id !== null && isset($parentIdMap[$menu->parent_id])) {
            $newParentId = $parentIdMap[$menu->parent_id];
        }

        // 5. 새 메뉴 생성
        $newId = DB::table('menus')->insertGetId([
            'tenant_id' => $tenantId,
            'parent_id' => $newParentId,  // ⚠️ 매핑된 parent_id 사용
            'name' => $menu->name,
            'code' => $menu->code ?? null,
            // ... 모든 필드 복사
        ]);

        // 6. 매핑 저장
        $parentIdMap[$menu->id] = $newId;
    }
}

핵심:

  • 루트 메뉴 우선 처리 (parent_id IS NULL DESC)
  • parent_id 매핑으로 계층 구조 정확히 유지
  • 모든 메뉴 속성 보존 (name, code, icon, url, route_name 등)

4. RegisterController 구현

패턴:

public function register(RegisterRequest $request)
{
    return ApiResponse::handle(function () use ($request) {
        return RegisterService::register($request->validated());
    }, __('message.registered'));
}

특징:

  • FormRequest 타입 힌트 (자동 검증)
  • Service DI + ApiResponse::handle()
  • i18n 메시지 키 사용
  • Controller는 단순 래퍼 역할

5. Swagger 문서 (RegisterApi.php)

요청 스키마:

required: user_id, name, email, password, password_confirmation, company_name, business_num
optional: phone, position, company_scale, industry

응답 스키마 (200):

{
  "success": true,
  "message": "회원가입이 완료되었습니다",
  "data": {
    "user": {
      "id": 1,
      "user_id": "john_doe",
      "name": "홍길동",
      "email": "john@example.com",
      "phone": "010-1234-5678",
      "options": {"position": "개발팀장"}
    },
    "tenant": {
      "id": 1,
      "company_name": "(주)테크컴퍼니",
      "business_num": "123-45-67890",
      "tenant_st_code": "trial",
      "options": {
        "company_scale": "중소기업",
        "industry": "IT/소프트웨어"
      }
    }
  }
}

에러 응답 (422):

{
  "success": false,
  "message": "유효성 검증에 실패했습니다",
  "errors": {
    "user_id": ["이미 사용 중인 아이디입니다"],
    "business_num": ["사업자등록번호 형식이 올바르지 않습니다 (000-00-00000)"]
  }
}

6. i18n 메시지 추가

lang/ko/message.php:

'registered' => '회원가입이 완료되었습니다.',

lang/ko/error.php:

'business_num_format' => '사업자등록번호 형식이 올바르지 않습니다 (000-00-00000)',
'business_num_duplicate_active' => '이미 등록된 사업자등록번호입니다 (정식 서비스 업체)',
'user_id_format' => '아이디는 영문, 숫자, _, - 만 사용할 수 있습니다',
'phone_format' => '전화번호 형식이 올바르지 않습니다',

7. Routes 등록

routes/api.php:

use App\Http\Controllers\Api\V1\RegisterController;

Route::middleware('auth.apikey')->group(function () {
    Route::post('register', [RegisterController::class, 'register'])->name('v1.register');
});

엔드포인트:

  • POST /api/v1/register (auth.apikey 미들웨어)

SAM API Development Rules 준수:

Service-First 아키텍처:

  • RegisterService에 모든 비즈니스 로직
  • Controller는 DI + ApiResponse::handle()만

FormRequest 검증:

  • RegisterRequest로 모든 검증 규칙 분리

i18n 메시지 키:

  • __('message.registered'), __('error.xxx') 사용

Swagger 문서:

  • 별도 파일 (app/Swagger/v1/RegisterApi.php)
  • 완전한 요청/응답 스키마

멀티테넌시:

  • BelongsToTenant 스코프 (Tenant, Role, Permission)
  • Explicit tenant context 설정

감사 로그:

  • created_by, updated_by 컬럼 포함

SoftDeletes:

  • Tenant, User 모델에 적용

기술 세부사항:

조건부 Unique 제약

// trial/none 테넌트는 사업자번호 중복 허용
Rule::unique('tenants', 'business_num')->where(function ($query) {
    return $query->where('tenant_st_code', 'active');
})

parent_id 매핑 알고리즘

// 1. 루트 메뉴 먼저 처리 (parent_id IS NULL)
// 2. insertGetId로 새 ID 캡처
// 3. old_id => new_id 매핑 저장
// 4. 자식 메뉴 처리 시 매핑된 parent_id 사용
$parentIdMap[$oldId] = $newId;
$newParentId = $parentIdMap[$menu->parent_id] ?? null;

DB Transaction

return DB::transaction(function () use ($params) {
    // 모든 작업이 성공하거나 전체 롤백
    $tenant = Tenant::create([...]);
    app(RecipeRegistry::class)->bootstrap($tenant->id);
    $user = User::create([...]);
    // ...
    return ['user' => $user->only([...]), 'tenant' => $tenant->only([...])];
});

예상 효과:

  1. 원스톱 가입: 1회 요청으로 모든 설정 완료
  2. 즉시 사용 가능: system_manager 권한으로 모든 메뉴 접근
  3. 멀티테넌트 격리: 각 테넌트별 독립적인 메뉴 구조
  4. 유연한 검증: trial 단계에서는 사업자번호 중복 허용

다음 작업:

  • Swagger 재생성 (php artisan l5-swagger:generate)
  • Postman/Swagger UI로 API 테스트
  • Frontend 회원가입 화면 구현
  • 이메일 인증 기능 추가 (선택)
  • API 문서 최종 검토

Git 커밋 준비:

  • 다음 커밋 예정: feat: 회원가입 + 테넌트 생성 통합 API 추가 (/api/v1/register)

(이전 작업 내역은 그대로 유지...)