diff --git a/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php b/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php new file mode 100644 index 00000000..fa1cb987 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php @@ -0,0 +1,230 @@ +when($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}%") + ->orWhere('biz_no', 'like', "%{$search}%") + ->orWhere('barobill_id', 'like', "%{$search}%") + ->orWhere('manager_name', 'like', "%{$search}%"); + }); + }) + ->when($request->status, fn($q, $status) => $q->where('status', $status)) + ->with('tenant:id,company_name') + ->orderBy('created_at', 'desc'); + + $members = $query->paginate($request->integer('per_page', 15)); + + // HTMX 요청 시 HTML 반환 + if ($request->header('HX-Request')) { + return response( + view('barobill.members.partials.table', compact('members'))->render(), + 200, + ['Content-Type' => 'text/html'] + ); + } + + return response()->json([ + 'success' => true, + 'data' => $members->items(), + 'meta' => [ + 'current_page' => $members->currentPage(), + 'last_page' => $members->lastPage(), + 'per_page' => $members->perPage(), + 'total' => $members->total(), + ], + ]); + } + + /** + * 회원사 등록 + */ + public function store(Request $request): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + if (!$tenantId) { + return response()->json([ + 'success' => false, + 'message' => '테넌트를 선택해주세요.', + ], 400); + } + + $validated = $request->validate([ + 'biz_no' => [ + 'required', + 'string', + 'max:20', + Rule::unique('barobill_members')->where(function ($query) use ($tenantId) { + return $query->where('tenant_id', $tenantId); + }), + ], + 'corp_name' => 'required|string|max:100', + 'ceo_name' => 'required|string|max:50', + 'addr' => 'nullable|string|max:255', + 'biz_type' => 'nullable|string|max:50', + 'biz_class' => 'nullable|string|max:50', + 'barobill_id' => 'required|string|max:50', + 'barobill_pwd' => 'required|string|max:255', + 'manager_name' => 'nullable|string|max:50', + 'manager_email' => 'nullable|email|max:100', + 'manager_hp' => 'nullable|string|max:20', + 'status' => 'nullable|in:active,inactive,pending', + ], [ + 'biz_no.required' => '사업자번호를 입력해주세요.', + 'biz_no.unique' => '이미 등록된 사업자번호입니다.', + 'corp_name.required' => '상호명을 입력해주세요.', + 'ceo_name.required' => '대표자명을 입력해주세요.', + 'barobill_id.required' => '바로빌 아이디를 입력해주세요.', + 'barobill_pwd.required' => '비밀번호를 입력해주세요.', + ]); + + $validated['tenant_id'] = $tenantId; + $validated['barobill_pwd'] = Hash::make($validated['barobill_pwd']); + $validated['status'] = $validated['status'] ?? 'active'; + + $member = BarobillMember::create($validated); + + return response()->json([ + 'success' => true, + 'message' => '회원사가 등록되었습니다.', + 'data' => $member, + ], 201); + } + + /** + * 회원사 상세 조회 + */ + public function show(Request $request, int $id): JsonResponse + { + $member = BarobillMember::with('tenant:id,company_name')->find($id); + + if (!$member) { + return response()->json([ + 'success' => false, + 'message' => '회원사를 찾을 수 없습니다.', + ], 404); + } + + // HTMX 요청 시 HTML 반환 + if ($request->header('HX-Request')) { + return response()->json([ + 'html' => view('barobill.members.partials.detail', compact('member'))->render(), + ]); + } + + return response()->json([ + 'success' => true, + 'data' => $member, + ]); + } + + /** + * 회원사 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $member = BarobillMember::find($id); + + if (!$member) { + return response()->json([ + 'success' => false, + 'message' => '회원사를 찾을 수 없습니다.', + ], 404); + } + + $validated = $request->validate([ + 'corp_name' => 'required|string|max:100', + 'ceo_name' => 'required|string|max:50', + 'addr' => 'nullable|string|max:255', + 'biz_type' => 'nullable|string|max:50', + 'biz_class' => 'nullable|string|max:50', + 'manager_name' => 'nullable|string|max:50', + 'manager_email' => 'nullable|email|max:100', + 'manager_hp' => 'nullable|string|max:20', + 'status' => 'nullable|in:active,inactive,pending', + ]); + + $member->update($validated); + + return response()->json([ + 'success' => true, + 'message' => '회원사 정보가 수정되었습니다.', + 'data' => $member->fresh(), + ]); + } + + /** + * 회원사 삭제 + */ + public function destroy(int $id): JsonResponse + { + $member = BarobillMember::find($id); + + if (!$member) { + return response()->json([ + 'success' => false, + 'message' => '회원사를 찾을 수 없습니다.', + ], 404); + } + + $member->delete(); + + return response()->json([ + 'success' => true, + 'message' => '회원사가 삭제되었습니다.', + ]); + } + + /** + * 통계 조회 + */ + public function stats(Request $request): JsonResponse|Response + { + $tenantId = session('selected_tenant_id'); + + $query = BarobillMember::query() + ->when($tenantId, fn($q) => $q->where('tenant_id', $tenantId)); + + $stats = [ + 'total' => (clone $query)->count(), + 'active' => (clone $query)->where('status', 'active')->count(), + 'inactive' => (clone $query)->where('status', 'inactive')->count(), + 'pending' => (clone $query)->where('status', 'pending')->count(), + ]; + + // HTMX 요청 시 HTML 반환 + if ($request->header('HX-Request')) { + return response( + view('barobill.members.partials.stats', compact('stats'))->render(), + 200, + ['Content-Type' => 'text/html'] + ); + } + + return response()->json([ + 'success' => true, + 'data' => $stats, + ]); + } +} diff --git a/app/Http/Controllers/Barobill/BarobillController.php b/app/Http/Controllers/Barobill/BarobillController.php index bf1871b1..7079fda1 100644 --- a/app/Http/Controllers/Barobill/BarobillController.php +++ b/app/Http/Controllers/Barobill/BarobillController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers\Barobill; use App\Http\Controllers\Controller; +use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\View\View; /** @@ -12,9 +14,14 @@ class BarobillController extends Controller { /** * 회원사관리 페이지 + * HTMX 요청 시 전체 페이지 리로드 (스크립트 로딩을 위해) */ - public function members(): View + public function members(Request $request): View|Response { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('barobill.members.index')); + } + return view('barobill.members.index'); } } diff --git a/app/Models/Barobill/BarobillMember.php b/app/Models/Barobill/BarobillMember.php new file mode 100644 index 00000000..6d5ae1a2 --- /dev/null +++ b/app/Models/Barobill/BarobillMember.php @@ -0,0 +1,87 @@ + 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'barobill_pwd', + ]; + + /** + * 테넌트 관계 + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * 사업자번호 포맷팅 (XXX-XX-XXXXX) + */ + public function getFormattedBizNoAttribute(): string + { + $bizNo = preg_replace('/[^0-9]/', '', $this->biz_no); + if (strlen($bizNo) === 10) { + return substr($bizNo, 0, 3) . '-' . substr($bizNo, 3, 2) . '-' . substr($bizNo, 5); + } + return $this->biz_no; + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'active' => '활성', + 'inactive' => '비활성', + 'pending' => '대기중', + default => $this->status, + }; + } + + /** + * 상태별 색상 클래스 + */ + public function getStatusColorAttribute(): string + { + return match ($this->status) { + 'active' => 'bg-green-100 text-green-800', + 'inactive' => 'bg-gray-100 text-gray-800', + 'pending' => 'bg-yellow-100 text-yellow-800', + default => 'bg-gray-100 text-gray-800', + }; + } +} diff --git a/database/migrations/2026_01_22_084412_create_barobill_members_table.php b/database/migrations/2026_01_22_084412_create_barobill_members_table.php new file mode 100644 index 00000000..d35a391a --- /dev/null +++ b/database/migrations/2026_01_22_084412_create_barobill_members_table.php @@ -0,0 +1,44 @@ +id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->string('biz_no', 20)->comment('사업자번호'); + $table->string('corp_name', 100)->comment('상호명'); + $table->string('ceo_name', 50)->comment('대표자명'); + $table->string('addr', 255)->nullable()->comment('주소'); + $table->string('biz_type', 50)->nullable()->comment('업태'); + $table->string('biz_class', 50)->nullable()->comment('종목'); + $table->string('barobill_id', 50)->comment('바로빌 아이디'); + $table->string('barobill_pwd', 255)->comment('바로빌 비밀번호 (해시)'); + $table->string('manager_name', 50)->nullable()->comment('담당자명'); + $table->string('manager_email', 100)->nullable()->comment('담당자 이메일'); + $table->string('manager_hp', 20)->nullable()->comment('담당자 전화번호'); + $table->enum('status', ['active', 'inactive', 'pending'])->default('active')->comment('상태'); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['tenant_id', 'biz_no'], 'unique_tenant_biz_no'); + $table->index('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('barobill_members'); + } +}; diff --git a/resources/views/barobill/members/index.blade.php b/resources/views/barobill/members/index.blade.php index e67e46a8..98c217e7 100644 --- a/resources/views/barobill/members/index.blade.php +++ b/resources/views/barobill/members/index.blade.php @@ -5,12 +5,31 @@ @section('content')
-

