json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } if (empty($member->barobill_pwd)) { return response()->json([ 'success' => false, 'message' => '바로빌 비밀번호가 설정되지 않았습니다. 회원사 정보를 수정하여 비밀번호를 입력해주세요.', 'error_code' => 'MISSING_PASSWORD', ], 422); } // 회원사의 서버 모드로 BarobillService 전환 $this->barobillService->setServerMode($member->server_mode ?? 'test'); return $member; } /** * 회원사 목록 조회 * * - 테넌트 1(본사)이면 자동으로 모든 회원사 표시 * - 다른 테넌트면 해당 테넌트의 회원사만 표시 * * @param all_tenants bool 전체 테넌트 조회 모드 (수동 선택용) */ public function index(Request $request): JsonResponse|Response { $tenantId = session('selected_tenant_id'); $allTenants = $request->boolean('all_tenants', false); // 테넌트 1(본사)이면 자동으로 전체 테넌트 모드 $isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID; // 본사이면 barobill_members 미등록 테넌트에 기본 레코드 자동 생성 if ($isHeadquarters || $allTenants) { $existingTenantIds = BarobillMember::pluck('tenant_id')->toArray(); $missingTenants = Tenant::whereNotIn('id', $existingTenantIds)->get(); foreach ($missingTenants as $tenant) { BarobillMember::create([ 'tenant_id' => $tenant->id, 'biz_no' => $tenant->business_num ?? '', 'corp_name' => $tenant->company_name, 'ceo_name' => $tenant->ceo_name ?? '', 'barobill_id' => '', 'barobill_pwd' => '', 'status' => 'pending', 'server_mode' => 'test', ]); } } $query = BarobillMember::query() // 본사(테넌트 1)이거나 전체 테넌트 모드일 때는 필터 없음 ->when(! $isHeadquarters && ! $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}%") ->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)); // 본사이거나 전체 테넌트 모드면 테넌트 컬럼 표시 $showAllTenants = $isHeadquarters || $allTenants; // HTMX 요청 시 HTML 반환 if ($request->header('HX-Request')) { return response( view('barobill.members.partials.table', [ 'members' => $members, 'allTenants' => $showAllTenants, ])->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(), ], ]); } /** * 회원사 등록 * * 1. 바로빌 API를 통해 회원사 등록 (RegistCorp) * 2. API 성공 시 로컬 DB에 저장 * * @param tenant_id int 테넌트 ID (전체 테넌트 모드에서 직접 지정) */ public function store(Request $request): JsonResponse { // 요청에 tenant_id가 있으면 사용, 없으면 세션에서 가져오기 $tenantId = $request->filled('tenant_id') ? $request->integer('tenant_id') : session('selected_tenant_id'); if (! $tenantId) { return response()->json([ 'success' => false, 'message' => '테넌트를 선택해주세요.', ], 400); } $validated = $request->validate([ 'tenant_id' => 'nullable|integer|exists:tenants,id', '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', 'tel' => 'nullable|string|max:20', 'post_num' => 'nullable|string|max:10', 'status' => 'nullable|in:active,inactive,pending', 'skip_api' => 'nullable|boolean', // API 호출 스킵 옵션 (테스트용) ], [ 'tenant_id.exists' => '유효하지 않은 테넌트입니다.', 'biz_no.required' => '사업자번호를 입력해주세요.', 'biz_no.unique' => '이미 등록된 사업자번호입니다.', 'corp_name.required' => '상호명을 입력해주세요.', 'ceo_name.required' => '대표자명을 입력해주세요.', 'barobill_id.required' => '바로빌 아이디를 입력해주세요.', 'barobill_pwd.required' => '비밀번호를 입력해주세요.', ]); $skipApi = $validated['skip_api'] ?? false; unset($validated['skip_api']); // 바로빌 API 호출 (skip_api가 false일 때만) if (! $skipApi) { $apiResult = $this->barobillService->registCorp([ 'biz_no' => $validated['biz_no'], 'corp_name' => $validated['corp_name'], 'ceo_name' => $validated['ceo_name'], 'biz_type' => $validated['biz_type'] ?? '', 'biz_class' => $validated['biz_class'] ?? '', 'addr' => $validated['addr'] ?? '', 'post_num' => $validated['post_num'] ?? '', 'barobill_id' => $validated['barobill_id'], 'barobill_pwd' => $validated['barobill_pwd'], 'manager_name' => $validated['manager_name'] ?? '', 'manager_email' => $validated['manager_email'] ?? '', 'manager_hp' => $validated['manager_hp'] ?? '', 'tel' => $validated['tel'] ?? '', ]); if (! $apiResult['success']) { Log::error('바로빌 회원사 등록 API 실패', [ 'biz_no' => $validated['biz_no'], 'error' => $apiResult['error'] ?? 'Unknown error', 'error_code' => $apiResult['error_code'] ?? null, ]); return response()->json([ 'success' => false, 'message' => '바로빌 회원사 등록에 실패했습니다.', 'error' => $apiResult['error'] ?? '알 수 없는 오류가 발생했습니다.', 'error_code' => $apiResult['error_code'] ?? null, ], 422); } Log::info('바로빌 회원사 등록 API 성공', [ 'biz_no' => $validated['biz_no'], 'api_response' => $apiResult['data'] ?? null, ]); } // 로컬 DB에 저장 (비밀번호는 모델의 encrypted cast로 자동 암호화됨) $validated['tenant_id'] = $tenantId; // 항상 계산된 tenantId 사용 $validated['status'] = $validated['status'] ?? 'active'; unset($validated['tel'], $validated['post_num']); // DB에 없는 필드 제거 $member = BarobillMember::create($validated); return response()->json([ 'success' => true, 'message' => $skipApi ? '회원사가 등록되었습니다. (API 스킵)' : '바로빌 회원사 등록이 완료되었습니다.', '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(), ]); } // 비밀번호 설정 여부 추가 (encrypted cast 복호화 실패 대비) $memberData = $member->toArray(); try { $memberData['has_password'] = ! empty($member->barobill_pwd); } catch (\Throwable $e) { $memberData['has_password'] = ! empty($member->getRawOriginal('barobill_pwd')); } return response()->json([ 'success' => true, 'data' => $memberData, ]); } /** * 회원사 수정 * * 비밀번호는 선택적으로 업데이트 가능 (기존 Hash 저장 데이터 호환) */ 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', 'barobill_id' => 'nullable|string|max:50', 'barobill_pwd' => 'nullable|string|max:255', ]); // 비어있으면 제외 (기존 값 유지) if (empty($validated['barobill_id'])) { unset($validated['barobill_id']); } if (empty($validated['barobill_pwd'])) { unset($validated['barobill_pwd']); } $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' => '회원사가 삭제되었습니다.', ]); } /** * 통계 조회 * * - 테넌트 1(본사)이면 자동으로 전체 회원사 통계 * - 다른 테넌트면 해당 테넌트의 회원사만 통계 */ public function stats(Request $request): JsonResponse|Response { $tenantId = session('selected_tenant_id'); $allTenants = $request->boolean('all_tenants', false); // 테넌트 1(본사)이면 자동으로 전체 테넌트 모드 $isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID; $query = BarobillMember::query() ->when(! $isHeadquarters && ! $allTenants && $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, ]); } // ======================================== // 바로빌 URL 조회 API (리다이렉트용) // ======================================== /** * 계좌 등록 URL 조회 * * 회원사가 계좌를 등록할 수 있는 바로빌 페이지 URL을 반환합니다. */ public function getBankAccountUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getBankAccountScrapRequestUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '계좌 등록 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'bank_account_register', ], ]); } /** * 계좌 관리 URL 조회 * * 회원사가 등록된 계좌를 관리할 수 있는 바로빌 페이지 URL을 반환합니다. */ public function getBankAccountManageUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getBankAccountManagementUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '계좌 관리 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'bank_account_manage', ], ]); } /** * 인증서 등록 URL 조회 * * 회원사가 공동인증서를 등록할 수 있는 바로빌 페이지 URL을 반환합니다. */ public function getCertificateUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getCertificateRegistUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '인증서 등록 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'certificate_register', ], ]); } /** * 현금충전 URL 조회 * * 회원사가 바로빌 포인트를 충전할 수 있는 페이지 URL을 반환합니다. */ public function getCashChargeUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getCashChargeUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '충전 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'cash_charge', ], ]); } /** * 인증서 상태 조회 * * 회원사의 공동인증서 등록 상태를 확인합니다. */ public function getCertificateStatus(int $id): JsonResponse { $member = BarobillMember::find($id); if (! $member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } // 인증서 유효성 확인 $validResult = $this->barobillService->checkCertificateValid($member->biz_no); // 인증서 만료일 조회 $expireResult = $this->barobillService->getCertificateExpireDate($member->biz_no); // 인증서 등록일 조회 $registResult = $this->barobillService->getCertificateRegistDate($member->biz_no); return response()->json([ 'success' => true, 'data' => [ 'is_valid' => $validResult['success'] && $validResult['data'] > 0, 'expire_date' => $expireResult['success'] ? $expireResult['data'] : null, 'regist_date' => $registResult['success'] ? $registResult['data'] : null, ], ]); } /** * 충전 잔액 조회 * * 회원사의 바로빌 충전 잔액을 확인합니다. */ public function getBalance(int $id): JsonResponse { $member = BarobillMember::find($id); if (! $member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } $result = $this->barobillService->getBalanceCostAmount($member->biz_no); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '잔액 조회에 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'balance' => $result['data'], ], ]); } /** * 등록 계좌 목록 조회 * * 회원사가 바로빌에 등록한 계좌 목록을 조회합니다. */ public function getBankAccounts(int $id): JsonResponse { $member = BarobillMember::find($id); if (! $member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } $result = $this->barobillService->getBankAccounts($member->biz_no); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '계좌 목록 조회에 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => $result['data'], ]); } /** * 계좌 입출금내역 URL 조회 */ public function getBankAccountLogUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getBankAccountLogUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '입출금내역 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'bank_account_log', ], ]); } // ======================================== // 카드조회 관련 API // ======================================== /** * 카드 등록 URL 조회 * * 회원사가 카드를 등록할 수 있는 바로빌 페이지 URL을 반환합니다. */ public function getCardUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getCardScrapRequestUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '카드 등록 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'card_register', ], ]); } /** * 카드 관리 URL 조회 * * 회원사가 등록된 카드를 관리할 수 있는 바로빌 페이지 URL을 반환합니다. */ public function getCardManageUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getCardManagementUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '카드 관리 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'card_manage', ], ]); } /** * 카드 사용내역 URL 조회 */ public function getCardLogUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getCardLogUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '카드 사용내역 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'card_log', ], ]); } /** * 등록 카드 목록 조회 * * 회원사가 바로빌에 등록한 카드 목록을 조회합니다. */ public function getCards(int $id): JsonResponse { $member = BarobillMember::find($id); if (! $member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } $result = $this->barobillService->getCards($member->biz_no); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '카드 목록 조회에 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => $result['data'], ]); } // ======================================== // 전자세금계산서 관련 API // ======================================== /** * 세금계산서 발행 URL 조회 */ public function getTaxInvoiceUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getTaxInvoiceIssueUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '세금계산서 발행 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'tax_invoice_issue', ], ]); } /** * 세금계산서 목록 URL 조회 */ public function getTaxInvoiceListUrl(Request $request, int $id): JsonResponse { $member = $this->validateMemberForUrlApi($id); if ($member instanceof JsonResponse) { return $member; } $result = $this->barobillService->getTaxInvoiceListUrl( $member->biz_no, $member->barobill_id, $member->barobill_pwd ); if (! $result['success']) { return response()->json([ 'success' => false, 'message' => '세금계산서 목록 URL을 가져오는데 실패했습니다.', 'error' => $result['error'] ?? null, ], 422); } return response()->json([ 'success' => true, 'data' => [ 'url' => $result['data'], 'type' => 'tax_invoice_list', ], ]); } /** * 서비스 코드 목록 조회 (카드사/은행) */ public function getServiceCodes(): JsonResponse { return response()->json([ 'success' => true, 'data' => [ 'card_companies' => BarobillService::$cardCompanyCodes, 'banks' => BarobillService::$bankCodes, ], ]); } /** * 회원사별 서버 모드 변경 * * 특정 회원사의 바로빌 서버 모드(테스트/운영)를 변경합니다. * 주의: 운영 서버로 전환 시 요금이 부과됩니다. */ public function updateServerMode(Request $request, int $id): JsonResponse { $member = BarobillMember::find($id); if (! $member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } $validated = $request->validate([ 'server_mode' => 'required|in:test,production', 'confirmed' => 'required|boolean', ], [ 'server_mode.required' => '서버 모드를 선택해주세요.', 'server_mode.in' => '서버 모드는 test 또는 production 이어야 합니다.', 'confirmed.required' => '경고 확인이 필요합니다.', ]); // 확인 체크 if (! $validated['confirmed']) { return response()->json([ 'success' => false, 'message' => '서버 변경 경고를 확인해주세요.', ], 422); } $oldMode = $member->server_mode ?? 'test'; $newMode = $validated['server_mode']; // 변경 없으면 바로 반환 if ($oldMode === $newMode) { return response()->json([ 'success' => true, 'message' => '서버 모드가 이미 '.($newMode === 'test' ? '테스트' : '운영').' 서버입니다.', 'data' => $member, ]); } $member->update(['server_mode' => $newMode]); Log::info('바로빌 회원사 서버 모드 변경', [ 'member_id' => $id, 'biz_no' => $member->biz_no, 'corp_name' => $member->corp_name, 'old_mode' => $oldMode, 'new_mode' => $newMode, 'user_id' => auth()->id(), ]); return response()->json([ 'success' => true, 'message' => ($newMode === 'test' ? '테스트' : '운영').' 서버로 변경되었습니다.', 'data' => $member->fresh(), ]); } /** * 회원사별 서버 모드 조회 */ public function getServerMode(int $id): JsonResponse { $member = BarobillMember::find($id); if (! $member) { return response()->json([ 'success' => false, 'message' => '회원사를 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'data' => [ 'server_mode' => $member->server_mode ?? 'test', 'server_mode_label' => $member->server_mode_label, ], ]); } }