From f60f84670a6fb24d26c82bb8253e26bf1f9e773b Mon Sep 17 00:00:00 2001 From: pro Date: Thu, 22 Jan 2026 08:49:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=94=EB=A1=9C=EB=B9=8C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=82=AC=EA=B4=80=EB=A6=AC=20CRUD=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 레거시(sam/sales/barobill/registration)를 Laravel 스타일로 마이그레이션 - Migration: barobill_members 테이블 생성 - Model: BarobillMember (상태 라벨, 사업자번호 포맷팅 등) - API Controller: CRUD + 통계 조회 (HTMX HTML 반환 지원) - API Routes: /api/admin/barobill/members/* - Views: - index.blade.php (통계 카드, 필터, 테이블, 모달) - partials/table.blade.php (HTMX 테이블) - partials/stats.blade.php (통계 카드) - partials/modal-form.blade.php (등록/수정 폼, 자동완성) 기능: - 회원사 목록 조회 (검색, 상태 필터) - 회원사 등록 (사업자번호 중복 체크) - 회원사 수정 (모달) - 회원사 삭제 (확인 후) - 테스트 데이터 자동완성 Co-Authored-By: Claude Opus 4.5 --- .../Barobill/BarobillMemberController.php | 230 +++++++++++++ .../Barobill/BarobillController.php | 9 +- app/Models/Barobill/BarobillMember.php | 87 +++++ ...2_084412_create_barobill_members_table.php | 44 +++ .../views/barobill/members/index.blade.php | 311 ++++++++++++------ .../members/partials/modal-form.blade.php | 143 ++++++++ .../members/partials/stats-skeleton.blade.php | 33 ++ .../barobill/members/partials/stats.blade.php | 55 ++++ .../barobill/members/partials/table.blade.php | 104 ++++++ routes/api.php | 13 + 10 files changed, 926 insertions(+), 103 deletions(-) create mode 100644 app/Http/Controllers/Api/Admin/Barobill/BarobillMemberController.php create mode 100644 app/Models/Barobill/BarobillMember.php create mode 100644 database/migrations/2026_01_22_084412_create_barobill_members_table.php create mode 100644 resources/views/barobill/members/partials/modal-form.blade.php create mode 100644 resources/views/barobill/members/partials/stats-skeleton.blade.php create mode 100644 resources/views/barobill/members/partials/stats.blade.php create mode 100644 resources/views/barobill/members/partials/table.blade.php 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 () { // 고정 경로는 먼저 정의