회원사관리

-
+ +
+ @include('barobill.members.partials.stats-skeleton') +
+
@@ -18,12 +37,12 @@
-
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+
+ + + diff --git a/resources/views/barobill/members/partials/stats-skeleton.blade.php b/resources/views/barobill/members/partials/stats-skeleton.blade.php new file mode 100644 index 00000000..de1770fe --- /dev/null +++ b/resources/views/barobill/members/partials/stats-skeleton.blade.php @@ -0,0 +1,33 @@ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/barobill/members/partials/stats.blade.php b/resources/views/barobill/members/partials/stats.blade.php new file mode 100644 index 00000000..2bd58e7d --- /dev/null +++ b/resources/views/barobill/members/partials/stats.blade.php @@ -0,0 +1,55 @@ + +
+
+

연동 회원사

+
+ + + +
+
+
{{ number_format($stats['total']) }}
+
DB 실시간 합계
+
+ + +
+
+

활성 회원사

+
+ + + +
+
+
{{ number_format($stats['active']) }}
+
정상 운영 중
+
+ + +
+
+

비활성

+
+ + + +
+
+
{{ number_format($stats['inactive']) }}
+
일시 중지
+
+ + +
+
+

대기중

+
+ + + +
+
+
{{ number_format($stats['pending']) }}
+
승인 대기
+
diff --git a/resources/views/barobill/members/partials/table.blade.php b/resources/views/barobill/members/partials/table.blade.php new file mode 100644 index 00000000..74c475e5 --- /dev/null +++ b/resources/views/barobill/members/partials/table.blade.php @@ -0,0 +1,104 @@ +@if($members->isEmpty()) +
+ + + +

