- CooconConfig 모델 및 마이그레이션 추가 - CooconService 클래스 구현 (OA12~OA17 API) - CreditController 확장 (설정 관리, 조회 기능) - 설정 관리 화면 추가 (CRUD, 활성화 토글) - 사업자번호 조회 화면 업데이트 (API 연동) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
430 lines
23 KiB
PHP
430 lines
23 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '쿠콘 API 설정')
|
|
|
|
@section('content')
|
|
<div class="flex flex-col h-full">
|
|
<!-- 페이지 헤더 -->
|
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-800">쿠콘 API 설정</h1>
|
|
<p class="text-sm text-gray-500 mt-1">신용평가 API 연동을 위한 쿠콘 설정을 관리합니다</p>
|
|
</div>
|
|
<button type="button"
|
|
id="btn-add"
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition flex items-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-white rounded-lg shadow-sm flex-1 overflow-hidden">
|
|
@if($configs->isEmpty())
|
|
<div class="flex flex-col items-center justify-center h-full text-gray-400 p-12">
|
|
<svg class="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
<p class="text-lg font-medium">등록된 설정이 없습니다</p>
|
|
<p class="text-sm mt-1">새 설정을 추가하여 쿠콘 API를 연동하세요</p>
|
|
</div>
|
|
@else
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50 border-b">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">설정명</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">환경</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">API Key</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록일</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200">
|
|
@foreach($configs as $config)
|
|
<tr class="hover:bg-gray-50" data-id="{{ $config->id }}">
|
|
<td class="px-6 py-4">
|
|
<div>
|
|
<div class="font-medium text-gray-900">{{ $config->name }}</div>
|
|
@if($config->description)
|
|
<div class="text-sm text-gray-500">{{ Str::limit($config->description, 50) }}</div>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
{{ $config->environment === 'production' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800' }}">
|
|
{{ $config->environment_label }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<code class="text-sm text-gray-600 bg-gray-100 px-2 py-1 rounded">{{ $config->masked_api_key }}</code>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
{{ $config->is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
|
|
{{ $config->status_label }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-gray-500">
|
|
{{ $config->created_at->format('Y-m-d H:i') }}
|
|
</td>
|
|
<td class="px-6 py-4 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button type="button"
|
|
class="btn-toggle p-1.5 text-gray-500 hover:text-{{ $config->is_active ? 'gray' : 'green' }}-600 hover:bg-gray-100 rounded-lg transition"
|
|
data-id="{{ $config->id }}"
|
|
title="{{ $config->is_active ? '비활성화' : '활성화' }}">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@if($config->is_active)
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
@else
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
@endif
|
|
</svg>
|
|
</button>
|
|
<button type="button"
|
|
class="btn-edit p-1.5 text-gray-500 hover:text-blue-600 hover:bg-gray-100 rounded-lg transition"
|
|
data-id="{{ $config->id }}"
|
|
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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</button>
|
|
<button type="button"
|
|
class="btn-delete p-1.5 text-gray-500 hover:text-red-600 hover:bg-gray-100 rounded-lg transition"
|
|
data-id="{{ $config->id }}"
|
|
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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 설정 추가/수정 모달 -->
|
|
<div id="config-modal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black/50" id="modal-backdrop"></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 relative">
|
|
<div class="flex items-center justify-between p-6 border-b">
|
|
<h3 class="text-lg font-semibold text-gray-800" id="modal-title">새 설정 추가</h3>
|
|
<button type="button" id="btn-modal-close" class="text-gray-400 hover:text-gray-600 transition">
|
|
<svg class="w-6 h-6" 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>
|
|
<form id="config-form">
|
|
<input type="hidden" id="config-id" name="id">
|
|
<div class="p-6 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">설정명 <span class="text-red-500">*</span></label>
|
|
<input type="text"
|
|
id="config-name"
|
|
name="name"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="예: 테스트 서버 설정">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">환경 <span class="text-red-500">*</span></label>
|
|
<select id="config-environment"
|
|
name="environment"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<option value="test">테스트서버</option>
|
|
<option value="production">운영서버</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">API Key <span class="text-red-500">*</span></label>
|
|
<input type="text"
|
|
id="config-api-key"
|
|
name="api_key"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="쿠콘에서 발급받은 API Key">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Base URL <span class="text-red-500">*</span></label>
|
|
<input type="url"
|
|
id="config-base-url"
|
|
name="base_url"
|
|
required
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp">
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
테스트: https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp<br>
|
|
운영: https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">설명</label>
|
|
<textarea id="config-description"
|
|
name="description"
|
|
rows="3"
|
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="설정에 대한 설명 (선택사항)"></textarea>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox"
|
|
id="config-is-active"
|
|
name="is_active"
|
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
|
<label for="config-is-active" class="text-sm text-gray-700">활성화 (같은 환경에서 하나만 활성화 가능)</label>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end gap-3 p-6 border-t bg-gray-50 rounded-b-xl">
|
|
<button type="button"
|
|
id="btn-cancel"
|
|
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-100 transition">
|
|
취소
|
|
</button>
|
|
<button type="submit"
|
|
id="btn-submit"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition flex items-center gap-2">
|
|
<svg class="w-4 h-4 hidden animate-spin" id="submit-spinner" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 삭제 확인 모달 -->
|
|
<div id="delete-modal" class="fixed inset-0 z-50 hidden">
|
|
<div class="fixed inset-0 bg-black/50" id="delete-backdrop"></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-sm relative">
|
|
<div class="p-6 text-center">
|
|
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg class="w-6 h-6 text-red-600" 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>
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-2">설정 삭제</h3>
|
|
<p class="text-gray-600 mb-6">이 설정을 삭제하시겠습니까?<br>삭제된 설정은 복구할 수 없습니다.</p>
|
|
<input type="hidden" id="delete-id">
|
|
<div class="flex gap-3">
|
|
<button type="button"
|
|
id="btn-delete-cancel"
|
|
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-100 transition">
|
|
취소
|
|
</button>
|
|
<button type="button"
|
|
id="btn-delete-confirm"
|
|
class="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const modal = document.getElementById('config-modal');
|
|
const deleteModal = document.getElementById('delete-modal');
|
|
const form = document.getElementById('config-form');
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
|
|
|
// 모달 열기 (추가)
|
|
document.getElementById('btn-add').addEventListener('click', function() {
|
|
document.getElementById('modal-title').textContent = '새 설정 추가';
|
|
document.getElementById('config-id').value = '';
|
|
form.reset();
|
|
modal.classList.remove('hidden');
|
|
});
|
|
|
|
// 모달 닫기
|
|
function closeModal() {
|
|
modal.classList.add('hidden');
|
|
}
|
|
|
|
document.getElementById('btn-modal-close').addEventListener('click', closeModal);
|
|
document.getElementById('btn-cancel').addEventListener('click', closeModal);
|
|
document.getElementById('modal-backdrop').addEventListener('click', closeModal);
|
|
|
|
// 수정 버튼
|
|
document.querySelectorAll('.btn-edit').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const id = this.dataset.id;
|
|
try {
|
|
const response = await fetch(`{{ url('credit/settings') }}/${id}/edit`);
|
|
const html = await response.text();
|
|
|
|
// 파싱하여 데이터 추출 (간단한 방법)
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const data = JSON.parse(doc.getElementById('config-data')?.textContent || '{}');
|
|
|
|
document.getElementById('modal-title').textContent = '설정 수정';
|
|
document.getElementById('config-id').value = data.id || id;
|
|
document.getElementById('config-name').value = data.name || '';
|
|
document.getElementById('config-environment').value = data.environment || 'test';
|
|
document.getElementById('config-api-key').value = data.api_key || '';
|
|
document.getElementById('config-base-url').value = data.base_url || '';
|
|
document.getElementById('config-description').value = data.description || '';
|
|
document.getElementById('config-is-active').checked = data.is_active || false;
|
|
|
|
modal.classList.remove('hidden');
|
|
} catch (error) {
|
|
showToast('설정 정보를 불러오는데 실패했습니다.', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
// 삭제 버튼
|
|
document.querySelectorAll('.btn-delete').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.getElementById('delete-id').value = this.dataset.id;
|
|
deleteModal.classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
// 삭제 모달 닫기
|
|
document.getElementById('btn-delete-cancel').addEventListener('click', function() {
|
|
deleteModal.classList.add('hidden');
|
|
});
|
|
document.getElementById('delete-backdrop').addEventListener('click', function() {
|
|
deleteModal.classList.add('hidden');
|
|
});
|
|
|
|
// 삭제 확인
|
|
document.getElementById('btn-delete-confirm').addEventListener('click', async function() {
|
|
const id = document.getElementById('delete-id').value;
|
|
try {
|
|
const response = await fetch(`{{ url('credit/settings') }}/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showToast(result.message, 'success');
|
|
setTimeout(() => location.reload(), 500);
|
|
} else {
|
|
showToast(result.error || '삭제에 실패했습니다.', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('삭제 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
deleteModal.classList.add('hidden');
|
|
});
|
|
|
|
// 활성화/비활성화 토글
|
|
document.querySelectorAll('.btn-toggle').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const id = this.dataset.id;
|
|
try {
|
|
const response = await fetch(`{{ url('credit/settings') }}/${id}/toggle`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showToast(result.message, 'success');
|
|
setTimeout(() => location.reload(), 500);
|
|
} else {
|
|
showToast(result.error || '상태 변경에 실패했습니다.', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('상태 변경 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
// 폼 제출
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const id = document.getElementById('config-id').value;
|
|
const isEdit = !!id;
|
|
const spinner = document.getElementById('submit-spinner');
|
|
const submitBtn = document.getElementById('btn-submit');
|
|
|
|
spinner.classList.remove('hidden');
|
|
submitBtn.disabled = true;
|
|
|
|
const formData = {
|
|
name: document.getElementById('config-name').value,
|
|
environment: document.getElementById('config-environment').value,
|
|
api_key: document.getElementById('config-api-key').value,
|
|
base_url: document.getElementById('config-base-url').value,
|
|
description: document.getElementById('config-description').value,
|
|
is_active: document.getElementById('config-is-active').checked,
|
|
};
|
|
|
|
try {
|
|
const url = isEdit ? `{{ url('credit/settings') }}/${id}` : '{{ route('credit.settings.store') }}';
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message, 'success');
|
|
closeModal();
|
|
setTimeout(() => location.reload(), 500);
|
|
} else {
|
|
showToast(result.error || '저장에 실패했습니다.', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('저장 중 오류가 발생했습니다.', 'error');
|
|
} finally {
|
|
spinner.classList.add('hidden');
|
|
submitBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
// 환경 선택 시 기본 URL 자동 설정
|
|
document.getElementById('config-environment').addEventListener('change', function() {
|
|
const baseUrlInput = document.getElementById('config-base-url');
|
|
if (!baseUrlInput.value) {
|
|
if (this.value === 'test') {
|
|
baseUrlInput.value = 'https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp';
|
|
} else {
|
|
baseUrlInput.value = 'https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|