feat:바로빌 테넌트(회원사) 동기화 기능 추가
- barobill_companies 테이블에서 barobill_members로 동기화 API 구현 - 바로빌본사 설정 페이지에 테넌트 목록 및 동기화 버튼 추가 - 동기화 시 신규 데이터 생성 및 기존 데이터 업데이트
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
560
resources/views/barobill/config/index.blade.php
Normal file
560
resources/views/barobill/config/index.blade.php
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user