Files
sam-manage/resources/views/sales/products/index.blade.php

393 lines
20 KiB
PHP

@extends('layouts.app')
@section('title', '상품관리')
@section('content')
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8" x-data="productManager()">
{{-- 헤더 --}}
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="p-3 bg-indigo-100 rounded-xl">
<svg class="w-8 h-8 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900">상품관리</h1>
<p class="text-sm text-gray-500">SAM 솔루션 상품 요금 설정</p>
</div>
</div>
<button type="button"
x-on:click="openCategoryModal()"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<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="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>
카테고리 관리
</button>
</div>
{{-- 카테고리 --}}
<div class="bg-white rounded-xl shadow-sm">
<div class="border-b border-gray-200">
<nav class="flex -mb-px px-4" aria-label="카테고리">
@foreach($categories as $category)
<button type="button"
x-on:click="selectCategory('{{ $category->code }}')"
:class="currentCategory === '{{ $category->code }}'
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="whitespace-nowrap py-4 px-6 border-b-2 font-medium text-sm transition-colors">
{{ $category->name }}
<span class="ml-2 px-2 py-0.5 text-xs rounded-full"
:class="currentCategory === '{{ $category->code }}' ? 'bg-indigo-100 text-indigo-600' : 'bg-gray-100 text-gray-500'">
{{ $category->products->count() }}
</span>
</button>
@endforeach
</nav>
</div>
{{-- 상품 목록 영역 --}}
<div class="p-6">
{{-- 헤더 --}}
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="text-lg font-semibold text-gray-900" x-text="categoryName"></span>
<span class="text-sm text-gray-500">(기본 제공: <span x-text="baseStorage"></span>)</span>
</div>
<button type="button"
x-on:click="openProductModal()"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors">
<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="M12 4v16m8-8H4" />
</svg>
상품 추가
</button>
</div>
{{-- 상품 카드 그리드 --}}
<div id="product-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@include('sales.products.partials.product-list', ['category' => $currentCategory])
</div>
</div>
</div>
{{-- 상품 추가/수정 모달 --}}
<div x-show="showProductModal"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
{{-- 배경 오버레이 (클릭해도 닫히지 않음) --}}
<div class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm"></div>
<div class="min-h-screen px-4 flex items-center justify-center relative">
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 relative">
<button type="button" x-on:click="showProductModal = false"
class="absolute top-4 right-4 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>
<h3 class="text-lg font-bold text-gray-900 mb-4" x-text="editingProduct ? '상품 수정' : '상품 추가'"></h3>
<form x-on:submit.prevent="saveProduct()">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상품 코드</label>
<input type="text" x-model="productForm.code" :disabled="editingProduct"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 disabled:bg-gray-100"
placeholder="basic, quality, ai 등" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상품명</label>
<input type="text" x-model="productForm.name"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
placeholder="기본형, 품질관리 등" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">설명</label>
<textarea x-model="productForm.description" rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
placeholder="프로그램 타입 및 포함 기능"></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">개발비 (가입비)</label>
<input type="text"
:value="formatNumber(productForm.development_fee)"
x-on:input="productForm.development_fee = parseNumber($event.target.value); $event.target.value = formatNumber(productForm.development_fee)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-right"
placeholder="0" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1"> 구독료</label>
<input type="text"
:value="formatNumber(productForm.subscription_fee)"
x-on:input="productForm.subscription_fee = parseNumber($event.target.value); $event.target.value = formatNumber(productForm.subscription_fee)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 text-right"
placeholder="0" required>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">수당율 (%)</label>
<input type="number" x-model="productForm.commission_rate"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
min="0" max="100" step="0.01">
</div>
<div class="flex items-end gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" x-model="productForm.is_required"
class="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<span class="text-sm text-gray-700">필수 상품</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" x-model="productForm.allow_flexible_pricing"
class="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<span class="text-sm text-gray-700">재량권 허용</span>
</label>
</div>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button type="button" x-on:click="showProductModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50">
취소
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700">
저장
</button>
</div>
</form>
</div>
</div>
</div>
{{-- 카테고리 관리 모달 --}}
<div x-show="showCategoryModal"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
{{-- 배경 오버레이 (클릭해도 닫히지 않음) --}}
<div class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm"></div>
<div class="min-h-screen px-4 flex items-center justify-center relative">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6 relative">
<h3 class="text-lg font-bold text-gray-900 mb-4">카테고리 관리</h3>
<div class="space-y-3 mb-4">
@foreach($categories as $category)
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<div class="font-medium text-gray-900">{{ $category->name }}</div>
<div class="text-xs text-gray-500">{{ $category->code }} / {{ $category->base_storage }}</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-400">{{ $category->products->count() }} 상품</span>
</div>
</div>
@endforeach
</div>
<div class="border-t pt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2"> 카테고리 추가</h4>
<div class="space-y-3">
<input type="text" x-model="categoryForm.code" placeholder="코드 (영문)"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg">
<input type="text" x-model="categoryForm.name" placeholder="카테고리명"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg">
<button type="button" x-on:click="saveCategory()"
class="w-full px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700">
추가
</button>
</div>
</div>
<button type="button" x-on:click="showCategoryModal = false"
class="absolute top-4 right-4 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>
</div>
</div>
@push('scripts')
<script>
function productManager() {
return {
currentCategory: '{{ $currentCategory?->code ?? '' }}',
categoryName: '{{ $currentCategory?->name ?? '' }}',
baseStorage: '{{ $currentCategory?->base_storage ?? '100GB' }}',
categories: @json($categories),
showProductModal: false,
showCategoryModal: false,
editingProduct: null,
productForm: {
code: '',
name: '',
description: '',
development_fee: 0,
subscription_fee: 0,
commission_rate: 25,
is_required: false,
allow_flexible_pricing: true,
},
categoryForm: {
code: '',
name: '',
},
selectCategory(code) {
this.currentCategory = code;
const cat = this.categories.find(c => c.code === code);
if (cat) {
this.categoryName = cat.name;
this.baseStorage = cat.base_storage;
}
htmx.ajax('GET', '{{ route("sales.products.list") }}?category=' + code, {
target: '#product-list',
swap: 'innerHTML'
});
},
openProductModal(product = null) {
this.editingProduct = product;
if (product) {
this.productForm = { ...product };
} else {
this.productForm = {
code: '',
name: '',
description: '',
development_fee: 0,
subscription_fee: 0,
commission_rate: 25,
is_required: false,
allow_flexible_pricing: true,
};
}
this.showProductModal = true;
},
async saveProduct() {
const cat = this.categories.find(c => c.code === this.currentCategory);
if (!cat) return;
const url = this.editingProduct
? '{{ url("sales/products") }}/' + this.editingProduct.id
: '{{ route("sales.products.store") }}';
const method = this.editingProduct ? 'PUT' : 'POST';
const data = {
...this.productForm,
category_id: cat.id,
};
try {
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
},
body: JSON.stringify(data),
});
const result = await response.json();
if (result.success) {
this.showProductModal = false;
this.selectCategory(this.currentCategory);
} else {
alert(result.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error(error);
alert('저장 중 오류가 발생했습니다.');
}
},
async deleteProduct(id) {
if (!confirm('이 상품을 삭제하시겠습니까?')) return;
try {
const response = await fetch('{{ url("sales/products") }}/' + id, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
},
});
const result = await response.json();
if (result.success) {
this.selectCategory(this.currentCategory);
}
} catch (error) {
console.error(error);
}
},
openCategoryModal() {
this.categoryForm = { code: '', name: '' };
this.showCategoryModal = true;
},
async saveCategory() {
if (!this.categoryForm.code || !this.categoryForm.name) {
alert('코드와 이름을 입력하세요.');
return;
}
try {
const response = await fetch('{{ route("sales.products.categories.store") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
},
body: JSON.stringify(this.categoryForm),
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert(result.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error(error);
alert('저장 중 오류가 발생했습니다.');
}
},
formatCurrency(value) {
return '₩' + Number(value).toLocaleString();
},
formatNumber(value) {
if (value === null || value === undefined || value === '') return '';
return Math.floor(Number(value)).toLocaleString('ko-KR');
},
parseNumber(value) {
if (!value) return 0;
return parseInt(String(value).replace(/[^\d]/g, ''), 10) || 0;
}
};
}
</script>
@endpush
@endsection