feat:바로빌 테넌트(회원사) 동기화 기능 추가

- barobill_companies 테이블에서 barobill_members로 동기화 API 구현
- 바로빌본사 설정 페이지에 테넌트 목록 및 동기화 버튼 추가
- 동기화 시 신규 데이터 생성 및 기존 데이터 업데이트
This commit is contained in:
pro
2026-01-22 16:12:55 +09:00
parent 613f65928a
commit 45f73ce7c8
3 changed files with 925 additions and 0 deletions

View File

@@ -0,0 +1,317 @@
<?php
namespace App\Http\Controllers\Api\Admin\Barobill;
use App\Http\Controllers\Controller;
use App\Models\Barobill\BarobillConfig;
use App\Models\Barobill\BarobillMember;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class BarobillConfigController extends Controller
{
/**
* 설정 목록 조회
*/
public function index(Request $request): JsonResponse|Response
{
$configs = BarobillConfig::query()
->orderBy('environment')
->orderBy('is_active', 'desc')
->orderBy('created_at', 'desc')
->get();
// HTMX 요청 시 HTML 반환
if ($request->header('HX-Request')) {
return response(
view('barobill.config.partials.table', compact('configs'))->render(),
200,
['Content-Type' => 'text/html']
);
}
return response()->json([
'success' => true,
'data' => $configs,
]);
}
/**
* 설정 등록
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'environment' => 'required|in:test,production',
'cert_key' => 'required|string|max:100',
'corp_num' => 'nullable|string|max:20',
'base_url' => 'required|url|max:255',
'description' => 'nullable|string|max:500',
'is_active' => 'nullable|boolean',
], [
'name.required' => '설정 이름을 입력해주세요.',
'environment.required' => '환경을 선택해주세요.',
'cert_key.required' => '인증키(CERTKEY)를 입력해주세요.',
'base_url.required' => '서버 URL을 입력해주세요.',
'base_url.url' => '올바른 URL 형식을 입력해주세요.',
]);
DB::beginTransaction();
try {
// is_active가 true이면 같은 환경의 다른 설정들은 비활성화
if ($validated['is_active'] ?? false) {
BarobillConfig::where('environment', $validated['environment'])
->update(['is_active' => false]);
}
$config = BarobillConfig::create($validated);
DB::commit();
return response()->json([
'success' => true,
'message' => '바로빌 설정이 등록되었습니다.',
'data' => $config,
], 201);
} catch (\Exception $e) {
DB::rollBack();
Log::error('바로빌 설정 등록 실패', ['error' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => '설정 등록 중 오류가 발생했습니다.',
], 500);
}
}
/**
* 설정 상세 조회
*/
public function show(int $id): JsonResponse
{
$config = BarobillConfig::find($id);
if (!$config) {
return response()->json([
'success' => false,
'message' => '설정을 찾을 수 없습니다.',
], 404);
}
return response()->json([
'success' => true,
'data' => $config,
]);
}
/**
* 설정 수정
*/
public function update(Request $request, int $id): JsonResponse
{
$config = BarobillConfig::find($id);
if (!$config) {
return response()->json([
'success' => false,
'message' => '설정을 찾을 수 없습니다.',
], 404);
}
$validated = $request->validate([
'name' => 'required|string|max:50',
'environment' => 'required|in:test,production',
'cert_key' => 'required|string|max:100',
'corp_num' => 'nullable|string|max:20',
'base_url' => 'required|url|max:255',
'description' => 'nullable|string|max:500',
'is_active' => 'nullable|boolean',
]);
DB::beginTransaction();
try {
// is_active가 true이면 같은 환경의 다른 설정들은 비활성화
if ($validated['is_active'] ?? false) {
BarobillConfig::where('environment', $validated['environment'])
->where('id', '!=', $id)
->update(['is_active' => false]);
}
$config->update($validated);
DB::commit();
return response()->json([
'success' => true,
'message' => '바로빌 설정이 수정되었습니다.',
'data' => $config->fresh(),
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('바로빌 설정 수정 실패', ['error' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => '설정 수정 중 오류가 발생했습니다.',
], 500);
}
}
/**
* 설정 삭제
*/
public function destroy(int $id): JsonResponse
{
$config = BarobillConfig::find($id);
if (!$config) {
return response()->json([
'success' => false,
'message' => '설정을 찾을 수 없습니다.',
], 404);
}
if ($config->is_active) {
return response()->json([
'success' => false,
'message' => '활성화된 설정은 삭제할 수 없습니다. 먼저 비활성화해주세요.',
], 422);
}
$config->delete();
return response()->json([
'success' => true,
'message' => '바로빌 설정이 삭제되었습니다.',
]);
}
/**
* 설정 활성화/비활성화 토글
*/
public function toggleActive(int $id): JsonResponse
{
$config = BarobillConfig::find($id);
if (!$config) {
return response()->json([
'success' => false,
'message' => '설정을 찾을 수 없습니다.',
], 404);
}
DB::beginTransaction();
try {
if (!$config->is_active) {
// 활성화하려면 같은 환경의 다른 설정들 비활성화
BarobillConfig::where('environment', $config->environment)
->where('id', '!=', $id)
->update(['is_active' => false]);
}
$config->update(['is_active' => !$config->is_active]);
DB::commit();
return response()->json([
'success' => true,
'message' => $config->is_active ? '설정이 활성화되었습니다.' : '설정이 비활성화되었습니다.',
'data' => $config->fresh(),
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('바로빌 설정 토글 실패', ['error' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => '설정 변경 중 오류가 발생했습니다.',
], 500);
}
}
/**
* barobill_companies에서 barobill_members로 동기화
*/
public function syncCompanies(): JsonResponse
{
DB::beginTransaction();
try {
// barobill_companies 테이블에서 데이터 조회
$companies = DB::table('barobill_companies')
->where('is_active', 1)
->whereNotNull('barobill_user_id')
->get();
$synced = 0;
$skipped = 0;
$errors = [];
foreach ($companies as $company) {
// 이미 존재하는지 확인 (사업자번호 기준)
$existing = BarobillMember::where('biz_no', $company->corp_num)->first();
if ($existing) {
// 기존 데이터 업데이트 (비밀번호 제외)
$existing->update([
'corp_name' => $company->company_name,
'ceo_name' => $company->ceo_name ?? $existing->ceo_name,
'barobill_id' => $company->barobill_user_id,
]);
$skipped++;
} else {
// 새로 생성
BarobillMember::create([
'tenant_id' => 1, // 기본 테넌트
'biz_no' => $company->corp_num,
'corp_name' => $company->company_name,
'ceo_name' => $company->ceo_name ?? '',
'barobill_id' => $company->barobill_user_id,
'barobill_pwd' => '', // 비밀번호는 별도로 입력 필요
'status' => 'active',
]);
$synced++;
}
}
DB::commit();
return response()->json([
'success' => true,
'message' => "동기화 완료: 신규 {$synced}건, 업데이트 {$skipped}",
'data' => [
'synced' => $synced,
'updated' => $skipped,
'total' => $companies->count(),
],
]);
} catch (\Exception $e) {
DB::rollBack();
Log::error('바로빌 회원사 동기화 실패', ['error' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => '동기화 중 오류가 발생했습니다: ' . $e->getMessage(),
], 500);
}
}
/**
* barobill_companies 목록 조회
*/
public function getCompanies(): JsonResponse
{
$companies = DB::table('barobill_companies')
->select('id', 'company_name', 'corp_num', 'barobill_user_id', 'ceo_name', 'is_active', 'memo', 'created_at')
->orderBy('id')
->get();
return response()->json([
'success' => true,
'data' => $companies,
]);
}
}

View File

@@ -0,0 +1,560 @@
@extends('layouts.app')
@section('title', '바로빌설정')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">바로빌설정</h1>
<p class="text-sm text-gray-500 mt-1">바로빌 API 연동을 위한 인증키 서버 설정을 관리합니다</p>
</div>
<button
type="button"
onclick="ConfigModal.openCreate()"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-center w-full sm:w-auto flex items-center justify-center gap-2"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
설정 추가
</button>
</div>
<!-- 안내 카드 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-sm text-blue-800">
<p class="font-medium mb-1">바로빌 연동 안내</p>
<ul class="list-disc list-inside space-y-1 text-blue-700">
<li>테스트서버와 운영서버 각각 별도의 인증키(CERTKEY) 필요합니다.</li>
<li>인증키는 <a href="https://dev.barobill.co.kr/" target="_blank" class="underline hover:text-blue-900">바로빌 개발자센터</a>에서 발급받을 있습니다.</li>
<li>환경당 하나의 설정만 활성화할 있습니다.</li>
</ul>
</div>
</div>
</div>
<!-- 설정 카드 (환경별) -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- 테스트 서버 카드 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-100">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-yellow-50 text-yellow-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 class="font-semibold text-gray-800">테스트서버</h3>
<p class="text-xs text-gray-500">https://testws.baroservice.com</p>
</div>
</div>
<span id="test-status-badge" class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-600">
미설정
</span>
</div>
<div id="test-config-content" class="p-6">
<div class="text-center text-gray-400 py-4">
<svg class="w-12 h-12 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<p class="text-sm">로딩 ...</p>
</div>
</div>
</div>
<!-- 운영 서버 카드 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-100">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-50 text-green-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
</div>
<div>
<h3 class="font-semibold text-gray-800">운영서버</h3>
<p class="text-xs text-gray-500">https://ws.baroservice.com</p>
</div>
</div>
<span id="prod-status-badge" class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-600">
미설정
</span>
</div>
<div id="prod-config-content" class="p-6">
<div class="text-center text-gray-400 py-4">
<svg class="w-12 h-12 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<p class="text-sm">로딩 ...</p>
</div>
</div>
</div>
</div>
<!-- 테넌트 동기화 카드 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-100 mb-6">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-purple-50 text-purple-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<div>
<h3 class="font-semibold text-gray-800">테넌트(회원사) 동기화</h3>
<p class="text-xs text-gray-500">sales 시스템의 테넌트 데이터를 회원사로 동기화합니다</p>
</div>
</div>
<button
type="button"
onclick="syncCompanies()"
id="syncBtn"
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition flex items-center gap-2 text-sm"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
동기화 실행
</button>
</div>
<div class="p-6">
<div id="companies-list" class="space-y-2">
<div class="text-center text-gray-400 py-4">
<p class="text-sm">테넌트 목록을 불러오는 ...</p>
</div>
</div>
</div>
</div>
<!-- 전체 설정 목록 테이블 -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100">
<h3 class="font-semibold text-gray-800">전체 설정 목록</h3>
</div>
<div id="config-table"
hx-get="/api/admin/barobill/configs"
hx-trigger="load, configUpdated from:body"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
class="overflow-x-auto">
<div class="flex justify-center items-center p-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
</div>
</div>
<!-- 등록/수정 모달 -->
@include('barobill.config.partials.modal-form')
@endsection
@push('scripts')
<script>
// 설정 모달 관리
const ConfigModal = {
modal: null,
form: null,
isEditing: false,
currentId: null,
init() {
this.modal = document.getElementById('configModal');
this.form = document.getElementById('configForm');
this.loadConfigs();
},
async loadConfigs() {
try {
const res = await fetch('/api/admin/barobill/configs', {
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const result = await res.json();
if (result.success) {
this.renderConfigCards(result.data);
}
} catch (error) {
console.error('설정 로드 실패:', error);
}
},
renderConfigCards(configs) {
const testConfigs = configs.filter(c => c.environment === 'test');
const prodConfigs = configs.filter(c => c.environment === 'production');
const testActive = testConfigs.find(c => c.is_active);
const prodActive = prodConfigs.find(c => c.is_active);
// 테스트 서버 카드
this.renderCard('test', testActive, testConfigs.length);
// 운영 서버 카드
this.renderCard('prod', prodActive, prodConfigs.length);
},
renderCard(type, activeConfig, totalCount) {
const statusBadge = document.getElementById(`${type}-status-badge`);
const content = document.getElementById(`${type}-config-content`);
if (activeConfig) {
statusBadge.className = 'px-2 py-1 text-xs rounded-full bg-green-100 text-green-700';
statusBadge.textContent = '활성';
content.innerHTML = `
<div class="space-y-4">
<div>
<label class="text-xs text-gray-500 block mb-1">설정 이름</label>
<p class="font-medium text-gray-800">${activeConfig.name}</p>
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">인증키 (CERTKEY)</label>
<div class="flex items-center gap-2">
<code class="flex-1 text-sm bg-gray-50 px-3 py-2 rounded border font-mono text-blue-600 truncate">${activeConfig.cert_key}</code>
<button type="button" onclick="copyToClipboard('${activeConfig.cert_key}')" class="p-2 text-gray-400 hover:text-gray-600" title="복사하기">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
<div>
<label class="text-xs text-gray-500 block mb-1">서버 URL</label>
<p class="text-sm text-gray-700">${activeConfig.base_url}</p>
</div>
${activeConfig.corp_num ? `
<div>
<label class="text-xs text-gray-500 block mb-1">파트너 사업자번호</label>
<p class="text-sm text-gray-700">${activeConfig.corp_num}</p>
</div>
` : ''}
<div class="pt-4 border-t border-gray-100 flex gap-2">
<button type="button" onclick="ConfigModal.openEdit(${activeConfig.id})" class="flex-1 px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition">
수정
</button>
</div>
</div>
`;
} else {
statusBadge.className = 'px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-600';
statusBadge.textContent = totalCount > 0 ? '비활성' : '미설정';
content.innerHTML = `
<div class="text-center py-6">
<svg class="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<p class="text-sm text-gray-500 mb-4">${totalCount > 0 ? '활성화된 설정이 없습니다' : '설정이 없습니다'}</p>
<button type="button" onclick="ConfigModal.openCreate('${type === 'test' ? 'test' : 'production'}')" class="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition">
설정 추가
</button>
</div>
`;
}
},
openCreate(env = null) {
this.isEditing = false;
this.currentId = null;
this.resetForm();
document.getElementById('modalTitle').textContent = '바로빌 설정 추가';
document.getElementById('submitBtn').textContent = '등록하기';
if (env) {
this.form.environment.value = env;
this.updateUrlPlaceholder(env);
}
this.modal.classList.remove('hidden');
},
openEdit(id) {
this.isEditing = true;
this.currentId = id;
this.resetForm();
document.getElementById('modalTitle').textContent = '바로빌 설정 수정';
document.getElementById('submitBtn').textContent = '수정하기';
fetch(`/api/admin/barobill/configs/${id}`, {
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
const c = data.data;
this.form.name.value = c.name || '';
this.form.environment.value = c.environment || 'test';
this.form.cert_key.value = c.cert_key || '';
this.form.corp_num.value = c.corp_num || '';
this.form.base_url.value = c.base_url || '';
this.form.description.value = c.description || '';
this.form.is_active.checked = c.is_active || false;
}
});
this.modal.classList.remove('hidden');
},
close() {
this.modal.classList.add('hidden');
this.resetForm();
},
resetForm() {
this.form.reset();
},
updateUrlPlaceholder(env) {
const urlInput = this.form.base_url;
if (env === 'test') {
urlInput.placeholder = 'https://testws.baroservice.com';
if (!urlInput.value) {
urlInput.value = 'https://testws.baroservice.com';
}
} else {
urlInput.placeholder = 'https://ws.baroservice.com';
if (!urlInput.value) {
urlInput.value = 'https://ws.baroservice.com';
}
}
},
async submit(e) {
e.preventDefault();
const formData = new FormData(this.form);
const data = Object.fromEntries(formData.entries());
data.is_active = this.form.is_active.checked;
const url = this.isEditing
? `/api/admin/barobill/configs/${this.currentId}`
: '/api/admin/barobill/configs';
const method = this.isEditing ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
showToast(result.message, 'success');
this.close();
this.loadConfigs();
htmx.trigger(document.body, 'configUpdated');
} else {
showToast(result.message || '오류가 발생했습니다.', 'error');
}
} catch (error) {
showToast('통신 오류가 발생했습니다.', 'error');
}
},
fillTestData(env) {
if (env === 'test') {
this.form.name.value = '테스트서버 기본';
this.form.environment.value = 'test';
this.form.cert_key.value = '2DD6C76C-04DB-44F7-B6E9-3FC0B2211826';
this.form.base_url.value = 'https://testws.baroservice.com';
} else {
this.form.name.value = '운영서버 기본';
this.form.environment.value = 'production';
this.form.cert_key.value = 'C0B36577-286F-491B-9747-EE1A00DBF5F5';
this.form.base_url.value = 'https://ws.baroservice.com';
}
this.form.is_active.checked = true;
}
};
// 삭제 확인
window.confirmDeleteConfig = function(id, name) {
showDeleteConfirm(name, async () => {
try {
const res = await fetch(`/api/admin/barobill/configs/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const result = await res.json();
if (result.success) {
showToast(result.message, 'success');
ConfigModal.loadConfigs();
htmx.trigger(document.body, 'configUpdated');
} else {
showToast(result.message || '삭제 실패', 'error');
}
} catch (error) {
showToast('통신 오류가 발생했습니다.', 'error');
}
});
};
// 활성화 토글
window.toggleConfigActive = async function(id) {
try {
const res = await fetch(`/api/admin/barobill/configs/${id}/toggle-active`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const result = await res.json();
if (result.success) {
showToast(result.message, 'success');
ConfigModal.loadConfigs();
htmx.trigger(document.body, 'configUpdated');
} else {
showToast(result.message || '변경 실패', 'error');
}
} catch (error) {
showToast('통신 오류가 발생했습니다.', 'error');
}
};
// 클립보드 복사
window.copyToClipboard = function(text) {
navigator.clipboard.writeText(text).then(() => {
showToast('클립보드에 복사되었습니다.', 'success');
}).catch(() => {
showToast('복사에 실패했습니다.', 'error');
});
};
// 테넌트 목록 로드
async function loadCompanies() {
try {
const res = await fetch('/api/admin/barobill/companies', {
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const result = await res.json();
const container = document.getElementById('companies-list');
if (result.success && result.data.length > 0) {
container.innerHTML = `
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">회사명</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">사업자번호</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">바로빌 ID</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
${result.data.map(c => `
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 text-gray-500">${c.id}</td>
<td class="px-4 py-2 font-medium text-gray-900">${c.company_name}</td>
<td class="px-4 py-2 text-gray-500">${c.corp_num}</td>
<td class="px-4 py-2 text-gray-500">${c.barobill_user_id}</td>
<td class="px-4 py-2">
<span class="px-2 py-1 text-xs rounded-full ${c.is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}">
${c.is_active ? '활성' : '비활성'}
</span>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<p class="text-xs text-gray-500 mt-3">총 ${result.data.length}개의 테넌트가 등록되어 있습니다.</p>
`;
} else {
container.innerHTML = `
<div class="text-center py-6 text-gray-400">
<svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<p class="text-sm">등록된 테넌트가 없습니다.</p>
<p class="text-xs mt-1">sales 시스템에서 테넌트를 등록해주세요.</p>
</div>
`;
}
} catch (error) {
console.error('테넌트 목록 로드 실패:', error);
document.getElementById('companies-list').innerHTML = `
<div class="text-center py-4 text-red-500">
<p class="text-sm">테넌트 목록을 불러오는데 실패했습니다.</p>
</div>
`;
}
}
// 동기화 실행
async function syncCompanies() {
const btn = document.getElementById('syncBtn');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
동기화 중...
`;
try {
const res = await fetch('/api/admin/barobill/companies/sync', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const result = await res.json();
if (result.success) {
showToast(result.message, 'success');
loadCompanies();
} else {
showToast(result.message || '동기화 실패', 'error');
}
} catch (error) {
showToast('통신 오류가 발생했습니다.', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
// 초기화
document.addEventListener('DOMContentLoaded', function() {
ConfigModal.init();
loadCompanies();
// 환경 변경 시 URL 자동 설정
document.getElementById('configForm').environment.addEventListener('change', function(e) {
ConfigModal.updateUrlPlaceholder(e.target.value);
});
});
</script>
@endpush

View File

@@ -84,10 +84,28 @@
Route::patch('/{id}/status', [\App\Http\Controllers\Api\Admin\FundScheduleController::class, 'updateStatus'])->name('status');
});
// 바로빌 설정 API
Route::prefix('barobill/configs')->name('barobill.configs.')->group(function () {
Route::get('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'index'])->name('index');
Route::post('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'store'])->name('store');
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'show'])->name('show');
Route::put('/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'update'])->name('update');
Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'destroy'])->name('destroy');
Route::post('/{id}/toggle-active', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'toggleActive'])->name('toggle-active');
});
// 바로빌 테넌트(회원사) 동기화 API
Route::prefix('barobill/companies')->name('barobill.companies.')->group(function () {
Route::get('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'getCompanies'])->name('index');
Route::post('/sync', [\App\Http\Controllers\Api\Admin\Barobill\BarobillConfigController::class, 'syncCompanies'])->name('sync');
});
// 바로빌 회원사 관리 API
Route::prefix('barobill/members')->name('barobill.members.')->group(function () {
// 통계
Route::get('/stats', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'stats'])->name('stats');
// 서비스 코드 목록 (카드사/은행)
Route::get('/service-codes', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getServiceCodes'])->name('service-codes');
// 기본 CRUD
Route::get('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'index'])->name('index');
@@ -95,6 +113,36 @@
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');
// ==========================================
// 계좌 관련 URL 조회
// ==========================================
Route::post('/{id}/bank-account-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getBankAccountUrl'])->name('bank-account-url');
Route::post('/{id}/bank-account-manage-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getBankAccountManageUrl'])->name('bank-account-manage-url');
Route::post('/{id}/bank-account-log-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getBankAccountLogUrl'])->name('bank-account-log-url');
Route::get('/{id}/bank-accounts', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getBankAccounts'])->name('bank-accounts');
// ==========================================
// 카드 관련 URL 조회
// ==========================================
Route::post('/{id}/card-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCardUrl'])->name('card-url');
Route::post('/{id}/card-manage-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCardManageUrl'])->name('card-manage-url');
Route::post('/{id}/card-log-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCardLogUrl'])->name('card-log-url');
Route::get('/{id}/cards', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCards'])->name('cards');
// ==========================================
// 전자세금계산서 관련 URL 조회
// ==========================================
Route::post('/{id}/certificate-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCertificateUrl'])->name('certificate-url');
Route::post('/{id}/tax-invoice-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getTaxInvoiceUrl'])->name('tax-invoice-url');
Route::post('/{id}/tax-invoice-list-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getTaxInvoiceListUrl'])->name('tax-invoice-list-url');
// ==========================================
// 공통 조회
// ==========================================
Route::post('/{id}/cash-charge-url', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCashChargeUrl'])->name('cash-charge-url');
Route::get('/{id}/certificate-status', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getCertificateStatus'])->name('certificate-status');
Route::get('/{id}/balance', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getBalance'])->name('balance');
});
// 테넌트 관리 API