refactor:바로빌 서버 모드를 회원사별 개별 설정으로 변경

기존 전역 세션 기반 서버 모드 → 회원사별 개별 설정 방식으로 변경

주요 변경사항:
- BarobillMember 모델: server_mode 필드 및 accessor 추가
- BarobillService: switchServerMode() 메서드 추가 (동적 서버 전환)
- BarobillMemberController: 회원사별 서버 모드 변경 API 추가
- 회원사 목록 테이블: 서버 모드 컬럼 추가 (클릭 시 변경 모달)
- 서버 변경 확인 모달: 요금 부과 경고 및 동의 체크박스 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-03 07:48:13 +09:00
parent 56aa538b3b
commit 425e0e79d6
6 changed files with 349 additions and 102 deletions

View File

@@ -24,6 +24,8 @@ public function __construct(
/**
* 회원사 조회 및 비밀번호 검증 헬퍼
*
* 회원사의 서버 모드에 따라 BarobillService의 서버 모드도 자동 전환합니다.
*
* @return BarobillMember|JsonResponse 회원사 객체 또는 에러 응답
*/
private function validateMemberForUrlApi(int $id): BarobillMember|JsonResponse
@@ -45,6 +47,9 @@ private function validateMemberForUrlApi(int $id): BarobillMember|JsonResponse
], 422);
}
// 회원사의 서버 모드로 BarobillService 전환
$this->barobillService->setServerMode($member->server_mode ?? 'test');
return $member;
}
@@ -858,57 +863,89 @@ public function getServiceCodes(): JsonResponse
}
/**
* 서버 모드 설정 (테스트/운영 서버 전환)
* 회원사별 서버 모드 변경
*
* 세션에 바로빌 서버 모드를 저장합니다.
* BarobillService 생성 시 이 세션 값을 읽어 사용합니다.
* 특정 회원사의 바로빌 서버 모드(테스트/운영)를 변경합니다.
* 주의: 운영 서버로 전환 시 요금이 부과됩니다.
*/
public function setServerMode(Request $request): JsonResponse
public function updateServerMode(Request $request, int $id): JsonResponse
{
$member = BarobillMember::find($id);
if (!$member) {
return response()->json([
'success' => false,
'message' => '회원사를 찾을 수 없습니다.',
], 404);
}
$validated = $request->validate([
'mode' => 'required|in:test,production',
'server_mode' => 'required|in:test,production',
'confirmed' => 'required|boolean',
], [
'mode.required' => '서버 모드를 선택해주세요.',
'mode.in' => '서버 모드는 test 또는 production 이어야 합니다.',
'server_mode.required' => '서버 모드를 선택해주세요.',
'server_mode.in' => '서버 모드는 test 또는 production 이어야 합니다.',
'confirmed.required' => '경고 확인이 필요합니다.',
]);
$mode = $validated['mode'];
session(['barobill_server_mode' => $mode]);
// 확인 체크
if (!$validated['confirmed']) {
return response()->json([
'success' => false,
'message' => '서버 변경 경고를 확인해주세요.',
], 422);
}
Log::info('바로빌 서버 모드 변경', [
'mode' => $mode,
$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,
'mode' => $mode,
'message' => ($mode === 'test' ? '테스트' : '운영') . ' 서버로 전환되었습니다.',
'message' => ($newMode === 'test' ? '테스트' : '운영') . ' 서버로 변경되었습니다.',
'data' => $member->fresh(),
]);
}
/**
* 서버 모드 조회
*
* 현재 세션에 저장된 바로빌 서버 모드를 반환합니다.
* 세션 값이 없으면 .env 설정값을 반환합니다.
* 회원사별 서버 모드 조회
*/
public function getServerMode(): JsonResponse
public function getServerMode(int $id): JsonResponse
{
$sessionMode = session('barobill_server_mode');
$member = BarobillMember::find($id);
if ($sessionMode) {
$mode = $sessionMode;
} else {
// .env 설정 기준 (BAROBILL_TEST_MODE)
$isTestMode = config('services.barobill.test_mode', true);
$mode = $isTestMode ? 'test' : 'production';
if (!$member) {
return response()->json([
'success' => false,
'message' => '회원사를 찾을 수 없습니다.',
], 404);
}
return response()->json([
'success' => true,
'mode' => $mode,
'source' => $sessionMode ? 'session' : 'env',
'data' => [
'server_mode' => $member->server_mode ?? 'test',
'server_mode_label' => $member->server_mode_label,
],
]);
}
}

