18. 서비스 까지

This commit is contained in:
choibk 2025-12-07 01:44:14 +09:00
parent 3958ed8b80
commit fa0d25c7c2
4 changed files with 111 additions and 38 deletions

View File

@ -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', '상품이 등록되었습니다.');
}
}

View File

@ -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' ? '입고처리 완료' : '출고처리 완료');
}
}

View 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
]);
});
}
}

View File

@ -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