feat:환불/해지 관리 목업 데이터를 실제 DB CRUD로 전환
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
app/Http/Controllers/Finance/RefundController.php
Normal file
176
app/Http/Controllers/Finance/RefundController.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\Refund;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RefundController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$query = Refund::forTenant($tenantId);
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('customer_name', 'like', "%{$search}%")
|
||||
->orWhere('product_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($status = $request->input('status')) {
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type = $request->input('type')) {
|
||||
if ($type !== 'all') {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
}
|
||||
|
||||
$refunds = $query->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'type' => $item->type,
|
||||
'customerName' => $item->customer_name,
|
||||
'requestDate' => $item->request_date?->format('Y-m-d'),
|
||||
'productName' => $item->product_name,
|
||||
'originalAmount' => $item->original_amount,
|
||||
'refundAmount' => $item->refund_amount,
|
||||
'reason' => $item->reason,
|
||||
'status' => $item->status,
|
||||
'processDate' => $item->process_date?->format('Y-m-d'),
|
||||
'note' => $item->note,
|
||||
];
|
||||
});
|
||||
|
||||
$all = Refund::forTenant($tenantId)->get();
|
||||
|
||||
$stats = [
|
||||
'pending' => $all->where('status', 'pending')->count(),
|
||||
'completed' => $all->where('status', 'completed')->count(),
|
||||
'rejected' => $all->where('status', 'rejected')->count(),
|
||||
'totalRefunded' => $all->whereIn('status', ['completed', 'approved'])->sum('refund_amount'),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $refunds,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'customerName' => 'required|string|max:100',
|
||||
'productName' => 'required|string|max:100',
|
||||
'originalAmount' => 'required|integer|min:0',
|
||||
'type' => 'required|in:refund,cancel',
|
||||
]);
|
||||
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
Refund::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'type' => $request->input('type', 'refund'),
|
||||
'customer_name' => $request->input('customerName'),
|
||||
'request_date' => $request->input('requestDate'),
|
||||
'product_name' => $request->input('productName'),
|
||||
'original_amount' => $request->input('originalAmount', 0),
|
||||
'refund_amount' => 0,
|
||||
'reason' => $request->input('reason'),
|
||||
'status' => 'pending',
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '환불/해지 요청이 등록되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$refund = Refund::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'customerName' => 'required|string|max:100',
|
||||
'productName' => 'required|string|max:100',
|
||||
'originalAmount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$refund->update([
|
||||
'type' => $request->input('type', $refund->type),
|
||||
'customer_name' => $request->input('customerName'),
|
||||
'request_date' => $request->input('requestDate'),
|
||||
'product_name' => $request->input('productName'),
|
||||
'original_amount' => $request->input('originalAmount'),
|
||||
'refund_amount' => $request->input('refundAmount', $refund->refund_amount),
|
||||
'reason' => $request->input('reason'),
|
||||
'status' => $request->input('status', $refund->status),
|
||||
'process_date' => $request->input('processDate'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '환불/해지 요청이 수정되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function process(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$refund = Refund::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'action' => 'required|in:approved,completed,rejected',
|
||||
]);
|
||||
|
||||
$action = $request->input('action');
|
||||
$today = now()->format('Y-m-d');
|
||||
|
||||
if ($action === 'rejected') {
|
||||
$refund->update([
|
||||
'status' => 'rejected',
|
||||
'refund_amount' => 0,
|
||||
'process_date' => $today,
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
} else {
|
||||
$refund->update([
|
||||
'status' => $action,
|
||||
'refund_amount' => $request->input('refundAmount', 0),
|
||||
'process_date' => $today,
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '처리되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$refund = Refund::forTenant($tenantId)->findOrFail($id);
|
||||
$refund->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '환불/해지 요청이 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
39
app/Models/Finance/Refund.php
Normal file
39
app/Models/Finance/Refund.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Refund extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'refunds';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'type',
|
||||
'customer_name',
|
||||
'request_date',
|
||||
'product_name',
|
||||
'original_amount',
|
||||
'refund_amount',
|
||||
'reason',
|
||||
'status',
|
||||
'process_date',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'original_amount' => 'integer',
|
||||
'refund_amount' => 'integer',
|
||||
'request_date' => 'date',
|
||||
'process_date' => 'date',
|
||||
];
|
||||
|
||||
public function scopeForTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
35
database/seeders/RefundMenuRenameSeeder.php
Normal file
35
database/seeders/RefundMenuRenameSeeder.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Commons\Menu;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class RefundMenuRenameSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$tenantId = 1;
|
||||
|
||||
$menu = Menu::where('tenant_id', $tenantId)
|
||||
->where(function ($q) {
|
||||
$q->where('name', '환불관리')
|
||||
->orWhere('name', '환불 관리');
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($menu) {
|
||||
$oldName = $menu->name;
|
||||
$menu->name = '환불/해지 관리';
|
||||
$menu->save();
|
||||
$this->command->info("메뉴 이름 변경: {$oldName} → 환불/해지 관리");
|
||||
} else {
|
||||
$this->command->warn('환불관리 메뉴를 찾을 수 없습니다.');
|
||||
Menu::where('tenant_id', $tenantId)
|
||||
->whereNull('parent_id')
|
||||
->orderBy('sort_order')
|
||||
->get(['id', 'name', 'url'])
|
||||
->each(fn ($m) => $this->command->line(" - [{$m->id}] {$m->name} ({$m->url})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<div id="refunds-root"></div>
|
||||
@endsection
|
||||
|
||||
@@ -48,13 +49,9 @@
|
||||
const PlayCircle = createIcon('play-circle');
|
||||
|
||||
function RefundsManagement() {
|
||||
const [refunds, setRefunds] = useState([
|
||||
{ id: 1, type: 'refund', customerName: '김철수', requestDate: '2026-01-18', productName: '프리미엄 구독', originalAmount: 99000, refundAmount: 49500, reason: '서비스 불만족', status: 'approved', processDate: '2026-01-19', note: '월정액 50% 환불' },
|
||||
{ id: 2, type: 'cancel', customerName: '이영희', requestDate: '2026-01-17', productName: '엔터프라이즈 플랜', originalAmount: 500000, refundAmount: 300000, reason: '사업 종료', status: 'completed', processDate: '2026-01-18', note: '잔여 기간 환불' },
|
||||
{ id: 3, type: 'refund', customerName: '박민수', requestDate: '2026-01-15', productName: '베이직 플랜', originalAmount: 29000, refundAmount: 29000, reason: '결제 오류', status: 'completed', processDate: '2026-01-16', note: '전액 환불' },
|
||||
{ id: 4, type: 'cancel', customerName: '정수연', requestDate: '2026-01-20', productName: '프로 플랜', originalAmount: 199000, refundAmount: 0, reason: '경쟁사 이전', status: 'pending', processDate: '', note: '' },
|
||||
{ id: 5, type: 'refund', customerName: '최지훈', requestDate: '2026-01-12', productName: '추가 스토리지', originalAmount: 50000, refundAmount: 0, reason: '중복 결제', status: 'rejected', processDate: '2026-01-14', note: '이미 사용한 서비스' },
|
||||
]);
|
||||
const [refunds, setRefunds] = useState([]);
|
||||
const [stats, setStats] = useState({ pending: 0, completed: 0, rejected: 0, totalRefunded: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
@@ -63,6 +60,7 @@ function RefundsManagement() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [modalMode, setModalMode] = useState('add');
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [showProcessModal, setShowProcessModal] = useState(false);
|
||||
const [processingItem, setProcessingItem] = useState(null);
|
||||
@@ -70,6 +68,8 @@ function RefundsManagement() {
|
||||
const [processRefundAmount, setProcessRefundAmount] = useState('');
|
||||
const [processNote, setProcessNote] = useState('');
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const types = ['refund', 'cancel'];
|
||||
const reasons = ['서비스 불만족', '결제 오류', '사업 종료', '경쟁사 이전', '중복 결제', '기타'];
|
||||
|
||||
@@ -95,31 +95,84 @@ function RefundsManagement() {
|
||||
};
|
||||
const parseInputCurrency = (value) => String(value).replace(/[^\d]/g, '');
|
||||
|
||||
const fetchRefunds = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/finance/refunds/list');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setRefunds(data.data);
|
||||
setStats(data.stats);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('환불/해지 조회 실패:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchRefunds(); }, []);
|
||||
|
||||
const filteredRefunds = refunds.filter(item => {
|
||||
const matchesSearch = item.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.productName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesSearch = (item.customerName || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(item.productName || '').toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = filterStatus === 'all' || item.status === filterStatus;
|
||||
const matchesType = filterType === 'all' || item.type === filterType;
|
||||
return matchesSearch && matchesStatus && matchesType;
|
||||
});
|
||||
|
||||
const pendingCount = refunds.filter(i => i.status === 'pending').length;
|
||||
const completedCount = refunds.filter(i => i.status === 'completed').length;
|
||||
const rejectedCount = refunds.filter(i => i.status === 'rejected').length;
|
||||
const totalRefunded = refunds.filter(i => i.status === 'completed' || i.status === 'approved').reduce((sum, item) => sum + item.refundAmount, 0);
|
||||
|
||||
const handleAdd = () => { setModalMode('add'); setFormData(initialFormState); setShowModal(true); };
|
||||
const handleEdit = (item) => { setModalMode('edit'); setEditingItem(item); setFormData({ ...item }); setShowModal(true); };
|
||||
const handleSave = () => {
|
||||
if (!formData.customerName || !formData.productName || !formData.originalAmount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
if (modalMode === 'add') {
|
||||
setRefunds(prev => [{ id: Date.now(), ...formData, originalAmount: parseInt(formData.originalAmount) || 0, refundAmount: 0 }, ...prev]);
|
||||
} else {
|
||||
setRefunds(prev => prev.map(item => item.id === editingItem.id ? { ...item, ...formData, originalAmount: parseInt(formData.originalAmount) || 0, refundAmount: parseInt(formData.refundAmount) || 0 } : item));
|
||||
}
|
||||
setShowModal(false); setEditingItem(null);
|
||||
const handleEdit = (item) => {
|
||||
setModalMode('edit');
|
||||
setEditingItem(item);
|
||||
const safeItem = {};
|
||||
Object.keys(initialFormState).forEach(key => { safeItem[key] = item[key] ?? ''; });
|
||||
setFormData(safeItem);
|
||||
setShowModal(true);
|
||||
};
|
||||
const handleSave = async () => {
|
||||
if (!formData.customerName || !formData.productName || !formData.originalAmount) { alert('필수 항목을 입력해주세요.'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const url = modalMode === 'add' ? '/finance/refunds/store' : `/finance/refunds/${editingItem.id}`;
|
||||
const body = { ...formData, originalAmount: parseInt(formData.originalAmount) || 0, refundAmount: parseInt(formData.refundAmount) || 0 };
|
||||
const res = await fetch(url, {
|
||||
method: modalMode === 'add' ? 'POST' : 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
const errors = data.errors ? Object.values(data.errors).flat().join('\n') : data.message;
|
||||
alert(errors || '저장에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
setShowModal(false);
|
||||
setEditingItem(null);
|
||||
fetchRefunds();
|
||||
} catch (err) {
|
||||
console.error('저장 실패:', err);
|
||||
alert('저장에 실패했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await fetch(`/finance/refunds/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': csrfToken },
|
||||
});
|
||||
if (res.ok) {
|
||||
setShowModal(false);
|
||||
fetchRefunds();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('삭제 실패:', err);
|
||||
alert('삭제에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
const handleDelete = (id) => { if (confirm('정말 삭제하시겠습니까?')) { setRefunds(prev => prev.filter(item => item.id !== id)); setShowModal(false); } };
|
||||
|
||||
const handleProcess = (item) => {
|
||||
setProcessingItem(item);
|
||||
@@ -129,20 +182,30 @@ function RefundsManagement() {
|
||||
setShowProcessModal(true);
|
||||
};
|
||||
|
||||
const executeProcess = () => {
|
||||
const executeProcess = async () => {
|
||||
const refundAmt = parseInt(parseInputCurrency(processRefundAmount)) || 0;
|
||||
setRefunds(prev => prev.map(item => {
|
||||
if (item.id === processingItem.id) {
|
||||
if (processAction === 'rejected') {
|
||||
return { ...item, status: 'rejected', refundAmount: 0, processDate: new Date().toISOString().split('T')[0], note: processNote };
|
||||
} else {
|
||||
return { ...item, status: processAction, refundAmount: refundAmt, processDate: new Date().toISOString().split('T')[0], note: processNote };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/finance/refunds/${processingItem.id}/process`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
||||
body: JSON.stringify({
|
||||
action: processAction,
|
||||
refundAmount: processAction === 'rejected' ? 0 : refundAmt,
|
||||
note: processNote,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert(data.message || '처리에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
setShowProcessModal(false);
|
||||
setProcessingItem(null);
|
||||
setShowProcessModal(false);
|
||||
setProcessingItem(null);
|
||||
fetchRefunds();
|
||||
} catch (err) {
|
||||
console.error('처리 실패:', err);
|
||||
alert('처리에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
@@ -199,19 +262,19 @@ function RefundsManagement() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-amber-200 p-6 bg-amber-50/30">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-amber-700">처리 대기</span><Clock className="w-5 h-5 text-amber-500" /></div>
|
||||
<p className="text-2xl font-bold text-amber-600">{pendingCount}건</p>
|
||||
<p className="text-2xl font-bold text-amber-600">{stats.pending}건</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-emerald-200 p-6 bg-emerald-50/30">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-emerald-700">처리 완료</span><CheckCircle className="w-5 h-5 text-emerald-500" /></div>
|
||||
<p className="text-2xl font-bold text-emerald-600">{completedCount}건</p>
|
||||
<p className="text-2xl font-bold text-emerald-600">{stats.completed}건</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-red-200 p-6 bg-red-50/30">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-red-700">거절</span><XCircle className="w-5 h-5 text-red-500" /></div>
|
||||
<p className="text-2xl font-bold text-red-600">{rejectedCount}건</p>
|
||||
<p className="text-2xl font-bold text-red-600">{stats.rejected}건</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-gray-500">환불 총액</span><RotateCcw className="w-5 h-5 text-gray-400" /></div>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatCurrency(totalRefunded)}원</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{formatCurrency(stats.totalRefunded)}원</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -254,7 +317,9 @@ function RefundsManagement() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{filteredRefunds.length === 0 ? (
|
||||
{loading ? (
|
||||
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400"><div className="flex items-center justify-center gap-2"><svg className="animate-spin h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="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>데이터를 불러오는 중...</div></td></tr>
|
||||
) : filteredRefunds.length === 0 ? (
|
||||
<tr><td colSpan="8" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : filteredRefunds.map(item => (
|
||||
<tr key={item.id} onClick={() => handleEdit(item)} className="hover:bg-gray-50 cursor-pointer">
|
||||
@@ -308,7 +373,7 @@ function RefundsManagement() {
|
||||
<div className="flex gap-3 mt-6">
|
||||
{modalMode === 'edit' && <button onClick={() => handleDelete(editingItem.id)} className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg">삭제</button>}
|
||||
<button onClick={() => setShowModal(false)} className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">취소</button>
|
||||
<button onClick={handleSave} className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">{modalMode === 'add' ? '등록' : '저장'}</button>
|
||||
<button onClick={handleSave} disabled={saving} className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed">{saving ? '저장 중...' : (modalMode === 'add' ? '등록' : '저장')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -912,6 +912,16 @@
|
||||
|
||||
return view('finance.refunds');
|
||||
})->name('refunds');
|
||||
|
||||
// 환불/해지 관리 API
|
||||
Route::prefix('refunds')->name('refunds.')->group(function () {
|
||||
Route::get('/list', [\App\Http\Controllers\Finance\RefundController::class, 'index'])->name('list');
|
||||
Route::post('/store', [\App\Http\Controllers\Finance\RefundController::class, 'store'])->name('store');
|
||||
Route::put('/{id}', [\App\Http\Controllers\Finance\RefundController::class, 'update'])->name('update');
|
||||
Route::post('/{id}/process', [\App\Http\Controllers\Finance\RefundController::class, 'process'])->name('process');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Finance\RefundController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
Route::get('/vat', function () {
|
||||
if (request()->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('finance.vat'));
|
||||
|
||||
Reference in New Issue
Block a user