View File

@@ -27,6 +27,7 @@ class BarobillMember extends Model
'manager_email',
'manager_hp',
'status',
'server_mode',
];
protected $casts = [
@@ -85,4 +86,36 @@ public function getStatusColorAttribute(): string
default => 'bg-gray-100 text-gray-800',
};
}
/**
* 서버 모드 라벨
*/
public function getServerModeLabelAttribute(): string
{
return match ($this->server_mode) {
'test' => '테스트',
'production' => '운영',
default => '테스트',
};
}
/**
* 서버 모드별 색상 클래스
*/
public function getServerModeColorAttribute(): string
{
return match ($this->server_mode) {
'test' => 'bg-amber-100 text-amber-800',
'production' => 'bg-green-100 text-green-800',
default => 'bg-amber-100 text-amber-800',
};
}
/**
* 테스트 모드 여부
*/
public function isTestMode(): bool
{
return $this->server_mode !== 'production';
}
}

View File

@@ -96,15 +96,55 @@ class BarobillService
public function __construct()
{
// 1. 세션에서 서버 모드 가져오기 (최우선)
$sessionMode = session('barobill_server_mode');
if ($sessionMode) {
$this->isTestMode = ($sessionMode === 'test');
} else {
// 2. .env에서 테스트 모드 설정 가져오기 (기본값: true = 테스트 모드)
$this->isTestMode = config('services.barobill.test_mode', true);
// 기본값: .env 설정 사용
$this->isTestMode = config('services.barobill.test_mode', true);
$this->initializeConfig();
}
/**
* 서버 모드 전환 (회원사별 설정 적용)
*
* @param bool $isTestMode true: 테스트서버, false: 운영서버
*/
public function switchServerMode(bool $isTestMode): self
{
if ($this->isTestMode !== $isTestMode) {
$this->isTestMode = $isTestMode;
// SOAP 클라이언트 초기화 (새 서버로 재연결)
$this->corpStateClient = null;
$this->tiClient = null;
$this->bankAccountClient = null;
$this->cardClient = null;
// 설정 재로드
$this->initializeConfig();
}
return $this;
}
/**
* 서버 모드 문자열로 전환
*
* @param string $mode 'test' 또는 'production'
*/
public function setServerMode(string $mode): self
{
return $this->switchServerMode($mode === 'test');
}
/**
* 현재 서버 모드 조회
*/
public function getServerMode(): string
{
return $this->isTestMode ? 'test' : 'production';
}
/**
* 설정 초기화 (서버 모드에 따른 설정 로드)
*/
protected function initializeConfig(): void
{
// DB에서 활성화된 설정 가져오기 (우선순위)
$dbConfig = $this->loadConfigFromDatabase();

View File

@@ -10,30 +10,16 @@
<h1 class="text-2xl font-bold text-gray-800">회원사관리</h1>
<p class="text-sm text-gray-500 mt-1">바로빌 연동 회원사를 관리합니다</p>
</div>
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<!-- 서버 선택 토글 -->
<div id="serverModeToggle" class="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-lg border border-gray-200">
<span class="text-sm font-medium text-gray-600">서버:</span>
<button id="testServerBtn" onclick="BarobillServer.setMode('test')"
class="px-3 py-1 text-xs rounded-full transition font-medium">
테스트
</button>
<button id="prodServerBtn" onclick="BarobillServer.setMode('production')"
class="px-3 py-1 text-xs rounded-full transition font-medium">
운영
</button>
</div>
<button
type="button"
onclick="MemberModal.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>
<button
type="button"
onclick="MemberModal.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>
<!-- 통계 카드 -->
@@ -137,6 +123,78 @@ class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg
</div>
</div>
</div>
<!-- 서버 모드 변경 확인 모달 -->
<div id="serverModeConfirmModal" class="fixed inset-0 z-50 hidden">
<div class="fixed inset-0 bg-black/50" onclick="ServerModeManager.closeModal()"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg" onclick="event.stopPropagation()">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-800">바로빌 서버 변경</h3>
<p class="text-sm text-gray-500">
<span id="serverModeModalMemberName" class="font-medium text-gray-700"></span> 회원사
</p>
</div>
<button type="button" onclick="ServerModeManager.closeModal()" class="text-gray-400 hover:text-gray-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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-6 space-y-4">
<!-- 변경 정보 -->
<div class="flex items-center justify-center gap-4 text-center">
<div>
<p class="text-xs text-gray-500 mb-1">현재</p>
<p id="serverModeModalCurrentMode" class="font-semibold"></p>
</div>
<svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
<div>
<p class="text-xs text-gray-500 mb-1">변경</p>
<p id="serverModeModalNewMode" class="font-semibold"></p>
</div>
</div>
<!-- 경고 메시지 -->
<div id="serverModeWarning">
<!-- 동적으로 채워짐 -->
</div>
<!-- 확인 체크박스 -->
<label class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition">
<input type="checkbox"
id="serverModeConfirmCheckbox"
onchange="ServerModeManager.onConfirmCheckChange(this.checked)"
class="mt-0.5 w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">
내용을 확인하였으며, <strong>서버 변경에 따른 요금 부과</strong> 동의합니다.
</span>
</label>
</div>
<div class="px-6 py-4 border-t border-gray-100 flex gap-3">
<button
type="button"
onclick="ServerModeManager.closeModal()"
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
>
취소
</button>
<button
type="button"
id="serverModeConfirmBtn"
onclick="ServerModeManager.confirmChange()"
disabled
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
서버 변경
</button>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
@@ -635,49 +693,111 @@ function closeBarobillDropdown() {
}
});
// 바로빌 서버 모드 관리
const BarobillServer = {
currentMode: 'test',
// 회원사별 서버 모드 변경 관리
const ServerModeManager = {
pendingMemberId: null,
pendingMode: null,
async init() {
try {
const res = await fetch('/api/admin/barobill/members/server-mode', {
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const data = await res.json();
if (data.success) {
this.currentMode = data.mode;
this.updateUI();
}
} catch (error) {
console.error('서버 모드 조회 실패:', error);
// 서버 모드 변경 요청 (경고 모달 표시)
requestChange(memberId, memberName, currentMode) {
this.pendingMemberId = memberId;
const newMode = currentMode === 'test' ? 'production' : 'test';
this.pendingMode = newMode;
const modal = document.getElementById('serverModeConfirmModal');
const memberNameEl = document.getElementById('serverModeModalMemberName');
const currentModeEl = document.getElementById('serverModeModalCurrentMode');
const newModeEl = document.getElementById('serverModeModalNewMode');
const warningEl = document.getElementById('serverModeWarning');
const confirmCheckbox = document.getElementById('serverModeConfirmCheckbox');
memberNameEl.textContent = memberName;
currentModeEl.textContent = currentMode === 'test' ? '테스트 서버' : '운영 서버';
currentModeEl.className = currentMode === 'test'
? 'font-semibold text-amber-600'
: 'font-semibold text-green-600';
newModeEl.textContent = newMode === 'test' ? '테스트 서버' : '운영 서버';
newModeEl.className = newMode === 'test'
? 'font-semibold text-amber-600'
: 'font-semibold text-green-600';
// 운영 서버로 전환 시 추가 경고
if (newMode === 'production') {
warningEl.innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<p class="font-semibold">⚠️ 요금 부과 안내</p>
<ul class="mt-2 text-sm space-y-1">
<li>• 운영 서버 사용 시 <strong>실제 요금이 부과</strong>됩니다.</li>
<li>• 회원사 등록, 세금계산서 발행 등 모든 API 호출에 과금됩니다.</li>
<li>• 테스트 목적이라면 테스트 서버를 사용해 주세요.</li>
</ul>
</div>
</div>
</div>
`;
} else {
warningEl.innerHTML = `
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 text-amber-700">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" 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>
<p class="font-semibold">테스트 서버 안내</p>
<ul class="mt-2 text-sm space-y-1">
<li>• 테스트 서버는 개발/테스트 용도로만 사용됩니다.</li>
<li>• 테스트 데이터는 실제 국세청에 전송되지 않습니다.</li>
<li>• 운영 환경에서는 반드시 운영 서버로 전환해 주세요.</li>
</ul>
</div>
</div>
</div>
`;
}
confirmCheckbox.checked = false;
document.getElementById('serverModeConfirmBtn').disabled = true;
modal.classList.remove('hidden');
},
async setMode(mode) {
if (mode === this.currentMode) return;
// 확인 체크박스 상태 변경
onConfirmCheckChange(checked) {
document.getElementById('serverModeConfirmBtn').disabled = !checked;
},
// 서버 모드 변경 실행
async confirmChange() {
if (!this.pendingMemberId || !this.pendingMode) return;
const confirmBtn = document.getElementById('serverModeConfirmBtn');
const originalText = confirmBtn.textContent;
confirmBtn.disabled = true;
confirmBtn.textContent = '변경 중...';
try {
const res = await fetch('/api/admin/barobill/members/server-mode', {
const res = await fetch(`/api/admin/barobill/members/${this.pendingMemberId}/server-mode`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ mode })
body: JSON.stringify({
server_mode: this.pendingMode,
confirmed: true
})
});
const data = await res.json();
if (data.success) {
this.currentMode = mode;
this.updateUI();
showToast(data.message, 'success');
// 목록 새로고침
this.closeModal();
htmx.trigger(document.body, 'memberUpdated');
} else {
showToast(data.message || '서버 모드 변경 실패', 'error');
@@ -685,31 +805,23 @@ function closeBarobillDropdown() {
} catch (error) {
console.error('서버 모드 변경 실패:', error);
showToast('서버 모드 변경 중 오류가 발생했습니다.', 'error');
} finally {
confirmBtn.disabled = false;
confirmBtn.textContent = originalText;
}
},
updateUI() {
const testBtn = document.getElementById('testServerBtn');
const prodBtn = document.getElementById('prodServerBtn');
// 버튼 스타일 초기화
testBtn.classList.remove('bg-amber-500', 'text-white', 'bg-gray-200', 'text-gray-600');
prodBtn.classList.remove('bg-green-500', 'text-white', 'bg-gray-200', 'text-gray-600');
if (this.currentMode === 'test') {
testBtn.classList.add('bg-amber-500', 'text-white');
prodBtn.classList.add('bg-gray-200', 'text-gray-600');
} else {
testBtn.classList.add('bg-gray-200', 'text-gray-600');
prodBtn.classList.add('bg-green-500', 'text-white');
}
// 모달 닫기
closeModal() {
document.getElementById('serverModeConfirmModal').classList.add('hidden');
this.pendingMemberId = null;
this.pendingMode = null;
}
};
// 초기화
document.addEventListener('DOMContentLoaded', function() {
MemberModal.init();
BarobillServer.init();
});
</script>
@endpush

View File

@@ -46,6 +46,9 @@ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm te
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
상태
</th>
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
서버
</th>
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
바로빌 서비스
</th>
@@ -88,6 +91,25 @@ class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm te
{{ $member->status_label }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<button
type="button"
onclick="ServerModeManager.requestChange({{ $member->id }}, '{{ addslashes($member->corp_name) }}', '{{ $member->server_mode ?? 'test' }}')"
class="px-2.5 py-1 inline-flex items-center text-xs leading-5 font-semibold rounded-full cursor-pointer hover:opacity-80 transition {{ $member->server_mode_color }}"
title="클릭하여 서버 변경"
>
@if(($member->server_mode ?? 'test') === 'test')
<svg class="w-3 h-3 mr-1" 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>
@else
<svg class="w-3 h-3 mr-1" 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>
@endif
{{ $member->server_mode_label }}
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<div class="relative inline-block text-left barobill-dropdown">
<button

View File

@@ -118,9 +118,6 @@
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');
// 서버 모드 전환 (테스트/운영)
Route::get('/server-mode', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getServerMode'])->name('server-mode.get');
Route::post('/server-mode', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'setServerMode'])->name('server-mode.set');
// 기본 CRUD
Route::get('/', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'index'])->name('index');
@@ -158,6 +155,12 @@
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');
// ==========================================
// 서버 모드 관리 (회원사별)
// ==========================================
Route::get('/{id}/server-mode', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'getServerMode'])->name('server-mode.get');
Route::post('/{id}/server-mode', [\App\Http\Controllers\Api\Admin\Barobill\BarobillMemberController::class, 'updateServerMode'])->name('server-mode.update');
});
// 바로빌 사용량조회 API