18. 서비스 까지
This commit is contained in:
parent
3958ed8b80
commit
fa0d25c7c2
@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Product;
|
||||
use APP\StockLog\StockService;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@ -88,52 +89,58 @@ class ProductController extends Controller
|
||||
->with('success', '상품이 삭제되었습니다.');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function store(Request $request, StockService $stock)
|
||||
{
|
||||
// 1) 검증 + 중복 검사 + 사용자 정의 메시지
|
||||
// 1) 검증
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'sku' => ['required', 'string', 'max:255', 'unique:products,sku'],
|
||||
'quantity' => ['required', 'numeric', 'min:0'],
|
||||
'quantity' => ['nullable', 'numeric', 'min:0'], // 초기 재고, 없어도 됨
|
||||
'price' => ['required', 'numeric', 'min:0'],
|
||||
'image' => ['nullable', 'image', 'max:2048'],
|
||||
], [
|
||||
// name
|
||||
'name.required' => '상품명은 필수 입력 항목입니다.',
|
||||
'name.unique' => '이미 등록된 상품명입니다.',
|
||||
|
||||
// sku
|
||||
'sku.required' => 'SKU는 필수 입력 항목입니다.',
|
||||
'sku.string' => 'SKU는 문자열이어야 합니다.',
|
||||
'sku.unique' => '이미 등록된 SKU입니다.',
|
||||
|
||||
// quantity
|
||||
'quantity.required' => '수량은 필수 입력 항목입니다.',
|
||||
'name.required' => '상품명은 필수 입력 항목입니다.',
|
||||
'sku.required' => 'SKU는 필수 입력 항목입니다.',
|
||||
'sku.unique' => '이미 등록된 SKU입니다.',
|
||||
'quantity.numeric' => '수량은 숫자여야 합니다.',
|
||||
'quantity.min' => '수량은 0 이상이어야 합니다.',
|
||||
|
||||
// price
|
||||
'price.required' => '가격은 필수 입력 항목입니다.',
|
||||
'price.numeric' => '가격은 숫자여야 합니다.',
|
||||
'price.min' => '가격은 0 이상이어야 합니다.',
|
||||
|
||||
// image
|
||||
'image.image' => '업로드된 파일은 이미지여야 합니다.',
|
||||
'image.max' => '이미지 파일 크기는 2MB 이하만 가능합니다.',
|
||||
'price.required' => '가격은 필수 입력 항목입니다.',
|
||||
'price.numeric' => '가격은 숫자여야 합니다.',
|
||||
'price.min' => '가격은 0 이상이어야 합니다.',
|
||||
'image.image' => '업로드된 파일은 이미지여야 합니다.',
|
||||
'image.max' => '이미지 파일 크기는 2MB 이하만 가능합니다.',
|
||||
]);
|
||||
|
||||
// 2) 이미지 업로드 처리
|
||||
// 2) 초기 수량은 따로 빼 둔다 (입출고 처리용)
|
||||
$initialQty = isset($data['quantity']) ? (int) $data['quantity'] : 0;
|
||||
|
||||
// 3) 상품 생성용 배열 (동영상처럼 quantity는 0으로 고정)
|
||||
$productData = [
|
||||
'name' => $data['name'],
|
||||
'sku' => $data['sku'],
|
||||
'price' => $data['price'],
|
||||
'quantity' => 0, // 상품 재고는 0으로 생성
|
||||
];
|
||||
|
||||
// 이미지가 있으면 경로만 추가
|
||||
if ($request->hasFile('image')) {
|
||||
$data['image'] = $request->file('image')->store('products', 'public');
|
||||
// 저장 경로 예: storage/app/public/products/파일명.jpg
|
||||
$productData['image'] = $request
|
||||
->file('image')
|
||||
->store('products', 'public');
|
||||
}
|
||||
|
||||
// 3) DB 저장
|
||||
Product::create($data);
|
||||
// 4) 상품 등록
|
||||
$product = Product::create($productData);
|
||||
|
||||
// 4) 등록 후 리다이렉트
|
||||
// 5) 초기 재고가 있으면 세팅 + 입출고 로그 기록
|
||||
if ($initialQty > 0) {
|
||||
// StockService 안에서 product.quantity 업데이트 + stock_logs 기록
|
||||
$stock->setInitialStock($product, $initialQty);
|
||||
}
|
||||
|
||||
// 6) 리다이렉트
|
||||
return redirect()
|
||||
->route('product.input')
|
||||
->route('product') // 실제 목록 라우트명에 맞게 수정
|
||||
->with('success', '상품이 등록되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\StockLog;
|
||||
use App\Services\StockService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StockLogController extends Controller
|
||||
@ -23,7 +24,7 @@ class StockLogController extends Controller
|
||||
return view('stock.input', compact('product', 'title', 'action'));
|
||||
}
|
||||
|
||||
public function Store(Request $request, $id)
|
||||
public function store(Request $request, $id, StockService $stock)
|
||||
{
|
||||
$action = $request->query('action', 'in');
|
||||
if(!in_array($action, ['in','out']))
|
||||
@ -32,13 +33,15 @@ class StockLogController extends Controller
|
||||
}
|
||||
$product = Product::findOrFail($id);
|
||||
$validated = $request->validate([
|
||||
'amount' => ['required', 'integer', 'min:1']
|
||||
]);
|
||||
StockLog::create([
|
||||
'product_id' => $product->id,
|
||||
'change_type' => $action,
|
||||
'change_amount' => $validated['amount']
|
||||
'amount' => ['required', 'integer']
|
||||
]);
|
||||
// StockLog::create([
|
||||
// 'product_id' => $product->id,
|
||||
// 'change_type' => $action,
|
||||
// 'change_amount' => $validated['amount']
|
||||
// ]);
|
||||
$stock->adjust($product, $action, (int) $validated['amount']);
|
||||
|
||||
return redirect()->route('product')->with('success', $action == 'in' ? '입고처리 완료' : '출고처리 완료');
|
||||
}
|
||||
}
|
||||
|
||||
61
app/Services/StockService.php
Normal file
61
app/Services/StockService.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\StockLog;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class StockService
|
||||
{
|
||||
public function setInitialStock(Product $product, int $qty): void
|
||||
{
|
||||
if($qty < 0) {
|
||||
throw ValidationException::withMessages(['initial_stock' => '초기 재고는 0 이상이어야 합니다.']);
|
||||
}
|
||||
// use 명령을 통해서 클로저에서 $product와 $qty 값을 사용하겠다.
|
||||
// transaction() 스태틱 메소드 안에서 동작하는 DB 작업은 에러 발생 시 전체가 롤백된다.
|
||||
DB::transaction(function () use($product, $qty) {
|
||||
// 행 잠금으로 동시성 방지
|
||||
$p = Product::whereKey($product->getKey())->lockForUpdate()->first();
|
||||
// 총 재고 갱신
|
||||
$p->quantity = $qty;
|
||||
$p->save();
|
||||
|
||||
// 로그 기록 (초기 세팅도 입고 형태로 남김)
|
||||
if ($qty > 0) {
|
||||
StockLog::create([
|
||||
'product_id' => $p->id,
|
||||
'change_type' => 'in',
|
||||
'change_amount' => $qty
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function adjust(Product $product, string $type, int $amount): void
|
||||
{
|
||||
if(!in_array($type, ['in', 'out'])) {
|
||||
throw ValidationException::withMessages(['action' => 'action은 in/out 이어야 합니다.']);
|
||||
}
|
||||
if($amount <= 0) {
|
||||
throw ValidationException::withMessages(['amount' => '수량은 1 이상이어야 합니다.']);
|
||||
}
|
||||
DB::transaction(function() use($product, $type, $amount) {
|
||||
$p = Product::whereKey($product->getKey())->lockForUpdate()->first();
|
||||
$newQty = $type === 'in' ? $p->quantity + $amount : $p->quantity - $amount;
|
||||
if($newQty < 0) {
|
||||
throw ValidationException::withMessages(['amount' => '현재 재고보다 많은 수량의 출고는 불가능합니다.']);
|
||||
}
|
||||
$p->quantity = $newQty;
|
||||
$p->save();
|
||||
|
||||
StockLog::create([
|
||||
'product_id' => $p->id,
|
||||
'change_type' => $type,
|
||||
'change_amount' => $amount
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,10 @@
|
||||
@section('main')
|
||||
|
||||
<h4>입고 처리</h4>
|
||||
@error('amount')
|
||||
<div class="alert alert-danger">{{ $message }}</div>
|
||||
@enderror
|
||||
|
||||
<div class="alert alert-danger">에러메시지</div>
|
||||
|
||||
<form method="post" action="">
|
||||
@csrf
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user