feat: H-2 재고 현황 API 구현
- StockController: 재고 조회 및 통계 API - StockService: 재고 비즈니스 로직 - Stock, StockLot 모델: 재고/로트 관리 - Swagger 문서화 - stocks, stock_lots 테이블 마이그레이션 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
137
app/Services/StockService.php
Normal file
137
app/Services/StockService.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Tenants\Stock;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class StockService extends Service
|
||||
{
|
||||
/**
|
||||
* 재고 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = Stock::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('item_code', 'like', "%{$search}%")
|
||||
->orWhere('item_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 품목유형 필터
|
||||
if (! empty($params['item_type'])) {
|
||||
$query->where('item_type', $params['item_type']);
|
||||
}
|
||||
|
||||
// 재고 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 위치 필터
|
||||
if (! empty($params['location'])) {
|
||||
$query->where('location', 'like', "%{$params['location']}%");
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'item_code';
|
||||
$sortDir = $params['sort_dir'] ?? 'asc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 통계 조회
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$totalItems = Stock::where('tenant_id', $tenantId)->count();
|
||||
|
||||
$normalCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'normal')
|
||||
->count();
|
||||
|
||||
$lowCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'low')
|
||||
->count();
|
||||
|
||||
$outCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'out')
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total_items' => $totalItems,
|
||||
'normal_count' => $normalCount,
|
||||
'low_count' => $lowCount,
|
||||
'out_count' => $outCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 상세 조회 (LOT 포함)
|
||||
*/
|
||||
public function show(int $id): Stock
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Stock::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['lots' => function ($query) {
|
||||
$query->orderBy('fifo_order');
|
||||
}])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목코드로 재고 조회
|
||||
*/
|
||||
public function findByItemCode(string $itemCode): ?Stock
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Stock::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_code', $itemCode)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목유형별 통계
|
||||
*/
|
||||
public function statsByItemType(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$stats = Stock::where('tenant_id', $tenantId)
|
||||
->selectRaw('item_type, COUNT(*) as count, SUM(stock_qty) as total_qty')
|
||||
->groupBy('item_type')
|
||||
->get()
|
||||
->keyBy('item_type');
|
||||
|
||||
$result = [];
|
||||
foreach (Stock::ITEM_TYPES as $key => $label) {
|
||||
$data = $stats->get($key);
|
||||
$result[$key] = [
|
||||
'label' => $label,
|
||||
'count' => $data?->count ?? 0,
|
||||
'total_qty' => $data?->total_qty ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user