등록된 회원사가 없습니다

+

새 회원사를 등록해보세요.

+
+ +
+
+@else +
+ + + + + + + + + + + + + @foreach($members as $member) + + + + + + + + + @endforeach + +
+ 사업자번호 + + 상호 / 대표자 + + 바로빌 ID + + 담당자 정보 + + 상태 + + 관리 +
+
{{ $member->formatted_biz_no }}
+
+
{{ $member->corp_name }}
+
{{ $member->ceo_name }}
+
+
{{ $member->barobill_id }}
+
+
{{ $member->manager_name ?: '-' }}
+
{{ $member->manager_email ?: '' }}
+
+ + {{ $member->status_label }} + + +
+ + +
+
+
+ + + @if($members->hasPages()) +
+ {{ $members->links() }} +
+ @endif +@endif diff --git a/routes/api.php b/routes/api.php index 1c5624fc..241263ba 100644 --- a/routes/api.php +++ b/routes/api.php @@ -84,6 +84,19 @@ Route::patch('/{id}/status', [\App\Http\Controllers\Api\Admin\FundScheduleController::class, 'updateStatus'])->name('status'); }); + // 바로빌 회원사 관리 API + Route::prefix('barobill/members')->name('barobill.members.')->group(function () { + // 통계 + Route::get('/stats', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'stats'])->name('stats'); + + // 기본 CRUD + Route::get('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'store'])->name('store'); + Route::get('/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'show'])->name('show'); + Route::put('/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'update'])->name('update'); + Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'destroy'])->name('destroy'); + }); + // 테넌트 관리 API Route::prefix('tenants')->name('tenants.')->group(function () { // 고정 경로는 먼저 정의