diff --git a/.env.example b/.env.example index 94f1cc61..de90e952 100644 --- a/.env.example +++ b/.env.example @@ -76,10 +76,8 @@ FCM_BATCH_DELAY_MS=100 FCM_LOGGING_ENABLED=true FCM_LOG_CHANNEL=stack -# FCM (Firebase Cloud Messaging) -FCM_PROJECT_ID= -FCM_SA_PATH=secrets/firebase-service-account.json -FCM_BATCH_CHUNK_SIZE=200 -FCM_BATCH_DELAY_MS=100 -FCM_LOGGING_ENABLED=true -FCM_LOG_CHANNEL=stack +# 바로빌 API (세금계산서 연동) +# @see https://dev.barobill.co.kr/ +BAROBILL_CERT_KEY= +BAROBILL_CORP_NUM= +BAROBILL_TEST_MODE=true diff --git a/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php b/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php index 54afab76..da0b9e09 100644 --- a/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php +++ b/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php @@ -20,13 +20,17 @@ public function __construct( ) {} /** * 회원사 목록 조회 + * + * @param all_tenants bool 전체 테넌트 조회 모드 (바로빌본사용) */ public function index(Request $request): JsonResponse|Response { $tenantId = session('selected_tenant_id'); + $allTenants = $request->boolean('all_tenants', false); $query = BarobillMember::query() - ->when($tenantId, fn($q) => $q->where('tenant_id', $tenantId)) + // 전체 테넌트 모드가 아닐 때만 테넌트 필터 적용 + ->when(!$allTenants && $tenantId, fn($q) => $q->where('tenant_id', $tenantId)) ->when($request->search, function ($q, $search) { $q->where(function ($q) use ($search) { $q->where('corp_name', 'like', "%{$search}%") @@ -44,7 +48,7 @@ public function index(Request $request): JsonResponse|Response // HTMX 요청 시 HTML 반환 if ($request->header('HX-Request')) { return response( - view('barobill.members.partials.table', compact('members'))->render(), + view('barobill.members.partials.table', compact('members', 'allTenants'))->render(), 200, ['Content-Type' => 'text/html'] ); @@ -67,10 +71,15 @@ public function index(Request $request): JsonResponse|Response * * 1. 바로빌 API를 통해 회원사 등록 (RegistCorp) * 2. API 성공 시 로컬 DB에 저장 + * + * @param tenant_id int 테넌트 ID (전체 테넌트 모드에서 직접 지정) */ public function store(Request $request): JsonResponse { - $tenantId = session('selected_tenant_id'); + // 요청에 tenant_id가 있으면 사용, 없으면 세션에서 가져오기 + $tenantId = $request->filled('tenant_id') + ? $request->integer('tenant_id') + : session('selected_tenant_id'); if (!$tenantId) { return response()->json([ @@ -80,6 +89,7 @@ public function store(Request $request): JsonResponse } $validated = $request->validate([ + 'tenant_id' => 'nullable|integer|exists:tenants,id', 'biz_no' => [ 'required', 'string', @@ -103,6 +113,7 @@ public function store(Request $request): JsonResponse 'status' => 'nullable|in:active,inactive,pending', 'skip_api' => 'nullable|boolean', // API 호출 스킵 옵션 (테스트용) ], [ + 'tenant_id.exists' => '유효하지 않은 테넌트입니다.', 'biz_no.required' => '사업자번호를 입력해주세요.', 'biz_no.unique' => '이미 등록된 사업자번호입니다.', 'corp_name.required' => '상호명을 입력해주세요.', @@ -154,9 +165,9 @@ public function store(Request $request): JsonResponse } // 로컬 DB에 저장 (비밀번호는 모델의 encrypted cast로 자동 암호화됨) - $validated['tenant_id'] = $tenantId; + $validated['tenant_id'] = $tenantId; // 항상 계산된 tenantId 사용 $validated['status'] = $validated['status'] ?? 'active'; - unset($validated['tel'], $validated['post_num']); // DB에 없는 필드 제거 + unset($validated['tel'], $validated['post_num']); // DB에 없는 필드 제거 $member = BarobillMember::create($validated); @@ -263,13 +274,16 @@ public function destroy(int $id): JsonResponse /** * 통계 조회 + * + * @param all_tenants bool 전체 테넌트 조회 모드 (바로빌본사용) */ public function stats(Request $request): JsonResponse|Response { $tenantId = session('selected_tenant_id'); + $allTenants = $request->boolean('all_tenants', false); $query = BarobillMember::query() - ->when($tenantId, fn($q) => $q->where('tenant_id', $tenantId)); + ->when(!$allTenants && $tenantId, fn($q) => $q->where('tenant_id', $tenantId)); $stats = [ 'total' => (clone $query)->count(), diff --git a/app/Http/Controllers/Barobill/BarobillController.php b/app/Http/Controllers/Barobill/BarobillController.php index 26a2cd49..64b515f6 100644 --- a/app/Http/Controllers/Barobill/BarobillController.php +++ b/app/Http/Controllers/Barobill/BarobillController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Barobill; use App\Http\Controllers\Controller; +use App\Models\Tenants\Tenant; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\View\View; @@ -35,7 +36,10 @@ public function members(Request $request): View|Response return response('', 200)->header('HX-Redirect', route('barobill.members.index')); } - return view('barobill.members.index'); + // 테넌트 목록 (전체 테넌트 모드에서 선택용) + $tenants = Tenant::select('id', 'company_name')->orderBy('company_name')->get(); + + return view('barobill.members.index', compact('tenants')); } /** diff --git a/app/Models/Barobill/BarobillConfig.php b/app/Models/Barobill/BarobillConfig.php new file mode 100644 index 00000000..4439350b --- /dev/null +++ b/app/Models/Barobill/BarobillConfig.php @@ -0,0 +1,109 @@ + 'boolean', + ]; + + /** + * 활성화된 테스트 서버 설정 조회 + */ + public static function getActiveTest(): ?self + { + return self::where('environment', 'test') + ->where('is_active', true) + ->first(); + } + + /** + * 활성화된 운영 서버 설정 조회 + */ + public static function getActiveProduction(): ?self + { + return self::where('environment', 'production') + ->where('is_active', true) + ->first(); + } + + /** + * 현재 환경에 맞는 활성 설정 조회 + */ + public static function getActive(bool $isTestMode = false): ?self + { + return $isTestMode ? self::getActiveTest() : self::getActiveProduction(); + } + + /** + * 환경 라벨 + */ + public function getEnvironmentLabelAttribute(): string + { + return match ($this->environment) { + 'test' => '테스트서버', + 'production' => '운영서버', + default => $this->environment, + }; + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + return $this->is_active ? '활성' : '비활성'; + } + + /** + * 상태 색상 (Tailwind) + */ + public function getStatusColorAttribute(): string + { + return $this->is_active ? 'green' : 'gray'; + } + + /** + * 마스킹된 인증키 (앞 8자리만 표시) + */ + public function getMaskedCertKeyAttribute(): string + { + if (strlen($this->cert_key) <= 8) { + return $this->cert_key; + } + + return substr($this->cert_key, 0, 8) . str_repeat('*', 8) . '...'; + } +} diff --git a/config/services.php b/config/services.php index c96836c1..0f602588 100644 --- a/config/services.php +++ b/config/services.php @@ -49,6 +49,23 @@ 'storage_bucket' => env('GOOGLE_STORAGE_BUCKET'), ], + /* + |-------------------------------------------------------------------------- + | 바로빌 API + |-------------------------------------------------------------------------- + | 바로빌 SOAP 웹서비스 연동 설정 + | - cert_key: 바로빌 개발자센터에서 발급받은 CERTKEY + | - corp_num: 파트너 사업자번호 (하이픈 제외) + | - test_mode: 테스트 환경 사용 여부 + | + | @see https://dev.barobill.co.kr/ + */ + 'barobill' => [ + 'cert_key' => env('BAROBILL_CERT_KEY', ''), + 'corp_num' => env('BAROBILL_CORP_NUM', ''), + 'test_mode' => env('BAROBILL_TEST_MODE', false), + ], + /* |-------------------------------------------------------------------------- | SAM API Server diff --git a/database/migrations/2026_01_22_091500_create_barobill_configs_table.php b/database/migrations/2026_01_22_091500_create_barobill_configs_table.php new file mode 100644 index 00000000..7d0c0b5b --- /dev/null +++ b/database/migrations/2026_01_22_091500_create_barobill_configs_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('name', 50)->comment('설정 이름 (예: 테스트서버, 운영서버)'); + $table->enum('environment', ['test', 'production'])->comment('환경 (test/production)'); + $table->string('cert_key', 100)->comment('CERTKEY (인증키)'); + $table->string('corp_num', 20)->nullable()->comment('파트너 사업자번호'); + $table->string('base_url', 255)->comment('API 서버 URL'); + $table->text('description')->nullable()->comment('설명'); + $table->boolean('is_active')->default(false)->comment('활성화 여부 (환경당 1개만 활성화)'); + $table->timestamps(); + $table->softDeletes(); + + // 환경별로 하나만 활성화 가능하도록 부분 유니크 인덱스 (is_active=true인 것 중에서) + $table->index(['environment', 'is_active']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('barobill_configs'); + } +}; diff --git a/resources/views/barobill/config/partials/modal-form.blade.php b/resources/views/barobill/config/partials/modal-form.blade.php new file mode 100644 index 00000000..b62a43b8 --- /dev/null +++ b/resources/views/barobill/config/partials/modal-form.blade.php @@ -0,0 +1,119 @@ + + diff --git a/resources/views/barobill/config/partials/table.blade.php b/resources/views/barobill/config/partials/table.blade.php new file mode 100644 index 00000000..617db7e8 --- /dev/null +++ b/resources/views/barobill/config/partials/table.blade.php @@ -0,0 +1,89 @@ +@if($configs->isEmpty()) +
+ + + + +

