diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 4f693f6..c52b517 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -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', '상품이 등록되었습니다.'); } } diff --git a/app/Http/Controllers/StockLogController.php b/app/Http/Controllers/StockLogController.php index 19e253e..74e9289 100644 --- a/app/Http/Controllers/StockLogController.php +++ b/app/Http/Controllers/StockLogController.php @@ -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' ? '입고처리 완료' : '출고처리 완료'); } } diff --git a/app/Services/StockService.php b/app/Services/StockService.php new file mode 100644 index 0000000..9f49f5e --- /dev/null +++ b/app/Services/StockService.php @@ -0,0 +1,61 @@ + '초기 재고는 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 + ]); + }); + } +} diff --git a/resources/views/stock/input.blade.php b/resources/views/stock/input.blade.php index 30d7575..2fcfc76 100644 --- a/resources/views/stock/input.blade.php +++ b/resources/views/stock/input.blade.php @@ -3,8 +3,10 @@ @section('main')

입고 처리

+@error('amount') +
{{ $message }}
+@enderror -
에러메시지
@csrf