등록된 설정이 없습니다.

+ +
+@else + + + + + + + + + + + + + @foreach($configs as $config) + + + + + + + + + @endforeach + +
설정 이름환경인증키서버 URL상태관리
+
{{ $config->name }}
+ @if($config->description) +
{{ Str::limit($config->description, 50) }}
+ @endif +
+ + {{ $config->environment_label }} + + +
+ {{ $config->masked_cert_key }} + +
+
+ {{ Str::limit($config->base_url, 35) }} + + + +
+ + @unless($config->is_active) + + @endunless +
+
+@endif diff --git a/resources/views/barobill/members/index.blade.php b/resources/views/barobill/members/index.blade.php index 3764fed0..9e25e463 100644 --- a/resources/views/barobill/members/index.blade.php +++ b/resources/views/barobill/members/index.blade.php @@ -25,7 +25,8 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
@include('barobill.members.partials.stats-skeleton') @@ -34,7 +35,17 @@ class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 flex-shrink-0">
-
+ + + +
+ + +
diff --git a/resources/views/barobill/members/partials/table.blade.php b/resources/views/barobill/members/partials/table.blade.php index 74c475e5..58f71dac 100644 --- a/resources/views/barobill/members/partials/table.blade.php +++ b/resources/views/barobill/members/partials/table.blade.php @@ -19,10 +19,15 @@ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm te
@else -
+
- + + @if($allTenants ?? false) + + @endif @@ -38,6 +43,9 @@ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm te + @@ -46,6 +54,13 @@ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm te @foreach($members as $member) + @if($allTenants ?? false) + + @endif @@ -65,6 +80,106 @@ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm te {{ $member->status_label }} +
+ 테넌트 + 사업자번호 상태 + 바로빌 서비스 + 관리
+ + {{ $member->tenant?->company_name ?? '미지정' }} + +
{{ $member->formatted_biz_no }}
+
+ + +
+